Interface:
Console output:
{
"Action": [
"Across The Void",
"Alien Shooter 2 Conscription",
"Alien Shooter 2 Reloaded",
"Aquaria",
"Assassin's Creed 3",
"Assassin's Creed 3 Liberation",
"Child of Light",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Fable 3",
"Fallout New Vegas",
"Ghost 1.0",
"Jade Empire",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Star Wars KOTOR",
"Tomb Raider (2013)",
"Tomb Raider Anniversary",
"Tomb Raider Legend",
"Tomb Raider Underworld",
"Vampire The Masquerade - Bloodlines",
"XCOM"
],
"Adventure": [
"Across The Void",
"Alien Shooter 2 Conscription",
"Alien Shooter 2 Reloaded",
"Aquaria",
"Assassin's Creed 3",
"Assassin's Creed 3 Liberation",
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Back to the Future - The Game",
"Child of Light",
"Choice of Robots",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Emerald City Confidential",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Forgotton Anne",
"Ghost 1.0",
"Headliner",
"Headliner NoviNews",
"Invisible Inc",
"Jade Empire",
"Jurassic Park - The Game",
"Jydge",
"Kathy Rain",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Lucid Dream Adventures",
"Machinarium",
"Marvel's Guardians of the Galaxy",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Neon Chrome",
"Open Sorcery",
"Oxenfree",
"Perfect Match",
"Primordia",
"Queen's Wish - The Conqueror",
"Rising Angels Reborn",
"Serafina's Saga",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Shardlight",
"Shattered Pixel Dungeon",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Story Horizon Escape",
"Star Wars KOTOR",
"The Age of Decadence",
"The Book of Unwritten Tales",
"The Longest Journey",
"The Wolf Among Us",
"This War of Mine",
"Tomb Raider (2013)",
"Tomb Raider Anniversary",
"Tomb Raider Legend",
"Tomb Raider Underworld",
"Unavowed",
"Valiant Hearts The Great War",
"Vampire The Masquerade - Bloodlines",
"Whispers of a Machine",
"XCOM"
],
"Branching Story": [
"Across The Void",
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Choice of Robots",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Forgotton Anne",
"Headliner",
"Headliner NoviNews",
"Heart's Blight",
"Jade Empire",
"Jurassic Park - The Game",
"Kathy Rain",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Marvel's Guardians of the Galaxy",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Night of the Lesbian Vampires",
"Open Sorcery",
"Oxenfree",
"Perfect Match",
"Primordia",
"Queen's Wish - The Conqueror",
"Rising Angels Reborn",
"Serafina's Saga",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Shardlight",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Story Horizon Escape",
"Star Wars KOTOR",
"The Age of Decadence",
"The Longest Journey",
"The Wolf Among Us",
"This War of Mine",
"Unavowed",
"Vampire The Masquerade - Bloodlines"
],
"Choices Matter": [
"Across The Void",
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Choice of Robots",
"Codex of Victory",
"Demon's Rise War For The Deep",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Ghost 1.0",
"Headliner",
"Headliner NoviNews",
"Invisible Inc",
"Jade Empire",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Open Sorcery",
"Oxenfree",
"Perfect Match",
"Primordia",
"Queen's Wish - The Conqueror",
"Rising Angels Reborn",
"Serafina's Saga",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Shardlight",
"Shattered Pixel Dungeon",
"Sid Meier's Civilization Beyond Earth",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Story Horizon Escape",
"Star Wars KOTOR",
"The Age of Decadence",
"The Longest Journey",
"This War of Mine",
"Unavowed",
"Vampire The Masquerade - Bloodlines",
"World In Conflict",
"XCOM"
],
"Choose Your Own Adventure": [
"Across The Void",
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Choice of Robots",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Headliner",
"Headliner NoviNews",
"Jade Empire",
"Jurassic Park - The Game",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Marvel's Guardians of the Galaxy",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Open Sorcery",
"Perfect Match",
"Primordia",
"Queen's Wish - The Conqueror",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Shardlight",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Story Horizon Escape",
"Star Wars KOTOR",
"The Age of Decadence",
"The Longest Journey",
"This War of Mine",
"Unavowed",
"Vampire The Masquerade - Bloodlines"
],
"Computer RPG": [
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Choice of Robots",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Jade Empire",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Neon Chrome",
"Open Sorcery",
"Perfect Match",
"Queen's Wish - The Conqueror",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Story Horizon Escape",
"Star Wars KOTOR",
"The Age of Decadence",
"This War of Mine",
"Unavowed",
"Vampire The Masquerade - Bloodlines",
"XCOM"
],
"Dialogue Options": [
"Across The Void",
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Back to the Future - The Game",
"Choice of Robots",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Emerald City Confidential",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Forgotton Anne",
"Headliner",
"Headliner NoviNews",
"Heart's Blight",
"Jade Empire",
"Jurassic Park - The Game",
"Kathy Rain",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Marvel's Guardians of the Galaxy",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Night of the Lesbian Vampires",
"Open Sorcery",
"Oxenfree",
"Perfect Match",
"Primordia",
"Queen's Wish - The Conqueror",
"Rising Angels Reborn",
"Serafina's Saga",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Shardlight",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Story Horizon Escape",
"Star Wars KOTOR",
"The Age of Decadence",
"The Book of Unwritten Tales",
"The Grey Wolf and The Little Lamb",
"The Longest Journey",
"Unavowed",
"Vampire The Masquerade - Bloodlines",
"Whispers of a Machine",
"Yuki's 4P"
],
"Fantasy": [],
"Female-Protagonist": [
"Across The Void",
"Alien Shooter 2 Reloaded",
"Aquaria",
"Assassin's Creed 3 Liberation",
"Avadon 1 The Black Fortress",
"Avadon 2 The Corruption",
"Avadon 3 The Warborn",
"Avernum Escape From The Pit",
"Child of Light",
"Choice of Robots",
"Contract Demon",
"Dragon Age 2",
"Dragon Age Origins",
"Dragon Age Origins - Awakening",
"Dreamfall",
"Emerald City Confidential",
"Exiled Kingdoms",
"Fable 3",
"Fallout New Vegas",
"Forgotton Anne",
"Ghost 1.0",
"Headliner",
"Headliner NoviNews",
"Heart's Blight",
"Jade Empire",
"Kathy Rain",
"King's Bounty Crossworlds",
"King's Bounty Dark Side",
"Life Is Strange",
"Lucid Dream Adventures",
"Mass Effect 1",
"Mass Effect 2",
"Mass Effect 3",
"Night of the Lesbian Vampires",
"Open Sorcery",
"Oxenfree",
"Perfect Match",
"Queen's Wish - The Conqueror",
"Rising Angels Reborn",
"Serafina's Saga",
"Shadowrun 1 Returns",
"Shadowrun 2 Dragonfall",
"Shadowrun 3 Hong Kong",
"Shardlight",
"Shattered Pixel Dungeon",
"Sorcery 1",
"Sorcery 2",
"Sorcery 3",
"Sorcery 4",
"Star Wars KOTOR",
"The Age of Decadence",
"The Grey Wolf and The Little Lamb",
"The Longest Journey",
"Tomb Raider (2013)",
"Tomb Raider Anniversary",
"Tomb Raider Legend",
"Tomb Raider Underworld",
"Unavowed",
"Vampire The Masquerade - Bloodlines",
"Whispers of a Machine",
"Yuki's 4P"
],
"Graphical Adventure": [],
"Interactive Fiction": [],
"Lesbian": [],
"Moral Dilemma": [],
"Multiple-Endings": [],
"Real-Time Strategy": [],
"Real-Time Tactics": [],
"Role-Playing Games": [],
"Romance": [],
"Sci-Fi": [],
"Side-Scroller": [],
"Singleplayer": [],
"Story Rich": [],
"Tactical RPG": [],
"Third-Person": [],
"Third-Person Shooter": [],
"Top-Down Shooter": [],
"Turn-Based Strategy": [],
"Turn-Based Tactics": [],
"Visual Novel": [],
"Western RPG": [],
"Yuri": []
}
Basically a glorified json
editor, but it has a GUI.
When you open the program, the left part and right part are both initially empty.
When you click Import Items, a QInputDialog
pops up to get the absolute path of the file you want to open, the file should be a .txt file containing a newline separated list with each line an item in the list, but the program only validates if there is a file located at the inputted path and loads whatever contained inside the file as text, then puts each line as an item in the left side.
When you click Import Tags, it gets your input, and the file format is expected to be the same as above. Now for each line in the text, the program creates a QGroupBox
and puts it to the right side. Each box is composed of a QLabel
and a QListWidget
. The text of the line is inside the QLabel
, and the QListWidget
is initially empty.
You can drag and drop items in the lists, and drag (multiple) items to the right side. The items cannot be duplicated, if the source of the drag action is the same as the destination of the drop action, the action is treated as internal move so you can sort the items by drag and drop.
All lists are in PyQt6.QtWidgets.QAbstractItemView.SelectionMode.SingleSelection
mode. In the left side, the items can be checked, and you can toggle the checkstates of the items by mouse clicks (you don't need to click the checkbox).
When you drag an item from the left side to the right side, the program copies the item being dragged plus all checked items, into the QListWidget
you dropped the items to, and updates the underlying data structures accordingly, if the items are not already inside the destination.
You can't drag and drop items from right side to the left side, nor between the lists in the right side.
If you double click an item in the lists, that item will be deleted, and the data will be removed from the corresponding underlying data structure. If you right click on a QGroupBox
on the right side, that box will be deleted and the corresponding tag will be deleted.
When you import items, the left side will be cleared, then updated with the new data, the lists in the right side will all be cleared, and the data structures will also be updated.
When you import tags, the right side will be cleared then updated with new data, and the underlying data structures will also be updated.
If you try to add items or tags, the thing will only be added if it does not already exist in the data structures, and the check is case-insensitive.
When you press Sort, the program refreshes the left-side and right-side with sorted data, and sorts the underlying data structures.
If you press Print, the program outputs one of the underlying data structure to the console in the format shown above, if you press Export, the program will ask for a filepath, validate the filepath then redirect the output to that path if it is valid.
Code
import ctypes
import json
import re
import sys
from collections import Counter
from pathlib import Path
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from PyQt6.sip import delete
DIRECTORY = str(Path(__file__).absolute().parent).replace('\\', '/')
ALIGNMENT = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
FLAG = Qt.ItemFlag
MAINSTYLE = """
QListWidget {{
background: #161940;
alternate-background-color: #1c1c5c;
show-decoration-selected: 1;
}}
QListWidget::indicator {{
border: 0px solid black;
width: 16px;
height: 16px;
}}
QListWidget::indicator:unchecked {{
image: url({0}/icons/checkbox-unchecked.png);
}}
QListWidget::indicator:checked {{
image: url({0}/icons/checkbox-checked.png);
}}
QListWidget::indicator:hover {{
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
}}
QListWidget::item {{
border: 2px groove #204080;
height: 20px;
color: #89cff0;
}}
QListWidget::item:hover {{
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #244872, stop: 1 #00c0ff);
color: #b2ffff;
border: 2px groove #bfcde4;
}}
QListWidget::item:selected {{
border: 3px groove #4080ff;
border-radius: 6px;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #0080c0, stop: 1 #4080c0);
color: #00ffff;
}}
QScrollBar::vertical {{
border: 2px solid #204060;
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #004060, stop: 1 #00b2e4);
position: absolute;
width: 16px;
}}
QScrollBar::handle:vertical {{
min-height: 32px;
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #00c0ff, stop: 1 #102372);
margin: 18px;
}}
QScrollBar::horizontal {{
border: 2px solid #204060;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #004060, stop: 1 #00b2e4);
position: absolute;
height: 16px;
}}
QScrollBar::handle:horizontal {{
min-width: 32px;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #00c0ff, stop: 1 #102372);
margin: 18px;
}}
QGroupBox {{
border: 3px groove #204080;
border-radius: 6px;
background: #140052;
color: #b2ffff;
}}
QLabel {{
color: #15f4ee;
font: 10pt 'Noto Serif';
}}
QMainWindow {{
background: #00122b;
}}
QInputDialog {{
background: #00122b;
}}
QPushButton {{
border: 3px outset #4080c0;
border-radius: 4px;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #00aae4, stop: 1 #4080c0);
color: blue;
}}
QPushButton:hover {{
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
}}
QPushButton:pressed {{
border: 3px inset #4080c0;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #005572, stop: 1 #4080c0);
color: #00ffff;
}}
QLineEdit {{
border: 2px solid #306090;
border-radius: 6px;
background: #1d2252;
color: #b2ffff;
}}
""".format(DIRECTORY)
class Font(QFont):
def __init__(self, size):
super().__init__()
self.setFamily("Noto Serif")
self.setStyleHint(QFont.StyleHint.Times)
self.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
self.setPointSize(size)
self.setHintingPreference(QFont.HintingPreference.PreferFullHinting)
class Button(QPushButton):
def __init__(self, text):
super().__init__()
font = Font(8)
self.setFont(font)
self.setFixedSize(75, 20)
self.setText(text)
class Label(QLabel):
def __init__(self, text):
super().__init__()
font = Font(10)
font.setBold(True)
self.setFont(font)
self.fontRuler = QFontMetrics(font)
self.setText(text)
self.autoResize()
def autoResize(self):
self.Height = self.fontRuler.size(0, self.text()).height()
self.Width = self.fontRuler.size(0, self.text()).width()
self.setFixedSize(self.Width, self.Height)
class LeftPane(QListWidget):
def __init__(self):
super(LeftPane, self).__init__()
font = Font(9)
self.setFont(font)
self.selected = None
self.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.setAlternatingRowColors(True)
self.setAcceptDrops(True)
self.itemClicked.connect(self.onItemClicked)
def startDrag(self, actions):
selected = self.selectedItems()[0].text()
checked = []
for i in range(self.count()):
if self.item(i).checkState() == Qt.CheckState.Checked:
checked.append(self.item(i).text())
if selected not in checked:
checked.append(selected)
self.selected = checked
super(LeftPane, self).startDrag(actions)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.accept()
else:
super(LeftPane, self).dragEnterEvent(event)
def dragMoveEvent(self, event):
if event.mimeData().hasUrls():
event.setDropAction(Qt.DropAction.CopyAction)
event.accept()
else:
super(LeftPane, self).dragMoveEvent(event)
def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction
def dropEvent(self, event):
if event.source() is self:
event.setDropAction(Qt.DropAction.MoveAction)
super().dropEvent(event)
else:
return
def onItemClicked(self, item):
if item.checkState() == Qt.CheckState.Unchecked:
item.setCheckState(Qt.CheckState.Checked)
else:
item.setCheckState(Qt.CheckState.Unchecked)
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
row = self.currentRow()
item = self.item(row)
self.takeItem(row)
text = item.text()
Manager.items.remove(text)
Manager.lower_items.remove(text.lower())
indexer = list(Manager.tagdict)
for tag, values in Manager.tagdict.items():
if text in values:
index = indexer.index(tag)
index1 = values.index(text)
values.remove(text)
taglist = window.scrollArea.Layout.itemAt(index).widget().taglist
taglist.takeItem(index1)
class TagListWidget(QListWidget):
def __init__(self, tag):
super(TagListWidget, self).__init__()
self.setContentsMargins(0, 0, 0, 0)
font = Font(9)
self.setFont(font)
self.accepted = set()
self.tag = tag
self.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.setAlternatingRowColors(True)
self.setAcceptDrops(True)
self.setFixedHeight(48)
def dropEvent(self, event):
if event.source() is self:
event.setDropAction(Qt.DropAction.MoveAction)
super().dropEvent(event)
elif event.source().__class__.__name__ == 'LeftPane':
selected = event.source().selected
for item in selected:
if item not in self.accepted:
self.accepted.add(item)
self.addItem(QListWidgetItem(item))
Manager.tagdict[self.tag].append(item)
self.autoResize()
def autoResize(self):
count = self.count()
if not count:
count = 1
if count > 5:
count = 5
self.setFixedHeight(24*(count+1))
def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
if e.button() == Qt.MouseButton.LeftButton:
row = self.currentRow()
item = self.item(row)
if item:
self.takeItem(row)
Manager.tagdict[self.tag].remove(item.text())
self.autoResize()
class TagGroupBox(QGroupBox):
def __init__(self, tag):
super().__init__()
self.setContentsMargins(3, 3, 3, 3)
self.vbox = QVBoxLayout()
self.vbox.setContentsMargins(3, 3, 3, 3)
self.vbox.setAlignment(ALIGNMENT)
self.label = Label(tag)
self.vbox.addWidget(self.label)
self.taglist = TagListWidget(tag)
self.tag = tag
self.vbox.addWidget(self.taglist)
self.setLayout(self.vbox)
def mousePressEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MouseButton.RightButton:
if self.tag in Manager.tagdict:
Manager.tagdict.pop(self.tag)
Manager.tags.remove(self.tag)
Manager.lower_tags.remove(self.tag.lower())
self.setParent(None)
self.deleteLater()
else:
return super().mousePressEvent(event)
class ScrollArea(QScrollArea):
def __init__(self, parent):
super().__init__()
self.setParent(parent)
self.setWidgetResizable(True)
self.Widget = QWidget()
palette = QPalette()
brush = QBrush(QColor(0, 12, 61, 255))
brush.setStyle(Qt.BrushStyle.SolidPattern)
palette.setBrush(QPalette.ColorGroup.Active,
QPalette.ColorRole.Window, brush)
palette.setBrush(QPalette.ColorGroup.Inactive,
QPalette.ColorRole.Window, brush)
self.Widget.setPalette(palette)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.Layout = QVBoxLayout(self.Widget)
self.Layout.setAlignment(ALIGNMENT)
self.Layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
self.setWidget(self.Widget)
self.setAlignment(ALIGNMENT)
self.setFrameShape(QFrame.Shape.Box)
self.setFrameShadow(QFrame.Shadow.Raised)
class Window(QMainWindow):
def __init__(self):
super().__init__()
frame = self.frameGeometry()
center = self.screen().availableGeometry().center()
frame.moveCenter(center)
self.move(frame.topLeft())
font = Font(10)
self.setFont(font)
self.setWindowTitle('Careyina')
self.setWindowIcon(ICON)
self.setStyleSheet(MAINSTYLE)
self.centralwidget = QWidget(self)
self.vbox = QVBoxLayout(self.centralwidget)
grid = QGridLayout()
self.leftpane = LeftPane()
grid.addWidget(self.leftpane, 0, 0, 100, 75)
self.scrollArea = ScrollArea(self.centralwidget)
grid.addWidget(self.scrollArea, 0, 75, 100, 25)
self.vbox.addLayout(grid)
buttonbar = QHBoxLayout()
self.import_items = Button('Import Items')
self.import_tags = Button('Import Tags')
self.add_item = Button('Add Item')
self.add_tag = Button('Add Tag')
self.sort_btn = Button('Sort')
self.print_btn = Button('Print')
self.export_btn = Button('Export')
for btn in [
self.import_items, self.import_tags,
self.add_item, self.add_tag,
self.sort_btn, self.print_btn
]:
buttonbar.addWidget(btn)
buttonbar.addStretch()
buttonbar.addWidget(self.export_btn)
self.vbox.addLayout(buttonbar)
self.setCentralWidget(self.centralwidget)
if __name__ == '__main__':
app = QApplication([])
ICON = QIcon(DIRECTORY + '/icons/icon.png')
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("Careyina")
app.setStyle("Fusion")
window = Window()
class Manager:
items = []
lower_items = []
tags = []
lower_tags = []
tagdict = dict()
@staticmethod
def additem():
text, ok = QInputDialog.getText(
window, "Add Item", "Item:",
QLineEdit.EchoMode.Normal, ""
)
if ok and text:
if text.lower() not in Manager.lower_items:
item = QListWidgetItem(text)
item.setFlags(item.flags() | FLAG.ItemIsUserCheckable | FLAG.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Unchecked)
window.leftpane.addItem(item)
Manager.items.append(text)
Manager.lower_items.append(text.lower())
@staticmethod
def addtag():
text, ok = QInputDialog.getText(
window, "Add Tag", "Tag:",
QLineEdit.EchoMode.Normal, ""
)
if ok and text:
if text.lower() not in Manager.lower_tags:
window.scrollArea.Layout.addWidget(TagGroupBox(text))
Manager.tags.append(text)
Manager.lower_tags.append(text.lower())
Manager.tagdict[text] = []
@staticmethod
def refresh_left():
window.leftpane.clear()
for item in Manager.items:
item = QListWidgetItem(item)
item.setFlags(item.flags() | FLAG.ItemIsUserCheckable | FLAG.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Unchecked)
window.leftpane.addItem(item)
@staticmethod
def refresh_right():
while window.scrollArea.Layout.count():
item = window.scrollArea.Layout.takeAt(0)
delete(item.widget())
delete(item)
Manager.tagdict = {tag: sorted(items) for tag, items in sorted(Manager.tagdict.items())}
for tag, items in Manager.tagdict.items():
tagbox = TagGroupBox(tag)
window.scrollArea.Layout.addWidget(tagbox)
for item in items:
tagbox.taglist.addItem(QListWidgetItem(item))
tagbox.taglist.autoResize()
tagbox.taglist.accepted = set(items)
@staticmethod
def sortall():
if not (Manager.items and Manager.tags and Manager.tagdict):
return
Manager.items = sorted(Manager.items)
Manager.tags = sorted(Manager.tags)
Manager.refresh_left()
Manager.refresh_right()
@staticmethod
def import_items():
text, ok = QInputDialog.getText(
window, "Import Items", "Filepath:",
QLineEdit.EchoMode.Normal, ""
)
if ok and text:
if Path(text).is_file():
Manager.items = sorted(Path(text).read_text(encoding='utf8').splitlines())
Manager.lower_items = [i.lower() for i in Manager.items]
Manager.refresh_left()
@staticmethod
def import_tags():
text, ok = QInputDialog.getText(
window, "Import Tags", "Filepath:",
QLineEdit.EchoMode.Normal, ""
)
if ok and text:
if Path(text).is_file():
Manager.tags = sorted(Path(text).read_text(encoding='utf8').splitlines())
Manager.lower_tags = [i.lower() for i in Manager.tags]
Manager.tagdict = {tag: [] for tag in Manager.tags}
Manager.refresh_right()
@staticmethod
def serialize():
return json.dumps(Manager.tagdict, ensure_ascii=False, indent=4)
@staticmethod
def print2console():
print(Manager.serialize())
@staticmethod
def export():
text, ok = QInputDialog.getText(
window, "Export to", "Filepath:",
QLineEdit.EchoMode.Normal, ""
)
if ok and text:
try:
Path(text).resolve()
Path(text).write_text(Manager.serialize(), encoding='utf8')
except OSError:
return
window.add_item.clicked.connect(Manager.additem)
window.add_tag.clicked.connect(Manager.addtag)
window.export_btn.clicked.connect(Manager.export)
window.import_items.clicked.connect(Manager.import_items)
window.import_tags.clicked.connect(Manager.import_tags)
window.print_btn.clicked.connect(Manager.print2console)
window.sort_btn.clicked.connect(Manager.sortall)
window.show()
sys.exit(app.exec())
Icons:
checkbox-checked.png
checkbox-unchecked.png
icon.png
So how is the code? What mistakes did I make? How should I refactor it? What improvements can be made?