From 92171e25e631bb0fc6aeb2bcaaeb08a28f81525b Mon Sep 17 00:00:00 2001 From: Sam Bradshaw <33942237+roxgib@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:04:58 +1000 Subject: [PATCH] Add MessageBox class and associated funcs to aqt.utils and update the first few callers (#2010) * Add MessageBox class and associated funcs to aqt.utils and update some callers in aqt.sync and aqt.addons * Cleanup imports in aqt.sync * Fix return values for ask_user and ask_user_dialog * Fix wrong argument name in aqt.utils.ask_user * Add type annotations to **kwargs in utils.py * Type annotation for callback in aqt.sync.full_sync * MessageBox accepts StandardButton in addition to str, fix linting issues * Assess default buttons in correct order and return correct button name in MessageBox * Add explicit Optionals in aqt.utils * Pass button index to callback in MessageBox * Update type hints for aqt.utils.MessageBox * Use Sequence for aqt.utils.MessageBox buttons arg * default_button > default_yes in aqt.utils.ask_user * Dark mode question icon in aqt.utils.MessageBox --- qt/aqt/addons.py | 5 +- qt/aqt/sync.py | 53 +++++++--------- qt/aqt/utils.py | 159 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 36 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 30e75e173..871f3a4ee 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -43,6 +43,7 @@ from aqt.utils import ( saveGeom, saveSplitter, send_to_trash, + show_info, showInfo, showWarning, tooltip, @@ -862,14 +863,14 @@ class AddonsDialog(QDialog): def onlyOneSelected(self) -> str | None: dirs = self.selectedAddons() if len(dirs) != 1: - showInfo(tr.addons_please_select_a_single_addon_first()) + show_info(tr.addons_please_select_a_single_addon_first()) return None return dirs[0] def selected_addon_meta(self) -> AddonMeta | None: idxs = [x.row() for x in self.form.addonList.selectedIndexes()] if len(idxs) != 1: - showInfo(tr.addons_please_select_a_single_addon_first()) + show_info(tr.addons_please_select_a_single_addon_first()) return None return self.addons[idxs[0]] diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 8dfdd298a..2ff0de237 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -3,7 +3,6 @@ from __future__ import annotations -import enum import os from concurrent.futures import Future from typing import Callable @@ -27,8 +26,8 @@ from aqt.qt import ( qconnect, ) from aqt.utils import ( + ask_user_dialog, askUser, - askUserDialog, disable_help_button, showText, showWarning, @@ -36,12 +35,6 @@ from aqt.utils import ( ) -class FullSyncChoice(enum.Enum): - CANCEL = 0 - UPLOAD = 1 - DOWNLOAD = 2 - - def get_sync_status( mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None] ) -> None: @@ -135,13 +128,26 @@ def full_sync( elif out.required == out.FULL_UPLOAD: full_upload(mw, on_done) else: - choice = ask_user_to_decide_direction() - if choice == FullSyncChoice.UPLOAD: - full_upload(mw, on_done) - elif choice == FullSyncChoice.DOWNLOAD: - full_download(mw, on_done) - else: - on_done() + button_labels: list[str] = [ + tr.sync_upload_to_ankiweb(), + tr.sync_download_from_ankiweb(), + tr.sync_cancel_button(), + ] + + def callback(choice: int) -> None: + if choice == 0: + full_upload(mw, on_done) + elif choice == 1: + full_download(mw, on_done) + else: + on_done() + + ask_user_dialog( + tr.sync_conflict_explanation(), + callback=callback, + buttons=button_labels, + default_button=2, + ) def confirm_full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: @@ -277,23 +283,6 @@ def sync_login( ) -def ask_user_to_decide_direction() -> FullSyncChoice: - button_labels = [ - tr.sync_upload_to_ankiweb(), - tr.sync_download_from_ankiweb(), - tr.sync_cancel_button(), - ] - diag = askUserDialog(tr.sync_conflict_explanation(), button_labels) - diag.setDefault(2) - ret = diag.run() - if ret == button_labels[0]: - return FullSyncChoice.UPLOAD - elif ret == button_labels[1]: - return FullSyncChoice.DOWNLOAD - else: - return FullSyncChoice.CANCEL - - def get_id_and_pass_from_user( mw: aqt.main.AnkiQt, username: str = "", password: str = "" ) -> tuple[str, str]: diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 1d6163937..b067c1b3e 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -7,9 +7,9 @@ import re import shutil import subprocess import sys -from functools import wraps +from functools import partial, wraps from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Sequence, no_type_check +from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, Union, no_type_check from send2trash import send2trash @@ -25,6 +25,56 @@ from anki.utils import ( version_with_build, ) from aqt.qt import * +from aqt.qt import ( + PYQT_VERSION_STR, + QT_VERSION_STR, + QAction, + QApplication, + QCheckBox, + QColor, + QComboBox, + QDesktopServices, + QDialog, + QDialogButtonBox, + QEvent, + QFileDialog, + QFrame, + QHeaderView, + QIcon, + QKeySequence, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QMenu, + QMessageBox, + QMouseEvent, + QNativeGestureEvent, + QOffscreenSurface, + QOpenGLContext, + QPalette, + QPixmap, + QPlainTextEdit, + QPoint, + QPushButton, + QShortcut, + QSize, + QSplitter, + QStandardPaths, + Qt, + QTextBrowser, + QTextOption, + QTimer, + QUrl, + QVBoxLayout, + QWheelEvent, + QWidget, + pyqtSlot, + qconnect, + qtmajor, + qtminor, + traceback, +) from aqt.theme import theme_manager if TYPE_CHECKING: @@ -70,6 +120,111 @@ def openLink(link: str | QUrl) -> None: QDesktopServices.openUrl(QUrl(link)) +class MessageBox(QMessageBox): + def __init__( + self, + text: str, + callback: Callable[[int], None] | None = None, + parent: QWidget | None = None, + icon: QMessageBox.Icon = QMessageBox.Icon.NoIcon, + help: HelpPageArgument | None = None, + title: str = "Anki", + buttons: Sequence[str | QMessageBox.StandardButton] | None = None, + default_button: int = 0, + textFormat: Qt.TextFormat = Qt.TextFormat.PlainText, + ) -> None: + parent = parent or aqt.mw.app.activeWindow() or aqt.mw + super().__init__(parent) + self.setText(text) + self.setWindowTitle(title) + self.setWindowModality(Qt.WindowModality.WindowModal) + self.setIcon(icon) + if icon == QMessageBox.Icon.Question and theme_manager.night_mode: + img = self.iconPixmap().toImage() + img.invertPixels() + self.setIconPixmap(QPixmap(img)) + self.setTextFormat(textFormat) + if buttons is None: + buttons = [QMessageBox.StandardButton.Ok] + for i, button in enumerate(buttons): + if isinstance(button, str): + b = self.addButton(button, QMessageBox.ButtonRole.ActionRole) + elif isinstance(button, QMessageBox.StandardButton): + b = self.addButton(button) + else: + continue + if callback is not None: + qconnect(b.clicked, partial(callback, i)) + if i == default_button: + self.setDefaultButton(b) + if help is not None: + b = self.addButton(QMessageBox.StandardButton.Help) + qconnect(b.clicked, lambda: openHelp(help)) + self.open() + + +def ask_user( + text: str, + callback: Callable[[bool], None], + defaults_yes: bool = True, + **kwargs: Any, +) -> MessageBox: + "Shows a yes/no question, passes the answer to the callback function as a bool." + return MessageBox( + text, + callback=lambda response: callback(not response), + icon=QMessageBox.Icon.Question, + buttons=[QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No], + default_button=not defaults_yes, + **kwargs, + ) + + +def ask_user_dialog( + text: str, + callback: Callable[[int], None], + buttons: Sequence[str | QMessageBox.StandardButton] | None = None, + default_button: int = 1, + **kwargs: Any, +) -> MessageBox: + "Shows a question to the user, passes the index of the button clicked to the callback." + if buttons is None: + buttons = [QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No] + return MessageBox( + text, + callback=callback, + icon=QMessageBox.Icon.Question, + buttons=buttons, + default_button=default_button, + **kwargs, + ) + + +def show_info(text: str, callback: Callable | None = None, **kwargs: Any) -> MessageBox: + "Show a small info window with an OK button." + if "icon" not in kwargs: + kwargs["icon"] = QMessageBox.Icon.Information + return MessageBox( + text, + callback=(lambda _: callback()) if callback is not None else None, + **kwargs, + ) + + +def show_warning( + text: str, callback: Callable | None = None, **kwargs: Any +) -> MessageBox: + "Show a small warning window with an OK button." + return show_info(text, icon=QMessageBox.Icon.Warning, callback=callback, **kwargs) + + +def show_critical( + text: str, callback: Callable | None = None, **kwargs: Any +) -> MessageBox: + "Show a small critical error window with an OK button." + return show_info(text, icon=QMessageBox.Icon.Critical, callback=callback, **kwargs) + + def showWarning( text: str, parent: QWidget | None = None,