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
|
2021-03-17 05:51:59 +01:00
|
|
|
|
2020-02-12 22:00:13 +01:00
|
|
|
import dataclasses
|
2018-03-08 08:48:14 +01:00
|
|
|
import json
|
2020-04-15 21:21:47 +02:00
|
|
|
import re
|
2019-12-20 10:19:03 +01:00
|
|
|
import sys
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Any, Callable, Optional, Sequence, cast
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2020-07-21 03:52:25 +02:00
|
|
|
import anki
|
2022-02-13 04:40:47 +01:00
|
|
|
import anki.lang
|
2022-10-03 04:50:22 +02:00
|
|
|
from anki._legacy import deprecated
|
2020-11-18 02:53:33 +01:00
|
|
|
from anki.lang import is_rtl
|
2021-11-25 00:06:16 +01:00
|
|
|
from anki.utils import is_lin, is_mac, is_win
|
2021-02-05 09:50:01 +01:00
|
|
|
from aqt import colors, gui_hooks
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.qt import *
|
2020-01-23 06:08:10 +01:00
|
|
|
from aqt.theme import theme_manager
|
2022-01-20 02:56:12 +01:00
|
|
|
from aqt.utils import askUser, is_gesture_or_zoom_event, openLink, showInfo, tr
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-04-17 08:42:57 +02:00
|
|
|
serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Page for debug messages
|
|
|
|
##########################################################################
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
BridgeCommandHandler = Callable[[str], Any]
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-06-02 09:56:15 +02:00
|
|
|
class AnkiWebPage(QWebEnginePage):
|
2021-02-02 14:30:53 +01:00
|
|
|
def __init__(self, onBridgeCmd: BridgeCommandHandler) -> None:
|
2016-05-31 10:51:40 +02:00
|
|
|
QWebEnginePage.__init__(self)
|
2016-06-06 07:50:03 +02:00
|
|
|
self._onBridgeCmd = onBridgeCmd
|
|
|
|
self._setupBridge()
|
2020-06-29 05:49:35 +02:00
|
|
|
self.open_links_externally = True
|
2016-06-06 07:50:03 +02:00
|
|
|
|
2020-07-31 00:49:05 +02:00
|
|
|
def _setupBridge(self) -> None:
|
2016-06-06 07:50:03 +02:00
|
|
|
class Bridge(QObject):
|
2021-03-17 05:51:59 +01:00
|
|
|
def __init__(self, bridge_handler: Callable[[str], Any]) -> None:
|
|
|
|
super().__init__()
|
|
|
|
self.onCmd = bridge_handler
|
|
|
|
|
2020-07-31 00:49:05 +02:00
|
|
|
@pyqtSlot(str, result=str) # type: ignore
|
2021-02-02 14:30:53 +01:00
|
|
|
def cmd(self, str: str) -> Any:
|
2018-03-08 08:48:14 +01:00
|
|
|
return json.dumps(self.onCmd(str))
|
2016-06-06 07:50:03 +02:00
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
self._bridge = Bridge(self._onCmd)
|
2016-06-06 07:50:03 +02:00
|
|
|
|
|
|
|
self._channel = QWebChannel(self)
|
|
|
|
self._channel.registerObject("py", self._bridge)
|
|
|
|
self.setWebChannel(self._channel)
|
|
|
|
|
2020-05-10 02:58:42 +02:00
|
|
|
qwebchannel = ":/qtwebchannel/qwebchannel.js"
|
|
|
|
jsfile = QFile(qwebchannel)
|
2021-10-05 05:53:01 +02:00
|
|
|
if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly):
|
2020-05-10 02:58:42 +02:00
|
|
|
print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr)
|
2021-03-17 05:51:59 +01:00
|
|
|
jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8")
|
2020-05-10 02:58:42 +02:00
|
|
|
jsfile.close()
|
2016-06-06 07:50:03 +02:00
|
|
|
|
|
|
|
script = QWebEngineScript()
|
2019-12-23 01:34:10 +01:00
|
|
|
script.setSourceCode(
|
2020-05-10 02:58:42 +02:00
|
|
|
jstext
|
2019-12-23 01:34:10 +01:00
|
|
|
+ """
|
2020-08-27 13:46:34 +02:00
|
|
|
var pycmd, bridgeCommand;
|
2016-06-06 07:50:03 +02:00
|
|
|
new QWebChannel(qt.webChannelTransport, function(channel) {
|
2020-08-27 13:46:34 +02:00
|
|
|
bridgeCommand = pycmd = function (arg, cb) {
|
2018-03-08 08:48:14 +01:00
|
|
|
var resultCB = function (res) {
|
|
|
|
// pass result back to user-provided callback
|
|
|
|
if (cb) {
|
|
|
|
cb(JSON.parse(res));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-23 08:47:01 +02:00
|
|
|
channel.objects.py.cmd(arg, resultCB);
|
|
|
|
return false;
|
2018-03-08 08:48:14 +01:00
|
|
|
}
|
2016-06-06 07:50:03 +02:00
|
|
|
pycmd("domDone");
|
|
|
|
});
|
2019-12-23 01:34:10 +01:00
|
|
|
"""
|
|
|
|
)
|
2021-10-05 05:53:01 +02:00
|
|
|
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
|
|
|
|
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
2016-06-06 07:50:03 +02:00
|
|
|
script.setRunsOnSubFrames(False)
|
|
|
|
self.profile().scripts().insert(script)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def javaScriptConsoleMessage(
|
|
|
|
self,
|
|
|
|
level: QWebEnginePage.JavaScriptConsoleMessageLevel,
|
|
|
|
msg: str,
|
|
|
|
line: int,
|
|
|
|
srcID: str,
|
|
|
|
) -> None:
|
2017-08-06 05:10:51 +02:00
|
|
|
# not translated because console usually not visible,
|
|
|
|
# and may only accept ascii text
|
2020-04-17 02:49:48 +02:00
|
|
|
if srcID.startswith("data"):
|
|
|
|
srcID = ""
|
|
|
|
else:
|
2020-04-17 08:42:57 +02:00
|
|
|
srcID = serverbaseurl.sub("", srcID[:80], 1)
|
2021-10-05 05:53:01 +02:00
|
|
|
if level == QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel:
|
2021-02-02 14:30:53 +01:00
|
|
|
level_str = "info"
|
2021-10-05 05:53:01 +02:00
|
|
|
elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:
|
2021-02-02 14:30:53 +01:00
|
|
|
level_str = "warning"
|
2021-10-05 05:53:01 +02:00
|
|
|
elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel:
|
2021-02-02 14:30:53 +01:00
|
|
|
level_str = "error"
|
|
|
|
else:
|
|
|
|
level_str = str(level)
|
2020-04-15 21:21:47 +02:00
|
|
|
buf = "JS %(t)s %(f)s:%(a)d %(b)s" % dict(
|
2021-02-11 01:09:06 +01:00
|
|
|
t=level_str, a=line, f=srcID, b=f"{msg}\n"
|
2020-04-15 09:00:32 +02:00
|
|
|
)
|
2020-11-18 12:34:44 +01:00
|
|
|
if "MathJax localStorage" in buf:
|
|
|
|
# silence localStorage noise
|
|
|
|
return
|
2021-08-17 18:58:23 +02:00
|
|
|
elif "link preload" in buf:
|
|
|
|
# silence 'link preload' warning on the first card
|
|
|
|
return
|
2019-05-17 00:43:25 +02:00
|
|
|
# ensure we don't try to write characters the terminal can't handle
|
2020-04-15 21:04:57 +02:00
|
|
|
buf = buf.encode(sys.stdout.encoding, "backslashreplace").decode(
|
|
|
|
sys.stdout.encoding
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2020-04-15 21:04:57 +02:00
|
|
|
# output to stdout because it may raise error messages on the anki GUI
|
|
|
|
# https://github.com/ankitects/anki/pull/560
|
|
|
|
sys.stdout.write(buf)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def acceptNavigationRequest(
|
|
|
|
self, url: QUrl, navType: Any, isMainFrame: bool
|
|
|
|
) -> bool:
|
2021-10-07 14:23:00 +02:00
|
|
|
if (
|
|
|
|
not self.open_links_externally
|
|
|
|
or "_anki/pages" in url.path()
|
|
|
|
or url.path() == "/_anki/legacyPageData"
|
|
|
|
):
|
2020-06-29 05:49:35 +02:00
|
|
|
return super().acceptNavigationRequest(url, navType, isMainFrame)
|
|
|
|
|
2017-07-17 04:40:38 +02:00
|
|
|
if not isMainFrame:
|
|
|
|
return True
|
2018-10-23 08:11:08 +02:00
|
|
|
# data: links generated by setHtml()
|
|
|
|
if url.scheme() == "data":
|
|
|
|
return True
|
2018-10-23 08:47:01 +02:00
|
|
|
# catch buggy <a href='#' onclick='func()'> links
|
2017-08-11 12:59:15 +02:00
|
|
|
from aqt import mw
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-10-05 05:53:01 +02:00
|
|
|
if url.matches(
|
|
|
|
QUrl(mw.serverURL()), cast(Any, QUrl.UrlFormattingOption.RemoveFragment)
|
|
|
|
):
|
2019-04-10 04:44:01 +02:00
|
|
|
print("onclick handler needs to return false")
|
2017-08-11 12:59:15 +02:00
|
|
|
return False
|
2016-06-06 07:50:03 +02:00
|
|
|
# load all other links in browser
|
|
|
|
openLink(url)
|
|
|
|
return False
|
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
def _onCmd(self, str: str) -> Any:
|
2018-03-08 08:48:14 +01:00
|
|
|
return self._onBridgeCmd(str)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-07-24 14:04:06 +02:00
|
|
|
def javaScriptAlert(self, frame: Any, text: str) -> None:
|
2020-08-06 01:50:12 +02:00
|
|
|
showInfo(text)
|
|
|
|
|
2021-07-24 14:04:06 +02:00
|
|
|
def javaScriptConfirm(self, frame: Any, text: str) -> bool:
|
|
|
|
return askUser(text)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-02-12 22:00:13 +01:00
|
|
|
# Add-ons
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class WebContent:
|
2020-02-15 15:03:58 +01:00
|
|
|
"""Stores all dynamically modified content that a particular web view
|
|
|
|
will be populated with.
|
2020-02-12 22:00:13 +01:00
|
|
|
|
|
|
|
Attributes:
|
|
|
|
body {str} -- HTML body
|
|
|
|
head {str} -- HTML head
|
2020-02-15 15:03:58 +01:00
|
|
|
css {List[str]} -- List of media server subpaths,
|
|
|
|
each pointing to a CSS file
|
|
|
|
js {List[str]} -- List of media server subpaths,
|
|
|
|
each pointing to a JS file
|
2020-02-12 22:00:13 +01:00
|
|
|
|
|
|
|
Important Notes:
|
2020-02-15 15:03:58 +01:00
|
|
|
- When modifying the attributes specified above, please make sure your
|
2022-11-24 11:18:57 +01:00
|
|
|
changes only perform the minimum required edits to make your add-on work.
|
2020-02-15 15:03:58 +01:00
|
|
|
You should avoid overwriting or interfering with existing data as much
|
|
|
|
as possible, instead opting to append your own changes, e.g.:
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_webview_will_set_content(web_content: WebContent, context) -> None:
|
2020-02-12 22:00:13 +01:00
|
|
|
web_content.body += "<my_html>"
|
|
|
|
web_content.head += "<my_head>"
|
|
|
|
|
2020-02-15 15:03:58 +01:00
|
|
|
- The paths specified in `css` and `js` need to be accessible by Anki's
|
|
|
|
media server. All list members without a specified subpath are assumed
|
|
|
|
to be located under `/_anki`, which is the media server subpath used
|
|
|
|
for all web assets shipped with Anki.
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-02-15 15:03:58 +01:00
|
|
|
Add-ons may expose their own web assets by utilizing
|
|
|
|
aqt.addons.AddonManager.setWebExports(). Web exports registered
|
|
|
|
in this manner may then be accessed under the `/_addons` subpath.
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-02-15 15:03:58 +01:00
|
|
|
E.g., to allow access to a `my-addon.js` and `my-addon.css` residing
|
|
|
|
in a "web" subfolder in your add-on package, first register the
|
|
|
|
corresponding web export:
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-02-12 22:00:13 +01:00
|
|
|
> from aqt import mw
|
|
|
|
> mw.addonManager.setWebExports(__name__, r"web/.*(css|js)")
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-02-15 15:03:58 +01:00
|
|
|
Then append the subpaths to the corresponding web_content fields
|
|
|
|
within a function subscribing to gui_hooks.webview_will_set_content:
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_webview_will_set_content(web_content: WebContent, context) -> None:
|
2020-02-12 22:00:13 +01:00
|
|
|
addon_package = mw.addonManager.addonFromModule(__name__)
|
|
|
|
web_content.css.append(
|
2020-02-15 15:03:58 +01:00
|
|
|
f"/_addons/{addon_package}/web/my-addon.css")
|
2020-02-12 22:00:13 +01:00
|
|
|
web_content.js.append(
|
2020-02-15 15:03:58 +01:00
|
|
|
f"/_addons/{addon_package}/web/my-addon.js")
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-02-29 04:36:10 +01:00
|
|
|
Note that '/' will also match the os specific path separator.
|
2020-02-12 22:00:13 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
body: str = ""
|
|
|
|
head: str = ""
|
2021-10-03 10:59:42 +02:00
|
|
|
css: list[str] = dataclasses.field(default_factory=lambda: [])
|
|
|
|
js: list[str] = dataclasses.field(default_factory=lambda: [])
|
2020-02-12 22:00:13 +01:00
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Main web view
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
|
2020-06-02 09:56:15 +02:00
|
|
|
class AnkiWebView(QWebEngineView):
|
2022-06-10 15:33:53 +02:00
|
|
|
allow_drops = False
|
|
|
|
|
2020-02-12 21:03:11 +01:00
|
|
|
def __init__(
|
2021-07-03 01:54:10 +02:00
|
|
|
self,
|
|
|
|
parent: Optional[QWidget] = None,
|
|
|
|
title: str = "default",
|
2020-02-12 21:03:11 +01:00
|
|
|
) -> None:
|
2020-06-02 09:56:15 +02:00
|
|
|
QWebEngineView.__init__(self, parent=parent)
|
2021-03-26 07:06:02 +01:00
|
|
|
self.set_title(title)
|
2016-06-06 07:50:03 +02:00
|
|
|
self._page = AnkiWebPage(self._onBridgeCmd)
|
2021-06-19 20:04:24 +02:00
|
|
|
# reduce flicker
|
Introduce new color palette using Sass maps (#2016)
* Remove --medium-border variable
* Implement color palette using Sass maps
I hand-picked the gray tones, the other colors are from the Tailwind CSS v3 palette.
Significant changes:
- light theme is brighter
- dark theme is darker
- borders are softer
I also deleted some platform- and night-mode-specific code.
* Use custom colors for note view switch
* Use same placeholder color for all inputs
* Skew color palette for more dark values
by removing gray[3], which wasn't used anywhere. Slight adjustments were made to the darker tones.
* Adjust frame- window- and border colors
* Give deck browser entries --frame-bg as background color
* Define styling for QComboBox and QLineEdit globally
* Experiment with CSS filter for inline-colors
Inside darker inputs, some colors like dark blue will be hard to read, so we could try to improve text-color contrast with global adjustments depending on the theme.
* Use different map structure for _vars.scss
after @hgiesel's idea: https://github.com/ankitects/anki/pull/2016#discussion_r947087871
* Move custom QLineEdit styles out of searchbar.py
* Merge branch 'main' into color-palette
* Revert QComboBox stylesheet override
* Align gray color palette more with macOS
* Adjust light theme
* Use --slightly-grey-text for options tab color
* Replace gray tones with more neutral values
* Improve categorization of global colors
by renaming almost all of them and sorting them into separate maps.
* Saturate highlight-bg in light theme
* Tweak gray tones
* Adjust box-shadow of EditingArea to make fields look inset
* Add Sass functions to access color palette and semantic variables
in response to https://github.com/ankitects/anki/pull/2016#issuecomment-1220571076
* Showcase use of access functions in several locations
@hgiesel in buttons.scss I access the color palette directly. Is this what you meant by "... keep it local to the component, and possibly make it global at a later time ..."?
* Fix focus box shadow transition and remove default shadow for a cleaner look
I couldn't quite get the inset look the way I wanted, because inset box-shadows do not respect the border radius, therefore causing aliasing.
* Tweak light theme border and shadow colors
* Add functions and colors to base_lib
* Add vars_lib as dependency to base_lib and button_mixins_lib
* Improve uses of default-themed variables
* Use old --frame-bg color and use darker tone for canvas-default
* Return CSS var by default and add palette-of function for raw value
* Showcase use of palette-of function
The #{...} syntax is required only because the use cases are CSS var definitions. In other cases a simple palette-of(keyword, theme) would suffice.
* Light theme: decrease brightness of canvas-default and adjust fg-default
* Use canvas-inset variable for switch knob
* Adjust light theme
* Add back box-shadow to EditingArea
* Light theme: darken background and flatten transition
also set hue and saturation of gray-8 to 0 (like all the other grays).
* Reduce flag colors to single default value
* Tweak card/note accent colors
* Experiment with inset look for fields again
Is this too dark in night mode? It's the same color used for all other text inputs.
* Dark theme: make border-default one shade darker
* Tweak inset shadow color
* Dark theme: make border-faint darker than canvas-default
meaning two shades darker than it currently was.
* Fix PlainTextInput not expanding
* Dark theme: use less saturated flag colors
* Adjust gray tones
* Fix nested variables not getting extracted correctly
* Rename canvas-outset to canvas-elevated
* Light theme: darken canvas-default
* Make canvas-elevated a bit darker
* Rename variables and use them in various components
* Refactor button mixins
* Remove fusion vars from Anki
* Adjust button gradients
* Refactor button mixins
* Fix deck browser table td background color
* Use color function in buttons.scss
* Rework QTabWidget stylesheet
* Fix crash on browser open
* Perfect QTableView header
* Fix bottom toolbar button gradient
* Fix focus outline of bottom toolbar buttons
* Fix custom webview scrollbar
* Fix uses of vars in various webviews
The command @use vars as * lead to repeated inclusion of the CSS vars.
* Enable primary button color with mixin
* Run prettier
* Fix Python code style issues
* Tweak colors
* Lighten scrollbar shades in light theme
* Fix code style issues caused by merge
* Fix harsh border color in editor
caused by leftover --medium-border variables, probably introduced with a merge commit.
* Compile Sass before extracting Python colors/props
This means the Python side doesn't need to worry about the map structure and Sass functions, just copy the output CSS values.
* Desaturate primary button colors by 10%
* Convert accidentally capitalized variable names to lowercase
* Simplify color definitions with qcolor function
* Remove default border-focus variable
* Remove redundant colon
* Apply custom scrollbar CSS only on Windows and Linux
* Make border-subtle color brighter than background in dark theme
* Make border-subtle color a shade brighter in light theme
* Use border-subtle for NoteEditor and EditorToolbar border
* Small patches
2022-09-16 06:11:18 +02:00
|
|
|
self._page.setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
|
2016-06-06 07:50:03 +02:00
|
|
|
|
2020-01-22 01:46:35 +01:00
|
|
|
# in new code, use .set_bridge_command() instead of setting this directly
|
|
|
|
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
|
|
|
|
2017-08-01 06:30:04 +02:00
|
|
|
self._domDone = True
|
2021-10-03 10:59:42 +02:00
|
|
|
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
|
2018-10-12 04:08:19 +02:00
|
|
|
self.requiresCol = True
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setPage(self._page)
|
2022-01-18 10:12:57 +01:00
|
|
|
self._disable_zoom = False
|
2017-01-08 11:02:49 +01:00
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
self.resetHandlers()
|
2018-10-23 08:06:56 +02:00
|
|
|
self._filterSet = False
|
2020-01-22 07:54:18 +01:00
|
|
|
QShortcut( # type: ignore
|
2019-12-23 01:34:10 +01:00
|
|
|
QKeySequence("Esc"),
|
|
|
|
self,
|
2021-10-05 05:53:01 +02:00
|
|
|
context=Qt.ShortcutContext.WidgetWithChildrenShortcut,
|
2019-12-23 01:34:10 +01:00
|
|
|
activated=self.onEsc,
|
2020-01-22 07:54:18 +01:00
|
|
|
)
|
2021-11-24 22:17:41 +01:00
|
|
|
gui_hooks.theme_did_change.append(self.on_theme_did_change)
|
2017-06-22 08:36:54 +02:00
|
|
|
|
2021-03-26 07:06:02 +01:00
|
|
|
def set_title(self, title: str) -> None:
|
|
|
|
self.title = title # type: ignore[assignment]
|
|
|
|
|
2022-01-18 10:12:57 +01:00
|
|
|
def disable_zoom(self) -> None:
|
|
|
|
self._disable_zoom = True
|
|
|
|
|
2022-01-24 03:23:01 +01:00
|
|
|
def createWindow(self, windowType: QWebEnginePage.WebWindowType) -> QWebEngineView:
|
|
|
|
# intercept opening a new window (hrefs
|
|
|
|
# with target="_blank") and return view
|
|
|
|
return AnkiWebView()
|
|
|
|
|
2020-05-26 10:08:55 +02:00
|
|
|
def eventFilter(self, obj: QObject, evt: QEvent) -> bool:
|
2022-01-20 02:56:12 +01:00
|
|
|
if self._disable_zoom and is_gesture_or_zoom_event(evt):
|
2017-06-23 06:34:56 +02:00
|
|
|
return True
|
2022-01-18 10:12:57 +01:00
|
|
|
|
|
|
|
if (
|
2021-10-05 05:53:01 +02:00
|
|
|
isinstance(evt, QMouseEvent)
|
|
|
|
and evt.type() == QEvent.Type.MouseButtonRelease
|
|
|
|
):
|
2021-11-25 00:06:16 +01:00
|
|
|
if evt.button() == Qt.MouseButton.MiddleButton and is_lin:
|
2018-03-02 02:16:02 +01:00
|
|
|
self.onMiddleClickPaste()
|
|
|
|
return True
|
2022-01-18 10:12:57 +01:00
|
|
|
|
2017-06-23 06:34:56 +02:00
|
|
|
return False
|
|
|
|
|
2020-06-29 05:49:35 +02:00
|
|
|
def set_open_links_externally(self, enable: bool) -> None:
|
|
|
|
self._page.open_links_externally = enable
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onEsc(self) -> None:
|
2017-06-22 08:36:54 +02:00
|
|
|
w = self.parent()
|
|
|
|
while w:
|
|
|
|
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
|
|
|
|
from aqt import mw
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-06-22 08:36:54 +02:00
|
|
|
# esc in a child window closes the window
|
|
|
|
if w != mw:
|
|
|
|
w.close()
|
|
|
|
else:
|
|
|
|
# in the main window, removes focus from type in area
|
2022-10-03 04:53:09 +02:00
|
|
|
parent = self.parent()
|
|
|
|
assert isinstance(parent, QWidget)
|
|
|
|
parent.setFocus()
|
2017-06-22 08:36:54 +02:00
|
|
|
break
|
|
|
|
w = w.parent()
|
2016-06-06 09:54:39 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onCopy(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.WebAction.Copy)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onCut(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.WebAction.Cut)
|
2016-06-06 09:54:39 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onPaste(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.WebAction.Paste)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onMiddleClickPaste(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.WebAction.Paste)
|
2018-03-02 02:16:02 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onSelectAll(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.WebAction.SelectAll)
|
2017-06-22 08:36:54 +02:00
|
|
|
|
2020-05-26 10:12:39 +02:00
|
|
|
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
m = QMenu(self)
|
2021-03-26 04:48:26 +01:00
|
|
|
a = m.addAction(tr.actions_copy())
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.onCopy)
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.webview_will_show_context_menu(self, m)
|
2012-12-21 08:51:59 +01:00
|
|
|
m.popup(QCursor.pos())
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def dropEvent(self, evt: QDropEvent) -> None:
|
2022-06-10 15:33:53 +02:00
|
|
|
if self.allow_drops:
|
|
|
|
super().dropEvent(evt)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-06-02 09:56:15 +02:00
|
|
|
def setHtml(self, html: str) -> None: # type: ignore
|
2019-04-29 08:41:47 +02:00
|
|
|
# discard any previous pending actions
|
|
|
|
self._pendingActions = []
|
|
|
|
self._domDone = True
|
2017-08-07 08:01:35 +02:00
|
|
|
self._queueAction("setHtml", html)
|
2020-09-27 06:36:55 +02:00
|
|
|
self.set_open_links_externally(True)
|
2022-06-10 15:33:53 +02:00
|
|
|
self.allow_drops = False
|
2021-04-03 08:59:23 +02:00
|
|
|
self.show()
|
2017-08-07 08:01:35 +02:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _setHtml(self, html: str) -> None:
|
2021-10-07 14:23:00 +02:00
|
|
|
"""Send page data to media server, then surf to it.
|
|
|
|
|
|
|
|
This function used to be implemented by QWebEngine's
|
|
|
|
.setHtml() call. It is no longer used, as it has a
|
|
|
|
maximum size limit, and due to security changes, it
|
|
|
|
will stop working in the future."""
|
2021-03-17 05:51:59 +01:00
|
|
|
from aqt import mw
|
|
|
|
|
|
|
|
oldFocus = mw.app.focusWidget()
|
2017-08-01 06:30:04 +02:00
|
|
|
self._domDone = False
|
2021-10-07 14:23:00 +02:00
|
|
|
|
|
|
|
webview_id = id(self)
|
|
|
|
mw.mediaServer.set_page_html(webview_id, html)
|
|
|
|
self.load_url(QUrl(f"{mw.serverURL()}_anki/legacyPageData?id={webview_id}"))
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
# work around webengine stealing focus on setHtml()
|
2021-10-07 14:23:00 +02:00
|
|
|
# fixme: check which if any qt versions this is still required on
|
2016-05-31 10:51:40 +02:00
|
|
|
if oldFocus:
|
|
|
|
oldFocus.setFocus()
|
|
|
|
|
2021-03-17 05:51:59 +01:00
|
|
|
def load_url(self, url: QUrl) -> None:
|
2020-07-16 19:56:57 +02:00
|
|
|
# allow queuing actions when loading url directly
|
|
|
|
self._domDone = False
|
2022-06-10 15:33:53 +02:00
|
|
|
self.allow_drops = False
|
2020-07-16 19:56:57 +02:00
|
|
|
super().load(url)
|
|
|
|
|
2022-02-17 07:31:46 +01:00
|
|
|
def app_zoom_factor(self) -> float:
|
2017-11-27 02:01:15 +01:00
|
|
|
# overridden scale factor?
|
|
|
|
webscale = os.environ.get("ANKI_WEBSCALE")
|
|
|
|
if webscale:
|
|
|
|
return float(webscale)
|
|
|
|
|
2021-11-25 00:06:16 +01:00
|
|
|
if qtmajor > 5 or is_mac:
|
2017-08-10 07:02:46 +02:00
|
|
|
return 1
|
2021-10-07 07:36:50 +02:00
|
|
|
screen = QApplication.desktop().screen() # type: ignore
|
2020-06-02 01:48:51 +02:00
|
|
|
if screen is None:
|
|
|
|
return 1
|
|
|
|
|
2016-06-07 06:27:33 +02:00
|
|
|
dpi = screen.logicalDpiX()
|
2017-08-15 06:47:03 +02:00
|
|
|
factor = dpi / 96.0
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_lin:
|
2017-08-15 06:47:03 +02:00
|
|
|
factor = max(1, factor)
|
|
|
|
return factor
|
2021-10-05 03:06:34 +02:00
|
|
|
return 1
|
2017-10-05 08:42:46 +02:00
|
|
|
|
2021-12-07 23:08:56 +01:00
|
|
|
def setPlaybackRequiresGesture(self, value: bool) -> None:
|
|
|
|
self.settings().setAttribute(
|
|
|
|
QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture, value
|
|
|
|
)
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def _getQtIntScale(self, screen: QWidget) -> int:
|
2017-10-05 08:42:46 +02:00
|
|
|
# try to detect if Qt has scaled the screen
|
|
|
|
# - qt will round the scale factor to a whole number, so a dpi of 125% = 1x,
|
|
|
|
# and a dpi of 150% = 2x
|
|
|
|
# - a screen with a normal physical dpi of 72 will have a dpi of 32
|
|
|
|
# if the scale factor has been rounded to 2x
|
|
|
|
# - different screens have different physical DPIs (eg 72, 93, 102)
|
|
|
|
# - until a better solution presents itself, assume a physical DPI at
|
|
|
|
# or above 70 is unscaled
|
|
|
|
if screen.physicalDpiX() > 70:
|
|
|
|
return 1
|
|
|
|
elif screen.physicalDpiX() > 35:
|
|
|
|
return 2
|
|
|
|
else:
|
|
|
|
return 3
|
2016-06-07 06:27:33 +02:00
|
|
|
|
2020-08-27 07:57:24 +02:00
|
|
|
def standard_css(self) -> str:
|
2021-06-19 20:04:24 +02:00
|
|
|
palette = theme_manager.default_palette
|
2021-10-05 05:53:01 +02:00
|
|
|
color_hl = palette.color(QPalette.ColorRole.Highlight).name()
|
2019-04-10 07:16:06 +02:00
|
|
|
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2019-12-23 01:34:10 +01:00
|
|
|
# T: include a font for your language on Windows, eg: "Segoe UI", "MS Mincho"
|
2021-03-26 04:48:26 +01:00
|
|
|
family = tr.qt_misc_segoe_ui()
|
2022-02-07 10:41:19 +01:00
|
|
|
button_style = f"""
|
|
|
|
button {{ font-family: {family}; }}
|
2022-10-29 02:48:53 +02:00
|
|
|
"""
|
2022-11-23 07:50:15 +01:00
|
|
|
font = f"font-family:{family};"
|
2021-11-25 00:06:16 +01:00
|
|
|
elif is_mac:
|
2019-12-23 01:34:10 +01:00
|
|
|
family = "Helvetica"
|
2022-11-23 07:50:15 +01:00
|
|
|
font = f'font-family:"{family}";'
|
2022-10-29 02:48:53 +02:00
|
|
|
button_style = """
|
|
|
|
button {
|
|
|
|
--canvas: #fff;
|
|
|
|
-webkit-appearance: none;
|
|
|
|
background: var(--canvas);
|
|
|
|
border-radius: var(--border-radius);
|
|
|
|
padding: 3px 12px;
|
|
|
|
border: 0.5px solid var(--border);
|
|
|
|
box-shadow: 0px 1px 3px var(--border-subtle);
|
|
|
|
font-family: Helvetica
|
|
|
|
}
|
|
|
|
.night-mode button { --canvas: #606060; --fg: #eee; }
|
|
|
|
"""
|
2016-07-08 08:17:06 +02:00
|
|
|
else:
|
|
|
|
family = self.font().family()
|
2022-11-23 07:50:15 +01:00
|
|
|
font = f'font-family:"{family}", sans-serif;'
|
2020-08-27 07:57:24 +02:00
|
|
|
button_style = """
|
2018-08-30 22:07:06 +02:00
|
|
|
/* Buttons */
|
2021-10-03 10:59:42 +02:00
|
|
|
button{{
|
2022-10-29 02:48:53 +02:00
|
|
|
font-family: "{family}", sans-serif;
|
|
|
|
}}
|
2018-08-30 22:07:06 +02:00
|
|
|
/* Input field focus outline */
|
|
|
|
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
|
2021-10-03 10:59:42 +02:00
|
|
|
div[contenteditable="true"]:focus {{
|
2018-08-30 22:07:06 +02:00
|
|
|
outline: 0 none;
|
2021-10-03 10:59:42 +02:00
|
|
|
border-color: {color_hl};
|
|
|
|
}}""".format(
|
|
|
|
family=family,
|
|
|
|
color_hl=color_hl,
|
|
|
|
)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-02-17 07:31:46 +01:00
|
|
|
zoom = self.app_zoom_factor()
|
2021-06-19 20:04:24 +02:00
|
|
|
|
2020-08-27 07:57:24 +02:00
|
|
|
return f"""
|
Introduce new color palette using Sass maps (#2016)
* Remove --medium-border variable
* Implement color palette using Sass maps
I hand-picked the gray tones, the other colors are from the Tailwind CSS v3 palette.
Significant changes:
- light theme is brighter
- dark theme is darker
- borders are softer
I also deleted some platform- and night-mode-specific code.
* Use custom colors for note view switch
* Use same placeholder color for all inputs
* Skew color palette for more dark values
by removing gray[3], which wasn't used anywhere. Slight adjustments were made to the darker tones.
* Adjust frame- window- and border colors
* Give deck browser entries --frame-bg as background color
* Define styling for QComboBox and QLineEdit globally
* Experiment with CSS filter for inline-colors
Inside darker inputs, some colors like dark blue will be hard to read, so we could try to improve text-color contrast with global adjustments depending on the theme.
* Use different map structure for _vars.scss
after @hgiesel's idea: https://github.com/ankitects/anki/pull/2016#discussion_r947087871
* Move custom QLineEdit styles out of searchbar.py
* Merge branch 'main' into color-palette
* Revert QComboBox stylesheet override
* Align gray color palette more with macOS
* Adjust light theme
* Use --slightly-grey-text for options tab color
* Replace gray tones with more neutral values
* Improve categorization of global colors
by renaming almost all of them and sorting them into separate maps.
* Saturate highlight-bg in light theme
* Tweak gray tones
* Adjust box-shadow of EditingArea to make fields look inset
* Add Sass functions to access color palette and semantic variables
in response to https://github.com/ankitects/anki/pull/2016#issuecomment-1220571076
* Showcase use of access functions in several locations
@hgiesel in buttons.scss I access the color palette directly. Is this what you meant by "... keep it local to the component, and possibly make it global at a later time ..."?
* Fix focus box shadow transition and remove default shadow for a cleaner look
I couldn't quite get the inset look the way I wanted, because inset box-shadows do not respect the border radius, therefore causing aliasing.
* Tweak light theme border and shadow colors
* Add functions and colors to base_lib
* Add vars_lib as dependency to base_lib and button_mixins_lib
* Improve uses of default-themed variables
* Use old --frame-bg color and use darker tone for canvas-default
* Return CSS var by default and add palette-of function for raw value
* Showcase use of palette-of function
The #{...} syntax is required only because the use cases are CSS var definitions. In other cases a simple palette-of(keyword, theme) would suffice.
* Light theme: decrease brightness of canvas-default and adjust fg-default
* Use canvas-inset variable for switch knob
* Adjust light theme
* Add back box-shadow to EditingArea
* Light theme: darken background and flatten transition
also set hue and saturation of gray-8 to 0 (like all the other grays).
* Reduce flag colors to single default value
* Tweak card/note accent colors
* Experiment with inset look for fields again
Is this too dark in night mode? It's the same color used for all other text inputs.
* Dark theme: make border-default one shade darker
* Tweak inset shadow color
* Dark theme: make border-faint darker than canvas-default
meaning two shades darker than it currently was.
* Fix PlainTextInput not expanding
* Dark theme: use less saturated flag colors
* Adjust gray tones
* Fix nested variables not getting extracted correctly
* Rename canvas-outset to canvas-elevated
* Light theme: darken canvas-default
* Make canvas-elevated a bit darker
* Rename variables and use them in various components
* Refactor button mixins
* Remove fusion vars from Anki
* Adjust button gradients
* Refactor button mixins
* Fix deck browser table td background color
* Use color function in buttons.scss
* Rework QTabWidget stylesheet
* Fix crash on browser open
* Perfect QTableView header
* Fix bottom toolbar button gradient
* Fix focus outline of bottom toolbar buttons
* Fix custom webview scrollbar
* Fix uses of vars in various webviews
The command @use vars as * lead to repeated inclusion of the CSS vars.
* Enable primary button color with mixin
* Run prettier
* Fix Python code style issues
* Tweak colors
* Lighten scrollbar shades in light theme
* Fix code style issues caused by merge
* Fix harsh border color in editor
caused by leftover --medium-border variables, probably introduced with a merge commit.
* Compile Sass before extracting Python colors/props
This means the Python side doesn't need to worry about the map structure and Sass functions, just copy the output CSS values.
* Desaturate primary button colors by 10%
* Convert accidentally capitalized variable names to lowercase
* Simplify color definitions with qcolor function
* Remove default border-focus variable
* Remove redundant colon
* Apply custom scrollbar CSS only on Windows and Linux
* Make border-subtle color brighter than background in dark theme
* Make border-subtle color a shade brighter in light theme
* Use border-subtle for NoteEditor and EditorToolbar border
* Small patches
2022-09-16 06:11:18 +02:00
|
|
|
body {{ zoom: {zoom}; background-color: var(--canvas); }}
|
2021-05-19 11:06:20 +02:00
|
|
|
html {{ {font} }}
|
2020-08-27 07:57:24 +02:00
|
|
|
{button_style}
|
2022-10-29 02:48:53 +02:00
|
|
|
:root {{ --canvas: {colors.CANVAS["light"]} }}
|
|
|
|
:root[class*=night-mode] {{ --canvas: {colors.CANVAS["dark"]} }}
|
2020-08-27 07:57:24 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
def stdHtml(
|
|
|
|
self,
|
|
|
|
body: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
css: Optional[list[str]] = None,
|
|
|
|
js: Optional[list[str]] = None,
|
2020-08-27 07:57:24 +02:00
|
|
|
head: str = "",
|
|
|
|
context: Optional[Any] = None,
|
2021-04-14 03:28:40 +02:00
|
|
|
default_css: bool = True,
|
2021-02-02 14:30:53 +01:00
|
|
|
) -> None:
|
2021-10-02 14:47:13 +02:00
|
|
|
css = (["css/webview.css"] if default_css else []) + (
|
|
|
|
[] if css is None else css
|
|
|
|
)
|
2020-08-27 07:57:24 +02:00
|
|
|
web_content = WebContent(
|
|
|
|
body=body,
|
|
|
|
head=head,
|
2020-12-28 14:56:41 +01:00
|
|
|
js=["js/webview.js"] + (["js/vendor/jquery.min.js"] if js is None else js),
|
2021-10-02 14:47:13 +02:00
|
|
|
css=css,
|
2020-08-27 07:57:24 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
gui_hooks.webview_will_set_content(web_content, context)
|
|
|
|
|
|
|
|
csstxt = ""
|
2021-10-02 14:47:13 +02:00
|
|
|
if "css/webview.css" in css:
|
2020-08-27 07:57:24 +02:00
|
|
|
# we want our dynamic styling to override the defaults in
|
2020-11-01 05:26:58 +01:00
|
|
|
# css/webview.css, but come before user-provided stylesheets so that
|
2020-08-27 07:57:24 +02:00
|
|
|
# they can override us if necessary
|
2020-11-01 05:26:58 +01:00
|
|
|
web_content.css.remove("css/webview.css")
|
|
|
|
csstxt = self.bundledCSS("css/webview.css")
|
2020-08-27 07:57:24 +02:00
|
|
|
csstxt += f"<style>{self.standard_css()}</style>"
|
|
|
|
|
|
|
|
csstxt += "\n".join(self.bundledCSS(fname) for fname in web_content.css)
|
2020-02-12 22:00:13 +01:00
|
|
|
jstxt = "\n".join(self.bundledScript(fname) for fname in web_content.js)
|
|
|
|
|
2017-08-11 12:59:15 +02:00
|
|
|
from aqt import mw
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-04-19 09:16:56 +02:00
|
|
|
head = mw.baseHTML() + csstxt + web_content.head
|
2020-01-23 06:08:10 +01:00
|
|
|
body_class = theme_manager.body_class()
|
|
|
|
|
2020-08-27 07:57:24 +02:00
|
|
|
if theme_manager.night_mode:
|
|
|
|
doc_class = "night-mode"
|
2020-07-21 03:52:25 +02:00
|
|
|
else:
|
2020-08-27 07:57:24 +02:00
|
|
|
doc_class = ""
|
2020-07-21 03:52:25 +02:00
|
|
|
|
2021-12-06 09:40:26 +01:00
|
|
|
if is_rtl(anki.lang.current_lang):
|
|
|
|
lang_dir = "rtl"
|
|
|
|
else:
|
|
|
|
lang_dir = "ltr"
|
|
|
|
|
2020-08-27 07:57:24 +02:00
|
|
|
html = f"""
|
2017-08-10 11:02:32 +02:00
|
|
|
<!doctype html>
|
2021-12-06 09:40:26 +01:00
|
|
|
<html class="{doc_class}" dir="{lang_dir}">
|
2020-08-27 07:57:24 +02:00
|
|
|
<head>
|
|
|
|
<title>{self.title}</title>
|
|
|
|
{head}
|
2012-12-21 08:51:59 +01:00
|
|
|
</head>
|
2017-08-10 11:02:32 +02:00
|
|
|
|
2022-04-19 09:16:56 +02:00
|
|
|
<body class="{body_class}">
|
|
|
|
{jstxt}
|
|
|
|
{web_content.body}</body>
|
2020-08-27 07:57:24 +02:00
|
|
|
</html>"""
|
2019-12-23 01:34:10 +01:00
|
|
|
# print(html)
|
2017-06-22 10:01:01 +02:00
|
|
|
self.setHtml(html)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-04-15 09:00:32 +02:00
|
|
|
@classmethod
|
|
|
|
def webBundlePath(cls, path: str) -> str:
|
2017-07-28 08:19:06 +02:00
|
|
|
from aqt import mw
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-02-15 15:03:43 +01:00
|
|
|
if path.startswith("/"):
|
|
|
|
subpath = ""
|
|
|
|
else:
|
|
|
|
subpath = "/_anki/"
|
|
|
|
|
|
|
|
return f"http://127.0.0.1:{mw.mediaServer.getPort()}{subpath}{path}"
|
2017-07-28 08:19:06 +02:00
|
|
|
|
2020-02-12 22:00:13 +01:00
|
|
|
def bundledScript(self, fname: str) -> str:
|
2021-02-11 01:09:06 +01:00
|
|
|
return f'<script src="{self.webBundlePath(fname)}"></script>'
|
2017-07-28 08:19:06 +02:00
|
|
|
|
2020-02-12 22:00:13 +01:00
|
|
|
def bundledCSS(self, fname: str) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath(
|
|
|
|
fname
|
|
|
|
)
|
2017-07-28 08:19:06 +02:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def eval(self, js: str) -> None:
|
2017-08-02 07:39:49 +02:00
|
|
|
self.evalWithCallback(js, None)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def evalWithCallback(self, js: str, cb: Callable) -> None:
|
2017-08-07 08:01:35 +02:00
|
|
|
self._queueAction("eval", js, cb)
|
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
|
2017-08-07 08:01:35 +02:00
|
|
|
if cb:
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def handler(val: Any) -> None:
|
2018-09-28 08:27:41 +02:00
|
|
|
if self._shouldIgnoreWebEvent():
|
|
|
|
print("ignored late js callback", cb)
|
|
|
|
return
|
|
|
|
cb(val)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-09-28 08:27:41 +02:00
|
|
|
self.page().runJavaScript(js, handler)
|
2017-08-02 07:39:49 +02:00
|
|
|
else:
|
2017-08-07 08:01:35 +02:00
|
|
|
self.page().runJavaScript(js)
|
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _queueAction(self, name: str, *args: Any) -> None:
|
2017-08-07 08:01:35 +02:00
|
|
|
self._pendingActions.append((name, args))
|
|
|
|
self._maybeRunActions()
|
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _maybeRunActions(self) -> None:
|
2021-11-24 22:17:41 +01:00
|
|
|
if sip.isdeleted(self):
|
|
|
|
return
|
2017-08-07 08:01:35 +02:00
|
|
|
while self._pendingActions and self._domDone:
|
|
|
|
name, args = self._pendingActions.pop(0)
|
|
|
|
|
|
|
|
if name == "eval":
|
|
|
|
self._evalWithCallback(*args)
|
|
|
|
elif name == "setHtml":
|
|
|
|
self._setHtml(*args)
|
|
|
|
else:
|
2021-02-11 00:37:38 +01:00
|
|
|
raise Exception(f"unknown action: {name}")
|
2016-06-07 06:27:33 +02:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _openLinksExternally(self, url: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
openLink(url)
|
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _shouldIgnoreWebEvent(self) -> bool:
|
2018-09-28 08:27:41 +02:00
|
|
|
# async web events may be received after the profile has been closed
|
|
|
|
# or the underlying webview has been deleted
|
|
|
|
from aqt import mw
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-10-12 04:08:19 +02:00
|
|
|
if sip.isdeleted(self):
|
|
|
|
return True
|
|
|
|
if not mw.col and self.requiresCol:
|
|
|
|
return True
|
|
|
|
return False
|
2018-09-28 08:27:41 +02:00
|
|
|
|
2020-01-22 01:46:35 +01:00
|
|
|
def _onBridgeCmd(self, cmd: str) -> Any:
|
2018-09-28 08:27:41 +02:00
|
|
|
if self._shouldIgnoreWebEvent():
|
|
|
|
print("ignored late bridge cmd", cmd)
|
2017-08-07 05:12:47 +02:00
|
|
|
return
|
|
|
|
|
2019-04-21 11:12:04 +02:00
|
|
|
if not self._filterSet:
|
|
|
|
self.focusProxy().installEventFilter(self)
|
|
|
|
self._filterSet = True
|
|
|
|
|
2016-06-06 07:50:03 +02:00
|
|
|
if cmd == "domDone":
|
2017-08-01 06:30:04 +02:00
|
|
|
self._domDone = True
|
2017-08-07 08:01:35 +02:00
|
|
|
self._maybeRunActions()
|
2016-06-06 07:50:03 +02:00
|
|
|
else:
|
2020-01-22 01:46:35 +01:00
|
|
|
handled, result = gui_hooks.webview_did_receive_js_message(
|
2020-02-08 23:59:29 +01:00
|
|
|
(False, None), cmd, self._bridge_context
|
2020-01-22 01:46:35 +01:00
|
|
|
)
|
|
|
|
if handled:
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
return self.onBridgeCmd(cmd)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def defaultOnBridgeCmd(self, cmd: str) -> None:
|
2016-06-06 07:50:03 +02:00
|
|
|
print("unhandled bridge cmd:", cmd)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
2020-01-22 01:46:35 +01:00
|
|
|
# legacy
|
2020-02-27 03:08:38 +01:00
|
|
|
def resetHandlers(self) -> None:
|
2016-06-06 07:50:03 +02:00
|
|
|
self.onBridgeCmd = self.defaultOnBridgeCmd
|
2020-02-08 23:59:29 +01:00
|
|
|
self._bridge_context = None
|
2017-08-02 08:22:54 +02:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def adjustHeightToFit(self) -> None:
|
2021-04-13 21:32:53 +02:00
|
|
|
self.evalWithCallback("document.documentElement.offsetHeight", self._onHeight)
|
2017-08-02 08:22:54 +02:00
|
|
|
|
2020-02-27 03:08:38 +01:00
|
|
|
def _onHeight(self, qvar: Optional[int]) -> None:
|
2019-12-24 11:15:47 +01:00
|
|
|
from aqt import mw
|
2019-12-24 11:23:36 +01:00
|
|
|
|
2018-06-29 09:17:13 +02:00
|
|
|
if qvar is None:
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2022-02-24 12:15:56 +01:00
|
|
|
mw.progress.single_shot(1000, mw.reset)
|
2018-06-29 09:17:13 +02:00
|
|
|
return
|
|
|
|
|
2021-03-30 23:32:20 +02:00
|
|
|
self.setFixedHeight(int(qvar))
|
2020-01-22 01:46:35 +01:00
|
|
|
|
2020-02-08 23:59:29 +01:00
|
|
|
def set_bridge_command(self, func: Callable[[str], Any], context: Any) -> None:
|
2020-01-22 01:46:35 +01:00
|
|
|
"""Set a handler for pycmd() messages received from Javascript.
|
|
|
|
|
2020-02-08 23:59:29 +01:00
|
|
|
Context is the object calling this routine, eg an instance of
|
|
|
|
aqt.reviewer.Reviewer or aqt.deckbrowser.DeckBrowser."""
|
2020-01-22 01:46:35 +01:00
|
|
|
self.onBridgeCmd = func
|
2020-02-08 23:59:29 +01:00
|
|
|
self._bridge_context = context
|
2020-08-27 10:39:27 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def hide_while_preserving_layout(self) -> None:
|
2020-08-27 10:39:27 +02:00
|
|
|
"Hide but keep existing size."
|
|
|
|
sp = self.sizePolicy()
|
|
|
|
sp.setRetainSizeWhenHidden(True)
|
|
|
|
self.setSizePolicy(sp)
|
|
|
|
self.hide()
|
|
|
|
|
2022-11-24 11:37:01 +01:00
|
|
|
def add_dynamic_css_and_classes_then_show(self) -> None:
|
|
|
|
"Add dynamic styling, set platform-specific body classes and reveal."
|
2020-08-27 10:39:27 +02:00
|
|
|
css = self.standard_css()
|
2022-11-24 11:37:01 +01:00
|
|
|
body_classes = theme_manager.body_class().split(" ")
|
2020-08-29 14:00:28 +02:00
|
|
|
|
2022-11-24 11:37:01 +01:00
|
|
|
def after_injection(arg: Any) -> None:
|
2020-08-28 09:10:35 +02:00
|
|
|
gui_hooks.webview_did_inject_style_into_page(self)
|
|
|
|
self.show()
|
2020-08-29 14:00:28 +02:00
|
|
|
|
2020-08-27 13:46:34 +02:00
|
|
|
self.evalWithCallback(
|
|
|
|
f"""
|
2022-02-11 05:34:00 +01:00
|
|
|
(function(){{
|
|
|
|
const style = document.createElement('style');
|
|
|
|
style.innerHTML = `{css}`;
|
|
|
|
document.head.appendChild(style);
|
2022-11-24 11:37:01 +01:00
|
|
|
document.body.classList.add({", ".join([f'"{c}"' for c in body_classes])});
|
2022-02-11 05:34:00 +01:00
|
|
|
}})();
|
2020-08-29 14:00:28 +02:00
|
|
|
""",
|
2022-11-24 11:37:01 +01:00
|
|
|
after_injection,
|
2020-08-29 14:00:28 +02:00
|
|
|
)
|
2020-08-27 13:53:28 +02:00
|
|
|
|
|
|
|
def load_ts_page(self, name: str) -> None:
|
|
|
|
from aqt import mw
|
|
|
|
|
2021-02-06 06:03:21 +01:00
|
|
|
self.set_open_links_externally(True)
|
2020-08-27 13:53:28 +02:00
|
|
|
if theme_manager.night_mode:
|
|
|
|
extra = "#night"
|
|
|
|
else:
|
|
|
|
extra = ""
|
2021-03-17 05:51:59 +01:00
|
|
|
self.load_url(QUrl(f"{mw.serverURL()}_anki/pages/{name}.html{extra}"))
|
2022-11-24 11:37:01 +01:00
|
|
|
self.add_dynamic_css_and_classes_then_show()
|
2021-03-17 05:51:59 +01:00
|
|
|
|
|
|
|
def force_load_hack(self) -> None:
|
|
|
|
"""Force process to initialize.
|
|
|
|
Must be done on Windows prior to changing current working directory."""
|
|
|
|
self.requiresCol = False
|
|
|
|
self._domReady = False
|
2021-10-07 14:23:00 +02:00
|
|
|
self._page.setContent(cast(QByteArray, bytes("", "ascii")))
|
|
|
|
|
2021-11-29 03:31:37 +01:00
|
|
|
def cleanup(self) -> None:
|
2021-10-12 09:07:07 +02:00
|
|
|
try:
|
|
|
|
from aqt import mw
|
|
|
|
except ImportError:
|
|
|
|
# this will fail when __del__ is called during app shutdown
|
|
|
|
return
|
2021-10-07 14:23:00 +02:00
|
|
|
|
2021-11-24 22:17:41 +01:00
|
|
|
gui_hooks.theme_did_change.remove(self.on_theme_did_change)
|
2021-10-07 14:23:00 +02:00
|
|
|
mw.mediaServer.clear_page_html(id(self))
|
2022-05-25 13:46:47 +02:00
|
|
|
self._page.deleteLater()
|
2021-11-24 22:17:41 +01:00
|
|
|
|
|
|
|
def on_theme_did_change(self) -> None:
|
|
|
|
# avoid flashes if page reloaded
|
Introduce new color palette using Sass maps (#2016)
* Remove --medium-border variable
* Implement color palette using Sass maps
I hand-picked the gray tones, the other colors are from the Tailwind CSS v3 palette.
Significant changes:
- light theme is brighter
- dark theme is darker
- borders are softer
I also deleted some platform- and night-mode-specific code.
* Use custom colors for note view switch
* Use same placeholder color for all inputs
* Skew color palette for more dark values
by removing gray[3], which wasn't used anywhere. Slight adjustments were made to the darker tones.
* Adjust frame- window- and border colors
* Give deck browser entries --frame-bg as background color
* Define styling for QComboBox and QLineEdit globally
* Experiment with CSS filter for inline-colors
Inside darker inputs, some colors like dark blue will be hard to read, so we could try to improve text-color contrast with global adjustments depending on the theme.
* Use different map structure for _vars.scss
after @hgiesel's idea: https://github.com/ankitects/anki/pull/2016#discussion_r947087871
* Move custom QLineEdit styles out of searchbar.py
* Merge branch 'main' into color-palette
* Revert QComboBox stylesheet override
* Align gray color palette more with macOS
* Adjust light theme
* Use --slightly-grey-text for options tab color
* Replace gray tones with more neutral values
* Improve categorization of global colors
by renaming almost all of them and sorting them into separate maps.
* Saturate highlight-bg in light theme
* Tweak gray tones
* Adjust box-shadow of EditingArea to make fields look inset
* Add Sass functions to access color palette and semantic variables
in response to https://github.com/ankitects/anki/pull/2016#issuecomment-1220571076
* Showcase use of access functions in several locations
@hgiesel in buttons.scss I access the color palette directly. Is this what you meant by "... keep it local to the component, and possibly make it global at a later time ..."?
* Fix focus box shadow transition and remove default shadow for a cleaner look
I couldn't quite get the inset look the way I wanted, because inset box-shadows do not respect the border radius, therefore causing aliasing.
* Tweak light theme border and shadow colors
* Add functions and colors to base_lib
* Add vars_lib as dependency to base_lib and button_mixins_lib
* Improve uses of default-themed variables
* Use old --frame-bg color and use darker tone for canvas-default
* Return CSS var by default and add palette-of function for raw value
* Showcase use of palette-of function
The #{...} syntax is required only because the use cases are CSS var definitions. In other cases a simple palette-of(keyword, theme) would suffice.
* Light theme: decrease brightness of canvas-default and adjust fg-default
* Use canvas-inset variable for switch knob
* Adjust light theme
* Add back box-shadow to EditingArea
* Light theme: darken background and flatten transition
also set hue and saturation of gray-8 to 0 (like all the other grays).
* Reduce flag colors to single default value
* Tweak card/note accent colors
* Experiment with inset look for fields again
Is this too dark in night mode? It's the same color used for all other text inputs.
* Dark theme: make border-default one shade darker
* Tweak inset shadow color
* Dark theme: make border-faint darker than canvas-default
meaning two shades darker than it currently was.
* Fix PlainTextInput not expanding
* Dark theme: use less saturated flag colors
* Adjust gray tones
* Fix nested variables not getting extracted correctly
* Rename canvas-outset to canvas-elevated
* Light theme: darken canvas-default
* Make canvas-elevated a bit darker
* Rename variables and use them in various components
* Refactor button mixins
* Remove fusion vars from Anki
* Adjust button gradients
* Refactor button mixins
* Fix deck browser table td background color
* Use color function in buttons.scss
* Rework QTabWidget stylesheet
* Fix crash on browser open
* Perfect QTableView header
* Fix bottom toolbar button gradient
* Fix focus outline of bottom toolbar buttons
* Fix custom webview scrollbar
* Fix uses of vars in various webviews
The command @use vars as * lead to repeated inclusion of the CSS vars.
* Enable primary button color with mixin
* Run prettier
* Fix Python code style issues
* Tweak colors
* Lighten scrollbar shades in light theme
* Fix code style issues caused by merge
* Fix harsh border color in editor
caused by leftover --medium-border variables, probably introduced with a merge commit.
* Compile Sass before extracting Python colors/props
This means the Python side doesn't need to worry about the map structure and Sass functions, just copy the output CSS values.
* Desaturate primary button colors by 10%
* Convert accidentally capitalized variable names to lowercase
* Simplify color definitions with qcolor function
* Remove default border-focus variable
* Remove redundant colon
* Apply custom scrollbar CSS only on Windows and Linux
* Make border-subtle color brighter than background in dark theme
* Make border-subtle color a shade brighter in light theme
* Use border-subtle for NoteEditor and EditorToolbar border
* Small patches
2022-09-16 06:11:18 +02:00
|
|
|
self._page.setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
|
2021-11-24 22:17:41 +01:00
|
|
|
# update night-mode class, and legacy nightMode/night-mode body classes
|
|
|
|
self.eval(
|
|
|
|
f"""
|
|
|
|
(function() {{
|
|
|
|
const doc = document.documentElement.classList;
|
|
|
|
const body = document.body.classList;
|
|
|
|
if ({1 if theme_manager.night_mode else 0}) {{
|
|
|
|
doc.add("night-mode");
|
2022-11-21 01:20:00 +01:00
|
|
|
body.add("night_mode");
|
2021-11-24 22:17:41 +01:00
|
|
|
body.add("nightMode");
|
2022-11-21 01:20:00 +01:00
|
|
|
{"body.add('macos-dark-mode');" if theme_manager.macos_dark_mode() else ""}
|
2021-11-24 22:17:41 +01:00
|
|
|
}} else {{
|
|
|
|
doc.remove("night-mode");
|
2022-11-21 01:20:00 +01:00
|
|
|
body.remove("night_mode");
|
2021-11-24 22:17:41 +01:00
|
|
|
body.remove("nightMode");
|
2022-11-21 01:20:00 +01:00
|
|
|
body.remove("macos-dark-mode");
|
2021-11-24 22:17:41 +01:00
|
|
|
}}
|
|
|
|
}})();
|
|
|
|
"""
|
|
|
|
)
|
2022-10-03 04:50:22 +02:00
|
|
|
|
|
|
|
@deprecated(info="use theme_manager.qcolor() instead")
|
|
|
|
def get_window_bg_color(self, night_mode: Optional[bool] = None) -> QColor:
|
|
|
|
return theme_manager.qcolor(colors.CANVAS)
|