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
This commit is contained in:
Sam Bradshaw 2022-08-19 10:04:58 +10:00 committed by GitHub
parent 5f6ac1a916
commit 92171e25e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 181 additions and 36 deletions

View file

@ -43,6 +43,7 @@ from aqt.utils import (
saveGeom, saveGeom,
saveSplitter, saveSplitter,
send_to_trash, send_to_trash,
show_info,
showInfo, showInfo,
showWarning, showWarning,
tooltip, tooltip,
@ -862,14 +863,14 @@ class AddonsDialog(QDialog):
def onlyOneSelected(self) -> str | None: def onlyOneSelected(self) -> str | None:
dirs = self.selectedAddons() dirs = self.selectedAddons()
if len(dirs) != 1: 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 None
return dirs[0] return dirs[0]
def selected_addon_meta(self) -> AddonMeta | None: def selected_addon_meta(self) -> AddonMeta | None:
idxs = [x.row() for x in self.form.addonList.selectedIndexes()] idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
if len(idxs) != 1: 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 None
return self.addons[idxs[0]] return self.addons[idxs[0]]

View file

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import enum
import os import os
from concurrent.futures import Future from concurrent.futures import Future
from typing import Callable from typing import Callable
@ -27,8 +26,8 @@ from aqt.qt import (
qconnect, qconnect,
) )
from aqt.utils import ( from aqt.utils import (
ask_user_dialog,
askUser, askUser,
askUserDialog,
disable_help_button, disable_help_button,
showText, showText,
showWarning, showWarning,
@ -36,12 +35,6 @@ from aqt.utils import (
) )
class FullSyncChoice(enum.Enum):
CANCEL = 0
UPLOAD = 1
DOWNLOAD = 2
def get_sync_status( def get_sync_status(
mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None] mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]
) -> None: ) -> None:
@ -135,13 +128,26 @@ def full_sync(
elif out.required == out.FULL_UPLOAD: elif out.required == out.FULL_UPLOAD:
full_upload(mw, on_done) full_upload(mw, on_done)
else: else:
choice = ask_user_to_decide_direction() button_labels: list[str] = [
if choice == FullSyncChoice.UPLOAD: tr.sync_upload_to_ankiweb(),
full_upload(mw, on_done) tr.sync_download_from_ankiweb(),
elif choice == FullSyncChoice.DOWNLOAD: tr.sync_cancel_button(),
full_download(mw, on_done) ]
else:
on_done() 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: 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( def get_id_and_pass_from_user(
mw: aqt.main.AnkiQt, username: str = "", password: str = "" mw: aqt.main.AnkiQt, username: str = "", password: str = ""
) -> tuple[str, str]: ) -> tuple[str, str]:

View file

@ -7,9 +7,9 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
from functools import wraps from functools import partial, wraps
from pathlib import Path 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 from send2trash import send2trash
@ -25,6 +25,56 @@ from anki.utils import (
version_with_build, version_with_build,
) )
from aqt.qt import * 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 from aqt.theme import theme_manager
if TYPE_CHECKING: if TYPE_CHECKING:
@ -70,6 +120,111 @@ def openLink(link: str | QUrl) -> None:
QDesktopServices.openUrl(QUrl(link)) 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( def showWarning(
text: str, text: str,
parent: QWidget | None = None, parent: QWidget | None = None,