mirror of
https://github.com/ankitects/anki.git
synced 2025-12-11 13:56:55 -05:00
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:
parent
5f6ac1a916
commit
92171e25e6
3 changed files with 181 additions and 36 deletions
|
|
@ -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]]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
159
qt/aqt/utils.py
159
qt/aqt/utils.py
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue