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,
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]]

View file

@ -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]:

View file

@ -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,