├── icon.ico
├── openfile.bat
├── README.md
├── windowstuff.py
├── database.xml
├── globalstuff.py
├── titles.py
├── .gitignore
├── common.py
├── options.py
├── widgets.py
├── database.py
├── codeeditor.py
├── exporting.py
├── codelist.py
├── main.py
└── importing.py
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CLF78/CodeManager2/HEAD/icon.ico
--------------------------------------------------------------------------------
/openfile.bat:
--------------------------------------------------------------------------------
1 | C:\Users\CLF78\Desktop\CodeManagerProj\main.py "%1"
2 | pause
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Code Manager Reborn
2 | A revamped version of the Ocarina Code Manager
3 |
4 | # Source
5 | To run from source, these modules are required:
6 | * chardet
7 | * lxml
8 | * PyQt5
9 |
10 | # Special Thanks
11 | * Seeky, tZ and Brawlboxgaming for bearing with me through the entirety of development
12 | * Cryoma for the icon
13 |
--------------------------------------------------------------------------------
/windowstuff.py:
--------------------------------------------------------------------------------
1 | import globalstuff
2 | from PyQt5.QtCore import QPoint, QRect
3 |
4 |
5 | def Half(isright=False):
6 | win = globalstuff.mainWindow.mdi.currentSubWindow()
7 | if win:
8 | pos = QPoint(globalstuff.mainWindow.mdi.width() // 2 if isright else 0, 0)
9 | rect = QRect(0, 0, globalstuff.mainWindow.mdi.width() // 2, globalstuff.mainWindow.mdi.height())
10 | win.setGeometry(rect)
11 | win.move(pos)
12 |
13 |
14 | def TileHorizontal():
15 | pos = QPoint(0, 0)
16 | for window in globalstuff.mainWindow.mdi.subWindowList():
17 | rect = QRect(0, 0, globalstuff.mainWindow.mdi.width() // len(globalstuff.mainWindow.mdi.subWindowList()), globalstuff.mainWindow.mdi.height())
18 | window.setGeometry(rect)
19 | window.move(pos)
20 | pos.setX(pos.x() + window.width())
21 |
22 |
23 | def TileVertical():
24 | pos = QPoint(0, 0)
25 | for window in globalstuff.mainWindow.mdi.subWindowList():
26 | rect = QRect(0, 0, globalstuff.mainWindow.mdi.width(), globalstuff.mainWindow.mdi.height() // len(globalstuff.mainWindow.mdi.subWindowList()))
27 | window.setGeometry(rect)
28 | window.move(pos)
29 | pos.setY(pos.y() + window.height())
30 |
31 |
32 | def MinimizeAll():
33 | for window in globalstuff.mainWindow.mdi.subWindowList():
34 | window.showMinimized()
35 |
36 |
37 | def CloseAll():
38 | for window in globalstuff.mainWindow.mdi.subWindowList():
39 | window.close()
40 |
--------------------------------------------------------------------------------
/database.xml:
--------------------------------------------------------------------------------
1 |
2 | RMCP01
3 | https://raw.githubusercontent.com/H1dd3nM1nd/CodeManager2/master/database.xml
4 |
5 |
6 |
7 | 06001600 000000B8
8 | 38E00008 4800002C
9 | 38E0000B 48000024
10 | 38E0000C 4800001C
11 | 38E0000D 48000014
12 | 38E00004 4800000C
13 | 38E0000F 48000004
14 | 9421FFF0 7C0802A6
15 | 90010014 80C30004
16 | 3C80809C 80843618
17 | 80840014 38600000
18 | 80A40000 7C053000
19 | 41820018 38840248
20 | 38630001 2C03000C
21 | 41820038 4BFFFFE4
22 | 7C832378 80830090
23 | 38840001 90830090
24 | 3C80809C 608436B8
25 | 1CE7001C 7C84382E
26 | 2C040000 4182000C
27 | 7C8903A6 4E800421
28 | 3860FFFF 80010014
29 | 7C0803A6 38210010
30 | 4E800020 00000000
31 | 047A66C4 60000000
32 | 04796D30 38600000
33 | 04790EF0 39800001
34 | 04790EF4 39600001
35 | 04790EF8 39400001
36 | 04790EFC 39200001
37 | 048B54B8 80001600
38 | 048B54D0 80001608
39 | 048B54E8 80001610
40 | 048B54F4 80001618
41 | 048B5500 80001620
42 | 048B550C 80001628
43 |
44 |
45 |
46 |
47 |
48 | C2790E94 00000002
49 | 3A600010 92640004
50 | 60000000 00000000
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/globalstuff.py:
--------------------------------------------------------------------------------
1 | """
2 | Fuck globals, all my homies hate globals
3 | """
4 | import os
5 | import sys
6 |
7 | from PyQt5.QtGui import QPalette, QColor
8 | from PyQt5.Qt import Qt
9 |
10 | # Main window
11 | app = None
12 | mainWindow = None
13 |
14 | # Wii Title Database
15 | wiitdb = os.path.join(os.path.dirname(sys.argv[0]), 'wiitdb.txt')
16 |
17 | # GCT specific data
18 | gctmagic = b'\0\xd0\xc0\xde' * 2
19 | gctend = b'\xf0' + b'\0' * 7
20 |
21 | # Program settings
22 | nowarn = False
23 | theme = 'default'
24 |
25 | # Palettes
26 | # This palette is a workaround so that QMdiSubWindow titles don't look like crap
27 | textpal = QPalette()
28 | textpal.setColor(QPalette.Text, Qt.white)
29 |
30 | # The actual dark mode palette
31 | darkpal = QPalette()
32 | darkpal.setColor(QPalette.Window, QColor(53, 53, 53))
33 | darkpal.setColor(QPalette.WindowText, Qt.white)
34 | darkpal.setColor(QPalette.Base, QColor(25, 25, 25))
35 | darkpal.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
36 | darkpal.setColor(QPalette.ToolTipBase, Qt.white)
37 | darkpal.setColor(QPalette.ToolTipText, Qt.white)
38 | darkpal.setColor(QPalette.Button, QColor(53, 53, 53))
39 | darkpal.setColor(QPalette.ButtonText, Qt.white)
40 | darkpal.setColor(QPalette.BrightText, Qt.red)
41 | darkpal.setColor(QPalette.Link, QColor(42, 130, 218))
42 | darkpal.setColor(QPalette.Highlight, QColor(42, 130, 218))
43 | darkpal.setColor(QPalette.Disabled, QPalette.ButtonText, Qt.darkGray)
44 |
45 | # Empty icon for sub windows
46 | empty = None
47 |
48 | # Stylesheet for TreeWidgets because Qt sucks
49 | treeqss = """
50 | QTreeView::indicator:unchecked { border: 1px solid #C0C0C0 }
51 | QTreeView::item { color: #FFFFFF }
52 | """
53 |
54 | # Stylesheet for Options Menu, also because Qt sucks
55 | checkqss = """
56 | QCheckBox::indicator:unchecked { border: 1px solid #C0C0C0 }
57 | QComboBox::item { color: #FFFFFF }
58 | """
59 |
60 | # Program icon
61 | progico = None
62 |
--------------------------------------------------------------------------------
/titles.py:
--------------------------------------------------------------------------------
1 | import os
2 | import urllib.request
3 |
4 | import globalstuff
5 | from PyQt5 import QtWidgets
6 |
7 |
8 | def DownloadError():
9 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Title Database Missing',
10 | 'The Title Database (wiitdb.txt) is missing. Do you want to download it?')
11 | if msgbox == QtWidgets.QMessageBox.Yes:
12 | return DownloadTitles()
13 | return False
14 |
15 |
16 | def DownloadTitles():
17 | try:
18 | with urllib.request.urlopen('https://www.gametdb.com/wiitdb.txt?LANG=EN') as src, open(globalstuff.wiitdb, 'wb') as dst:
19 | dst.write(src.read())
20 | return True
21 | except:
22 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Download Error',
23 | 'There was an error during the database download. Retry?')
24 | if msgbox == QtWidgets.QMessageBox.Yes:
25 | DownloadTitles()
26 | else:
27 | return False
28 |
29 |
30 | def TitleLookup(gid: str):
31 | """
32 | Looks up the game name for the given game id in the title database txt
33 | """
34 | # First, check the file is still here
35 | if os.path.exists(globalstuff.wiitdb):
36 | with open(globalstuff.wiitdb, 'rb') as f:
37 | next(f) # Skip first line
38 | while True:
39 | try: # Read the line, split it and check the game id. If it matches, return the game name
40 | line = next(f).decode('utf-8', 'ignore').split(' = ')
41 | if line[0].lower() == gid.lower():
42 | return line[1].rstrip('\r\n')
43 | except StopIteration: # We've reached EOF
44 | return 'Unknown Game'
45 | else:
46 | # Ask the user if they want to download the database
47 | retry = DownloadError()
48 | if retry:
49 | TitleLookup(gid) # Try again
50 | else:
51 | return 'Unknown Game' # Gave up, RIP
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 |
132 | # My stuff
133 | old/
134 | .idea/
135 | config.ini
136 | wiitdb.txt
137 |
--------------------------------------------------------------------------------
/common.py:
--------------------------------------------------------------------------------
1 | """
2 | This file contains functions that are used by multiple windows to prevent duplication.
3 | """
4 | from PyQt5.Qt import Qt
5 | from PyQt5 import QtWidgets
6 |
7 | import globalstuff
8 |
9 |
10 | def GameIDMismatch():
11 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Game ID Mismatch',
12 | "The Game ID in this codelist doesn't match this file's."
13 | "Do you want to continue?")
14 | return msgbox
15 |
16 |
17 | def CheckChildren(item: QtWidgets.QTreeWidgetItem):
18 | """
19 | Recursively enables the check on an item's children
20 | """
21 | for i in range(item.childCount()):
22 | child = item.child(i)
23 | if child.childCount():
24 | CheckChildren(child)
25 | else:
26 | child.setCheckState(0, Qt.Checked)
27 |
28 |
29 | def CountCheckedCodes(source: QtWidgets.QTreeWidget, userecursive: bool):
30 | """
31 | Returns a list of the codes currently enabled, based on certain criteria. Matchflag returns 64 if userecursive is
32 | False, 1 if True.
33 | """
34 | return filter(lambda x: bool(x.checkState(0)), source.findItems('', Qt.MatchContains | Qt.MatchFlag(64 >> 6 * int(not userecursive))))
35 |
36 |
37 | def SelectItems(source: QtWidgets.QTreeWidget):
38 | """
39 | Marks items as checked if they are selected, otherwise unchecks them
40 | """
41 | bucketlist = source.findItems('', Qt.MatchContains | Qt.MatchRecursive)
42 | for item in bucketlist:
43 | if item in source.selectedItems():
44 | item.setCheckState(0, Qt.Checked)
45 | else:
46 | item.setCheckState(0, Qt.Unchecked)
47 |
48 | # This for categories which aren't expanded
49 | for item in filter(lambda x: x in source.selectedItems() and x.childCount() and not x.isExpanded(), bucketlist):
50 | CheckChildren(item)
51 |
52 |
53 | def CleanChildren(item: QtWidgets.QTreeWidgetItem):
54 | """
55 | The clone function duplicates unchecked children as well, so we're cleaning those off. I'm sorry, little ones.
56 | """
57 | for i in range(item.childCount()):
58 | child = item.child(i)
59 | if child: # Failsafe
60 | if child.childCount():
61 | CleanChildren(child)
62 | elif child.checkState(0) == Qt.Unchecked:
63 | item.takeChild(i)
64 |
65 |
66 | def AssembleCode(code: str):
67 | """
68 | Takes an unformatted string and adds spaces and newlines.
69 | """
70 | assembledcode = ''
71 | for index, char in enumerate(code):
72 | if not index % 16 and index:
73 | assembledcode = '\n'.join([assembledcode, char.upper()])
74 | elif not index % 8 and index:
75 | assembledcode = ' '.join([assembledcode, char.upper()])
76 | else:
77 | assembledcode = ''.join([assembledcode, char.upper()])
78 | return assembledcode
79 |
--------------------------------------------------------------------------------
/options.py:
--------------------------------------------------------------------------------
1 | """
2 | A tiny settings widget
3 | """
4 | import configparser
5 | import os
6 |
7 | from PyQt5 import QtWidgets
8 | from PyQt5.Qt import Qt
9 |
10 | import globalstuff
11 |
12 |
13 | class SettingsWidget(QtWidgets.QDialog):
14 | def __init__(self):
15 | super().__init__()
16 |
17 | # Disable the "?" button and resizing
18 | self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
19 | self.setFixedSize(self.minimumSize())
20 | self.setWindowIcon(globalstuff.progico)
21 |
22 | # Initialize some variables
23 | themelist = ['Default', 'Dark']
24 |
25 | # Autosave checkbox
26 | self.NoWarnLabel = QtWidgets.QLabel('Disable Close Warning')
27 | self.NoWarnCheckbox = QtWidgets.QCheckBox()
28 | self.NoWarnCheckbox.setChecked(globalstuff.nowarn)
29 | self.NoWarnCheckbox.stateChanged.connect(self.HandleNoWarn)
30 |
31 | # Theme selector
32 | self.ThemeLabel = QtWidgets.QLabel('Theme')
33 | self.Theme = QtWidgets.QComboBox()
34 | self.Theme.addItems(themelist)
35 | for index, content in enumerate(themelist):
36 | if content.lower() == globalstuff.theme:
37 | self.Theme.setCurrentIndex(index)
38 | break
39 | self.Theme.currentIndexChanged.connect(self.HandleThemeChoose)
40 |
41 | # Add elements to layout
42 | L = QtWidgets.QGridLayout()
43 | L.addWidget(self.NoWarnLabel, 0, 0)
44 | L.addWidget(self.NoWarnCheckbox, 0, 1)
45 | L.addWidget(self.ThemeLabel, 1, 0)
46 | L.addWidget(self.Theme, 1, 1)
47 | self.setLayout(L)
48 | self.setWindowTitle('Settings')
49 |
50 | if globalstuff.theme == 'dark':
51 | self.setStyleSheet(globalstuff.checkqss)
52 |
53 | def HandleNoWarn(self, state: int):
54 | globalstuff.nowarn = bool(state)
55 |
56 | def HandleThemeChoose(self, index: int):
57 | globalstuff.theme = self.Theme.itemText(index).lower()
58 | if globalstuff.theme == 'dark':
59 | SetDarkPalette()
60 | self.setStyleSheet(globalstuff.checkqss)
61 | else:
62 | SetLightPalette()
63 | self.setStyleSheet('')
64 |
65 |
66 | def readconfig(config: configparser.ConfigParser, file='config.ini'):
67 | """
68 | Reads a config file, or creates one if it doesn't exist
69 | """
70 | if not os.path.isfile(file):
71 | config['General'] = {'NoWarning': 'False', 'Theme': 'default'}
72 | else:
73 | config.read(file)
74 |
75 | # Set the globals
76 | globalstuff.nowarn = config.getboolean('General', 'NoWarning')
77 | globalstuff.theme = config['General']['Theme']
78 |
79 |
80 | def writeconfig(config: configparser.ConfigParser, file='config.ini'):
81 | """
82 | Writes settings to an ini file.
83 | """
84 | config.set('General', 'NoWarning', str(globalstuff.nowarn))
85 | config.set('General', 'Theme', globalstuff.theme)
86 | with open(file, 'w') as file:
87 | config.write(file)
88 |
89 |
90 | def SetDarkPalette():
91 | """
92 | Does all the changes required for dark mode.
93 | """
94 | # Set style to fusion
95 | globalstuff.app.setStyle('Fusion')
96 |
97 | # Set mdi area bg
98 | globalstuff.mainWindow.mdi.setBackground(Qt.darkGray)
99 |
100 | # Set palette
101 | globalstuff.app.setPalette(globalstuff.darkpal)
102 | for window in globalstuff.mainWindow.mdi.subWindowList():
103 | window.widget().setPalette(globalstuff.textpal)
104 | if hasattr(window.widget(), 'TreeWidget'):
105 | window.widget().TreeWidget.setStyleSheet(globalstuff.treeqss)
106 |
107 | # Force stylesheet on menu bar because it doesn't want to cooperate
108 | qss = """
109 | QMenu::item { color: white }
110 | QMenu::item:disabled { color: transparent }
111 | """
112 | globalstuff.mainWindow.menuBar().setStyleSheet(qss)
113 |
114 |
115 | def SetLightPalette():
116 | """
117 | Resets the default palette.
118 | """
119 | # Set style to the first element of keys, which should be OS-specific
120 | globalstuff.app.setStyle(QtWidgets.QStyleFactory.keys()[0])
121 |
122 | # Reset mdi area bg
123 | globalstuff.mainWindow.mdi.setBackground(Qt.gray)
124 |
125 | # Reset palette
126 | globalstuff.app.setPalette(globalstuff.app.style().standardPalette())
127 | for window in globalstuff.mainWindow.mdi.subWindowList():
128 | window.widget().setPalette(globalstuff.app.style().standardPalette())
129 | if hasattr(window.widget(), 'TreeWidget'):
130 | window.widget().TreeWidget.setStyleSheet('')
131 |
132 | # Reset menu bar stylesheet
133 | globalstuff.mainWindow.menuBar().setStyleSheet('')
134 |
--------------------------------------------------------------------------------
/widgets.py:
--------------------------------------------------------------------------------
1 | """
2 | This file contains modified widgets used by various windows.
3 | """
4 | import globalstuff
5 | from PyQt5 import QtWidgets, QtGui
6 | from PyQt5.Qt import Qt
7 |
8 |
9 | class ModdedTreeWidget(QtWidgets.QTreeWidget):
10 | """
11 | This modded tree widget lets me move codes between subwindows without losing data
12 | """
13 | def __init__(self):
14 | super().__init__()
15 |
16 | # Hide header, enable multiple selection and reordering, add space to the right and set edit trigger to select+click
17 | self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
18 | self.setHeaderHidden(True)
19 | self.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection)
20 | self.setEditTriggers(QtWidgets.QAbstractItemView.SelectedClicked)
21 | header = self.header()
22 | header.setStretchLastSection(False)
23 | header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
24 |
25 | def dragEnterEvent(self, e: QtGui.QDragEnterEvent):
26 | """
27 | This forces the widget to accept drops, which would otherwise be rejected due to the InternalMove flag.
28 | """
29 | src = e.source()
30 | if isinstance(src, QtWidgets.QTreeWidget):
31 | e.accept()
32 |
33 | def dropEvent(self, e: QtGui.QDropEvent):
34 | """
35 | This bad hack adds a copy of the source widget's selected items in the destination widget. This is due to PyQt
36 | clearing the hidden columns, which we don't want.
37 | """
38 | src = e.source()
39 | if src is not self:
40 | for item in src.selectedItems():
41 | clone = item.clone()
42 | clone.setFlags(clone.flags() | Qt.ItemIsEditable)
43 | self.addTopLevelItem(clone)
44 | super().dropEvent(e) # Call the original function
45 |
46 |
47 | class ModdedTreeWidgetItem(QtWidgets.QTreeWidgetItem):
48 | """
49 | Basically a glorified QTreeWidgetItem, with a couple of improvements that should make them less annoying to use.
50 | """
51 | def __init__(self, text: str, iscategory: bool, iseditable: bool):
52 | super().__init__()
53 |
54 | # Set check state
55 | self.setCheckState(0, Qt.Unchecked)
56 |
57 | # Set default text
58 | if text:
59 | self.setText(0, text)
60 | elif iscategory:
61 | self.setText(0, 'New Category')
62 | else:
63 | self.setText(0, 'New Code')
64 |
65 | # Set flags based on the given directives
66 | self.setAsCategory(iscategory)
67 | self.setAsEditable(iseditable)
68 |
69 | def setAsCategory(self, iscategory: bool):
70 | if iscategory:
71 | self.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
72 | self.setFlags(self.flags() | Qt.ItemIsAutoTristate | Qt.ItemIsDropEnabled)
73 | else:
74 | self.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.DontShowIndicator)
75 | self.setFlags(self.flags() ^ Qt.ItemIsDropEnabled ^ Qt.ItemIsAutoTristate)
76 |
77 | def setAsEditable(self, iseditable: bool):
78 | if iseditable:
79 | self.setFlags(self.flags() | Qt.ItemIsEditable)
80 | elif self.flags() & Qt.ItemIsEditable:
81 | self.setFlags(self.flags() ^ Qt.ItemIsEditable)
82 |
83 |
84 | class ModdedSubWindow(QtWidgets.QMdiSubWindow):
85 | """
86 | Dark mode and box updating functionality.
87 | """
88 | def __init__(self, islist: bool):
89 | super().__init__()
90 | self.islist = islist
91 | self.setWindowIcon(globalstuff.empty)
92 | self.setAttribute(Qt.WA_DeleteOnClose)
93 |
94 | def setWidget(self, widget: QtWidgets.QWidget):
95 | """
96 | Adds a fix for dark theme if it's enabled
97 | """
98 | super().setWidget(widget)
99 | if globalstuff.theme == 'dark':
100 | w = self.widget()
101 | w.setPalette(globalstuff.textpal)
102 | if hasattr(w, 'TreeWidget'):
103 | w.TreeWidget.setStyleSheet(globalstuff.treeqss)
104 |
105 | def closeEvent(self, e: QtGui.QCloseEvent):
106 | super().closeEvent(e)
107 | if self.islist:
108 | globalstuff.mainWindow.updateboxes()
109 |
110 |
111 | class ModdedMdiArea(QtWidgets.QMdiArea):
112 | """
113 | Modded MdiArea to accept file drops
114 | """
115 | def __init__(self):
116 | super().__init__()
117 | self.setAcceptDrops(True)
118 |
119 | def dragEnterEvent(self, e: QtGui.QDragEnterEvent):
120 | if e.mimeData().hasUrls:
121 | e.accept()
122 | else:
123 | e.ignore()
124 |
125 | def dragMoveEvent(self, e: QtGui.QDragMoveEvent):
126 | if e.mimeData().hasUrls:
127 | e.setDropAction(Qt.CopyAction)
128 | e.accept()
129 | else:
130 | e.ignore()
131 |
132 | def dropEvent(self, e: QtGui.QDropEvent):
133 | if e.mimeData().hasUrls():
134 | e.setDropAction(Qt.CopyAction)
135 | e.accept()
136 | links = [str(url.toLocalFile()) for url in e.mimeData().urls()]
137 | globalstuff.mainWindow.openCodelist(None, links)
138 | else:
139 | e.ignore()
140 |
--------------------------------------------------------------------------------
/database.py:
--------------------------------------------------------------------------------
1 | """
2 | Databases are basically read-only lists of codes read from an xml, which adds extra information to the manager.
3 | """
4 | import os
5 | import shutil
6 | import urllib.request
7 | from pkg_resources import parse_version as vercomp
8 |
9 | from lxml import etree
10 | from PyQt5 import QtWidgets
11 | from PyQt5.Qt import Qt
12 |
13 | import globalstuff
14 | from codelist import CodeList
15 | from codeeditor import CodeEditor, HandleCodeOpen, CleanParentz
16 | from common import CountCheckedCodes, SelectItems
17 | from titles import TitleLookup
18 | from widgets import ModdedTreeWidgetItem
19 |
20 |
21 | class Database(QtWidgets.QWidget):
22 | def __init__(self, name):
23 | super().__init__()
24 |
25 | # Create the Database Browser and connect it to the handlers
26 | self.TreeWidget = QtWidgets.QTreeWidget()
27 | self.TreeWidget.itemSelectionChanged.connect(self.HandleSelection)
28 | self.TreeWidget.itemDoubleClicked.connect(lambda x: HandleCodeOpen(x, True))
29 | self.TreeWidget.itemClicked.connect(self.EnableButtons)
30 |
31 | # Hide header, enable multiple selection, set items as draggable and add some space on the right
32 | self.TreeWidget.setHeaderHidden(True)
33 | self.TreeWidget.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection)
34 | self.TreeWidget.setDragDropMode(QtWidgets.QAbstractItemView.DragOnly)
35 | header = self.TreeWidget.header()
36 | header.setStretchLastSection(False)
37 | header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
38 |
39 | # Add the search bar
40 | self.SearchBar = QtWidgets.QLineEdit()
41 | self.SearchBar.setPlaceholderText('Search codes...')
42 | self.SearchBar.textEdited.connect(self.HandleSearch)
43 |
44 | # Add the opened codelist combo box...
45 | self.Combox = QtWidgets.QComboBox()
46 | self.Combox.addItem('Create New Codelist')
47 |
48 | # ...and the "Add" button
49 | self.AddButton = QtWidgets.QPushButton('Add to Codelist')
50 | self.AddButton.setEnabled(False)
51 | self.AddButton.clicked.connect(self.HandleAdd)
52 |
53 | # Finally, add the "Update" button
54 | self.UpdateButton = QtWidgets.QPushButton('Download Updates')
55 | self.UpdateButton.clicked.connect(self.UpdateDatabase)
56 |
57 | # Make a layout and set it
58 | lyt = QtWidgets.QGridLayout()
59 | lyt.addWidget(self.SearchBar, 0, 0, 1, 2)
60 | lyt.addWidget(self.TreeWidget, 1, 0, 1, 2)
61 | lyt.addWidget(self.Combox, 2, 0)
62 | lyt.addWidget(self.AddButton, 2, 1)
63 | lyt.addWidget(self.UpdateButton, 3, 0, 1, 2)
64 | self.setLayout(lyt)
65 |
66 | # Open the database
67 | self.dbfile = name
68 | tree = etree.parse(name).getroot()
69 |
70 | # Parse game id, lookup the corresponding name, then apply them to the window title
71 | try:
72 | self.gameID = tree.xpath('id')[0].text
73 | self.gameName = TitleLookup(self.gameID)
74 | except:
75 | self.gameID = 'UNKW00' # Failsafe
76 | self.gameName = 'Unknown Game'
77 | self.setWindowTitle('Database Browser - {} [{}]'.format(self.gameName, self.gameID))
78 |
79 | # Add the update url
80 | try:
81 | self.ver = tree.xpath('update')[0].attrib['version']
82 | self.updateURL = tree.xpath('update')[0].text
83 | except:
84 | self.ver = '0'
85 | self.updateURL = ''
86 |
87 | # Enable the update button if an url is present
88 | self.UpdateButton.setEnabled(bool(self.updateURL))
89 |
90 | # Import the codes (the second tree is because there can be codes without a category)
91 | self.ParseDatabase(tree.xpath('category') + tree.xpath('code'))
92 |
93 | def ParseDatabase(self, tree: etree, parent: QtWidgets.QTreeWidgetItem = None):
94 | """
95 | Recursively create the code tree based on the xml
96 | """
97 | for entry in tree:
98 | newitem = ModdedTreeWidgetItem(entry.attrib['name'], entry.tag == 'category', False)
99 |
100 | # Determine parenthood
101 | if parent:
102 | parent.addChild(newitem)
103 | else:
104 | self.TreeWidget.addTopLevelItem(newitem)
105 |
106 | # Determine type of entry. Elif makes sure unknown entries are ignored.
107 | if entry.tag == 'category':
108 | self.ParseDatabase(entry, newitem)
109 | elif entry.tag == 'code':
110 | newitem.setText(1, entry[0].text.strip().upper())
111 | newitem.setText(2, entry.attrib['comment'])
112 | newitem.setText(4, entry.attrib['author'])
113 |
114 | def HandleSelection(self):
115 | """
116 | Self explanatory.
117 | """
118 | SelectItems(self.TreeWidget)
119 | self.EnableButtons()
120 |
121 | def EnableButtons(self):
122 | """
123 | Updates the Add button.
124 | """
125 | self.AddButton.setEnabled(bool(list(CountCheckedCodes(self.TreeWidget, False))))
126 |
127 | def HandleSearch(self, text: str):
128 | """
129 | Filters codes based on a given string
130 | """
131 | for item in self.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive):
132 | # Hide all items
133 | item.setHidden(True)
134 |
135 | # Unhide the item if its name or code match, then unhide its parents
136 | if item.text(1) and any(text.lower() in item.text(i).lower() for i in range(2)):
137 | item.setHidden(False)
138 | self.UnhideParent(item)
139 |
140 | def UnhideParent(self, item: QtWidgets.QTreeWidgetItem):
141 | """
142 | Recursively unhides a given item's parents
143 | """
144 | if item.parent():
145 | item.parent().setHidden(False)
146 | self.UnhideParent(item.parent())
147 |
148 | def HandleAdd(self):
149 | """
150 | Transfers the selected codes to the chosen codelist
151 | """
152 | enabledlist = CountCheckedCodes(self.TreeWidget, False)
153 | if self.Combox.currentIndex() > 0:
154 | self.Combox.currentData().AddFromDatabase(enabledlist, self.gameID)
155 | else:
156 | win = globalstuff.mainWindow.CreateNewWindow(CodeList())
157 | win.AddFromDatabase(enabledlist, self.gameID)
158 |
159 | def UpdateDatabase(self):
160 | """
161 | Updates the database from the given url.
162 | """
163 | # Download the file
164 | try:
165 | with urllib.request.urlopen(self.updateURL) as src, open('tmp.xml', 'wb') as dst:
166 | dst.write(src.read())
167 | except:
168 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Download Error',
169 | 'There was an error during the database download. Retry?')
170 | if msgbox == QtWidgets.QMessageBox.Yes:
171 | self.UpdateDatabase()
172 | else:
173 | return
174 |
175 | # Get the tree and the version. If the program fails to do so, quietly exit
176 | try:
177 | tree = etree.parse('tmp.xml').getroot()
178 | ver = tree.xpath('update')[0].attrib['version']
179 | except:
180 | os.remove('tmp.xml')
181 | return
182 |
183 | # Check that the new version is actually newer, otherwise exit
184 | if vercomp(ver) <= vercomp(self.ver):
185 | QtWidgets.QMessageBox.information(globalstuff.mainWindow, 'Up to date', 'Database is up to date!')
186 | os.remove('tmp.xml')
187 | return
188 |
189 | # Change the string
190 | self.ver = ver
191 |
192 | # Clean the parentz parameter of affected Code Editors
193 | # The window list is created earlier so it isn't generated a gazillion times in the for loop
194 | wlist = [w.widget() for w in globalstuff.mainWindow.mdi.subWindowList() if isinstance(w.widget(), CodeEditor)]
195 | for item in filter(lambda x: bool(x.text(1)), self.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive)):
196 | CleanParentz(item, wlist)
197 |
198 | # Clear the tree and import the codes
199 | self.TreeWidget.clear()
200 | self.ParseDatabase(tree.xpath('category') + tree.xpath('code'))
201 |
202 | # Overwrite the original file and disable the update button, since we no longer need it.
203 | shutil.move('tmp.xml', self.dbfile)
204 | self.UpdateButton.setEnabled(False)
205 |
--------------------------------------------------------------------------------
/codeeditor.py:
--------------------------------------------------------------------------------
1 | """
2 | The CodeEditor is a relatively simple window which shows a code, its name, author and comment. It also lets you edit it
3 | and open it with other windows, or add it to different lists.
4 | """
5 | import re
6 |
7 | from PyQt5 import QtGui, QtWidgets
8 |
9 | import globalstuff
10 | from common import AssembleCode
11 |
12 |
13 | class CodeEditor(QtWidgets.QWidget):
14 | def __init__(self, parent: QtWidgets.QTreeWidgetItem = None, fromdb: bool = None):
15 | super().__init__()
16 |
17 | # Initialize vars
18 | self.parentz = parent # This is named parentz due to a name conflict
19 | self.fromdb = fromdb if fromdb else False
20 | self.dirty = False
21 | name = 'New Code'
22 | code = comment = author = ''
23 |
24 | if self.parentz:
25 | name = parent.text(0)
26 | code = parent.text(1)
27 | comment = parent.text(2)
28 | author = parent.text(4)
29 |
30 | # Create the author, code and comment forms
31 | self.NameLabel = QtWidgets.QLabel('Name:')
32 | self.CodeName = QtWidgets.QLineEdit(name)
33 | self.AuthorLabel = QtWidgets.QLabel('Author:')
34 | self.CodeAuthor = QtWidgets.QLineEdit(author)
35 | self.CodeLabel = QtWidgets.QLabel('Code:')
36 | self.CodeContent = QtWidgets.QPlainTextEdit(code)
37 | self.CommentLabel = QtWidgets.QLabel('Comment:')
38 | self.CodeComment = QtWidgets.QPlainTextEdit(comment)
39 |
40 | # Use Monospaced font for the code
41 | self.CodeContent.setFont(QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont))
42 |
43 | # Connect
44 | self.CodeName.textEdited.connect(self.SetDirty)
45 | self.CodeAuthor.textEdited.connect(self.SetDirty)
46 | self.CodeContent.textChanged.connect(self.SetDirty)
47 | self.CodeComment.textChanged.connect(self.SetDirty)
48 |
49 | # Save button
50 | self.SaveButton = QtWidgets.QPushButton('Save Changes')
51 | self.SaveButton.setEnabled(False)
52 | self.SaveButton.clicked.connect(self.SaveCode)
53 |
54 | # Add the opened codelist combo box
55 | self.Combox = QtWidgets.QComboBox()
56 | self.Combox.addItem('Create New Codelist')
57 |
58 | # Finally, add the "Add" button
59 | self.AddButton = QtWidgets.QPushButton('Save to Codelist')
60 | self.AddButton.clicked.connect(lambda: globalstuff.mainWindow.AddFromEditor(self, self.Combox.currentData()))
61 | if not code:
62 | self.AddButton.setEnabled(False)
63 |
64 | # Set the window title
65 | if author:
66 | self.setWindowTitle('Code Editor - {} [{}]'.format(name, author))
67 | else:
68 | self.setWindowTitle('Code Editor - {}'.format(name))
69 |
70 | # Make a layout and set it
71 | lyt = QtWidgets.QGridLayout()
72 | lyt.addWidget(self.NameLabel, 0, 0, 1, 2)
73 | lyt.addWidget(self.CodeName, 1, 0, 1, 2)
74 | lyt.addWidget(self.AuthorLabel, 2, 0, 1, 2)
75 | lyt.addWidget(self.CodeAuthor, 3, 0, 1, 2)
76 | lyt.addWidget(self.CodeLabel, 4, 0, 1, 2)
77 | lyt.addWidget(self.CodeContent, 5, 0, 1, 2)
78 | lyt.addWidget(self.CommentLabel, 6, 0, 1, 2)
79 | lyt.addWidget(self.CodeComment, 7, 0, 1, 2)
80 | lyt.addWidget(self.SaveButton, 8, 0, 1, 2)
81 | lyt.addWidget(self.Combox, 9, 0)
82 | lyt.addWidget(self.AddButton, 9, 1)
83 | self.setLayout(lyt)
84 |
85 | def SetDirty(self):
86 | """
87 | Enables the save button if the code is not empty and the parent is set (otherwise we'd have nowhere to save to)
88 | """
89 | # Add some dirt
90 | self.dirty = True
91 |
92 | # Set the buttons
93 | if self.CodeContent.toPlainText() and self.CodeName.text():
94 | if not self.fromdb:
95 | self.SaveButton.setEnabled(bool(self.parentz))
96 | self.AddButton.setEnabled(True)
97 | else:
98 | self.SaveButton.setEnabled(False)
99 | self.AddButton.setEnabled(False)
100 |
101 | # Add asterisk to window title
102 | if not self.windowTitle().startswith('*'):
103 | self.setWindowTitle('*' + self.windowTitle())
104 |
105 | def SaveCode(self):
106 | """
107 | Saves the code to the designated parent.
108 | """
109 | # Initialize vars
110 | code = self.ParseCode()
111 | comment = re.sub('\n{2,}', '\n', self.CodeComment.toPlainText())
112 | author = self.CodeAuthor.text()
113 |
114 | # Save the stuff
115 | self.parentz.setText(0, self.CodeName.text())
116 | self.parentz.setText(1, code)
117 | self.parentz.setText(2, comment)
118 | self.parentz.setText(4, author)
119 |
120 | # Update the fields
121 | self.CodeContent.setPlainText(code)
122 | self.CodeComment.setPlainText(comment)
123 |
124 | # Update window title
125 | self.ParseAuthor(author)
126 |
127 | # Disable the save button
128 | self.SaveButton.setEnabled(False)
129 |
130 | # Clean dirt
131 | self.setWindowTitle(self.windowTitle().lstrip('*'))
132 | self.dirty = False
133 |
134 | def ParseCode(self):
135 | """
136 | Parses the code to make sure it is formatted properly.
137 | """
138 | # Remove spaces and new lines
139 | code = re.sub('[* \n]', '', self.CodeContent.toPlainText())
140 |
141 | # Add padding if the code is not a multiple of 16
142 | while len(code) % 16:
143 | code += '0'
144 |
145 | # Assemble the code and force uppercase
146 | return AssembleCode(code)
147 |
148 | def ParseAuthor(self, author):
149 | """
150 | Because we really don't like duplication.
151 | """
152 | # Update the window title
153 | if author:
154 | self.setWindowTitle('Code Editor - {} [{}]'.format(self.CodeName.text(), author))
155 | else:
156 | self.setWindowTitle('Code Editor - {}'.format(self.CodeName.text()))
157 |
158 | def closeEvent(self, e: QtGui.QCloseEvent):
159 | """
160 | Overrides close event to ask the user if they want to save.
161 | """
162 | if self.dirty:
163 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Unsaved Changes', "The code has unsaved changes in it. Save?")
164 | if msgbox == QtWidgets.QMessageBox.Yes:
165 | if self.parentz and not self.fromdb:
166 | self.SaveCode()
167 | else:
168 | globalstuff.mainWindow.AddFromEditor(self)
169 | super().closeEvent(e)
170 |
171 |
172 | def HandleCodeOpen(item: QtWidgets.QTreeWidgetItem, fromdb: bool, willcreate=True):
173 | """
174 | Opens a tree's currently selected code in a CodeEditor window.
175 | """
176 | if item.text(1):
177 | for window in globalstuff.mainWindow.mdi.subWindowList(): # Find if there's an existing CodeEditor with same parent and window title
178 | if isinstance(window.widget(), CodeEditor) and window.widget().parentz == item:
179 | willcreate = False
180 | globalstuff.mainWindow.mdi.setActiveSubWindow(window) # This code was already opened, so let's just set the focus on the existing window
181 | break
182 | if willcreate: # If the code is not already open, go ahead and do it
183 | globalstuff.mainWindow.CreateNewWindow(CodeEditor(item, fromdb))
184 |
185 |
186 | def CleanParentz(item: QtWidgets.QTreeWidgetItem, wlist: list):
187 | """
188 | Unsets the parentz parameter for the removed tree item.
189 | """
190 | for window in wlist:
191 | if window.parentz == item:
192 | window.parentz = None
193 | break
194 |
195 |
196 | def RenameWindows(item: QtWidgets.QTreeWidgetItem):
197 | """
198 | When you rename a code, the program will look for code editors that originated from that code and update their
199 | window title accordingly
200 | """
201 | # First, verify that the name is not empty. If so, restore the original string and clear the backup.
202 | if not item.text(0):
203 | item.setText(0, item.statusTip(0))
204 | item.setStatusTip(0, '')
205 | return # Since there was no update, we don't need to run the below stuff
206 |
207 | # Do the rename
208 | for w in globalstuff.mainWindow.mdi.subWindowList():
209 | if isinstance(w.widget(), CodeEditor) and w.widget().parentz == item:
210 | w.widget().CodeName.setText(item.text(0))
211 | if item.text(4):
212 | w.widget().setWindowTitle('Code Editor - {} [{}]'.format(item.text(0), item.text(4)))
213 | else:
214 | w.widget().setWindowTitle('Code Editor - {}'.format(item.text(0)))
215 | return
216 |
--------------------------------------------------------------------------------
/exporting.py:
--------------------------------------------------------------------------------
1 | """
2 | This files contains multiple functions to export codelists.
3 | """
4 | import os
5 | import re
6 | from binascii import unhexlify
7 |
8 | from PyQt5 import QtWidgets
9 | from PyQt5.Qt import Qt
10 |
11 | import globalstuff
12 | from codelist import CodeList
13 | from common import CountCheckedCodes
14 |
15 |
16 | def WriteCheck(filename: str, silent: bool):
17 | """
18 | This function performs a couple preliminary operations before importing can take place. Very informative, i know.
19 | """
20 | # Check if we can write the file. If not, trigger an error message.
21 | if not os.access(filename, os.W_OK):
22 | if not silent:
23 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'File Write Error', "Can't write file " + filename)
24 | return False
25 | return True
26 |
27 |
28 | def WriteItems(f, enabledlist, depth):
29 | """
30 | This recursive function is used by the TXT exporter. So much fun.
31 | """
32 | for item in enabledlist:
33 |
34 | # It's a category. Write it only if it's not empty.
35 | if not item.text(1):
36 | if item.childCount():
37 | f.write(''.join(['#' * depth, item.text(0), '\n\n'])) # Add the hashtags if we're in a nested category
38 | WriteItems(f, [item.child(i) for i in range(item.childCount())], depth + 1) # Recursive :o
39 |
40 | # It's a code
41 | else:
42 |
43 | # Write the code name
44 | f.write(item.text(0))
45 |
46 | # If the code has an author, add it between "[]"
47 | if item.text(4):
48 | f.write(''.join([' [', item.text(4), ']']))
49 |
50 | # If the code is enabled, add an asterisk at the beginning of each line
51 | if item.checkState(0) == Qt.Checked:
52 | f.writelines(['\n* ' + line for line in item.text(1).splitlines()])
53 |
54 | # Otherwise just add a new line and write the entire code
55 | else:
56 | f.write('\n')
57 | f.write(item.text(1))
58 |
59 | # Add the comment if it exists, preceded by a newline
60 | if item.text(2):
61 | f.write('\n')
62 | f.write(item.text(2))
63 |
64 | # Add the final padding newlines
65 | f.write('\n\n')
66 |
67 | # We have reached the end of the list (or category). If we're in the latter, write the category escape character and the newlines
68 | if depth > 0:
69 | f.write('#' * depth)
70 | f.write('\n\n')
71 |
72 |
73 | def InvalidCharacter(name: str, line: int, char: list):
74 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Invalid Line', ''.join(['Invalid character "', char,
75 | '" in code "', name,
76 | '" in line ', str(line),
77 | '. Continue exporting?']))
78 | return msgbox
79 |
80 |
81 | def ExportTXT(filename: str, source: CodeList, silent: bool):
82 | # Open the file
83 | f = open(filename, 'w')
84 |
85 | # Now that we opened the file, we can check if it can be written.
86 | if not WriteCheck(filename, silent):
87 | f.close()
88 | os.remove(filename)
89 | return False
90 |
91 | # Initialize vars
92 | enabledlist = source.TreeWidget.findItems('', Qt.MatchContains)
93 |
94 | # Write the game id and name
95 | f.write('\n'.join([source.gameID, source.gameName, '', '']))
96 |
97 | # Write the codes!
98 | WriteItems(f, enabledlist, 0)
99 |
100 | # Remove the extra newline at the end, then close the file!
101 | f.seek(f.tell() - 2) # We have to use seek type 0 or the program will crash
102 | f.truncate()
103 | f.close()
104 | return True
105 |
106 |
107 | def ExportINI(filename: str, source: CodeList, silent: bool):
108 | """
109 | The simplest export function so far. A real piece of cake.
110 | """
111 | # Open the file. Not using "with" here due to error handling later on
112 | f = open(filename, 'w')
113 |
114 | # Now that we opened the file, we can check if it can be written.
115 | if not WriteCheck(filename, silent):
116 | f.close()
117 | os.remove(filename)
118 | return False
119 |
120 | # Initialize vars
121 | linerule = re.compile('^[\dA-F]{8} [\dA-F]{8}$', re.I | re.M) # Ignore case + multiple lines
122 | enabledlist = filter(lambda x: bool(x.text(1)), source.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive))
123 | geckostr = '[Gecko]'
124 | geckoenabledstr = '\n[Gecko_Enabled]' # Adding a new line because it's not at the beginning of the file
125 |
126 | # Assemble the giant strings
127 | for item in enabledlist:
128 |
129 | # Add code name, code and author if present. Code must be lowercase because Dolphin.
130 | if item.text(4):
131 | geckostr = ''.join([geckostr, '\n$', item.text(0), ' [', item.text(4), ']\n', item.text(1).lower()])
132 | else:
133 | geckostr = ''.join([geckostr, '\n$', item.text(0), '\n', item.text(1).lower()])
134 |
135 | # Add comment if present
136 | if item.text(2):
137 | for line in item.text(2).splitlines():
138 | geckostr = '\n*'.join([geckostr, line])
139 | else:
140 | geckostr += '\n*'
141 |
142 | # Add to Gecko_Enabled if checked, but only if the code is valid
143 | if item.checkState(0) == Qt.Checked and len(re.findall(linerule, item.text(1))) == item.text(1).count('\n') + 1:
144 | geckoenabledstr = '\n$'.join([geckoenabledstr, item.text(0)])
145 |
146 | # Write the codes!
147 | f.write(geckostr)
148 |
149 | # Only write gecko enabled if at least one code is enabled
150 | if len(geckoenabledstr) > 16:
151 | f.write(geckoenabledstr)
152 |
153 | # Autosaved data was found, ask the user what they want to do with it. The warning is fake as per usual.
154 | if source.scrap:
155 | if not silent:
156 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Additional Data Found',
157 | 'Additional data was found in a previously imported .ini file.'
158 | 'Port the data over to this file?')
159 | if silent or msgbox == QtWidgets.QMessageBox.Yes:
160 | f.write('\n')
161 | f.write(source.scrap)
162 | source.scrap = ''
163 |
164 | # Write the final newline and close the file. Time to pack up and go home.
165 | f.write('\n')
166 | f.close()
167 | return True
168 |
169 |
170 | def ExportGCT(filename: str, source: CodeList, silent: bool):
171 | """
172 | Exports a GCT in the regular format (screw BrawlBox)
173 | """
174 | # Open the file. Not using "with" here due to error handling later on
175 | f = open(filename, 'wb')
176 |
177 | # Now that we opened the file, we can check if it can be written.
178 | if not WriteCheck(filename, silent):
179 | f.close()
180 | os.remove(filename)
181 | return False
182 |
183 | # Initialize vars
184 | charrule = re.compile('[\d A-F]', re.I)
185 | linerule = re.compile('^[\dA-F]{8} [\dA-F]{8}$', re.I)
186 | enabledlist = filter(lambda x: bool(x.text(1)), CountCheckedCodes(source.TreeWidget, True))
187 |
188 | # Write the gct!
189 | f.write(globalstuff.gctmagic)
190 | for item in enabledlist:
191 | code = item.text(1).splitlines() # Remove newlines
192 | currline = 1
193 | for line in code:
194 |
195 | # Make sure there are no non-hex characters
196 | if re.match(linerule, line):
197 | f.write(unhexlify(line.replace(' ', ''))) # Didn't strip spaces earlier for line count purposes ;)
198 | currline += 1
199 |
200 | # There's an invalid character! FIND HIM!
201 | else:
202 | char = re.sub(charrule, '', line)[0]
203 |
204 | # Caught the offender. You're under arrest!
205 | if not silent and InvalidCharacter(item.text(0), currline, char) == QtWidgets.QMessageBox.No:
206 | f.close()
207 | os.remove(filename) # Remove the incomplete file
208 | return False
209 | else:
210 | f.seek(-8 * currline, 1) # Go back to the beginning of this code
211 | f.truncate() # Remove all lines after it, the code is broken
212 | break # Go to next code
213 |
214 | # Finish it off
215 | f.write(globalstuff.gctend)
216 | flen = f.tell()
217 | f.close()
218 |
219 | # If we didn't write anything at all, might as well remove the file
220 | if flen == 16:
221 | os.remove(filename)
222 | return False
223 | return True
224 |
--------------------------------------------------------------------------------
/codelist.py:
--------------------------------------------------------------------------------
1 | """
2 | Codelists are different from databases, as they accept adding/removing, importing/exporting, reordering, dropping
3 | and more.
4 | """
5 | from PyQt5 import QtWidgets
6 | from PyQt5.Qt import Qt
7 |
8 | import globalstuff
9 | from codeeditor import CodeEditor, HandleCodeOpen, CleanParentz, RenameWindows
10 | from common import CountCheckedCodes, SelectItems, GameIDMismatch, CleanChildren
11 | from titles import TitleLookup
12 | from widgets import ModdedTreeWidget, ModdedTreeWidgetItem
13 |
14 |
15 | class CodeList(QtWidgets.QWidget):
16 | def __init__(self, wintitle: str = None):
17 | super().__init__()
18 |
19 | # Create the codelist and connect it to the handlers
20 | self.TreeWidget = ModdedTreeWidget()
21 | self.TreeWidget.itemSelectionChanged.connect(self.HandleSelection)
22 | self.TreeWidget.itemDoubleClicked.connect(lambda x: HandleCodeOpen(x, False))
23 | self.TreeWidget.itemChanged.connect(RenameWindows)
24 | self.TreeWidget.itemClicked.connect(self.HandleClicking)
25 |
26 | # Merge button, up here for widget height purposes
27 | self.mergeButton = QtWidgets.QPushButton('Merge Selected')
28 | self.mergeButton.clicked.connect(lambda: self.HandleMerge(CountCheckedCodes(self.TreeWidget, True)))
29 | #self.mergeButton.setShortcut('Ctrl+M')
30 |
31 | # Add button+menu
32 | addMenu = QtWidgets.QMenu()
33 | deface = addMenu.addAction('Add Code', lambda: globalstuff.mainWindow.CreateNewWindow(CodeEditor()))
34 | #deface.setShortcut('Ctrl+N')
35 | ace2 = addMenu.addAction('Add Category', self.HandleAddCategory)
36 | #ace2.setShortcut('Ctrl+Shift+N')
37 | self.addButton = QtWidgets.QToolButton()
38 | self.addButton.setDefaultAction(deface) # Do this if you click the Add button instead of the arrow
39 | self.addButton.setFixedHeight(self.mergeButton.sizeHint().height()) # Makes this the same height as QPushButton
40 | self.addButton.setMenu(addMenu)
41 | self.addButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
42 | self.addButton.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) # Use full widget width
43 | self.addButton.setText('Add')
44 |
45 | # Sort button+menu
46 | sortMenu = QtWidgets.QMenu()
47 | defact = sortMenu.addAction('Alphabetical', lambda: self.TreeWidget.sortItems(0, Qt.AscendingOrder))
48 | sortMenu.addAction('Alphabetical (Reverse)', lambda: self.TreeWidget.sortItems(0, Qt.DescendingOrder))
49 | sortMenu.addAction('Size', self.SortListSize)
50 | self.sortButton = QtWidgets.QToolButton()
51 | self.sortButton.setDefaultAction(defact) # Do this if you click the Sort button instead of the arrow
52 | self.sortButton.setMenu(sortMenu)
53 | self.sortButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
54 | self.sortButton.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) # Use full widget width
55 | self.sortButton.setText('Sort')
56 |
57 | # Import, Export and Remove buttons
58 | self.importButton = QtWidgets.QPushButton('Import List')
59 | self.exportButton = QtWidgets.QPushButton('Export List')
60 | self.removeButton = QtWidgets.QPushButton('Remove Selected')
61 | self.importButton.clicked.connect(lambda: globalstuff.mainWindow.openCodelist(self))
62 | self.exportButton.clicked.connect(lambda: globalstuff.mainWindow.exportList(self))
63 | self.removeButton.clicked.connect(self.HandleRemove)
64 | #self.importButton.setShortcut('Ctrl+I')
65 | #self.exportButton.setShortcut('Ctrl+E')
66 | self.removeButton.setShortcut(Qt.Key_Delete)
67 |
68 | # Configure the buttons
69 | self.EnableButtons()
70 |
71 | # Game ID text field + save button
72 | self.gidInput = QtWidgets.QLineEdit()
73 | self.gidInput.setPlaceholderText('Insert GameID here...')
74 | self.gidInput.setMaxLength(6)
75 | self.gidInput.textEdited.connect(self.UpdateButton)
76 | self.savegid = QtWidgets.QPushButton('Save')
77 | self.savegid.setEnabled(False)
78 | self.savegid.clicked.connect(lambda: self.SetGameID(self.gidInput.text()))
79 |
80 | # Set game id, game name and update the window title accordingly. Also add the scrap
81 | self.gameID = 'UNKW00'
82 | self.gameName = 'Unknown Game'
83 | self.SetGameID(wintitle if wintitle else '')
84 | self.scrap = ''
85 |
86 | # Make a horizontal layout for these two
87 | hlyt = QtWidgets.QHBoxLayout()
88 | hlyt.addWidget(self.gidInput)
89 | hlyt.addWidget(self.savegid)
90 |
91 | # Line counter
92 | self.lineLabel = QtWidgets.QLabel('Lines: 2')
93 | self.lineLabel.setAlignment(Qt.AlignRight)
94 |
95 | # Make a layout and set it
96 | lyt = QtWidgets.QGridLayout()
97 | lyt.addLayout(hlyt, 0, 0, 1, 2)
98 | lyt.addWidget(self.TreeWidget, 1, 0, 1, 2)
99 | lyt.addWidget(self.lineLabel, 2, 0, 1, 2)
100 | lyt.addWidget(self.addButton, 3, 0)
101 | lyt.addWidget(self.sortButton, 3, 1)
102 | lyt.addWidget(self.mergeButton, 4, 0)
103 | lyt.addWidget(self.removeButton, 4, 1)
104 | lyt.addWidget(self.importButton, 5, 0)
105 | lyt.addWidget(self.exportButton, 5, 1)
106 | self.setLayout(lyt)
107 |
108 | def AddFromDatabase(self, enabledlist: list, gameid: str):
109 | """
110 | Takes a list of the enabled items and clones it in the codelist.
111 | """
112 | # Check for game id mismatch and update if necessary
113 | if gameid != self.gameID:
114 | if self.gameID != 'UNKW00' and GameIDMismatch() == QtWidgets.QMessageBox.No:
115 | return
116 | self.SetGameID(gameid)
117 |
118 | # Add the codes
119 | for item in enabledlist:
120 | clone = item.clone()
121 | clone.setFlags(clone.flags() | Qt.ItemIsEditable) # Enable renaming
122 | self.TreeWidget.addTopLevelItem(clone)
123 | CleanChildren(clone)
124 |
125 | # Update the selection
126 | self.HandleSelection()
127 | self.UpdateLines()
128 |
129 | def HandleSelection(self):
130 | """
131 | Self explanatory
132 | """
133 | SelectItems(self.TreeWidget)
134 | self.EnableButtons()
135 | self.UpdateLines()
136 |
137 | def HandleClicking(self, item: QtWidgets.QTreeWidgetItem):
138 | """
139 | Backs up the codename and checks the buttons
140 | """
141 | item.setStatusTip(0, item.text(0))
142 | self.EnableButtons()
143 | self.UpdateLines()
144 |
145 | def EnableButtons(self, canexport=False, canremove=False, canmerge=False):
146 | """
147 | Enables the Remove, Export and Merge button if the respective conditions are met
148 | """
149 | for item in CountCheckedCodes(self.TreeWidget, True):
150 | if item.text(1):
151 | if canexport:
152 | canmerge = True
153 | break # All the options are already enabled, no need to parse the list any further
154 | canexport = True
155 | canremove = True
156 |
157 | self.removeButton.setEnabled(canremove)
158 | self.exportButton.setEnabled(canexport)
159 | self.mergeButton.setEnabled(canmerge)
160 |
161 | def HandleAddCategory(self):
162 | """
163 | Adds a new category to the codelist
164 | """
165 | newitem = ModdedTreeWidgetItem('', True, True)
166 | self.TreeWidget.addTopLevelItem(newitem)
167 | self.TreeWidget.editItem(newitem, 0) # Let the user rename it immediately
168 |
169 | def SortListSize(self):
170 | """
171 | Temporarily removes all items without children, then orders the remaining items alphabetically. The removed
172 | items will then be ordered by code size and re-added to the tree.
173 | """
174 | # Remove all codes
175 | backuplist = []
176 | for item in filter(lambda x: bool(x.text(1)), self.TreeWidget.findItems('', Qt.MatchContains)):
177 | backuplist.append(self.TreeWidget.takeTopLevelItem(self.TreeWidget.indexOfTopLevelItem(item)))
178 |
179 | # Sort the categories alphabetically
180 | self.TreeWidget.sortItems(0, Qt.AscendingOrder)
181 |
182 | # Sort the backup list by code size (bigger codes first)
183 | backuplist.sort(key=lambda x: len(x.text(1)), reverse=True)
184 |
185 | # Reinsert the items
186 | self.TreeWidget.insertTopLevelItems(self.TreeWidget.topLevelItemCount(), backuplist)
187 |
188 | def HandleMerge(self, mergedlist: list):
189 | """
190 | Merges codes together
191 | """
192 | # Initialize vars
193 | destination = None
194 | wlist = [w.widget() for w in globalstuff.mainWindow.mdi.subWindowList() if isinstance(w.widget(), CodeEditor)]
195 |
196 | # Begin working
197 | for item in filter(lambda x: bool(x.text(1)), mergedlist):
198 |
199 | # We have a destination
200 | if destination:
201 | # Merge the codes
202 | destination.setText(1, '\n'.join([destination.text(1), item.text(1)]))
203 |
204 | # Kill any reference to the item
205 | CleanParentz(item, wlist)
206 | if item.parent():
207 | item.parent().takeChild(item.parent().indexOfChild(item))
208 | else:
209 | self.TreeWidget.takeTopLevelItem(self.TreeWidget.indexOfTopLevelItem(item))
210 |
211 | # It's the first code in the list, set it as destination
212 | else:
213 | destination = item
214 | destination.setText(2, '') # Clear the comment, as it no longer applies
215 |
216 | # Now find all instances of CodeEditor that have the destination code open, and update their code widget
217 | for window in wlist:
218 | if window.parentz == destination:
219 | window.setPlainText(destination.text(1))
220 | return
221 |
222 | def HandleRemove(self):
223 | """
224 | Handles item removal. Not much to say here :P
225 | """
226 | wlist = [w.widget() for w in globalstuff.mainWindow.mdi.subWindowList() if isinstance(w.widget(), CodeEditor)]
227 | for item in filter(lambda x: x.checkState(0) == Qt.Checked, CountCheckedCodes(self.TreeWidget, True)):
228 |
229 | # Remove the item
230 | if item.parent():
231 | item.parent().takeChild(item.parent().indexOfChild(item))
232 | else:
233 | self.TreeWidget.takeTopLevelItem(self.TreeWidget.indexOfTopLevelItem(item))
234 |
235 | # Set all code editor widgets that had this item as parent to None
236 | if item.text(1):
237 | CleanParentz(item, wlist)
238 |
239 | def UpdateButton(self):
240 | """
241 | Enables the button to save the game id if it's valid
242 | """
243 | self.savegid.setEnabled(len(self.gidInput.text()) > 3)
244 |
245 | def SetGameID(self, gameid: str):
246 | """
247 | Sets the given game id in the variable, game id text field and window title. Also looks up the game name.
248 | """
249 | if 4 <= len(gameid) <= 6:
250 | self.gameID = gameid
251 | self.gameName = TitleLookup(gameid)
252 | self.gidInput.setText(gameid)
253 | self.savegid.setEnabled(False)
254 | self.setWindowTitle('Codelist - {} [{}]'.format(self.gameName, gameid if gameid else self.gameID))
255 | globalstuff.mainWindow.updateboxes()
256 |
257 | def UpdateLines(self):
258 | """
259 | Updates the number of total code lines in the list
260 | """
261 | lines = 2 # One for the magic and one for the F0 terminator
262 | for item in filter(lambda x: bool(x.text(1)), CountCheckedCodes(self.TreeWidget, True)):
263 | lines += item.text(1).count('\n') + 1 # +1 is because the first line doesn't have an "\n" character
264 | self.lineLabel.setText('Lines: ' + str(lines))
265 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Main executable, unsurprisingly. Also known as the circular import prevention junkyard.
3 | """
4 | import configparser
5 | import os
6 | import re
7 | import sys
8 |
9 | from PyQt5 import QtWidgets, QtGui
10 | from PyQt5.Qt import Qt
11 |
12 | import exporting
13 | import importing
14 | import globalstuff
15 | from codeeditor import CodeEditor
16 | from codelist import CodeList
17 | from database import Database
18 | from options import SettingsWidget, SetDarkPalette, readconfig, writeconfig
19 | from titles import DownloadError
20 | from widgets import ModdedSubWindow, ModdedTreeWidgetItem, ModdedMdiArea
21 | from windowstuff import TileVertical, TileHorizontal, MinimizeAll, CloseAll, Half
22 |
23 |
24 | class MainWindow(QtWidgets.QMainWindow):
25 | def __init__(self):
26 | super().__init__()
27 |
28 | # Create the interface
29 | self.mdi = ModdedMdiArea()
30 | self.setCentralWidget(self.mdi)
31 |
32 | # Create the menubar
33 | self.optgct = self.optini = self.opttxt = None
34 | self.createMenubar()
35 |
36 | # Add the program icon
37 | globalstuff.progico = QtGui.QIcon('icon.ico')
38 | self.setWindowIcon(globalstuff.progico)
39 |
40 | # Set window title and show the window maximized
41 | self.setWindowTitle('Code Manager Reborn')
42 | self.showMaximized()
43 |
44 | # Check for the wiitdb.txt file
45 | if not os.path.isfile(globalstuff.wiitdb):
46 | DownloadError()
47 |
48 | def createMenubar(self):
49 | """
50 | Sets up the menubar
51 | """
52 | bar = self.menuBar()
53 |
54 | # File Menu
55 | file = bar.addMenu('File')
56 |
57 | # New
58 | newmenu = file.addMenu('New')
59 | newmenu.addAction('New Codelist', lambda: self.CreateNewWindow(CodeList()))
60 | newmenu.addAction('New Code', lambda: self.CreateNewWindow(CodeEditor()))
61 |
62 | # Import menu
63 | imports = file.addMenu('Import')
64 | imports.addAction('Import Codelist', self.openCodelist)
65 | imports.addAction('Import Database', self.openDatabase)
66 |
67 | # Export menu
68 | exports = file.addMenu('Export All')
69 | self.optgct = exports.addAction('GCT', lambda: self.exportMultiple('gct'))
70 | self.opttxt = exports.addAction('TXT', lambda: self.exportMultiple('txt'))
71 | self.optini = exports.addAction('INI', lambda: self.exportMultiple('ini'))
72 | file.addSeparator()
73 |
74 | # Settings
75 | file.addAction('Options', lambda: SettingsWidget().exec_())
76 |
77 | # Exit
78 | file.addAction('Exit', self.close)
79 |
80 | # Window Menu
81 | ws = bar.addMenu('Windows')
82 |
83 | # Half menu
84 | half = ws.addMenu('Half')
85 | half.addAction('Left', Half)
86 | half.addAction('Right', lambda: Half(True))
87 | ws.addSeparator()
88 |
89 | # Tile menu
90 | tile = ws.addMenu('Tile')
91 | tile.addAction('Horizontal', TileHorizontal)
92 | tile.addAction('Vertical', TileVertical)
93 | ws.addSeparator()
94 |
95 | # Automatic arrangements
96 | ws.addAction('Arrange', self.mdi.tileSubWindows)
97 | ws.addAction('Cascade', self.mdi.cascadeSubWindows)
98 | ws.addSeparator()
99 |
100 | # Mass minimize/close
101 | ws.addAction('Minimize All', MinimizeAll)
102 | ws.addAction('Close All', CloseAll)
103 |
104 | # Update the menu
105 | self.updateboxes()
106 |
107 | def openDatabase(self):
108 | """
109 | Opens a dialog to let the user choose a database.
110 | """
111 | name = QtWidgets.QFileDialog.getOpenFileName(self, 'Open Database', '', 'Code Database (*.xml)')[0]
112 | if name:
113 | self.CreateNewWindow(Database(name))
114 |
115 | def openCodelist(self, source: QtWidgets.QTreeWidget = None, files: list = None):
116 | """
117 | Opens a QFileDialog to import a file
118 | """
119 | if not files:
120 | files = QtWidgets.QFileDialog.getOpenFileNames(self, 'Open Files', '',
121 | 'All supported formats (*.txt *.ini *.gct *.dol);;'
122 | 'Text File (*.txt);;'
123 | 'Dolphin INI (*.ini);;'
124 | 'Gecko Code Table (*.gct);;'
125 | 'Dolphin Executable (*.dol)')[0]
126 |
127 | # Run the correct function based on the chosen format
128 | for file in files:
129 | func = getattr(importing, 'Import' + os.path.splitext(file)[1].lstrip('.').upper(), None)
130 | if func:
131 | func(file, source)
132 |
133 | def exportList(self, source: QtWidgets.QTreeWidget):
134 | """
135 | Opens a QFileDialog to save a single codelist to a file.
136 | """
137 | file = QtWidgets.QFileDialog.getSaveFileName(self, 'Save Codelist To', source.gameID,
138 | 'Gecko Code Table (*.gct);;'
139 | 'Text File (*.txt);;'
140 | 'Dolphin INI (*.ini);;')[0]
141 |
142 | # Run the correct function based on the chosen format
143 | func = getattr(exporting, 'Export' + os.path.splitext(file)[1].lstrip('.').upper(), None)
144 | if func:
145 | success = func(file, source, False)
146 |
147 | # Inform the user
148 | if success:
149 | QtWidgets.QMessageBox.information(self, 'Export Complete', 'List exported succesfully!')
150 |
151 | def exportMultiple(self, ext: str):
152 | """
153 | Exports all the currently opened codelists at once to the given format. Filename defaults to the game id.
154 | """
155 | # Get destination and codelists
156 | dest = QtWidgets.QFileDialog.getExistingDirectory(self, 'Save all Codelists to', '', QtWidgets.QFileDialog.ShowDirsOnly)
157 | success = total = 0
158 | overwrite = permanent = False
159 |
160 | # Do the thing
161 | if dest:
162 | for window in filter(lambda x: isinstance(x.widget(), CodeList), self.mdi.subWindowList()):
163 |
164 | # Initialize vars
165 | filename = os.path.join(dest, '.'.join([window.widget().gameID, ext]))
166 | i = 2
167 |
168 | # If the file already exists, ask the user what to do
169 | if os.path.isfile(filename) and not permanent:
170 | msgbox = QtWidgets.QMessageBox(self)
171 | msgbox.setIcon(QtWidgets.QMessageBox.Question)
172 | msgbox.setWindowTitle('Overwrite file?')
173 | msgbox.setText(os.path.basename(filename) + ' already exists. Overwrite?')
174 | msgbox.setStandardButtons(QtWidgets.QMessageBox.YesToAll | QtWidgets.QMessageBox.Yes |
175 | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.NoToAll |
176 | QtWidgets.QMessageBox.Ignore)
177 | ret = msgbox.exec_()
178 |
179 | # If the user presses ignore, skip writing, else set the flags
180 | if ret == QtWidgets.QMessageBox.Ignore:
181 | continue
182 | overwrite = ret <= QtWidgets.QMessageBox.YesToAll
183 | permanent = ret == QtWidgets.QMessageBox.YesToAll or ret == QtWidgets.QMessageBox.NoToAll
184 |
185 | # If we don't want to overwrite, check that the file doesn't exist and if so bump up the number
186 | if not overwrite:
187 | while os.path.isfile(filename):
188 | filename = os.path.join(dest, '{}_{}.{}'.format(window.widget().gameID, i, ext))
189 | i += 1
190 |
191 | # Choose the correct function based on the provided extension
192 | func = getattr(exporting, 'Export' + ext.upper(), None)
193 | if func:
194 | success += func(filename, window.widget(), True)
195 | total += 1
196 |
197 | # Reset overwrite flag is permanent is not active
198 | if not permanent:
199 | overwrite = False
200 |
201 | # Inform the user
202 | if success:
203 | QtWidgets.QMessageBox.information(self, 'Export Complete', '{}/{} lists exported successfully!'.format(success, total))
204 |
205 | def updateboxes(self):
206 | """
207 | Looks for opened codelist sub-windows and adds them to each database' combo box.
208 | """
209 | # Initialize vars
210 | dblist = []
211 | entries = []
212 |
213 | # Fill the two lists
214 | for window in self.mdi.subWindowList():
215 | if isinstance(window.widget(), CodeList):
216 | entries.append(window.widget())
217 | else:
218 | dblist.append(window.widget())
219 |
220 | # Update the "Export All" option
221 | notempty = bool(entries)
222 | self.optgct.setEnabled(notempty)
223 | self.opttxt.setEnabled(notempty)
224 | self.optini.setEnabled(notempty)
225 |
226 | # Begin updating each combo box
227 | for window in dblist:
228 |
229 | # Process the combo box in reverse, so we can safely delete items without worrying about wrong indexes
230 | for i in reversed(range(1, window.Combox.count())):
231 | item = window.Combox.itemData(i)
232 | if item.parentWidget() not in self.mdi.subWindowList():
233 | window.Combox.removeItem(i)
234 | else:
235 | # Sometimes this fails, so i added an except because i'm lame
236 | try:
237 | entries.remove(item)
238 | except ValueError:
239 | continue
240 |
241 | # Add the remaining windows if they meet the condition
242 | for entry in entries:
243 | window.Combox.addItem(entry.windowTitle().lstrip('Codelist - '), entry) # Only keep game name and id
244 |
245 | def CodeLookup(self, item: QtWidgets.QTreeWidgetItem, codelist: QtWidgets.QTreeWidget, gid: str):
246 | """
247 | Looks for a possible match in opened windows with the same game id.
248 | """
249 | # Initialize vars
250 | wlist = [w.widget() for w in self.mdi.subWindowList() if isinstance(w.widget(), Database)
251 | or isinstance(w.widget(), CodeList) and w.widget().TreeWidget is not codelist]
252 | lsplt = re.split('[ \n]', item.text(1))
253 | totalen = len(lsplt)
254 |
255 | # Begin search!
256 | for widget in wlist:
257 |
258 | # Mark code matches from different game ids with an additional asterisk
259 | regmatch = int(not(bool(widget.gameID == gid))) + 1
260 |
261 | # Process the widget's tree
262 | for child in filter(lambda x: x.text(1) and 'Unknown Code' not in x.text(0),
263 | widget.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive)):
264 | matches = 0
265 |
266 | # For each code, check each line of the code we're looking a name for
267 | for line in lsplt:
268 | if line in child.text(1):
269 | matches += 1
270 |
271 | # If more than 2/3rds of the code match, we found the code we were looking for
272 | if matches / totalen >= 2 / 3:
273 | item.setText(0, child.text(0) + '*' * regmatch)
274 | item.setText(2, child.text(2)) # Copy comment
275 | item.setText(4, child.text(4)) # Copy author
276 | return
277 |
278 | def AddFromEditor(self, src: CodeEditor, dest: CodeList = None):
279 | """
280 | Transfers the code editor's content to a code in a codelist. If you're wondering why this is here, it's to
281 | prevent circular imports. Fuck circular imports.
282 | """
283 | # Initialize vars
284 | code = src.ParseCode()
285 | comment = re.sub('\n{2,}', '\n', src.CodeComment.toPlainText()) # Consecutive new lines can screw things up
286 | author = src.CodeAuthor.text()
287 |
288 | # Create a new codelist if dest is None
289 | if not dest:
290 | dest = self.CreateNewWindow(CodeList())
291 |
292 | # Save the stuff
293 | newitem = ModdedTreeWidgetItem(src.CodeName.text(), False, True)
294 | newitem.setText(1, code)
295 | newitem.setText(2, comment)
296 | newitem.setText(4, author)
297 |
298 | # Update the fields
299 | src.CodeContent.setPlainText(code)
300 | src.CodeComment.setPlainText(comment)
301 |
302 | # Update window title
303 | src.ParseAuthor(author)
304 |
305 | # Remove the dirt
306 | src.dirty = False
307 | src.setWindowTitle(src.windowTitle().lstrip('*'))
308 |
309 | # Add the item to the widget
310 | dest.TreeWidget.addTopLevelItem(newitem)
311 |
312 | def closeEvent(self, e: QtGui.QCloseEvent):
313 | """
314 | Overrides the close event to warn the user of opened lists/codes.
315 | """
316 | # Check if the warning is disabled and that we have any code list/editor open
317 | if not globalstuff.nowarn and len([w for w in self.mdi.subWindowList() if isinstance(w.widget(), CodeList) or isinstance(w.widget(), CodeEditor)]):
318 |
319 | # Raise awareness!
320 | msgbox = QtWidgets.QMessageBox(self)
321 | cb = QtWidgets.QCheckBox("Don't show this again")
322 | msgbox.setIcon(QtWidgets.QMessageBox.Question)
323 | msgbox.setWindowTitle('Opened Codes')
324 | msgbox.setText('Some codes are still open, are you sure you want to close?')
325 | msgbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
326 | msgbox.setCheckBox(cb)
327 | ret = msgbox.exec_()
328 |
329 | # Update warning disable parameter
330 | globalstuff.nowarn = bool(cb.checkState())
331 |
332 | # Act in accordance to the user's choice
333 | if ret == QtWidgets.QMessageBox.No:
334 | e.ignore()
335 | return
336 | e.accept()
337 |
338 | def CreateNewWindow(self, widget: QtWidgets.QWidget):
339 | win = ModdedSubWindow(isinstance(widget, CodeList))
340 | win.setWidget(widget)
341 | self.mdi.addSubWindow(win)
342 | self.updateboxes()
343 | win.show()
344 | return widget
345 |
346 |
347 | def main():
348 |
349 | # Load config
350 | config = configparser.ConfigParser()
351 | readconfig(config)
352 |
353 | # Start the application
354 | globalstuff.app = QtWidgets.QApplication(sys.argv)
355 | globalstuff.mainWindow = MainWindow()
356 |
357 | # Add the empty icon
358 | icon = QtGui.QPixmap(1, 1)
359 | icon.fill(Qt.transparent)
360 | globalstuff.empty = QtGui.QIcon(icon)
361 |
362 | # Open codelists passed through the shell
363 | flist = [file for file in sys.argv[1:] if os.path.isfile(file)]
364 | if flist:
365 | globalstuff.mainWindow.openCodelist(None, flist)
366 |
367 | # Apply theme if dark mode is enabled
368 | if globalstuff.theme == 'dark':
369 | SetDarkPalette()
370 |
371 | # Execute
372 | ret = globalstuff.app.exec_()
373 |
374 | # Update config
375 | writeconfig(config)
376 |
377 | # Quit the process
378 | sys.exit(ret)
379 |
380 |
381 | if __name__ == '__main__':
382 | main()
383 |
--------------------------------------------------------------------------------
/importing.py:
--------------------------------------------------------------------------------
1 | """
2 | This file contains multiple functions to import codelists.
3 | """
4 | import os
5 | import re
6 | from itertools import chain
7 | from typing import Optional, BinaryIO
8 | from struct import unpack
9 |
10 | from chardet import detect
11 | from PyQt5 import QtWidgets
12 | from PyQt5.Qt import Qt
13 |
14 | import globalstuff
15 | from common import GameIDMismatch, AssembleCode
16 | from codelist import CodeList
17 | from widgets import ModdedTreeWidgetItem
18 |
19 |
20 | def GameIDCheck(gameid: str, codelist: CodeList):
21 | """
22 | Checks if the game id matches the codelist's current one. If not, it alerts the user, asking them whether they
23 | want to continue importing or not.
24 | """
25 | if codelist.gameID != gameid:
26 | if codelist.gameID != 'UNKW00' and GameIDMismatch() == QtWidgets.QMessageBox.No:
27 | return False
28 | codelist.SetGameID(gameid.upper())
29 | return True
30 |
31 |
32 | def DoPreliminaryOperations(filename: str, codelist: Optional[CodeList]):
33 | """
34 | This function performs a couple preliminary operations before importing can take place. Very informative, i know.
35 | """
36 | # Check if we can read the file. If not, trigger an error message.
37 | if not os.access(filename, os.R_OK):
38 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'File Read Error', "Couldn't read file " + filename)
39 | return None
40 |
41 | # If the codelist param is not set, we want to create a new window, so do that
42 | if not codelist:
43 | return globalstuff.mainWindow.CreateNewWindow(CodeList())
44 | return codelist
45 |
46 |
47 | def ImportTXT(filename: str, codelist: CodeList):
48 | """
49 | Imports a TXT. This took longer than it should have.
50 | """
51 | # Perform the initial operations. If they fail, abort everything.
52 | codelist = DoPreliminaryOperations(filename, codelist)
53 | if not codelist:
54 | return
55 |
56 | # Initialize vars
57 | linerule = re.compile('^(\* )?[\w]{8} [\w]{8}$', re.I)
58 | unkcount = 1 # Used for codes without names
59 | currdepth = 0 # Current depth, used for sub-categories
60 | parents = {0: None} # This dict stores the parent for each level. Not the best solution, but it gets the job done.
61 |
62 | # Set the tree widget
63 | listwidget = codelist.TreeWidget
64 |
65 | # Open the file and read it
66 | with open(filename, 'rb') as f:
67 | rawdata = f.read()
68 |
69 | # Now that we read the file, detect its encoding and split it into groups (there's an empty line between each).
70 | # This is done because the original Code Manager saves in UTF-16, which would fuck up the formatting if not decoded.
71 | rawdata = rawdata.decode(detect(rawdata)['encoding'], 'ignore').split(os.linesep * 2)
72 |
73 | # The first group contains the gameid, so check it with regex and set it if it's valid
74 | gameid = rawdata[0].splitlines()[0].strip()
75 | if 4 <= len(gameid) <= 6:
76 | if not GameIDCheck(gameid, codelist):
77 | return
78 | rawdata.pop(0) # Remove the parsed group
79 |
80 | # Begin parsing codes
81 | for group in rawdata:
82 |
83 | # Initialize vars
84 | name = code = comment = author = ''
85 | isenabled = False
86 |
87 | # Parse group
88 | for line in group.splitlines():
89 | m = re.match(linerule, line)
90 |
91 | # It's a code line
92 | if m:
93 | if not isenabled and '*' in m[0]: # Asterisks are used to mark enabled codes, so mark it as such
94 | isenabled = True
95 | code = '\n'.join([code, m[0].lstrip('* ')])
96 |
97 | # It's not a code line
98 | else:
99 | if name: # We already have a name set, so add this line to the comment
100 | comment = '\n'.join([comment, line])
101 | else: # The code doesn't have a name yet, so set it to this line. Also check for the author name
102 | lspl = line.split(' [')
103 | name = lspl[0]
104 | if len(lspl) > 1:
105 | author = lspl[1].rstrip(']') # Remove the last character
106 |
107 | # Failsafe if the code name is fully empty
108 | if not name:
109 | name = 'Unknown Code '
110 | while listwidget.findItems(name + str(unkcount), Qt.MatchExactly):
111 | unkcount += 1
112 | name += str(unkcount)
113 |
114 | # If the name only contains "#" characters, it represents the end of a category, so don't add it to the tree
115 | if not name.lstrip('#'):
116 | currdepth = name.count('#') - 1
117 |
118 | # Else, create the tree item
119 | else:
120 | newitem = ModdedTreeWidgetItem(name.lstrip('#'), not(bool(code)), True)
121 |
122 | # If it's a category, set the depth and the parents key
123 | if not code:
124 | currdepth = name.count('#')
125 | parents[currdepth+1] = newitem
126 |
127 | # Otherwise, it's a code, so add the code, comment and author
128 | else:
129 | newitem.setText(1, code.lstrip('\n').upper()) # Force uppercase, because lowercase sucks.
130 | newitem.setText(2, comment.lstrip('\n'))
131 | newitem.setText(4, author)
132 |
133 | # If enabled, tick the check
134 | if isenabled:
135 | newitem.setCheckState(0, Qt.Checked)
136 |
137 | # If the name is unknown, look it up
138 | if 'Unknown Code' in newitem.text(0):
139 | globalstuff.mainWindow.CodeLookup(newitem, codelist, gameid)
140 |
141 | # Set the item's parent. If there's a key error, don't do anything. Gotta stay safe.
142 | try:
143 | parent = parents[currdepth]
144 | except KeyError:
145 | pass
146 |
147 | # Determine parenthood. Don't believe the warning! Currdepth is 0 even if all parent changes are skipped ;)
148 | if parent:
149 | parent.addChild(newitem)
150 | else:
151 | listwidget.addTopLevelItem(newitem)
152 |
153 | # Add 1 to depth, as children will be 1 level further down
154 | if not code:
155 | currdepth += 1
156 |
157 | # Finally, trigger the buttons in the codelist
158 | codelist.EnableButtons()
159 | codelist.UpdateLines()
160 |
161 |
162 | def ImportINI(filename: str, codelist: CodeList):
163 | """
164 | ImportTXT's uglier brother. Also, Dolphin is an asshole.
165 | """
166 | # Perform the initial operations. If they fail, abort everything.
167 | codelist = DoPreliminaryOperations(filename, codelist)
168 | if not codelist:
169 | return
170 |
171 | # Set the tree widget
172 | listwidget = codelist.TreeWidget
173 |
174 | # Set the gameID
175 | gameid = os.path.splitext(os.path.basename(filename))[0] # Remove the file extension
176 | if 4 <= len(gameid) <= 6 and not GameIDCheck(gameid, codelist):
177 | return
178 |
179 | # Open the file
180 | with open(filename) as f:
181 | rawdata = f.read().splitlines()
182 |
183 | # First, we have to find the sections containing the codes between all the file's sections
184 | length = len(rawdata)
185 | n = o = 0
186 | m = p = length # These will be set to the end of the file, in case there are no other sections than what we need
187 | for i, line in enumerate(rawdata, 1): # This starts from 1, in case of the first section being at index 0
188 | if line == '[Gecko]':
189 | n = i
190 | elif line == '[Gecko_Enabled]':
191 | o = i
192 | elif i < length - 1 and rawdata[i].startswith('['):
193 | """
194 | If the next line begins a section, set this line as the end of the current section, but with some limits:
195 | - If n > o, we're in the Gecko section. But if m is set, we're somewhere between them, so don't do anything
196 | - If n < o, we're in the Gecko_Enabled section. But if p is set, we're somewhere between them, so don't do anything
197 | - Finally, if n = o, it means we're in an unknown section, so don't do anything either.
198 | """
199 | if n > o and m == length:
200 | m = i
201 | elif n < o and p == length:
202 | p = i
203 |
204 | # We got the indexes, create the subsections. My palms are already sweating.
205 | gecko = rawdata[n:m]
206 | geckoenabled = rawdata[o:p]
207 |
208 | # The rest of the file won't be wasted! It will be stored so if the user exports the list as ini, this data will be
209 | # ported over.
210 | if n or p != length or m != o-1:
211 | scrap = '\n'.join(chain(rawdata[:n-1], rawdata[m:o-1], rawdata[p:]))
212 | if scrap:
213 | codelist.scrap = scrap
214 |
215 | # Initialize vars
216 | entrylist = []
217 | unkcount = 1
218 |
219 | # Parse the gecko section
220 | for line in gecko:
221 |
222 | # It's a code name, and code names need some extra parsing
223 | if line.startswith('$'):
224 |
225 | # First, we must exclude the author from the code name, as it will fuck up Gecko_Enabled otherwise
226 | lspl = line.split(' [')
227 | name = lspl[0].lstrip('$') # Remove the first character
228 |
229 | # Set the author name if present
230 | author = ''
231 | if len(lspl) > 1:
232 | author = lspl[1].rstrip(']') # Remove the last character
233 |
234 | # If the resulting name is empty, apply the following failsafe
235 | if not name:
236 | name = 'Unknown Code '
237 | while listwidget.findItems(name + str(unkcount), Qt.MatchExactly):
238 | unkcount += 1
239 | name += str(unkcount)
240 | unkcount += 1
241 |
242 | # Create the widget
243 | newitem = ModdedTreeWidgetItem(name, False, True)
244 | newitem.setText(4, author)
245 | entrylist.append(newitem)
246 |
247 | # It's a comment line. Not using "and" because the line would end up in the "else"
248 | elif line.startswith('*'):
249 | if len(line) > 1:
250 | newitem.setText(2, '\n'.join([newitem.text(2), line.lstrip('*')])) # Only add if the line is not empty
251 |
252 | # It's a code line
253 | else:
254 | newitem.setText(1, '\n'.join([newitem.text(1), line.upper()]))
255 |
256 | # Parse the geckoenabled section and add the newly created widgets to the codelist
257 | for item in entrylist:
258 |
259 | # Enable the check if the name matches
260 | if '$' + item.text(0) in geckoenabled:
261 | item.setCheckState(0, Qt.Checked)
262 |
263 | # Remove the extra newlines at the beginning of these two fields
264 | item.setText(1, item.text(1).lstrip('\n'))
265 | item.setText(2, item.text(2).lstrip('\n'))
266 |
267 | # Do code lookup if code doesn't have a name
268 | if 'Unknown Code' in item.text(0):
269 | globalstuff.mainWindow.CodeLookup(item, codelist, gameid)
270 |
271 | # Add to tree widget
272 | listwidget.addTopLevelItem(item)
273 |
274 | # Finally, trigger the buttons in the codelist
275 | codelist.EnableButtons()
276 | codelist.UpdateLines()
277 |
278 |
279 | def ImportGCT(filename: str, codelist: CodeList):
280 | """
281 | ImportTXT's siamese twins.
282 | """
283 | # Perform the initial operations. If they fail, abort everything.
284 | codelist = DoPreliminaryOperations(filename, codelist)
285 | if not codelist:
286 | return
287 |
288 | # Do the parsing
289 | with open(filename, 'rb') as f:
290 | if f.read(8) == globalstuff.gctmagic: # Check for the magic
291 | f.seek(-8, 2) # Go to the end of the file
292 |
293 | # If the "Codelist End" is at the end of the file, we have a regular GCT
294 | if f.read() == globalstuff.gctend:
295 | ParseGCT(os.path.splitext(os.path.basename(filename))[0], f, codelist)
296 |
297 | # Otherwise we have an extended GCT
298 | else:
299 | ParseExtendedGCT(f, codelist)
300 | else:
301 | # This ain't it, chief
302 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Invalid file', 'This file is invalid')
303 |
304 |
305 | def ParseExtendedGCT(f: BinaryIO, codelist: CodeList):
306 | """
307 | BrawlBox allows you to store code names and offsets in the GCT. So, this is for GCTs using that feature.
308 | """
309 | # Initialize vars
310 | backupoffset = 0
311 |
312 | # Set the tree widget
313 | listwidget = codelist.TreeWidget
314 |
315 | # First, let's get the file's length
316 | filelen = f.tell()
317 | f.seek(0)
318 |
319 | # Now, let's find the codelist end
320 | while f.tell() < filelen:
321 | if f.read(8) == globalstuff.gctend:
322 | f.seek(4, 1)
323 | backupoffset = f.tell() # Saving this for when i need to go back
324 | break
325 |
326 | # Failsafe time
327 | if f.tell() == filelen:
328 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Invalid file', 'This file is invalid')
329 | return
330 |
331 | # Now let's find the game id. Why -8 ?
332 | # First, the offset is according to the entry's beginning (aka the game name which was skipped)
333 | # Second, the seek needs to be re-adjusted due to the read operation
334 | f.seek(unpack('I', f.read(4))-8, 1)
335 |
336 | # Get the string
337 | gameid = ''
338 | while f.tell() < filelen:
339 | char = f.read(1)
340 | if char == b'\0':
341 | break
342 | gameid += char.decode('utf-8', 'ignore')
343 |
344 | # Verify the gameid's validity
345 | if 4 <= len(gameid) <= 6 and not GameIDCheck(gameid, codelist):
346 | return
347 |
348 | # Read the amount of codes
349 | f.seek(backupoffset) # Go back
350 | f.seek(4, 1)
351 | amount = unpack('I', f.read(4))
352 |
353 | # Begin reading codes!
354 | while amount > 0:
355 | # Read the offsets
356 | codeoffs = unpack('I', f.read(4))
357 | codelen = unpack('I', f.read(4))
358 | nameoffs = f.tell() + unpack('I', f.read(4)) - 8 # Offset starts at beginning of entry
359 | commentoffs = f.tell() + unpack('I', f.read(4)) - 12 # Same here
360 | if commentoffs < f.tell(): # If there's no comment the value is 0, so if we subtract 12 we'll be at a smaller offset
361 | commentoffs = 0
362 | backupoffset = f.tell()
363 |
364 | # Go to the code and read it
365 | f.seek(codeoffs)
366 | code = AssembleCode(f.read(codelen * 8).hex()) # Convert to hex string and add spaces and newlines
367 |
368 | # Go to the code name and read it
369 | codename = ''
370 | f.seek(nameoffs)
371 | while f.tell() < filelen:
372 | char = f.read(1)
373 | if char == b'\0':
374 | break
375 | codename += char.decode('utf-8', 'ignore')
376 |
377 | # Find the author inside the name
378 | lspl = codename.split(' [')
379 | codename = lspl[0]
380 | author = ''
381 | if len(lspl) > 1:
382 | author = lspl[1].rstrip(']') # Remove the last character
383 |
384 | # Go the comment and read it
385 | comment = ''
386 | if commentoffs:
387 | f.seek(commentoffs)
388 | while f.tell() < filelen:
389 | char = f.read(1)
390 | if char == b'\0':
391 | break
392 | comment += char.decode('utf-8', 'ignore')
393 |
394 | # Create the tree widget
395 | newitem = ModdedTreeWidgetItem(codename, False, True)
396 | newitem.setText(1, code)
397 | newitem.setText(2, comment)
398 | newitem.setText(4, author)
399 | listwidget.addTopLevelItem(newitem)
400 |
401 | # Go back to the offset we backed up earlier
402 | f.seek(backupoffset)
403 | amount -= 1
404 |
405 |
406 | def ParseGCT(filename: str, f: BinaryIO, codelist: CodeList):
407 | """
408 | This GCT parser is for the normal format. It tries to split codes according to the codetypes.
409 | """
410 | # Initialize vars
411 | currentcode = False
412 | amount = 0
413 | unkcount = 1
414 | finalist = []
415 |
416 | # Set the tree widget
417 | listwidget = codelist.TreeWidget
418 |
419 | # First, let's get the file's length
420 | filelen = f.tell() - 8 # Ignore the F0 line
421 | f.seek(8) # Go back to the beginning and skip the GCT magic
422 |
423 | # Verify the gameid's validity
424 | gameid = os.path.splitext(os.path.basename(filename))[0]
425 | if 4 <= len(gameid) <= 6 and not GameIDCheck(gameid, codelist):
426 | return
427 |
428 | # Begin reading the GCT!
429 | while f.tell() < filelen:
430 | # Read the next line and get its first byte
431 | line = f.read(8)
432 | c = int(hex(line[0]), 16)
433 |
434 | # If we are currently in a code
435 | if currentcode:
436 | # If we have exhausted the amount of lines specified or we meet an "E0" line, don't add anymore lines
437 | if amount == 0 or (amount == -1 and c == 0xE0):
438 | currentcode = False
439 | elif amount > 0:
440 | amount -= 1
441 |
442 | # Add the line. Yes PyCharm, i know newitem could be referenced before assignment, but currentcode is never
443 | # true when the loop begins, so shut the fuck up.
444 | newitem.setText(1, newitem.text(1) + line.hex())
445 |
446 | # It's a new code!
447 | else:
448 | # Set name
449 | name = 'Unknown Code '
450 | while listwidget.findItems(name + str(unkcount), Qt.MatchExactly):
451 | unkcount += 1
452 | name += str(unkcount)
453 | unkcount += 1
454 |
455 | # Create the tree widget item
456 | newitem = ModdedTreeWidgetItem(name, False, True)
457 | newitem.setText(1, line.hex())
458 | finalist.append(newitem)
459 |
460 | # Check the codetype. If the line isn't listed here, it will be added as a single line if found standalone.
461 | # Type 06 (length specified by code, in bytes)
462 | if c == 6 or c == 7:
463 | lines = int(line[7:].hex(), 16)
464 | amount = (lines + 7) // 8 - 1 # Add 7 to approximate up
465 | currentcode = True
466 |
467 | # Type 08 (fixed length)
468 | elif c == 8 or c == 9:
469 | currentcode = True
470 |
471 | # Type 20-2F, 40, 42, 48, 4A, A8-AE, F6 (add lines until we find an E0 line)
472 | elif 0x20 <= c <= 0x2F or c == 0x40 or c == 0x42 or c == 0x48 or c == 0x4A or 0xA8 <= c <= 0xAE or c == 0xF6:
473 | amount = -1
474 | currentcode = True
475 |
476 | # Type C0, C2, C4, F2/F4 (length specified by code, in lines)
477 | elif c == 0xC0 or 0xC2 <= c <= 0xC5 or 0xF2 <= c <= 0xF5:
478 | amount = int(line[7:].hex(), 16) - 1
479 | currentcode = True
480 |
481 | # Add spaces and newlines to the codes, then add the items to the tree
482 | for item in finalist:
483 | item.setText(1, AssembleCode(item.text(1)))
484 | globalstuff.mainWindow.CodeLookup(item, listwidget, filename)
485 | listwidget.addTopLevelItem(item)
486 |
487 |
488 | def ImportDOL(filename: str, codelist: CodeList):
489 | """
490 | The ImportGCT twins' older sister.
491 | """
492 | # Perform the initial operations. If they fail, abort everything.
493 | codelist = DoPreliminaryOperations(filename, codelist)
494 | if not codelist:
495 | return
496 |
497 | # Initialize vars
498 | sections = []
499 |
500 | # Do the parsing
501 | with open(filename, 'rb') as f:
502 | # Get the entrypoint
503 | f.seek(0xE0)
504 | entrypoint = int(f.read(4).hex(), 16)
505 |
506 | # Go to the text sections' loading address. The one with the same address as the entrypoint usually contains the
507 | # codehandler+gct. But other custom code might override this, so as an additional check for 0x80001800 is made
508 | f.seek(0x48)
509 | for i in range(7):
510 | secmem = int(f.read(4).hex(), 16)
511 | if secmem == entrypoint or secmem == 0x80001800:
512 | sections.append(i)
513 |
514 | # If there are no matches, it means there's no codes here for us to find
515 | if not sections:
516 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Empty DOL', 'No GCTs were found in this file')
517 | return
518 |
519 | for section in sections:
520 | # Get the section offset and length
521 | f.seek(section * 4)
522 | sectionoffset = int(f.read(4).hex(), 16)
523 | f.seek(0x90 + section * 4)
524 | sectionend = sectionoffset + int(f.read(4).hex(), 16)
525 |
526 | # Initialize vars
527 | shouldadd = False
528 | buffer = globalstuff.gctmagic
529 |
530 | # Read the section
531 | f.seek(sectionoffset)
532 | while f.tell() < sectionend:
533 | # Read file
534 | bytez = f.read(8)
535 |
536 | # Check for the gct EOF, else add to the buffer if we found the magic
537 | if shouldadd:
538 | buffer += bytez
539 | if bytez == globalstuff.gctend:
540 | break
541 |
542 | # Found the GCT magic, start reading from here
543 | elif bytez == globalstuff.gctmagic:
544 | shouldadd = True
545 |
546 | # Skip the parsing if we didn't find anything
547 | if len(buffer) == 8:
548 | continue
549 |
550 | # Write the buffer to a temporary file, then feed it to the GCT parser
551 | with open('tmp.gct', 'wb+') as g:
552 | g.write(buffer)
553 | ParseGCT('tmp.gct', g, codelist)
554 |
555 | # Remove the file
556 | os.remove('tmp.gct')
557 | return # We're assuming there is only one GCT here. Who in their right mind would add more than one?!
558 |
559 | # This is only shown if nothing is found, as otherwise the function would have already returned
560 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Empty DOL', 'No GCTs were found in this file')
561 |
--------------------------------------------------------------------------------