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
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-12-20 10:19:03 +01:00
|
|
|
import base64
|
2016-12-15 09:14:47 +01:00
|
|
|
import html
|
2019-12-22 13:59:24 +01:00
|
|
|
import itertools
|
2019-12-20 10:19:03 +01:00
|
|
|
import json
|
2017-01-08 13:52:33 +01:00
|
|
|
import mimetypes
|
2019-12-20 10:19:03 +01:00
|
|
|
import re
|
|
|
|
import urllib.error
|
|
|
|
import urllib.parse
|
|
|
|
import urllib.request
|
|
|
|
import warnings
|
2022-01-12 05:51:43 +01:00
|
|
|
from enum import Enum
|
2021-01-10 01:10:23 +01:00
|
|
|
from random import randrange
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Any, Callable, Match, cast
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-07-29 00:19:16 +02:00
|
|
|
import bs4
|
2019-12-20 10:19:03 +01:00
|
|
|
import requests
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
|
|
|
|
import aqt
|
2022-02-13 04:40:47 +01:00
|
|
|
import aqt.forms
|
|
|
|
import aqt.operations
|
2020-01-02 10:43:19 +01:00
|
|
|
import aqt.sound
|
2021-08-20 03:37:11 +02:00
|
|
|
from anki._legacy import deprecated
|
2020-03-04 17:41:26 +01:00
|
|
|
from anki.cards import Card
|
2021-03-10 09:20:37 +01:00
|
|
|
from anki.collection import Config, SearchNode
|
2021-01-29 02:15:08 +01:00
|
|
|
from anki.consts import MODEL_CLOZE
|
2020-01-15 04:49:26 +01:00
|
|
|
from anki.hooks import runFilter
|
2020-01-19 02:33:27 +01:00
|
|
|
from anki.httpclient import HttpClient
|
2021-06-12 17:35:40 +02:00
|
|
|
from anki.notes import Note, NoteFieldsCheckResult
|
2021-11-25 00:06:16 +01:00
|
|
|
from anki.utils import checksum, is_lin, is_win, namedtmp
|
2021-02-05 09:50:01 +01:00
|
|
|
from aqt import AnkiQt, colors, gui_hooks
|
2021-05-08 08:20:10 +02:00
|
|
|
from aqt.operations import QueryOp
|
2021-04-03 08:26:10 +02:00
|
|
|
from aqt.operations.note import update_note
|
2021-06-21 15:48:22 +02:00
|
|
|
from aqt.operations.notetype import update_notetype_legacy
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.qt import *
|
2020-12-16 10:09:45 +01:00
|
|
|
from aqt.sound import av_player
|
2020-01-26 09:47:28 +01:00
|
|
|
from aqt.theme import theme_manager
|
2019-12-23 01:34:10 +01:00
|
|
|
from aqt.utils import (
|
2021-01-25 14:45:47 +01:00
|
|
|
HelpPage,
|
2021-03-17 05:51:59 +01:00
|
|
|
KeyboardModifiersPressed,
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button,
|
2019-12-23 01:34:10 +01:00
|
|
|
getFile,
|
|
|
|
openHelp,
|
|
|
|
qtMenuShortcutWorkaround,
|
2020-05-27 12:04:00 +02:00
|
|
|
restoreGeom,
|
|
|
|
saveGeom,
|
2019-12-23 01:34:10 +01:00
|
|
|
shortcut,
|
|
|
|
showInfo,
|
|
|
|
showWarning,
|
|
|
|
tooltip,
|
2020-11-17 08:42:43 +01:00
|
|
|
tr,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2023-02-10 05:53:11 +01:00
|
|
|
from aqt.webview import AnkiWebView, AnkiWebViewKind
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-10-02 23:17:38 +02:00
|
|
|
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico")
|
2019-12-23 01:34:10 +01:00
|
|
|
audio = (
|
2021-02-06 06:10:49 +01:00
|
|
|
"3gp",
|
2021-12-08 12:25:47 +01:00
|
|
|
"aac",
|
2021-02-06 06:10:49 +01:00
|
|
|
"avi",
|
2019-12-23 01:34:10 +01:00
|
|
|
"flac",
|
2021-02-06 06:10:49 +01:00
|
|
|
"flv",
|
|
|
|
"m4a",
|
|
|
|
"mkv",
|
2019-12-23 01:34:10 +01:00
|
|
|
"mov",
|
2021-02-06 06:10:49 +01:00
|
|
|
"mp3",
|
|
|
|
"mp4",
|
2019-12-23 01:34:10 +01:00
|
|
|
"mpeg",
|
2020-07-29 14:53:21 +02:00
|
|
|
"mpg",
|
2021-02-06 06:10:49 +01:00
|
|
|
"oga",
|
|
|
|
"ogg",
|
2020-07-29 14:53:21 +02:00
|
|
|
"ogv",
|
2021-02-06 06:10:49 +01:00
|
|
|
"ogx",
|
|
|
|
"opus",
|
|
|
|
"spx",
|
|
|
|
"swf",
|
|
|
|
"wav",
|
|
|
|
"webm",
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-16 09:30:54 +01:00
|
|
|
|
2022-01-12 05:51:43 +01:00
|
|
|
class EditorMode(Enum):
|
|
|
|
ADD_CARDS = 0
|
|
|
|
EDIT_CURRENT = 1
|
|
|
|
BROWSER = 2
|
|
|
|
|
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class Editor:
|
2021-03-16 07:39:41 +01:00
|
|
|
"""The screen that embeds an editing widget should listen for changes via
|
|
|
|
the `operation_did_execute` hook, and call set_note() when the editor needs
|
|
|
|
redrawing.
|
|
|
|
|
|
|
|
The editor will cause that hook to be fired when it saves changes. To avoid
|
2021-04-06 02:14:11 +02:00
|
|
|
an unwanted refresh, the parent widget should check if handler
|
2021-04-05 05:43:09 +02:00
|
|
|
corresponds to this editor instance, and ignore the change if it does.
|
2021-03-16 07:39:41 +01:00
|
|
|
"""
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def __init__(
|
2022-01-12 05:51:43 +01:00
|
|
|
self,
|
|
|
|
mw: AnkiQt,
|
|
|
|
widget: QWidget,
|
|
|
|
parentWindow: QWidget,
|
|
|
|
addMode: bool | None = None,
|
|
|
|
*,
|
2022-01-12 06:01:43 +01:00
|
|
|
editor_mode: EditorMode = EditorMode.EDIT_CURRENT,
|
2021-02-01 08:28:35 +01:00
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw = mw
|
|
|
|
self.widget = widget
|
|
|
|
self.parentWindow = parentWindow
|
2021-10-03 10:59:42 +02:00
|
|
|
self.note: Note | None = None
|
2022-01-12 05:51:43 +01:00
|
|
|
# legacy argument provided?
|
|
|
|
if addMode is not None:
|
2022-01-12 06:01:43 +01:00
|
|
|
editor_mode = EditorMode.ADD_CARDS if addMode else EditorMode.EDIT_CURRENT
|
|
|
|
self.addMode = editor_mode is EditorMode.ADD_CARDS
|
|
|
|
self.editorMode = editor_mode
|
2021-10-03 10:59:42 +02:00
|
|
|
self.currentField: int | None = None
|
2021-09-13 07:31:24 +02:00
|
|
|
# Similar to currentField, but not set to None on a blur. May be
|
|
|
|
# outside the bounds of the current notetype.
|
2021-10-03 10:59:42 +02:00
|
|
|
self.last_field_index: int | None = None
|
2012-12-21 08:51:59 +01:00
|
|
|
# current card, for card layout
|
2021-10-03 10:59:42 +02:00
|
|
|
self.card: Card | None = None
|
2022-02-18 10:00:12 +01:00
|
|
|
self._init_links()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupOuter()
|
2022-08-18 04:06:06 +02:00
|
|
|
self.add_webview()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupWeb()
|
2017-10-11 20:51:26 +02:00
|
|
|
self.setupShortcuts()
|
2020-03-22 17:15:47 +01:00
|
|
|
gui_hooks.editor_did_init(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Initial setup
|
|
|
|
############################################################
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def setupOuter(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
l = QVBoxLayout()
|
2019-12-23 01:34:10 +01:00
|
|
|
l.setContentsMargins(0, 0, 0, 0)
|
2012-12-21 08:51:59 +01:00
|
|
|
l.setSpacing(0)
|
|
|
|
self.widget.setLayout(l)
|
|
|
|
self.outerLayout = l
|
|
|
|
|
2022-08-18 04:06:06 +02:00
|
|
|
def add_webview(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web = EditorWebView(self.widget, self)
|
2020-02-08 23:59:29 +01:00
|
|
|
self.web.set_bridge_command(self.onBridgeCmd, self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.outerLayout.addWidget(self.web, 1)
|
2016-06-22 06:52:17 +02:00
|
|
|
|
2022-08-18 04:06:06 +02:00
|
|
|
def setupWeb(self) -> None:
|
2022-01-12 05:51:43 +01:00
|
|
|
if self.editorMode == EditorMode.ADD_CARDS:
|
|
|
|
file = "note_creator"
|
|
|
|
elif self.editorMode == EditorMode.BROWSER:
|
|
|
|
file = "browser_editor"
|
|
|
|
else:
|
|
|
|
file = "reviewer_editor"
|
|
|
|
|
2017-06-22 10:01:47 +02:00
|
|
|
# then load page
|
2019-12-23 01:34:10 +01:00
|
|
|
self.web.stdHtml(
|
2021-12-13 05:06:12 +01:00
|
|
|
"",
|
2022-01-12 05:51:43 +01:00
|
|
|
css=[f"css/{file}.css"],
|
2022-05-13 05:23:35 +02:00
|
|
|
js=[
|
|
|
|
"js/mathjax.js",
|
|
|
|
f"js/{file}.js",
|
|
|
|
],
|
2020-02-12 22:00:13 +01:00
|
|
|
context=self,
|
2021-10-31 00:29:22 +02:00
|
|
|
default_css=False,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
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.web.show()
|
2021-04-09 20:55:49 +02:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
lefttopbtns: list[str] = []
|
2021-04-09 20:55:49 +02:00
|
|
|
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
|
|
|
|
|
|
|
lefttopbtns_defs = [
|
2022-01-12 05:51:43 +01:00
|
|
|
f"uiPromise.then((noteEditor) => noteEditor.toolbar.notetypeButtons.appendButton({{ component: editorToolbar.Raw, props: {{ html: {json.dumps(button)} }} }}, -1));"
|
2021-04-09 20:55:49 +02:00
|
|
|
for button in lefttopbtns
|
|
|
|
]
|
|
|
|
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
righttopbtns: list[str] = []
|
2021-04-09 20:55:49 +02:00
|
|
|
gui_hooks.editor_did_init_buttons(righttopbtns, self)
|
|
|
|
# legacy filter
|
|
|
|
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
|
|
|
|
2021-05-07 14:22:06 +02:00
|
|
|
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
|
2021-04-09 20:55:49 +02:00
|
|
|
righttopbtns_js = (
|
|
|
|
f"""
|
2022-02-03 05:52:11 +01:00
|
|
|
require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].toolbar.toolbar.append({{
|
2021-05-06 20:29:55 +02:00
|
|
|
component: editorToolbar.AddonButtons,
|
|
|
|
id: "addons",
|
|
|
|
props: {{ buttons: [ {righttopbtns_defs} ] }},
|
|
|
|
}}));
|
2021-04-09 20:55:49 +02:00
|
|
|
"""
|
2021-05-06 20:29:55 +02:00
|
|
|
if len(righttopbtns) > 0
|
2021-04-09 20:55:49 +02:00
|
|
|
else ""
|
|
|
|
)
|
|
|
|
|
2021-05-06 20:29:55 +02:00
|
|
|
self.web.eval(f"{lefttopbtns_js} {righttopbtns_js}")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Top buttons
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def resourceToData(self, path: str) -> str:
|
2017-01-08 13:52:33 +01:00
|
|
|
"""Convert a file (specified by a path) into a data URI."""
|
2017-01-14 21:16:50 +01:00
|
|
|
if not os.path.exists(path):
|
|
|
|
raise FileNotFoundError
|
2017-01-08 13:52:33 +01:00
|
|
|
mime, _ = mimetypes.guess_type(path)
|
2019-12-23 01:34:10 +01:00
|
|
|
with open(path, "rb") as fp:
|
2017-01-08 13:52:33 +01:00
|
|
|
data = fp.read()
|
2019-12-23 01:34:10 +01:00
|
|
|
data64 = b"".join(base64.encodebytes(data).splitlines())
|
2021-02-11 01:09:06 +01:00
|
|
|
return f"data:{mime};base64,{data64.decode('ascii')}"
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
def addButton(
|
|
|
|
self,
|
2021-10-03 10:59:42 +02:00
|
|
|
icon: str | None,
|
2019-12-23 01:34:10 +01:00
|
|
|
cmd: str,
|
2021-10-03 10:59:42 +02:00
|
|
|
func: Callable[[Editor], None],
|
2019-12-23 01:34:10 +01:00
|
|
|
tip: str = "",
|
|
|
|
label: str = "",
|
|
|
|
id: str = None,
|
|
|
|
toggleable: bool = False,
|
|
|
|
keys: str = None,
|
|
|
|
disables: bool = True,
|
2020-10-04 23:03:37 +02:00
|
|
|
rightside: bool = True,
|
2021-02-01 08:28:35 +01:00
|
|
|
) -> str:
|
2017-08-23 23:53:57 +02:00
|
|
|
"""Assign func to bridge cmd, register shortcut, return button"""
|
2020-04-26 17:01:23 +02:00
|
|
|
if func:
|
2017-08-23 23:53:57 +02:00
|
|
|
self._links[cmd] = func
|
2021-01-10 01:44:08 +01:00
|
|
|
|
2021-01-10 13:38:20 +01:00
|
|
|
if keys:
|
2021-01-10 01:10:23 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def on_activated() -> None:
|
2021-01-10 13:38:20 +01:00
|
|
|
func(self)
|
2021-01-10 01:10:23 +01:00
|
|
|
|
2021-01-10 13:38:20 +01:00
|
|
|
if toggleable:
|
|
|
|
# generate a random id for triggering toggle
|
|
|
|
id = id or str(randrange(1_000_000))
|
2021-01-10 01:44:08 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def on_hotkey() -> None:
|
2021-01-10 13:38:20 +01:00
|
|
|
on_activated()
|
2023-06-05 04:43:50 +02:00
|
|
|
self.web.eval(
|
|
|
|
f'toggleEditorButton(document.getElementById("{id}"));'
|
|
|
|
)
|
2021-01-10 13:38:20 +01:00
|
|
|
|
|
|
|
else:
|
|
|
|
on_hotkey = on_activated
|
|
|
|
|
|
|
|
QShortcut( # type: ignore
|
|
|
|
QKeySequence(keys),
|
|
|
|
self.widget,
|
|
|
|
activated=on_hotkey,
|
|
|
|
)
|
2021-01-10 01:10:23 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
btn = self._addButton(
|
|
|
|
icon,
|
|
|
|
cmd,
|
|
|
|
tip=tip,
|
|
|
|
label=label,
|
|
|
|
id=id,
|
|
|
|
toggleable=toggleable,
|
|
|
|
disables=disables,
|
2020-10-04 23:03:37 +02:00
|
|
|
rightside=rightside,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2017-08-23 23:53:57 +02:00
|
|
|
return btn
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
def _addButton(
|
|
|
|
self,
|
2021-10-03 10:59:42 +02:00
|
|
|
icon: str | None,
|
2019-12-23 01:34:10 +01:00
|
|
|
cmd: str,
|
|
|
|
tip: str = "",
|
|
|
|
label: str = "",
|
2021-10-03 10:59:42 +02:00
|
|
|
id: str | None = None,
|
2019-12-23 01:34:10 +01:00
|
|
|
toggleable: bool = False,
|
|
|
|
disables: bool = True,
|
2020-10-04 23:03:37 +02:00
|
|
|
rightside: bool = True,
|
2020-08-11 22:56:58 +02:00
|
|
|
) -> str:
|
2017-08-23 23:48:08 +02:00
|
|
|
if icon:
|
2019-01-26 20:42:56 +01:00
|
|
|
if icon.startswith("qrc:/"):
|
|
|
|
iconstr = icon
|
|
|
|
elif os.path.isabs(icon):
|
2017-08-23 23:48:08 +02:00
|
|
|
iconstr = self.resourceToData(icon)
|
|
|
|
else:
|
2021-02-11 00:37:38 +01:00
|
|
|
iconstr = f"/_anki/imgs/{icon}.png"
|
2021-03-01 13:00:12 +01:00
|
|
|
imgelm = f"""<img class="topbut" src="{iconstr}">"""
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2017-08-23 23:48:08 +02:00
|
|
|
imgelm = ""
|
|
|
|
if label or not imgelm:
|
2021-02-27 17:22:55 +01:00
|
|
|
labelelm = label or cmd
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2017-08-23 23:48:08 +02:00
|
|
|
labelelm = ""
|
2017-01-06 16:40:10 +01:00
|
|
|
if id:
|
2021-02-11 00:37:38 +01:00
|
|
|
idstr = f"id={id}"
|
2017-01-06 16:40:10 +01:00
|
|
|
else:
|
|
|
|
idstr = ""
|
2017-01-08 14:33:45 +01:00
|
|
|
if toggleable:
|
2019-12-23 01:34:10 +01:00
|
|
|
toggleScript = "toggleEditorButton(this);"
|
2017-01-08 14:33:45 +01:00
|
|
|
else:
|
2019-12-23 01:34:10 +01:00
|
|
|
toggleScript = ""
|
2017-08-17 05:51:54 +02:00
|
|
|
tip = shortcut(tip)
|
2020-10-04 23:03:37 +02:00
|
|
|
if rightside:
|
|
|
|
class_ = "linkb"
|
|
|
|
else:
|
2021-03-01 13:00:12 +01:00
|
|
|
class_ = "rounded"
|
2017-12-08 12:06:16 +01:00
|
|
|
if not disables:
|
2020-10-04 22:51:34 +02:00
|
|
|
class_ += " perm"
|
2021-01-22 12:14:20 +01:00
|
|
|
return """<button tabindex=-1
|
2020-03-23 07:55:48 +01:00
|
|
|
{id}
|
2020-10-04 22:51:34 +02:00
|
|
|
class="{class_}"
|
2020-03-23 07:55:48 +01:00
|
|
|
type="button"
|
|
|
|
title="{tip}"
|
|
|
|
onclick="pycmd('{cmd}');{togglesc}return false;"
|
2021-04-09 20:55:49 +02:00
|
|
|
onmousedown="window.event.preventDefault();"
|
2020-03-23 07:55:48 +01:00
|
|
|
>
|
|
|
|
{imgelm}
|
|
|
|
{labelelm}
|
|
|
|
</button>""".format(
|
|
|
|
imgelm=imgelm,
|
|
|
|
cmd=cmd,
|
|
|
|
tip=tip,
|
|
|
|
labelelm=labelelm,
|
|
|
|
id=idstr,
|
|
|
|
togglesc=toggleScript,
|
2020-10-04 22:51:34 +02:00
|
|
|
class_=class_,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def setupShortcuts(self) -> None:
|
2017-12-04 03:53:28 +01:00
|
|
|
# if a third element is provided, enable shortcut even when no field selected
|
2021-10-03 10:59:42 +02:00
|
|
|
cuts: list[tuple] = []
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.editor_did_init_shortcuts(cuts, self)
|
2017-12-04 03:53:28 +01:00
|
|
|
for row in cuts:
|
|
|
|
if len(row) == 2:
|
2019-12-23 02:31:42 +01:00
|
|
|
keys, fn = row # pylint: disable=unbalanced-tuple-unpacking
|
2017-12-04 03:53:28 +01:00
|
|
|
fn = self._addFocusCheck(fn)
|
|
|
|
else:
|
|
|
|
keys, fn, _ = row
|
2020-01-15 22:53:12 +01:00
|
|
|
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
2016-06-22 06:52:17 +02:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def _addFocusCheck(self, fn: Callable) -> Callable:
|
|
|
|
def checkFocus() -> None:
|
2017-12-04 03:53:28 +01:00
|
|
|
if self.currentField is None:
|
|
|
|
return
|
|
|
|
fn()
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-12-04 03:53:28 +01:00
|
|
|
return checkFocus
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onFields(self) -> None:
|
2021-03-16 13:40:37 +01:00
|
|
|
self.call_after_note_saved(self._onFields)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def _onFields(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.fields import FieldDialog
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-06-27 04:12:23 +02:00
|
|
|
FieldDialog(self.mw, self.note.note_type(), parent=self.parentWindow)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onCardLayout(self) -> None:
|
2021-03-16 13:40:37 +01:00
|
|
|
self.call_after_note_saved(self._onCardLayout)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def _onCardLayout(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.clayout import CardLayout
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.card:
|
|
|
|
ord = self.card.ord
|
|
|
|
else:
|
|
|
|
ord = 0
|
2019-12-23 01:34:10 +01:00
|
|
|
CardLayout(
|
2020-05-14 12:58:45 +02:00
|
|
|
self.mw,
|
|
|
|
self.note,
|
|
|
|
ord=ord,
|
|
|
|
parent=self.parentWindow,
|
2021-09-20 11:34:49 +02:00
|
|
|
fill_empty=False,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_win:
|
2013-07-23 15:35:00 +02:00
|
|
|
self.parentWindow.activateWindow()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# JS->Python bridge
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-28 19:39:14 +01:00
|
|
|
def onBridgeCmd(self, cmd: str) -> Any:
|
2020-01-15 03:46:53 +01:00
|
|
|
if not self.note:
|
2012-12-21 08:51:59 +01:00
|
|
|
# shutdown
|
|
|
|
return
|
2021-03-01 13:34:49 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# focus lost or key/button pressed?
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
if cmd.startswith("blur") or cmd.startswith("key"):
|
2021-02-01 08:28:35 +01:00
|
|
|
(type, ord_str, nid_str, txt) = cmd.split(":", 3)
|
|
|
|
ord = int(ord_str)
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
try:
|
2021-02-01 08:28:35 +01:00
|
|
|
nid = int(nid_str)
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
except ValueError:
|
|
|
|
nid = 0
|
|
|
|
if nid != self.note.id:
|
|
|
|
print("ignored late blur")
|
|
|
|
return
|
2020-08-09 10:38:31 +02:00
|
|
|
|
2021-12-03 22:55:22 +01:00
|
|
|
try:
|
|
|
|
self.note.fields[ord] = self.mungeHTML(txt)
|
|
|
|
except IndexError:
|
|
|
|
print("ignored late blur after notetype change")
|
|
|
|
return
|
2020-08-09 10:38:31 +02:00
|
|
|
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
if not self.addMode:
|
2021-03-06 12:59:12 +01:00
|
|
|
self._save_current_note()
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
if type == "blur":
|
|
|
|
self.currentField = None
|
|
|
|
# run any filters
|
|
|
|
if gui_hooks.editor_did_unfocus_field(False, self.note, ord):
|
|
|
|
# something updated the note; update it after a subsequent focus
|
|
|
|
# event has had time to fire
|
2022-02-18 10:00:12 +01:00
|
|
|
self.mw.progress.timer(
|
|
|
|
100, self.loadNoteKeepingFocus, False, parent=self.widget
|
|
|
|
)
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
else:
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
self._check_and_update_duplicate_display_async()
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
else:
|
|
|
|
gui_hooks.editor_did_fire_typing_timer(self.note)
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
self._check_and_update_duplicate_display_async()
|
2021-03-01 13:34:49 +01:00
|
|
|
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
# focused into field?
|
|
|
|
elif cmd.startswith("focus"):
|
|
|
|
(type, num) = cmd.split(":", 1)
|
2021-09-13 07:31:24 +02:00
|
|
|
self.last_field_index = self.currentField = int(num)
|
Revert "Merge pull request #527 from Arthur-Milchior/explode_on_bridge_cmd"
This reverts commit 2264fe3f66871fc98d8a071b7be5d8a66983672c, reversing
changes made to 84b84ae31c264b4e6f70b3c9303b5a9f11438153.
Causes a traceback when opening the add screen, clicking on Type,
and choosing a note type.
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 31, in cmd
return json.dumps(self.onCmd(str))
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 97, in _onCmd
return self._onBridgeCmd(str)
File "/Users/dae/Work/code/dtop/qt/aqt/webview.py", line 500, in _onBridgeCmd
return self.onBridgeCmd(cmd)
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 374, in onBridgeCmd
self._links[cmd](self, *args) # type: ignore
File "/Users/dae/Work/code/dtop/qt/aqt/editor.py", line 404, in onBlur
if gui_hooks.editor_did_unfocus_field(False, self.note, int(ord)):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
2020-03-28 04:35:05 +01:00
|
|
|
gui_hooks.editor_did_focus_field(self.note, self.currentField)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-08-03 05:52:07 +02:00
|
|
|
elif cmd.startswith("toggleStickyAll"):
|
|
|
|
model = self.note.note_type()
|
|
|
|
flds = model["flds"]
|
|
|
|
|
|
|
|
any_sticky = any([fld["sticky"] for fld in flds])
|
|
|
|
result = []
|
|
|
|
for fld in flds:
|
|
|
|
if not any_sticky or fld["sticky"]:
|
|
|
|
fld["sticky"] = not fld["sticky"]
|
|
|
|
|
|
|
|
result.append(fld["sticky"])
|
|
|
|
|
2021-10-09 02:26:10 +02:00
|
|
|
update_notetype_legacy(parent=self.mw, notetype=model).run_in_background(
|
|
|
|
initiator=self
|
|
|
|
)
|
2021-08-03 05:52:07 +02:00
|
|
|
|
|
|
|
return result
|
|
|
|
|
2021-03-01 13:34:49 +01:00
|
|
|
elif cmd.startswith("toggleSticky"):
|
2021-02-28 01:17:42 +01:00
|
|
|
(type, num) = cmd.split(":", 1)
|
|
|
|
ord = int(num)
|
|
|
|
|
2021-06-27 04:12:23 +02:00
|
|
|
model = self.note.note_type()
|
2021-06-21 15:48:22 +02:00
|
|
|
fld = model["flds"][ord]
|
2021-02-28 18:46:43 +01:00
|
|
|
new_state = not fld["sticky"]
|
|
|
|
fld["sticky"] = new_state
|
|
|
|
|
2021-10-09 02:26:10 +02:00
|
|
|
update_notetype_legacy(parent=self.mw, notetype=model).run_in_background(
|
|
|
|
initiator=self
|
|
|
|
)
|
2021-06-21 15:48:22 +02:00
|
|
|
|
2021-02-28 18:46:43 +01:00
|
|
|
return new_state
|
2021-02-28 01:17:42 +01:00
|
|
|
|
2021-08-02 23:12:00 +02:00
|
|
|
elif cmd.startswith("lastTextColor"):
|
|
|
|
(_, textColor) = cmd.split(":", 1)
|
|
|
|
self.mw.pm.profile["lastTextColor"] = textColor
|
|
|
|
|
|
|
|
elif cmd.startswith("lastHighlightColor"):
|
|
|
|
(_, highlightColor) = cmd.split(":", 1)
|
|
|
|
self.mw.pm.profile["lastHighlightColor"] = highlightColor
|
|
|
|
|
2021-06-28 15:27:59 +02:00
|
|
|
elif cmd.startswith("saveTags"):
|
|
|
|
(type, tagsJson) = cmd.split(":", 1)
|
|
|
|
self.note.tags = json.loads(tagsJson)
|
|
|
|
|
|
|
|
gui_hooks.editor_did_update_tags(self.note)
|
|
|
|
if not self.addMode:
|
|
|
|
self._save_current_note()
|
|
|
|
|
2023-02-27 07:23:19 +01:00
|
|
|
elif cmd.startswith("setTagsCollapsed"):
|
|
|
|
(type, collapsed_string) = cmd.split(":", 1)
|
|
|
|
collapsed = collapsed_string == "true"
|
|
|
|
self.setTagsCollapsed(collapsed)
|
|
|
|
|
2021-03-01 13:34:49 +01:00
|
|
|
elif cmd in self._links:
|
2022-02-25 01:59:06 +01:00
|
|
|
return self._links[cmd](self)
|
2021-03-01 13:34:49 +01:00
|
|
|
|
|
|
|
else:
|
|
|
|
print("uncaught cmd", cmd)
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def mungeHTML(self, txt: str) -> str:
|
2020-08-08 23:29:28 +02:00
|
|
|
return gui_hooks.editor_will_munge_html(txt, self)
|
2019-01-25 23:12:48 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Setting/unsetting the current note
|
|
|
|
######################################################################
|
|
|
|
|
2021-03-16 07:39:41 +01:00
|
|
|
def set_note(
|
2021-10-03 10:59:42 +02:00
|
|
|
self, note: Note | None, hide: bool = True, focusTo: int | None = None
|
2021-02-01 08:28:35 +01:00
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Make NOTE the current note."
|
|
|
|
self.note = note
|
2017-08-05 07:15:19 +02:00
|
|
|
self.currentField = None
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.note:
|
2017-08-05 07:15:19 +02:00
|
|
|
self.loadNote(focusTo=focusTo)
|
2021-06-28 15:45:29 +02:00
|
|
|
elif hide:
|
|
|
|
self.widget.hide()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def loadNoteKeepingFocus(self) -> None:
|
2017-12-04 03:03:01 +01:00
|
|
|
self.loadNote(self.currentField)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def loadNote(self, focusTo: int | None = None) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.note:
|
|
|
|
return
|
2017-08-05 07:15:19 +02:00
|
|
|
|
2021-07-16 02:37:59 +02:00
|
|
|
data = [
|
|
|
|
(fld, self.mw.col.media.escape_media_filenames(val))
|
|
|
|
for fld, val in self.note.items()
|
|
|
|
]
|
2021-11-06 00:42:48 +01:00
|
|
|
|
|
|
|
flds = self.note.note_type()["flds"]
|
2022-08-31 15:34:39 +02:00
|
|
|
collapsed = [fld["collapsed"] for fld in flds]
|
2022-08-18 04:30:18 +02:00
|
|
|
plain_texts = [fld.get("plainText", False) for fld in flds]
|
2021-11-06 00:42:48 +01:00
|
|
|
descriptions = [fld.get("description", "") for fld in flds]
|
2023-06-14 01:38:06 +02:00
|
|
|
notetype_meta = {"id": self.note.mid, "modTime": self.note.note_type()["mod"]}
|
2021-11-06 00:42:48 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.widget.show()
|
2017-08-05 07:15:19 +02:00
|
|
|
|
2021-06-12 17:35:40 +02:00
|
|
|
note_fields_status = self.note.fields_check()
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def oncallback(arg: Any) -> None:
|
2017-08-05 07:15:19 +02:00
|
|
|
if not self.note:
|
|
|
|
return
|
|
|
|
self.setupForegroundButton()
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
# we currently do this synchronously to ensure we load before the
|
|
|
|
# sidebar on browser startup
|
2021-06-12 17:35:40 +02:00
|
|
|
self._update_duplicate_display(note_fields_status)
|
2017-08-15 03:38:32 +02:00
|
|
|
if focusTo is not None:
|
|
|
|
self.web.setFocus()
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.editor_did_load_note(self)
|
2017-08-05 07:15:19 +02:00
|
|
|
|
2022-12-13 02:35:21 +01:00
|
|
|
text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff")
|
|
|
|
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#0000ff")
|
2021-08-02 23:12:00 +02:00
|
|
|
|
2022-12-07 06:37:46 +01:00
|
|
|
js = f"""
|
|
|
|
saveSession();
|
|
|
|
setFields({json.dumps(data)});
|
2023-06-14 01:38:06 +02:00
|
|
|
setNotetypeMeta({json.dumps(notetype_meta)});
|
2022-12-07 06:37:46 +01:00
|
|
|
setCollapsed({json.dumps(collapsed)});
|
|
|
|
setPlainTexts({json.dumps(plain_texts)});
|
|
|
|
setDescriptions({json.dumps(descriptions)});
|
|
|
|
setFonts({json.dumps(self.fonts())});
|
|
|
|
focusField({json.dumps(focusTo)});
|
|
|
|
setNoteId({json.dumps(self.note.id)});
|
|
|
|
setColorButtons({json.dumps([text_color, highlight_color])});
|
|
|
|
setTags({json.dumps(self.note.tags)});
|
|
|
|
setTagsCollapsed({json.dumps(self.mw.pm.tags_collapsed(self.editorMode))});
|
|
|
|
setMathjaxEnabled({json.dumps(self.mw.col.get_config("renderMathjax", True))});
|
|
|
|
setShrinkImages({json.dumps(self.mw.col.get_config("shrinkEditorImages", True))});
|
|
|
|
setCloseHTMLTags({json.dumps(self.mw.col.get_config("closeHTMLTags", True))});
|
2023-03-08 10:22:02 +01:00
|
|
|
triggerChanges();
|
2022-12-07 06:37:46 +01:00
|
|
|
"""
|
2021-02-28 01:17:42 +01:00
|
|
|
|
|
|
|
if self.addMode:
|
2021-06-27 04:12:23 +02:00
|
|
|
sticky = [field["sticky"] for field in self.note.note_type()["flds"]]
|
2021-02-28 01:17:42 +01:00
|
|
|
js += " setSticky(%s);" % json.dumps(sticky)
|
|
|
|
|
2020-03-24 10:17:01 +01:00
|
|
|
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
2022-02-03 05:52:11 +01:00
|
|
|
self.web.evalWithCallback(
|
|
|
|
f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-06 12:59:12 +01:00
|
|
|
def _save_current_note(self) -> None:
|
|
|
|
"Call after note is updated with data from webview."
|
2021-04-06 06:56:36 +02:00
|
|
|
update_note(parent=self.widget, note=self.note).run_in_background(
|
|
|
|
initiator=self
|
|
|
|
)
|
2021-03-06 12:59:12 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def fonts(self) -> list[tuple[str, int, bool]]:
|
2019-12-23 01:34:10 +01:00
|
|
|
return [
|
2020-01-15 08:45:35 +01:00
|
|
|
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
|
2021-06-27 04:12:23 +02:00
|
|
|
for f in self.note.note_type()["flds"]
|
2019-12-23 01:34:10 +01:00
|
|
|
]
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-16 13:40:37 +01:00
|
|
|
def call_after_note_saved(
|
|
|
|
self, callback: Callable, keepFocus: bool = False
|
|
|
|
) -> None:
|
2016-07-14 12:23:44 +02:00
|
|
|
"Save unsaved edits then call callback()."
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.note:
|
2019-12-23 01:34:10 +01:00
|
|
|
# calling code may not expect the callback to fire immediately
|
2022-02-24 12:15:56 +01:00
|
|
|
self.mw.progress.single_shot(10, callback)
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
2018-05-28 05:40:35 +02:00
|
|
|
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-16 13:40:37 +01:00
|
|
|
saveNow = call_after_note_saved
|
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
def _check_and_update_duplicate_display_async(self) -> None:
|
|
|
|
note = self.note
|
2021-05-20 07:32:28 +02:00
|
|
|
if not note:
|
|
|
|
return
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
2021-06-12 17:35:40 +02:00
|
|
|
def on_done(result: NoteFieldsCheckResult.V) -> None:
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
if self.note != note:
|
|
|
|
return
|
|
|
|
self._update_duplicate_display(result)
|
|
|
|
|
2021-05-08 08:20:10 +02:00
|
|
|
QueryOp(
|
|
|
|
parent=self.parentWindow,
|
2021-06-12 17:35:40 +02:00
|
|
|
op=lambda _: note.fields_check(),
|
2021-05-08 08:20:10 +02:00
|
|
|
success=on_done,
|
|
|
|
).run_in_background()
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
|
|
|
checkValid = _check_and_update_duplicate_display_async
|
|
|
|
|
2021-06-12 17:35:40 +02:00
|
|
|
def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None:
|
2020-01-23 06:08:10 +01:00
|
|
|
cols = [""] * len(self.note.fields)
|
2021-06-14 10:21:42 +02:00
|
|
|
cloze_hint = ""
|
2021-06-12 17:35:40 +02:00
|
|
|
if result == NoteFieldsCheckResult.DUPLICATE:
|
2020-01-23 06:08:10 +01:00
|
|
|
cols[0] = "dupe"
|
2021-06-12 17:35:40 +02:00
|
|
|
elif result == NoteFieldsCheckResult.NOTETYPE_NOT_CLOZE:
|
2021-06-14 10:21:42 +02:00
|
|
|
cloze_hint = tr.adding_cloze_outside_cloze_notetype()
|
2021-06-12 17:35:40 +02:00
|
|
|
elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE:
|
2021-06-14 10:21:42 +02:00
|
|
|
cloze_hint = tr.adding_cloze_outside_cloze_field()
|
2021-01-28 19:13:39 +01:00
|
|
|
|
2022-02-03 05:52:11 +01:00
|
|
|
self.web.eval(
|
|
|
|
'require("anki/ui").loaded.then(() => {'
|
|
|
|
f"setBackgrounds({json.dumps(cols)});\n"
|
|
|
|
f"setClozeHint({json.dumps(cloze_hint)});\n"
|
|
|
|
"}); "
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def showDupes(self) -> None:
|
2021-02-01 11:54:28 +01:00
|
|
|
aqt.dialogs.open(
|
|
|
|
"Browser",
|
|
|
|
self.mw,
|
|
|
|
search=(
|
2021-02-11 10:57:19 +01:00
|
|
|
SearchNode(
|
|
|
|
dupe=SearchNode.Dupe(
|
2021-06-27 04:12:23 +02:00
|
|
|
notetype_id=self.note.note_type()["id"],
|
2021-02-01 11:54:28 +01:00
|
|
|
first_field=self.note.fields[0],
|
|
|
|
)
|
|
|
|
),
|
2021-02-01 13:55:03 +01:00
|
|
|
),
|
2021-01-29 18:27:33 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.note:
|
|
|
|
return True
|
2021-06-27 04:12:23 +02:00
|
|
|
m = self.note.note_type()
|
2013-05-24 05:04:28 +02:00
|
|
|
for c, f in enumerate(self.note.fields):
|
2020-03-24 11:54:19 +01:00
|
|
|
f = f.replace("<br>", "").strip()
|
2019-11-21 02:18:14 +01:00
|
|
|
notChangedvalues = {"", "<br>"}
|
2019-12-23 01:34:10 +01:00
|
|
|
if previousNote and m["flds"][c]["sticky"]:
|
2020-03-24 11:54:19 +01:00
|
|
|
notChangedvalues.add(previousNote.fields[c].replace("<br>", "").strip())
|
2019-11-21 02:18:14 +01:00
|
|
|
if f not in notChangedvalues:
|
2012-12-21 08:51:59 +01:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def cleanup(self) -> None:
|
2021-03-16 07:39:41 +01:00
|
|
|
self.set_note(None)
|
2017-08-16 04:45:33 +02:00
|
|
|
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
2022-05-25 14:04:50 +02:00
|
|
|
if self.web:
|
|
|
|
self.web.cleanup()
|
|
|
|
self.web = None
|
2017-08-16 04:45:33 +02:00
|
|
|
|
2021-03-16 07:39:41 +01:00
|
|
|
# legacy
|
|
|
|
|
|
|
|
setNote = set_note
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Tag handling
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def setupTags(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt.tagedit
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
g = QGroupBox(self.widget)
|
2020-01-26 09:47:28 +01:00
|
|
|
g.setStyleSheet("border: 0")
|
2012-12-21 08:51:59 +01:00
|
|
|
tb = QGridLayout()
|
|
|
|
tb.setSpacing(12)
|
2020-01-26 09:47:28 +01:00
|
|
|
tb.setContentsMargins(2, 6, 2, 6)
|
2012-12-21 08:51:59 +01:00
|
|
|
# tags
|
2021-03-26 04:48:26 +01:00
|
|
|
l = QLabel(tr.editing_tags())
|
2012-12-21 08:51:59 +01:00
|
|
|
tb.addWidget(l, 1, 0)
|
|
|
|
self.tags = aqt.tagedit.TagEdit(self.widget)
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
qconnect(self.tags.lostFocus, self.on_tag_focus_lost)
|
2021-03-26 04:48:26 +01:00
|
|
|
self.tags.setToolTip(shortcut(tr.editing_jump_to_tags_with_ctrlandshiftandt()))
|
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
|
|
|
border = theme_manager.var(colors.BORDER)
|
2020-01-26 09:47:28 +01:00
|
|
|
self.tags.setStyleSheet(f"border: 1px solid {border}")
|
2012-12-21 08:51:59 +01:00
|
|
|
tb.addWidget(self.tags, 1, 1)
|
|
|
|
g.setLayout(tb)
|
|
|
|
self.outerLayout.addWidget(g)
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def updateTags(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.tags.col != self.mw.col:
|
|
|
|
self.tags.setCol(self.mw.col)
|
|
|
|
if not self.tags.text() or not self.addMode:
|
2021-06-26 03:38:59 +02:00
|
|
|
self.tags.setText(self.note.string_tags().strip())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
def on_tag_focus_lost(self) -> None:
|
2020-05-07 09:54:23 +02:00
|
|
|
self.note.tags = self.mw.col.tags.split(self.tags.text())
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
gui_hooks.editor_did_update_tags(self.note)
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.addMode:
|
2021-03-06 12:59:12 +01:00
|
|
|
self._save_current_note()
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
|
|
|
def blur_tags_if_focused(self) -> None:
|
|
|
|
if not self.note:
|
|
|
|
return
|
|
|
|
if self.tags.hasFocus():
|
|
|
|
self.widget.setFocus()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def hideCompleters(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.tags.hideCompleter()
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onFocusTags(self) -> None:
|
2017-08-05 07:15:19 +02:00
|
|
|
self.tags.setFocus()
|
|
|
|
|
Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:
- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.
It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.
To address these points:
- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
|
|
|
# legacy
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
|
Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:
- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.
It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.
To address these points:
- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
|
|
|
def saveAddModeVars(self) -> None:
|
|
|
|
pass
|
|
|
|
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
saveTags = blur_tags_if_focused
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Audio/video/images
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onAddMedia(self) -> None:
|
2022-03-16 01:29:06 +01:00
|
|
|
"""Show a file selection screen, then add the selected media.
|
|
|
|
This expects initial setup to have been done by TemplateButtons.svelte."""
|
2019-12-23 01:34:10 +01:00
|
|
|
extension_filter = " ".join(
|
2021-02-11 01:09:06 +01:00
|
|
|
f"*.{extension}" for extension in sorted(itertools.chain(pics, audio))
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-03-26 04:48:26 +01:00
|
|
|
filter = f"{tr.editing_media()} ({extension_filter})"
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def accept(file: str) -> None:
|
2022-03-16 01:29:06 +01:00
|
|
|
self.resolve_media(file)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
file = getFile(
|
2021-02-19 01:18:40 +01:00
|
|
|
parent=self.widget,
|
2021-03-26 04:48:26 +01:00
|
|
|
title=tr.editing_add_media(),
|
2021-02-19 01:18:40 +01:00
|
|
|
cb=cast(Callable[[Any], None], accept),
|
|
|
|
filter=filter,
|
2021-02-01 11:23:48 +01:00
|
|
|
key="media",
|
|
|
|
)
|
2022-02-25 01:59:06 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.parentWindow.activateWindow()
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def addMedia(self, path: str, canDelete: bool = False) -> None:
|
2022-03-16 01:29:06 +01:00
|
|
|
"""Legacy routine used by add-ons to add a media file and update the current field.
|
|
|
|
canDelete is ignored."""
|
2022-02-25 01:59:06 +01:00
|
|
|
|
2022-03-16 01:29:06 +01:00
|
|
|
try:
|
|
|
|
html = self._addMedia(path)
|
|
|
|
except Exception as e:
|
|
|
|
showWarning(str(e))
|
|
|
|
return
|
|
|
|
|
|
|
|
self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});")
|
|
|
|
|
|
|
|
def resolve_media(self, path: str) -> None:
|
|
|
|
"""Finish inserting media into a field.
|
|
|
|
This expects initial setup to have been done by TemplateButtons.svelte."""
|
2020-07-01 03:19:06 +02:00
|
|
|
try:
|
2021-03-10 09:20:37 +01:00
|
|
|
html = self._addMedia(path)
|
2020-07-01 03:19:06 +02:00
|
|
|
except Exception as e:
|
|
|
|
showWarning(str(e))
|
|
|
|
return
|
2022-02-25 01:59:06 +01:00
|
|
|
|
|
|
|
self.web.eval(
|
2022-03-16 01:29:06 +01:00
|
|
|
f'require("anki/TemplateButtons").resolveMedia({json.dumps(html)})'
|
2022-02-25 01:59:06 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
2021-03-10 09:20:37 +01:00
|
|
|
"""Add to media folder and return local img or sound tag."""
|
2012-12-21 08:51:59 +01:00
|
|
|
# copy to media folder
|
2021-10-25 06:50:13 +02:00
|
|
|
fname = self.mw.col.media.add_file(path)
|
2012-12-21 08:51:59 +01:00
|
|
|
# return a local html link
|
2013-07-11 10:21:16 +02:00
|
|
|
return self.fnameToLink(fname)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def _addMediaFromData(self, fname: str, data: bytes) -> str:
|
2021-10-25 06:50:13 +02:00
|
|
|
return self.mw.col.media._legacy_write_data(fname, data)
|
2018-05-01 05:16:46 +02:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onRecSound(self) -> None:
|
2020-12-16 10:09:45 +01:00
|
|
|
aqt.sound.record_audio(
|
|
|
|
self.parentWindow,
|
|
|
|
self.mw,
|
|
|
|
True,
|
2022-03-16 01:29:06 +01:00
|
|
|
self.resolve_media,
|
2020-12-16 10:09:45 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2013-07-11 10:21:16 +02:00
|
|
|
# Media downloads
|
|
|
|
######################################################################
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def urlToLink(self, url: str) -> str | None:
|
2013-07-11 10:21:16 +02:00
|
|
|
fname = self.urlToFile(url)
|
|
|
|
if not fname:
|
2022-04-21 06:10:53 +02:00
|
|
|
return '<a href="{}">{}</a>'.format(
|
|
|
|
url, html.escape(urllib.parse.unquote(url))
|
|
|
|
)
|
2013-07-11 10:21:16 +02:00
|
|
|
return self.fnameToLink(fname)
|
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def fnameToLink(self, fname: str) -> str:
|
2013-07-11 10:21:16 +02:00
|
|
|
ext = fname.split(".")[-1].lower()
|
|
|
|
if ext in pics:
|
2016-05-12 06:45:35 +02:00
|
|
|
name = urllib.parse.quote(fname.encode("utf8"))
|
2021-02-11 01:09:06 +01:00
|
|
|
return f'<img src="{name}">'
|
2013-07-11 10:21:16 +02:00
|
|
|
else:
|
2020-09-04 00:34:26 +02:00
|
|
|
av_player.play_file(fname)
|
2021-02-11 01:09:06 +01:00
|
|
|
return f"[sound:{html.escape(fname, quote=False)}]"
|
2013-07-11 10:21:16 +02:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def urlToFile(self, url: str) -> str | None:
|
2013-07-11 10:21:16 +02:00
|
|
|
l = url.lower()
|
2019-12-23 01:34:10 +01:00
|
|
|
for suffix in pics + audio:
|
2021-02-11 01:09:06 +01:00
|
|
|
if l.endswith(f".{suffix}"):
|
2013-07-11 10:21:16 +02:00
|
|
|
return self._retrieveURL(url)
|
2013-11-26 09:57:02 +01:00
|
|
|
# not a supported type
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2013-07-11 10:21:16 +02:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def isURL(self, s: str) -> bool:
|
2013-07-18 13:32:41 +02:00
|
|
|
s = s.lower()
|
2019-12-23 01:34:10 +01:00
|
|
|
return (
|
|
|
|
s.startswith("http://")
|
2013-07-18 13:32:41 +02:00
|
|
|
or s.startswith("https://")
|
2013-09-20 07:41:56 +02:00
|
|
|
or s.startswith("ftp://")
|
2019-12-23 01:34:10 +01:00
|
|
|
or s.startswith("file://")
|
|
|
|
)
|
2013-07-18 13:32:41 +02:00
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def inlinedImageToFilename(self, txt: str) -> str:
|
2018-05-01 05:16:46 +02:00
|
|
|
prefix = "data:image/"
|
|
|
|
suffix = ";base64,"
|
2018-08-20 05:02:30 +02:00
|
|
|
for ext in ("jpg", "jpeg", "png", "gif"):
|
2018-05-01 05:16:46 +02:00
|
|
|
fullPrefix = prefix + ext + suffix
|
|
|
|
if txt.startswith(fullPrefix):
|
2019-12-23 01:34:10 +01:00
|
|
|
b64data = txt[len(fullPrefix) :].strip()
|
2018-05-01 05:16:46 +02:00
|
|
|
data = base64.b64decode(b64data, validate=True)
|
|
|
|
if ext == "jpeg":
|
|
|
|
ext = "jpg"
|
2021-02-11 01:09:06 +01:00
|
|
|
return self._addPastedImage(data, f".{ext}")
|
2018-05-01 05:16:46 +02:00
|
|
|
|
|
|
|
return ""
|
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def inlinedImageToLink(self, src: str) -> str:
|
2018-08-08 03:38:45 +02:00
|
|
|
fname = self.inlinedImageToFilename(src)
|
|
|
|
if fname:
|
|
|
|
return self.fnameToLink(fname)
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
2018-05-01 05:16:46 +02:00
|
|
|
# ext should include dot
|
2020-06-07 05:06:11 +02:00
|
|
|
def _addPastedImage(self, data: bytes, ext: str) -> str:
|
2018-05-01 05:16:46 +02:00
|
|
|
# hash and write
|
|
|
|
csum = checksum(data)
|
2021-02-11 01:09:06 +01:00
|
|
|
fname = f"paste-{csum}{ext}"
|
2018-05-01 05:16:46 +02:00
|
|
|
return self._addMediaFromData(fname, data)
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _retrieveURL(self, url: str) -> str | None:
|
2013-07-11 10:21:16 +02:00
|
|
|
"Download file into media folder and return local filename or None."
|
2013-09-20 07:41:56 +02:00
|
|
|
# urllib doesn't understand percent-escaped utf8, but requires things like
|
2017-08-06 19:03:00 +02:00
|
|
|
# '#' to be escaped.
|
|
|
|
url = urllib.parse.unquote(url)
|
2013-07-11 10:21:16 +02:00
|
|
|
if url.lower().startswith("file://"):
|
|
|
|
url = url.replace("%", "%25")
|
|
|
|
url = url.replace("#", "%23")
|
2017-10-25 11:42:20 +02:00
|
|
|
local = True
|
|
|
|
else:
|
|
|
|
local = False
|
2013-09-20 07:41:56 +02:00
|
|
|
# fetch it into a temporary folder
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.progress.start(immediate=not local, parent=self.parentWindow)
|
2020-05-21 01:22:34 +02:00
|
|
|
content_type = None
|
2021-10-03 10:59:42 +02:00
|
|
|
error_msg: str | None = None
|
2013-07-11 10:21:16 +02:00
|
|
|
try:
|
2017-10-25 11:42:20 +02:00
|
|
|
if local:
|
2019-12-23 01:34:10 +01:00
|
|
|
req = urllib.request.Request(
|
|
|
|
url, None, {"User-Agent": "Mozilla/5.0 (compatible; Anki)"}
|
|
|
|
)
|
2020-05-21 02:57:49 +02:00
|
|
|
with urllib.request.urlopen(req) as response:
|
|
|
|
filecontents = response.read()
|
2017-10-25 11:42:20 +02:00
|
|
|
else:
|
2020-05-21 02:57:49 +02:00
|
|
|
with HttpClient() as client:
|
|
|
|
client.timeout = 30
|
|
|
|
with client.get(url) as response:
|
|
|
|
if response.status_code != 200:
|
2021-03-26 05:38:15 +01:00
|
|
|
error_msg = tr.qt_misc_unexpected_response_code(
|
2020-11-17 12:47:47 +01:00
|
|
|
val=response.status_code,
|
2020-05-21 02:57:49 +02:00
|
|
|
)
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2020-05-21 02:57:49 +02:00
|
|
|
filecontents = response.content
|
|
|
|
content_type = response.headers.get("content-type")
|
2020-05-20 08:12:41 +02:00
|
|
|
except (urllib.error.URLError, requests.exceptions.RequestException) as e:
|
2021-03-26 05:21:04 +01:00
|
|
|
error_msg = tr.editing_an_error_occurred_while_opening(val=str(e))
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2013-07-11 10:21:16 +02:00
|
|
|
finally:
|
|
|
|
self.mw.progress.finish()
|
2020-05-20 08:12:41 +02:00
|
|
|
if error_msg:
|
|
|
|
showWarning(error_msg)
|
2017-11-17 07:20:33 +01:00
|
|
|
# strip off any query string
|
2017-12-13 05:34:54 +01:00
|
|
|
url = re.sub(r"\?.*?$", "", url)
|
2020-01-28 12:45:26 +01:00
|
|
|
fname = os.path.basename(urllib.parse.unquote(url))
|
2020-05-31 23:33:11 +02:00
|
|
|
if not fname.strip():
|
|
|
|
fname = "paste"
|
2020-05-21 01:22:34 +02:00
|
|
|
if content_type:
|
|
|
|
fname = self.mw.col.media.add_extension_based_on_mime(fname, content_type)
|
2020-01-28 12:45:26 +01:00
|
|
|
|
|
|
|
return self.mw.col.media.write_data(fname, filecontents)
|
2013-07-11 10:21:16 +02:00
|
|
|
|
2016-12-15 09:14:47 +01:00
|
|
|
# Paste/drag&drop
|
2013-07-11 10:21:16 +02:00
|
|
|
######################################################################
|
|
|
|
|
2016-12-15 09:14:47 +01:00
|
|
|
removeTags = ["script", "iframe", "object", "style"]
|
|
|
|
|
2020-07-29 00:19:16 +02:00
|
|
|
def _pastePreFilter(self, html: str, internal: bool) -> str:
|
2020-03-13 01:17:32 +01:00
|
|
|
# https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx
|
|
|
|
if html.find(">") < 0:
|
|
|
|
return html
|
|
|
|
|
2016-10-20 00:40:30 +02:00
|
|
|
with warnings.catch_warnings() as w:
|
2019-12-23 01:34:10 +01:00
|
|
|
warnings.simplefilter("ignore", UserWarning)
|
2016-10-20 00:40:30 +02:00
|
|
|
doc = BeautifulSoup(html, "html.parser")
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2020-07-29 00:19:16 +02:00
|
|
|
tag: bs4.element.Tag
|
2017-11-11 02:51:30 +01:00
|
|
|
if not internal:
|
|
|
|
for tag in self.removeTags:
|
|
|
|
for node in doc(tag):
|
|
|
|
node.decompose()
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2017-11-11 02:51:30 +01:00
|
|
|
# convert p tags to divs
|
|
|
|
for node in doc("p"):
|
|
|
|
node.name = "div"
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2013-07-11 10:21:16 +02:00
|
|
|
for tag in doc("img"):
|
|
|
|
try:
|
2019-12-23 01:34:10 +01:00
|
|
|
src = tag["src"]
|
2013-07-11 10:21:16 +02:00
|
|
|
except KeyError:
|
|
|
|
# for some bizarre reason, mnemosyne removes src elements
|
|
|
|
# from missing media
|
2017-11-11 02:51:30 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
# in internal pastes, rewrite mediasrv references to relative
|
|
|
|
if internal:
|
2017-12-13 05:34:54 +01:00
|
|
|
m = re.match(r"http://127.0.0.1:\d+/(.*)$", src)
|
2017-11-11 02:51:30 +01:00
|
|
|
if m:
|
2019-12-23 01:34:10 +01:00
|
|
|
tag["src"] = m.group(1)
|
2017-11-11 02:51:30 +01:00
|
|
|
else:
|
|
|
|
# in external pastes, download remote media
|
|
|
|
if self.isURL(src):
|
2017-11-17 07:20:33 +01:00
|
|
|
fname = self._retrieveURL(src)
|
2017-11-11 02:51:30 +01:00
|
|
|
if fname:
|
2019-12-23 01:34:10 +01:00
|
|
|
tag["src"] = fname
|
2018-08-08 03:38:45 +02:00
|
|
|
elif src.startswith("data:image/"):
|
|
|
|
# and convert inlined data
|
2019-12-23 01:34:10 +01:00
|
|
|
tag["src"] = self.inlinedImageToFilename(src)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2016-05-12 06:45:35 +02:00
|
|
|
html = str(doc)
|
2013-07-11 10:21:16 +02:00
|
|
|
return html
|
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def doPaste(self, html: str, internal: bool, extended: bool = False) -> None:
|
2017-11-11 02:51:30 +01:00
|
|
|
html = self._pastePreFilter(html, internal)
|
2017-10-25 12:20:28 +02:00
|
|
|
if extended:
|
2020-06-07 05:06:11 +02:00
|
|
|
ext = "true"
|
2017-10-25 12:20:28 +02:00
|
|
|
else:
|
2020-06-07 05:06:11 +02:00
|
|
|
ext = "false"
|
2021-02-11 01:09:06 +01:00
|
|
|
self.web.eval(f"pasteHTML({json.dumps(html)}, {json.dumps(internal)}, {ext});")
|
2021-07-19 07:21:13 +02:00
|
|
|
gui_hooks.editor_did_paste(self, html, internal, extended)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2021-09-05 13:54:04 +02:00
|
|
|
def doDrop(
|
|
|
|
self, html: str, internal: bool, extended: bool, cursor_pos: QPoint
|
|
|
|
) -> None:
|
2021-02-01 08:28:35 +01:00
|
|
|
def pasteIfField(ret: bool) -> None:
|
2020-08-19 06:37:14 +02:00
|
|
|
if ret:
|
2020-10-03 16:52:41 +02:00
|
|
|
self.doPaste(html, internal, extended)
|
2020-08-19 06:37:14 +02:00
|
|
|
|
2021-09-05 13:54:04 +02:00
|
|
|
self.web.evalWithCallback(
|
|
|
|
f"focusIfField({cursor_pos.x()}, {cursor_pos.y()});", pasteIfField
|
|
|
|
)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onPaste(self) -> None:
|
2016-12-15 09:14:47 +01:00
|
|
|
self.web.onPaste()
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onCutOrCopy(self) -> None:
|
2023-07-02 14:23:31 +02:00
|
|
|
self.web.user_cut_or_copied()
|
2017-09-02 05:48:03 +02:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
# Legacy editing routines
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
_js_legacy = "this routine has been moved into JS, and will be removed soon"
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def onHtmlEdit(self) -> None:
|
|
|
|
field = self.currentField
|
|
|
|
self.call_after_note_saved(lambda: self._onHtmlEdit(field))
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def _onHtmlEdit(self, field: int) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
d = QDialog(self.widget, Qt.WindowType.Window)
|
2021-08-20 03:37:11 +02:00
|
|
|
form = aqt.forms.edithtml.Ui_Dialog()
|
|
|
|
form.setupUi(d)
|
|
|
|
restoreGeom(d, "htmlEditor")
|
|
|
|
disable_help_button(d)
|
|
|
|
qconnect(
|
|
|
|
form.buttonBox.helpRequested, lambda: openHelp(HelpPage.EDITING_FEATURES)
|
|
|
|
)
|
|
|
|
font = QFont("Courier")
|
2021-10-05 05:53:01 +02:00
|
|
|
font.setStyleHint(QFont.StyleHint.TypeWriter)
|
2021-08-20 03:37:11 +02:00
|
|
|
form.textEdit.setFont(font)
|
|
|
|
form.textEdit.setPlainText(self.note.fields[field])
|
|
|
|
d.show()
|
2021-10-05 05:53:01 +02:00
|
|
|
form.textEdit.moveCursor(QTextCursor.MoveOperation.End)
|
2021-10-05 02:01:45 +02:00
|
|
|
d.exec()
|
2021-08-20 03:37:11 +02:00
|
|
|
html = form.textEdit.toPlainText()
|
|
|
|
if html.find(">") > -1:
|
|
|
|
# filter html through beautifulsoup so we can strip out things like a
|
|
|
|
# leading </div>
|
|
|
|
html_escaped = self.mw.col.media.escape_media_filenames(html)
|
|
|
|
with warnings.catch_warnings():
|
|
|
|
warnings.simplefilter("ignore", UserWarning)
|
|
|
|
html_escaped = str(BeautifulSoup(html_escaped, "html.parser"))
|
|
|
|
html = self.mw.col.media.escape_media_filenames(
|
|
|
|
html_escaped, unescape=True
|
|
|
|
)
|
|
|
|
self.note.fields[field] = html
|
|
|
|
if not self.addMode:
|
|
|
|
self._save_current_note()
|
|
|
|
self.loadNote(focusTo=field)
|
|
|
|
saveGeom(d, "htmlEditor")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def toggleBold(self) -> None:
|
|
|
|
self.web.eval("setFormat('bold');")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def toggleItalic(self) -> None:
|
|
|
|
self.web.eval("setFormat('italic');")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def toggleUnderline(self) -> None:
|
|
|
|
self.web.eval("setFormat('underline');")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def toggleSuper(self) -> None:
|
|
|
|
self.web.eval("setFormat('superscript');")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def toggleSub(self) -> None:
|
|
|
|
self.web.eval("setFormat('subscript');")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def removeFormat(self) -> None:
|
|
|
|
self.web.eval("setFormat('removeFormat');")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def onCloze(self) -> None:
|
|
|
|
self.call_after_note_saved(self._onCloze, keepFocus=True)
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def _onCloze(self) -> None:
|
|
|
|
# check that the model is set up for cloze deletion
|
|
|
|
if self.note.note_type()["type"] != MODEL_CLOZE:
|
|
|
|
if self.addMode:
|
|
|
|
tooltip(tr.editing_warning_cloze_deletions_will_not_work())
|
|
|
|
else:
|
|
|
|
showInfo(tr.editing_to_make_a_cloze_deletion_on())
|
|
|
|
return
|
|
|
|
# find the highest existing cloze
|
|
|
|
highest = 0
|
|
|
|
for name, val in list(self.note.items()):
|
|
|
|
m = re.findall(r"\{\{c(\d+)::", val)
|
|
|
|
if m:
|
2021-10-03 10:59:42 +02:00
|
|
|
highest = max(highest, sorted(int(x) for x in m)[-1])
|
2021-08-20 03:37:11 +02:00
|
|
|
# reuse last?
|
|
|
|
if not KeyboardModifiersPressed().alt:
|
|
|
|
highest += 1
|
|
|
|
# must start at 1
|
|
|
|
highest = max(1, highest)
|
|
|
|
self.web.eval("wrap('{{c%d::', '}}');" % highest)
|
|
|
|
|
|
|
|
def setupForegroundButton(self) -> None:
|
|
|
|
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
|
|
|
|
|
|
|
|
# use last colour
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def onForeground(self) -> None:
|
|
|
|
self._wrapWithColour(self.fcolour)
|
|
|
|
|
|
|
|
# choose new colour
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def onChangeCol(self) -> None:
|
2021-11-25 00:06:16 +01:00
|
|
|
if is_lin:
|
2021-08-20 03:37:11 +02:00
|
|
|
new = QColorDialog.getColor(
|
2021-10-05 05:53:01 +02:00
|
|
|
QColor(self.fcolour),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
QColorDialog.ColorDialogOption.DontUseNativeDialog,
|
2021-08-20 03:37:11 +02:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
new = QColorDialog.getColor(QColor(self.fcolour), None)
|
|
|
|
# native dialog doesn't refocus us for some reason
|
|
|
|
self.parentWindow.activateWindow()
|
|
|
|
if new.isValid():
|
|
|
|
self.fcolour = new.name()
|
|
|
|
self.onColourChanged()
|
|
|
|
self._wrapWithColour(self.fcolour)
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def _updateForegroundButton(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def onColourChanged(self) -> None:
|
|
|
|
self._updateForegroundButton()
|
|
|
|
self.mw.pm.profile["lastColour"] = self.fcolour
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
|
|
|
def _wrapWithColour(self, colour: str) -> None:
|
|
|
|
self.web.eval(f"setFormat('forecolor', '{colour}')")
|
|
|
|
|
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def onAdvanced(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
m = QMenu(self.mw)
|
2019-12-22 13:56:17 +01:00
|
|
|
|
|
|
|
for text, handler, shortcut in (
|
2021-03-26 04:48:26 +01:00
|
|
|
(tr.editing_mathjax_inline(), self.insertMathjaxInline, "Ctrl+M, M"),
|
|
|
|
(tr.editing_mathjax_block(), self.insertMathjaxBlock, "Ctrl+M, E"),
|
2020-11-17 08:42:43 +01:00
|
|
|
(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.editing_mathjax_chemistry(),
|
2020-11-17 08:42:43 +01:00
|
|
|
self.insertMathjaxChemistry,
|
|
|
|
"Ctrl+M, C",
|
|
|
|
),
|
2021-03-26 04:48:26 +01:00
|
|
|
(tr.editing_latex(), self.insertLatex, "Ctrl+T, T"),
|
|
|
|
(tr.editing_latex_equation(), self.insertLatexEqn, "Ctrl+T, E"),
|
|
|
|
(tr.editing_latex_math_env(), self.insertLatexMathEnv, "Ctrl+T, M"),
|
|
|
|
(tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"),
|
2019-12-22 13:56:17 +01:00
|
|
|
):
|
|
|
|
a = m.addAction(text)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, handler)
|
2019-12-22 13:56:17 +01:00
|
|
|
a.setShortcut(QKeySequence(shortcut))
|
2019-02-05 05:37:07 +01:00
|
|
|
|
|
|
|
qtMenuShortcutWorkaround(m)
|
|
|
|
|
2021-10-05 02:01:45 +02:00
|
|
|
m.exec(QCursor.pos())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def insertLatex(self) -> None:
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('[latex]', '[/latex]');")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def insertLatexEqn(self) -> None:
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('[$]', '[/$]');")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def insertLatexMathEnv(self) -> None:
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('[$$]', '[/$$]');")
|
2017-09-08 11:20:37 +02:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def insertMathjaxInline(self) -> None:
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('\\\\(', '\\\\)');")
|
2017-09-08 11:20:37 +02:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def insertMathjaxBlock(self) -> None:
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('\\\\[', '\\\\]');")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-08-20 03:37:11 +02:00
|
|
|
@deprecated(info=_js_legacy)
|
2021-02-01 08:28:35 +01:00
|
|
|
def insertMathjaxChemistry(self) -> None:
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
2018-08-06 05:17:57 +02:00
|
|
|
|
2022-08-18 04:06:06 +02:00
|
|
|
def toggleMathjax(self) -> None:
|
|
|
|
self.mw.col.set_config(
|
|
|
|
"renderMathjax", not self.mw.col.get_config("renderMathjax", False)
|
|
|
|
)
|
|
|
|
# hackily redraw the page
|
|
|
|
self.setupWeb()
|
|
|
|
self.loadNoteKeepingFocus()
|
|
|
|
|
2022-09-26 01:47:50 +02:00
|
|
|
def toggleShrinkImages(self) -> None:
|
|
|
|
self.mw.col.set_config(
|
|
|
|
"shrinkEditorImages",
|
|
|
|
not self.mw.col.get_config("shrinkEditorImages", True),
|
|
|
|
)
|
|
|
|
|
2022-10-03 05:14:57 +02:00
|
|
|
def toggleCloseHTMLTags(self) -> None:
|
|
|
|
self.mw.col.set_config(
|
|
|
|
"closeHTMLTags",
|
|
|
|
not self.mw.col.get_config("closeHTMLTags", True),
|
|
|
|
)
|
|
|
|
|
2023-02-27 07:23:19 +01:00
|
|
|
def setTagsCollapsed(self, collapsed: bool) -> None:
|
|
|
|
aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed)
|
2022-09-28 06:02:32 +02:00
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
# Links from HTML
|
|
|
|
######################################################################
|
|
|
|
|
2022-02-18 10:00:12 +01:00
|
|
|
def _init_links(self) -> None:
|
|
|
|
self._links: dict[str, Callable] = dict(
|
|
|
|
fields=Editor.onFields,
|
|
|
|
cards=Editor.onCardLayout,
|
|
|
|
bold=Editor.toggleBold,
|
|
|
|
italic=Editor.toggleItalic,
|
|
|
|
underline=Editor.toggleUnderline,
|
|
|
|
super=Editor.toggleSuper,
|
|
|
|
sub=Editor.toggleSub,
|
|
|
|
clear=Editor.removeFormat,
|
|
|
|
colour=Editor.onForeground,
|
|
|
|
changeCol=Editor.onChangeCol,
|
|
|
|
cloze=Editor.onCloze,
|
|
|
|
attach=Editor.onAddMedia,
|
|
|
|
record=Editor.onRecSound,
|
|
|
|
more=Editor.onAdvanced,
|
|
|
|
dupes=Editor.showDupes,
|
|
|
|
paste=Editor.onPaste,
|
|
|
|
cutOrCopy=Editor.onCutOrCopy,
|
|
|
|
htmlEdit=Editor.onHtmlEdit,
|
|
|
|
mathjaxInline=Editor.insertMathjaxInline,
|
|
|
|
mathjaxBlock=Editor.insertMathjaxBlock,
|
|
|
|
mathjaxChemistry=Editor.insertMathjaxChemistry,
|
2022-08-18 04:06:06 +02:00
|
|
|
toggleMathjax=Editor.toggleMathjax,
|
2022-09-26 01:47:50 +02:00
|
|
|
toggleShrinkImages=Editor.toggleShrinkImages,
|
2022-10-03 05:14:57 +02:00
|
|
|
toggleCloseHTMLTags=Editor.toggleCloseHTMLTags,
|
2022-02-18 10:00:12 +01:00
|
|
|
)
|
2016-06-22 06:52:17 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Pasting, drag & drop, and keyboard layouts
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class EditorWebView(AnkiWebView):
|
2021-02-01 08:28:35 +01:00
|
|
|
def __init__(self, parent: QWidget, editor: Editor) -> None:
|
2023-02-10 05:53:11 +01:00
|
|
|
AnkiWebView.__init__(self, kind=AnkiWebViewKind.EDITOR)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.editor = editor
|
2016-06-06 09:54:39 +02:00
|
|
|
self.setAcceptDrops(True)
|
2023-07-02 14:23:31 +02:00
|
|
|
self._store_field_content_on_next_clipboard_change = False
|
|
|
|
# when we detect the user copying from a field, we store the content
|
|
|
|
# here, and use it when they paste, so we avoid filtering field content
|
|
|
|
self._internal_field_text_for_paste: str | None = None
|
2017-08-31 10:10:37 +02:00
|
|
|
clip = self.editor.mw.app.clipboard()
|
2023-07-02 14:23:31 +02:00
|
|
|
qconnect(clip.dataChanged, self._on_clipboard_change)
|
2020-03-16 04:34:42 +01:00
|
|
|
gui_hooks.editor_web_view_did_init(self)
|
2017-08-31 10:10:37 +02:00
|
|
|
|
2023-07-02 14:23:31 +02:00
|
|
|
def user_cut_or_copied(self) -> None:
|
|
|
|
self._store_field_content_on_next_clipboard_change = True
|
|
|
|
|
|
|
|
def _on_clipboard_change(self) -> None:
|
|
|
|
if self._store_field_content_on_next_clipboard_change:
|
|
|
|
# if the flag was set, save the field data
|
|
|
|
self._internal_field_text_for_paste = self._get_clipboard_html_for_field()
|
|
|
|
self._store_field_content_on_next_clipboard_change = False
|
|
|
|
elif (
|
|
|
|
self._internal_field_text_for_paste != self._get_clipboard_html_for_field()
|
|
|
|
):
|
|
|
|
# if we've previously saved the field, blank it out if the clipboard state has changed
|
|
|
|
self._internal_field_text_for_paste = None
|
|
|
|
|
|
|
|
def _get_clipboard_html_for_field(self):
|
|
|
|
clip = self.editor.mw.app.clipboard()
|
|
|
|
mime = clip.mimeData()
|
|
|
|
if not mime.hasHtml():
|
|
|
|
return
|
|
|
|
return mime.html()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def onCut(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.WebAction.Cut)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:28:35 +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
|
|
|
|
2020-09-14 16:07:31 +02:00
|
|
|
def _wantsExtendedPaste(self) -> bool:
|
2021-03-10 09:20:37 +01:00
|
|
|
strip_html = self.editor.mw.col.get_config_bool(
|
|
|
|
Config.Bool.PASTE_STRIPS_FORMATTING
|
|
|
|
)
|
2021-03-17 05:51:59 +01:00
|
|
|
if KeyboardModifiersPressed().shift:
|
2021-03-10 09:20:37 +01:00
|
|
|
strip_html = not strip_html
|
2021-03-28 11:41:15 +02:00
|
|
|
return not strip_html
|
2020-09-14 16:07:31 +02:00
|
|
|
|
|
|
|
def _onPaste(self, mode: QClipboard.Mode) -> None:
|
|
|
|
extended = self._wantsExtendedPaste()
|
2023-07-02 14:23:31 +02:00
|
|
|
if html := self._internal_field_text_for_paste:
|
|
|
|
print("reuse internal")
|
|
|
|
self.editor.doPaste(html, True, extended)
|
|
|
|
else:
|
|
|
|
print("use clipboard")
|
|
|
|
mime = self.editor.mw.app.clipboard().mimeData(mode=mode)
|
|
|
|
html, internal = self._processMime(mime, extended)
|
|
|
|
if html:
|
|
|
|
self.editor.doPaste(html, internal, extended)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def onPaste(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self._onPaste(QClipboard.Mode.Clipboard)
|
2018-03-02 02:16:02 +01:00
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def onMiddleClickPaste(self) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
self._onPaste(QClipboard.Mode.Selection)
|
2018-03-02 02:16:02 +01:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
|
2020-08-19 06:37:14 +02:00
|
|
|
evt.accept()
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def dropEvent(self, evt: QDropEvent) -> None:
|
2020-10-03 16:52:41 +02:00
|
|
|
extended = self._wantsExtendedPaste()
|
2016-12-15 09:14:47 +01:00
|
|
|
mime = evt.mimeData()
|
2021-09-05 12:20:27 +02:00
|
|
|
cursor_pos = self.mapFromGlobal(QCursor.pos())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2016-12-15 09:14:47 +01:00
|
|
|
if evt.source() and mime.hasHtml():
|
|
|
|
# don't filter html from other fields
|
|
|
|
html, internal = mime.html(), True
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-09-05 12:20:27 +02:00
|
|
|
html, internal = self._processMime(mime, extended, drop_event=True)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
if not html:
|
|
|
|
return
|
|
|
|
|
2021-09-05 12:20:27 +02:00
|
|
|
self.editor.doDrop(html, internal, extended, cursor_pos)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
# returns (html, isInternal)
|
2021-09-05 13:54:04 +02:00
|
|
|
def _processMime(
|
|
|
|
self, mime: QMimeData, extended: bool = False, drop_event: bool = False
|
2021-10-03 10:59:42 +02:00
|
|
|
) -> tuple[str, bool]:
|
2017-01-25 06:12:48 +01:00
|
|
|
# print("html=%s image=%s urls=%s txt=%s" % (
|
|
|
|
# mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()))
|
|
|
|
# print("html", mime.html())
|
|
|
|
# print("urls", mime.urls())
|
|
|
|
# print("text", mime.text())
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2023-07-02 14:23:31 +02:00
|
|
|
internal = False
|
2021-09-05 12:20:27 +02:00
|
|
|
|
2021-09-05 13:54:04 +02:00
|
|
|
mime = gui_hooks.editor_will_process_mime(
|
|
|
|
mime, self, internal, extended, drop_event
|
|
|
|
)
|
2021-09-05 12:20:27 +02:00
|
|
|
|
2016-12-15 09:14:47 +01:00
|
|
|
# try various content types in turn
|
2021-09-05 12:20:27 +02:00
|
|
|
if mime.hasHtml():
|
2021-09-05 15:10:34 +02:00
|
|
|
html_content = mime.html()[11:] if internal else mime.html()
|
|
|
|
return html_content, internal
|
2018-08-08 04:46:51 +02:00
|
|
|
|
|
|
|
# favour url if it's a local link
|
2023-03-22 07:24:50 +01:00
|
|
|
if (
|
|
|
|
mime.hasUrls()
|
|
|
|
and (urls := mime.urls())
|
|
|
|
and urls[0].toString().startswith("file://")
|
|
|
|
):
|
2018-08-08 04:46:51 +02:00
|
|
|
types = (self._processUrls, self._processImage, self._processText)
|
|
|
|
else:
|
|
|
|
types = (self._processImage, self._processUrls, self._processText)
|
|
|
|
|
|
|
|
for fn in types:
|
2020-10-03 16:52:41 +02:00
|
|
|
html = fn(mime, extended)
|
2016-12-15 09:14:47 +01:00
|
|
|
if html:
|
2020-01-26 10:13:31 +01:00
|
|
|
return html, True
|
2016-12-15 09:14:47 +01:00
|
|
|
return "", False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _processUrls(self, mime: QMimeData, extended: bool = False) -> str | None:
|
2016-12-15 09:14:47 +01:00
|
|
|
if not mime.hasUrls():
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2019-09-02 02:17:04 +02:00
|
|
|
buf = ""
|
2020-06-07 05:06:11 +02:00
|
|
|
for qurl in mime.urls():
|
|
|
|
url = qurl.toString()
|
2019-09-02 02:17:04 +02:00
|
|
|
# chrome likes to give us the URL twice with a \n
|
|
|
|
url = url.splitlines()[0]
|
2022-04-21 06:10:53 +02:00
|
|
|
buf += self.editor.urlToLink(url)
|
2019-09-02 02:17:04 +02:00
|
|
|
|
|
|
|
return buf
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _processText(self, mime: QMimeData, extended: bool = False) -> str | None:
|
2016-12-15 09:14:47 +01:00
|
|
|
if not mime.hasText():
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
txt = mime.text()
|
2020-07-24 07:12:46 +02:00
|
|
|
processed = []
|
2020-07-24 04:51:36 +02:00
|
|
|
lines = txt.split("\n")
|
|
|
|
|
|
|
|
for line in lines:
|
2020-07-24 08:18:05 +02:00
|
|
|
for token in re.split(r"(\S+)", line):
|
2020-07-24 04:51:36 +02:00
|
|
|
# inlined data in base64?
|
2020-10-03 16:52:41 +02:00
|
|
|
if extended and token.startswith("data:image/"):
|
2020-07-24 07:12:46 +02:00
|
|
|
processed.append(self.editor.inlinedImageToLink(token))
|
2020-10-03 16:52:41 +02:00
|
|
|
elif extended and self.editor.isURL(token):
|
2022-04-21 06:10:53 +02:00
|
|
|
# if the user is pasting an image or sound link, convert it to local, otherwise paste as a hyperlink
|
2020-07-24 04:51:36 +02:00
|
|
|
link = self.editor.urlToLink(token)
|
2022-04-21 06:10:53 +02:00
|
|
|
processed.append(link)
|
2020-07-24 04:51:36 +02:00
|
|
|
else:
|
|
|
|
token = html.escape(token).replace("\t", " " * 4)
|
2023-03-31 06:02:40 +02:00
|
|
|
|
2020-07-24 04:51:36 +02:00
|
|
|
# if there's more than one consecutive space,
|
|
|
|
# use non-breaking spaces for the second one on
|
2021-02-11 01:09:06 +01:00
|
|
|
def repl(match: Match) -> str:
|
|
|
|
return f"{match.group(1).replace(' ', ' ')} "
|
2020-07-24 08:00:34 +02:00
|
|
|
|
2020-07-24 04:51:36 +02:00
|
|
|
token = re.sub(" ( +)", repl, token)
|
2020-07-24 07:12:46 +02:00
|
|
|
processed.append(token)
|
2020-07-24 04:51:36 +02:00
|
|
|
|
2020-07-24 07:12:46 +02:00
|
|
|
processed.append("<br>")
|
|
|
|
# remove last <br>
|
|
|
|
processed.pop()
|
|
|
|
return "".join(processed)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _processImage(self, mime: QMimeData, extended: bool = False) -> str | None:
|
2018-05-10 08:44:55 +02:00
|
|
|
if not mime.hasImage():
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
im = QImage(mime.imageData())
|
2016-12-15 09:14:47 +01:00
|
|
|
uname = namedtmp("paste")
|
2021-03-10 09:20:37 +01:00
|
|
|
if self.editor.mw.col.get_config_bool(Config.Bool.PASTE_IMAGES_AS_PNG):
|
2012-12-21 08:51:59 +01:00
|
|
|
ext = ".png"
|
2019-12-23 01:34:10 +01:00
|
|
|
im.save(uname + ext, None, 50)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
ext = ".jpg"
|
2019-12-23 01:34:10 +01:00
|
|
|
im.save(uname + ext, None, 80)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# invalid image?
|
2019-12-23 01:34:10 +01:00
|
|
|
path = uname + ext
|
2016-12-15 09:14:47 +01:00
|
|
|
if not os.path.exists(path):
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2020-05-10 02:58:42 +02:00
|
|
|
with open(path, "rb") as file:
|
|
|
|
data = file.read()
|
2018-08-08 04:45:59 +02:00
|
|
|
fname = self.editor._addPastedImage(data, ext)
|
|
|
|
if fname:
|
|
|
|
return self.editor.fnameToLink(fname)
|
2020-06-07 05:06:11 +02:00
|
|
|
return None
|
2012-12-21 08:51:59 +01: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.editing_cut())
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, self.onCut)
|
2021-03-26 04:48:26 +01:00
|
|
|
a = m.addAction(tr.actions_copy())
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, self.onCopy)
|
2021-03-26 04:48:26 +01:00
|
|
|
a = m.addAction(tr.editing_paste())
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, self.onPaste)
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.editor_will_show_context_menu(self, m)
|
2012-12-21 08:51:59 +01:00
|
|
|
m.popup(QCursor.pos())
|
2018-11-12 07:30:11 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-11-15 05:04:08 +01:00
|
|
|
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
|
2018-11-12 07:30:11 +01:00
|
|
|
# - there may be other cases like a trailing 'Bold' that need fixing, but will
|
|
|
|
# wait for further reports first.
|
2021-02-01 08:28:35 +01:00
|
|
|
def fontMungeHack(font: str) -> str:
|
2018-11-15 05:04:08 +01:00
|
|
|
return re.sub(" L$", " Light", font)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def munge_html(txt: str, editor: Editor) -> str:
|
2020-08-08 23:29:28 +02:00
|
|
|
return "" if txt in ("<br>", "<div><br></div>") else txt
|
|
|
|
|
2020-08-09 11:16:19 +02:00
|
|
|
|
2021-02-01 08:28:35 +01:00
|
|
|
def remove_null_bytes(txt: str, editor: Editor) -> str:
|
2020-08-09 10:35:52 +02:00
|
|
|
# misbehaving apps may include a null byte in the text
|
|
|
|
return txt.replace("\x00", "")
|
2020-08-08 23:29:28 +02:00
|
|
|
|
2020-08-09 11:16:19 +02:00
|
|
|
|
2021-07-16 02:37:59 +02:00
|
|
|
def reverse_url_quoting(txt: str, editor: Editor) -> str:
|
|
|
|
# reverse the url quoting we added to get images to display
|
|
|
|
return editor.mw.col.media.escape_media_filenames(txt, unescape=True)
|
|
|
|
|
|
|
|
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.editor_will_use_font_for_field.append(fontMungeHack)
|
2020-08-08 23:29:28 +02:00
|
|
|
gui_hooks.editor_will_munge_html.append(munge_html)
|
2020-08-09 10:35:52 +02:00
|
|
|
gui_hooks.editor_will_munge_html.append(remove_null_bytes)
|
2021-07-16 02:37:59 +02:00
|
|
|
gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
2021-04-08 23:17:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
def set_cloze_button(editor: Editor) -> None:
|
2022-02-03 05:52:11 +01:00
|
|
|
action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide"
|
|
|
|
editor.web.eval(
|
|
|
|
'require("anki/ui").loaded.then(() =>'
|
2022-03-31 05:30:00 +02:00
|
|
|
f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")'
|
2022-02-03 05:52:11 +01:00
|
|
|
"); "
|
|
|
|
)
|
2021-04-08 23:17:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
gui_hooks.editor_did_load_note.append(set_cloze_button)
|