diff --git a/INSTALL.rst b/INSTALL.rst index fe8d9b8aa..00d98ae35 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -2,7 +2,7 @@ Requirements ============ To run PyFFI's graphical file editor QSkope, you need -`PyQt4 `_. +`PyQt6 `_. Using the Windows installer =========================== diff --git a/README.rst b/README.rst index 43a453883..216d00c03 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ Python library for processing block structured binary files: * **Batteries included:** Many tools for files used by 3D games, such as optimizers, stripifier, tangent space calculator, 2d/3d hull algorithms, inertia calculator, as well as a general purpose file - editor QSkope (using `PyQt4 + editor QSkope (using `PyQt6 `_), are included. diff --git a/pyffi/formats/cgf/__init__.py b/pyffi/formats/cgf/__init__.py index 353509d36..91c8943df 100644 --- a/pyffi/formats/cgf/__init__.py +++ b/pyffi/formats/cgf/__init__.py @@ -716,7 +716,7 @@ def read(self, stream): # is it a caf file? these are missing chunk headers on controllers # (note: stream.name may not be a python string for some file - # implementations, notably PyQt4, so convert it explicitely) + # implementations, notably PyQt6, so convert it explicitely) is_caf = (str(stream.name)[-4:].lower() == ".caf") chunk_types = [ diff --git a/pyffi/qskope/__init__.py b/pyffi/qskope/__init__.py index e65090c3d..a922918f2 100644 --- a/pyffi/qskope/__init__.py +++ b/pyffi/qskope/__init__.py @@ -37,7 +37,7 @@ # # ***** END LICENSE BLOCK ***** -from PyQt4 import QtGui, QtCore +from PyQt6 import QtGui, QtCore , QtWidgets import pyffi.qskope.global_model import pyffi.qskope.detail_model @@ -61,22 +61,22 @@ # implementation details: # http://doc.trolltech.com/4.3/qmainwindow.html#details -class QSkope(QtGui.QMainWindow): +class QSkope(QtWidgets.QMainWindow): """Main QSkope window.""" def __init__(self, parent = None): """Initialize the main window.""" - QtGui.QMainWindow.__init__(self, parent) + QtWidgets.QMainWindow.__init__(self, parent) # set up the menu bar self.createActions() self.createMenus() # set up the global model view - self.globalWidget = QtGui.QTreeView() + self.globalWidget = QtWidgets.QTreeView() self.globalWidget.setAlternatingRowColors(True) # set up the detail model view - self.detailWidget = QtGui.QTreeView() + self.detailWidget = QtWidgets.QTreeView() self.detailDelegate = pyffi.qskope.detail_delegate.DetailDelegate() self.detailWidget.setItemDelegate(self.detailDelegate) self.detailWidget.setAlternatingRowColors(True) @@ -84,12 +84,10 @@ def __init__(self, parent = None): # connect global with detail: # if object is selected in global view, then show its details in the # detail view - QtCore.QObject.connect(self.globalWidget, - QtCore.SIGNAL("clicked(const QModelIndex &)"), - self.setDetailModel) + self.globalWidget.clicked.connect(self.setDetailModel) # set up the central widget - self.splitter = QtGui.QSplitter() + self.splitter = QtWidgets.QSplitter() self.splitter.addWidget(self.globalWidget) self.splitter.addWidget(self.detailWidget) self.setCentralWidget(self.splitter) @@ -114,42 +112,30 @@ def createActions(self): # open a file self.openAct = QtGui.QAction("&Open", self) self.openAct.setShortcut("Ctrl+O") - QtCore.QObject.connect(self.openAct, - QtCore.SIGNAL("triggered()"), - self.openAction) + self.openAct.triggered.connect(self.openAction) # save a file self.saveAct = QtGui.QAction("&Save", self) self.saveAct.setShortcut("Ctrl+S") - QtCore.QObject.connect(self.saveAct, - QtCore.SIGNAL("triggered()"), - self.saveAction) + self.saveAct.triggered.connect(self.saveAction) # save a file as ... self.saveAsAct = QtGui.QAction("Save As...", self) self.saveAsAct.setShortcut("Ctrl+Shift+S") - QtCore.QObject.connect(self.saveAsAct, - QtCore.SIGNAL("triggered()"), - self.saveAsAction) + self.saveAsAct.triggered.connect(self.saveAsAction) # exit self.exitAct = QtGui.QAction("E&xit", self) self.exitAct.setShortcut("Ctrl+Q") - QtCore.QObject.connect(self.exitAct, - QtCore.SIGNAL("triggered()"), - QtGui.qApp.quit) + self.exitAct.triggered.connect(QtWidgets.QApplication.quit) # tell something about QSkope self.aboutQSkopeAct = QtGui.QAction("About QSkope", self) - QtCore.QObject.connect(self.aboutQSkopeAct, - QtCore.SIGNAL("triggered()"), - self.aboutQSkopeAction) + self.aboutQSkopeAct.triggered.connect(self.aboutQSkopeAction) # tell something about Qt self.aboutQtAct = QtGui.QAction("About Qt", self) - QtCore.QObject.connect(self.aboutQtAct, - QtCore.SIGNAL("triggered()"), - QtGui.qApp.aboutQt) + self.aboutQtAct.triggered.connect(QtWidgets.QApplication.aboutQt) # implementation details: # http://doc.trolltech.com/4.3/mainwindows-menus.html @@ -172,7 +158,7 @@ def closeEvent(self, event): """Called when the application is closed. Saves the settings.""" settings = self.getSettings(versioned = True) settings.setValue("MainWindow/geometry", self.saveGeometry()) - QtGui.QMainWindow.closeEvent(self, event) + QtWidgets.QMainWindow.closeEvent(self, event) # @@ -294,7 +280,7 @@ def openAction(self): """Open a file.""" # wrapper around openFile # (displays an extra file dialog) - filename = QtGui.QFileDialog.getOpenFileName(self, "Open File") + filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open File") if filename: self.openFile(filename = filename) @@ -302,7 +288,7 @@ def saveAsAction(self): """Save a file.""" # wrapper around saveAction # (displays an extra file dialog) - filename = QtGui.QFileDialog.getSaveFileName(self, "Save File") + filename = QtWidgets.QFileDialog.getSaveFileName(self, "Save File") if filename: self.fileName = filename self.saveAction() @@ -317,7 +303,7 @@ def saveAction(self): def aboutQSkopeAction(self): """Display an information window about QSkope.""" # create the box - mbox = QtGui.QMessageBox(self) + mbox = QtWidgets.QMessageBox(self) # set window title and window text mbox.setWindowTitle("About QSkope") mbox.setText(""" @@ -336,4 +322,4 @@ def aboutQSkopeAction(self): PyFFI Github Releases page.""" % pyffi.__version__) # display the window - mbox.exec_() + mbox.exec() diff --git a/pyffi/qskope/detail_delegate.py b/pyffi/qskope/detail_delegate.py index 0b341101d..35748e93c 100644 --- a/pyffi/qskope/detail_delegate.py +++ b/pyffi/qskope/detail_delegate.py @@ -37,7 +37,7 @@ # # ***** END LICENSE BLOCK ***** -from PyQt4 import QtCore, QtGui +from PyQt6 import QtCore, QtGui, QtWidgets # each delegate type corresponds to a QtGui delegate type # (see _checkValidEditor for more details) @@ -50,7 +50,7 @@ # implementation details: # http://doc.trolltech.com/4.3/model-view-delegate.html # http://doc.trolltech.com/4.3/qitemdelegate.html#details -class DetailDelegate(QtGui.QItemDelegate): +class DetailDelegate(QtWidgets.QItemDelegate): """Defines an editor for data in the detail view.""" def _checkValidEditor(self, data, editor): @@ -78,19 +78,19 @@ def _checkValidEditor(self, data, editor): # (some combo types may also derive from spin box such as bools, # in that case prefer the combo box representation) if isinstance(data, EditableComboBox): - isvalid = isinstance(editor, QtGui.QComboBox) + isvalid = isinstance(editor, QtWidgets.QComboBox) # check float spin box elif isinstance(data, EditableFloatSpinBox): - isvalid = isinstance(editor, QtGui.QDoubleSpinBox) + isvalid = isinstance(editor, QtWidgets.QDoubleSpinBox) # check spin box elif isinstance(data, EditableSpinBox): - isvalid = isinstance(editor, QtGui.QSpinBox) + isvalid = isinstance(editor, QtWidgets.QSpinBox) # check text editor elif isinstance(data, EditableTextEdit): - isvalid = isinstance(editor, QtGui.QTextEdit) + isvalid = isinstance(editor, QtWidgets.QTextEdit) # check line editor elif isinstance(data, EditableLineEdit): - isvalid = isinstance(editor, QtGui.QLineEdit) + isvalid = isinstance(editor, QtWidgets.QLineEdit) else: # data has no delegate class, which is classified as invalid isvalid = False @@ -112,7 +112,7 @@ def createEditor(self, parent, option, index): # (see _checkValidEditor for the correct delegate preference order) if isinstance(node, EditableComboBox): # a general purpose combo box - editor = QtGui.QComboBox(parent) + editor = QtWidgets.QComboBox(parent) for key in node.get_editor_keys(): editor.addItem(key) elif isinstance(node, EditableFloatSpinBox): @@ -123,17 +123,17 @@ def createEditor(self, parent, option, index): editor.setDecimals(node.get_editor_decimals()) elif isinstance(node, EditableSpinBox): # an integer spin box - editor = QtGui.QSpinBox(parent) + editor = QtWidgets.QSpinBox(parent) editor.setMinimum(node.get_editor_minimum()) # work around a qt "bug": maximum must be C type "int" # so cannot be larger than 0x7fffffff editor.setMaximum(min(node.get_editor_maximum(), 0x7fffffff)) elif isinstance(node, EditableTextEdit): # a text editor - editor = QtGui.QTextEdit(parent) + editor = QtWidgets.QTextEdit(parent) elif isinstance(node, EditableLineEdit): # a line editor - editor = QtGui.QLineEdit(parent) + editor = QtWidgets.QLineEdit(parent) else: return None # check validity @@ -195,5 +195,5 @@ def setModelData(self, editor, model, index): return # set the model data # EditRole ensures that setData uses set_editor_value to set the data - model.setData(index, editorvalue, QtCore.Qt.EditRole) + model.setData(index, editorvalue, QtCore.Qt.ItemDataRole.EditRole) diff --git a/pyffi/qskope/detail_model.py b/pyffi/qskope/detail_model.py index 552e080e5..96c4869d4 100644 --- a/pyffi/qskope/detail_model.py +++ b/pyffi/qskope/detail_model.py @@ -38,7 +38,7 @@ # # ***** END LICENSE BLOCK ***** -from PyQt4 import QtCore +from PyQt6 import QtCore from pyffi.utils.graph import EdgeFilter, GlobalNode from pyffi.qskope.detail_tree import DetailTreeItem, DetailTreeItemData @@ -81,9 +81,9 @@ def flags(self, index): """Return flags for the given index: all indices are enabled and selectable.""" if not index.isValid(): - return QtCore.Qt.ItemFlags() + return QtCore.Qt.ItemFlag() # all items are enabled and selectable - flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + flags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable # determine whether item value can be set if index.column() == self.COL_VALUE: try: @@ -94,8 +94,8 @@ def flags(self, index): except NotImplementedError: pass else: - flags |= QtCore.Qt.ItemIsEditable - return QtCore.Qt.ItemFlags(flags) + flags |= QtCore.Qt.ItemFlag.ItemIsEditable + return QtCore.Qt.ItemFlag(flags) def data(self, index, role): """Return the data of model index in a particular role. Only @@ -103,7 +103,7 @@ def data(self, index, role): """ # check if the index is valid # check if the role is supported - if not index.isValid() or role != QtCore.Qt.DisplayRole: + if not index.isValid() or role != QtCore.Qt.ItemDataRole.DisplayRole: return None # get the data for display item = index.internalPointer() @@ -142,8 +142,8 @@ def data(self, index, role): def headerData(self, section, orientation, role): """Return header data.""" - if (orientation == QtCore.Qt.Horizontal - and role == QtCore.Qt.DisplayRole): + if (orientation == QtCore.Qt.Orientation.Horizontal + and role == QtCore.Qt.ItemDataRole.DisplayRole): if section == self.COL_TYPE: return "Type" elif section == self.COL_NAME: @@ -194,9 +194,9 @@ def parent(self, index): def setData(self, index, value, role): """Set data of a given index from given QVariant value. Only - QtCore.Qt.EditRole is implemented. + QtCore.Qt.ItemDataRole.EditRole is implemented. """ - if role == QtCore.Qt.EditRole: + if role == QtCore.Qt.ItemDataRole.EditRole: # fetch the current data, as a regular Python type node = index.internalPointer().data.node currentvalue = node.get_value() @@ -224,8 +224,7 @@ def setData(self, index, value, role): # set the value (EditRole, so use set_editor_value, not set_value) node.set_editor_value(pyvalue) # tell everyone that the data has changed - self.emit(QtCore.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), - index, index) + self.dataChanged.emit(index,index, []) return True # all other cases: failed return False diff --git a/pyffi/qskope/global_model.py b/pyffi/qskope/global_model.py index 7ddfbf3dd..bba53ff78 100644 --- a/pyffi/qskope/global_model.py +++ b/pyffi/qskope/global_model.py @@ -38,9 +38,9 @@ # # ***** END LICENSE BLOCK ***** -from collections import MutableMapping +from collections.abc import MutableMapping -from PyQt4 import QtGui, QtCore +from PyQt6 import QtCore from pyffi.utils.graph import EdgeFilter from pyffi.qskope.global_tree import GlobalTreeItemData, GlobalTreeItem @@ -76,7 +76,7 @@ def __delitem__(self, key): # index becomes available self.free_indices.append(self.data[id(key)]) # remove it - del self.data[id(key)] + del self.data[id(key)] def clear(self): # all indices larger than the first element @@ -107,7 +107,7 @@ def updateIndexDict(self, item): self.index_dict[item.data.node] for child_item in item.children: self.updateIndexDict(child_item) - + def flags(self, index): """Return flags for the given index: all indices are enabled and @@ -115,19 +115,19 @@ def flags(self, index): # all items are selectable # they are enabled if their edge_type is active if not index.isValid(): - return QtCore.Qt.ItemFlags() + return QtCore.Qt.ItemFlag() item = index.internalPointer() if item.edge_type.active: - flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + flags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable else: - flags = QtCore.Qt.ItemIsSelectable - return QtCore.Qt.ItemFlags(flags) + flags = QtCore.Qt.ItemFlag.ItemIsSelectable + return QtCore.Qt.ItemFlag(flags) def data(self, index, role): """Return the data of model index in a particular role.""" # check if the index is valid # check if the role is supported - if not index.isValid() or role != QtCore.Qt.DisplayRole: + if not index.isValid() or role != QtCore.Qt.ItemDataRole.DisplayRole: return None # get the data for display data = index.internalPointer().data @@ -146,8 +146,8 @@ def data(self, index, role): def headerData(self, section, orientation, role): """Return header data.""" - if (orientation == QtCore.Qt.Horizontal - and role == QtCore.Qt.DisplayRole): + if (orientation == QtCore.Qt.Orientation.Horizontal + and role == QtCore.Qt.ItemDataRole.DisplayRole): if section == self.COL_TYPE: return "Type" elif section == self.COL_NAME: diff --git a/scripts/qskope.py b/scripts/qskope.py index e5ff81265..dc4867c8c 100755 --- a/scripts/qskope.py +++ b/scripts/qskope.py @@ -41,11 +41,11 @@ import logging -# check if PyQt4 is installed +# check if PyQt6 is installed try: - from PyQt4 import QtGui + from PyQt6 import QtWidgets except ImportError: - input("""PyQt4 not found. Please download and install from + input("""PyQt6 not found. Please download and install from http://www.riverbankcomputing.co.uk/software/pyqt/download""") raise @@ -74,12 +74,12 @@ def main(): parser.error("incorrect number of arguments (one at most)") # run the application - app = QtGui.QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) mainwindow = QSkope() if len(args) >= 1: mainwindow.openFile(filename = args[0]) mainwindow.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) if __name__ == "__main__": # set up logger