anki/qt/aqt/webview.py

727 lines
24 KiB
Python
Raw Normal View History

2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import dataclasses
import json
import re
2019-12-20 10:19:03 +01:00
import sys
from typing import Any, Callable, Optional, Sequence, cast
2019-12-20 10:19:03 +01:00
import anki
import anki.lang
from anki._legacy import deprecated
from anki.lang import is_rtl
from anki.utils import is_lin, is_mac, is_win
from aqt import colors, gui_hooks
from aqt.qt import *
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
serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
# Page for debug messages
##########################################################################
2021-02-02 14:30:53 +01:00
BridgeCommandHandler = Callable[[str], Any]
class AnkiWebPage(QWebEnginePage):
2021-02-02 14:30:53 +01:00
def __init__(self, onBridgeCmd: BridgeCommandHandler) -> None:
QWebEnginePage.__init__(self)
self._onBridgeCmd = onBridgeCmd
self._setupBridge()
self.open_links_externally = True
def _setupBridge(self) -> None:
class Bridge(QObject):
def __init__(self, bridge_handler: Callable[[str], Any]) -> None:
super().__init__()
self.onCmd = bridge_handler
@pyqtSlot(str, result=str) # type: ignore
2021-02-02 14:30:53 +01:00
def cmd(self, str: str) -> Any:
return json.dumps(self.onCmd(str))
self._bridge = Bridge(self._onCmd)
self._channel = QWebChannel(self)
self._channel.registerObject("py", self._bridge)
self.setWebChannel(self._channel)
qwebchannel = ":/qtwebchannel/qwebchannel.js"
jsfile = QFile(qwebchannel)
if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly):
print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr)
jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8")
jsfile.close()
script = QWebEngineScript()
2019-12-23 01:34:10 +01:00
script.setSourceCode(
jstext
2019-12-23 01:34:10 +01:00
+ """
var pycmd, bridgeCommand;
new QWebChannel(qt.webChannelTransport, function(channel) {
bridgeCommand = pycmd = function (arg, cb) {
var resultCB = function (res) {
// pass result back to user-provided callback
if (cb) {
cb(JSON.parse(res));
}
}
channel.objects.py.cmd(arg, resultCB);
return false;
}
pycmd("domDone");
});
2019-12-23 01:34:10 +01:00
"""
)
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
script.setRunsOnSubFrames(False)
self.profile().scripts().insert(script)
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
if srcID.startswith("data"):
srcID = ""
else:
srcID = serverbaseurl.sub("", srcID[:80], 1)
if level == QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel:
2021-02-02 14:30:53 +01:00
level_str = "info"
elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:
2021-02-02 14:30:53 +01:00
level_str = "warning"
elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel:
2021-02-02 14:30:53 +01:00
level_str = "error"
else:
level_str = str(level)
buf = "JS %(t)s %(f)s:%(a)d %(b)s" % dict(
t=level_str, a=line, f=srcID, b=f"{msg}\n"
)
2020-11-18 12:34:44 +01:00
if "MathJax localStorage" in buf:
# silence localStorage noise
return
elif "link preload" in buf:
# silence 'link preload' warning on the first card
return
# ensure we don't try to write characters the terminal can't handle
buf = buf.encode(sys.stdout.encoding, "backslashreplace").decode(
sys.stdout.encoding
2019-12-23 01:34:10 +01: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)
2021-02-02 14:30:53 +01:00
def acceptNavigationRequest(
self, url: QUrl, navType: Any, isMainFrame: bool
) -> bool:
if (
not self.open_links_externally
or "_anki/pages" in url.path()
or url.path() == "/_anki/legacyPageData"
):
return super().acceptNavigationRequest(url, navType, isMainFrame)
if not isMainFrame:
return True
# data: links generated by setHtml()
if url.scheme() == "data":
return True
# catch buggy <a href='#' onclick='func()'> links
from aqt import mw
2019-12-23 01:34:10 +01:00
if url.matches(
QUrl(mw.serverURL()), cast(Any, QUrl.UrlFormattingOption.RemoveFragment)
):
print("onclick handler needs to return false")
return False
# load all other links in browser
openLink(url)
return False
def _onCmd(self, str: str) -> Any:
return self._onBridgeCmd(str)
def javaScriptAlert(self, frame: Any, text: str) -> None:
showInfo(text)
def javaScriptConfirm(self, frame: Any, text: str) -> bool:
return askUser(text)
2019-12-23 01:34:10 +01:00
# Add-ons
##########################################################################
@dataclasses.dataclass
class WebContent:
"""Stores all dynamically modified content that a particular web view
will be populated with.
Attributes:
body {str} -- HTML body
head {str} -- HTML head
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
Important Notes:
- 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.
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:
web_content.body += "<my_html>"
web_content.head += "<my_head>"
- 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
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
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
> from aqt import mw
> mw.addonManager.setWebExports(__name__, r"web/.*(css|js)")
2020-08-31 05:29:28 +02: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:
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
2020-08-31 05:29:28 +02:00
Note that '/' will also match the os specific path separator.
"""
body: str = ""
head: str = ""
css: list[str] = dataclasses.field(default_factory=lambda: [])
js: list[str] = dataclasses.field(default_factory=lambda: [])
# Main web view
##########################################################################
class AnkiWebView(QWebEngineView):
allow_drops = False
def __init__(
2021-07-03 01:54:10 +02:00
self,
parent: Optional[QWidget] = None,
title: str = "default",
) -> None:
QWebEngineView.__init__(self, parent=parent)
self.set_title(title)
self._page = AnkiWebPage(self._onBridgeCmd)
# 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))
# in new code, use .set_bridge_command() instead of setting this directly
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
self._domDone = True
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
self.requiresCol = True
self.setPage(self._page)
self._disable_zoom = False
self.resetHandlers()
self._filterSet = False
QShortcut( # type: ignore
2019-12-23 01:34:10 +01:00
QKeySequence("Esc"),
self,
context=Qt.ShortcutContext.WidgetWithChildrenShortcut,
2019-12-23 01:34:10 +01:00
activated=self.onEsc,
)
gui_hooks.theme_did_change.append(self.on_theme_did_change)
def set_title(self, title: str) -> None:
self.title = title # type: ignore[assignment]
def disable_zoom(self) -> None:
self._disable_zoom = True
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
if (
isinstance(evt, QMouseEvent)
and evt.type() == QEvent.Type.MouseButtonRelease
):
if evt.button() == Qt.MouseButton.MiddleButton and is_lin:
2018-03-02 02:16:02 +01:00
self.onMiddleClickPaste()
return True
2017-06-23 06:34:56 +02:00
return False
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:
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
# esc in a child window closes the window
if w != mw:
w.close()
else:
# in the main window, removes focus from type in area
parent = self.parent()
assert isinstance(parent, QWidget)
parent.setFocus()
break
w = w.parent()
2021-02-01 14:28:21 +01:00
def onCopy(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.Copy)
2021-02-01 14:28:21 +01:00
def onCut(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.Cut)
2021-02-01 14:28:21 +01:00
def onPaste(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.Paste)
2021-02-01 14:28:21 +01:00
def onMiddleClickPaste(self) -> None:
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:
self.triggerPageAction(QWebEnginePage.WebAction.SelectAll)
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
m = QMenu(self)
2021-03-26 04:48:26 +01:00
a = m.addAction(tr.actions_copy())
qconnect(a.triggered, self.onCopy)
gui_hooks.webview_will_show_context_menu(self, m)
m.popup(QCursor.pos())
2021-02-02 14:30:53 +01:00
def dropEvent(self, evt: QDropEvent) -> None:
if self.allow_drops:
super().dropEvent(evt)
def setHtml(self, html: str) -> None: # type: ignore
# discard any previous pending actions
self._pendingActions = []
self._domDone = True
self._queueAction("setHtml", html)
self.set_open_links_externally(True)
self.setZoomFactor(1)
self.allow_drops = False
self.show()
2020-02-27 03:08:38 +01:00
def _setHtml(self, html: str) -> None:
"""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."""
from aqt import mw
oldFocus = mw.app.focusWidget()
self._domDone = False
webview_id = id(self)
mw.mediaServer.set_page_html(webview_id, html)
self.load_url(QUrl(f"{mw.serverURL()}_anki/legacyPageData?id={webview_id}"))
# work around webengine stealing focus on setHtml()
# fixme: check which if any qt versions this is still required on
if oldFocus:
oldFocus.setFocus()
def load_url(self, url: QUrl) -> None:
# allow queuing actions when loading url directly
self._domDone = False
self.allow_drops = False
super().load(url)
def app_zoom_factor(self) -> float:
# overridden scale factor?
webscale = os.environ.get("ANKI_WEBSCALE")
if webscale:
return float(webscale)
if qtmajor > 5 or is_mac:
return 1
screen = QApplication.desktop().screen() # type: ignore
if screen is None:
return 1
2016-06-07 06:27:33 +02:00
dpi = screen.logicalDpiX()
factor = dpi / 96.0
if is_lin:
factor = max(1, factor)
return factor
return 1
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:
# 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
def standard_css(self) -> str:
palette = theme_manager.default_palette
color_hl = palette.color(QPalette.ColorRole.Highlight).name()
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()
button_style = f"""
button {{ font-family: {family}; }}
Add comments to Sass variables and tweak main window (#2137) * Prevent multiple inclusion of variables in CSS files * Use dict instead of tuple for variables * Add comments to variables * Improve appearance of main window * Tweak main window styles * Use json.dumps over pprint.format * Make study button primary * Improve header margin * Make bottom toolbar slimmer * Make congrats page more balanced * Fix type issue * Replace day/night with light/dark * Exclude top-level-drag-row from hover effect * Create dataclass for variables * Run formatter * Apply CSS variables from Python side Why go full-circle with the Sass variables? This way we only need one interface for add-on authors to interact with. It also makes it easier for us to apply additional themes in the future. * Fix typing * Fix rgba values in Qt * Darken button background * Fix palette not being applied in light theme For some odd reason this problem arose much later than #2016. * Tweak default button look * Reformat * Apply CSS vars to ts pages * Include elevation in button_mixins_lib * Cast opacity to int * Add some margin to studiedToday info * Tweak light theme button gradient * Tweak highlight-bg for light theme * Add back default button color as it made the browser sidebar tool icons dark in light theme. * Reformat * Tweak light theme buttons once more Sorry for the back-and-forth. Sass only compiles when there are changes in user files, not when I only change the vars. * Fix bottom toolbar button indicators * Make buttons more clicky * Fix button padding * Handle macOS separately again * Decrease elevation effect for main window buttons to 1 * Imitate box-shadow for Qt elements * Adjust shadow vars * Adjust primary border color because the save button in the deck options had a lighter color than its background gradient. * Boost box-shadow color of primary buttons * Format * Adjust Qt box-shadow imitation and shadow colors * Use more subtle default shadow color * Add some more padding to top toolbar * Revert "Apply CSS vars to ts pages" This reverts commit 5d8e7f6b7ffc8894b6517ecbb8cfba35407fc69a. * Revert "Apply CSS variables from Python side" This reverts commit 87db774412fd2bfd75e2630d2c5e782daef96b5f. * Better match the standard macOS buttons In the dark theme the standard color is a lighter grey, but at least the size/shape is similar again. This doesn't work for the editor buttons. * Reduce the top margin of the congrats screen * Fix illegible buttons when changing theme on macOS; match dark button style
2022-10-29 02:48:53 +02:00
"""
font = f"font-family:{family};"
elif is_mac:
2019-12-23 01:34:10 +01:00
family = "Helvetica"
font = f'font-family:"{family}";'
Add comments to Sass variables and tweak main window (#2137) * Prevent multiple inclusion of variables in CSS files * Use dict instead of tuple for variables * Add comments to variables * Improve appearance of main window * Tweak main window styles * Use json.dumps over pprint.format * Make study button primary * Improve header margin * Make bottom toolbar slimmer * Make congrats page more balanced * Fix type issue * Replace day/night with light/dark * Exclude top-level-drag-row from hover effect * Create dataclass for variables * Run formatter * Apply CSS variables from Python side Why go full-circle with the Sass variables? This way we only need one interface for add-on authors to interact with. It also makes it easier for us to apply additional themes in the future. * Fix typing * Fix rgba values in Qt * Darken button background * Fix palette not being applied in light theme For some odd reason this problem arose much later than #2016. * Tweak default button look * Reformat * Apply CSS vars to ts pages * Include elevation in button_mixins_lib * Cast opacity to int * Add some margin to studiedToday info * Tweak light theme button gradient * Tweak highlight-bg for light theme * Add back default button color as it made the browser sidebar tool icons dark in light theme. * Reformat * Tweak light theme buttons once more Sorry for the back-and-forth. Sass only compiles when there are changes in user files, not when I only change the vars. * Fix bottom toolbar button indicators * Make buttons more clicky * Fix button padding * Handle macOS separately again * Decrease elevation effect for main window buttons to 1 * Imitate box-shadow for Qt elements * Adjust shadow vars * Adjust primary border color because the save button in the deck options had a lighter color than its background gradient. * Boost box-shadow color of primary buttons * Format * Adjust Qt box-shadow imitation and shadow colors * Use more subtle default shadow color * Add some more padding to top toolbar * Revert "Apply CSS vars to ts pages" This reverts commit 5d8e7f6b7ffc8894b6517ecbb8cfba35407fc69a. * Revert "Apply CSS variables from Python side" This reverts commit 87db774412fd2bfd75e2630d2c5e782daef96b5f. * Better match the standard macOS buttons In the dark theme the standard color is a lighter grey, but at least the size/shape is similar again. This doesn't work for the editor buttons. * Reduce the top margin of the congrats screen * Fix illegible buttons when changing theme on macOS; match dark button style
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()
font = f'font-family:"{family}", sans-serif;'
button_style = """
/* Buttons */
button{{
Add comments to Sass variables and tweak main window (#2137) * Prevent multiple inclusion of variables in CSS files * Use dict instead of tuple for variables * Add comments to variables * Improve appearance of main window * Tweak main window styles * Use json.dumps over pprint.format * Make study button primary * Improve header margin * Make bottom toolbar slimmer * Make congrats page more balanced * Fix type issue * Replace day/night with light/dark * Exclude top-level-drag-row from hover effect * Create dataclass for variables * Run formatter * Apply CSS variables from Python side Why go full-circle with the Sass variables? This way we only need one interface for add-on authors to interact with. It also makes it easier for us to apply additional themes in the future. * Fix typing * Fix rgba values in Qt * Darken button background * Fix palette not being applied in light theme For some odd reason this problem arose much later than #2016. * Tweak default button look * Reformat * Apply CSS vars to ts pages * Include elevation in button_mixins_lib * Cast opacity to int * Add some margin to studiedToday info * Tweak light theme button gradient * Tweak highlight-bg for light theme * Add back default button color as it made the browser sidebar tool icons dark in light theme. * Reformat * Tweak light theme buttons once more Sorry for the back-and-forth. Sass only compiles when there are changes in user files, not when I only change the vars. * Fix bottom toolbar button indicators * Make buttons more clicky * Fix button padding * Handle macOS separately again * Decrease elevation effect for main window buttons to 1 * Imitate box-shadow for Qt elements * Adjust shadow vars * Adjust primary border color because the save button in the deck options had a lighter color than its background gradient. * Boost box-shadow color of primary buttons * Format * Adjust Qt box-shadow imitation and shadow colors * Use more subtle default shadow color * Add some more padding to top toolbar * Revert "Apply CSS vars to ts pages" This reverts commit 5d8e7f6b7ffc8894b6517ecbb8cfba35407fc69a. * Revert "Apply CSS variables from Python side" This reverts commit 87db774412fd2bfd75e2630d2c5e782daef96b5f. * Better match the standard macOS buttons In the dark theme the standard color is a lighter grey, but at least the size/shape is similar again. This doesn't work for the editor buttons. * Reduce the top margin of the congrats screen * Fix illegible buttons when changing theme on macOS; match dark button style
2022-10-29 02:48:53 +02:00
font-family: "{family}", sans-serif;
}}
/* Input field focus outline */
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
div[contenteditable="true"]:focus {{
outline: 0 none;
border-color: {color_hl};
}}""".format(
family=family,
color_hl=color_hl,
)
2019-12-23 01:34:10 +01:00
zoom = self.app_zoom_factor()
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); }}
html {{ {font} }}
{button_style}
Add comments to Sass variables and tweak main window (#2137) * Prevent multiple inclusion of variables in CSS files * Use dict instead of tuple for variables * Add comments to variables * Improve appearance of main window * Tweak main window styles * Use json.dumps over pprint.format * Make study button primary * Improve header margin * Make bottom toolbar slimmer * Make congrats page more balanced * Fix type issue * Replace day/night with light/dark * Exclude top-level-drag-row from hover effect * Create dataclass for variables * Run formatter * Apply CSS variables from Python side Why go full-circle with the Sass variables? This way we only need one interface for add-on authors to interact with. It also makes it easier for us to apply additional themes in the future. * Fix typing * Fix rgba values in Qt * Darken button background * Fix palette not being applied in light theme For some odd reason this problem arose much later than #2016. * Tweak default button look * Reformat * Apply CSS vars to ts pages * Include elevation in button_mixins_lib * Cast opacity to int * Add some margin to studiedToday info * Tweak light theme button gradient * Tweak highlight-bg for light theme * Add back default button color as it made the browser sidebar tool icons dark in light theme. * Reformat * Tweak light theme buttons once more Sorry for the back-and-forth. Sass only compiles when there are changes in user files, not when I only change the vars. * Fix bottom toolbar button indicators * Make buttons more clicky * Fix button padding * Handle macOS separately again * Decrease elevation effect for main window buttons to 1 * Imitate box-shadow for Qt elements * Adjust shadow vars * Adjust primary border color because the save button in the deck options had a lighter color than its background gradient. * Boost box-shadow color of primary buttons * Format * Adjust Qt box-shadow imitation and shadow colors * Use more subtle default shadow color * Add some more padding to top toolbar * Revert "Apply CSS vars to ts pages" This reverts commit 5d8e7f6b7ffc8894b6517ecbb8cfba35407fc69a. * Revert "Apply CSS variables from Python side" This reverts commit 87db774412fd2bfd75e2630d2c5e782daef96b5f. * Better match the standard macOS buttons In the dark theme the standard color is a lighter grey, but at least the size/shape is similar again. This doesn't work for the editor buttons. * Reduce the top margin of the congrats screen * Fix illegible buttons when changing theme on macOS; match dark button style
2022-10-29 02:48:53 +02:00
:root {{ --canvas: {colors.CANVAS["light"]} }}
:root[class*=night-mode] {{ --canvas: {colors.CANVAS["dark"]} }}
"""
def stdHtml(
self,
body: str,
css: Optional[list[str]] = None,
js: Optional[list[str]] = None,
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:
css = (["css/webview.css"] if default_css else []) + (
[] if css is None else css
)
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),
css=css,
)
gui_hooks.webview_will_set_content(web_content, context)
csstxt = ""
if "css/webview.css" in css:
# we want our dynamic styling to override the defaults in
# css/webview.css, but come before user-provided stylesheets so that
# they can override us if necessary
web_content.css.remove("css/webview.css")
csstxt = self.bundledCSS("css/webview.css")
csstxt += f"<style>{self.standard_css()}</style>"
csstxt += "\n".join(self.bundledCSS(fname) for fname in web_content.css)
jstxt = "\n".join(self.bundledScript(fname) for fname in web_content.js)
from aqt import mw
2019-12-23 01:34:10 +01:00
head = mw.baseHTML() + csstxt + web_content.head
body_class = theme_manager.body_class()
if theme_manager.night_mode:
doc_class = "night-mode"
else:
doc_class = ""
if is_rtl(anki.lang.current_lang):
lang_dir = "rtl"
else:
lang_dir = "ltr"
html = f"""
<!doctype html>
<html class="{doc_class}" dir="{lang_dir}">
<head>
<title>{self.title}</title>
{head}
</head>
<body class="{body_class}">
{jstxt}
{web_content.body}</body>
</html>"""
2019-12-23 01:34:10 +01:00
# print(html)
self.setHtml(html)
@classmethod
def webBundlePath(cls, path: str) -> str:
from aqt import mw
2019-12-23 01:34:10 +01:00
if path.startswith("/"):
subpath = ""
else:
subpath = "/_anki/"
return f"http://127.0.0.1:{mw.mediaServer.getPort()}{subpath}{path}"
def bundledScript(self, fname: str) -> str:
return f'<script src="{self.webBundlePath(fname)}"></script>'
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
)
2020-02-27 03:08:38 +01:00
def eval(self, js: str) -> None:
self.evalWithCallback(js, None)
2020-02-27 03:08:38 +01:00
def evalWithCallback(self, js: str, cb: Callable) -> None:
self._queueAction("eval", js, cb)
2020-02-27 03:08:38 +01:00
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
if cb:
2019-12-23 01:34:10 +01:00
2021-02-02 14:30:53 +01:00
def handler(val: Any) -> None:
if self._shouldIgnoreWebEvent():
print("ignored late js callback", cb)
return
cb(val)
2019-12-23 01:34:10 +01:00
self.page().runJavaScript(js, handler)
else:
self.page().runJavaScript(js)
2020-02-27 03:08:38 +01:00
def _queueAction(self, name: str, *args: Any) -> None:
self._pendingActions.append((name, args))
self._maybeRunActions()
2020-02-27 03:08:38 +01:00
def _maybeRunActions(self) -> None:
if sip.isdeleted(self):
return
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:
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:
openLink(url)
2020-02-27 03:08:38 +01:00
def _shouldIgnoreWebEvent(self) -> bool:
# 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
if sip.isdeleted(self):
return True
if not mw.col and self.requiresCol:
return True
return False
def _onBridgeCmd(self, cmd: str) -> Any:
if self._shouldIgnoreWebEvent():
print("ignored late bridge cmd", cmd)
return
if not self._filterSet:
self.focusProxy().installEventFilter(self)
self._filterSet = True
if cmd == "domDone":
self._domDone = True
self._maybeRunActions()
else:
handled, result = gui_hooks.webview_did_receive_js_message(
(False, None), cmd, self._bridge_context
)
if handled:
return result
else:
return self.onBridgeCmd(cmd)
2020-02-27 03:08:38 +01:00
def defaultOnBridgeCmd(self, cmd: str) -> None:
print("unhandled bridge cmd:", cmd)
# legacy
2020-02-27 03:08:38 +01:00
def resetHandlers(self) -> None:
self.onBridgeCmd = self.defaultOnBridgeCmd
self._bridge_context = None
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)
2020-02-27 03:08:38 +01:00
def _onHeight(self, qvar: Optional[int]) -> None:
from aqt import mw
if qvar is None:
2019-12-23 01:34:10 +01:00
mw.progress.single_shot(1000, mw.reset)
return
self.setFixedHeight(int(qvar))
def set_bridge_command(self, func: Callable[[str], Any], context: Any) -> None:
"""Set a handler for pycmd() messages received from Javascript.
Context is the object calling this routine, eg an instance of
aqt.reviewer.Reviewer or aqt.deckbrowser.DeckBrowser."""
self.onBridgeCmd = func
self._bridge_context = context
2021-02-01 14:28:21 +01:00
def hide_while_preserving_layout(self) -> None:
"Hide but keep existing size."
sp = self.sizePolicy()
sp.setRetainSizeWhenHidden(True)
self.setSizePolicy(sp)
self.hide()
2021-02-01 14:28:21 +01:00
def inject_dynamic_style_and_show(self) -> None:
"Add dynamic styling, and reveal."
css = self.standard_css()
2020-08-29 14:00:28 +02:00
2021-02-02 14:30:53 +01:00
def after_style(arg: Any) -> None:
gui_hooks.webview_did_inject_style_into_page(self)
self.show()
2020-08-29 14:00:28 +02:00
self.evalWithCallback(
f"""
(function(){{
const style = document.createElement('style');
style.innerHTML = `{css}`;
document.head.appendChild(style);
}})();
2020-08-29 14:00:28 +02:00
""",
after_style,
)
def load_ts_page(self, name: str) -> None:
from aqt import mw
self.set_open_links_externally(True)
if theme_manager.night_mode:
extra = "#night"
else:
extra = ""
self.hide_while_preserving_layout()
self.setZoomFactor(1)
self.load_url(QUrl(f"{mw.serverURL()}_anki/pages/{name}.html{extra}"))
self.inject_dynamic_style_and_show()
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
self._page.setContent(cast(QByteArray, bytes("", "ascii")))
2021-11-29 03:31:37 +01:00
def cleanup(self) -> None:
try:
from aqt import mw
except ImportError:
# this will fail when __del__ is called during app shutdown
return
gui_hooks.theme_did_change.remove(self.on_theme_did_change)
mw.mediaServer.clear_page_html(id(self))
self._page.deleteLater()
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))
# 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");
body.add("night_mode");
body.add("nightMode");
{"body.add('macos-dark-mode');" if theme_manager.macos_dark_mode() else ""}
}} else {{
doc.remove("night-mode");
body.remove("night_mode");
body.remove("nightMode");
body.remove("macos-dark-mode");
}}
}})();
"""
)
@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)