From e4e92639db5ee4877c1e495665651fa9d47a6194 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 17:57:12 +0200 Subject: [PATCH 1/9] Add custom zoom handling w/ improved image support Introduces a custom JS-based zoom system for the reviewer that enables zooming into viewport-constrained images. Additionally adds zoom persistence across Anki sessions. Fixes Image occlusion doesn't scale when page zoomed in #2588 (but also addresses general image zoom issues across all note types) --- qt/aqt/main.py | 30 +++++++++++++++------- qt/aqt/reviewer.py | 24 ++++++++++++++++++ ts/reviewer/index.ts | 14 ++++++++--- ts/reviewer/zoom.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 ts/reviewer/zoom.ts diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 231a6486059..1a9dff939d4 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1342,15 +1342,9 @@ def setupMenus(self) -> None: qconnect(m.actionPreferences.triggered, self.onPrefs) # View - qconnect( - m.actionZoomIn.triggered, - lambda: self.web.setZoomFactor(self.web.zoomFactor() + 0.1), - ) - qconnect( - m.actionZoomOut.triggered, - lambda: self.web.setZoomFactor(self.web.zoomFactor() - 0.1), - ) - qconnect(m.actionResetZoom.triggered, lambda: self.web.setZoomFactor(1)) + qconnect(m.actionZoomIn.triggered, self.zoom_in) + qconnect(m.actionZoomOut.triggered, self.zoom_out) + qconnect(m.actionResetZoom.triggered, self.reset_zoom) # app-wide shortcut qconnect(m.actionFullScreen.triggered, self.on_toggle_full_screen) m.actionFullScreen.setShortcut( @@ -1397,6 +1391,24 @@ def show_menubar(self) -> None: self.form.menubar.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) self.form.menubar.setMinimumSize(0, 0) + def zoom_in(self) -> None: + if self.state != "review": + self.web.setZoomFactor(self.web.zoomFactor() + 0.1) + else: + self.reviewer.zoom_in() + + def zoom_out(self) -> None: + if self.state != "review": + self.web.setZoomFactor(self.web.zoomFactor() - 0.1) + else: + self.reviewer.zoom_out() + + def reset_zoom(self) -> None: + if self.state != "review": + self.web.setZoomFactor(1) + else: + self.reviewer.reset_zoom() + # Auto update ########################################################################## diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 89f1d05de9c..321d3128a02 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -389,6 +389,7 @@ def _showQuestion(self) -> None: self._update_mark_icon() self._showAnswerButton() self.mw.web.setFocus() + self.maybe_restore_scale_factor() # user hook gui_hooks.reviewer_did_show_question(c) @@ -586,6 +587,8 @@ def _linkHandler(self, url: str) -> None: play_clicked_audio(url, self.card) elif url.startswith("updateToolbar"): self.mw.toolbarWeb.update_background_image() + elif url.startswith("scale"): + self.store_scale_factor(float(url.split(":")[1])) elif url == "statesMutated": self._states_mutated = True else: @@ -1077,6 +1080,27 @@ def onReplayRecorded(self) -> None: return av_player.play_file(self._recordedAudio) + # Zoom handling + + def zoom_in(self): + self.web.eval(f"anki.triggerScaleStep(1)") + + def zoom_out(self): + self.web.eval(f"anki.triggerScaleStep(-1)") + + def reset_zoom(self): + self.web.eval(f"anki.setScaleFactor(1)") + + def set_scale_factor(self, scale_factor: float, store: bool = True): + self.web.eval(f"anki.setScaleFactor({json.dumps(scale_factor)})") + + def store_scale_factor(self, scale_factor: float): + self.mw.pm.profile["lastReviewerScaleFactor"] = scale_factor + + def maybe_restore_scale_factor(self): + if scale_factor := self.mw.pm.profile.get("lastReviewerScaleFactor", None): + self.set_scale_factor(scale_factor, store=False) + # legacy onBuryCard = bury_current_card diff --git a/ts/reviewer/index.ts b/ts/reviewer/index.ts index 7825f8472a5..9a7c1b2a6ca 100644 --- a/ts/reviewer/index.ts +++ b/ts/reviewer/index.ts @@ -11,10 +11,14 @@ export { default as $, default as jQuery } from "jquery/dist/jquery"; import { setupImageCloze } from "../image-occlusion/review"; import { mutateNextCardStates } from "./answering"; +import { resetScaleFactor, setScaleFactor, setupWheelZoom, triggerScaleStep } from "./zoom"; globalThis.anki = globalThis.anki || {}; globalThis.anki.mutateNextCardStates = mutateNextCardStates; globalThis.anki.setupImageCloze = setupImageCloze; +globalThis.anki.setScaleFactor = setScaleFactor +globalThis.anki.triggerScaleStep = triggerScaleStep +globalThis.anki.resetScaleFactor = resetScaleFactor import { bridgeCommand } from "@tslib/bridgecommand"; import { registerPackage } from "@tslib/runtime-require"; @@ -167,13 +171,13 @@ export function _showQuestion(q: string, a: string, bodyclass: string): void { _updateQA( q, null, - function() { + function () { // return to top of window window.scrollTo(0, 0); document.body.className = bodyclass; }, - function() { + function () { // focus typing area if visible typeans = document.getElementById("typeans") as HTMLInputElement; if (typeans) { @@ -195,7 +199,7 @@ export function _showAnswer(a: string, bodyclass: string): void { _updateQA( a, null, - function() { + function () { if (bodyclass) { // when previewing document.body.className = bodyclass; @@ -204,7 +208,7 @@ export function _showAnswer(a: string, bodyclass: string): void { // avoid scrolling to the answer until images load allImagesLoaded().then(scrollToAnswer); }, - function() { + function () { /* noop */ }, ) @@ -263,6 +267,8 @@ document.addEventListener("focusout", (event) => { } }); +setupWheelZoom() + registerPackage("anki/reviewer", { // If you append a function to this each time the question or answer // is shown, it will be called before MathJax has been rendered. diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts new file mode 100644 index 00000000000..e184dad0bac --- /dev/null +++ b/ts/reviewer/zoom.ts @@ -0,0 +1,60 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/* eslint +@typescript-eslint/no-explicit-any: "off", + */ + +import { bridgeCommand } from "@tslib/bridgecommand"; + +const ZOOM_STEP = 0.1; +const DEFAULT_SCALE_FACTOR = 1; +const MAXIMUM_SCALE_FACTOR = 5; // Chromium defaults +const MINIMUM_SCALE_FACTOR = 0.25; + +let scaleFactor = 1.0; +let scaleTimer: null | number = null; + +export function triggerScaleStep(sign: number) { + scaleFactor = Math.min( + Math.max(scaleFactor * (1 + sign * ZOOM_STEP), MINIMUM_SCALE_FACTOR), + MAXIMUM_SCALE_FACTOR + ); + setScaleFactor(scaleFactor); +} + +export function setScaleFactor(newScaleFactor: number, store = true) { + const scaledContainer = document.body; + scaledContainer.style.transform = `scale(${newScaleFactor})`; + scaleFactor = newScaleFactor; + if (scaleTimer) { + clearTimeout(scaleTimer); + } + if (store) { + scaleTimer = setTimeout(() => { + storeScaleFactor(newScaleFactor); + }, 100); + } +} + +export function resetScaleFactor() { + setScaleFactor(DEFAULT_SCALE_FACTOR); +} + +export function setupWheelZoom() { + document.addEventListener( + "wheel", + (event) => { + if (!event.ctrlKey) { + return; + } + event.preventDefault(); + triggerScaleStep(-Math.sign(event.deltaY)); + }, + { passive: false } + ); +} + +function storeScaleFactor(scale: number) { + bridgeCommand(`scale:${scale}`); +} From 8f797d672295ce884937f9feb14be62533e94409 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 20:20:52 +0200 Subject: [PATCH 2/9] Set transform origin and slightly refactor --- ts/reviewer/reviewer.scss | 10 +++++++++- ts/reviewer/zoom.ts | 10 +++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ts/reviewer/reviewer.scss b/ts/reviewer/reviewer.scss index 1c5d6a7d4a0..e915c000a5c 100644 --- a/ts/reviewer/reviewer.scss +++ b/ts/reviewer/reviewer.scss @@ -11,6 +11,7 @@ hr { body { margin: 20px; overflow-wrap: break-word; + transform-origin: top; // default background setting to fit with toolbar background-size: cover; background-repeat: no-repeat; @@ -40,12 +41,15 @@ pre { #_flag { position: fixed; + [dir="ltr"] & { right: 10px; } + [dir="rtl"] & { left: 10px; } + top: 0; font-size: 30px; -webkit-text-stroke-width: 1px; @@ -54,12 +58,15 @@ pre { #_mark { position: fixed; + [dir="ltr"] & { left: 10px; } + [dir="rtl"] & { right: 10px; } + top: 0; font-size: 30px; color: yellow; @@ -126,6 +133,7 @@ button { .drawing { zoom: 50%; } + .nightMode img.drawing { filter: unquote("invert(1) hue-rotate(180deg)"); -} +} \ No newline at end of file diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts index e184dad0bac..637c654c8aa 100644 --- a/ts/reviewer/zoom.ts +++ b/ts/reviewer/zoom.ts @@ -12,7 +12,7 @@ const DEFAULT_SCALE_FACTOR = 1; const MAXIMUM_SCALE_FACTOR = 5; // Chromium defaults const MINIMUM_SCALE_FACTOR = 0.25; -let scaleFactor = 1.0; +let scaleFactor = DEFAULT_SCALE_FACTOR; let scaleTimer: null | number = null; export function triggerScaleStep(sign: number) { @@ -41,6 +41,10 @@ export function resetScaleFactor() { setScaleFactor(DEFAULT_SCALE_FACTOR); } +function storeScaleFactor(scale: number) { + bridgeCommand(`scale:${scale}`); +} + export function setupWheelZoom() { document.addEventListener( "wheel", @@ -54,7 +58,3 @@ export function setupWheelZoom() { { passive: false } ); } - -function storeScaleFactor(scale: number) { - bridgeCommand(`scale:${scale}`); -} From f17a15c886c0efd775e22b53708acb7c9a19d481 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 20:52:27 +0200 Subject: [PATCH 3/9] Display zoom info box --- ts/reviewer/reviewer.scss | 12 ++++++++++++ ts/reviewer/zoom.ts | 20 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/ts/reviewer/reviewer.scss b/ts/reviewer/reviewer.scss index e915c000a5c..8879e83d380 100644 --- a/ts/reviewer/reviewer.scss +++ b/ts/reviewer/reviewer.scss @@ -74,6 +74,18 @@ pre { -webkit-text-stroke-color: black; } +#_scaleinfo { + position: fixed; + display: none; + + + right: 10px; + top: 0; + font-size: 20px; + color: var(--fg-subtle); + background: var(--canvas-overlay); +} + #typeans { width: 100%; // https://anki.tenderapp.com/discussions/beta-testing/1854-using-margin-auto-causes-horizontal-scrollbar-on-typesomething diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts index 637c654c8aa..ad4a7eba78e 100644 --- a/ts/reviewer/zoom.ts +++ b/ts/reviewer/zoom.ts @@ -23,14 +23,15 @@ export function triggerScaleStep(sign: number) { setScaleFactor(scaleFactor); } -export function setScaleFactor(newScaleFactor: number, store = true) { +export function setScaleFactor(newScaleFactor: number, interactive = true) { const scaledContainer = document.body; scaledContainer.style.transform = `scale(${newScaleFactor})`; scaleFactor = newScaleFactor; if (scaleTimer) { clearTimeout(scaleTimer); } - if (store) { + if (interactive) { + displayScaleInfo(newScaleFactor) scaleTimer = setTimeout(() => { storeScaleFactor(newScaleFactor); }, 100); @@ -45,6 +46,21 @@ function storeScaleFactor(scale: number) { bridgeCommand(`scale:${scale}`); } +const scaleInfoId = "_scaleinfo" + +function displayScaleInfo(scaleFactor: number) { + console.log("displayscaleinfo") + let scaleInfoBox = document.getElementById(scaleInfoId) + if (!scaleInfoBox) { + scaleInfoBox = document.createElement("div") + document.documentElement.appendChild(scaleInfoBox) + scaleInfoBox.id = scaleInfoId + } + scaleInfoBox.innerHTML = `${Math.round(scaleFactor * 100)}%` + scaleInfoBox.style.display = "block"; + setTimeout(() => { scaleInfoBox!.style.display = "none"; }, 1000) +} + export function setupWheelZoom() { document.addEventListener( "wheel", From 84015b768b1cfde8eb1ff69871be9fba7676c2a4 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 21:34:17 +0200 Subject: [PATCH 4/9] Switch to preset zoom factors and change naming from scale to zoom Preset factors follow example of Chromium implementation --- qt/aqt/reviewer.py | 28 +++++++---- ts/reviewer/index.ts | 8 +-- ts/reviewer/reviewer.scss | 2 +- ts/reviewer/zoom.ts | 103 ++++++++++++++++++++++++-------------- 4 files changed, 87 insertions(+), 54 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 321d3128a02..1b1cc9e2acf 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -587,8 +587,8 @@ def _linkHandler(self, url: str) -> None: play_clicked_audio(url, self.card) elif url.startswith("updateToolbar"): self.mw.toolbarWeb.update_background_image() - elif url.startswith("scale"): - self.store_scale_factor(float(url.split(":")[1])) + elif url.startswith("zoom"): + self.store_zoom_step(int(url.split(":")[1])) elif url == "statesMutated": self._states_mutated = True else: @@ -1083,23 +1083,29 @@ def onReplayRecorded(self) -> None: # Zoom handling def zoom_in(self): - self.web.eval(f"anki.triggerScaleStep(1)") + self.web.eval(f"anki.triggerZoomStep(1)") def zoom_out(self): - self.web.eval(f"anki.triggerScaleStep(-1)") + self.web.eval(f"anki.triggerZoomStep(-1)") def reset_zoom(self): - self.web.eval(f"anki.setScaleFactor(1)") + self.web.eval(f"anki.resetZoom()") - def set_scale_factor(self, scale_factor: float, store: bool = True): - self.web.eval(f"anki.setScaleFactor({json.dumps(scale_factor)})") + def set_zoom_step(self, step: int, interactive: bool = True): + """Set predefined reviewer zoom step, cf. zoom.ts for indices - def store_scale_factor(self, scale_factor: float): - self.mw.pm.profile["lastReviewerScaleFactor"] = scale_factor + Args: + step: zoom step corresponding to predefined zoom factor index + interactive: controls zoom info box and zoom step persistence + """ + self.web.eval(f"anki.setZoomStep({json.dumps(step)})") + + def store_zoom_step(self, step: int): + self.mw.pm.profile["lastReviewerZoomStep"] = step def maybe_restore_scale_factor(self): - if scale_factor := self.mw.pm.profile.get("lastReviewerScaleFactor", None): - self.set_scale_factor(scale_factor, store=False) + if scale_factor := self.mw.pm.profile.get("lastReviewerZoomStep", None): + self.set_zoom_step(scale_factor, interactive=False) # legacy diff --git a/ts/reviewer/index.ts b/ts/reviewer/index.ts index 9a7c1b2a6ca..7eb9d2121de 100644 --- a/ts/reviewer/index.ts +++ b/ts/reviewer/index.ts @@ -11,14 +11,14 @@ export { default as $, default as jQuery } from "jquery/dist/jquery"; import { setupImageCloze } from "../image-occlusion/review"; import { mutateNextCardStates } from "./answering"; -import { resetScaleFactor, setScaleFactor, setupWheelZoom, triggerScaleStep } from "./zoom"; +import { resetZoom, setupWheelZoom, setZoomStep, triggerZoomStep } from "./zoom"; globalThis.anki = globalThis.anki || {}; globalThis.anki.mutateNextCardStates = mutateNextCardStates; globalThis.anki.setupImageCloze = setupImageCloze; -globalThis.anki.setScaleFactor = setScaleFactor -globalThis.anki.triggerScaleStep = triggerScaleStep -globalThis.anki.resetScaleFactor = resetScaleFactor +globalThis.anki.setZoomStep = setZoomStep +globalThis.anki.triggerZoomStep = triggerZoomStep +globalThis.anki.resetZoom = resetZoom import { bridgeCommand } from "@tslib/bridgecommand"; import { registerPackage } from "@tslib/runtime-require"; diff --git a/ts/reviewer/reviewer.scss b/ts/reviewer/reviewer.scss index 8879e83d380..c0a185ca13b 100644 --- a/ts/reviewer/reviewer.scss +++ b/ts/reviewer/reviewer.scss @@ -74,7 +74,7 @@ pre { -webkit-text-stroke-color: black; } -#_scaleinfo { +#_zoominfo { position: fixed; display: none; diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts index ad4a7eba78e..1c77781118c 100644 --- a/ts/reviewer/zoom.ts +++ b/ts/reviewer/zoom.ts @@ -7,61 +7,88 @@ import { bridgeCommand } from "@tslib/bridgecommand"; -const ZOOM_STEP = 0.1; -const DEFAULT_SCALE_FACTOR = 1; -const MAXIMUM_SCALE_FACTOR = 5; // Chromium defaults -const MINIMUM_SCALE_FACTOR = 0.25; +// Chromium defaults +const PRESET_ZOOM_FACTORS = [ + 0.25, + 1 / 3.0, + 0.5, + 2 / 3.0, + 0.75, + 0.8, + 0.9, + 1.0, + 1.1, + 1.25, + 1.5, + 1.75, + 2.0, + 2.5, + 3.0, + 4.0, + 5.0, +]; +const DEFAULT_ZOOM_STEP = 7; -let scaleFactor = DEFAULT_SCALE_FACTOR; -let scaleTimer: null | number = null; +let zoomStep = DEFAULT_ZOOM_STEP; +let zoomSaveTimer: number | null = null; -export function triggerScaleStep(sign: number) { - scaleFactor = Math.min( - Math.max(scaleFactor * (1 + sign * ZOOM_STEP), MINIMUM_SCALE_FACTOR), - MAXIMUM_SCALE_FACTOR - ); - setScaleFactor(scaleFactor); +export function triggerZoomStep(sign: number): void { + const step = zoomStep + sign + if (step < 0 || step > PRESET_ZOOM_FACTORS.length) { + return + } + + setZoomStep(step); } -export function setScaleFactor(newScaleFactor: number, interactive = true) { - const scaledContainer = document.body; - scaledContainer.style.transform = `scale(${newScaleFactor})`; - scaleFactor = newScaleFactor; - if (scaleTimer) { - clearTimeout(scaleTimer); +export function setZoomStep(step: number, interactive = true): void { + const zoomedContainer = document.body; + const zoomFactor = PRESET_ZOOM_FACTORS[step] + if (zoomFactor === undefined) { + return + } + zoomedContainer.style.transform = `scale(${zoomFactor})`; + zoomStep = step + if (zoomSaveTimer) { + clearTimeout(zoomSaveTimer); } if (interactive) { - displayScaleInfo(newScaleFactor) - scaleTimer = setTimeout(() => { - storeScaleFactor(newScaleFactor); + displayZoomInfo(zoomFactor); + zoomSaveTimer = setTimeout(() => { + storeZoomStep(step); }, 100); } } -export function resetScaleFactor() { - setScaleFactor(DEFAULT_SCALE_FACTOR); +export function resetZoom(): void { + setZoomStep(DEFAULT_ZOOM_STEP) } -function storeScaleFactor(scale: number) { - bridgeCommand(`scale:${scale}`); +function storeZoomStep(step: number) { + bridgeCommand(`zoom:${step}`); } -const scaleInfoId = "_scaleinfo" +const zoomInfoId = "_zoominfo"; +let zoomInfoTimer: number | null = null; -function displayScaleInfo(scaleFactor: number) { - console.log("displayscaleinfo") - let scaleInfoBox = document.getElementById(scaleInfoId) - if (!scaleInfoBox) { - scaleInfoBox = document.createElement("div") - document.documentElement.appendChild(scaleInfoBox) - scaleInfoBox.id = scaleInfoId +function displayZoomInfo(zoomFactor: number) { + let zoomInfoBox = document.getElementById(zoomInfoId); + if (!zoomInfoBox) { + zoomInfoBox = document.createElement("div"); + document.documentElement.appendChild(zoomInfoBox); + zoomInfoBox.id = zoomInfoId; + } + if (zoomInfoTimer) { + clearTimeout(zoomInfoTimer) } - scaleInfoBox.innerHTML = `${Math.round(scaleFactor * 100)}%` - scaleInfoBox.style.display = "block"; - setTimeout(() => { scaleInfoBox!.style.display = "none"; }, 1000) + zoomInfoBox.innerHTML = `${Math.round(zoomFactor * 100)}%`; + zoomInfoBox.style.display = "block"; + zoomInfoTimer = setTimeout(() => { + zoomInfoBox!.style.display = "none"; + }, 1000); } -export function setupWheelZoom() { +export function setupWheelZoom(): void { document.addEventListener( "wheel", (event) => { @@ -69,7 +96,7 @@ export function setupWheelZoom() { return; } event.preventDefault(); - triggerScaleStep(-Math.sign(event.deltaY)); + triggerZoomStep(-Math.sign(event.deltaY)); }, { passive: false } ); From 04409206e5c192350c90fef750f90fe2714b15c7 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 22:05:26 +0200 Subject: [PATCH 5/9] Restore layout-changing zoom as default, making new zoom optional While scale-transforming the document body works well for images, it does not cover text use cases well: Scale-transform zooming preserves the document layout and thus increases font sizes without triggering text reflow. This makes it ill-suited for common use cases where users might want to boost the readability of a card at larger distances. With this commit both zoom modes are now active in parallel, with users being able to trigger the scale-transform mode for any existing zoom action by holding down Ctrl+Shift. --- qt/aqt/main.py | 24 +++++++++++++++--------- ts/reviewer/zoom.ts | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 1a9dff939d4..ae0cfafd6f3 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1391,23 +1391,29 @@ def show_menubar(self) -> None: self.form.menubar.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) self.form.menubar.setMinimumSize(0, 0) + # Triggering a zoom action with Shift held down changes the zoom level without + # affecting layout sizing, thus allowing users to zoom into images + def zoom_in(self) -> None: - if self.state != "review": - self.web.setZoomFactor(self.web.zoomFactor() + 0.1) - else: + if self.state == "review" and bool( + self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier + ): self.reviewer.zoom_in() + else: + self.web.setZoomFactor(self.web.zoomFactor() + 0.1) def zoom_out(self) -> None: - if self.state != "review": - self.web.setZoomFactor(self.web.zoomFactor() - 0.1) - else: + if self.state == "review" and bool( + self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier + ): self.reviewer.zoom_out() + else: + self.web.setZoomFactor(self.web.zoomFactor() - 0.1) def reset_zoom(self) -> None: - if self.state != "review": - self.web.setZoomFactor(1) - else: + if self.state == "review": self.reviewer.reset_zoom() + self.web.setZoomFactor(1) # Auto update ########################################################################## diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts index 1c77781118c..1ebaf2698fa 100644 --- a/ts/reviewer/zoom.ts +++ b/ts/reviewer/zoom.ts @@ -92,7 +92,7 @@ export function setupWheelZoom(): void { document.addEventListener( "wheel", (event) => { - if (!event.ctrlKey) { + if (!event.ctrlKey || !event.shiftKey) { return; } event.preventDefault(); From a9cda91178532834c2b3818c0377a34be95599d3 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 22:10:23 +0200 Subject: [PATCH 6/9] Refactor and fix max index calculation --- ts/reviewer/zoom.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts index 1ebaf2698fa..ce786312434 100644 --- a/ts/reviewer/zoom.ts +++ b/ts/reviewer/zoom.ts @@ -34,7 +34,7 @@ let zoomSaveTimer: number | null = null; export function triggerZoomStep(sign: number): void { const step = zoomStep + sign - if (step < 0 || step > PRESET_ZOOM_FACTORS.length) { + if (step < 0 || step > (PRESET_ZOOM_FACTORS.length - 1)) { return } @@ -92,7 +92,7 @@ export function setupWheelZoom(): void { document.addEventListener( "wheel", (event) => { - if (!event.ctrlKey || !event.shiftKey) { + if (!(event.ctrlKey && event.shiftKey)) { return; } event.preventDefault(); From b872ce10ff1f3234651be45112fa88274a65b4a8 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 22:26:13 +0200 Subject: [PATCH 7/9] Prevent info pop-up on restoring saved zoom level --- qt/aqt/reviewer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 1b1cc9e2acf..36a91878385 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1098,7 +1098,9 @@ def set_zoom_step(self, step: int, interactive: bool = True): step: zoom step corresponding to predefined zoom factor index interactive: controls zoom info box and zoom step persistence """ - self.web.eval(f"anki.setZoomStep({json.dumps(step)})") + self.web.eval( + f"anki.setZoomStep({json.dumps(step)}, {json.dumps(interactive)})" + ) def store_zoom_step(self, step: int): self.mw.pm.profile["lastReviewerZoomStep"] = step From 2a0a3288a8996a716df34ec285d67a5dcbff0d54 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 23:04:25 +0200 Subject: [PATCH 8/9] Drop unneccessary f-strings --- qt/aqt/reviewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 36a91878385..c1da7328276 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1083,13 +1083,13 @@ def onReplayRecorded(self) -> None: # Zoom handling def zoom_in(self): - self.web.eval(f"anki.triggerZoomStep(1)") + self.web.eval("anki.triggerZoomStep(1)") def zoom_out(self): - self.web.eval(f"anki.triggerZoomStep(-1)") + self.web.eval("anki.triggerZoomStep(-1)") def reset_zoom(self): - self.web.eval(f"anki.resetZoom()") + self.web.eval("anki.resetZoom()") def set_zoom_step(self, step: int, interactive: bool = True): """Set predefined reviewer zoom step, cf. zoom.ts for indices From d8191c73650c9d22d2683454accffc6c23d4e628 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 25 Jul 2023 23:21:36 +0200 Subject: [PATCH 9/9] Satisfy formatter --- ts/reviewer/index.ts | 16 ++++++++-------- ts/reviewer/reviewer.scss | 3 +-- ts/reviewer/zoom.ts | 16 ++++++++-------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ts/reviewer/index.ts b/ts/reviewer/index.ts index 7eb9d2121de..eb59d2ccf5d 100644 --- a/ts/reviewer/index.ts +++ b/ts/reviewer/index.ts @@ -16,9 +16,9 @@ import { resetZoom, setupWheelZoom, setZoomStep, triggerZoomStep } from "./zoom" globalThis.anki = globalThis.anki || {}; globalThis.anki.mutateNextCardStates = mutateNextCardStates; globalThis.anki.setupImageCloze = setupImageCloze; -globalThis.anki.setZoomStep = setZoomStep -globalThis.anki.triggerZoomStep = triggerZoomStep -globalThis.anki.resetZoom = resetZoom +globalThis.anki.setZoomStep = setZoomStep; +globalThis.anki.triggerZoomStep = triggerZoomStep; +globalThis.anki.resetZoom = resetZoom; import { bridgeCommand } from "@tslib/bridgecommand"; import { registerPackage } from "@tslib/runtime-require"; @@ -171,13 +171,13 @@ export function _showQuestion(q: string, a: string, bodyclass: string): void { _updateQA( q, null, - function () { + function() { // return to top of window window.scrollTo(0, 0); document.body.className = bodyclass; }, - function () { + function() { // focus typing area if visible typeans = document.getElementById("typeans") as HTMLInputElement; if (typeans) { @@ -199,7 +199,7 @@ export function _showAnswer(a: string, bodyclass: string): void { _updateQA( a, null, - function () { + function() { if (bodyclass) { // when previewing document.body.className = bodyclass; @@ -208,7 +208,7 @@ export function _showAnswer(a: string, bodyclass: string): void { // avoid scrolling to the answer until images load allImagesLoaded().then(scrollToAnswer); }, - function () { + function() { /* noop */ }, ) @@ -267,7 +267,7 @@ document.addEventListener("focusout", (event) => { } }); -setupWheelZoom() +setupWheelZoom(); registerPackage("anki/reviewer", { // If you append a function to this each time the question or answer diff --git a/ts/reviewer/reviewer.scss b/ts/reviewer/reviewer.scss index c0a185ca13b..1103127af55 100644 --- a/ts/reviewer/reviewer.scss +++ b/ts/reviewer/reviewer.scss @@ -78,7 +78,6 @@ pre { position: fixed; display: none; - right: 10px; top: 0; font-size: 20px; @@ -148,4 +147,4 @@ button { .nightMode img.drawing { filter: unquote("invert(1) hue-rotate(180deg)"); -} \ No newline at end of file +} diff --git a/ts/reviewer/zoom.ts b/ts/reviewer/zoom.ts index ce786312434..ecf71255c02 100644 --- a/ts/reviewer/zoom.ts +++ b/ts/reviewer/zoom.ts @@ -33,9 +33,9 @@ let zoomStep = DEFAULT_ZOOM_STEP; let zoomSaveTimer: number | null = null; export function triggerZoomStep(sign: number): void { - const step = zoomStep + sign + const step = zoomStep + sign; if (step < 0 || step > (PRESET_ZOOM_FACTORS.length - 1)) { - return + return; } setZoomStep(step); @@ -43,12 +43,12 @@ export function triggerZoomStep(sign: number): void { export function setZoomStep(step: number, interactive = true): void { const zoomedContainer = document.body; - const zoomFactor = PRESET_ZOOM_FACTORS[step] + const zoomFactor = PRESET_ZOOM_FACTORS[step]; if (zoomFactor === undefined) { - return + return; } zoomedContainer.style.transform = `scale(${zoomFactor})`; - zoomStep = step + zoomStep = step; if (zoomSaveTimer) { clearTimeout(zoomSaveTimer); } @@ -61,7 +61,7 @@ export function setZoomStep(step: number, interactive = true): void { } export function resetZoom(): void { - setZoomStep(DEFAULT_ZOOM_STEP) + setZoomStep(DEFAULT_ZOOM_STEP); } function storeZoomStep(step: number) { @@ -79,7 +79,7 @@ function displayZoomInfo(zoomFactor: number) { zoomInfoBox.id = zoomInfoId; } if (zoomInfoTimer) { - clearTimeout(zoomInfoTimer) + clearTimeout(zoomInfoTimer); } zoomInfoBox.innerHTML = `${Math.round(zoomFactor * 100)}%`; zoomInfoBox.style.display = "block"; @@ -98,6 +98,6 @@ export function setupWheelZoom(): void { event.preventDefault(); triggerZoomStep(-Math.sign(event.deltaY)); }, - { passive: false } + { passive: false }, ); }