2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2020-02-17 01:18:20 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2023-05-18 01:44:12 +02:00
|
|
|
import enum
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
import inspect
|
2019-12-20 10:19:03 +01:00
|
|
|
import os
|
|
|
|
import re
|
2022-03-28 06:40:31 +02:00
|
|
|
import shutil
|
2019-12-20 10:19:03 +01:00
|
|
|
import subprocess
|
|
|
|
import sys
|
2022-08-19 02:04:58 +02:00
|
|
|
from functools import partial, wraps
|
2022-03-28 06:40:31 +02:00
|
|
|
from pathlib import Path
|
2022-11-04 05:56:36 +01:00
|
|
|
from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, Union
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2022-03-28 06:40:31 +02:00
|
|
|
from send2trash import send2trash
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt
|
2021-12-04 05:35:47 +01:00
|
|
|
from anki._legacy import DeprecatedNamesMixinForModule
|
2021-07-22 10:07:13 +02:00
|
|
|
from anki.collection import Collection, HelpPage
|
2021-03-26 04:48:26 +01:00
|
|
|
from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import
|
2021-10-25 06:50:13 +02:00
|
|
|
from anki.utils import (
|
|
|
|
invalid_filename,
|
2021-11-25 00:06:16 +01:00
|
|
|
is_mac,
|
|
|
|
is_win,
|
2021-10-25 06:50:13 +02:00
|
|
|
no_bundled_libs,
|
|
|
|
version_with_build,
|
|
|
|
)
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.qt import *
|
2022-08-19 02:04:58 +02:00
|
|
|
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,
|
2022-09-21 04:21:10 +02:00
|
|
|
qVersion,
|
2022-08-19 02:04:58 +02:00
|
|
|
traceback,
|
|
|
|
)
|
2021-05-20 10:26:18 +02:00
|
|
|
from aqt.theme import theme_manager
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2020-05-27 01:14:02 +02:00
|
|
|
if TYPE_CHECKING:
|
2023-09-23 06:04:25 +02:00
|
|
|
TextFormat = Literal["plain", "rich", "markdown"]
|
2021-01-27 05:22:17 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
def aqt_data_path() -> Path:
|
2021-10-28 10:46:45 +02:00
|
|
|
# packaged?
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
if getattr(sys, "frozen", False):
|
|
|
|
prefix = Path(sys.prefix)
|
|
|
|
path = prefix / "lib/_aqt/data"
|
|
|
|
if path.exists():
|
2021-10-28 10:46:45 +02:00
|
|
|
return path
|
|
|
|
else:
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
return prefix / "../Resources/_aqt/data"
|
2021-02-04 11:28:25 +01:00
|
|
|
else:
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
import _aqt.colors
|
|
|
|
|
|
|
|
data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data")
|
|
|
|
if data_folder.exists():
|
|
|
|
return data_folder.absolute()
|
|
|
|
else:
|
|
|
|
# should only happen when running unit tests
|
|
|
|
print("warning, data folder not found")
|
|
|
|
return Path(".")
|
|
|
|
|
|
|
|
|
|
|
|
def aqt_data_folder() -> str:
|
|
|
|
return str(aqt_data_path())
|
2020-01-02 10:43:19 +01:00
|
|
|
|
|
|
|
|
2021-03-26 04:48:26 +01:00
|
|
|
# shortcut to access Fluent translations; set as
|
|
|
|
tr = tr_legacyglobal
|
2020-02-17 01:18:20 +01:00
|
|
|
|
2021-07-23 12:17:20 +02:00
|
|
|
HelpPageArgument = Union["HelpPage.V", str]
|
2021-01-25 14:45:47 +01:00
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def openHelp(section: HelpPageArgument) -> None:
|
2021-07-22 10:07:13 +02:00
|
|
|
if isinstance(section, str):
|
2021-07-22 16:32:49 +02:00
|
|
|
link = tr.backend().help_page_link(page=HelpPage.INDEX) + section
|
2021-07-22 10:07:13 +02:00
|
|
|
else:
|
2021-07-22 16:32:49 +02:00
|
|
|
link = tr.backend().help_page_link(page=section)
|
|
|
|
openLink(link)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def openLink(link: str | QUrl) -> None:
|
2021-03-26 04:48:26 +01:00
|
|
|
tooltip(tr.qt_misc_loading(), period=1000)
|
2021-10-25 06:50:13 +02:00
|
|
|
with no_bundled_libs():
|
2017-06-27 04:04:42 +02:00
|
|
|
QDesktopServices.openUrl(QUrl(link))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-08-19 02:04:58 +02:00
|
|
|
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,
|
2023-09-10 05:22:20 +02:00
|
|
|
modality: Qt.WindowModality = Qt.WindowModality.WindowModal,
|
2022-08-19 02:04:58 +02:00
|
|
|
) -> None:
|
|
|
|
parent = parent or aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
super().__init__(parent)
|
|
|
|
self.setText(text)
|
|
|
|
self.setWindowTitle(title)
|
2023-09-10 05:22:20 +02:00
|
|
|
self.setWindowModality(modality)
|
2022-08-19 02:04:58 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2021-01-27 05:22:17 +01:00
|
|
|
def showWarning(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QWidget | None = None,
|
2022-03-28 14:17:50 +02:00
|
|
|
help: HelpPageArgument | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
title: str = "Anki",
|
2021-10-03 10:59:42 +02:00
|
|
|
textFormat: TextFormat | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small warning with an OK button."
|
2019-02-26 00:36:02 +01:00
|
|
|
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-27 05:22:17 +01:00
|
|
|
def showCritical(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QDialog | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
help: str = "",
|
|
|
|
title: str = "Anki",
|
2021-10-03 10:59:42 +02:00
|
|
|
textFormat: TextFormat | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small critical error with an OK button."
|
2019-02-26 00:36:02 +01:00
|
|
|
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-01-03 17:48:17 +01:00
|
|
|
def showInfo(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QWidget | None = None,
|
2022-03-28 14:17:50 +02:00
|
|
|
help: HelpPageArgument | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
type: str = "info",
|
|
|
|
title: str = "Anki",
|
2021-10-03 10:59:42 +02:00
|
|
|
textFormat: TextFormat | None = None,
|
|
|
|
customBtns: list[QMessageBox.StandardButton] | None = None,
|
2021-01-27 05:22:17 +01:00
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small info window with an OK button."
|
2021-02-01 11:23:48 +01:00
|
|
|
parent_widget: QWidget
|
2021-03-17 05:51:59 +01:00
|
|
|
if parent is None:
|
2021-02-01 11:23:48 +01:00
|
|
|
parent_widget = aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
else:
|
|
|
|
parent_widget = parent
|
2012-12-21 08:51:59 +01:00
|
|
|
if type == "warning":
|
2021-10-05 05:53:01 +02:00
|
|
|
icon = QMessageBox.Icon.Warning
|
2012-12-21 08:51:59 +01:00
|
|
|
elif type == "critical":
|
2021-10-05 05:53:01 +02:00
|
|
|
icon = QMessageBox.Icon.Critical
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-10-05 05:53:01 +02:00
|
|
|
icon = QMessageBox.Icon.Information
|
2021-02-01 11:23:48 +01:00
|
|
|
mb = QMessageBox(parent_widget) #
|
2019-02-26 00:36:02 +01:00
|
|
|
if textFormat == "plain":
|
2021-10-05 05:53:01 +02:00
|
|
|
mb.setTextFormat(Qt.TextFormat.PlainText)
|
2019-02-26 00:36:02 +01:00
|
|
|
elif textFormat == "rich":
|
2021-10-05 05:53:01 +02:00
|
|
|
mb.setTextFormat(Qt.TextFormat.RichText)
|
2023-09-23 06:04:25 +02:00
|
|
|
elif textFormat == "markdown":
|
|
|
|
mb.setTextFormat(Qt.TextFormat.MarkdownText)
|
2019-02-27 05:16:35 +01:00
|
|
|
elif textFormat is not None:
|
2019-02-26 00:36:02 +01:00
|
|
|
raise Exception("unexpected textFormat type")
|
2012-12-21 08:51:59 +01:00
|
|
|
mb.setText(text)
|
|
|
|
mb.setIcon(icon)
|
2016-04-30 07:44:41 +02:00
|
|
|
mb.setWindowTitle(title)
|
2020-01-03 17:48:17 +01:00
|
|
|
if customBtns:
|
|
|
|
default = None
|
|
|
|
for btn in customBtns:
|
|
|
|
b = mb.addButton(btn)
|
|
|
|
if not default:
|
|
|
|
default = b
|
|
|
|
mb.setDefaultButton(default)
|
|
|
|
else:
|
2021-10-05 05:53:01 +02:00
|
|
|
b = mb.addButton(QMessageBox.StandardButton.Ok)
|
2020-01-03 17:48:17 +01:00
|
|
|
b.setDefault(True)
|
2022-03-28 14:17:50 +02:00
|
|
|
if help is not None:
|
2021-10-05 05:53:01 +02:00
|
|
|
b = mb.addButton(QMessageBox.StandardButton.Help)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.clicked, lambda: openHelp(help))
|
2012-12-21 08:51:59 +01:00
|
|
|
b.setAutoDefault(False)
|
2021-10-05 02:01:45 +02:00
|
|
|
return mb.exec()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
def showText(
|
2021-02-01 11:23:48 +01:00
|
|
|
txt: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QWidget | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
type: str = "text",
|
|
|
|
run: bool = True,
|
2021-10-03 10:59:42 +02:00
|
|
|
geomKey: str | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
minWidth: int = 500,
|
|
|
|
minHeight: int = 400,
|
|
|
|
title: str = "Anki",
|
|
|
|
copyBtn: bool = False,
|
2021-02-02 06:47:51 +01:00
|
|
|
plain_text_edit: bool = False,
|
2021-10-03 10:59:42 +02:00
|
|
|
) -> tuple[QDialog, QDialogButtonBox] | None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
diag = QDialog(parent)
|
2016-04-30 07:44:41 +02:00
|
|
|
diag.setWindowTitle(title)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(diag)
|
2012-12-21 08:51:59 +01:00
|
|
|
layout = QVBoxLayout(diag)
|
|
|
|
diag.setLayout(layout)
|
2021-10-03 10:59:42 +02:00
|
|
|
text: QPlainTextEdit | QTextBrowser
|
2021-02-02 06:47:51 +01:00
|
|
|
if plain_text_edit:
|
|
|
|
# used by the importer
|
|
|
|
text = QPlainTextEdit()
|
|
|
|
text.setReadOnly(True)
|
2021-10-05 05:53:01 +02:00
|
|
|
text.setWordWrapMode(QTextOption.WrapMode.NoWrap)
|
2021-03-22 02:24:53 +01:00
|
|
|
text.setPlainText(txt)
|
2021-02-02 06:47:51 +01:00
|
|
|
else:
|
|
|
|
text = QTextBrowser()
|
|
|
|
text.setOpenExternalLinks(True)
|
2021-03-17 05:51:59 +01:00
|
|
|
if type == "text":
|
|
|
|
text.setPlainText(txt)
|
|
|
|
else:
|
|
|
|
text.setHtml(txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
layout.addWidget(text)
|
2021-10-05 05:53:01 +02:00
|
|
|
box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
2012-12-21 08:51:59 +01:00
|
|
|
layout.addWidget(box)
|
2019-02-16 23:05:06 +01:00
|
|
|
if copyBtn:
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onCopy() -> None:
|
2019-02-16 23:05:06 +01:00
|
|
|
QApplication.clipboard().setText(text.toPlainText())
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-03-26 04:48:26 +01:00
|
|
|
btn = QPushButton(tr.qt_misc_copy_to_clipboard())
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(btn.clicked, onCopy)
|
2021-10-05 05:53:01 +02:00
|
|
|
box.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onReject() -> None:
|
2014-06-18 20:47:45 +02:00
|
|
|
if geomKey:
|
|
|
|
saveGeom(diag, geomKey)
|
|
|
|
QDialog.reject(diag)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(box.rejected, onReject)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onFinish() -> None:
|
2017-04-26 22:25:16 +02:00
|
|
|
if geomKey:
|
|
|
|
saveGeom(diag, geomKey)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(box.accepted, onFinish)
|
2016-04-30 07:44:41 +02:00
|
|
|
diag.setMinimumHeight(minHeight)
|
|
|
|
diag.setMinimumWidth(minWidth)
|
2014-06-18 20:47:45 +02:00
|
|
|
if geomKey:
|
|
|
|
restoreGeom(diag, geomKey)
|
2012-12-21 08:51:59 +01:00
|
|
|
if run:
|
2021-10-05 02:01:45 +02:00
|
|
|
diag.exec()
|
2021-02-01 11:23:48 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
return diag, box
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-25 14:45:47 +01:00
|
|
|
def askUser(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
2021-03-17 05:51:59 +01:00
|
|
|
parent: QWidget = None,
|
2021-01-25 14:45:47 +01:00
|
|
|
help: HelpPageArgument = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
defaultno: bool = False,
|
2021-10-03 10:59:42 +02:00
|
|
|
msgfunc: Callable | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
title: str = "Anki",
|
|
|
|
) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a yes/no question. Return true if yes."
|
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow()
|
|
|
|
if not msgfunc:
|
|
|
|
msgfunc = QMessageBox.question
|
2021-10-05 05:53:01 +02:00
|
|
|
sb = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2021-10-05 05:53:01 +02:00
|
|
|
sb |= QMessageBox.StandardButton.Help
|
2012-12-21 08:51:59 +01:00
|
|
|
while 1:
|
|
|
|
if defaultno:
|
2021-10-05 05:53:01 +02:00
|
|
|
default = QMessageBox.StandardButton.No
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-10-05 05:53:01 +02:00
|
|
|
default = QMessageBox.StandardButton.Yes
|
|
|
|
r = msgfunc(parent, title, text, sb, default)
|
|
|
|
if r == QMessageBox.StandardButton.Help:
|
2012-12-21 08:51:59 +01:00
|
|
|
openHelp(help)
|
|
|
|
else:
|
|
|
|
break
|
2021-10-05 05:53:01 +02:00
|
|
|
return r == QMessageBox.StandardButton.Yes
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class ButtonedDialog(QMessageBox):
|
2021-01-25 14:45:47 +01:00
|
|
|
def __init__(
|
2021-02-01 11:23:48 +01:00
|
|
|
self,
|
|
|
|
text: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
buttons: list[str],
|
|
|
|
parent: QWidget | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
help: HelpPageArgument = None,
|
|
|
|
title: str = "Anki",
|
2021-01-25 14:45:47 +01:00
|
|
|
):
|
2019-03-04 06:59:53 +01:00
|
|
|
QMessageBox.__init__(self, parent)
|
2021-10-03 10:59:42 +02:00
|
|
|
self._buttons: list[QPushButton] = []
|
2016-04-30 07:44:41 +02:00
|
|
|
self.setWindowTitle(title)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.help = help
|
2021-10-05 05:53:01 +02:00
|
|
|
self.setIcon(QMessageBox.Icon.Warning)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setText(text)
|
|
|
|
for b in buttons:
|
2021-10-05 05:53:01 +02:00
|
|
|
self._buttons.append(self.addButton(b, QMessageBox.ButtonRole.AcceptRole))
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.addButton(tr.actions_help(), QMessageBox.ButtonRole.HelpRole)
|
2021-03-26 04:48:26 +01:00
|
|
|
buttons.append(tr.actions_help())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def run(self) -> str:
|
2021-10-05 02:01:45 +02:00
|
|
|
self.exec()
|
2012-12-21 08:51:59 +01:00
|
|
|
but = self.clickedButton().text()
|
|
|
|
if but == "Help":
|
|
|
|
# FIXME stop dialog closing?
|
|
|
|
openHelp(self.help)
|
2017-07-31 07:48:34 +02:00
|
|
|
txt = self.clickedButton().text()
|
|
|
|
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
|
|
|
return txt.replace("&", "")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def setDefault(self, idx: int) -> None:
|
2020-08-02 02:25:48 +02:00
|
|
|
self.setDefaultButton(self._buttons[idx])
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-25 14:45:47 +01:00
|
|
|
def askUserDialog(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
buttons: list[str],
|
|
|
|
parent: QWidget | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
help: HelpPageArgument = None,
|
|
|
|
title: str = "Anki",
|
|
|
|
) -> ButtonedDialog:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw
|
2016-04-30 07:44:41 +02:00
|
|
|
diag = ButtonedDialog(text, buttons, parent, help, title=title)
|
2012-12-21 08:51:59 +01:00
|
|
|
return diag
|
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class GetTextDialog(QDialog):
|
|
|
|
def __init__(
|
|
|
|
self,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QWidget | None,
|
2021-02-01 11:23:48 +01:00
|
|
|
question: str,
|
2021-01-25 14:45:47 +01:00
|
|
|
help: HelpPageArgument = None,
|
2021-10-03 10:59:42 +02:00
|
|
|
edit: QLineEdit | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
default: str = "",
|
|
|
|
title: str = "Anki",
|
|
|
|
minWidth: int = 400,
|
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, parent)
|
|
|
|
self.setWindowTitle(title)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.question = question
|
|
|
|
self.help = help
|
|
|
|
self.qlabel = QLabel(question)
|
2016-04-30 07:44:41 +02:00
|
|
|
self.setMinimumWidth(minWidth)
|
2012-12-21 08:51:59 +01:00
|
|
|
v = QVBoxLayout()
|
|
|
|
v.addWidget(self.qlabel)
|
|
|
|
if not edit:
|
|
|
|
edit = QLineEdit()
|
|
|
|
self.l = edit
|
|
|
|
if default:
|
|
|
|
self.l.setText(default)
|
|
|
|
self.l.selectAll()
|
|
|
|
v.addWidget(self.l)
|
2021-10-05 05:53:01 +02:00
|
|
|
buts = (
|
|
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2021-10-05 05:53:01 +02:00
|
|
|
buts |= QDialogButtonBox.StandardButton.Help
|
2020-08-02 02:25:48 +02:00
|
|
|
b = QDialogButtonBox(buts) # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
v.addWidget(b)
|
|
|
|
self.setLayout(v)
|
2021-10-05 05:53:01 +02:00
|
|
|
qconnect(b.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept)
|
|
|
|
qconnect(b.button(QDialogButtonBox.StandardButton.Cancel).clicked, self.reject)
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2021-10-05 05:53:01 +02:00
|
|
|
qconnect(
|
|
|
|
b.button(QDialogButtonBox.StandardButton.Help).clicked,
|
|
|
|
self.helpRequested,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def accept(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
return QDialog.accept(self)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def reject(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
return QDialog.reject(self)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def helpRequested(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
openHelp(self.help)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
def getText(
|
2021-02-01 11:23:48 +01:00
|
|
|
prompt: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QWidget | None = None,
|
2021-01-25 14:45:47 +01:00
|
|
|
help: HelpPageArgument = None,
|
2021-10-03 10:59:42 +02:00
|
|
|
edit: QLineEdit | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
default: str = "",
|
|
|
|
title: str = "Anki",
|
2021-10-03 10:59:42 +02:00
|
|
|
geomKey: str | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
**kwargs: Any,
|
2021-10-03 10:59:42 +02:00
|
|
|
) -> tuple[str, int]:
|
Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"
For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.
When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.
For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.
- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.
There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.
Other notes:
- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 11:58:16 +01:00
|
|
|
"Returns (string, succeeded)."
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
2019-12-23 01:34:10 +01:00
|
|
|
d = GetTextDialog(
|
|
|
|
parent, prompt, help=help, edit=edit, default=default, title=title, **kwargs
|
|
|
|
)
|
2021-10-05 05:53:01 +02:00
|
|
|
d.setWindowModality(Qt.WindowModality.WindowModal)
|
2017-05-03 10:55:24 +02:00
|
|
|
if geomKey:
|
|
|
|
restoreGeom(d, geomKey)
|
2021-10-05 02:01:45 +02:00
|
|
|
ret = d.exec()
|
2017-05-03 10:55:24 +02:00
|
|
|
if geomKey and ret:
|
|
|
|
saveGeom(d, geomKey)
|
2016-05-12 06:45:35 +02:00
|
|
|
return (str(d.l.text()), ret)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getOnlyText(*args: Any, **kwargs: Any) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
(s, r) = getText(*args, **kwargs)
|
|
|
|
if r:
|
|
|
|
return s
|
|
|
|
else:
|
2016-05-12 06:45:35 +02:00
|
|
|
return ""
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# fixme: these utilities could be combined into a single base class
|
2021-02-01 11:23:48 +01:00
|
|
|
# unused by Anki, but used by add-ons
|
|
|
|
def chooseList(
|
2021-10-03 10:59:42 +02:00
|
|
|
prompt: str, choices: list[str], startrow: int = 0, parent: Any = None
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow()
|
|
|
|
d = QDialog(parent)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(d)
|
2021-10-05 05:53:01 +02:00
|
|
|
d.setWindowModality(Qt.WindowModality.WindowModal)
|
2012-12-21 08:51:59 +01:00
|
|
|
l = QVBoxLayout()
|
|
|
|
d.setLayout(l)
|
|
|
|
t = QLabel(prompt)
|
|
|
|
l.addWidget(t)
|
|
|
|
c = QListWidget()
|
|
|
|
c.addItems(choices)
|
|
|
|
c.setCurrentRow(startrow)
|
|
|
|
l.addWidget(c)
|
2021-10-05 05:53:01 +02:00
|
|
|
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(bb.accepted, d.accept)
|
2012-12-21 08:51:59 +01:00
|
|
|
l.addWidget(bb)
|
2021-10-05 02:01:45 +02:00
|
|
|
d.exec()
|
2012-12-21 08:51:59 +01:00
|
|
|
return c.currentRow()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getTag(
|
2021-03-17 05:51:59 +01:00
|
|
|
parent: QWidget, deck: Collection, question: str, **kwargs: Any
|
2021-10-03 10:59:42 +02:00
|
|
|
) -> tuple[str, int]:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.tagedit import TagEdit
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
te = TagEdit(parent)
|
|
|
|
te.setCol(deck)
|
2019-12-23 01:34:10 +01:00
|
|
|
ret = getText(question, parent, edit=te, geomKey="getTag", **kwargs)
|
2012-12-21 08:51:59 +01:00
|
|
|
te.hideCompleter()
|
|
|
|
return ret
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-07 05:24:49 +01:00
|
|
|
def disable_help_button(widget: QWidget) -> None:
|
|
|
|
"Disable the help button in the window titlebar."
|
2021-10-05 05:53:01 +02:00
|
|
|
widget.setWindowFlags(
|
|
|
|
widget.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint
|
|
|
|
)
|
2021-01-07 05:24:49 +01:00
|
|
|
|
|
|
|
|
2021-10-17 17:56:40 +02:00
|
|
|
def setWindowIcon(widget: QWidget) -> None:
|
|
|
|
icon = QIcon()
|
|
|
|
icon.addPixmap(QPixmap("icons:anki.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
|
|
|
widget.setWindowIcon(icon)
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# File handling
|
|
|
|
######################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getFile(
|
2021-03-17 05:51:59 +01:00
|
|
|
parent: QWidget,
|
2021-02-01 11:23:48 +01:00
|
|
|
title: str,
|
2021-02-02 14:30:53 +01:00
|
|
|
# single file returned unless multi=True
|
2021-10-03 10:59:42 +02:00
|
|
|
cb: Callable[[str | Sequence[str]], None] | None,
|
2022-07-04 01:33:20 +02:00
|
|
|
filter: str = "*",
|
2021-10-03 10:59:42 +02:00
|
|
|
dir: str | None = None,
|
|
|
|
key: str | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
multi: bool = False, # controls whether a single or multiple files is returned
|
2021-10-03 10:59:42 +02:00
|
|
|
) -> Sequence[str] | str | None:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Ask the user for a file."
|
2021-11-25 08:47:50 +01:00
|
|
|
if dir and key:
|
|
|
|
raise Exception("expected dir or key")
|
2012-12-21 08:51:59 +01:00
|
|
|
if not dir:
|
2021-02-11 01:09:06 +01:00
|
|
|
dirkey = f"{key}Directory"
|
2012-12-21 08:51:59 +01:00
|
|
|
dir = aqt.mw.pm.profile.get(dirkey, "")
|
|
|
|
else:
|
|
|
|
dirkey = None
|
|
|
|
d = QFileDialog(parent)
|
2021-10-05 05:53:01 +02:00
|
|
|
mode = (
|
|
|
|
QFileDialog.FileMode.ExistingFiles
|
|
|
|
if multi
|
|
|
|
else QFileDialog.FileMode.ExistingFile
|
|
|
|
)
|
2019-02-18 07:10:43 +01:00
|
|
|
d.setFileMode(mode)
|
2016-03-21 00:57:45 +01:00
|
|
|
if os.path.exists(dir):
|
|
|
|
d.setDirectory(dir)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.setWindowTitle(title)
|
|
|
|
d.setNameFilter(filter)
|
|
|
|
ret = []
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def accept() -> None:
|
2019-02-18 07:10:43 +01:00
|
|
|
files = list(d.selectedFiles())
|
2012-12-21 08:51:59 +01:00
|
|
|
if dirkey:
|
2019-02-18 07:10:43 +01:00
|
|
|
dir = os.path.dirname(files[0])
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.mw.pm.profile[dirkey] = dir
|
2019-02-18 07:10:43 +01:00
|
|
|
result = files if multi else files[0]
|
2012-12-21 08:51:59 +01:00
|
|
|
if cb:
|
2019-02-18 07:10:43 +01:00
|
|
|
cb(result)
|
|
|
|
ret.append(result)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(d.accepted, accept)
|
2018-07-23 05:57:17 +02:00
|
|
|
if key:
|
|
|
|
restoreState(d, key)
|
2021-10-05 02:01:45 +02:00
|
|
|
d.exec()
|
2018-07-23 05:57:17 +02:00
|
|
|
if key:
|
|
|
|
saveState(d, key)
|
2021-02-01 11:23:48 +01:00
|
|
|
return ret[0] if ret else None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2023-03-09 11:23:58 +01:00
|
|
|
def running_in_sandbox():
|
|
|
|
"""Check whether running in Flatpak or Snap. When in such a sandbox, Qt
|
|
|
|
will not report the true location of user-chosen files, but instead a
|
|
|
|
temporary location from which the sandboxing software will copy the file to
|
|
|
|
the user-chosen destination. Thus file renames are impossible and caching
|
|
|
|
the reported file location is unhelpful."""
|
|
|
|
in_flatpak = (
|
|
|
|
QStandardPaths.locate(
|
|
|
|
QStandardPaths.StandardLocation.RuntimeLocation,
|
|
|
|
"flatpak-info",
|
|
|
|
)
|
|
|
|
!= ""
|
|
|
|
)
|
2023-04-28 03:39:18 +02:00
|
|
|
in_snap = bool(os.environ.get("SNAP"))
|
2023-03-09 11:23:58 +01:00
|
|
|
return in_flatpak or in_snap
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getSaveFile(
|
|
|
|
parent: QDialog,
|
|
|
|
title: str,
|
|
|
|
dir_description: str,
|
|
|
|
key: str,
|
|
|
|
ext: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
fname: str | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> str:
|
2013-05-18 18:24:53 +02:00
|
|
|
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
2013-05-21 04:53:02 +02:00
|
|
|
variable. The file dialog will default to open with FNAME."""
|
2021-02-11 01:09:06 +01:00
|
|
|
config_key = f"{dir_description}Directory"
|
2017-09-10 08:42:29 +02:00
|
|
|
|
2021-10-05 05:53:01 +02:00
|
|
|
defaultPath = QStandardPaths.writableLocation(
|
|
|
|
QStandardPaths.StandardLocation.DocumentsLocation
|
|
|
|
)
|
2017-09-10 09:01:52 +02:00
|
|
|
base = aqt.mw.pm.profile.get(config_key, defaultPath)
|
2013-05-21 04:53:02 +02:00
|
|
|
path = os.path.join(base, fname)
|
2016-05-31 10:51:40 +02:00
|
|
|
file = QFileDialog.getSaveFileName(
|
2019-12-23 01:34:10 +01:00
|
|
|
parent,
|
|
|
|
title,
|
|
|
|
path,
|
2021-02-11 00:37:38 +01:00
|
|
|
f"{key} (*{ext})",
|
2021-10-05 05:53:01 +02:00
|
|
|
options=QFileDialog.Option.DontConfirmOverwrite,
|
2019-12-23 01:34:10 +01:00
|
|
|
)[0]
|
2023-03-09 11:23:58 +01:00
|
|
|
if file and not running_in_sandbox():
|
2012-12-21 08:51:59 +01:00
|
|
|
# add extension
|
|
|
|
if not file.lower().endswith(ext):
|
|
|
|
file += ext
|
|
|
|
# save new default
|
|
|
|
dir = os.path.dirname(file)
|
2013-05-18 18:24:53 +02:00
|
|
|
aqt.mw.pm.profile[config_key] = dir
|
2012-12-21 08:51:59 +01:00
|
|
|
# check if it exists
|
|
|
|
if os.path.exists(file):
|
2021-03-26 04:48:26 +01:00
|
|
|
if not askUser(tr.qt_misc_this_file_exists_are_you_sure(), parent):
|
2012-12-21 08:51:59 +01:00
|
|
|
return None
|
|
|
|
return file
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2023-05-18 01:44:12 +02:00
|
|
|
class _QtStateKeyKind(enum.Enum):
|
|
|
|
HEADER = enum.auto()
|
|
|
|
SPLITTER = enum.auto()
|
|
|
|
STATE = enum.auto()
|
|
|
|
GEOMETRY = enum.auto()
|
|
|
|
|
|
|
|
|
|
|
|
def _qt_state_key(kind: _QtStateKeyKind, key: str) -> str:
|
|
|
|
"""Construct a key used to save/restore geometry, state, etc.
|
|
|
|
|
|
|
|
Adds Qt version number to key so that different data is saved per Qt version,
|
|
|
|
preventing crashes and bugs when restoring data saved with a different Qt version.
|
|
|
|
"""
|
|
|
|
qt_suffix = f"{qtmajor}.{qtminor}" if qtmajor > 5 else ""
|
|
|
|
return f"{key}{kind.name.capitalize()}{qt_suffix}"
|
|
|
|
|
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
def saveGeom(widget: QWidget, key: str) -> None:
|
2023-09-20 06:16:12 +02:00
|
|
|
# restoring a fullscreen window breaks the tab functionality of 5.15
|
|
|
|
if not widget.isFullScreen() or qtmajor == 6:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key)
|
|
|
|
aqt.mw.pm.profile[key] = widget.saveGeometry()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreGeom(
|
2022-12-08 12:51:47 +01:00
|
|
|
widget: QWidget,
|
|
|
|
key: str,
|
|
|
|
adjustSize: bool = False,
|
|
|
|
default_size: tuple[int, int] | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key)
|
2022-12-08 12:51:47 +01:00
|
|
|
if existing_geom := aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreGeometry(existing_geom)
|
2019-02-11 22:49:35 +01:00
|
|
|
ensureWidgetInScreenBoundaries(widget)
|
2022-12-08 12:51:47 +01:00
|
|
|
elif adjustSize:
|
|
|
|
widget.adjustSize()
|
|
|
|
elif default_size:
|
|
|
|
widget.resize(*default_size)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
2019-02-11 22:49:35 +01:00
|
|
|
handle = widget.window().windowHandle()
|
|
|
|
if not handle:
|
|
|
|
# window has not yet been shown, retry later
|
2022-02-18 10:00:12 +01:00
|
|
|
aqt.mw.progress.timer(
|
|
|
|
50, lambda: ensureWidgetInScreenBoundaries(widget), False, parent=widget
|
|
|
|
)
|
2019-02-11 22:49:35 +01:00
|
|
|
return
|
|
|
|
|
2019-02-14 04:47:44 +01:00
|
|
|
# ensure widget is smaller than screen bounds
|
2019-02-11 22:49:35 +01:00
|
|
|
geom = handle.screen().availableGeometry()
|
2019-02-14 04:47:44 +01:00
|
|
|
wsize = widget.size()
|
|
|
|
cappedWidth = min(geom.width(), wsize.width())
|
|
|
|
cappedHeight = min(geom.height(), wsize.height())
|
|
|
|
if cappedWidth > wsize.width() or cappedHeight > wsize.height():
|
|
|
|
widget.resize(QSize(cappedWidth, cappedHeight))
|
|
|
|
|
|
|
|
# ensure widget is inside top left
|
|
|
|
wpos = widget.pos()
|
|
|
|
x = max(geom.x(), wpos.x())
|
|
|
|
y = max(geom.y(), wpos.y())
|
|
|
|
# and bottom right
|
2019-12-23 01:34:10 +01:00
|
|
|
x = min(x, geom.width() + geom.x() - cappedWidth)
|
|
|
|
y = min(y, geom.height() + geom.y() - cappedHeight)
|
2019-02-14 04:47:44 +01:00
|
|
|
if x != wpos.x() or y != wpos.y():
|
2019-02-11 22:49:35 +01:00
|
|
|
widget.move(x, y)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def saveState(widget: QFileDialog | QMainWindow, key: str) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.STATE, key)
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.STATE, key)
|
|
|
|
if data := aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreState(data)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def saveSplitter(widget: QSplitter, key: str) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.SPLITTER, key)
|
2020-06-08 20:55:25 +02:00
|
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreSplitter(widget: QSplitter, key: str) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.SPLITTER, key)
|
|
|
|
if data := aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreState(data)
|
2021-10-15 02:53:28 +02:00
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def saveHeader(widget: QHeaderView, key: str) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.HEADER, key)
|
|
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
2020-06-08 20:55:25 +02:00
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreHeader(widget: QHeaderView, key: str) -> None:
|
2023-05-18 01:44:12 +02:00
|
|
|
key = _qt_state_key(_QtStateKeyKind.HEADER, key)
|
|
|
|
if state := aqt.mw.pm.profile.get(key):
|
2021-10-15 02:53:28 +02:00
|
|
|
widget.restoreState(state)
|
2020-06-08 20:55:25 +02:00
|
|
|
|
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
def save_is_checked(widget: QCheckBox, key: str) -> None:
|
2020-05-31 05:57:11 +02:00
|
|
|
key += "IsChecked"
|
|
|
|
aqt.mw.pm.profile[key] = widget.isChecked()
|
|
|
|
|
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
def restore_is_checked(widget: QCheckBox, key: str) -> None:
|
2020-05-31 05:57:11 +02:00
|
|
|
key += "IsChecked"
|
|
|
|
if aqt.mw.pm.profile.get(key) is not None:
|
|
|
|
widget.setChecked(aqt.mw.pm.profile[key])
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
|
2021-02-11 01:09:06 +01:00
|
|
|
textKey = f"{key}ComboActiveText"
|
|
|
|
indexKey = f"{key}ComboActiveIndex"
|
2020-06-01 17:47:46 +02:00
|
|
|
aqt.mw.pm.session[textKey] = widget.currentText()
|
|
|
|
aqt.mw.pm.session[indexKey] = widget.currentIndex()
|
2020-05-31 21:52:58 +02:00
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restore_combo_index_for_session(
|
2021-10-03 10:59:42 +02:00
|
|
|
widget: QComboBox, history: list[str], key: str
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> None:
|
2021-02-11 01:09:06 +01:00
|
|
|
textKey = f"{key}ComboActiveText"
|
|
|
|
indexKey = f"{key}ComboActiveIndex"
|
2020-06-01 17:47:46 +02:00
|
|
|
text = aqt.mw.pm.session.get(textKey)
|
|
|
|
index = aqt.mw.pm.session.get(indexKey)
|
2020-05-31 21:52:58 +02:00
|
|
|
if text is not None and index is not None:
|
|
|
|
if index < len(history) and history[index] == text:
|
|
|
|
widget.setCurrentIndex(index)
|
|
|
|
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str:
|
2020-05-31 22:30:14 +02:00
|
|
|
name += "BoxHistory"
|
|
|
|
text_input = comboBox.lineEdit().text()
|
|
|
|
if text_input in history:
|
|
|
|
history.remove(text_input)
|
|
|
|
history.insert(0, text_input)
|
|
|
|
history = history[:50]
|
|
|
|
comboBox.clear()
|
|
|
|
comboBox.addItems(history)
|
2020-06-01 17:47:46 +02:00
|
|
|
aqt.mw.pm.session[name] = text_input
|
2020-05-31 22:30:14 +02:00
|
|
|
aqt.mw.pm.profile[name] = history
|
|
|
|
return text_input
|
|
|
|
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]:
|
2020-06-01 17:34:26 +02:00
|
|
|
name += "BoxHistory"
|
|
|
|
history = aqt.mw.pm.profile.get(name, [])
|
2020-06-01 17:47:46 +02:00
|
|
|
comboBox.addItems([""] + history)
|
|
|
|
if history:
|
|
|
|
session_input = aqt.mw.pm.session.get(name)
|
|
|
|
if session_input and session_input == history[0]:
|
|
|
|
comboBox.lineEdit().setText(session_input)
|
|
|
|
comboBox.lineEdit().selectAll()
|
2020-06-01 17:34:26 +02:00
|
|
|
return history
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def mungeQA(col: Collection, txt: str) -> str:
|
2021-07-16 02:37:59 +02:00
|
|
|
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
|
|
|
|
txt = col.media.escape_media_filenames(txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
return txt
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def openFolder(path: str) -> None:
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2021-03-29 06:37:03 +02:00
|
|
|
subprocess.run(["explorer", f"file://{path}"], check=False)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-10-25 06:50:13 +02:00
|
|
|
with no_bundled_libs():
|
2021-02-11 01:09:06 +01:00
|
|
|
QDesktopServices.openUrl(QUrl(f"file://{path}"))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def shortcut(key: str) -> str:
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_mac:
|
2012-12-21 08:51:59 +01:00
|
|
|
return re.sub("(?i)ctrl", "Command", key)
|
|
|
|
return key
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def maybeHideClose(bbox: QDialogButtonBox) -> None:
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_mac:
|
2021-10-05 05:53:01 +02:00
|
|
|
b = bbox.button(QDialogButtonBox.StandardButton.Close)
|
2012-12-21 08:51:59 +01:00
|
|
|
if b:
|
|
|
|
bbox.removeButton(b)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addCloseShortcut(widg: QDialog) -> None:
|
2021-11-25 00:06:16 +01:00
|
|
|
if not is_mac:
|
2012-12-22 01:11:29 +01:00
|
|
|
return
|
2021-03-17 05:51:59 +01:00
|
|
|
shortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
|
|
|
qconnect(shortcut.activated, widg.reject)
|
|
|
|
setattr(widg, "_closeShortcut", shortcut)
|
2012-12-22 01:11:29 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-08-31 12:09:09 +02:00
|
|
|
def add_close_shortcut(widg: QWidget) -> None:
|
|
|
|
if not is_mac:
|
|
|
|
return
|
|
|
|
shortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
|
|
|
qconnect(shortcut.activated, widg.close)
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def downArrow() -> str:
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2016-05-12 06:45:35 +02:00
|
|
|
return "▼"
|
2015-09-28 15:09:30 +02:00
|
|
|
# windows 10 is lacking the smaller arrow on English installs
|
2016-05-12 06:45:35 +02:00
|
|
|
return "▾"
|
2015-09-28 15:09:30 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def current_window() -> QWidget | None:
|
2021-03-14 13:08:37 +01:00
|
|
|
if widget := QApplication.focusWidget():
|
2021-06-01 07:16:53 +02:00
|
|
|
return widget.window()
|
2021-03-14 13:08:37 +01:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2022-03-28 06:40:31 +02:00
|
|
|
def send_to_trash(path: Path) -> None:
|
2022-11-24 11:18:57 +01:00
|
|
|
"Place file/folder in recycling bin, or delete permanently on failure."
|
2022-03-28 06:40:31 +02:00
|
|
|
if not path.exists():
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
send2trash(path)
|
|
|
|
except Exception as exc:
|
|
|
|
# Linux users may not have a trash folder set up
|
|
|
|
print("trash failure:", path, exc)
|
|
|
|
if path.is_dir:
|
|
|
|
shutil.rmtree(path)
|
|
|
|
else:
|
|
|
|
path.unlink()
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Tooltips
|
|
|
|
######################################################################
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
_tooltipTimer: QTimer | None = None
|
|
|
|
_tooltipLabel: QLabel | None = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def tooltip(
|
|
|
|
msg: str,
|
|
|
|
period: int = 3000,
|
2021-10-03 10:59:42 +02:00
|
|
|
parent: QWidget | None = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
x_offset: int = 0,
|
|
|
|
y_offset: int = 100,
|
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global _tooltipTimer, _tooltipLabel
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class CustomLabel(QLabel):
|
2017-08-25 04:14:59 +02:00
|
|
|
silentlyClose = True
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def mousePressEvent(self, evt: QMouseEvent) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
evt.accept()
|
|
|
|
self.hide()
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
closeTooltip()
|
|
|
|
aw = parent or aqt.mw.app.activeWindow() or aqt.mw
|
2019-12-23 01:34:10 +01:00
|
|
|
lab = CustomLabel(
|
2021-02-11 01:09:06 +01:00
|
|
|
f"""<table cellpadding=10>
|
2012-12-21 08:51:59 +01:00
|
|
|
<tr>
|
2021-02-11 01:09:06 +01:00
|
|
|
<td>{msg}</td>
|
2012-12-21 08:51:59 +01:00
|
|
|
</tr>
|
2021-02-11 01:09:06 +01:00
|
|
|
</table>""",
|
2019-12-23 01:34:10 +01:00
|
|
|
aw,
|
|
|
|
)
|
2021-10-05 05:53:01 +02:00
|
|
|
lab.setFrameStyle(QFrame.Shape.Panel)
|
2012-12-21 08:51:59 +01:00
|
|
|
lab.setLineWidth(2)
|
2021-10-05 05:53:01 +02:00
|
|
|
lab.setWindowFlags(Qt.WindowType.ToolTip)
|
2020-01-23 22:55:14 +01:00
|
|
|
if not theme_manager.night_mode:
|
|
|
|
p = QPalette()
|
2021-10-05 05:53:01 +02:00
|
|
|
p.setColor(QPalette.ColorRole.Window, QColor("#feffc4"))
|
|
|
|
p.setColor(QPalette.ColorRole.WindowText, QColor("#000000"))
|
2020-01-23 22:55:14 +01:00
|
|
|
lab.setPalette(p)
|
2020-06-07 22:06:23 +02:00
|
|
|
lab.move(aw.mapToGlobal(QPoint(0 + x_offset, aw.height() - y_offset)))
|
2012-12-21 08:51:59 +01:00
|
|
|
lab.show()
|
|
|
|
_tooltipTimer = aqt.mw.progress.timer(
|
2022-02-18 10:00:12 +01:00
|
|
|
period, closeTooltip, False, requiresCollection=False, parent=aw
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
_tooltipLabel = lab
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def closeTooltip() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global _tooltipLabel, _tooltipTimer
|
|
|
|
if _tooltipLabel:
|
|
|
|
try:
|
|
|
|
_tooltipLabel.deleteLater()
|
2022-02-22 11:09:43 +01:00
|
|
|
except RuntimeError:
|
2012-12-21 08:51:59 +01:00
|
|
|
# already deleted as parent window closed
|
|
|
|
pass
|
|
|
|
_tooltipLabel = None
|
|
|
|
if _tooltipTimer:
|
2022-02-22 11:09:43 +01:00
|
|
|
try:
|
|
|
|
_tooltipTimer.deleteLater()
|
|
|
|
except RuntimeError:
|
|
|
|
pass
|
2012-12-21 08:51:59 +01:00
|
|
|
_tooltipTimer = None
|
2013-02-20 07:12:07 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2013-02-20 07:12:07 +01:00
|
|
|
# true if invalid; print warning
|
2021-02-01 11:23:48 +01:00
|
|
|
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
2021-10-25 06:50:13 +02:00
|
|
|
bad = invalid_filename(str, dirsep)
|
2013-02-20 07:12:07 +01:00
|
|
|
if bad:
|
2021-03-26 05:21:04 +01:00
|
|
|
showWarning(tr.qt_misc_the_following_character_can_not_be(val=bad))
|
2013-02-20 07:12:07 +01:00
|
|
|
return True
|
|
|
|
return False
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
# Menus
|
|
|
|
######################################################################
|
2021-02-05 06:26:12 +01:00
|
|
|
# This code will be removed in the future, please don't rely on it.
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
class MenuList:
|
2021-02-01 11:23:48 +01:00
|
|
|
def __init__(self) -> None:
|
2021-02-05 06:26:12 +01:00
|
|
|
traceback.print_stack(file=sys.stdout)
|
|
|
|
print(
|
|
|
|
"MenuList will be removed; please copy it into your add-on's code if you need it."
|
|
|
|
)
|
2021-10-03 10:59:42 +02:00
|
|
|
self.children: list[MenuListChild] = []
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addItem(self, title: str, func: Callable) -> MenuItem:
|
2017-08-15 10:41:36 +02:00
|
|
|
item = MenuItem(title, func)
|
|
|
|
self.children.append(item)
|
|
|
|
return item
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addSeparator(self) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
self.children.append(None)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addMenu(self, title: str) -> SubMenu:
|
2017-08-15 10:41:36 +02:00
|
|
|
submenu = SubMenu(title)
|
|
|
|
self.children.append(submenu)
|
|
|
|
return submenu
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def addChild(self, child: SubMenu | QAction | MenuList) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
self.children.append(child)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def renderTo(self, qmenu: QMenu) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
for child in self.children:
|
|
|
|
if child is None:
|
|
|
|
qmenu.addSeparator()
|
|
|
|
elif isinstance(child, QAction):
|
|
|
|
qmenu.addAction(child)
|
|
|
|
else:
|
|
|
|
child.renderTo(qmenu)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def popupOver(self, widget: QPushButton) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
qmenu = QMenu()
|
|
|
|
self.renderTo(qmenu)
|
2021-10-05 02:01:45 +02:00
|
|
|
qmenu.exec(widget.mapToGlobal(QPoint(0, 0)))
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
class SubMenu(MenuList):
|
2021-02-01 11:23:48 +01:00
|
|
|
def __init__(self, title: str) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
super().__init__()
|
|
|
|
self.title = title
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def renderTo(self, menu: QMenu) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
submenu = menu.addMenu(self.title)
|
|
|
|
super().renderTo(submenu)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
class MenuItem:
|
2021-02-01 11:23:48 +01:00
|
|
|
def __init__(self, title: str, func: Callable) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
self.title = title
|
|
|
|
self.func = func
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def renderTo(self, qmenu: QMenu) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
a = qmenu.addAction(self.title)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.func)
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def qtMenuShortcutWorkaround(qmenu: QMenu) -> None:
|
2019-02-05 05:37:07 +01:00
|
|
|
for act in qmenu.actions():
|
|
|
|
act.setShortcutVisibleInContextMenu(True)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-12-13 11:59:06 +01:00
|
|
|
######################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-03-09 05:21:54 +01:00
|
|
|
def disallow_full_screen() -> bool:
|
2023-09-16 03:28:20 +02:00
|
|
|
"""Test for OpenGl on Windows, which is known to cause issues with full screen mode."""
|
2022-03-09 05:21:54 +01:00
|
|
|
from aqt import mw
|
|
|
|
from aqt.profiles import VideoDriver
|
|
|
|
|
|
|
|
return is_win and (
|
2023-09-16 03:28:20 +02:00
|
|
|
mw.pm.video_driver() == VideoDriver.OpenGL
|
|
|
|
and not os.environ.get("ANKI_SOFTWAREOPENGL")
|
2022-03-09 05:21:54 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-12-07 23:40:48 +01:00
|
|
|
def add_ellipsis_to_action_label(*actions: QAction) -> None:
|
|
|
|
"""Pass actions to add '...' to their labels, indicating that more input is
|
|
|
|
required before they can be performed.
|
|
|
|
|
|
|
|
This approach is used so that the same fluent translations can be used on
|
|
|
|
mobile, where the '...' convention does not exist.
|
|
|
|
"""
|
|
|
|
for action in actions:
|
|
|
|
action.setText(tr.actions_with_ellipsis(action=action.text()))
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def supportText() -> str:
|
2019-02-24 14:50:39 +01:00
|
|
|
import platform
|
2020-08-31 04:05:36 +02:00
|
|
|
|
2019-02-24 14:50:39 +01:00
|
|
|
from aqt import mw
|
|
|
|
|
2022-08-02 11:49:02 +02:00
|
|
|
platname = platform.platform()
|
2019-02-24 14:50:39 +01:00
|
|
|
|
|
|
|
return """\
|
2023-11-29 01:25:32 +01:00
|
|
|
Anki {} {} {}
|
|
|
|
Python {} Qt {} PyQt {}
|
2019-02-24 14:50:39 +01:00
|
|
|
Platform: {}
|
2019-12-23 01:34:10 +01:00
|
|
|
""".format(
|
2021-10-25 06:50:13 +02:00
|
|
|
version_with_build(),
|
2023-11-29 01:25:32 +01:00
|
|
|
"(src)" if not getattr(sys, "frozen", False) else "",
|
|
|
|
"(ao)" if mw.addonManager.dirty else "",
|
2019-12-23 01:34:10 +01:00
|
|
|
platform.python_version(),
|
2022-09-21 04:21:10 +02:00
|
|
|
qVersion(),
|
2019-12-23 01:34:10 +01:00
|
|
|
PYQT_VERSION_STR,
|
|
|
|
platname,
|
|
|
|
)
|
|
|
|
|
2019-02-24 14:50:39 +01:00
|
|
|
|
2018-12-18 10:29:34 +01:00
|
|
|
######################################################################
|
|
|
|
|
2023-03-31 06:02:40 +02:00
|
|
|
|
2018-12-18 10:29:34 +01:00
|
|
|
# adapted from version detection in qutebrowser
|
2021-10-03 10:59:42 +02:00
|
|
|
def opengl_vendor() -> str | None:
|
2021-10-07 07:36:50 +02:00
|
|
|
if qtmajor != 5:
|
|
|
|
return "unknown"
|
2018-12-18 10:29:34 +01:00
|
|
|
old_context = QOpenGLContext.currentContext()
|
|
|
|
old_surface = None if old_context is None else old_context.surface()
|
|
|
|
|
|
|
|
surface = QOffscreenSurface()
|
|
|
|
surface.create()
|
|
|
|
|
|
|
|
ctx = QOpenGLContext()
|
|
|
|
ok = ctx.create()
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
|
|
|
|
ok = ctx.makeCurrent(surface)
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
if ctx.isOpenGLES():
|
|
|
|
# Can't use versionFunctions there
|
|
|
|
return None
|
|
|
|
|
2021-10-05 05:53:01 +02:00
|
|
|
vp = QOpenGLVersionProfile() # type: ignore # pylint: disable=undefined-variable
|
2018-12-18 10:29:34 +01:00
|
|
|
vp.setVersion(2, 0)
|
|
|
|
|
|
|
|
try:
|
2021-10-07 07:36:50 +02:00
|
|
|
vf = ctx.versionFunctions(vp) # type: ignore
|
2018-12-18 10:29:34 +01:00
|
|
|
except ImportError as e:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if vf is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return vf.glGetString(vf.GL_VENDOR)
|
|
|
|
finally:
|
|
|
|
ctx.doneCurrent()
|
|
|
|
if old_context and old_surface:
|
|
|
|
old_context.makeCurrent(old_surface)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def gfxDriverIsBroken() -> bool:
|
2018-12-18 10:29:34 +01:00
|
|
|
driver = opengl_vendor()
|
|
|
|
return driver == "nouveau"
|
2020-01-23 07:10:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
|
|
def startup_info() -> Any:
|
|
|
|
"Use subprocess.Popen(startupinfo=...) to avoid opening a console window."
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
if sys.platform != "win32":
|
2020-01-23 07:10:41 +01:00
|
|
|
return None
|
|
|
|
si = subprocess.STARTUPINFO() # pytype: disable=module-attr
|
|
|
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr
|
|
|
|
return si
|
2021-03-16 13:40:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
def ensure_editor_saved(func: Callable) -> Callable:
|
|
|
|
"""Ensure the current editor's note is saved before running the wrapped function.
|
|
|
|
|
|
|
|
Must be used on functions that may be invoked from a shortcut key while the
|
|
|
|
editor has focus. For functions that can't be activated while the editor has
|
|
|
|
focus, you don't need this.
|
|
|
|
|
|
|
|
Will look for the editor as self.editor.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
def decorated(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
|
|
self.editor.call_after_note_saved(lambda: func(self, *args, **kwargs))
|
|
|
|
|
|
|
|
return decorated
|
|
|
|
|
|
|
|
|
2021-04-25 08:35:21 +02:00
|
|
|
def skip_if_selection_is_empty(func: Callable) -> Callable:
|
|
|
|
"""Make the wrapped method a no-op and show a hint if the table selection is empty."""
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
def decorated(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
|
|
if self.table.len_selection() > 0:
|
|
|
|
func(self, *args, **kwargs)
|
|
|
|
else:
|
|
|
|
tooltip(tr.browsing_no_selection())
|
|
|
|
|
|
|
|
return decorated
|
|
|
|
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
def no_arg_trigger(func: Callable) -> Callable:
|
|
|
|
"""Tells Qt this function takes no args.
|
|
|
|
|
|
|
|
This ensures PyQt doesn't attempt to pass a `toggled` arg
|
|
|
|
into functions connected to a `triggered` signal.
|
|
|
|
"""
|
2021-04-25 19:50:56 +02:00
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
return pyqtSlot()(func) # type: ignore
|
2021-04-25 19:50:56 +02:00
|
|
|
|
|
|
|
|
2022-01-20 02:56:12 +01:00
|
|
|
def is_gesture_or_zoom_event(evt: QEvent) -> bool:
|
|
|
|
"""If the event is a gesture and/or will trigger zoom.
|
2022-01-18 10:12:57 +01:00
|
|
|
|
2022-01-20 02:56:12 +01:00
|
|
|
Includes zoom by pinching, and Ctrl-scrolling on Win and Linux.
|
2022-01-18 10:12:57 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
return isinstance(evt, QNativeGestureEvent) or (
|
|
|
|
isinstance(evt, QWheelEvent)
|
2022-01-20 02:56:12 +01:00
|
|
|
and not is_mac
|
|
|
|
and KeyboardModifiersPressed().control
|
2022-01-18 10:12:57 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
class KeyboardModifiersPressed:
|
|
|
|
"Util for type-safe checks of currently-pressed modifier keys."
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
from aqt import mw
|
|
|
|
|
2021-10-05 05:53:01 +02:00
|
|
|
self._modifiers = mw.app.keyboardModifiers()
|
2021-03-17 05:51:59 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def shift(self) -> bool:
|
2021-10-05 05:53:01 +02:00
|
|
|
return bool(self._modifiers & Qt.KeyboardModifier.ShiftModifier)
|
2021-03-17 05:51:59 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def control(self) -> bool:
|
2021-10-05 05:53:01 +02:00
|
|
|
return bool(self._modifiers & Qt.KeyboardModifier.ControlModifier)
|
2021-03-17 05:51:59 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def alt(self) -> bool:
|
2021-10-05 05:53:01 +02:00
|
|
|
return bool(self._modifiers & Qt.KeyboardModifier.AltModifier)
|
2021-12-04 05:35:47 +01:00
|
|
|
|
2022-01-18 10:12:57 +01:00
|
|
|
@property
|
|
|
|
def meta(self) -> bool:
|
|
|
|
return bool(self._modifiers & Qt.KeyboardModifier.MetaModifier)
|
|
|
|
|
2021-12-04 05:35:47 +01:00
|
|
|
|
|
|
|
# add-ons attempting to import isMac from this module :-(
|
|
|
|
_deprecated_names = DeprecatedNamesMixinForModule(globals())
|
|
|
|
|
|
|
|
|
2022-11-04 05:56:36 +01:00
|
|
|
if not TYPE_CHECKING:
|
|
|
|
|
|
|
|
def __getattr__(name: str) -> Any:
|
|
|
|
return _deprecated_names.__getattr__(name)
|