Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new Reviewer zoom mode for images #2591

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
36 changes: 27 additions & 9 deletions qt/aqt/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -1397,6 +1391,30 @@ 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" 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" 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.reviewer.reset_zoom()
self.web.setZoomFactor(1)

# Auto update
##########################################################################

Expand Down
32 changes: 32 additions & 0 deletions qt/aqt/reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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("zoom"):
self.store_zoom_step(int(url.split(":")[1]))
elif url == "statesMutated":
self._states_mutated = True
else:
Expand Down Expand Up @@ -1077,6 +1080,35 @@ def onReplayRecorded(self) -> None:
return
av_player.play_file(self._recordedAudio)

# Zoom handling

def zoom_in(self):
self.web.eval("anki.triggerZoomStep(1)")

def zoom_out(self):
self.web.eval("anki.triggerZoomStep(-1)")

def reset_zoom(self):
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

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)}, {json.dumps(interactive)})"
)

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("lastReviewerZoomStep", None):
self.set_zoom_step(scale_factor, interactive=False)

# legacy

onBuryCard = bury_current_card
Expand Down
6 changes: 6 additions & 0 deletions ts/reviewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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;

import { bridgeCommand } from "@tslib/bridgecommand";
import { registerPackage } from "@tslib/runtime-require";
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions ts/reviewer/reviewer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -54,19 +58,33 @@ pre {

#_mark {
position: fixed;

[dir="ltr"] & {
left: 10px;
}

[dir="rtl"] & {
right: 10px;
}

top: 0;
font-size: 30px;
color: yellow;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: black;
}

#_zoominfo {
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
Expand Down Expand Up @@ -126,6 +144,7 @@ button {
.drawing {
zoom: 50%;
}

.nightMode img.drawing {
filter: unquote("invert(1) hue-rotate(180deg)");
}
103 changes: 103 additions & 0 deletions ts/reviewer/zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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";

// 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 zoomStep = DEFAULT_ZOOM_STEP;
let zoomSaveTimer: number | null = null;

export function triggerZoomStep(sign: number): void {
const step = zoomStep + sign;
if (step < 0 || step > (PRESET_ZOOM_FACTORS.length - 1)) {
return;
}

setZoomStep(step);
}

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) {
displayZoomInfo(zoomFactor);
zoomSaveTimer = setTimeout(() => {
storeZoomStep(step);
}, 100);
}
}

export function resetZoom(): void {
setZoomStep(DEFAULT_ZOOM_STEP);
}

function storeZoomStep(step: number) {
bridgeCommand(`zoom:${step}`);
}

const zoomInfoId = "_zoominfo";
let zoomInfoTimer: number | null = null;

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);
}
zoomInfoBox.innerHTML = `${Math.round(zoomFactor * 100)}%`;
zoomInfoBox.style.display = "block";
zoomInfoTimer = setTimeout(() => {
zoomInfoBox!.style.display = "none";
}, 1000);
}

export function setupWheelZoom(): void {
document.addEventListener(
"wheel",
(event) => {
if (!(event.ctrlKey && event.shiftKey)) {
return;
}
event.preventDefault();
triggerZoomStep(-Math.sign(event.deltaY));
},
{ passive: false },
);
}