diff --git a/MANIFEST.in b/MANIFEST.in index 239e346f..79543a81 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ graft preditor/resource graft preditor/dccs recursive-include preditor *.ui +recursive-exclude tests * diff --git a/README.md b/README.md index 974f4e12..3e59abad 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,13 @@ a better `parent_callback`. ## Installing Qt PrEditor is built on Qt, but uses [Qt.py](https://github.com/mottosso/Qt.py) so -you can choose to use PySide2 or PyQt5. We have elected to not directly depend -on either of these packages as if you want to use PrEditor inside of a an existing -application like Maya or Houdini, they already come with PySide2 installed. If -you are using it externally, add them to your pip install command. +you can choose to use PySide6, PySide2, PyQt6 or PyQt5. We have elected to not +directly depend on either of these packages so that you can use PrEditor inside +of existing applications like Maya or Houdini that already come with PySide +installed. If you are using it externally add them to your pip install command. -- PySide2: `pip install preditor PySide2` -- PyQt5: `pip install preditor PyQt5` +- PySide6: `pip install preditor PySide6` +- PyQt6: `pip install preditor PyQt6` ## Cli @@ -125,35 +125,68 @@ this is only useful for windows. ## QScintilla workbox The more mature QScintilla workbox requires a few extra dependencies that must -be passed manually. It hasn't been added to `extras_require` because we plan to -split it into its own pip module due to it requiring PyQt5 which is a little hard -to get working inside of DCC's that ship with PySide2 by default. Here is the -python 3 pip install command. +be passed manually. We have added it as pip `optional-dependencies`. QScintilla +only works with PyQt5/6 and it is a little hard to get PyQt working inside of +DCC's that ship with PySide2/6 by default. Here is the python 3 pip install command. -- `pip install preditor PyQt5, QScintilla>=2.11.4 aspell-python-py3` +- PyQt6: `pip install preditor[qsci6] PyQt6, aspell-python-py3` +- PyQt5: `pip install preditor[qsci5] PyQt5, aspell-python-py3` The aspell-python-py3 requirement is optional to enable spell check. +You may need to set the `QT_PREFERRED_BINDING` or `QT_PREFERRED_BINDING_JSON` +[environment variable](https://github.com/mottosso/Qt.py?tab=readme-ov-file#override-preferred-choice) to ensure that PrEditor can use PyQt5/PyQt6. # DCC Integration -## Maya - -PrEditor is pre-setup to use as a Maya module. To use it, create a virtualenv -with the same python as maya, or install it using mayapy. - -``` -virtualenv venv_preditor -venv_preditor\Scripts\activate -pip install PrEditor -set MAYA_MODULE_PATH=c:\path\to\venv_preditor\Lib\site-packages\preditor\dccs -``` -Note: Due to how maya .mod files works if you are using development installs you -can't use pip editable installs. This is due to the relative path used -`PYTHONPATH +:= ../..` in `PrEditor_maya.mod`. You can modify that to use a hard -coded file path for testing, or add a second .mod file to add the virtualenv's -`site-packages` file path as a hard coded file path. - +Here are several example integrations for DCC's included in PrEditor. These +require some setup to manage installing all pip requirements. These will require +you to follow the [Setup](#setup) instructions below. + +- [Maya](/preditor/dccs/maya/README.md) +- [3ds Max](/preditor/dccs/studiomax/README.md) + +If you are using hab, you can simply add the path to the [preditor](/preditor) folder to your site's `distro_paths`. [See .hab.json](/preditor/dccs/.hab.json) + +## Setup + +PrEditor has many python pip requirements. The easiest way to get access to all +of them inside an DCC is to create a virtualenv and pip install the requirements. +You can possibly use the python included with DCC(mayapy), but this guide covers +using a system install of python. + +1. Identify the minor version of python that the dcc is using. Running `sys.version_info[:2]` in the DCC returns the major and minor version of python. +2. Download and install the required version of python. Note, you likely only need to match the major and minor version of python(3.11 not 3.11.12). It's recommended that you don't use the windows store to install python as it has had issues when used to create virtualenvs. +3. Create a virtualenv using that version of python. On windows you can use `py.exe -3.11` or call the correct python.exe file. Change `-3.11` to match the major and minor version returned by step 1. Note that you should create separate venvs for a given python minor version and potentially for minor versions of Qt if you are using PyQt. + ```batch + cd c:\path\to\venv\parent + py -3.11 -m virtualenv preditor_311 + ``` +4. Use the newly created pip exe to install PrEditor and its dependencies. + * This example shows using PySide and the simple TextEdit workbox in a minimal configuration. + ```batch + c:\path\to\venv\parent\preditor_311\Scripts\pip install PrEditor + ``` + * This example shows using QScintilla in PyQt6 for a better editing experience. Note that you need to match the PyQt version used by the DCC, This may require matching the exact version of PyQt. + ```batch + c:\path\to\venv\parent\preditor_311\Scripts\pip install PrEditor[qsci6] PyQt6==6.5.3 + ``` + +### Editable install + +You should skip this section unless you want to develop PrEditor's code from an git repo using python's editable pip install. + +Due to how editable installs work you will need to set an environment variable +specifying the site-packages directory of the virtualenv you created in the +previous step. On windows this should be the `lib\site-packages` folder inside +of the venv you just created. Store this in the `PREDITOR_SITE`, this can be done +permanently or temporarily(via `set "PREDITOR_SITE=c:\path\to\venv\parent\preditor_311\lib\site-packages"`). + +This is required because you are going to use the path to your git repo's preditor +folder in the module/plugin loading methods for the the DCC you are using, but +there is no way to automatically find the virtualenv that your random git repo +is installed in. In fact, you may have have your git repo installed into multiple +virtualenvs at once. # Plugins diff --git a/preditor/__init__.py b/preditor/__init__.py index 75f6a86a..f7083083 100644 --- a/preditor/__init__.py +++ b/preditor/__init__.py @@ -184,7 +184,10 @@ def launch(run_workbox=False, app_id=None, name=None, standalone=False): # it regain focus. widget.activateWindow() widget.raise_() - widget.setWindowState(widget.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) + widget.setWindowState( + widget.windowState() & ~Qt.WindowState.WindowMinimized + | Qt.WindowState.WindowActive + ) widget.console().setFocus() app.start() diff --git a/preditor/about_module.py b/preditor/about_module.py index 8223211c..99c27975 100644 --- a/preditor/about_module.py +++ b/preditor/about_module.py @@ -111,10 +111,14 @@ def text(self): pass # Add info for all Qt5 bindings that have been imported - if 'PyQt5.QtCore' in sys.modules: - ret.append('PyQt5: {}'.format(sys.modules['PyQt5.QtCore'].PYQT_VERSION_STR)) + if 'PySide6.QtCore' in sys.modules: + ret.append('PySide6: {}'.format(sys.modules['PySide6.QtCore'].qVersion())) + if 'PyQt6.QtCore' in sys.modules: + ret.append('PyQt6: {}'.format(sys.modules['PyQt6.QtCore'].PYQT_VERSION_STR)) if 'PySide2.QtCore' in sys.modules: ret.append('PySide2: {}'.format(sys.modules['PySide2.QtCore'].qVersion())) + if 'PyQt5.QtCore' in sys.modules: + ret.append('PyQt5: {}'.format(sys.modules['PyQt5.QtCore'].PYQT_VERSION_STR)) # Add qt library paths for plugin debugging for i, path in enumerate(QtCore.QCoreApplication.libraryPaths()): diff --git a/preditor/dccs/.hab.json b/preditor/dccs/.hab.json new file mode 100644 index 00000000..4113ff3a --- /dev/null +++ b/preditor/dccs/.hab.json @@ -0,0 +1,10 @@ +{ + "name": "preditor", + "version": "0.0.dev0", + "environment": { + "append": { + "MAYA_MODULE_PATH": "{relative_root}/maya", + "ADSK_APPLICATION_PLUGINS": "{relative_root}/studiomax" + } + } +} diff --git a/preditor/dccs/maya/PrEditor_maya.mod b/preditor/dccs/maya/PrEditor_maya.mod index 8a8016d4..187c05f8 100644 --- a/preditor/dccs/maya/PrEditor_maya.mod +++ b/preditor/dccs/maya/PrEditor_maya.mod @@ -1,2 +1 @@ + PrEditor DEVELOPMENT . -PYTHONPATH +:= ../.. diff --git a/preditor/dccs/maya/README.md b/preditor/dccs/maya/README.md new file mode 100644 index 00000000..d1ab8a36 --- /dev/null +++ b/preditor/dccs/maya/README.md @@ -0,0 +1,22 @@ +# Maya Integration + +This is an example of using an Maya module to add PrEditor into Maya. This adds +a PrEditor menu with a PrEditor action in Maya's menu bar letting you open PrEditor. It +adds the excepthook so if a python exception is raised it will prompt the user +to show PrEditor. PrEditor will show all python stdout/stderr output generated +after the plugin is loaded. + +# Setup + +Make sure to follow these [setup instructions](/preditor/README.md#Setup) first to create the virtualenv. + +Alternatively you can use [myapy's](https://help.autodesk.com/view/MAYAUL/2026/ENU/?guid=GUID-72A245EC-CDB4-46AB-BEE0-4BBBF9791627) pip to install the requirements, but a +separate virtualenv is recommended. This method should not require setting the +`PREDITOR_SITE` environment variable even if you use an editable install. + +# Use + +The [preditor/dccs/maya](/preditor/dccs/maya) directory is setup as a Maya Module. To load it in +maya add the full path to that directory to the `MAYA_MODULE_PATH` environment +variable. You can use `;` on windows and `:` on linux to join multiple paths together. +You will need to enable auto load for the `PrEditor_maya.py` plugin. diff --git a/preditor/dccs/maya/plug-ins/PrEditor_maya.py b/preditor/dccs/maya/plug-ins/PrEditor_maya.py index ec993855..702417a5 100644 --- a/preditor/dccs/maya/plug-ins/PrEditor_maya.py +++ b/preditor/dccs/maya/plug-ins/PrEditor_maya.py @@ -1,5 +1,9 @@ from __future__ import absolute_import +import os +import site +from pathlib import Path + import maya.mel from maya import OpenMayaUI, cmds @@ -35,12 +39,39 @@ def launch(ignored): return widget +def update_site(): + """Adds a site dir to python. This makes its contents importable to python. + + This includes making any editable installs located in that site-packages folder + accessible to this python instance. This does not activate the virtualenv. + + If the env var `PREDITOR_SITE` is set, this path is used. Otherwise the + parent directory of preditor is used. + + - `PREDITOR_SITE` is useful if you want to use an editable install of preditor + for development. This should point to a virtualenv's site-packages folder. + - Otherwise if the virtualenv has a regular pip install of preditor you can + skip setting the env var. + """ + venv_path = os.getenv("PREDITOR_SITE") + # If the env var is not defined then use the parent dir of this preditor package. + if venv_path is None: + venv_path = cmds.moduleInfo(moduleName="PrEditor", path=True) + venv_path = Path(venv_path).parent.parent.parent + venv_path = str(venv_path) + + print(f"Preditor is adding python site: {venv_path}") + site.addsitedir(venv_path) + + def initializePlugin(mobject): # noqa: N802 """Initialize the script plug-in""" global preditor_menu # If running headless, there is no need to build a gui and create the python logger if not headless(): + update_site() + from Qt.QtWidgets import QApplication import preditor @@ -67,7 +98,7 @@ def initializePlugin(mobject): # noqa: N802 gmainwindow = maya.mel.eval("$temp1=$gMainWindow") preditor_menu = cmds.menu(label="PrEditor", parent=gmainwindow, tearOff=True) cmds.menuItem( - label="Show", + label="PrEditor", command=launch, sourceType="python", image=preditor.resourcePath('img/preditor.png'), diff --git a/preditor/dccs/studiomax/PackageContents.xml b/preditor/dccs/studiomax/PackageContents.xml new file mode 100644 index 00000000..3e29c439 --- /dev/null +++ b/preditor/dccs/studiomax/PackageContents.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr b/preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr new file mode 100644 index 00000000..3e8705eb --- /dev/null +++ b/preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr @@ -0,0 +1,8 @@ +macroScript PrEditor_Show +category:"PrEditor" +tooltip:"PrEditor..." +IconName:"preditor.ico" +( + local preditor = python.import "preditor" + preditor.launch() +) diff --git a/preditor/dccs/studiomax/README.md b/preditor/dccs/studiomax/README.md new file mode 100644 index 00000000..5b0fe7e2 --- /dev/null +++ b/preditor/dccs/studiomax/README.md @@ -0,0 +1,17 @@ +# 3ds Max Integration + +This is an example of using an Autodesk Application Package to load PrEditor into +3ds Max. This adds a PrEditor item to the Scripting menu in 3ds Max's menu bar +to show PrEditor. It adds the excepthook so if a python exception is raised +it will prompt the user to show PrEditor. PrEditor will show all python stdout/stderr +output generated after the plugin is loaded. + +# Setup + +Make sure to follow these [setup instructions](/preditor/README.md#Setup) first to create the virtualenv. + +# Use + +The [preditor/dccs/studiomax](/preditor/dccs/studiomax) directory is setup as a 3ds Max Application Plugin. +To load it in 3ds Max add the full path to that directory to the `ADSK_APPLICATION_PLUGINS` environment +variable. You can use `;` on windows and `:` on linux to join multiple paths together. diff --git a/preditor/dccs/studiomax/preditor.ms b/preditor/dccs/studiomax/preditor.ms new file mode 100644 index 00000000..3c5cb731 --- /dev/null +++ b/preditor/dccs/studiomax/preditor.ms @@ -0,0 +1,16 @@ + +function configure_preditor = ( + local pysite = python.import "site" + local WinEnv = dotNetClass "System.Environment" + -- If the env var PREDITOR_SITE is set, add its packages to python + local venv_path = WinEnv.GetEnvironmentVariable "PREDITOR_SITE" + if venv_path != undefined do ( + print("Preditor is adding python site: " + venv_path) + pysite.addsitedir venv_path + ) + -- Configure preditor, adding excepthook etc. + local preditor = python.import "preditor" + preditor.configure "studiomax" +) + +configure_preditor() diff --git a/preditor/dccs/studiomax/preditor_menu.mnx b/preditor/dccs/studiomax/preditor_menu.mnx new file mode 100644 index 00000000..5c5f588f --- /dev/null +++ b/preditor/dccs/studiomax/preditor_menu.mnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/preditor/debug.py b/preditor/debug.py index 39854c76..63dbdbc6 100644 --- a/preditor/debug.py +++ b/preditor/debug.py @@ -21,11 +21,15 @@ def clear(self, stamp=False): """Removes the contents of the log file.""" open(self._logfile, 'w').close() if stamp: - msg = '--------- Date: {today} Version: {version} ---------' - print(msg.format(today=datetime.datetime.today(), version=sys.version)) + print(self.stamp()) def flush(self): - self._stdhandle.flush() + if self._stdhandle: + self._stdhandle.flush() + + def stamp(self): + msg = '--------- Date: {today} Version: {version} ---------' + return msg.format(today=datetime.datetime.today(), version=sys.version) def write(self, msg): f = open(self._logfile, 'a') diff --git a/preditor/excepthooks.py b/preditor/excepthooks.py index 1b166e2a..dc7b3ff2 100644 --- a/preditor/excepthooks.py +++ b/preditor/excepthooks.py @@ -15,7 +15,7 @@ class PreditorExceptHook(object): This calls each callable in the `preditor.config.excepthooks` list any time `sys.excepthook` is called due to an raised exception. - If `config.excepthook` is empty when installing this class, it will + If `config.excepthooks` is empty when installing this class, it will automatically add `default_excepthooks`. You can disable this by adding `None` to the list before this class is initialized. """ diff --git a/preditor/gui/app.py b/preditor/gui/app.py index c96dbef2..afa938d8 100644 --- a/preditor/gui/app.py +++ b/preditor/gui/app.py @@ -93,7 +93,7 @@ def dpi_awareness_args(cls): Returns: args: Extend the arguments used to intialize the QApplication. """ - if settings.OS_TYPE == "Windows" and Qt.IsPyQt5: + if settings.OS_TYPE == "Windows" and (Qt.IsPyQt6 or Qt.IsPyQt5): # Make Qt automatically scale based on the monitor the window is # currently located. return ["--platform", "windows:dpiawareness=0"] @@ -157,4 +157,4 @@ def start(self): """Exec's the QApplication if it hasn't already been started.""" if self.app_created and self.app and not self.app_has_exec: self.app_has_exec = True - self.app.exec_() + Qt.QtCompat.QApplication.exec_() diff --git a/preditor/gui/codehighlighter.py b/preditor/gui/codehighlighter.py index 0f509947..f0000794 100644 --- a/preditor/gui/codehighlighter.py +++ b/preditor/gui/codehighlighter.py @@ -4,7 +4,6 @@ import os import re -from Qt.QtCore import QRegExp from Qt.QtGui import QColor, QSyntaxHighlighter, QTextCharFormat from .. import resourcePath @@ -68,59 +67,46 @@ def highlightBlock(self, text): if parent and hasattr(parent, 'outputPrompt'): self.highlightText( text, - QRegExp('%s[^\\n]*' % re.escape(parent.outputPrompt())), + re.compile('%s[^\\n]*' % re.escape(parent.outputPrompt())), format, ) # format the keywords format = self.keywordFormat() for kwd in self._keywords: - self.highlightText(text, QRegExp(r'\b%s\b' % kwd), format) + self.highlightText(text, re.compile(r'\b%s\b' % kwd), format) # format the strings format = self.stringFormat() + for string in self._strings: self.highlightText( text, - QRegExp('%s[^%s]*' % (string, string)), + re.compile('{s}[^{s}]*{s}'.format(s=string)), format, - includeLast=True, ) # format the comments format = self.commentFormat() for comment in self._comments: - self.highlightText(text, QRegExp(comment), format) + self.highlightText(text, re.compile(comment), format) def highlightText(self, text, expr, format, offset=0, includeLast=False): """Highlights a text group with an expression and format Args: text (str): text to highlight - expr (QRegExp): search parameter + expr (QRegularExpression): search parameter format (QTextCharFormat): formatting rule - offset (int): number of characters to offset by when highlighting includeLast (bool): whether or not the last character should be highlighted """ - pos = expr.indexIn(text, 0) - # highlight all the given matches to the expression in the text - while pos != -1: - pos = expr.pos(offset) - length = len(expr.cap(offset)) - - # use the last character if desired + for match in expr.finditer(text): + start, end = match.span() + length = end - start if includeLast: length += 1 - - # set the formatting - self.setFormat(pos, length, format) - - matched = expr.matchedLength() - if includeLast: - matched += 1 - - pos = expr.indexIn(text, pos + matched) + self.setFormat(start, length, format) def keywordColor(self): # pull the color from the parent if possible because this doesn't support diff --git a/preditor/gui/completer.py b/preditor/gui/completer.py index fcb742c9..343e3c51 100644 --- a/preditor/gui/completer.py +++ b/preditor/gui/completer.py @@ -5,6 +5,7 @@ import sys from enum import Enum +import Qt as Qt_py from Qt.QtCore import QRegExp, QSortFilterProxyModel, QStringListModel, Qt from Qt.QtGui import QCursor, QTextCursor from Qt.QtWidgets import QCompleter, QToolTip @@ -63,12 +64,16 @@ def __init__(self, widget): def setCaseSensitive(self, caseSensitive=True): """Set case sensitivity for completions""" - self._sensitivity = Qt.CaseSensitive if caseSensitive else Qt.CaseInsensitive + self._sensitivity = ( + Qt.CaseSensitivity.CaseSensitive + if caseSensitive + else Qt.CaseSensitivity.CaseInsensitive + ) self.buildCompleter() def caseSensitive(self): """Return current case sensitivity state for completions""" - caseSensitive = self._sensitivity == Qt.CaseSensitive + caseSensitive = self._sensitivity == Qt.CaseSensitivity.CaseSensitive return caseSensitive def setCompleterMode(self, completerMode=CompleterMode.STARTS_WITH): @@ -89,7 +94,7 @@ def buildCompleter(self): self.filterModel.setSourceModel(model) self.filterModel.setFilterCaseSensitivity(self._sensitivity) self.setModel(self.filterModel) - self.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + self.setCompletionMode(QCompleter.CompletionMode.UnfilteredPopupCompletion) def currentObject(self, scope=None, docMode=False): if self._enabled: @@ -155,8 +160,14 @@ def refreshList(self, scope=None): if self._completerMode == CompleterMode.FULL_FUZZY: regExStr = ".*".join(prefix) - regexp = QRegExp(regExStr, self._sensitivity) - self.filterModel.setFilterRegExp(regexp) + if Qt_py.IsPyQt6 or Qt_py.IsPySide6: + regexp = QRegExp(regExStr) + if self._sensitivity: + regexp.setPatternOptions(QRegExp.PatternOption.CaseInsensitiveOption) + self.filterModel.setFilterRegularExpression(regexp) + else: + regexp = QRegExp(regExStr, self._sensitivity) + self.filterModel.setFilterRegExp(regexp) def clear(self): self.popup().hide() @@ -193,7 +204,7 @@ def textUnderCursor(self, useParens=False): """pulls out the text underneath the cursor of this items widget""" cursor = self.widget().textCursor() - cursor.select(QTextCursor.WordUnderCursor) + cursor.select(QTextCursor.SelectionType.WordUnderCursor) # grab the selected word word = cursor.selectedText() diff --git a/preditor/gui/console.py b/preditor/gui/console.py index 1e28d0c7..1bf7b358 100644 --- a/preditor/gui/console.py +++ b/preditor/gui/console.py @@ -15,7 +15,14 @@ import __main__ from Qt import QtCompat from Qt.QtCore import QPoint, Qt, QTimer -from Qt.QtGui import QColor, QFontMetrics, QTextCharFormat, QTextCursor, QTextDocument +from Qt.QtGui import ( + QColor, + QFontMetrics, + QKeySequence, + QTextCharFormat, + QTextCursor, + QTextDocument, +) from Qt.QtWidgets import QAbstractItemView, QAction, QApplication, QTextEdit from .. import settings, stream @@ -32,7 +39,7 @@ class ConsolePrEdit(QTextEdit): # These Qt Properties can be customized using style sheets. commentColor = QtPropertyInit('_commentColor', QColor(0, 206, 52)) - errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.red)) + errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.GlobalColor.red)) keywordColor = QtPropertyInit('_keywordColor', QColor(17, 154, 255)) resultColor = QtPropertyInit('_resultColor', QColor(128, 128, 128)) stdoutColor = QtPropertyInit('_stdoutColor', QColor(17, 154, 255)) @@ -99,7 +106,9 @@ def __init__(self, parent): self.uiClearToLastPromptACT = QAction('Clear to Last', self) self.uiClearToLastPromptACT.triggered.connect(self.clearToLastPrompt) - self.uiClearToLastPromptACT.setShortcut(Qt.CTRL | Qt.SHIFT | Qt.Key_Backspace) + self.uiClearToLastPromptACT.setShortcut( + QKeySequence(Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_Backspace) + ) self.addAction(self.uiClearToLastPromptACT) self.x = 0 @@ -155,8 +164,8 @@ def setConsoleFont(self, font): workbox = self.window().current_workbox() if workbox: tab_width = workbox.__tab_width__() - fontPixelWidth = QFontMetrics(font).width(" ") - self.setTabStopWidth(fontPixelWidth * tab_width) + fontPixelWidth = QFontMetrics(font).horizontalAdvance(" ") + self.setTabStopDistance(fontPixelWidth * tab_width) # Scroll to same relative position where we started if origPercent is not None: @@ -170,7 +179,7 @@ def mousePressEvent(self, event): self.clickPos = event.pos() self.anchor = self.anchorAt(event.pos()) if self.anchor: - QApplication.setOverrideCursor(Qt.PointingHandCursor) + QApplication.setOverrideCursor(Qt.CursorShape.PointingHandCursor) return super(ConsolePrEdit, self).mousePressEvent(event) def mouseReleaseEvent(self, event): @@ -178,7 +187,7 @@ def mouseReleaseEvent(self, event): click position is the same as release position, if so, call errorHyperlink. """ samePos = event.pos() == self.clickPos - left = event.button() == Qt.LeftButton + left = event.button() == Qt.MouseButton.LeftButton if samePos and left and self.anchor: self.errorHyperlink() @@ -192,7 +201,7 @@ def wheelEvent(self, event): # scrolling. If used in LoggerWindow, use that wheel event # May not want to import LoggerWindow, so perhaps # check by str(type()) - ctrlPressed = event.modifiers() == Qt.ControlModifier + ctrlPressed = event.modifiers() == Qt.KeyboardModifier.ControlModifier if ctrlPressed and "LoggerWindow" in str(type(self.window())): self.window().wheelEvent(event) else: @@ -202,7 +211,7 @@ def keyReleaseEvent(self, event): """Override of keyReleaseEvent to determine when to end navigation of previous commands """ - if event.key() == Qt.Key_Alt: + if event.key() == Qt.Key.Key_Alt: self._prevCommandIndex = 0 else: event.ignore() @@ -319,7 +328,7 @@ def setCommand(self): prevCommand = self._prevCommands[self._prevCommandIndex] cursor = self.textCursor() - cursor.select(QTextCursor.LineUnderCursor) + cursor.select(QTextCursor.SelectionType.LineUnderCursor) if cursor.selectedText().startswith(self._consolePrompt): prevCommand = "{}{}".format(self._consolePrompt, prevCommand) cursor.insertText(prevCommand) @@ -335,7 +344,7 @@ def clearToLastPrompt(self): currentCursor = self.textCursor() # move to the end of the document so we can search backwards cursor = self.textCursor() - cursor.movePosition(cursor.End) + cursor.movePosition(QTextCursor.MoveOperation.End) self.setTextCursor(cursor) # Check if the last line is a empty prompt. If so, then preform two finds so we # find the prompt we are looking for instead of this empty prompt @@ -343,12 +352,14 @@ def clearToLastPrompt(self): 2 if self.toPlainText()[-len(self.prompt()) :] == self.prompt() else 1 ) for _ in range(findCount): - self.find(self.prompt(), QTextDocument.FindBackward) + self.find(self.prompt(), QTextDocument.FindFlag.FindBackward) # move to the end of the found line, select the rest of the text and remove it # preserving history if there is anything to remove. cursor = self.textCursor() - cursor.movePosition(cursor.EndOfLine) - cursor.movePosition(cursor.End, cursor.KeepAnchor) + cursor.movePosition(QTextCursor.MoveOperation.EndOfLine) + cursor.movePosition( + QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor + ) txt = cursor.selectedText() if txt: self.setTextCursor(cursor) @@ -364,7 +375,7 @@ def executeString(self, commandText, filename='', extraPrint=True if self.clearExecutionTime is not None: self.clearExecutionTime() cursor = self.textCursor() - cursor.select(QTextCursor.BlockUnderCursor) + cursor.select(QTextCursor.SelectionType.BlockUnderCursor) line = cursor.selectedText() if line and line[0] not in string.printable: line = line[1:] @@ -472,7 +483,7 @@ def insertCompletion(self, completion): """inserts the completion text into the editor""" if self.completer().widget() == self: cursor = self.textCursor() - cursor.select(QTextCursor.WordUnderCursor) + cursor.select(QTextCursor.SelectionType.WordUnderCursor) cursor.insertText(completion) self.setTextCursor(cursor) @@ -535,21 +546,21 @@ def keyPressEvent(self, event): # character, or remove it if backspace or delete has just been pressed. key = event.text() _, prefix = completer.currentObject(scope=__main__.__dict__) - isBackspaceOrDel = event.key() in (Qt.Key_Backspace, Qt.Key_Delete) + isBackspaceOrDel = event.key() in (Qt.Key.Key_Backspace, Qt.Key.Key_Delete) if key.isalnum() or key in ("-", "_"): prefix += str(key) elif isBackspaceOrDel and prefix: prefix = prefix[:-1] if completer and event.key() in ( - Qt.Key_Backspace, - Qt.Key_Delete, - Qt.Key_Escape, + Qt.Key.Key_Backspace, + Qt.Key.Key_Delete, + Qt.Key.Key_Escape, ): completer.hideDocumentation() # enter || return keys will execute the command - if event.key() in (Qt.Key_Return, Qt.Key_Enter): + if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if completer.popup().isVisible(): completer.clear() event.ignore() @@ -557,11 +568,11 @@ def keyPressEvent(self, event): self.executeCommand() # home key will move the cursor to home - elif event.key() == Qt.Key_Home: + elif event.key() == Qt.Key.Key_Home: self.moveToHome() # otherwise, ignore the event for completion events - elif event.key() in (Qt.Key_Tab, Qt.Key_Backtab): + elif event.key() in (Qt.Key.Key_Tab, Qt.Key.Key_Backtab): if not completer.popup().isVisible(): # The completer does not get updated if its not visible while typing. # We are about to complete the text using it so ensure its updated. @@ -571,19 +582,28 @@ def keyPressEvent(self, event): ) # Insert the correct text and clear the completion model index = completer.popup().currentIndex() - self.insertCompletion(index.data(Qt.DisplayRole)) + self.insertCompletion(index.data(Qt.ItemDataRole.DisplayRole)) completer.clear() - elif event.key() == Qt.Key_Escape and completer.popup().isVisible(): + elif event.key() == Qt.Key.Key_Escape and completer.popup().isVisible(): completer.clear() # other wise handle the keypress else: # define special key sequences modifiers = QApplication.instance().keyboardModifiers() - ctrlSpace = event.key() == Qt.Key_Space and modifiers == Qt.ControlModifier - ctrlM = event.key() == Qt.Key_M and modifiers == Qt.ControlModifier - ctrlI = event.key() == Qt.Key_I and modifiers == Qt.ControlModifier + ctrlSpace = ( + event.key() == Qt.Key.Key_Space + and modifiers == Qt.KeyboardModifier.ControlModifier + ) + ctrlM = ( + event.key() == Qt.Key.Key_M + and modifiers == Qt.KeyboardModifier.ControlModifier + ) + ctrlI = ( + event.key() == Qt.Key.Key_I + and modifiers == Qt.KeyboardModifier.ControlModifier + ) # Process all events we do not want to override if not (ctrlSpace or ctrlM or ctrlI): @@ -602,20 +622,20 @@ def keyPressEvent(self, event): # check for particular events for the completion if completer: # look for documentation popups - if event.key() == Qt.Key_ParenLeft: + if event.key() == Qt.Key.Key_ParenLeft: rect = self.cursorRect() point = self.mapToGlobal(QPoint(rect.x(), rect.y())) completer.showDocumentation(pos=point, scope=__main__.__dict__) # hide documentation popups - elif event.key() == Qt.Key_ParenRight: + elif event.key() == Qt.Key.Key_ParenRight: completer.hideDocumentation() # determine if we need to show the popup or if it already is visible, we # need to update it elif ( - event.key() == Qt.Key_Period - or event.key() == Qt.Key_Escape + event.key() == Qt.Key.Key_Period + or event.key() == Qt.Key.Key_Escape or completer.popup().isVisible() or ctrlSpace or ctrlI @@ -646,7 +666,9 @@ def keyPressEvent(self, event): completer.setCurrentRow(index.row()) # Make sure that current selection is visible, ie scroll to it - completer.popup().scrollTo(index, QAbstractItemView.EnsureVisible) + completer.popup().scrollTo( + index, QAbstractItemView.ScrollHint.EnsureVisible + ) # show the completer for the rect rect = self.cursorRect() @@ -663,7 +685,7 @@ def keyPressEvent(self, event): if completer.wasCompleting and not completer.popup().isVisible(): wasCompletingCounterMax = completer.wasCompletingCounterMax if completer.wasCompletingCounter <= wasCompletingCounterMax: - if event.key() not in (Qt.Key_Backspace, Qt.Key_Left): + if event.key() not in (Qt.Key.Key_Backspace, Qt.Key.Key_Left): completer.wasCompletingCounter += 1 else: completer.wasCompletingCounter = 0 @@ -671,20 +693,26 @@ def keyPressEvent(self, event): def moveToHome(self): """moves the cursor to the home location""" - mode = QTextCursor.MoveAnchor + mode = QTextCursor.MoveMode.MoveAnchor # select the home - if QApplication.instance().keyboardModifiers() == Qt.ShiftModifier: - mode = QTextCursor.KeepAnchor + if ( + QApplication.instance().keyboardModifiers() + == Qt.KeyboardModifier.ShiftModifier + ): + mode = QTextCursor.MoveMode.KeepAnchor # grab the cursor cursor = self.textCursor() - if QApplication.instance().keyboardModifiers() == Qt.ControlModifier: + if ( + QApplication.instance().keyboardModifiers() + == Qt.KeyboardModifier.ControlModifier + ): # move to the top of the document if control is pressed - cursor.movePosition(QTextCursor.Start) + cursor.movePosition(QTextCursor.MoveOperation.Start) else: # Otherwise just move it to the start of the line - cursor.movePosition(QTextCursor.StartOfBlock, mode) + cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock, mode) # move the cursor to the end of the prompt. - cursor.movePosition(QTextCursor.Right, mode, len(self.prompt())) + cursor.movePosition(QTextCursor.MoveOperation.Right, mode, len(self.prompt())) self.setTextCursor(cursor) def outputPrompt(self): @@ -721,7 +749,7 @@ def startPrompt(self, prompt): prompt(str): The prompt to start the line with. If this prompt is already the only text on the last line this function does nothing. """ - self.moveCursor(QTextCursor.End) + self.moveCursor(QTextCursor.MoveOperation.End) # if this is not already a new line if self.textCursor().block().text() != prompt: @@ -744,9 +772,11 @@ def startOutputLine(self): self.startPrompt(self._outputPrompt) def removeCurrentLine(self): - self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor) - self.moveCursor(QTextCursor.StartOfLine, QTextCursor.MoveAnchor) - self.moveCursor(QTextCursor.End, QTextCursor.KeepAnchor) + self.moveCursor(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.MoveAnchor) + self.moveCursor( + QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.MoveAnchor + ) + self.moveCursor(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor) self.textCursor().removeSelectedText() self.textCursor().deletePreviousChar() self.insertPlainText("\n") @@ -787,7 +817,7 @@ def write(self, msg, error=False): hasattr(window, 'uiErrorHyperlinksACT') and window.uiErrorHyperlinksACT.isChecked() ) - self.moveCursor(QTextCursor.End) + self.moveCursor(QTextCursor.MoveOperation.End) charFormat = QTextCharFormat() if not error: @@ -806,7 +836,7 @@ def write(self, msg, error=False): info = None if doHyperlink and msg == '\n': - cursor.select(QTextCursor.BlockUnderCursor) + cursor.select(QTextCursor.SelectionType.BlockUnderCursor) line = cursor.selectedText() # Remove possible leading unicode paragraph separator, which really diff --git a/preditor/gui/dialog.py b/preditor/gui/dialog.py index 293382fc..6dfed9a8 100644 --- a/preditor/gui/dialog.py +++ b/preditor/gui/dialog.py @@ -24,11 +24,14 @@ def instance(cls, parent=None): if not cls._instance: cls._instance = cls(parent=parent) # protect the memory - cls._instance.setAttribute(Qt.WA_DeleteOnClose, False) + cls._instance.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) return cls._instance def __init__( - self, parent=None, flags=Qt.WindowMinMaxButtonsHint | Qt.WindowCloseButtonHint + self, + parent=None, + flags=Qt.WindowType.WindowMinMaxButtonsHint + | Qt.WindowType.WindowCloseButtonHint, ): # if there is no root, create if not parent: @@ -68,7 +71,7 @@ def __init__( # dead dialogs # set the delete attribute to clean up the window once it is closed - self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) # set this property to true to properly handle tracking events to control # keyboard overrides @@ -109,7 +112,7 @@ def _shouldDisableAccelerators(self, old, now): def closeEvent(self, event): # ensure this object gets deleted wwidget = None - if self.testAttribute(Qt.WA_DeleteOnClose): + if self.testAttribute(Qt.WidgetAttribute.WA_DeleteOnClose): # collect the win widget to uncache it if self.parent() and self.parent().inherits('QWinWidget'): wwidget = self.parent() @@ -128,10 +131,10 @@ def exec_(self): # This function properly transfers ownership of the dialog instance back to # Python anyway - self.setAttribute(Qt.WA_DeleteOnClose, False) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) # execute the dialog - return QDialog.exec_(self) + return super().exec() def setGeometry(self, *args): """ @@ -158,7 +161,7 @@ def _shutdown(cls, this): # allow the global instance to be cleared if this == cls._instance: cls._instance = None - this.setAttribute(Qt.WA_DeleteOnClose, True) + this.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) try: this.close() except RuntimeError: diff --git a/preditor/gui/drag_tab_bar.py b/preditor/gui/drag_tab_bar.py index 530654e2..e0945d23 100644 --- a/preditor/gui/drag_tab_bar.py +++ b/preditor/gui/drag_tab_bar.py @@ -52,12 +52,12 @@ def mouseMoveEvent(self, event): # noqa: N802 drag.setMimeData(self._mime_data) drag.setPixmap(self._mime_data.imageData()) drag.setHotSpot(event_pos - pos_in_tab) - cursor = QCursor(Qt.OpenHandCursor) - drag.setDragCursor(cursor.pixmap(), Qt.MoveAction) - action = drag.exec_(Qt.MoveAction) + cursor = QCursor(Qt.CursorShape.OpenHandCursor) + drag.setDragCursor(cursor.pixmap(), Qt.DropAction.MoveAction) + action = drag.exec(Qt.DropAction.MoveAction) # If the user didn't successfully add this to a new tab widget, restore # the tab to the original location. - if action == Qt.IgnoreAction: + if action == Qt.DropAction.IgnoreAction: original_tab_index = self._mime_data.property('original_tab_index') self.parentWidget().insertTab( original_tab_index, widget, self._mime_data.text() @@ -66,7 +66,7 @@ def mouseMoveEvent(self, event): # noqa: N802 self._mime_data = None def mousePressEvent(self, event): # noqa: N802 - if event.button() == Qt.LeftButton and not self._mime_data: + if event.button() == Qt.MouseButton.LeftButton and not self._mime_data: tab_index = self.tabAt(event.pos()) # While we don't remove the tab on mouse press, capture its tab image @@ -123,7 +123,7 @@ def dropEvent(self, event): # noqa: N802 if event.source().parentWidget() == self: return - event.setDropAction(Qt.MoveAction) + event.setDropAction(Qt.DropAction.MoveAction) event.accept() counter = self.count() @@ -184,7 +184,7 @@ def install_tab_widget(cls, tab_widget, mime_type='DragTabBar', menu=True): tab_widget.setDocumentMode(True) if menu: - bar.setContextMenuPolicy(Qt.CustomContextMenu) + bar.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) bar.customContextMenuRequested.connect(bar.tab_menu) return bar diff --git a/preditor/gui/errordialog.py b/preditor/gui/errordialog.py index d4de030b..0322066b 100644 --- a/preditor/gui/errordialog.py +++ b/preditor/gui/errordialog.py @@ -21,7 +21,7 @@ def __init__(self, parent): self.parent_ = parent self.setWindowTitle('Error Occurred') - self.uiErrorLBL.setTextFormat(Qt.RichText) + self.uiErrorLBL.setTextFormat(Qt.TextFormat.RichText) self.uiIconLBL.setPixmap( QPixmap( os.path.join( @@ -30,7 +30,7 @@ def __init__(self, parent): 'img', 'warning-big.png', ) - ).scaledToHeight(64, Qt.SmoothTransformation) + ).scaledToHeight(64, Qt.TransformationMode.SmoothTransformation) ) self.uiLoggerBTN.clicked.connect(self.show_logger) diff --git a/preditor/gui/find_files.py b/preditor/gui/find_files.py index 4ea2dc29..f677f69b 100644 --- a/preditor/gui/find_files.py +++ b/preditor/gui/find_files.py @@ -30,22 +30,24 @@ def __init__(self, parent=None, managers=None, console=None): # Create shortcuts self.uiCloseSCT = QShortcut( - QKeySequence(Qt.Key_Escape), self, context=Qt.WidgetWithChildrenShortcut + QKeySequence(Qt.Key.Key_Escape), + self, + context=Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.uiCloseSCT.activated.connect(self.hide) self.uiCaseSensitiveSCT = QShortcut( - QKeySequence(Qt.AltModifier | Qt.Key_C), + QKeySequence(Qt.KeyboardModifier.AltModifier | Qt.Key.Key_C), self, - context=Qt.WidgetWithChildrenShortcut, + context=Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.uiCaseSensitiveSCT.activated.connect(self.uiCaseSensitiveBTN.toggle) self.uiRegexSCT = QShortcut( - QKeySequence(Qt.AltModifier | Qt.Key_R), + QKeySequence(Qt.KeyboardModifier.AltModifier | Qt.Key.Key_R), self, - context=Qt.WidgetWithChildrenShortcut, + context=Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.uiRegexSCT.activated.connect(self.uiRegexBTN.toggle) diff --git a/preditor/gui/fuzzy_search/fuzzy_search.py b/preditor/gui/fuzzy_search/fuzzy_search.py index f341ac3b..ed7acdcc 100644 --- a/preditor/gui/fuzzy_search/fuzzy_search.py +++ b/preditor/gui/fuzzy_search/fuzzy_search.py @@ -22,14 +22,18 @@ def __init__(self, model, parent=None, **kwargs): self.y_offset = 100 self.setMinimumSize(400, 200) self.uiCloseSCT = QShortcut( - Qt.Key_Escape, self, context=Qt.WidgetWithChildrenShortcut + Qt.Key.Key_Escape, + self, + context=Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.uiCloseSCT.activated.connect(self._canceled) - self.uiUpSCT = QShortcut(Qt.Key_Up, self, context=Qt.WidgetWithChildrenShortcut) + self.uiUpSCT = QShortcut( + Qt.Key.Key_Up, self, context=Qt.ShortcutContext.WidgetWithChildrenShortcut + ) self.uiUpSCT.activated.connect(partial(self.increment_selection, -1)) self.uiDownSCT = QShortcut( - Qt.Key_Down, self, context=Qt.WidgetWithChildrenShortcut + Qt.Key.Key_Down, self, context=Qt.ShortcutContext.WidgetWithChildrenShortcut ) self.uiDownSCT.activated.connect(partial(self.increment_selection, 1)) @@ -90,4 +94,4 @@ def reposition(self): def popup(self): self.show() self.reposition() - self.uiLineEDIT.setFocus(Qt.PopupFocusReason) + self.uiLineEDIT.setFocus(Qt.FocusReason.PopupFocusReason) diff --git a/preditor/gui/group_tab_widget/__init__.py b/preditor/gui/group_tab_widget/__init__.py index 5d06d6a5..23deb79f 100644 --- a/preditor/gui/group_tab_widget/__init__.py +++ b/preditor/gui/group_tab_widget/__init__.py @@ -61,13 +61,13 @@ def __init__(self, editor_kwargs=None, core_name=None, *args, **kwargs): corner.uiMenuBTN = QToolButton(corner) corner.uiMenuBTN.setIcon(QIcon(resourcePath('img/chevron-down.png'))) corner.uiMenuBTN.setObjectName('group_tab_widget_menu_btn') - corner.uiMenuBTN.setPopupMode(QToolButton.InstantPopup) + corner.uiMenuBTN.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) corner.uiCornerMENU = GroupTabMenu(self, parent=corner.uiMenuBTN) corner.uiMenuBTN.setMenu(corner.uiCornerMENU) lyt.addWidget(corner.uiMenuBTN) self.uiCornerBTN = corner - self.setCornerWidget(self.uiCornerBTN, Qt.TopRightCorner) + self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner) def add_new_tab(self, group, title="Workbox", group_fmt=None): """Adds a new tab to the requested group, creating the group if the group @@ -148,9 +148,9 @@ def close_tab(self, index): 'Are you sure you want to close all tabs under the "{}" tab?'.format( self.tabText(self.currentIndex()) ), - QMessageBox.Yes | QMessageBox.Cancel, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, ) - if ret == QMessageBox.Yes: + if ret == QMessageBox.StandardButton.Yes: # Clean up all temp files created by this group's editors if they # are not using actual saved files. tab_widget = self.widget(self.currentIndex()) diff --git a/preditor/gui/group_tab_widget/grouped_tab_models.py b/preditor/gui/group_tab_widget/grouped_tab_models.py index e7eac9a5..dda635d3 100644 --- a/preditor/gui/group_tab_widget/grouped_tab_models.py +++ b/preditor/gui/group_tab_widget/grouped_tab_models.py @@ -7,7 +7,7 @@ class GroupTabItemModel(QStandardItemModel): - GroupIndexRole = Qt.UserRole + 1 + GroupIndexRole = Qt.ItemDataRole.UserRole + 1 TabIndexRole = GroupIndexRole + 1 def __init__(self, manager, *args, **kwargs): @@ -24,7 +24,7 @@ def workbox_indexes_from_model_index(self, index): def pathFromIndex(self, index): parts = [""] while index.isValid(): - parts.append(self.data(index, Qt.DisplayRole)) + parts.append(self.data(index, Qt.ItemDataRole.DisplayRole)) index = index.parent() if len(parts) == 1: return "" @@ -56,7 +56,7 @@ def process(self): class GroupTabListItemModel(GroupTabItemModel): def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable def process(self): root = self.invisibleRootItem() @@ -101,7 +101,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): def pathFromIndex(self, index): parts = [""] while index.isValid(): - parts.append(self.data(index, Qt.DisplayRole)) + parts.append(self.data(index, Qt.ItemDataRole.DisplayRole)) index = index.parent() if len(parts) == 1: return "" diff --git a/preditor/gui/group_tab_widget/grouped_tab_widget.py b/preditor/gui/group_tab_widget/grouped_tab_widget.py index dd0d9edf..c9902692 100644 --- a/preditor/gui/group_tab_widget/grouped_tab_widget.py +++ b/preditor/gui/group_tab_widget/grouped_tab_widget.py @@ -25,7 +25,7 @@ def __init__(self, editor_kwargs, editor_cls=None, core_name=None, *args, **kwar self.uiCornerBTN.setText('+') self.uiCornerBTN.setIcon(QIcon(resourcePath('img/file-plus.png'))) self.uiCornerBTN.released.connect(lambda: self.add_new_editor()) - self.setCornerWidget(self.uiCornerBTN, Qt.TopRightCorner) + self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner) def add_new_editor(self, title="Workbox"): editor, title = self.default_tab(title) @@ -41,16 +41,18 @@ def addTab(self, *args, **kwargs): # noqa: N802 def close_tab(self, index): if self.count() == 1: msg = "You have to leave at least one tab open." - QMessageBox.critical(self, 'Tab can not be closed.', msg, QMessageBox.Ok) + QMessageBox.critical( + self, 'Tab can not be closed.', msg, QMessageBox.StandardButton.Ok + ) return ret = QMessageBox.question( self, 'Donate to the cause?', "Would you like to donate this tabs contents to the /dev/null fund " "for wayward code?", - QMessageBox.Yes | QMessageBox.Cancel, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, ) - if ret == QMessageBox.Yes: + if ret == QMessageBox.StandardButton.Yes: # If the tab was saved to a temp file, remove it from disk editor = self.widget(index) editor.__remove_tempfile__() diff --git a/preditor/gui/level_buttons.py b/preditor/gui/level_buttons.py index 7af59948..25fa1dcb 100644 --- a/preditor/gui/level_buttons.py +++ b/preditor/gui/level_buttons.py @@ -134,7 +134,7 @@ def __init__(self, parent=None): parent (QWidget, optional): The parent widget for this button. """ super(LoggingLevelButton, self).__init__(parent=parent) - self.setPopupMode(QToolButton.InstantPopup) + self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) # create root logger menu root = logging.getLogger("") diff --git a/preditor/gui/loggerwindow.py b/preditor/gui/loggerwindow.py index d6364adc..575e7956 100644 --- a/preditor/gui/loggerwindow.py +++ b/preditor/gui/loggerwindow.py @@ -12,15 +12,17 @@ from functools import partial import __main__ +import Qt as Qt_py from Qt import QtCompat, QtCore, QtWidgets from Qt.QtCore import QByteArray, Qt, QTimer, Signal, Slot -from Qt.QtGui import QCursor, QFont, QIcon, QTextCursor +from Qt.QtGui import QCursor, QFont, QIcon, QKeySequence, QTextCursor from Qt.QtWidgets import ( QApplication, QFontDialog, QInputDialog, QMessageBox, QTextBrowser, + QTextEdit, QToolTip, QVBoxLayout, ) @@ -198,7 +200,7 @@ def __init__(self, parent, name=None, run_workbox=False, standalone=False): self.uiCompleterModeMENU.addSeparator() action = self.uiCompleterModeMENU.addAction('Cycle mode') action.setObjectName('uiCycleModeACT') - action.setShortcut(Qt.CTRL | Qt.Key_M) + action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_M)) action.triggered.connect(self.cycleCompleterMode) self.uiCompleterModeMENU.hovered.connect(self.handleMenuHovered) @@ -490,7 +492,7 @@ def setup_run_workbox(self): def openSetPreferredTextEditorDialog(self): dlg = SetTextEditorPathDialog(parent=self) - dlg.exec_() + dlg.exec() def focusToConsole(self): """Move focus to the console""" @@ -529,7 +531,7 @@ def copyToWorkbox(self): cursor = console.textCursor() if not cursor.hasSelection(): - cursor.select(QTextCursor.LineUnderCursor) + cursor.select(QTextCursor.SelectionType.LineUnderCursor) text = cursor.selectedText() prompt = console.prompt() if text.startswith(prompt): @@ -565,7 +567,7 @@ def getPrevCommand(self): def wheelEvent(self, event): """adjust font size on ctrl+scrollWheel""" - if event.modifiers() == Qt.ControlModifier: + if event.modifiers() == Qt.KeyboardModifier.ControlModifier: # WheelEvents can be emitted in a cluster, but we only want one at a time # (ie to change font size by 1, rather than 2 or 3). Let's bail if previous # font-resize wheel event was within a certain threshhold. @@ -605,7 +607,10 @@ def handleMenuHovered(self, action): else: text = action.toolTip() - menu = action.parentWidget() + if Qt_py.IsPyQt4: + menu = action.parentWidget() + else: + menu = action.parent() QToolTip.showText(QCursor.pos(), text, menu) def selectFont(self, monospace=False, proportional=False): @@ -620,13 +625,16 @@ def selectFont(self, monospace=False, proportional=False): curFontFamily = origFont.family() if monospace and proportional: - options = QFontDialog.MonospacedFonts | QFontDialog.ProportionalFonts + options = ( + QFontDialog.FontDialogOption.MonospacedFonts + | QFontDialog.FontDialogOption.ProportionalFonts + ) kind = "monospace or proportional " elif monospace: - options = QFontDialog.MonospacedFonts + options = QFontDialog.FontDialogOption.MonospacedFonts kind = "monospace " elif proportional: - options = QFontDialog.ProportionalFonts + options = QFontDialog.FontDialogOption.ProportionalFonts kind = "proportional " # Present a QFontDialog for user to choose a font @@ -686,9 +694,9 @@ def _genPrefName(cls, baseName, index): def adjustWorkboxOrientation(self, state): if state: - self.uiSplitterSPLIT.setOrientation(Qt.Horizontal) + self.uiSplitterSPLIT.setOrientation(Qt.Orientation.Horizontal) else: - self.uiSplitterSPLIT.setOrientation(Qt.Vertical) + self.uiSplitterSPLIT.setOrientation(Qt.Orientation.Vertical) def backupPreferences(self): """Saves a copy of the current preferences to a zip archive.""" @@ -757,7 +765,11 @@ def execSelected(self, truncate=True): def keyPressEvent(self, event): # Fix 'Maya : Qt tools lose focus' https://redmine.blur.com/issues/34430 - if event.modifiers() & (Qt.AltModifier | Qt.ControlModifier | Qt.ShiftModifier): + if event.modifiers() & ( + Qt.KeyboardModifier.AltModifier + | Qt.KeyboardModifier.ControlModifier + | Qt.KeyboardModifier.ShiftModifier + ): pass else: super(LoggerWindow, self).keyPressEvent(event) @@ -781,7 +793,7 @@ def recordPrefs(self, manual=False): pref.update( { 'loggergeom': [geo.x(), geo.y(), geo.width(), geo.height()], - 'windowState': int(self.windowState()), + 'windowState': QtCompat.enumValue(self.windowState()), 'SplitterVertical': self.uiEditorVerticalACT.isChecked(), 'SplitterSize': self.uiSplitterSPLIT.sizes(), 'tabIndent': self.uiIndentationsTabsACT.isChecked(), @@ -863,7 +875,7 @@ def maybeDisplayDialog(self, dialog): if dialog.objectName() in self.dont_ask_again: return - dialog.exec_() + dialog.exec() def restartLogger(self): """Closes this PrEditor instance and starts a new process with the same @@ -911,7 +923,7 @@ def restorePrefs(self): sizes = pref.get('SplitterSize') if sizes: self.uiSplitterSPLIT.setSizes(sizes) - self.setWindowState(Qt.WindowStates(pref.get('windowState', 0))) + self.setWindowState(Qt.WindowState(pref.get('windowState', 0))) self.uiIndentationsTabsACT.setChecked(pref.get('tabIndent', True)) self.uiCopyTabsToSpacesACT.setChecked(pref.get('copyIndentsAsSpaces', False)) @@ -983,7 +995,7 @@ def restorePrefs(self): _font = pref.get('consoleFont', None) if _font: font = QFont() - if font.fromString(_font): + if QtCompat.QFont.fromString(font, _font): self.console().setConsoleFont(font) self.dont_ask_again = pref.get('dont_ask_again', []) @@ -1166,9 +1178,9 @@ def setFlashWindowInterval(self): def setWordWrap(self, state): if state: - self.uiConsoleTXT.setLineWrapMode(self.uiConsoleTXT.WidgetWidth) + self.uiConsoleTXT.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) else: - self.uiConsoleTXT.setLineWrapMode(self.uiConsoleTXT.NoWrap) + self.uiConsoleTXT.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) def show_about(self): """Shows `preditor.about_preditor()`'s output in a message box.""" @@ -1248,7 +1260,7 @@ def shutdown(self): # if this is the global instance, then allow it to be deleted on close if self == LoggerWindow._instance: - self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) LoggerWindow._instance = None # clear out the system @@ -1339,7 +1351,7 @@ def instance( ) # protect the memory - inst.setAttribute(Qt.WA_DeleteOnClose, False) + inst.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) # cache the instance LoggerWindow._instance = inst diff --git a/preditor/gui/set_text_editor_path_dialog.py b/preditor/gui/set_text_editor_path_dialog.py index 1d14e868..59df355d 100644 --- a/preditor/gui/set_text_editor_path_dialog.py +++ b/preditor/gui/set_text_editor_path_dialog.py @@ -56,4 +56,6 @@ def accept(self): else: msg = "That path doesn't exists or isn't an executable file." label = 'Incorrect Path' - QMessageBox.warning(self.window(), label, msg, QMessageBox.Ok) + QMessageBox.warning( + self.window(), label, msg, QMessageBox.StandardButton.Ok + ) diff --git a/preditor/gui/window.py b/preditor/gui/window.py index ac9fee0c..116c2e43 100644 --- a/preditor/gui/window.py +++ b/preditor/gui/window.py @@ -25,7 +25,7 @@ def instance(cls, parent=None): if not cls._instance: cls._instance = cls(parent=parent) # protect the memory - cls._instance.setAttribute(Qt.WA_DeleteOnClose, False) + cls._instance.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) return cls._instance def __init__(self, parent=None, flags=0): @@ -67,7 +67,7 @@ def __init__(self, parent=None, flags=0): # dead dialogs # set the delete attribute to clean up the window once it is closed - self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) # If this value is set to False calling setGeometry on this window will not # adjust the geometry to ensure the window is on a valid screen. self.checkScreenGeo = True @@ -103,7 +103,7 @@ def _shouldDisableAccelerators(self, old, now): def closeEvent(self, event): # ensure this object gets deleted wwidget = None - if self.testAttribute(Qt.WA_DeleteOnClose): + if self.testAttribute(Qt.WidgetAttribute.WA_DeleteOnClose): # collect the win widget to uncache it if self.parent() and self.parent().inherits('QWinWidget'): wwidget = self.parent() @@ -141,7 +141,7 @@ def _shutdown(cls, this): # allow the global instance to be cleared if this == cls._instance: cls._instance = None - this.setAttribute(Qt.WA_DeleteOnClose, True) + this.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) try: this.close() except RuntimeError: diff --git a/preditor/gui/workbox_mixin.py b/preditor/gui/workbox_mixin.py index efa2b63b..a447e67c 100644 --- a/preditor/gui/workbox_mixin.py +++ b/preditor/gui/workbox_mixin.py @@ -1,9 +1,11 @@ from __future__ import absolute_import, print_function +import io import os import tempfile import textwrap +import chardet from Qt.QtCore import Qt from Qt.QtWidgets import QStackedWidget @@ -317,14 +319,37 @@ def __remove_tempfile__(self): os.remove(tempfile) @classmethod - def __open_file__(cls, filename): - with open(filename) as fle: - return fle.read() - return "" + def __open_file__(cls, filename, strict=True): + """Open a file and try to detect the text encoding it was saved as. + + Returns: + encoding(str): The detected encoding, Defaults to "utf-8" if unable + to detect encoding. + text(str): The contents of the file decoded to a str. + """ + with open(filename, "rb") as f: + text_bytes = f.read() + + # Open file, detect source encoding and convert to utf-8 + encoding = chardet.detect(text_bytes)['encoding'] or 'utf-8' + try: + text = text_bytes.decode(encoding) + except UnicodeDecodeError as e: + if strict: + raise UnicodeDecodeError( # noqa: B904 + e.encoding, + e.object, + e.start, + e.end, + f"{e.reason}, Filename: {filename}", + ) + encoding = 'utf-8' + text = text_bytes.decode(encoding, errors="ignore") + return encoding, text @classmethod - def __write_file__(cls, filename, txt): - with open(filename, 'w') as fle: + def __write_file__(cls, filename, txt, encoding=None): + with io.open(filename, 'w', newline='\n', encoding=encoding) as fle: fle.write(txt) def __show__(self): @@ -335,7 +360,7 @@ def __show__(self): if self._filename_pref: self.__load__(self._filename_pref) elif self._tempfile: - txt = self.__open_file__(self.__tempfile__()) + _, txt = self.__open_file__(self.__tempfile__(), strict=False) self.__set_text__(txt) def process_shortcut(self, event, run=True): @@ -365,10 +390,14 @@ def process_shortcut(self, event, run=True): modifiers = event.modifiers() # Determine which relevant combos are pressed - ret = key == Qt.Key_Return - enter = key == Qt.Key_Enter - shift = modifiers == Qt.ShiftModifier - ctrlShift = modifiers == Qt.ControlModifier | Qt.ShiftModifier + ret = key == Qt.Key.Key_Return + enter = key == Qt.Key.Key_Enter + shift = modifiers == Qt.KeyboardModifier.ShiftModifier + ctrlShift = ( + modifiers + == Qt.KeyboardModifier.ControlModifier + | Qt.KeyboardModifier.ShiftModifier + ) # Determine which actions to take evalTrunc = enter or (ret and shift) diff --git a/preditor/gui/workbox_text_edit.py b/preditor/gui/workbox_text_edit.py index 4bf0a392..476aaf32 100644 --- a/preditor/gui/workbox_text_edit.py +++ b/preditor/gui/workbox_text_edit.py @@ -27,6 +27,7 @@ def __init__( ): super(WorkboxTextEdit, self).__init__(parent=parent, core_name=core_name) self._filename = None + self._encoding = None self.__set_console__(console) highlight = CodeHighlighter(self) highlight.setLanguage('Python') @@ -64,7 +65,7 @@ def __font__(self): def __set_font__(self, font): metrics = QFontMetrics(font) - self.setTabStopDistance(metrics.width(" ") * 4) + self.setTabStopDistance(metrics.horizontalAdvance(" ") * 4) super(WorkboxTextEdit, self).setFont(font) def __goto_line__(self, line): @@ -79,7 +80,8 @@ def __set_indentations_use_tabs__(self, state): def __load__(self, filename): self._filename = filename - txt = self.__open_file__(self._filename) + enc, txt = self.__open_file__(self._filename) + self._encoding = enc self.__set_text__(txt) def __margins_font__(self): @@ -114,7 +116,7 @@ def __selected_text__(self, start_of_line=False, selectText=False): selectText = self.window().uiSelectTextACT.isChecked() or selectText if selectText: - cursor.select(QTextCursor.LineUnderCursor) + cursor.select(QTextCursor.SelectionType.LineUnderCursor) self.setTextCursor(cursor) return text, line diff --git a/preditor/gui/workboxwidget.py b/preditor/gui/workboxwidget.py index 048898ce..c26a57a8 100644 --- a/preditor/gui/workboxwidget.py +++ b/preditor/gui/workboxwidget.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, print_function -import io import re import time @@ -10,6 +9,7 @@ from .. import core, resourcePath from ..gui.workbox_mixin import WorkboxMixin +from ..scintilla import QsciScintilla from ..scintilla.documenteditor import DocumentEditor, SearchOptions from ..scintilla.finddialog import FindDialog @@ -36,15 +36,19 @@ def __init__( self.initShortcuts() self.setLanguage('Python') # Default to unix newlines - self.setEolMode(self.EolUnix) + self.setEolMode(QsciScintilla.EolMode.EolUnix) if hasattr(self.window(), "setWorkboxFontBasedOnConsole"): self.window().setWorkboxFontBasedOnConsole() def __auto_complete_enabled__(self): - return self.autoCompletionSource() == self.AcsAll + return self.autoCompletionSource() == QsciScintilla.AutoCompletionSource.AcsAll def __set_auto_complete_enabled__(self, state): - state = self.AcsAll if state else self.AcsNone + state = ( + QsciScintilla.AutoCompletionSource.AcsAll + if state + else QsciScintilla.AutoCompletionSource.AcsNone + ) self.setAutoCompletionSource(state) def __clear__(self): @@ -120,7 +124,7 @@ def __marker_add__(self, line): try: marker = self._marker except AttributeError: - self._marker = self.markerDefine(self.Circle) + self._marker = self.markerDefine(QsciScintilla.MarkerSymbol.Circle) marker = self._marker self.markerAdd(line, marker) @@ -197,10 +201,10 @@ def __set_text__(self, txt): self.setText(txt) @classmethod - def __write_file__(cls, filename, txt): - with io.open(filename, 'w', newline='\n') as fle: - # Save unix newlines for simplicity - fle.write(cls.__unix_end_lines__(txt)) + def __write_file__(cls, filename, txt, encoding=None): + # Save unix newlines for simplicity + txt = cls.__unix_end_lines__(txt) + super(WorkboxWidget, cls).__write_file__(filename, txt, encoding=encoding) def keyPressEvent(self, event): """Check for certain keyboard shortcuts, and handle them as needed, @@ -216,13 +220,13 @@ def keyPressEvent(self, event): when Return is pressed), so this combination is not detectable. """ if self._software == 'softimage': - DocumentEditor.keyPressEvent(self, event) + super(WorkboxWidget, self).keyPressEvent(event) else: if self.process_shortcut(event): return else: # Send regular keystroke - DocumentEditor.keyPressEvent(self, event) + super(WorkboxWidget, self).keyPressEvent(event) def initShortcuts(self): """Use this to set up shortcuts when the DocumentEditor""" @@ -248,7 +252,7 @@ def initShortcuts(self): # create the search dialog and connect actions self._searchDialog = FindDialog(self) - self._searchDialog.setAttribute(Qt.WA_DeleteOnClose, False) + self._searchDialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) self.uiFindACT.triggered.connect( lambda: self._searchDialog.search(self.searchText()) ) diff --git a/preditor/scintilla/__init__.py b/preditor/scintilla/__init__.py index 3fb4d133..9a57bf80 100644 --- a/preditor/scintilla/__init__.py +++ b/preditor/scintilla/__init__.py @@ -1,5 +1,23 @@ from __future__ import absolute_import +__all__ = ["delayables", "FindState", "Qsci", "QsciScintilla"] + +import Qt + +if Qt.IsPyQt6: + from PyQt6 import Qsci + from PyQt6.Qsci import QsciScintilla +elif Qt.IsPyQt5: + from PyQt5 import Qsci + from PyQt5.Qsci import QsciScintilla +elif Qt.IsPyQt4: + from PyQt4 import Qsci + from PyQt4.Qsci import QsciScintilla +else: + raise ImportError( + "QScintilla library is not supported by {}".format(Qt.__binding__) + ) + class FindState(object): """ @@ -19,4 +37,4 @@ def __init__(self): self.end_pos = None -from . import delayables # noqa: F401, E402 +from . import delayables # noqa: E402 diff --git a/preditor/scintilla/delayables/smart_highlight.py b/preditor/scintilla/delayables/smart_highlight.py index f73c8500..1ac17216 100644 --- a/preditor/scintilla/delayables/smart_highlight.py +++ b/preditor/scintilla/delayables/smart_highlight.py @@ -1,17 +1,17 @@ from __future__ import absolute_import, print_function -from PyQt5.Qsci import QsciScintilla +import Qt from Qt.QtCore import QSignalMapper from Qt.QtWidgets import QWidget from ...delayable_engine.delayables import SearchDelayable -from .. import FindState +from .. import FindState, QsciScintilla class SmartHighlight(SearchDelayable): key = 'smart_highlight' indicator_number = 30 - indicator_style = QsciScintilla.StraightBoxIndicator + indicator_style = QsciScintilla.IndicatorStyle.StraightBoxIndicator border_alpha = 255 def __init__(self, engine): @@ -36,7 +36,10 @@ def add_document(self, document): ) self.signal_mapper.setMapping(document, document) - self.signal_mapper.mapped[QWidget].connect(self.update_highlighter) + if Qt.IsPyQt4: + self.signal_mapper.mapped[QWidget].connect(self.update_highlighter) + else: + self.signal_mapper.mappedObject.connect(self.update_highlighter) document.selectionChanged.connect(self.signal_mapper.map) def clear_markings(self, document): diff --git a/preditor/scintilla/delayables/spell_check.py b/preditor/scintilla/delayables/spell_check.py index 1838edbb..75c8349a 100644 --- a/preditor/scintilla/delayables/spell_check.py +++ b/preditor/scintilla/delayables/spell_check.py @@ -4,12 +4,11 @@ import re import string -from PyQt5.Qsci import QsciScintilla from Qt.QtCore import Qt from Qt.QtGui import QColor from ...delayable_engine.delayables import RangeDelayable -from .. import lang +from .. import QsciScintilla, lang logger = logging.getLogger(__name__) @@ -49,12 +48,14 @@ def add_document(self, document): # https://www.scintilla.org/ScintillaDox.html#SCI_INDICSETSTYLE # https://qscintilla.com/#clickable_text/indicators document.indicatorDefine( - QsciScintilla.SquiggleLowIndicator, self.indicator_number + QsciScintilla.IndicatorStyle.SquiggleLowIndicator, self.indicator_number ) document.SendScintilla( QsciScintilla.SCI_SETINDICATORCURRENT, self.indicator_number ) - document.setIndicatorForegroundColor(QColor(Qt.red), self.indicator_number) + document.setIndicatorForegroundColor( + QColor(Qt.GlobalColor.red), self.indicator_number + ) document.SCN_MODIFIED.connect(document.onTextModified) diff --git a/preditor/scintilla/documenteditor.py b/preditor/scintilla/documenteditor.py index f49e0dda..0f67b3c8 100644 --- a/preditor/scintilla/documenteditor.py +++ b/preditor/scintilla/documenteditor.py @@ -19,11 +19,9 @@ from contextlib import contextmanager from functools import partial -from PyQt5.Qsci import QsciScintilla -from PyQt5.QtCore import QTextCodec -from Qt import QtCompat -from Qt.QtCore import Property, QFile, QPoint, Qt, Signal -from Qt.QtGui import QColor, QFont, QFontMetrics, QIcon +import Qt as Qt_py +from Qt.QtCore import Property, QEvent, QPoint, Qt, Signal +from Qt.QtGui import QColor, QFont, QFontMetrics, QIcon, QKeySequence from Qt.QtWidgets import ( QAction, QApplication, @@ -37,7 +35,8 @@ from ..delayable_engine import DelayableEngine from ..enum import Enum, EnumGroup from ..gui import QtPropertyInit -from . import lang +from ..gui.workbox_mixin import WorkboxMixin +from . import QsciScintilla, lang logger = logging.getLogger(__name__) @@ -90,7 +89,7 @@ def __init__(self, parent, filename='', lineno=0, delayable_engine='default'): self.additionalFilenames = [] self._language = '' self._lastSearch = '' - self._textCodec = None + self._encoding = 'utf-8' self._fileMonitoringActive = False self._marginsFont = self._defaultFont self._lastSearchDirection = SearchDirection.First @@ -99,21 +98,21 @@ def __init__(self, parent, filename='', lineno=0, delayable_engine='default'): self._enableFontResizing = True # QSci doesnt provide accessors to these values, so store them internally self._foldMarginBackgroundColor = QColor(224, 224, 224) - self._foldMarginForegroundColor = QColor(Qt.white) + self._foldMarginForegroundColor = QColor(Qt.GlobalColor.white) self._marginsBackgroundColor = QColor(224, 224, 224) self._marginsForegroundColor = QColor() self._matchedBraceBackgroundColor = QColor(224, 224, 224) self._matchedBraceForegroundColor = QColor() - self._unmatchedBraceBackgroundColor = QColor(Qt.white) - self._unmatchedBraceForegroundColor = QColor(Qt.blue) + self._unmatchedBraceBackgroundColor = QColor(Qt.GlobalColor.white) + self._unmatchedBraceForegroundColor = QColor(Qt.GlobalColor.blue) self._caretForegroundColor = QColor() self._caretBackgroundColor = QColor(255, 255, 255, 255) self._selectionBackgroundColor = QColor(192, 192, 192) - self._selectionForegroundColor = QColor(Qt.black) - self._indentationGuidesBackgroundColor = QColor(Qt.white) - self._indentationGuidesForegroundColor = QColor(Qt.black) - self._markerBackgroundColor = QColor(Qt.white) - self._markerForegroundColor = QColor(Qt.black) + self._selectionForegroundColor = QColor(Qt.GlobalColor.black) + self._indentationGuidesBackgroundColor = QColor(Qt.GlobalColor.white) + self._indentationGuidesForegroundColor = QColor(Qt.GlobalColor.black) + self._markerBackgroundColor = QColor(Qt.GlobalColor.white) + self._markerForegroundColor = QColor(Qt.GlobalColor.black) # Setup the DelayableEngine and add the document to it self.delayable_info = OrderedDict() @@ -134,13 +133,13 @@ def __init__(self, parent, filename='', lineno=0, delayable_engine='default'): self.initSettings(first_time=True) # set one time properties - self.setFolding(QsciScintilla.BoxedTreeFoldStyle) - self.setBraceMatching(QsciScintilla.SloppyBraceMatch) - self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setFolding(QsciScintilla.FoldStyle.BoxedTreeFoldStyle) + self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.setAcceptDrops(False) # Not supported by older builds of QsciScintilla if hasattr(self, 'setTabDrawMode'): - self.setTabDrawMode(QsciScintilla.TabStrikeOut) + self.setTabDrawMode(QsciScintilla.TabDrawMode.TabStrikeOut) # create connections self.customContextMenuRequested.connect(self.showMenu) @@ -173,23 +172,55 @@ def __init__(self, parent, filename='', lineno=0, delayable_engine='default'): commands = self.standardCommands() # Remove the Ctrl+/ "Move left one word part" shortcut so it can be used to # comment - command = commands.boundTo(Qt.ControlModifier | Qt.Key_Slash) + if Qt_py.IsPyQt6: + # In Qt6 enums are not longer simple ints. boundTo still requires ints + def to_int(shortcut): + return shortcut.toCombined() + + else: + + def to_int(shortcut): + return shortcut + + command = commands.boundTo( + to_int(Qt.KeyboardModifier.ControlModifier | Qt.Key.Key_Slash) + ) if command is not None: command.setKey(0) for command in commands.commands(): if command.description() == 'Move selected lines up one line': - command.setKey(Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_Up) + command.setKey( + to_int( + Qt.KeyboardModifier.ControlModifier + | Qt.KeyboardModifier.ShiftModifier + | Qt.Key.Key_Up + ) + ) if command.description() == 'Move selected lines down one line': - command.setKey(Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_Down) + command.setKey( + to_int( + Qt.KeyboardModifier.ControlModifier + | Qt.KeyboardModifier.ShiftModifier + | Qt.Key.Key_Down + ) + ) if command.description() == 'Duplicate selection': - command.setKey(Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_D) + command.setKey( + to_int( + Qt.KeyboardModifier.ControlModifier + | Qt.KeyboardModifier.ShiftModifier + | Qt.Key.Key_D + ) + ) if command.description() == 'Cut current line': command.setKey(0) # Add QShortcuts self.uiShowAutoCompleteSCT = QShortcut( - Qt.CTRL | Qt.Key_Space, self, context=Qt.WidgetShortcut + QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Space), + self, + context=Qt.ShortcutContext.WidgetShortcut, ) self.uiShowAutoCompleteSCT.activated.connect(lambda: self.showAutoComplete()) @@ -227,11 +258,13 @@ def checkForSave(self): self.window(), 'Save changes to...', 'Do you want to save your changes?', - QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, + QMessageBox.StandardButton.Yes + | QMessageBox.StandardButton.No + | QMessageBox.StandardButton.Cancel, ) - if result == QMessageBox.Yes: + if result == QMessageBox.StandardButton.Yes: return self.save() - elif result == QMessageBox.Cancel: + elif result == QMessageBox.StandardButton.Cancel: return False return True @@ -508,22 +541,22 @@ def detectEndLine(self, text): if newlineN != -1 and newlineR != -1: if newlineN == newlineR + 1: # CR LF Windows - return self.EolWindows + return QsciScintilla.EolMode.EolWindows elif newlineR == newlineN + 1: - # LF CR ACorn and RISC unsuported + # LF CR ACorn and RISC unsupported return self.eolMode() if newlineN != -1 and newlineR != -1: if newlineN < newlineR: # First return is a LF - return self.EolUnix + return QsciScintilla.EolMode.EolUnix else: # first return is a CR - return self.EolMac + return QsciScintilla.EolMode.EolMac if newlineN != -1: - return self.EolUnix + return QsciScintilla.EolMode.EolUnix if sys.platform == 'win32': - return self.EolWindows - return self.EolUnix + return QsciScintilla.EolMode.EolWindows + return QsciScintilla.EolMode.EolUnix def editPermaHighlight(self): text, success = QInputDialog.getText( @@ -564,7 +597,7 @@ def enableTitleUpdate(self): self.modificationChanged.connect(self.refreshTitle) def eventFilter(self, object, event): - if event.type() == event.Close and not self.checkForSave(): + if event.type() == QEvent.Type.Close and not self.checkForSave(): event.ignore() return True return False @@ -657,14 +690,8 @@ def lineMarginWidth(self): def load(self, filename): filename = str(filename) if filename and os.path.exists(filename): - f = QFile(filename) - f.open(QFile.ReadOnly) - text = f.readAll() - self._textCodec = QTextCodec.codecForUtfText( - text, QTextCodec.codecForName('UTF-8') - ) - self.setText(self._textCodec.toUnicode(text)) - f.close() + self._encoding, text = WorkboxMixin.__open_file__(filename) + self.setText(text) self.updateFilename(filename) self.enableFileWatching(True) self.setEolMode(self.detectEndLine(self.text())) @@ -719,14 +746,14 @@ def find_simple(self, find_state): if find_state.start_pos == find_state.end_pos: return -1 - self.SendScintilla(self.SCI_SETTARGETSTART, find_state.start_pos) - self.SendScintilla(self.SCI_SETTARGETEND, find_state.end_pos) + self.SendScintilla(QsciScintilla.SCI_SETTARGETSTART, find_state.start_pos) + self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, find_state.end_pos) # scintilla can't match unicode strings, even in python 3 # In python 3 you have to cast it to a bytes object expr = bytes(str(find_state.expr).encode("utf-8")) - return self.SendScintilla(self.SCI_SEARCHINTARGET, len(expr), expr) + return self.SendScintilla(QsciScintilla.SCI_SEARCHINTARGET, len(expr), expr) def find_text(self, find_state): """Finds text in the document without changing the selection. @@ -739,10 +766,10 @@ def find_text(self, find_state): https://github.com/josephwilk/qscintilla/blob/master/Qt4Qt5/qsciscintilla.cpp """ # Set the search flags - self.SendScintilla(self.SCI_SETSEARCHFLAGS, find_state.flags) + self.SendScintilla(QsciScintilla.SCI_SETSEARCHFLAGS, find_state.flags) # If no end was specified, use the end of the document if find_state.end_pos is None: - find_state.end_pos = self.SendScintilla(self.SCI_GETLENGTH) + find_state.end_pos = self.SendScintilla(QsciScintilla.SCI_GETLENGTH) pos = self.find_simple(find_state) @@ -751,12 +778,14 @@ def find_text(self, find_state): if find_state.forward: find_state.start_pos = 0 if find_state.start_pos_original is None: - find_state.end_pos = self.SendScintilla(self.SCI_GETLENGTH) + find_state.end_pos = self.SendScintilla(QsciScintilla.SCI_GETLENGTH) else: find_state.end_pos = find_state.start_pos_original else: if find_state.start_pos_original is None: - find_state.start_pos = self.SendScintilla(self.SCI_GETLENGTH) + find_state.start_pos = self.SendScintilla( + QsciScintilla.SCI_GETLENGTH + ) else: find_state.start_pos = find_state.start_pos_original find_state.end_pos = 0 @@ -769,8 +798,8 @@ def find_text(self, find_state): return -1, 0 # It was found. - target_start = self.SendScintilla(self.SCI_GETTARGETSTART) - target_end = self.SendScintilla(self.SCI_GETTARGETEND) + target_start = self.SendScintilla(QsciScintilla.SCI_GETTARGETSTART) + target_end = self.SendScintilla(QsciScintilla.SCI_GETTARGETEND) # Finally adjust the start position so that we don't find the same one again. if find_state.forward: @@ -818,10 +847,12 @@ def findTextNotFound(self, text): self, 'No Text Found', msg % (text, line), - buttons=(QMessageBox.Yes | QMessageBox.No), - defaultButton=QMessageBox.Yes, + buttons=( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ), + defaultButton=QMessageBox.StandardButton.Yes, ) - if result == QMessageBox.Yes: + if result == QMessageBox.StandardButton.Yes: self.goToLine(line) except ValueError: QMessageBox.critical( @@ -830,20 +861,20 @@ def findTextNotFound(self, text): def keyPressEvent(self, event): key = event.key() - if key == Qt.Key_Backtab: + if key == Qt.Key.Key_Backtab: self.unindentSelection() - elif key == Qt.Key_Escape: + elif key == Qt.Key.Key_Escape: # Using QShortcut for Escape did not seem to work. self.showAutoComplete(True) else: return QsciScintilla.keyPressEvent(self, event) def keyReleaseEvent(self, event): - if event.key() == Qt.Key_Menu: + if event.key() == Qt.Key.Key_Menu: # Calculate the screen coordinates of the text cursor. position = self.positionFromLineIndex(*self.getCursorPosition()) - x = self.SendScintilla(self.SCI_POINTXFROMPOSITION, 0, position) - y = self.SendScintilla(self.SCI_POINTYFROMPOSITION, 0, position) + x = self.SendScintilla(QsciScintilla.SCI_POINTXFROMPOSITION, 0, position) + y = self.SendScintilla(QsciScintilla.SCI_POINTYFROMPOSITION, 0, position) # When using the menu key, show the right click menu at the text # cursor, not the mouse cursor, it is not in the correct place. self.showMenu(QPoint(x, y)) @@ -867,15 +898,20 @@ def initSettings(self, first_time=False): self.setShowSmartHighlighting(True) self.setBackspaceUnindents(True) - self.setEdgeMode(self.EdgeNone) + self.setEdgeMode(QsciScintilla.EdgeMode.EdgeNone) - # set autocompletion settings - self.setAutoCompletionSource(QsciScintilla.AcsAll) + # set auto-completion settings + self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAll) self.setAutoCompletionThreshold(3) self.setFont(self.documentFont) self.setMarginsFont(self.marginsFont()) - self.setMarginWidth(0, QFontMetrics(self.marginsFont()).width('0000000') + 5) + metric = QFontMetrics(self.marginsFont()) + if Qt_py.IsPyQt4: + width = metric.width('0000000') + else: + width = metric.horizontalAdvance('0000000') + self.setMarginWidth(0, width + 5) def markerNext(self): line, index = self.getCursorPosition() @@ -910,15 +946,15 @@ def marginsFont(self): def multipleSelection(self): """Returns if multiple selection is enabled.""" - return self.SendScintilla(self.SCI_GETMULTIPLESELECTION) + return self.SendScintilla(QsciScintilla.SCI_GETMULTIPLESELECTION) def multipleSelectionAdditionalSelectionTyping(self): """Returns if multiple selection allows additional typing.""" - return self.SendScintilla(self.SCI_GETMULTIPLESELECTION) + return self.SendScintilla(QsciScintilla.SCI_GETMULTIPLESELECTION) def multipleSelectionMultiPaste(self): """Paste into all multiple selections.""" - return self.SendScintilla(self.SCI_GETMULTIPASTE) + return self.SendScintilla(QsciScintilla.SCI_GETMULTIPASTE) def paste(self): text = QApplication.clipboard().text() @@ -926,9 +962,9 @@ def paste(self): return super(DocumentEditor, self).paste() def repForMode(mode): - if mode == self.EolWindows: + if mode == QsciScintilla.EolMode.EolWindows: return '\r\n' - elif mode == self.EolUnix: + elif mode == QsciScintilla.EolMode.EolUnix: return '\n' else: return '\r' @@ -987,11 +1023,11 @@ def reloadChange(self): self.window(), 'File Removed...', 'File: %s has been deleted.\nKeep file in editor?' % self.filename(), - QMessageBox.Yes, - QMessageBox.No, + QMessageBox.StandardButton.Yes, + QMessageBox.StandardButton.No, ) self._dialogShown = False - if result == QMessageBox.No: + if result == QMessageBox.StandardButton.No: logger.debug( 'The file was deleted, removing document from editor', ) @@ -1013,13 +1049,16 @@ def reloadDialog(self, message, title='Reload File...'): if not self._dialogShown: self._dialogShown = True if self._autoReloadOnChange or not self.isModified(): - result = QMessageBox.Yes + result = QMessageBox.StandardButton.Yes else: result = QMessageBox.question( - self.window(), title, message, QMessageBox.Yes | QMessageBox.No + self.window(), + title, + message, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) self._dialogShown = False - if result == QMessageBox.Yes: + if result == QMessageBox.StandardButton.Yes: return self.load(self.filename()) return False @@ -1037,7 +1076,9 @@ def replace(self, text, searchtext=None, all=False): # replace all of the instances of the text if all: - count = self.text().count(searchtext, Qt.CaseInsensitive) + count = self.text().count( + searchtext, Qt.CaseSensitivity.CaseInsensitive + ) found = 0 while self.findFirst(searchtext, False, False, False, True, True): if found == count: @@ -1084,32 +1125,28 @@ def saveAs(self, filename='', setFilename=True): if not filename: newFile = True filename = self.filename() - filename, extFilter = QtCompat.QFileDialog.getSaveFileName( + filename, extFilter = Qt_py.QtCompat.QFileDialog.getSaveFileName( self.window(), 'Save File as...', filename ) if filename: self._saveTimer = time.time() # save the file to disk - f = QFile(filename) - f.open(QFile.WriteOnly) - # make sure the file is writeable - if f.error() != QFile.NoError: - logger.debug('An error occured while saving') + try: + txt = self.text() + WorkboxMixin.__write_file__(filename, txt, encoding=self._encoding) + with open(filename, "w", encoding=self._encoding) as f: + f.write(self.text()) + except PermissionError as error: + logger.debug('An error occurred while saving') QMessageBox.question( self.window(), 'Error saving file...', - 'There was a error saving the file. Error Code: %i' % f.error(), - QMessageBox.Ok, + 'There was a error saving the file. Error: {}'.format(error), + QMessageBox.StandardButton.Ok, ) - f.close() return False - # Attempt to save the file using the same codec that it used to display it - if self._textCodec: - f.write(self._textCodec.fromUnicode(self.text())) - else: - self.write(f) - f.close() + # notify that the document was saved self.documentSaved.emit(self, filename) @@ -1165,8 +1202,8 @@ def is_word(self, start, end): return False # Get the word at the start of selection, if the selection doesn't match # its not a word. - start_pos = self.SendScintilla(self.SCI_WORDSTARTPOSITION, start, True) - end_pos = self.SendScintilla(self.SCI_WORDENDPOSITION, start, True) + start_pos = self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, start, True) + end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, start, True) return start == start_pos and end == end_pos @@ -1227,7 +1264,9 @@ def setLexer(self, lexer): # from a wordCharactersOverride lexer to a lexer that doesn't define custom # wordCharacters. wordCharacters = self.wordCharacters() - self.SendScintilla(self.SCI_SETWORDCHARS, wordCharacters.encode('utf8')) + self.SendScintilla( + QsciScintilla.SCI_SETWORDCHARS, wordCharacters.encode('utf8') + ) if lexer: lexer.setFont(font) @@ -1250,7 +1289,7 @@ def setMultipleSelection(self, state): ranges by holding down the Ctrl key while dragging with the mouse. """ - self.SendScintilla(self.SCI_SETMULTIPLESELECTION, state) + self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, state) def setMultipleSelectionAdditionalSelectionTyping(self, state): """Enables or disables multiple selection allows additional typing. @@ -1261,7 +1300,7 @@ def setMultipleSelectionAdditionalSelectionTyping(self, state): simultaneously. Also allows selection and word and line deletion commands. """ - self.SendScintilla(self.SCI_SETADDITIONALSELECTIONTYPING, state) + self.SendScintilla(QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, state) def setMultipleSelectionMultiPaste(self, state): """Enables or disables multiple selection allows additional typing. @@ -1272,7 +1311,7 @@ def setMultipleSelectionMultiPaste(self, state): into each selection with self.SC_MULTIPASTE_EACH. self.SC_MULTIPASTE_ONCE is the default. """ - self.SendScintilla(self.SCI_SETMULTIPASTE, state) + self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, state) def setSmartHighlightingRegEx( self, exp=r'[ \t\n\r\.,?;:!()\[\]+\-\*\/#@^%$"\\~&{}|=<>\']' @@ -1300,9 +1339,9 @@ def setShowSmartHighlighting(self, state): def setShowWhitespaces(self, state): if state: - self.setWhitespaceVisibility(QsciScintilla.WsVisible) + self.setWhitespaceVisibility(QsciScintilla.WhitespaceVisibility.WsVisible) else: - self.setWhitespaceVisibility(QsciScintilla.WsInvisible) + self.setWhitespaceVisibility(QsciScintilla.WhitespaceVisibility.WsInvisible) def spellCheckEnabled(self): """Is spellcheck is enabled for this document.""" @@ -1320,13 +1359,13 @@ def addWordToDict(self, word): self.__speller__.saveAllwords() self.spellCheck(0, None) self.pos += len(word) - self.SendScintilla(self.SCI_GOTOPOS, self.pos) + self.SendScintilla(QsciScintilla.SCI_GOTOPOS, self.pos) def correctSpelling(self, action): - self.SendScintilla(self.SCI_GOTOPOS, self.pos) - self.SendScintilla(self.SCI_SETANCHOR, self.anchor) + self.SendScintilla(QsciScintilla.SCI_GOTOPOS, self.pos) + self.SendScintilla(QsciScintilla.SCI_SETANCHOR, self.anchor) with undo_step(self): - self.SendScintilla(self.SCI_REPLACESEL, action.text()) + self.SendScintilla(QsciScintilla.SCI_REPLACESEL, action.text()) def spellCheck(self, start_pos, end_pos): """Check spelling for some text in the document. @@ -1360,18 +1399,20 @@ def onTextModified( or (mtype & self.SC_MOD_DELETETEXT) == self.SC_MOD_DELETETEXT ): # Only spell-check if text was inserted/deleted - line = self.SendScintilla(self.SCI_LINEFROMPOSITION, pos) + line = self.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, pos) # More than one line could have been inserted. # If this number is negative it will cause Qt to crash. lines_to_check = line + max(0, linesAdded) self.spellCheck( - self.SendScintilla(self.SCI_POSITIONFROMLINE, line), - self.SendScintilla(self.SCI_GETLINEENDPOSITION, lines_to_check), + self.SendScintilla(QsciScintilla.SCI_POSITIONFROMLINE, line), + self.SendScintilla( + QsciScintilla.SCI_GETLINEENDPOSITION, lines_to_check + ), ) def showAutoComplete(self, toggle=False): # if using autoComplete toggle the autoComplete list - if self.autoCompletionSource() == QsciScintilla.AcsAll: + if self.autoCompletionSource() == QsciScintilla.AutoCompletionSource.AcsAll: if self.isListActive(): # is the autoComplete list visible if toggle: self.cancelList() # Close the autoComplete list @@ -1389,9 +1430,11 @@ def showMenu(self, pos, popup=True): x = point.x() y = point.y() wordUnderMouse = self.wordAtPoint(point) - positionMouse = self.SendScintilla(self.SCI_POSITIONFROMPOINT, x, y) + positionMouse = self.SendScintilla( + QsciScintilla.SCI_POSITIONFROMPOINT, x, y + ) wordStartPosition = self.SendScintilla( - self.SCI_WORDSTARTPOSITION, positionMouse, True + QsciScintilla.SCI_WORDSTARTPOSITION, positionMouse, True ) spell_check = self.delayable_engine.delayables['spell_check'] results = spell_check.chunk_re.findall( @@ -1556,7 +1599,9 @@ def showSmartHighlighting(self): return self.delayable_engine.delayable_enabled('smart_highlight') def showWhitespaces(self): - return self.whitespaceVisibility() == QsciScintilla.WsVisible + return ( + self.whitespaceVisibility() == QsciScintilla.WhitespaceVisibility.WsVisible + ) def smartHighlightingRegEx(self): return self._smartHighlightingRegEx @@ -1570,7 +1615,10 @@ def toLower(self): self.setSelection(lineFrom, indexFrom, lineTo, indexTo) def toggleFolding(self): - self.foldAll(QApplication.instance().keyboardModifiers() == Qt.ShiftModifier) + self.foldAll( + QApplication.instance().keyboardModifiers() + == Qt.KeyboardModifier.ShiftModifier + ) def toUpper(self): with undo_step(self): @@ -1710,10 +1758,8 @@ def updateSelectionInfo(self): epos=epos, lineCount=eline - sline + 1, ) - if self._textCodec and self._textCodec.name() != 'System': - text = 'Encoding: {enc} {text}'.format( - enc=self._textCodec.name(), text=text - ) + if self._encoding: + text = 'Encoding: {enc} {text}'.format(enc=self._encoding, text=text) window.uiCursorInfoLBL.setText(text) def setAutoReloadOnChange(self, state): @@ -1754,7 +1800,10 @@ def windowTitle(self): return title def wheelEvent(self, event): - if self._enableFontResizing and event.modifiers() == Qt.ControlModifier: + if ( + self._enableFontResizing + and event.modifiers() == Qt.KeyboardModifier.ControlModifier + ): # If used in LoggerWindow, use that wheel event # May not want to import LoggerWindow, so perhaps # check by str(type()) diff --git a/preditor/scintilla/finddialog.py b/preditor/scintilla/finddialog.py index 86a856ea..59686925 100644 --- a/preditor/scintilla/finddialog.py +++ b/preditor/scintilla/finddialog.py @@ -35,10 +35,10 @@ def __init__(self, parent): def eventFilter(self, object, event): from Qt.QtCore import QEvent, Qt - if event.type() == QEvent.KeyPress: + if event.type() == QEvent.Type.KeyPress: if ( - event.key() in (Qt.Key_Enter, Qt.Key_Return) - and not event.modifiers() == Qt.ShiftModifier + event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) + and not event.modifiers() == Qt.KeyboardModifier.ShiftModifier ): self.parent().uiFindNextACT.triggered.emit(True) self.accept() diff --git a/preditor/scintilla/lang/language.py b/preditor/scintilla/lang/language.py index c75d2b34..4b06fe72 100644 --- a/preditor/scintilla/lang/language.py +++ b/preditor/scintilla/lang/language.py @@ -8,7 +8,7 @@ except ImportError: from ConfigParser import ConfigParser -from PyQt5 import Qsci +from .. import Qsci class MethodDescriptor(object): diff --git a/preditor/scintilla/lexers/cpplexer.py b/preditor/scintilla/lexers/cpplexer.py index 43bc1046..e08c45b7 100644 --- a/preditor/scintilla/lexers/cpplexer.py +++ b/preditor/scintilla/lexers/cpplexer.py @@ -1,10 +1,11 @@ from __future__ import absolute_import -from PyQt5.Qsci import QsciLexerCPP from Qt.QtGui import QColor +from .. import Qsci -class CppLexer(QsciLexerCPP): + +class CppLexer(Qsci.QsciLexerCPP): # Items in this list will be highlighted using the color for self.KeywordSet2 highlightedKeywords = '' diff --git a/preditor/scintilla/lexers/javascriptlexer.py b/preditor/scintilla/lexers/javascriptlexer.py index 20531d05..e0e7ddf9 100644 --- a/preditor/scintilla/lexers/javascriptlexer.py +++ b/preditor/scintilla/lexers/javascriptlexer.py @@ -1,11 +1,13 @@ from __future__ import absolute_import -from PyQt5.Qsci import QsciLexerJavaScript from Qt.QtGui import QColor +from .. import Qsci -class JavaScriptLexer(QsciLexerJavaScript): - # Items in this list will be highlighted using the color for self.KeywordSet2 + +class JavaScriptLexer(Qsci.QsciLexerJavaScript): + # Items in this list will be highlighted using the color for + # `Qsci.QsciLexerJavaScript.KeywordSet2` highlightedKeywords = '' def defaultFont(self, index): @@ -13,7 +15,7 @@ def defaultFont(self, index): return self.font(0) def defaultPaper(self, style): - if style == self.KeywordSet2: + if style == Qsci.QsciLexerJavaScript.KeywordSet2: # Set the highlight color for this lexer return QColor(155, 255, 155) return super(JavaScriptLexer, self).defaultPaper(style) diff --git a/preditor/scintilla/lexers/maxscriptlexer.py b/preditor/scintilla/lexers/maxscriptlexer.py index 89dc3d63..01c56655 100644 --- a/preditor/scintilla/lexers/maxscriptlexer.py +++ b/preditor/scintilla/lexers/maxscriptlexer.py @@ -4,7 +4,8 @@ from builtins import str as text from future.utils import iteritems -from PyQt5.Qsci import QsciLexerCustom, QsciScintilla + +from .. import Qsci, QsciScintilla MS_KEYWORDS = """ if then else not and or key collect @@ -17,12 +18,12 @@ """ -class MaxscriptLexer(QsciLexerCustom): +class MaxscriptLexer(Qsci.QsciLexerCustom): # Items in this list will be highligheded using the color for self.SmartHighlight highlightedKeywords = '' def __init__(self, parent=None): - QsciLexerCustom.__init__(self, parent) + super(MaxscriptLexer, self).__init__(parent) self._styles = { 0: 'Default', 1: 'Comment', @@ -48,15 +49,15 @@ def defaultColor(self, style): return QColor(40, 160, 40) elif style in (self.Keyword, self.Operator): - return QColor(Qt.blue) + return QColor(Qt.GlobalColor.blue) elif style == self.Number: - return QColor(Qt.red) + return QColor(Qt.GlobalColor.red) elif style == self.String: return QColor(180, 140, 30) - return QsciLexerCustom.defaultColor(self, style) + return super(MaxscriptLexer, self).defaultColor(style) def defaultPaper(self, style): if style == self.SmartHighlight: @@ -77,7 +78,7 @@ def keywords(self, style): return MS_KEYWORDS if style == self.SmartHighlight: return self.highlightedKeywords - return QsciLexerCustom.keywords(self, style) + return super(MaxscriptLexer, self).keywords(style) def processChunk(self, chunk, lastState, keywords): # process the length of the chunk diff --git a/preditor/scintilla/lexers/mellexer.py b/preditor/scintilla/lexers/mellexer.py index 41ec7147..fc16264d 100644 --- a/preditor/scintilla/lexers/mellexer.py +++ b/preditor/scintilla/lexers/mellexer.py @@ -2,9 +2,10 @@ import re -from PyQt5.Qsci import QsciLexerCPP from Qt.QtGui import QColor +from .. import Qsci + MEL_SYNTAX = """and array as case catch continue do else exit float for from global if in int local not of off on or proc random return select string then throw to try vector when where while with true false @@ -310,7 +311,7 @@ """ -class MelLexer(QsciLexerCPP): +class MelLexer(Qsci.QsciLexerCPP): # Items in this list will be highlighted using the color for self.GlobalClass _highlightedKeywords = '' # Mel uses $varName for variables, so we have to allow them in words diff --git a/preditor/scintilla/lexers/mulexer.py b/preditor/scintilla/lexers/mulexer.py index 1e8855bb..19fb62d1 100644 --- a/preditor/scintilla/lexers/mulexer.py +++ b/preditor/scintilla/lexers/mulexer.py @@ -1,14 +1,15 @@ from __future__ import absolute_import -from PyQt5.Qsci import QsciLexerCPP from Qt.QtGui import QColor +from .. import Qsci + MU_KEYWORDS = """ method string Color use require module for_each let global function nil void """ -class MuLexer(QsciLexerCPP): +class MuLexer(Qsci.QsciLexerCPP): # Items in this list will be highlighted using the color for self.KeywordSet2 highlightedKeywords = '' diff --git a/preditor/scintilla/lexers/pythonlexer.py b/preditor/scintilla/lexers/pythonlexer.py index 07754549..991f228d 100644 --- a/preditor/scintilla/lexers/pythonlexer.py +++ b/preditor/scintilla/lexers/pythonlexer.py @@ -1,22 +1,23 @@ from __future__ import absolute_import -from PyQt5.Qsci import QsciLexerPython from Qt.QtGui import QColor +from .. import Qsci -class PythonLexer(QsciLexerPython): - # Items in this list will be highlighted using the color for - # self.HighlightedIdentifier + +class PythonLexer(Qsci.QsciLexerPython): + # Items in this list will be highlighted using the color + # for Qsci.QsciLexerPython.HighlightedIdentifier. highlightedKeywords = '' def __init__(self, *args): super(PythonLexer, self).__init__(*args) # set the indentation warning - self.setIndentationWarning(self.Inconsistent) + self.setIndentationWarning(Qsci.QsciLexerPython.IndentationWarning.Inconsistent) def defaultPaper(self, style): - if style == self.HighlightedIdentifier: + if style == Qsci.QsciLexerPython.HighlightedIdentifier: # Set the highlight color for this lexer return QColor(155, 255, 155) return super(PythonLexer, self).defaultPaper(style) diff --git a/preditor/utils/cute.py b/preditor/utils/cute.py index d6384e3b..0b893514 100644 --- a/preditor/utils/cute.py +++ b/preditor/utils/cute.py @@ -7,19 +7,20 @@ def ensureWindowIsVisible(widget): """ Checks the widget's geometry against all of the system's screens. If it does - not intersect it will reposition it to the top left corner of the highest - numbered desktop. Returns a boolean indicating if it had to move the - widget. + not intersect, it will reposition it to the top left corner of the highest + numbered screen. Returns a boolean indicating if it had to move the widget. """ - desktop = QApplication.desktop() + screens = QApplication.screens() geo = widget.geometry() - for screen in range(desktop.screenCount()): - monGeo = desktop.screenGeometry(screen) - if monGeo.intersects(geo): + + for screen in screens: + if screen.geometry().intersects(geo): break else: + monGeo = screens[-1].geometry() # Use the last screen available geo.moveTo(monGeo.x() + 7, monGeo.y() + 30) - # setting the geometry may trigger a second check if setGeometry is overridden + + # Setting the geometry may trigger a second check if setGeometry is overridden disable = hasattr(widget, 'checkScreenGeo') and widget.checkScreenGeo if disable: widget.checkScreenGeo = False diff --git a/pyproject.toml b/pyproject.toml index da171105..77653e75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ license = {text = "LGPL-3.0"} classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", @@ -22,14 +21,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] requires-python = ">=3.7" -dependencies = [ - "Qt.py", - "configparser>=4.0.2", - "future>=0.18.2", - "signalslot>=0.1.2", - "importlib-metadata>=4.8.3", -] -dynamic = ["version"] +dynamic = ["dependencies", "optional-dependencies", "version"] [project.readme] file = "README.md" @@ -40,27 +32,6 @@ Homepage = "https://github.com/blurstudio/PrEditor" Source = "https://github.com/blurstudio/PrEditor" Tracker = "https://github.com/blurstudio/PrEditor/issues" -[project.optional-dependencies] -cli =[ - "click>=7.1.2", - "click-default-group", -] -dev =[ - "black", - "build", - "covdefaults", - "coverage", - "flake8", - "flake8-bugbear", - "Flake8-pyproject", - "pep8-naming", - "pytest", - "tox", -] -shortcut =[ - "casement>=0.1.0;platform_system=='Windows'", -] - [project.scripts] preditor = "preditor.cli:cli" @@ -80,15 +51,25 @@ QScintilla = "preditor.gui.workboxwidget:WorkboxWidget" [project.entry-points."preditor.plug.logging_handlers"] PrEditor = "preditor.gui.logger_window_handler:LoggerWindowHandler" - [tool.setuptools] -include-package-data = true platforms = ["any"] license-files = ["LICENSE"] +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} + +[tool.setuptools.dynamic.optional-dependencies] +cli = {file = ["requirements-cli.txt"]} +dev = {file = ["requirements-dev.txt"]} +qsci5 = {file = ["requirements-qsci5.txt"]} +qsci6 = {file = ["requirements-qsci6.txt"]} +shortcut = {file = ["requirements-shortcut.txt"]} + [tool.setuptools.packages.find] -exclude = ["tests"] -namespaces = false +exclude = [ + "examples", + "tests", +] [tool.setuptools_scm] write_to = "preditor/version.py" diff --git a/requirements-cli.txt b/requirements-cli.txt new file mode 100644 index 00000000..d3b66e71 --- /dev/null +++ b/requirements-cli.txt @@ -0,0 +1,2 @@ +click>=7.1.2 +click-default-group diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..d7780b86 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,10 @@ +black +build +covdefaults +coverage +flake8 +flake8-bugbear +Flake8-pyproject +pep8-naming +pytest +tox diff --git a/requirements-qsci5.txt b/requirements-qsci5.txt new file mode 100644 index 00000000..9bbde904 --- /dev/null +++ b/requirements-qsci5.txt @@ -0,0 +1 @@ +QScintilla diff --git a/requirements-qsci6.txt b/requirements-qsci6.txt new file mode 100644 index 00000000..9b634eb5 --- /dev/null +++ b/requirements-qsci6.txt @@ -0,0 +1 @@ +PyQt6-QScintilla diff --git a/requirements-shortcut.txt b/requirements-shortcut.txt new file mode 100644 index 00000000..d2d285fa --- /dev/null +++ b/requirements-shortcut.txt @@ -0,0 +1 @@ +casement>=0.1.0;platform_system=='Windows' diff --git a/requirements.txt b/requirements.txt index 86b3ad81..d6085dd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -build +chardet configparser>=4.0.2 future>=0.18.2 importlib-metadata>=4.8.3 -Qt.py +Qt.py>=1.4.4 signalslot>=0.1.2