From 9df193130a00a0d5f1c72c667be6e2a559249d4a Mon Sep 17 00:00:00 2001 From: MAKOMO Date: Wed, 30 Oct 2024 17:32:27 +0100 Subject: [PATCH] - fixes the 30 ways to leave an Artisan dialog - lib updates --- src/artisanlib/ble_port.py | 2 +- src/artisanlib/dialogs.py | 10 +-- src/artisanlib/modbusport.py | 2 +- src/artisanlib/roast_properties.py | 102 +++++++++++++---------------- src/plus/blend.py | 41 ++++++++++-- src/plus/schedule.py | 9 ++- src/requirements-dev.txt | 14 ++-- src/requirements.txt | 9 +-- 8 files changed, 110 insertions(+), 79 deletions(-) diff --git a/src/artisanlib/ble_port.py b/src/artisanlib/ble_port.py index ab3cd115f..72147742f 100644 --- a/src/artisanlib/ble_port.py +++ b/src/artisanlib/ble_port.py @@ -410,7 +410,7 @@ def stop(self) -> None: if self._ble_client is None: ble.terminate_scan() # we stop ongoing scanning self._disconnect() - del self._async_loop_thread # on this level the released object should be automatically collected by the GC + #del self._async_loop_thread # on this level the released object should be automatically collected by the GC self._async_loop_thread = None self._ble_client = None self._connected_service_uuid = None diff --git a/src/artisanlib/dialogs.py b/src/artisanlib/dialogs.py index 85c577e41..f3362e010 100644 --- a/src/artisanlib/dialogs.py +++ b/src/artisanlib/dialogs.py @@ -93,11 +93,6 @@ def __init__(self, parent:Optional[QWidget], aw:'ApplicationWindow') -> None: (cancelButton,'Cancel',QApplication.translate('Button','Cancel'))]: self.setButtonTranslations(btn,txt,trans) - @pyqtSlot() - def cancelDialog(self) -> None: -# self.dialogbuttons.rejected.emit() - self.reject() - @staticmethod def setButtonTranslations(btn: Optional['QPushButton'], txt:str, trans:str) -> None: if btn is not None: @@ -108,6 +103,11 @@ def setButtonTranslations(btn: Optional['QPushButton'], txt:str, trans:str) -> N if txt != current_trans: btn.setText(current_trans) + @pyqtSlot() + def cancelDialog(self) -> None: # ESC key +# self.reject() # this does not call any closeEvent in subclasses! + self.dialogbuttons.rejected.emit() + @pyqtSlot('QCloseEvent') def closeEvent(self,_:Optional['QCloseEvent'] = None) -> None: self.dialogbuttons.rejected.emit() diff --git a/src/artisanlib/modbusport.py b/src/artisanlib/modbusport.py index 0b7c00383..4d3cae804 100644 --- a/src/artisanlib/modbusport.py +++ b/src/artisanlib/modbusport.py @@ -227,7 +227,7 @@ def disconnect(self) -> None: except Exception as e: # pylint: disable=broad-except _log.exception(e) self._client = None - del self._asyncLoopThread + #del self._asyncLoopThread self._asyncLoopThread = None def clearCommError(self) -> None: diff --git a/src/artisanlib/roast_properties.py b/src/artisanlib/roast_properties.py index be862aa14..10d452269 100644 --- a/src/artisanlib/roast_properties.py +++ b/src/artisanlib/roast_properties.py @@ -365,6 +365,8 @@ def keyPressEvent(self, event: Optional['QKeyEvent']) -> None: self.coffeeinweightEdit.setText(f'{float2float(v):g}') elif self.coffeeoutweightEdit.hasFocus(): self.coffeeoutweightEdit.setText(f'{float2float(v):g}') + else: + super().keyPressEvent(event) def widgetWeight(self, widget:QLineEdit) -> None: w = self.retrieveWeight() @@ -1146,7 +1148,7 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) -> # connect the ArtisanDialog standard OK/Cancel buttons self.dialogbuttons.accepted.connect(self.accept) - self.dialogbuttons.rejected.connect(self.cancel_dialog) + self.dialogbuttons.rejected.connect(self.closeEvent) # container tare self.tareComboBox = QComboBox() @@ -2525,57 +2527,14 @@ def addRecentRoast(self, __:bool = False) -> None: _, _, exc_tb = sys.exc_info() self.aw.qmc.adderror((QApplication.translate('Error Message', 'Exception:') + ' addRecentRoast(): {0}').format(str(e)),getattr(exc_tb, 'tb_lineno', '?')) - # triggered if dialog is closed via its windows close box - # and called from accept if dialog is closed via OK - @pyqtSlot('QCloseEvent') - def closeEvent(self, _:Optional['QCloseEvent'] = None) -> None: - self.disconnecting = True - if self.acaia is not None: - try: - self.acaia.battery_changed_signal.disconnect() - self.acaia.weight_changed_signal.disconnect() - self.acaia.disconnected_signal.disconnect() - except Exception as e: # pylint: disable=broad-except - _log.exception(e) - try: - #self.acaia.stop() - QTimer.singleShot(2,self.acaia.stop) # no delay on RoastProperties window close on Windows - self.updateWeightLCD('') - except Exception as e: # pylint: disable=broad-except - _log.exception(e) - self.acaia = None - settings = QSettings() - #save window geometry - settings.setValue('RoastGeometry',self.saveGeometry()) - self.aw.editGraphDlg_activeTab = self.TabWidget.currentIndex() -# self.aw.closeEventSettings() # save all app settings - self.aw.editgraphdialog = None - if self.stockWorker is not None and self.updateStockSignalConnection is not None: - self.stockWorker.updatedSignal.disconnect(self.updateStockSignalConnection) - # triggered via the cancel button + # called on CANCEL or WINDOW_CLOSE; reverts state and calls clean_up_and_close() @pyqtSlot() - def cancel_dialog(self) -> None: - self.disconnecting = True - if self.acaia is not None: - try: - self.acaia.battery_changed_signal.disconnect() - self.acaia.weight_changed_signal.disconnect() - self.acaia.disconnected_signal.disconnect() - except Exception as e: # pylint: disable=broad-except - _log.exception(e) - try: - self.acaia.stop() - self.updateWeightLCD('') - except Exception as e: # pylint: disable=broad-except - _log.exception(e) - self.acaia = None - settings = QSettings() - #save window geometry - settings.setValue('RoastGeometry',self.saveGeometry()) - self.aw.editGraphDlg_activeTab = self.TabWidget.currentIndex() + @pyqtSlot('QCloseEvent') + def closeEvent(self, _:Optional['QCloseEvent'] = None) -> None: + # restore self.restoreAllEnergySettings() self.aw.qmc.beans = self.org_beans @@ -2601,9 +2560,34 @@ def cancel_dialog(self) -> None: self.aw.qmc.roastpropertiesAutoOpenFlag = self.org_roastpropertiesAutoOpenFlag self.aw.qmc.roastpropertiesAutoOpenDropFlag = self.org_roastpropertiesAutoOpenDropFlag - self.aw.editgraphdialog = None - self.reject() + self.clean_up() + super().reject() + + + # called on CANCEL and WindowClose from closeEvent(), and on OK from accept() + def clean_up(self) -> None: + self.disconnecting = True + if self.acaia is not None: + try: + self.acaia.battery_changed_signal.disconnect() + self.acaia.weight_changed_signal.disconnect() + self.acaia.disconnected_signal.disconnect() + except Exception as e: # pylint: disable=broad-except + _log.exception(e) + try: + self.acaia.stop() + self.updateWeightLCD('') + except Exception as e: # pylint: disable=broad-except + _log.exception(e) + self.acaia = None + settings = QSettings() + #save window geometry + settings.setValue('RoastGeometry',self.saveGeometry()) + self.aw.editGraphDlg_activeTab = self.TabWidget.currentIndex() + self.aw.editgraphdialog = None + if self.stockWorker is not None and self.updateStockSignalConnection is not None: + self.stockWorker.updatedSignal.disconnect(self.updateStockSignalConnection) # calcs volume (in ml) from density (in g/l) and weight (in g) @staticmethod @@ -2625,14 +2609,16 @@ def keyPressEvent(self, event: Optional['QKeyEvent']) -> None: self.inWeight(True,overwrite=True) # we don't add to current reading but overwrite elif self.weightoutedit.hasFocus(): self.outWeight(True,overwrite=True) # we don't add to current reading but overwrite - if key == 76 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl L on Roast tab => open volume calculator + elif key == 76 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl L on Roast tab => open volume calculator self.volumeCalculatorTimer(True) - if key == 73 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl I on Roast tab => send scale weight to in-weight field + elif key == 73 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl I on Roast tab => send scale weight to in-weight field self.inWeight(True) - if key == 79 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl O on Roast tab => send scale weight to out-weight field + elif key == 79 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl O on Roast tab => send scale weight to out-weight field self.outWeight(True) - if key == 80 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl P on Roast tab => send scale weight to in-weight field + elif key == 80 and control_modifier and self.TabWidget.currentIndex() == 0: #ctrl P on Roast tab => send scale weight to in-weight field self.resetScaleSet() + else: + super().keyPressEvent(event) @pyqtSlot(int) def tareChanged(self, i:int) -> None: @@ -3643,7 +3629,10 @@ def energyTabSwitched(self, i:int) -> None: @staticmethod def validateText2Seconds(s:str) -> int: - return stringtoseconds(s) if len(s) > 0 else 0 + try: + return stringtoseconds(s) if len(s) > 0 else 0 + except Exception: # pylint: disable=broad-except + return 0 @staticmethod def validateSeconds2Text(seconds:float) -> str: @@ -5120,7 +5109,8 @@ def accept(self) -> None: plus.queue.addRoast() except Exception as e: # pylint: disable=broad-except _log.exception(e) - self.close() + self.clean_up() + super().accept() def getMeasuredvalues(self, title:str, func_updatefields:Callable[[],None], fields:List[QLineEdit], loadEnergy:List[float], func_updateduration:Callable[[],None], diff --git a/src/plus/blend.py b/src/plus/blend.py index 49268b463..bc304ced6 100644 --- a/src/plus/blend.py +++ b/src/plus/blend.py @@ -34,7 +34,7 @@ QHeaderView, # @UnusedImport @Reimport @UnresolvedImport ) from PyQt6.QtCore import Qt, pyqtSlot, QSize, QSettings # @UnusedImport @Reimport @UnresolvedImport - from PyQt6.QtGui import QIcon, QStandardItemModel # @UnusedImport @Reimport @UnresolvedImport + from PyQt6.QtGui import QKeySequence, QAction, QIcon, QStandardItemModel # @UnusedImport @Reimport @UnresolvedImport # from PyQt6 import sip # @UnusedImport @Reimport @UnresolvedImport except Exception: # pylint: disable=broad-except #pylint: disable = E, W, R, C @@ -49,7 +49,7 @@ QHeaderView, # @UnusedImport @Reimport @UnresolvedImport ) from PyQt5.QtCore import Qt, pyqtSlot, QSize, QSettings # type: ignore # @UnusedImport @Reimport @UnresolvedImport - from PyQt5.QtGui import QIcon, QStandardItemModel # type: ignore # @UnusedImport @Reimport @UnresolvedImport + from PyQt5.QtGui import QKeySequence, QAction, QIcon, QStandardItemModel # type: ignore # @UnusedImport @Reimport @UnresolvedImport # try: # from PyQt5 import sip # type: ignore # @Reimport @UnresolvedImport @UnusedImport # except Exception: # pylint: disable=broad-except @@ -65,7 +65,7 @@ if TYPE_CHECKING: from artisanlib.main import ApplicationWindow # noqa: F401 # pylint: disable=unused-import from PyQt6.QtWidgets import QWidget # noqa: F401 # pylint: disable=unused-import - from PyQt6.QtGui import QCloseEvent # pylint: disable=unused-import + from PyQt6.QtGui import QCloseEvent, QKeyEvent # pylint: disable=unused-import _log: Final[logging.Logger] = logging.getLogger(__name__) @@ -163,6 +163,23 @@ def __init__(self, parent:'QWidget', aw:'ApplicationWindow', inWeight:float, wei self.ui.buttonBox.removeButton(applyButton) self.applyButton = self.ui.buttonBox.addButton(applyButton.text(), QDialogButtonBox.ButtonRole.AcceptRole) + cancelButton = self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel) + if cancelButton is not None: + cancelButton.setDefault(False) + cancelButton.setAutoDefault(False) + # add additional CMD-. shortcut to close the dialog + cancelButton.setShortcut(QKeySequence('Ctrl+.')) + # add additional CMD-W shortcut to close this dialog (ESC on Mac OS X) + # cancelAction = QAction(self, triggered=lambda _:self.dialogbuttons.rejected.emit()) + cancelAction = QAction(self) + cancelAction.triggered.connect(self.cancelDialog) + try: + cancelAction.setShortcut(QKeySequence.StandardKey.Cancel) + except Exception: # pylint: disable=broad-except + pass + cancelButton.addActions([cancelAction]) + + # populate widgets self.ui.lineEdit_name.setText(self.blend.name) self.ui.label_weight.setText(QApplication.translate('Label','Weight')) @@ -183,6 +200,22 @@ def __init__(self, parent:'QWidget', aw:'ApplicationWindow', inWeight:float, wei if settings.contains('BlendGeometry'): self.restoreGeometry(settings.value('BlendGeometry')) + @pyqtSlot() + def cancelDialog(self) -> None: # ESC key + self.reject() + + def keyPressEvent(self, event: Optional['QKeyEvent']) -> None: + if event is not None: + key = int(event.key()) + #uncomment next line to find the integer value of a key + #print(key) + #modifiers = QApplication.keyboardModifiers() + modifiers = event.modifiers() + if key == 16777216 or (key == 87 and modifiers == Qt.KeyboardModifier.ControlModifier): #ESCAPE or CMD-W + self.reject() + else: + super().keyPressEvent(event) + @pyqtSlot(str) def textChanged(self,s:str) -> None: if s == '': # content got cleared @@ -291,7 +324,7 @@ def saveSettings(self) -> None: @pyqtSlot('QCloseEvent') def closeEvent(self, _:Optional['QCloseEvent'] = None) -> None: - self.saveSettings() + self.reject() @pyqtSlot() def accept(self) -> None: diff --git a/src/plus/schedule.py b/src/plus/schedule.py index 2287bb713..28c03e556 100644 --- a/src/plus/schedule.py +++ b/src/plus/schedule.py @@ -2086,11 +2086,12 @@ def keyPressEvent(self, event: Optional['QKeyEvent']) -> None: self.selected_completed_item.clicked.emit() else: self.selected_completed_item.selected.emit() + elif k == 46 and QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier: # CMD-. + self.closeEvent() else: super().keyPressEvent(event) - @pyqtSlot('QCloseEvent') def closeEvent(self, evnt:Optional['QCloseEvent'] = None) -> None: # type:ignore[reportIncompatibleMethodOverride, unused-ignore] if self.aw.scheduler_auto_open and len(self.scheduled_items) > 0 and self.aw.plus_account is not None: @@ -2119,6 +2120,11 @@ def closeEvent(self, evnt:Optional['QCloseEvent'] = None) -> None: # type:ignore else: self.closeScheduler() + @pyqtSlot() + def close(self) -> bool: + self.closeEvent(None) + return True + def closeScheduler(self) -> None: self.aw.scheduled_items_uuids = self.get_scheduled_items_ids() # remember Dialog geometry @@ -2146,6 +2152,7 @@ def closeScheduler(self) -> None: self.aw.qmc.scheduleID = None self.aw.qmc.scheduleDate = None self.aw.sendmessage(QApplication.translate('Message','Scheduler stopped')) + self.accept() # updates the current schedule items by joining its roast with those received as part of a stock update from the server # adding new items at the end diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index bb820b28a..0d3438c16 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -1,18 +1,18 @@ -types-openpyxl>=3.1.5.20240918 +types-openpyxl>=3.1.5.20241025 types-Pillow>=10.2.0.20240822 -types-protobuf>=5.28.0.20240924 -types-psutil>=6.0.0.20241011 +types-protobuf>=5.28.3.20241030 +types-psutil>=6.1.0.20241022 types-pyserial>=3.5.0.20240826 types-python-dateutil==2.9.0.20241003 types-pytz>=2024.2.0.20241003 types-pyyaml>=6.0.12.20240917 types-requests>=2.32.0.20241016 -types-setuptools>=75.1.0.20241014 +types-setuptools>=75.2.0.20241025 types-urllib3>=1.26.25.14 types-docutils>=0.21.0.20241005 lxml-stubs>=0.5.1 mypy==1.13.0 -pyright==1.1.386 +pyright==1.1.387 ruff>=0.7.1 pylint==3.3.1 pre-commit>=4.0.1 @@ -25,8 +25,8 @@ pytest-cov==5.0.0 #pytest-bdd==6.1.1 #pytest-benchmark==4.0.0 #pytest-mock==3.11.1 -hypothesis>=6.115.3 -coverage>=7.6.3 +hypothesis>=6.115.6 +coverage>=7.6.4 coverage-badge==1.1.2 codespell==2.3.0 # the following 2 packages are not installed along aiohttp on Python3.12 and make mypy complain diff --git a/src/requirements.txt b/src/requirements.txt index 53794b325..c723d6c91 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -45,9 +45,9 @@ portalocker==2.10.1 xlrd==2.0.1 websockets==13.1 PyYAML==6.0.2 -psutil==6.0.0 +psutil==6.1.0 typing-extensions==4.10.0; python_version < '3.8' # required for supporting Final and TypeDict on Python <3.8 -protobuf==5.28.2 +protobuf==5.28.3 numpy==1.24.3; python_version < '3.9' # last Python 3.8 release numpy==2.1.2; python_version >= '3.9' scipy==1.10.1; python_version < '3.9' # last Python 3.8 release @@ -55,8 +55,9 @@ scipy==1.14.1; python_version >= '3.9' wquantiles==0.6 colorspacious==1.1.2 openpyxl==3.1.5 -keyring==25.4.1 -prettytable==3.11.0 +keyring==25.5.0 +prettytable==3.11.0; python_version < '3.9' # last Python 3.8 release +prettytable==3.12.0; python_version >= '3.9' lxml==5.3.0 matplotlib==3.7.3; python_version < '3.9' # last Python 3.8 release matplotlib==3.9.2; python_version >= '3.9'