diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9432dbb01..a770dae66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -554,6 +554,9 @@ jobs: - name: Setup pip options run: setup_pip_options + - name: Install system dependencies + run: apt_get_install libdbus-1-dev libdbus-glib-1-dev libudev-dev libusb-1.0-0-dev libegl-dev + - name: Setup Python environment run: setup_python_env -c reqs/constraints.txt -r reqs/dist.txt -r reqs/dist_extra_gui_qt.txt -r reqs/test.txt - name: Build UI @@ -667,7 +670,7 @@ jobs: - name: List cache contents run: list_cache - + outputs: version: ${{ steps.set_version.outputs.version }} # }}} @@ -710,7 +713,7 @@ jobs: run: setup_pip_options - name: Install system dependencies - run: apt_get_install libdbus-1-dev libdbus-glib-1-dev libudev-dev libusb-1.0-0-dev + run: apt_get_install libdbus-1-dev libdbus-glib-1-dev libudev-dev libusb-1.0-0-dev libegl-dev - name: Setup Python environment run: setup_python_env -c reqs/constraints.txt -r reqs/build.txt -r reqs/setup.txt @@ -958,4 +961,4 @@ jobs: run: publish_github_release # }}} -# vim: foldmethod=marker foldlevel=0 \ No newline at end of file +# vim: foldmethod=marker foldlevel=0 diff --git a/.github/workflows/ci/workflow_template.yml b/.github/workflows/ci/workflow_template.yml index f5234ca70..95fb190f4 100644 --- a/.github/workflows/ci/workflow_template.yml +++ b/.github/workflows/ci/workflow_template.yml @@ -127,9 +127,9 @@ jobs: run: setup_osx_python '<@ j.python @>' <% endif %> - <% if j.type == 'build' and j.os == 'Linux' %> + <% if j.type in ['build', 'test_gui_qt'] and j.os == 'Linux' %> - name: Install system dependencies - run: apt_get_install libdbus-1-dev libdbus-glib-1-dev libudev-dev libusb-1.0-0-dev + run: apt_get_install libdbus-1-dev libdbus-glib-1-dev libudev-dev libusb-1.0-0-dev libegl-dev <% endif %> - name: Setup Python environment @@ -256,7 +256,7 @@ jobs: - name: List cache contents run: list_cache <% if j.type == 'test_packaging' %> - + outputs: version: ${{ steps.set_version.outputs.version }} <% endif %> diff --git a/MANIFEST.in b/MANIFEST.in index e2095bb7a..c0bc6501a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,10 +28,5 @@ include test/*.py include test/gui_qt/*.py include tox.ini include windows/* -# Exclude: CI/Git/GitHub specific files, -# as well as generated Python files (UI). -exclude .gitignore -exclude plover/gui_qt/*_rc.py + exclude plover/gui_qt/*_ui.py -exclude plover/gui_qt/.gitignore -prune .github diff --git a/linux/appimage/blacklist.txt b/linux/appimage/blacklist.txt index c89a7376a..14b7a9dfe 100644 --- a/linux/appimage/blacklist.txt +++ b/linux/appimage/blacklist.txt @@ -31,16 +31,14 @@ # Plover. :usr/lib/python${pyversion}/site-packages/plover gui_qt/*.ui - gui_qt/resources messages/**/*.po messages/plover.pot -# PyQt5. +# PyQt6. :usr/bin - pylupdate5 - pyrcc5 - pyuic5 -:usr/lib/python${pyversion}/site-packages/PyQt5 + pylupdate6 + pyuic6 +:usr/lib/python${pyversion}/site-packages/PyQt6 **/*Designer* **/*[Hh]elp* **/*[Qq]ml* @@ -49,19 +47,18 @@ **/*[Ww]ayland* **/*[Ww]eb[Ee]ngine* bindings - Qt5/plugins/egldeviceintegrations - Qt5/plugins/geoservices - Qt5/plugins/platforms/libqeglfs.so - Qt5/plugins/platforms/libqlinuxfb.so - Qt5/plugins/platforms/libqminimal.so - Qt5/plugins/platforms/libqminimalegl.so - Qt5/plugins/platforms/libqoffscreen.so - Qt5/plugins/platforms/libqvnc.so - Qt5/plugins/platforms/libqwebgl.so - Qt5/plugins/sceneparsers - Qt5/plugins/webview + Qt6/plugins/egldeviceintegrations + Qt6/plugins/geoservices + Qt6/plugins/platforms/libqeglfs.so + Qt6/plugins/platforms/libqlinuxfb.so + Qt6/plugins/platforms/libqminimal.so + Qt6/plugins/platforms/libqminimalegl.so + Qt6/plugins/platforms/libqoffscreen.so + Qt6/plugins/platforms/libqvnc.so + Qt6/plugins/platforms/libqwebgl.so + Qt6/plugins/sceneparsers + Qt6/plugins/webview pylupdate* - pyrcc* uic # vim: ft=config diff --git a/news.d/api/1601.break.md b/news.d/api/1601.break.md new file mode 100644 index 000000000..cc2b2ac57 --- /dev/null +++ b/news.d/api/1601.break.md @@ -0,0 +1 @@ +Update UI to PyQt6 from PyQt5 diff --git a/osx/app_resources/dist_blacklist.txt b/osx/app_resources/dist_blacklist.txt index c96eb6fe0..5d47ced4d 100644 --- a/osx/app_resources/dist_blacklist.txt +++ b/osx/app_resources/dist_blacklist.txt @@ -23,8 +23,8 @@ turtle* **/*.exe */test* -# PyQt5. -:lib/python$python_base_version/site-packages/PyQt5 +# PyQt6. +:lib/python$python_base_version/site-packages/PyQt6 **/*AxContainer* **/*Bluetooth* **/*CLucene* @@ -35,34 +35,32 @@ **/*Serial* **/*Sql* **/*Test* - Qt5/plugins/audio - Qt5/plugins/bearer - Qt5/plugins/generic - Qt5/plugins/geoservices - Qt5/plugins/mediaservice - Qt5/plugins/playlistformats - Qt5/plugins/position - Qt5/plugins/printsupport - Qt5/plugins/sceneparsers - Qt5/plugins/sensor* - Qt5/plugins/sqldrivers - Qt5/qml - Qt5/resources - Qt5/translations/qt_help_* - Qt5/translations/qtconnectivity_* - Qt5/translations/qtdeclarative_* - Qt5/translations/qtlocation_* - Qt5/translations/qtmultimedia_* - Qt5/translations/qtquick* - Qt5/translations/qtserialport_* - Qt5/translations/qtwebsockets_* + Qt6/plugins/audio + Qt6/plugins/bearer + Qt6/plugins/generic + Qt6/plugins/geoservices + Qt6/plugins/mediaservice + Qt6/plugins/playlistformats + Qt6/plugins/position + Qt6/plugins/printsupport + Qt6/plugins/sceneparsers + Qt6/plugins/sensor* + Qt6/plugins/sqldrivers + Qt6/qml + Qt6/resources + Qt6/translations/qt_help_* + Qt6/translations/qtconnectivity_* + Qt6/translations/qtdeclarative_* + Qt6/translations/qtlocation_* + Qt6/translations/qtmultimedia_* + Qt6/translations/qtquick* + Qt6/translations/qtserialport_* + Qt6/translations/qtwebsockets_* pylupdate* - pyrcc* uic # Plover. :lib/python$python_base_version/site-packages/plover gui_qt/*.ui - gui_qt/resources messages/**/*.po messages/plover.pot diff --git a/plover/gui_qt/about_dialog.py b/plover/gui_qt/about_dialog.py index c1fabe1b4..93ace6eba 100644 --- a/plover/gui_qt/about_dialog.py +++ b/plover/gui_qt/about_dialog.py @@ -1,7 +1,7 @@ import re -from PyQt5.QtWidgets import QDialog +from PyQt6.QtWidgets import QDialog import plover diff --git a/plover/gui_qt/add_translation_dialog.py b/plover/gui_qt/add_translation_dialog.py index 1973664c2..5ab15d4cd 100644 --- a/plover/gui_qt/add_translation_dialog.py +++ b/plover/gui_qt/add_translation_dialog.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QDialogButtonBox +from PyQt6.QtWidgets import QDialogButtonBox from plover import _ @@ -29,7 +29,7 @@ def __init__(self, engine, dictionary_path=None): self.finished.connect(self.save_state) def on_mapping_valid(self, valid): - self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(valid) + self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid) def on_config_changed(self, config_update): if 'translation_frame_opacity' in config_update: diff --git a/plover/gui_qt/add_translation_widget.py b/plover/gui_qt/add_translation_widget.py index 1d3643f76..144cbed7a 100644 --- a/plover/gui_qt/add_translation_widget.py +++ b/plover/gui_qt/add_translation_widget.py @@ -2,8 +2,8 @@ from html import escape as html_escape from os.path import split as os_path_split -from PyQt5.QtCore import QEvent, pyqtSignal -from PyQt5.QtWidgets import QApplication, QWidget +from PyQt6.QtCore import QEvent, pyqtSignal +from PyQt6.QtWidgets import QApplication, QWidget from plover import _ from plover.misc import shorten_path @@ -104,12 +104,12 @@ def select_dictionary(self, dictionary_path): self._update_items() def eventFilter(self, watched, event): - if event.type() == QEvent.FocusIn: + if event.type() == QEvent.Type.FocusIn: if watched == self.strokes: self._focus_strokes() elif watched == self.translation: self._focus_translation() - elif event.type() == QEvent.FocusOut: + elif event.type() == QEvent.Type.FocusOut: if watched in (self.strokes, self.translation): self._unfocus() return False diff --git a/plover/gui_qt/config_serial_widget.ui b/plover/gui_qt/config_serial_widget.ui index 3419b3565..39cb08711 100644 --- a/plover/gui_qt/config_serial_widget.ui +++ b/plover/gui_qt/config_serial_widget.ui @@ -341,7 +341,7 @@ baudrate - activated(QString) + textActivated(QString) SerialWidget on_baudrate_changed(QString) @@ -357,7 +357,7 @@ bytesize - activated(QString) + textActivated(QString) SerialWidget on_bytesize_changed(QString) @@ -373,7 +373,7 @@ parity - activated(QString) + textActivated(QString) SerialWidget on_parity_changed(QString) @@ -405,7 +405,7 @@ stopbits - activated(QString) + textActivated(QString) SerialWidget on_stopbits_changed(QString) diff --git a/plover/gui_qt/config_window.py b/plover/gui_qt/config_window.py index 7cc4b6948..5abbfc00d 100644 --- a/plover/gui_qt/config_window.py +++ b/plover/gui_qt/config_window.py @@ -3,12 +3,12 @@ from copy import copy from functools import partial -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( Qt, QVariant, pyqtSignal, ) -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QCheckBox, QComboBox, QDialog, @@ -123,9 +123,9 @@ class TableOption(QTableWidget): def __init__(self): super().__init__() self.horizontalHeader().setStretchLastSection(True) - self.setSelectionMode(self.SingleSelection) + self.setSelectionMode(self.SelectionMode.SingleSelection) self.setTabKeyNavigation(False) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.verticalHeader().hide() self.currentItemChanged.connect(self._on_current_item_changed) @@ -191,7 +191,7 @@ def setValue(self, value): row += 1 self.insertRow(row) item = QTableWidgetItem(key) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) self.setItem(row, 0, item) item = QTableWidgetItem(action) self.setItem(row, 1, item) @@ -203,8 +203,8 @@ def setValue(self, value): def _on_cell_changed(self, row, column): if self._updating: return - key = self.item(row, 0).data(Qt.DisplayRole) - action = self.item(row, 1).data(Qt.DisplayRole) + key = self.item(row, 0).data(Qt.ItemDataRole.DisplayRole) + action = self.item(row, 1).data(Qt.ItemDataRole.DisplayRole) bindings = self._value.get_bindings() if action: bindings[key] = action @@ -257,11 +257,11 @@ def setValue(self, value): row += 1 self.insertRow(row) item = QTableWidgetItem(self._choices[choice]) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) self.setItem(row, 0, item) item = QTableWidgetItem() - item.setFlags((item.flags() & ~Qt.ItemIsEditable) | Qt.ItemIsUserCheckable) - item.setCheckState(Qt.Checked if choice in value else Qt.Unchecked) + item.setFlags((item.flags() & ~Qt.ItemFlag.ItemIsEditable) | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked if choice in value else Qt.CheckState.Unchecked) self.setItem(row, 1, item) self.resizeColumnsToContents() self.setMinimumSize(self.viewportSizeHint()) @@ -271,7 +271,7 @@ def _on_cell_changed(self, row, column): if self._updating: return assert column == 1 - choice = self._reversed_choices[self.item(row, 0).data(Qt.DisplayRole)] + choice = self._reversed_choices[self.item(row, 0).data(Qt.ItemDataRole.DisplayRole)] if self.item(row, 1).checkState(): self._value.add(choice) else: @@ -437,8 +437,8 @@ def __init__(self, engine): (option_by_name[option_name], update_fn) for option_name, update_fn in option.dependents ] - self.buttons.button(QDialogButtonBox.Ok).clicked.connect(self.on_apply) - self.buttons.button(QDialogButtonBox.Apply).clicked.connect(self.on_apply) + self.buttons.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.on_apply) + self.buttons.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(self.on_apply) self.tabs.currentWidget().setFocus() self.restore_state() self.finished.connect(self.save_state) @@ -491,7 +491,7 @@ def _create_option_widget(self, option): def keyPressEvent(self, event): # Disable Enter/Return key to trigger "OK". - if event.key() in (Qt.Key_Enter, Qt.Key_Return): + if event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): return super().keyPressEvent(event) diff --git a/plover/gui_qt/dictionaries_widget.py b/plover/gui_qt/dictionaries_widget.py index b1937cd03..f45a67c92 100644 --- a/plover/gui_qt/dictionaries_widget.py +++ b/plover/gui_qt/dictionaries_widget.py @@ -1,14 +1,14 @@ from contextlib import contextmanager import os -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QAbstractListModel, QModelIndex, Qt, pyqtSignal, ) -from PyQt5.QtGui import QCursor, QIcon -from PyQt5.QtWidgets import ( +from PyQt6.QtGui import QCursor +from PyQt6.QtWidgets import ( QFileDialog, QGroupBox, QMenu, @@ -24,7 +24,7 @@ from plover.gui_qt.dictionaries_widget_ui import Ui_DictionariesWidget from plover.gui_qt.dictionary_editor import DictionaryEditor -from plover.gui_qt.utils import ToolBar +from plover.gui_qt.utils import ToolBar, Icon def _dictionary_formats(include_readonly=True): @@ -96,11 +96,13 @@ def is_loaded(self): return self.state not in {'loading', 'error'} SUPPORTED_ROLES = { - Qt.AccessibleTextRole, Qt.CheckStateRole, - Qt.DecorationRole, Qt.DisplayRole, Qt.ToolTipRole + Qt.ItemDataRole.AccessibleTextRole, Qt.ItemDataRole.CheckStateRole, + Qt.ItemDataRole.DecorationRole, Qt.ItemDataRole.DisplayRole, + Qt.ItemDataRole.ToolTipRole } - FLAGS = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable + FLAGS = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable \ + | Qt.ItemFlag.ItemIsUserCheckable has_undo_changed = pyqtSignal(bool) @@ -356,17 +358,17 @@ def rowCount(self, parent=QModelIndex()): @classmethod def flags(cls, index): - return cls.FLAGS if index.isValid() else Qt.NoItemFlags + return cls.FLAGS if index.isValid() else Qt.ItemFlag.NoItemFlags def data(self, index, role): if not index.isValid() or role not in self.SUPPORTED_ROLES: return None d = self._from_row[index.row()] - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: return d.short_path - if role == Qt.CheckStateRole: - return Qt.Checked if d.enabled else Qt.Unchecked - if role == Qt.AccessibleTextRole: + if role == Qt.ItemDataRole.CheckStateRole: + return Qt.CheckState.Checked if d.enabled else Qt.CheckState.Unchecked + if role == Qt.ItemDataRole.AccessibleTextRole: accessible_text = [d.short_path] if not d.enabled: # i18n: Widget: “DictionariesWidget”, accessible text. @@ -385,9 +387,9 @@ def data(self, index, role): # i18n: Widget: “DictionariesWidget”, accessible text. accessible_text.append(_('read-only')) return ', '.join(accessible_text) - if role == Qt.DecorationRole: + if role == Qt.ItemDataRole.DecorationRole: return self._icons.get('favorite' if d is self._favorite else d.state) - if role == Qt.ToolTipRole: + if role == Qt.ItemDataRole.ToolTipRole: # i18n: Widget: “DictionariesWidget”, tooltip. tooltip = [_('Full path: {path}.').format(path=d.config.path)] if d is self._favorite: @@ -407,11 +409,11 @@ def data(self, index, role): return None def setData(self, index, value, role): - if not index.isValid() or role != Qt.CheckStateRole: + if not index.isValid() or role != Qt.ItemDataRole.CheckStateRole: return False - if value == Qt.Checked: + if value == Qt.CheckState.Checked: enabled = True - elif value == Qt.Unchecked: + elif value == Qt.CheckState.Unchecked: enabled = False else: return False @@ -487,16 +489,16 @@ def __init__(self, *args, **kwargs): edit_menu.addSeparator() edit_menu.addAction(self.action_MoveDictionariesUp) edit_menu.addAction(self.action_MoveDictionariesDown) - self.view.setContextMenuPolicy(Qt.CustomContextMenu) + self.view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.view.customContextMenuRequested.connect( - lambda p: edit_menu.exec_(self.view.mapToGlobal(p))) + lambda p: edit_menu.exec(self.view.mapToGlobal(p))) self.edit_menu = edit_menu def setup(self, engine): assert not self._setup self._engine = engine self._model = DictionariesModel(engine, { - name: QIcon(':/dictionary_%s.svg' % name) + name: Icon(':/dictionary_%s.svg' % name) for name in 'favorite loading error readonly normal'.split() }) self._model.has_undo_changed.connect(self.on_has_undo) @@ -578,7 +580,7 @@ def _edit_dictionaries(self, index_list): if not path_list: return editor = DictionaryEditor(self._engine, path_list) - editor.exec_() + editor.exec() def _copy_dictionaries(self, dictionaries): need_reload = False @@ -661,7 +663,7 @@ def on_activate_dictionary(self, index): self._edit_dictionaries([index]) def on_add_dictionaries(self): - self.menu_AddDictionaries.exec_(QCursor.pos()) + self.menu_AddDictionaries.exec(QCursor.pos()) def on_add_translation(self): dictionary = next(self._model.iter_loaded([self.view.currentIndex()]), None) diff --git a/plover/gui_qt/dictionary_editor.py b/plover/gui_qt/dictionary_editor.py index a32a7bfe0..4c5c19b2c 100644 --- a/plover/gui_qt/dictionary_editor.py +++ b/plover/gui_qt/dictionary_editor.py @@ -3,13 +3,12 @@ from collections import namedtuple from itertools import chain -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QAbstractTableModel, QModelIndex, Qt, ) -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QComboBox, QDialog, QStyledItemDelegate, @@ -22,7 +21,7 @@ from plover.gui_qt.dictionary_editor_ui import Ui_DictionaryEditor from plover.gui_qt.steno_validator import StenoValidator -from plover.gui_qt.utils import ToolBar, WindowState +from plover.gui_qt.utils import ToolBar, WindowState, Icon _COL_STENO, _COL_TRANS, _COL_DICT, _COL_COUNT = range(3 + 1) @@ -65,7 +64,7 @@ class DictionaryItemModel(QAbstractTableModel): def __init__(self, dictionary_list, sort_column, sort_order): super().__init__() - self._error_icon = QIcon(':/dictionary_error.svg') + self._error_icon = Icon(':/dictionary_error.svg') self._dictionary_list = dictionary_list self._operations = [] self._entries = [] @@ -168,7 +167,7 @@ def columnCount(self, parent): return _COL_COUNT def headerData(self, section, orientation, role): - if orientation != Qt.Horizontal or role != Qt.DisplayRole: + if orientation != Qt.Orientation.Horizontal or role != Qt.ItemDataRole.DisplayRole: return None if section == _COL_STENO: # i18n: Widget: “DictionaryEditor”. @@ -181,11 +180,15 @@ def headerData(self, section, orientation, role): return _('Dictionary') def data(self, index, role): - if not index.isValid() or role not in (Qt.EditRole, Qt.DisplayRole, Qt.DecorationRole): + if not index.isValid() or role not in ( + Qt.ItemDataRole.EditRole, + Qt.ItemDataRole.DisplayRole, + Qt.ItemDataRole.DecorationRole + ): return None item = self._entries[index.row()] column = index.column() - if role == Qt.DecorationRole: + if role == Qt.ItemDataRole.DecorationRole: if column == _COL_STENO: try: normalize_steno(item.steno) @@ -202,10 +205,10 @@ def data(self, index, role): def flags(self, index): if not index.isValid(): return Qt.NoItemFlags - f = Qt.ItemIsEnabled | Qt.ItemIsSelectable + f = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable item = self._entries[index.row()] if not item.dictionary.readonly: - f |= Qt.ItemIsEditable + f |= Qt.ItemFlag.ItemIsEditable return f def filter(self, strokes_filter=None, translation_filter=None): @@ -226,13 +229,13 @@ def sort(self, column, order): else: key = itemgetter(column) self._entries.sort(key=key, - reverse=(order == Qt.DescendingOrder)) + reverse=(order == Qt.SortOrder.DescendingOrder)) self._sort_column = column self._sort_order = order self.layoutChanged.emit() - def setData(self, index, value, role=Qt.EditRole, record=True): - assert role == Qt.EditRole + def setData(self, index, value, role=Qt.ItemDataRole.EditRole, record=True): + assert role == Qt.ItemDataRole.EditRole row = index.row() column = index.column() old_item = self._entries[row] @@ -315,7 +318,7 @@ def __init__(self, engine, dictionary_paths): for dictionary in engine.dictionaries.dicts if dictionary.path in dictionary_paths ] - sort_column, sort_order = _COL_STENO, Qt.AscendingOrder + sort_column, sort_order = _COL_STENO, Qt.SortOrder.AscendingOrder self._model = DictionaryItemModel(dictionary_list, sort_column, sort_order) diff --git a/plover/gui_qt/engine.py b/plover/gui_qt/engine.py index 1c2d338c5..d564429df 100644 --- a/plover/gui_qt/engine.py +++ b/plover/gui_qt/engine.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QThread, QVariant, pyqtSignal, diff --git a/plover/gui_qt/log_qt.py b/plover/gui_qt/log_qt.py index ec3eaf01e..531c0897e 100644 --- a/plover/gui_qt/log_qt.py +++ b/plover/gui_qt/log_qt.py @@ -1,7 +1,7 @@ import logging -from PyQt5.QtCore import QObject, pyqtSignal +from PyQt6.QtCore import QObject, pyqtSignal from plover import log diff --git a/plover/gui_qt/lookup_dialog.py b/plover/gui_qt/lookup_dialog.py index fd868fba5..ab4268536 100644 --- a/plover/gui_qt/lookup_dialog.py +++ b/plover/gui_qt/lookup_dialog.py @@ -1,5 +1,5 @@ -from PyQt5.QtCore import QEvent, Qt +from PyQt6.QtCore import QEvent, Qt from plover import _ from plover.translation import unescape_translation @@ -28,8 +28,8 @@ def __init__(self, engine): self.finished.connect(self.save_state) def eventFilter(self, watched, event): - if event.type() == QEvent.KeyPress and \ - event.key() in (Qt.Key_Enter, Qt.Key_Return): + if event.type() == QEvent.Type.KeyPress and \ + event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): return True return False @@ -44,6 +44,6 @@ def on_lookup(self, pattern): def changeEvent(self, event): super().changeEvent(event) - if event.type() == QEvent.ActivationChange and self.isActiveWindow(): + if event.type() == QEvent.Type.ActivationChange and self.isActiveWindow(): self.pattern.setFocus() self.pattern.selectAll() diff --git a/plover/gui_qt/machine_options.py b/plover/gui_qt/machine_options.py index 3c6b69349..dcdf49024 100644 --- a/plover/gui_qt/machine_options.py +++ b/plover/gui_qt/machine_options.py @@ -1,15 +1,15 @@ from copy import copy from pathlib import Path -from PyQt5.QtCore import Qt, QVariant, pyqtSignal -from PyQt5.QtGui import ( +from PyQt6.QtCore import Qt, QVariant, pyqtSignal +from PyQt6.QtGui import ( QTextCharFormat, QTextFrameFormat, QTextListFormat, QTextCursor, QTextDocument, ) -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QGroupBox, QStyledItemDelegate, QStyle, @@ -68,19 +68,19 @@ def __init__(self): self._details_frame_format.setForeground(foreground) self._details_frame_format.setTopMargin(doc_margin) self._details_frame_format.setBottomMargin(-3 * doc_margin) - self._details_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle_Solid) + self._details_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle.BorderStyle_Solid) self._details_frame_format.setBorder(doc_margin / 2) self._details_frame_format.setPadding(doc_margin) self._details_list_format = QTextListFormat() - self._details_list_format.setStyle(QTextListFormat.ListSquare) + self._details_list_format.setStyle(QTextListFormat.Style.ListSquare) def _format_port(self, index): self._doc.clear() cursor = QTextCursor(self._doc) cursor.setCharFormat(self._device_format) - port_info = index.data(Qt.UserRole) + port_info = index.data(Qt.ItemDataRole.UserRole) if port_info is None: - cursor.insertText(index.data(Qt.DisplayRole)) + cursor.insertText(index.data(Qt.ItemDataRole.DisplayRole)) return cursor.insertText(port_info.device) details = serial_port_details(port_info) @@ -95,7 +95,7 @@ def _format_port(self, index): def paint(self, painter, option, index): painter.save() - if option.state & QStyle.State_Selected: + if option.state & QStyle.StateFlag.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) text_color = option.palette.highlightedText() else: diff --git a/plover/gui_qt/main.py b/plover/gui_qt/main.py index 49444c2fb..185fa2451 100644 --- a/plover/gui_qt/main.py +++ b/plover/gui_qt/main.py @@ -2,19 +2,17 @@ import signal import sys -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QCoreApplication, QLibraryInfo, QTimer, QTranslator, Qt, - QtDebugMsg, - QtInfoMsg, - QtWarningMsg, + QtMsgType, pyqtRemoveInputHook, qInstallMessageHandler, ) -from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt6.QtWidgets import QApplication, QMessageBox from plover import _, __name__ as __software_name__, __version__, log from plover.oslayer.config import CONFIG_DIR @@ -47,7 +45,6 @@ def __init__(self, config, controller, use_qt_notifications): QCoreApplication.setOrganizationDomain('openstenoproject.org') self._app = QApplication([sys.argv[0], '-name', 'plover']) - self._app.setAttribute(Qt.AA_UseHighDpiPixmaps) # Apply custom stylesheet if present. stylesheet_path = Path(CONFIG_DIR) / 'plover.qss' @@ -58,7 +55,7 @@ def __init__(self, config, controller, use_qt_notifications): # Enable localization of standard Qt controls. log.info('setting language to: %s', _.lang) self._translator = QTranslator() - translations_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath) + translations_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) self._translator.load('qtbase_' + _.lang, translations_dir) self._app.installTranslator(self._translator) @@ -86,7 +83,7 @@ def __del__(self): del self._translator def run(self): - self._app.exec_() + self._app.exec() return self._engine.join() @@ -102,9 +99,9 @@ def default_excepthook(*exc_info): def default_message_handler(msg_type, msg_log_context, msg_string): log_fn = { - QtDebugMsg: log.debug, - QtInfoMsg: log.info, - QtWarningMsg: log.warning, + QtMsgType.QtDebugMsg: log.debug, + QtMsgType.QtInfoMsg: log.info, + QtMsgType.QtWarningMsg: log.warning, }.get(msg_type, log.error) details = [] if msg_log_context.file is not None: diff --git a/plover/gui_qt/main_window.py b/plover/gui_qt/main_window.py index cc6de0df4..52b2f7924 100644 --- a/plover/gui_qt/main_window.py +++ b/plover/gui_qt/main_window.py @@ -4,9 +4,9 @@ import os import subprocess -from PyQt5.QtCore import QCoreApplication, Qt -from PyQt5.QtGui import QCursor, QIcon, QKeySequence -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QCoreApplication, Qt +from PyQt6.QtGui import QCursor, QKeySequence +from PyQt6.QtWidgets import ( QMainWindow, QMenu, ) @@ -22,7 +22,7 @@ from plover.gui_qt.config_window import ConfigWindow from plover.gui_qt.about_dialog import AboutDialog from plover.gui_qt.trayicon import TrayIcon -from plover.gui_qt.utils import WindowState, find_menu_actions +from plover.gui_qt.utils import Icon, WindowState, find_menu_actions class MainWindow(QMainWindow, Ui_MainWindow, WindowState): @@ -81,7 +81,7 @@ def __init__(self, engine, use_qt_notifications): self.action_Quit.triggered.connect(engine.quit) # Toolbar popup menu for selecting which tools are shown. self.toolbar_menu = QMenu() - self.toolbar.setContextMenuPolicy(Qt.CustomContextMenu) + self.toolbar.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.toolbar.customContextMenuRequested.connect( lambda: self.toolbar_menu.popup(QCursor.pos()) ) @@ -93,11 +93,7 @@ def __init__(self, engine, use_qt_notifications): if tool.SHORTCUT is not None: menu_action.setShortcut(QKeySequence.fromString(tool.SHORTCUT)) if tool.ICON is not None: - icon = tool.ICON - # Internal QT resources start with a `:`. - if not icon.startswith(':'): - icon = resource_filename(icon) - menu_action.setIcon(QIcon(icon)) + menu_action.setIcon(Icon(tool.ICON)) menu_action.triggered.connect(partial(self._activate_dialog, tool_plugin.name, args=())) toolbar_action = self.toolbar.addAction(menu_action.icon(), menu_action.text()) if tool.__doc__ is not None: diff --git a/plover/gui_qt/paper_tape.py b/plover/gui_qt/paper_tape.py index 62b180676..13a0e23e3 100644 --- a/plover/gui_qt/paper_tape.py +++ b/plover/gui_qt/paper_tape.py @@ -1,13 +1,13 @@ import time -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QAbstractListModel, QMimeData, QModelIndex, Qt, ) -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import ( +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import ( QFileDialog, QFontDialog, QMessageBox, @@ -72,12 +72,12 @@ def data(self, index, role): if not index.isValid(): return None stroke = self._stroke_list[index.row()] - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if self._style == STYLE_PAPER: return self._paper_format(stroke) if self._style == STYLE_RAW: return self._raw_format(stroke) - if role == Qt.AccessibleTextRole: + if role == Qt.ItemDataRole.AccessibleTextRole: return stroke.rtfcre return None @@ -105,7 +105,7 @@ def mimeTypes(self): def mimeData(self, indexes): data = QMimeData() data.setText('\n'.join(filter(None, ( - self.data(index, Qt.DisplayRole) + self.data(index, Qt.ItemDataRole.DisplayRole) for index in indexes )))) return data @@ -128,7 +128,7 @@ def __init__(self, engine): self.header.setContentsMargins(4, 0, 0, 0) self.styles.addItems(TAPE_STYLES) self.tape.setModel(self._model) - self.tape.setSelectionMode(self.tape.ExtendedSelection) + self.tape.setSelectionMode(self.tape.SelectionMode.ExtendedSelection) self._copy_action = ActionCopyViewSelectionToClipboard(self.tape) self.tape.addAction(self._copy_action) # Toolbar. @@ -167,7 +167,7 @@ def _restore_state(self, settings): def _save_state(self, settings): settings.setValue('style', TAPE_STYLES.index(self._style)) settings.setValue('font', self.tape.font().toString()) - ontop = bool(self.windowFlags() & Qt.WindowStaysOnTopHint) + ontop = bool(self.windowFlags() & Qt.WindowType.WindowStaysOnTopHint) settings.setValue('ontop', ontop) def on_config_changed(self, config): @@ -202,7 +202,7 @@ def on_style_changed(self, style): def on_select_font(self): font, ok = QFontDialog.getFont(self.tape.font(), self, '', - QFontDialog.MonospacedFonts) + QFontDialog.FontDialogOption.MonospacedFonts) if ok: self.header.setFont(font) self.tape.setFont(font) @@ -210,9 +210,9 @@ def on_select_font(self): def on_toggle_ontop(self, ontop): flags = self.windowFlags() if ontop: - flags |= Qt.WindowStaysOnTopHint + flags |= Qt.WindowType.WindowStaysOnTopHint else: - flags &= ~Qt.WindowStaysOnTopHint + flags &= ~Qt.WindowType.WindowStaysOnTopHint self.setWindowFlags(flags) self.show() @@ -222,8 +222,8 @@ def on_clear(self): msgbox.setText(_('Do you want to clear the paper tape?')) msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # Make sure the message box ends up above the paper tape! - msgbox.setWindowFlags(msgbox.windowFlags() | (flags & Qt.WindowStaysOnTopHint)) - if QMessageBox.Yes != msgbox.exec_(): + msgbox.setWindowFlags(msgbox.windowFlags() | (flags & Qt.WindowType.WindowStaysOnTopHint)) + if QMessageBox.Yes != msgbox.exec(): return self._strokes = [] self.action_Clear.setEnabled(False) @@ -241,4 +241,4 @@ def on_save(self): return with open(filename, 'w') as fp: for row in range(self._model.rowCount(self._model.index(-1, -1))): - print(self._model.data(self._model.index(row, 0), Qt.DisplayRole), file=fp) + print(self._model.data(self._model.index(row, 0), Qt.ItemDataRole.DisplayRole), file=fp) diff --git a/plover/gui_qt/paper_tape.ui b/plover/gui_qt/paper_tape.ui index 43b9f9fc5..13ddf0e1a 100644 --- a/plover/gui_qt/paper_tape.ui +++ b/plover/gui_qt/paper_tape.ui @@ -155,7 +155,7 @@ styles - activated(QString) + textActivated(QString) PaperTape on_style_changed() diff --git a/plover/gui_qt/resources/__init__.py b/plover/gui_qt/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plover/gui_qt/resources/resources.qrc b/plover/gui_qt/resources/resources.qrc index 17bf3904a..ddc074bfa 100644 --- a/plover/gui_qt/resources/resources.qrc +++ b/plover/gui_qt/resources/resources.qrc @@ -1,5 +1,5 @@ - + folder.svg up.svg down.svg diff --git a/plover/gui_qt/steno_validator.py b/plover/gui_qt/steno_validator.py index 7b620178f..2d76424dc 100644 --- a/plover/gui_qt/steno_validator.py +++ b/plover/gui_qt/steno_validator.py @@ -1,4 +1,4 @@ -from PyQt5.QtGui import QValidator +from PyQt6.QtGui import QValidator from plover.steno import normalize_steno @@ -7,17 +7,17 @@ class StenoValidator(QValidator): def validate(self, text, pos): if not text.strip('-/'): - state = QValidator.Intermediate + state = QValidator.State.Intermediate else: prefix = text.rstrip('-/') if text == prefix: - state = QValidator.Acceptable + state = QValidator.State.Acceptable steno = text else: - state = QValidator.Intermediate + state = QValidator.State.Intermediate steno = prefix try: normalize_steno(steno) except ValueError: - state = QValidator.Invalid + state = QValidator.State.Invalid return state, text, pos diff --git a/plover/gui_qt/suggestions_dialog.py b/plover/gui_qt/suggestions_dialog.py index 018bc15f1..ea4792389 100644 --- a/plover/gui_qt/suggestions_dialog.py +++ b/plover/gui_qt/suggestions_dialog.py @@ -1,13 +1,13 @@ import re -from PyQt5.QtCore import Qt -from PyQt5.QtGui import ( +from PyQt6.QtCore import Qt +from PyQt6.QtGui import ( + QAction, QCursor, QFont, ) -from PyQt5.QtWidgets import ( - QAction, +from PyQt6.QtWidgets import ( QFontDialog, QMenu, ) @@ -95,7 +95,7 @@ def _save_state(self, settings): font = self._get_font(name) font_string = font.toString() settings.setValue(name, font_string) - ontop = bool(self.windowFlags() & Qt.WindowStaysOnTopHint) + ontop = bool(self.windowFlags() & Qt.WindowType.WindowStaysOnTopHint) settings.setValue('ontop', ontop) def _show_suggestions(self, suggestion_list): @@ -143,7 +143,7 @@ def on_translation(self, old, new): self._show_suggestions(suggestion_list) def on_select_font(self): - action = self._font_menu.exec_(QCursor.pos()) + action = self._font_menu.exec(QCursor.pos()) if action is None: return if action == self._font_menu_text: @@ -151,7 +151,7 @@ def on_select_font(self): font_options = () elif action == self._font_menu_strokes: name = 'strokes_font' - font_options = (QFontDialog.MonospacedFonts,) + font_options = (QFontDialog.FontDialogOption.MonospacedFonts,) font = self._get_font(name) font, ok = QFontDialog.getFont(font, self, '', *font_options) if ok: @@ -160,9 +160,9 @@ def on_select_font(self): def on_toggle_ontop(self, ontop): flags = self.windowFlags() if ontop: - flags |= Qt.WindowStaysOnTopHint + flags |= Qt.WindowType.WindowStaysOnTopHint else: - flags &= ~Qt.WindowStaysOnTopHint + flags &= ~Qt.WindowType.WindowStaysOnTopHint self.setWindowFlags(flags) self.show() diff --git a/plover/gui_qt/suggestions_widget.py b/plover/gui_qt/suggestions_widget.py index c6755a25c..2a0c33d60 100644 --- a/plover/gui_qt/suggestions_widget.py +++ b/plover/gui_qt/suggestions_widget.py @@ -1,17 +1,17 @@ -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QAbstractListModel, QMimeData, QModelIndex, Qt, ) -from PyQt5.QtGui import ( +from PyQt6.QtGui import ( QFont, QFontMetrics, QTextCharFormat, QTextCursor, QTextDocument, ) -from PyQt5.QtWidgets import ( +from PyQt6.QtWidgets import ( QListView, QStyle, QStyledItemDelegate, @@ -35,7 +35,7 @@ def __init__(self, parent=None): self._doc = QTextDocument() self._translation_char_format = QTextCharFormat() self._strokes_char_format = QTextCharFormat() - self._strokes_char_format.font().setStyleHint(QFont.Monospace) + self._strokes_char_format.font().setStyleHint(QFont.StyleHint.Monospace) self._size_hint_cache = {} def clear_size_hint_cache(self): @@ -60,7 +60,7 @@ def strokes_font(self, font): self.clear_size_hint_cache() def _format_suggestion(self, index): - suggestion = index.data(Qt.DisplayRole) + suggestion = index.data(Qt.ItemDataRole.DisplayRole) translation = escape_translation(suggestion.text) + ':' if not suggestion.steno_list: translation += ' ' + NO_SUGGESTIONS_STRING @@ -79,7 +79,7 @@ def _suggestion_size_hint(self, index): def paint(self, painter, option, index): painter.save() - if option.state & QStyle.State_Selected: + if option.state & QStyle.StateFlag.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) text_color = option.palette.highlightedText() else: @@ -118,9 +118,9 @@ def data(self, index, role): if not index.isValid(): return None suggestion = self._suggestion_list[index.row()] - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: return suggestion - if role == Qt.AccessibleTextRole: + if role == Qt.ItemDataRole.AccessibleTextRole: translation = escape_translation(suggestion.text) if suggestion.steno_list: steno = ', '.join('/'.join(strokes_list) for strokes_list in @@ -147,7 +147,7 @@ def mimeTypes(self): def mimeData(self, indexes): data = QMimeData() data.setText('\n'.join(filter(None, ( - self.data(index, Qt.AccessibleTextRole) + self.data(index, Qt.ItemDataRole.AccessibleTextRole) for index in indexes )))) return data @@ -157,8 +157,8 @@ class SuggestionsWidget(QListView): def __init__(self, parent=None): super().__init__(parent=parent) - self.setResizeMode(self.Adjust) - self.setSelectionMode(self.ExtendedSelection) + self.setResizeMode(self.ResizeMode.Adjust) + self.setSelectionMode(self.SelectionMode.ExtendedSelection) self._copy_action = ActionCopyViewSelectionToClipboard(self) self.addAction(self._copy_action) self._model = SuggestionsModel() diff --git a/plover/gui_qt/tool.py b/plover/gui_qt/tool.py index efc2a6c81..e9a5be692 100644 --- a/plover/gui_qt/tool.py +++ b/plover/gui_qt/tool.py @@ -1,5 +1,5 @@ -from PyQt5.QtWidgets import QDialog +from PyQt6.QtWidgets import QDialog from plover.gui_qt.utils import WindowState diff --git a/plover/gui_qt/trayicon.py b/plover/gui_qt/trayicon.py index 9dc4451ae..de2b885a3 100644 --- a/plover/gui_qt/trayicon.py +++ b/plover/gui_qt/trayicon.py @@ -1,6 +1,5 @@ -from PyQt5.QtCore import QObject, pyqtSignal -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon +from PyQt6.QtCore import QObject, pyqtSignal +from PyQt6.QtWidgets import QMessageBox, QSystemTrayIcon from plover import _, __name__ as __software_name__ from plover import log @@ -11,6 +10,7 @@ STATE_RUNNING, STATE_ERROR, ) +from plover.gui_qt.utils import Icon class TrayIcon(QObject): @@ -27,7 +27,7 @@ def __init__(self): 'disabled', 'enabled', ): - icon = QIcon(':/state-%s.svg' % state) + icon = Icon(':/state-%s.svg' % state) if hasattr(icon, 'setIsMask'): icon.setIsMask(True) self._state_icons[state] = icon @@ -42,7 +42,7 @@ def set_menu(self, menu): self._trayicon.setContextMenu(menu) def show_message(self, message, - icon=QSystemTrayIcon.Information, + icon=QSystemTrayIcon.MessageIcon.Information, timeout=10000): self._trayicon.showMessage(__software_name__.capitalize(), message, icon, timeout) @@ -50,13 +50,13 @@ def show_message(self, message, def log(self, level, message): if self._enabled: if level <= log.INFO: - icon = QSystemTrayIcon.Information + icon = QSystemTrayIcon.MessageIcon.Information timeout = 10 elif level <= log.WARNING: - icon = QSystemTrayIcon.Warning + icon = QSystemTrayIcon.MessageIcon.Warning timeout = 15 else: - icon = QSystemTrayIcon.Critical + icon = QSystemTrayIcon.MessageIcon.Critical timeout = 25 self.show_message(message, icon, timeout * 1000) else: @@ -69,7 +69,7 @@ def log(self, level, message): msgbox = QMessageBox() msgbox.setText(message) msgbox.setIcon(icon) - msgbox.exec_() + msgbox.exec() def is_supported(self): return self._supported @@ -134,5 +134,5 @@ def _update_state(self): ) def _on_activated(self, reason): - if reason == QSystemTrayIcon.Trigger: + if reason == QSystemTrayIcon.ActivationReason.Trigger: self.clicked.emit() diff --git a/plover/gui_qt/utils.py b/plover/gui_qt/utils.py index 3549d596a..763c6b4f0 100644 --- a/plover/gui_qt/utils.py +++ b/plover/gui_qt/utils.py @@ -1,12 +1,18 @@ -from PyQt5.QtCore import QSettings -from PyQt5.QtGui import QGuiApplication, QKeySequence -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QSettings +from PyQt6.QtGui import ( QAction, + QGuiApplication, + QIcon, + QKeySequence, + QPixmap +) +from PyQt6.QtWidgets import ( QMainWindow, QToolBar, QToolButton, QWidget, ) +import importlib.resources from plover import _ @@ -17,7 +23,7 @@ def copy_selection_to_clipboard(): data = view.model().mimeData(indexes) QGuiApplication.clipboard().setMimeData(data) action = QAction(_('Copy selection to clipboard')) - action.setShortcut(QKeySequence(QKeySequence.Copy)) + action.setShortcut(QKeySequence(QKeySequence.StandardKey.Copy)) action.triggered.connect(copy_selection_to_clipboard) return action @@ -38,6 +44,24 @@ def ToolBar(*action_list): return toolbar +def Icon(resource): + icon = QIcon() + package = "plover.gui_qt.resources" + + if type(resource) is tuple: + package = resource[0] + resource = resource[1] + + if type(resource) is str: + if resource.startswith(":/"): + resource = resource[2:] + + with importlib.resources.path(package, resource) as f_path: + icon.addPixmap(QPixmap(str(f_path))) + + return icon + + class WindowState(QWidget): ROLE = None diff --git a/plover_build_utils/setup.py b/plover_build_utils/setup.py index 0be32e26e..63c35e2ff 100644 --- a/plover_build_utils/setup.py +++ b/plover_build_utils/setup.py @@ -100,17 +100,20 @@ def finalize_options(self): pass def _build_ui(self, src): + from pyqt6rc import convert_tools dst = os.path.splitext(src)[0] + '_ui.py' if not self.force and os.path.exists(dst) and \ os.path.getmtime(dst) >= os.path.getmtime(src): return - cmd = ( - sys.executable, '-m', 'PyQt5.uic.pyuic', - '--from-import', src, - ) if self.verbose: print('generating', dst) - contents = subprocess.check_output(cmd).decode('utf-8') + + resources = {} + resources_found = convert_tools.update_resources(src, resources) + contents = os.popen(f"python -m PyQt6.uic.pyuic {src}").read() + if resources_found is not None: + contents = convert_tools.modify_py(contents, resources) + for hook in self.hooks: mod_name, attr_name = hook.split(':') mod = importlib.import_module(mod_name) @@ -119,19 +122,6 @@ def _build_ui(self, src): with open(dst, 'w') as fp: fp.write(contents) - def _build_resources(self, src): - dst = os.path.join( - os.path.dirname(os.path.dirname(src)), - os.path.splitext(os.path.basename(src))[0] - ) + '_rc.py' - cmd = ( - sys.executable, '-m', 'PyQt5.pyrcc_main', - src, '-o', dst, - ) - if self.verbose: - print('generating', dst) - subprocess.check_call(cmd) - def run(self): self.run_command('egg_info') std_hook_prefix = __package__ + '.pyqt:' @@ -143,8 +133,6 @@ def run(self): print('generating UI using hooks:', ', '.join(hooks_info)) ei_cmd = self.get_finalized_command('egg_info') for src in ei_cmd.filelist.files: - if src.endswith('.qrc'): - self._build_resources(src) if src.endswith('.ui'): self._build_ui(src) diff --git a/pyproject.toml b/pyproject.toml index b6c489996..0a096030c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,8 @@ [build-system] requires = [ "Babel", - "PyQt5>=5.8.2", + "PyQt6>=6.4.2", + "pyqt6rc>=0.5.2", "setuptools>=38.2.4", "wheel", ] diff --git a/pytest.ini b/pytest.ini index 59abe85c3..2cf5b7375 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,7 +2,7 @@ addopts = -ra markers = gui_qt: GUI specific tests. -qt_api = pyqt5 +qt_api = pyqt6 testpaths = test diff --git a/reqs/constraints.txt b/reqs/constraints.txt index 928d34af0..a5d00c62c 100644 --- a/reqs/constraints.txt +++ b/reqs/constraints.txt @@ -12,7 +12,7 @@ charset-normalizer==2.0.7 check-manifest==0.47 click==8.0.3 click-default-group==1.2.2 -cmarkgfm==0.6.0 +cmarkgfm>=1.2.0 colorama==0.4.4 cryptography==35.0.0 dmgbuild==1.5.2 @@ -33,27 +33,27 @@ packaging==21.0 pep517==0.12.0 pip==21.3.1 pkginfo==1.7.1 -plover-plugins-manager==0.7.0 +plover-plugins-manager==0.7.1 plover-stroke==1.1.0 plover-treal==1.0.1 pluggy==1.0.0 py==1.10.0 pycparser==2.20 Pygments==2.10.0 -pyobjc-core==7.3 -pyobjc-framework-Cocoa==7.3 -pyobjc-framework-Quartz==7.3 +pyobjc-core==9.0 +pyobjc-framework-Cocoa==9.0 +pyobjc-framework-Quartz==9.0 pyparsing==3.0.3 -PyQt5==5.15.6 -PyQt5-Qt5==5.15.2 -PyQt5-sip==12.9.0 +PyQt6==6.4.2 +PyQt6-Qt6==6.4.3 +pyqt6rc==0.5.2 pyserial==3.5 pytest==6.2.5 pytest-qt==4.0.2 python-xlib==0.31 pytz==2021.3 PyYAML==6.0 -readme-renderer==30.0 +readme-renderer==37.3 requests==2.26.0 requests-cache==0.9.1 requests-futures==1.0.0 diff --git a/reqs/dist.txt b/reqs/dist.txt index 43102086a..fc1ec31b5 100644 --- a/reqs/dist.txt +++ b/reqs/dist.txt @@ -1,9 +1,9 @@ appdirs>=1.3.0 appnope>=0.1.0; "darwin" in sys_platform plover-stroke>=1.1.0 -pyobjc-core>=4.0; "darwin" in sys_platform -pyobjc-framework-Cocoa>=4.0; "darwin" in sys_platform -pyobjc-framework-Quartz>=4.0; "darwin" in sys_platform +pyobjc-core>=9.0; "darwin" in sys_platform +pyobjc-framework-Cocoa>=9.0; "darwin" in sys_platform +pyobjc-framework-Quartz>=9.0; "darwin" in sys_platform pyserial>=2.7 python-xlib>=0.16; ("linux" in sys_platform or "bsd" in sys_platform) and python_version < "3.9" python-xlib>=0.29; ("linux" in sys_platform or "bsd" in sys_platform) and python_version >= "3.9" diff --git a/reqs/dist_extra_gui_qt.txt b/reqs/dist_extra_gui_qt.txt index c7854c32a..294e4a5f0 100644 --- a/reqs/dist_extra_gui_qt.txt +++ b/reqs/dist_extra_gui_qt.txt @@ -1,3 +1,4 @@ -PyQt5>=5.5 +PyQt6>=6.4 +pyqt6rc>=0.5.2 # vim: ft=cfg commentstring=#\ %s list diff --git a/reqs/dist_plugins.txt b/reqs/dist_plugins.txt index 47e7ee15f..2540fc70b 100644 --- a/reqs/dist_plugins.txt +++ b/reqs/dist_plugins.txt @@ -1,4 +1,4 @@ -plover-plugins-manager +plover-plugins-manager @ https://github.com/greghope667/plover_plugins_manager/archive/pyqt6-migration.zip plover-treal # vim: ft=cfg commentstring=#\ %s list diff --git a/reqs/setup.txt b/reqs/setup.txt index ec915a75a..6417b6ccb 100644 --- a/reqs/setup.txt +++ b/reqs/setup.txt @@ -1,5 +1,6 @@ Babel -PyQt5>=5.8.2 +PyQt6>=6.4.2 +pyqt6rc>=0.5.2 setuptools>=38.2.4 wheel diff --git a/setup.cfg b/setup.cfg index 8a1ef9c3e..52a68e4a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ packages = plover.dictionary plover.gui_none plover.gui_qt + plover.gui_qt.resources plover.machine plover.machine.keyboard_capture plover.macro @@ -116,6 +117,5 @@ plover = messages/plover.pot plover.gui_qt = *.ui - resources/* # vim: commentstring=#\ %s list diff --git a/test/gui_qt/test_dictionaries_widget.py b/test/gui_qt/test_dictionaries_widget.py index 7f059ee9a..fdc7541e9 100644 --- a/test/gui_qt/test_dictionaries_widget.py +++ b/test/gui_qt/test_dictionaries_widget.py @@ -4,7 +4,7 @@ from types import SimpleNamespace import operator -from PyQt5.QtCore import QModelIndex, QPersistentModelIndex, Qt +from PyQt6.QtCore import QModelIndex, QPersistentModelIndex, Qt import pytest @@ -37,12 +37,13 @@ ENABLED_FROM_CHAR = {c: e for e, c in ENABLED_TO_CHAR.items()} CHECKED_TO_BOOL = { - Qt.Checked: True, - Qt.Unchecked: False, + Qt.CheckState.Checked: True, + Qt.CheckState.Unchecked: False, } -MODEL_ROLES = sorted([Qt.AccessibleTextRole, Qt.CheckStateRole, - Qt.DecorationRole, Qt.DisplayRole, Qt.ToolTipRole]) +MODEL_ROLES = sorted([Qt.ItemDataRole.AccessibleTextRole, Qt.ItemDataRole.CheckStateRole, + Qt.ItemDataRole.DecorationRole, Qt.ItemDataRole.DisplayRole, + Qt.ItemDataRole.ToolTipRole]) def parse_state(state_str): @@ -119,9 +120,9 @@ def check(self, expected, actual_state = [] for row in range(self.model.rowCount()): index = self.model.index(row) - enabled = CHECKED_TO_BOOL[index.data(Qt.CheckStateRole)] - icon = index.data(Qt.DecorationRole) - path = index.data(Qt.DisplayRole) + enabled = CHECKED_TO_BOOL[index.data(Qt.ItemDataRole.CheckStateRole)] + icon = index.data(Qt.ItemDataRole.DecorationRole) + path = index.data(Qt.ItemDataRole.DisplayRole) actual_state.append('%s %s %s' % ( ENABLED_TO_CHAR.get(enabled, '?'), ICON_TO_CHAR.get(icon, '?'), @@ -150,8 +151,10 @@ def check(self, expected, call.args[2].sort() assert call == mock.call.dataChanged(index, index, MODEL_ROLES) if layout_change: - assert signal_calls[0:2] == [mock.call.layoutAboutToBeChanged([], self.model.NoLayoutChangeHint), - mock.call.layoutChanged([], self.model.NoLayoutChangeHint)] + assert signal_calls[0:2] == [mock.call.layoutAboutToBeChanged( + [], self.model.LayoutChangeHint.NoLayoutChangeHint), + mock.call.layoutChanged( + [], self.model.LayoutChangeHint.NoLayoutChangeHint)] del signal_calls[0:2] assert not signal_calls self.signals.reset_mock() @@ -224,7 +227,7 @@ def test_model_accessible_text_1(model_test): 'commands.json, loading', 'asset:plover:assets/main.json, disabled, loading', )): - assert model_test.model.index(n).data(Qt.AccessibleTextRole) == expected + assert model_test.model.index(n).data(Qt.ItemDataRole.AccessibleTextRole) == expected def test_model_accessible_text_2(model_test): ''' @@ -239,21 +242,21 @@ def test_model_accessible_text_2(model_test): 'commands.json', 'asset:plover:assets/main.json, disabled, read-only', )): - assert model_test.model.index(n).data(Qt.AccessibleTextRole) == expected + assert model_test.model.index(n).data(Qt.ItemDataRole.AccessibleTextRole) == expected def test_model_accessible_text_3(model_test): ''' ☑ ! invalid.bad ''' expected = 'invalid.bad, errored: %s.' % INVALID_EXCEPTION - assert model_test.model.index(0).data(Qt.AccessibleTextRole) == expected + assert model_test.model.index(0).data(Qt.ItemDataRole.AccessibleTextRole) == expected def test_model_accessible_text_4(model_test): ''' ☐ ! invalid.bad ''' expected = 'invalid.bad, disabled, errored: %s.' % INVALID_EXCEPTION - assert model_test.model.index(0).data(Qt.AccessibleTextRole) == expected + assert model_test.model.index(0).data(Qt.ItemDataRole.AccessibleTextRole) == expected def test_model_add_existing(model_test): ''' @@ -424,7 +427,7 @@ def test_model_favorite(model_test): ☐ 🛇 asset:plover:assets/main.json ''' # New favorite. - model_test.model.setData(model_test.model.index(1), Qt.Unchecked, Qt.CheckStateRole) + model_test.model.setData(model_test.model.index(1), Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) model_test.check( ''' ☑ 🛇 read-only.ro @@ -438,7 +441,7 @@ def test_model_favorite(model_test): undo_change=True, ) # No favorite. - model_test.model.setData(model_test.model.index(3), Qt.Unchecked, Qt.CheckStateRole) + model_test.model.setData(model_test.model.index(3), Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) model_test.check( ''' ☑ 🛇 read-only.ro @@ -614,19 +617,19 @@ def test_model_persistent_index(model_test): ''' persistent_index = QPersistentModelIndex(model_test.model.index(1)) assert persistent_index.row() == 1 - assert persistent_index.data(Qt.CheckStateRole) == Qt.Checked - assert persistent_index.data(Qt.DecorationRole) == 'favorite' - assert persistent_index.data(Qt.DisplayRole) == 'user.json' + assert persistent_index.data(Qt.ItemDataRole.CheckStateRole) == Qt.CheckState.Checked + assert persistent_index.data(Qt.ItemDataRole.DecorationRole) == 'favorite' + assert persistent_index.data(Qt.ItemDataRole.DisplayRole) == 'user.json' model_test.configure(classic_dictionaries_display_order=True) assert persistent_index.row() == 2 - assert persistent_index.data(Qt.CheckStateRole) == Qt.Checked - assert persistent_index.data(Qt.DecorationRole) == 'favorite' - assert persistent_index.data(Qt.DisplayRole) == 'user.json' - model_test.model.setData(persistent_index, Qt.Unchecked, Qt.CheckStateRole) + assert persistent_index.data(Qt.ItemDataRole.CheckStateRole) == Qt.CheckState.Checked + assert persistent_index.data(Qt.ItemDataRole.DecorationRole) == 'favorite' + assert persistent_index.data(Qt.ItemDataRole.DisplayRole) == 'user.json' + model_test.model.setData(persistent_index, Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) assert persistent_index.row() == 2 - assert persistent_index.data(Qt.CheckStateRole) == Qt.Unchecked - assert persistent_index.data(Qt.DecorationRole) == 'normal' - assert persistent_index.data(Qt.DisplayRole) == 'user.json' + assert persistent_index.data(Qt.ItemDataRole.CheckStateRole) == Qt.CheckState.Unchecked + assert persistent_index.data(Qt.ItemDataRole.DecorationRole) == 'normal' + assert persistent_index.data(Qt.ItemDataRole.DisplayRole) == 'user.json' def test_model_qtmodeltester(model_test, qtmodeltester): ''' @@ -686,18 +689,18 @@ def test_model_set_checked(model_test): first_index = model_test.model.index(0) for index, value, ret, state in ( # Invalid index. - (QModelIndex(), Qt.Unchecked, False, on_state), + (QModelIndex(), Qt.CheckState.Unchecked, False, on_state), # Invalid values. (first_index, 'pouet', False, on_state), - (first_index, Qt.PartiallyChecked, False, on_state), + (first_index, Qt.CheckState.PartiallyChecked, False, on_state), # Already checked. - (first_index, Qt.Checked, False, on_state), + (first_index, Qt.CheckState.Checked, False, on_state), # Uncheck. - (first_index, Qt.Unchecked, True, off_state), + (first_index, Qt.CheckState.Unchecked, True, off_state), # Recheck. - (first_index, Qt.Checked, True, on_state), + (first_index, Qt.CheckState.Checked, True, on_state), ): - assert model_test.model.setData(index, value, Qt.CheckStateRole) == ret + assert model_test.model.setData(index, value, Qt.ItemDataRole.CheckStateRole) == ret model_test.check(state, config_change='update' if ret else None, data_change=[index.row()] if ret else None) @@ -727,7 +730,7 @@ def test_model_undo_1(model_test): state = state.split('\n') state[n] = '☑' + state[n][1:] state = '\n'.join(state) - model_test.model.setData(model_test.model.index(n), Qt.Checked, Qt.CheckStateRole) + model_test.model.setData(model_test.model.index(n), Qt.CheckState.Checked, Qt.ItemDataRole.CheckStateRole) model_test.check(state, config_change='update', data_change=[n], undo_change=(True if n == 0 else None)) for n in range(5): @@ -766,12 +769,12 @@ class WidgetTest(namedtuple('WidgetTest', ''' def select(self, selection): sm = self.widget.view.selectionModel() for row in selection: - sm.select(self.model.index(row), sm.Select) + sm.select(self.model.index(row), sm.SelectionFlag.Select) def unselect(self, selection): sm = self.widget.view.selectionModel() for row in selection: - sm.select(self.model.index(row), sm.Deselect) + sm.select(self.model.index(row), sm.SelectionFlag.Deselect) def __getattr__(self, name): return getattr(self.model_test, name) diff --git a/test/gui_qt/test_steno_validator.py b/test/gui_qt/test_steno_validator.py index 28359dc0c..1ab166b09 100644 --- a/test/gui_qt/test_steno_validator.py +++ b/test/gui_qt/test_steno_validator.py @@ -1,4 +1,4 @@ -from PyQt5.QtGui import QValidator +from PyQt6.QtGui import QValidator import pytest @@ -7,21 +7,21 @@ @pytest.mark.parametrize(('text', 'state'), ( # Acceptable. - ('ST', QValidator.Acceptable), - ('TEFT', QValidator.Acceptable), - ('TEFT/-G', QValidator.Acceptable), - ('/ST', QValidator.Acceptable), - ('-F', QValidator.Acceptable), + ('ST', QValidator.State.Acceptable), + ('TEFT', QValidator.State.Acceptable), + ('TEFT/-G', QValidator.State.Acceptable), + ('/ST', QValidator.State.Acceptable), + ('-F', QValidator.State.Acceptable), # Intermediate. - ('-', QValidator.Intermediate), - ('/', QValidator.Intermediate), - ('/-', QValidator.Intermediate), - ('ST/', QValidator.Intermediate), - ('ST/-', QValidator.Intermediate), - ('ST//', QValidator.Intermediate), + ('-', QValidator.State.Intermediate), + ('/', QValidator.State.Intermediate), + ('/-', QValidator.State.Intermediate), + ('ST/', QValidator.State.Intermediate), + ('ST/-', QValidator.State.Intermediate), + ('ST//', QValidator.State.Intermediate), # Invalid. - ('WK', QValidator.Invalid), - ('PLOVER', QValidator.Invalid), + ('WK', QValidator.State.Invalid), + ('PLOVER', QValidator.State.Invalid), )) def test_steno_validator_validate(text, state): validator = StenoValidator() diff --git a/windows/dist_blacklist.txt b/windows/dist_blacklist.txt index 6faaf7da8..3d7c601df 100644 --- a/windows/dist_blacklist.txt +++ b/windows/dist_blacklist.txt @@ -1,7 +1,7 @@ # Python. Scripts -# PyQt5. -:Lib/site-packages/PyQt5 +# PyQt6. +:Lib/site-packages/PyQt6 **/*Designer* **/*[Hh]elp* **/*Test* @@ -9,21 +9,18 @@ **/*[Qq]uick* **/*[Ww]eb[Ee]ngine* bindings - Qt5/bin/libeay32.dll - Qt5/bin/ssleay32.dll - Qt5/plugins/platforms/qminimal.dll - Qt5/plugins/platforms/qoffscreen.dll - Qt5/plugins/platforms/qwebgl.dll - Qt5/plugins/sceneparsers - Qt5/qml + Qt6/bin/libeay32.dll + Qt6/bin/ssleay32.dll + Qt6/plugins/platforms/qminimal.dll + Qt6/plugins/platforms/qoffscreen.dll + Qt6/plugins/platforms/qwebgl.dll + Qt6/plugins/sceneparsers + Qt6/qml pylupdate* - pyrcc* uic # Plover. :Lib/site-packages/plover gui_qt/*.ui - gui_qt/*.ui - gui_qt/resources messages/**/*.po messages/plover.pot