2012-12-21 08:51:59 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
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
|
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
|
2021-01-10 01:10:23 +01:00
|
|
|
from random import randrange
|
2019-12-22 03:30:29 +01:00
|
|
|
from typing import Callable, List, Optional, Tuple
|
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
|
2020-01-02 10:43:19 +01:00
|
|
|
import aqt.sound
|
2020-03-04 17:41:26 +01:00
|
|
|
from anki.cards import Card
|
2021-01-29 18:27:33 +01:00
|
|
|
from anki.collection import dupe_search_term
|
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
|
2020-01-15 22:41:23 +01:00
|
|
|
from anki.notes import Note
|
2021-01-07 07:20:02 +01:00
|
|
|
from anki.utils import checksum, isLin, isWin, namedtmp
|
2020-01-15 03:46:53 +01:00
|
|
|
from aqt import AnkiQt, gui_hooks
|
2020-08-16 18:49:51 +02:00
|
|
|
from aqt.main import ResetReason
|
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 (
|
2020-11-17 08:42:43 +01:00
|
|
|
TR,
|
2021-01-25 14:45:47 +01:00
|
|
|
HelpPage,
|
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
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.webview import AnkiWebView
|
|
|
|
|
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 = (
|
|
|
|
"wav",
|
|
|
|
"mp3",
|
|
|
|
"ogg",
|
|
|
|
"flac",
|
|
|
|
"mp4",
|
|
|
|
"swf",
|
|
|
|
"mov",
|
|
|
|
"mpeg",
|
|
|
|
"mkv",
|
|
|
|
"m4a",
|
|
|
|
"3gp",
|
|
|
|
"spx",
|
|
|
|
"oga",
|
|
|
|
"webm",
|
2020-07-29 14:53:21 +02:00
|
|
|
"mpg",
|
|
|
|
"ogx",
|
|
|
|
"avi",
|
|
|
|
"flv",
|
|
|
|
"ogv",
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
_html = """
|
2017-08-06 05:55:09 +02:00
|
|
|
<style>
|
|
|
|
html { background: %s; }
|
2021-01-22 12:14:20 +01:00
|
|
|
#topbutsOuter { background: %s; }
|
2017-08-06 05:55:09 +02:00
|
|
|
</style>
|
2021-01-21 21:20:57 +01:00
|
|
|
<div>
|
2021-01-22 12:14:20 +01:00
|
|
|
<div id="topbutsOuter">
|
2021-01-22 11:37:11 +01:00
|
|
|
%s
|
2021-01-21 21:20:57 +01:00
|
|
|
</div>
|
|
|
|
<div id="fields">
|
2020-03-23 07:55:48 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="dupes" style="display:none;">
|
|
|
|
<a href="#" onclick="pycmd('dupes');return false;">
|
|
|
|
%s
|
|
|
|
</a>
|
|
|
|
</div>
|
2012-12-21 08:51:59 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
# caller is responsible for resetting note on reset
|
2017-02-06 23:21:33 +01:00
|
|
|
class Editor:
|
2020-01-15 22:41:23 +01:00
|
|
|
def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw = mw
|
|
|
|
self.widget = widget
|
|
|
|
self.parentWindow = parentWindow
|
2020-01-15 22:41:23 +01:00
|
|
|
self.note: Optional[Note] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
self.addMode = addMode
|
2020-01-15 22:41:23 +01:00
|
|
|
self.currentField: Optional[int] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
# current card, for card layout
|
2020-03-04 17:41:26 +01:00
|
|
|
self.card: Optional[Card] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupOuter()
|
|
|
|
self.setupWeb()
|
2017-10-11 20:51:26 +02:00
|
|
|
self.setupShortcuts()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupTags()
|
2020-03-22 17:15:47 +01:00
|
|
|
gui_hooks.editor_did_init(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Initial setup
|
|
|
|
############################################################
|
|
|
|
|
|
|
|
def setupOuter(self):
|
|
|
|
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
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def setupWeb(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web = EditorWebView(self.widget, self)
|
|
|
|
self.web.allowDrops = True
|
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
|
|
|
|
2020-10-04 22:41:18 +02:00
|
|
|
lefttopbtns: List[str] = [
|
|
|
|
self._addButton(
|
|
|
|
None,
|
|
|
|
"fields",
|
2020-11-17 08:42:43 +01:00
|
|
|
tr(TR.EDITING_CUSTOMIZE_FIELDS),
|
|
|
|
tr(TR.EDITING_FIELDS) + "...",
|
2020-10-04 22:41:18 +02:00
|
|
|
disables=False,
|
2020-10-04 23:03:37 +02:00
|
|
|
rightside=False,
|
2020-10-04 22:41:18 +02:00
|
|
|
),
|
|
|
|
self._addButton(
|
|
|
|
None,
|
|
|
|
"cards",
|
2020-11-17 08:42:43 +01:00
|
|
|
tr(TR.EDITING_CUSTOMIZE_CARD_TEMPLATES_CTRLANDL),
|
|
|
|
tr(TR.EDITING_CARDS) + "...",
|
2020-10-04 22:41:18 +02:00
|
|
|
disables=False,
|
2020-10-04 23:03:37 +02:00
|
|
|
rightside=False,
|
2020-10-04 22:41:18 +02:00
|
|
|
),
|
|
|
|
]
|
|
|
|
|
2020-10-04 22:50:02 +02:00
|
|
|
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
|
|
|
|
2019-12-22 13:56:17 +01:00
|
|
|
righttopbtns: List[str] = [
|
2019-12-23 01:34:10 +01:00
|
|
|
self._addButton(
|
2020-11-17 08:42:43 +01:00
|
|
|
"text_bold", "bold", tr(TR.EDITING_BOLD_TEXT_CTRLANDB), id="bold"
|
2019-12-23 01:34:10 +01:00
|
|
|
),
|
|
|
|
self._addButton(
|
2020-11-17 08:42:43 +01:00
|
|
|
"text_italic",
|
|
|
|
"italic",
|
|
|
|
tr(TR.EDITING_ITALIC_TEXT_CTRLANDI),
|
|
|
|
id="italic",
|
2019-12-23 01:34:10 +01:00
|
|
|
),
|
|
|
|
self._addButton(
|
2020-11-17 08:42:43 +01:00
|
|
|
"text_under",
|
|
|
|
"underline",
|
|
|
|
tr(TR.EDITING_UNDERLINE_TEXT_CTRLANDU),
|
|
|
|
id="underline",
|
|
|
|
),
|
|
|
|
self._addButton(
|
|
|
|
"text_super",
|
|
|
|
"super",
|
|
|
|
tr(TR.EDITING_SUPERSCRIPT_CTRLANDAND),
|
|
|
|
id="superscript",
|
|
|
|
),
|
|
|
|
self._addButton(
|
|
|
|
"text_sub", "sub", tr(TR.EDITING_SUBSCRIPT_CTRLAND), id="subscript"
|
|
|
|
),
|
|
|
|
self._addButton(
|
|
|
|
"text_clear", "clear", tr(TR.EDITING_REMOVE_FORMATTING_CTRLANDR)
|
2019-12-23 01:34:10 +01:00
|
|
|
),
|
2020-10-04 22:41:18 +02:00
|
|
|
self._addButton(
|
|
|
|
None,
|
|
|
|
"colour",
|
2020-11-17 08:42:43 +01:00
|
|
|
tr(TR.EDITING_SET_FOREGROUND_COLOUR_F7),
|
2020-10-04 22:41:18 +02:00
|
|
|
"""
|
|
|
|
<div id="forecolor"
|
|
|
|
style="display: inline-block; background: #000; border-radius: 5px;"
|
|
|
|
class="topbut"
|
|
|
|
>""",
|
|
|
|
),
|
|
|
|
self._addButton(
|
|
|
|
None,
|
|
|
|
"changeCol",
|
2020-11-17 08:42:43 +01:00
|
|
|
tr(TR.EDITING_CHANGE_COLOUR_F8),
|
2020-10-04 22:41:18 +02:00
|
|
|
"""
|
|
|
|
<div style="display: inline-block; border-radius: 5px;"
|
|
|
|
class="topbut rainbow"
|
|
|
|
>""",
|
|
|
|
),
|
|
|
|
self._addButton(
|
2020-11-17 08:42:43 +01:00
|
|
|
"text_cloze", "cloze", tr(TR.EDITING_CLOZE_DELETION_CTRLANDSHIFTANDC)
|
|
|
|
),
|
|
|
|
self._addButton(
|
|
|
|
"paperclip", "attach", tr(TR.EDITING_ATTACH_PICTURESAUDIOVIDEO_F3)
|
2020-10-04 22:41:18 +02:00
|
|
|
),
|
2020-11-17 08:42:43 +01:00
|
|
|
self._addButton("media-record", "record", tr(TR.EDITING_RECORD_AUDIO_F5)),
|
2020-10-04 22:41:18 +02:00
|
|
|
self._addButton("more", "more"),
|
2019-12-22 13:56:17 +01:00
|
|
|
]
|
2020-10-04 22:41:18 +02:00
|
|
|
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.editor_did_init_buttons(righttopbtns, self)
|
2020-01-15 03:46:53 +01:00
|
|
|
# legacy filter
|
2017-01-06 16:37:57 +01:00
|
|
|
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
2020-10-04 22:41:18 +02:00
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
topbuts = """
|
2021-01-22 12:14:20 +01:00
|
|
|
<div id="topbutsleft" class="topbuts">
|
2020-10-04 22:41:18 +02:00
|
|
|
%(leftbts)s
|
2017-01-06 15:54:55 +01:00
|
|
|
</div>
|
2021-01-22 12:14:20 +01:00
|
|
|
<div id="topbutsright" class="topbuts">
|
2017-01-06 15:54:55 +01:00
|
|
|
%(rightbts)s
|
|
|
|
</div>
|
2019-12-23 01:34:10 +01:00
|
|
|
""" % dict(
|
2020-10-04 22:41:18 +02:00
|
|
|
leftbts="".join(lefttopbtns),
|
2019-12-23 01:34:10 +01:00
|
|
|
rightbts="".join(righttopbtns),
|
|
|
|
)
|
2020-01-15 22:53:12 +01:00
|
|
|
bgcol = self.mw.app.palette().window().color().name() # type: ignore
|
2017-06-22 10:01:47 +02:00
|
|
|
# then load page
|
2019-12-23 01:34:10 +01:00
|
|
|
self.web.stdHtml(
|
2020-11-17 08:42:43 +01:00
|
|
|
_html % (bgcol, bgcol, topbuts, tr(TR.EDITING_SHOW_DUPLICATES)),
|
2020-11-01 05:26:58 +01:00
|
|
|
css=["css/editor.css"],
|
2020-12-28 14:18:07 +01:00
|
|
|
js=["js/vendor/jquery.min.js", "js/editor.js"],
|
2020-02-12 22:00:13 +01:00
|
|
|
context=self,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-01-20 17:01:16 +01:00
|
|
|
self.web.eval("preventButtonFocus();")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Top buttons
|
|
|
|
######################################################################
|
|
|
|
|
2017-01-08 13:52:33 +01:00
|
|
|
def resourceToData(self, path):
|
|
|
|
"""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())
|
|
|
|
return "data:%s;base64,%s" % (mime, data64.decode("ascii"))
|
|
|
|
|
|
|
|
def addButton(
|
|
|
|
self,
|
2020-10-04 22:42:28 +02:00
|
|
|
icon: Optional[str],
|
2019-12-23 01:34:10 +01:00
|
|
|
cmd: str,
|
|
|
|
func: Callable[["Editor"], None],
|
|
|
|
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,
|
2019-12-23 01:34:10 +01:00
|
|
|
):
|
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-01-10 13:38:20 +01:00
|
|
|
def on_activated():
|
|
|
|
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-01-10 13:38:20 +01:00
|
|
|
def on_hotkey():
|
|
|
|
on_activated()
|
|
|
|
self.web.eval(f'toggleEditorButton("#{id}");')
|
|
|
|
|
|
|
|
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,
|
2020-10-04 22:42:28 +02:00
|
|
|
icon: Optional[str],
|
2019-12-23 01:34:10 +01:00
|
|
|
cmd: str,
|
|
|
|
tip: str = "",
|
|
|
|
label: str = "",
|
|
|
|
id: Optional[str] = None,
|
|
|
|
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:
|
|
|
|
iconstr = "/_anki/imgs/{}.png".format(icon)
|
2019-12-23 01:34:10 +01:00
|
|
|
imgelm = """<img class=topbut src="{}">""".format(iconstr)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2017-08-23 23:48:08 +02:00
|
|
|
imgelm = ""
|
|
|
|
if label or not imgelm:
|
2019-12-23 01:34:10 +01:00
|
|
|
labelelm = """<span class=blabel>{}</span>""".format(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:
|
2019-12-23 01:34:10 +01:00
|
|
|
idstr = "id={}".format(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:
|
|
|
|
class_ = ""
|
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;"
|
|
|
|
>
|
|
|
|
{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
|
2019-12-22 00:12:09 +01:00
|
|
|
cuts: List[Tuple] = [
|
2017-12-04 03:53:28 +01:00
|
|
|
("Ctrl+L", self.onCardLayout, True),
|
2016-06-22 06:52:17 +02:00
|
|
|
("Ctrl+B", self.toggleBold),
|
|
|
|
("Ctrl+I", self.toggleItalic),
|
|
|
|
("Ctrl+U", self.toggleUnderline),
|
2017-07-22 02:54:45 +02:00
|
|
|
("Ctrl++", self.toggleSuper),
|
2016-06-22 06:52:17 +02:00
|
|
|
("Ctrl+=", self.toggleSub),
|
|
|
|
("Ctrl+R", self.removeFormat),
|
|
|
|
("F7", self.onForeground),
|
|
|
|
("F8", self.onChangeCol),
|
|
|
|
("Ctrl+Shift+C", self.onCloze),
|
|
|
|
("Ctrl+Shift+Alt+C", self.onCloze),
|
|
|
|
("F3", self.onAddMedia),
|
|
|
|
("F5", self.onRecSound),
|
|
|
|
("Ctrl+T, T", self.insertLatex),
|
|
|
|
("Ctrl+T, E", self.insertLatexEqn),
|
|
|
|
("Ctrl+T, M", self.insertLatexMathEnv),
|
2017-09-08 11:20:37 +02:00
|
|
|
("Ctrl+M, M", self.insertMathjaxInline),
|
|
|
|
("Ctrl+M, E", self.insertMathjaxBlock),
|
2018-08-06 05:17:57 +02:00
|
|
|
("Ctrl+M, C", self.insertMathjaxChemistry),
|
2016-06-22 06:52:17 +02:00
|
|
|
("Ctrl+Shift+X", self.onHtmlEdit),
|
2019-12-23 01:34:10 +01:00
|
|
|
("Ctrl+Shift+T", self.onFocusTags, True),
|
2016-06-22 06:52:17 +02:00
|
|
|
]
|
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
|
|
|
|
2017-12-04 03:53:28 +01:00
|
|
|
def _addFocusCheck(self, fn):
|
|
|
|
def checkFocus():
|
|
|
|
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
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def onFields(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
self.saveNow(self._onFields)
|
|
|
|
|
|
|
|
def _onFields(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.fields import FieldDialog
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 13:52:48 +02:00
|
|
|
FieldDialog(self.mw, self.note.model(), parent=self.parentWindow)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onCardLayout(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
self.saveNow(self._onCardLayout)
|
|
|
|
|
|
|
|
def _onCardLayout(self):
|
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,
|
|
|
|
fill_empty=self.addMode,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2013-07-23 15:35:00 +02:00
|
|
|
if isWin:
|
|
|
|
self.parentWindow.activateWindow()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# JS->Python bridge
|
|
|
|
######################################################################
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def onBridgeCmd(self, cmd) -> None:
|
2020-01-15 03:46:53 +01:00
|
|
|
if not self.note:
|
2012-12-21 08:51:59 +01:00
|
|
|
# shutdown
|
|
|
|
return
|
|
|
|
# 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"):
|
|
|
|
(type, ord, nid, txt) = cmd.split(":", 3)
|
|
|
|
ord = int(ord)
|
|
|
|
try:
|
|
|
|
nid = int(nid)
|
|
|
|
except ValueError:
|
|
|
|
nid = 0
|
|
|
|
if nid != self.note.id:
|
|
|
|
print("ignored late blur")
|
|
|
|
return
|
2020-08-09 10:38:31 +02:00
|
|
|
|
|
|
|
self.note.fields[ord] = self.mungeHTML(txt)
|
|
|
|
|
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:
|
|
|
|
self.note.flush()
|
2020-08-16 18:49:51 +02:00
|
|
|
self.mw.requireReset(reason=ResetReason.EditorBridgeCmd, context=self)
|
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
|
|
|
|
self.mw.progress.timer(100, self.loadNoteKeepingFocus, False)
|
|
|
|
else:
|
|
|
|
self.checkValid()
|
|
|
|
else:
|
|
|
|
gui_hooks.editor_did_fire_typing_timer(self.note)
|
|
|
|
self.checkValid()
|
|
|
|
# focused into field?
|
|
|
|
elif cmd.startswith("focus"):
|
|
|
|
(type, num) = cmd.split(":", 1)
|
|
|
|
self.currentField = int(num)
|
|
|
|
gui_hooks.editor_did_focus_field(self.note, self.currentField)
|
|
|
|
elif cmd in self._links:
|
|
|
|
self._links[cmd](self)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2016-06-22 06:52:17 +02:00
|
|
|
print("uncaught cmd", cmd)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-01-25 23:12:48 +01:00
|
|
|
def mungeHTML(self, txt):
|
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
|
|
|
|
######################################################################
|
|
|
|
|
2017-08-05 07:15:19 +02:00
|
|
|
def setNote(self, note, hide=True, focusTo=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)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
self.hideCompleters()
|
|
|
|
if hide:
|
|
|
|
self.widget.hide()
|
|
|
|
|
2017-12-04 03:03:01 +01:00
|
|
|
def loadNoteKeepingFocus(self):
|
|
|
|
self.loadNote(self.currentField)
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def loadNote(self, focusTo=None) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.note:
|
|
|
|
return
|
2017-08-05 07:15:19 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
data = [
|
2020-11-10 14:50:17 +01:00
|
|
|
(fld, self.mw.col.media.escape_media_filenames(val))
|
|
|
|
for fld, val in self.note.items()
|
2019-12-23 01:34:10 +01:00
|
|
|
]
|
2012-12-21 08:51:59 +01:00
|
|
|
self.widget.show()
|
2017-08-05 07:15:19 +02:00
|
|
|
self.updateTags()
|
|
|
|
|
|
|
|
def oncallback(arg):
|
|
|
|
if not self.note:
|
|
|
|
return
|
|
|
|
self.setupForegroundButton()
|
|
|
|
self.checkValid()
|
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
|
|
|
|
2019-12-22 23:44:43 +01:00
|
|
|
js = "setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s)" % (
|
2019-12-23 01:34:10 +01:00
|
|
|
json.dumps(data),
|
|
|
|
json.dumps(self.fonts()),
|
|
|
|
json.dumps(focusTo),
|
|
|
|
json.dumps(self.note.id),
|
|
|
|
)
|
2020-03-24 10:17:01 +01:00
|
|
|
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
2019-12-22 23:44:43 +01:00
|
|
|
self.web.evalWithCallback(js, oncallback)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-15 22:53:12 +01: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"])
|
2019-12-23 01:34:10 +01:00
|
|
|
for f in self.note.model()["flds"]
|
|
|
|
]
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2018-05-28 05:40:35 +02:00
|
|
|
def saveNow(self, callback, keepFocus=False):
|
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
|
2017-08-16 03:49:33 +02:00
|
|
|
self.mw.progress.timer(10, callback, False)
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
|
|
|
self.saveTags()
|
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
|
|
|
|
|
|
|
def checkValid(self):
|
2020-01-23 06:08:10 +01:00
|
|
|
cols = [""] * len(self.note.fields)
|
2012-12-21 08:51:59 +01:00
|
|
|
err = self.note.dupeOrEmpty()
|
|
|
|
if err == 2:
|
2020-01-23 06:08:10 +01:00
|
|
|
cols[0] = "dupe"
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.eval("showDupes();")
|
|
|
|
else:
|
|
|
|
self.web.eval("hideDupes();")
|
|
|
|
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
|
|
|
|
|
|
|
def showDupes(self):
|
2021-01-29 18:27:33 +01:00
|
|
|
self.mw.browser_search(
|
|
|
|
dupe_search_term(self.note.model()["id"], self.note.fields[0])
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-11-21 02:18:14 +01:00
|
|
|
def fieldsAreBlank(self, previousNote=None):
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.note:
|
|
|
|
return True
|
2013-05-24 05:04:28 +02:00
|
|
|
m = self.note.model()
|
|
|
|
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
|
|
|
|
|
2017-08-16 04:45:33 +02:00
|
|
|
def cleanup(self):
|
|
|
|
self.setNote(None)
|
|
|
|
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
|
|
|
self.web = None
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# HTML editing
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def onHtmlEdit(self):
|
2017-08-05 07:15:19 +02:00
|
|
|
field = self.currentField
|
|
|
|
self.saveNow(lambda: self._onHtmlEdit(field))
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2017-08-05 07:15:19 +02:00
|
|
|
def _onHtmlEdit(self, field):
|
2020-05-29 00:43:33 +02:00
|
|
|
d = QDialog(self.widget, Qt.Window)
|
2012-12-21 08:51:59 +01:00
|
|
|
form = aqt.forms.edithtml.Ui_Dialog()
|
|
|
|
form.setupUi(d)
|
2020-05-27 12:04:00 +02:00
|
|
|
restoreGeom(d, "htmlEditor")
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(d)
|
2021-01-25 14:45:47 +01:00
|
|
|
qconnect(
|
|
|
|
form.buttonBox.helpRequested, lambda: openHelp(HelpPage.EDITING_FEATURES)
|
|
|
|
)
|
2017-08-05 07:15:19 +02:00
|
|
|
form.textEdit.setPlainText(self.note.fields[field])
|
2020-02-10 00:25:11 +01:00
|
|
|
d.show()
|
2012-12-21 08:51:59 +01:00
|
|
|
form.textEdit.moveCursor(QTextCursor.End)
|
|
|
|
d.exec_()
|
|
|
|
html = form.textEdit.toPlainText()
|
2020-03-13 01:17:32 +01:00
|
|
|
if html.find(">") > -1:
|
|
|
|
# filter html through beautifulsoup so we can strip out things like a
|
|
|
|
# leading </div>
|
2020-11-10 14:50:17 +01:00
|
|
|
html_escaped = self.mw.col.media.escape_media_filenames(html)
|
2020-05-27 12:04:49 +02:00
|
|
|
with warnings.catch_warnings():
|
2020-03-13 01:17:32 +01:00
|
|
|
warnings.simplefilter("ignore", UserWarning)
|
2020-06-09 03:21:48 +02:00
|
|
|
html_escaped = str(BeautifulSoup(html_escaped, "html.parser"))
|
2020-11-10 14:50:17 +01:00
|
|
|
html = self.mw.col.media.escape_media_filenames(
|
|
|
|
html_escaped, unescape=True
|
|
|
|
)
|
2017-08-05 07:15:19 +02:00
|
|
|
self.note.fields[field] = html
|
2020-05-20 06:59:22 +02:00
|
|
|
if not self.addMode:
|
|
|
|
self.note.flush()
|
2017-08-05 07:15:19 +02:00
|
|
|
self.loadNote(focusTo=field)
|
2020-05-27 12:04:00 +02:00
|
|
|
saveGeom(d, "htmlEditor")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Tag handling
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def setupTags(self):
|
|
|
|
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
|
2020-11-17 08:42:43 +01:00
|
|
|
l = QLabel(tr(TR.EDITING_TAGS))
|
2012-12-21 08:51:59 +01:00
|
|
|
tb.addWidget(l, 1, 0)
|
|
|
|
self.tags = aqt.tagedit.TagEdit(self.widget)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.tags.lostFocus, self.saveTags)
|
2020-11-17 08:42:43 +01:00
|
|
|
self.tags.setToolTip(
|
|
|
|
shortcut(tr(TR.EDITING_JUMP_TO_TAGS_WITH_CTRLANDSHIFTANDT))
|
|
|
|
)
|
2020-01-26 09:47:28 +01:00
|
|
|
border = theme_manager.str_color("border")
|
|
|
|
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)
|
|
|
|
|
|
|
|
def updateTags(self):
|
|
|
|
if self.tags.col != self.mw.col:
|
|
|
|
self.tags.setCol(self.mw.col)
|
|
|
|
if not self.tags.text() or not self.addMode:
|
|
|
|
self.tags.setText(self.note.stringTags().strip())
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def saveTags(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.note:
|
|
|
|
return
|
2020-05-07 09:54:23 +02:00
|
|
|
self.note.tags = self.mw.col.tags.split(self.tags.text())
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.addMode:
|
|
|
|
self.note.flush()
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.editor_did_update_tags(self.note)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def saveAddModeVars(self):
|
|
|
|
if self.addMode:
|
|
|
|
# save tags to model
|
|
|
|
m = self.note.model()
|
2019-12-23 01:34:10 +01:00
|
|
|
m["tags"] = self.note.tags
|
2019-12-16 11:27:58 +01:00
|
|
|
self.mw.col.models.save(m, updateReqs=False)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def hideCompleters(self):
|
|
|
|
self.tags.hideCompleter()
|
|
|
|
|
2017-08-05 07:15:19 +02:00
|
|
|
def onFocusTags(self):
|
|
|
|
self.tags.setFocus()
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Format buttons
|
|
|
|
######################################################################
|
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
def toggleBold(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.eval("setFormat('bold');")
|
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
def toggleItalic(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.eval("setFormat('italic');")
|
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
def toggleUnderline(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.eval("setFormat('underline');")
|
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
def toggleSuper(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.eval("setFormat('superscript');")
|
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
def toggleSub(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.eval("setFormat('subscript');")
|
|
|
|
|
|
|
|
def removeFormat(self):
|
|
|
|
self.web.eval("setFormat('removeFormat');")
|
|
|
|
|
|
|
|
def onCloze(self):
|
2018-05-28 05:40:35 +02:00
|
|
|
self.saveNow(self._onCloze, keepFocus=True)
|
|
|
|
|
|
|
|
def _onCloze(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
# check that the model is set up for cloze deletion
|
2019-12-23 01:34:10 +01:00
|
|
|
if not re.search("{{(.*:)*cloze:", self.note.model()["tmpls"][0]["qfmt"]):
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.addMode:
|
2020-11-18 02:32:22 +01:00
|
|
|
tooltip(tr(TR.EDITING_WARNING_CLOZE_DELETIONS_WILL_NOT_WORK))
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2020-11-18 02:32:22 +01:00
|
|
|
showInfo(tr(TR.EDITING_TO_MAKE_A_CLOZE_DELETION_ON))
|
2013-05-17 08:57:22 +02:00
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
# find the highest existing cloze
|
|
|
|
highest = 0
|
2016-05-12 06:45:35 +02:00
|
|
|
for name, val in list(self.note.items()):
|
2017-12-13 05:34:54 +01:00
|
|
|
m = re.findall(r"\{\{c(\d+)::", val)
|
2012-12-21 08:51:59 +01:00
|
|
|
if m:
|
|
|
|
highest = max(highest, sorted([int(x) for x in m])[-1])
|
|
|
|
# reuse last?
|
|
|
|
if not self.mw.app.keyboardModifiers() & Qt.AltModifier:
|
|
|
|
highest += 1
|
|
|
|
# must start at 1
|
|
|
|
highest = max(1, highest)
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('{{c%d::', '}}');" % highest)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Foreground colour
|
|
|
|
######################################################################
|
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
def setupForegroundButton(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
|
|
|
|
self.onColourChanged()
|
|
|
|
|
|
|
|
# use last colour
|
|
|
|
def onForeground(self):
|
|
|
|
self._wrapWithColour(self.fcolour)
|
|
|
|
|
|
|
|
# choose new colour
|
|
|
|
def onChangeCol(self):
|
2020-07-05 13:01:27 +02:00
|
|
|
if isLin:
|
|
|
|
new = QColorDialog.getColor(
|
|
|
|
QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
new = QColorDialog.getColor(QColor(self.fcolour), None)
|
2012-12-21 08:51:59 +01:00
|
|
|
# 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)
|
|
|
|
|
|
|
|
def _updateForegroundButton(self):
|
2016-06-22 06:52:17 +02:00
|
|
|
self.web.eval("setFGButton('%s')" % self.fcolour)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onColourChanged(self):
|
|
|
|
self._updateForegroundButton()
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.pm.profile["lastColour"] = self.fcolour
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def _wrapWithColour(self, colour):
|
|
|
|
self.web.eval("setFormat('forecolor', '%s')" % colour)
|
|
|
|
|
|
|
|
# Audio/video/images
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def onAddMedia(self):
|
2019-12-23 01:34:10 +01:00
|
|
|
extension_filter = " ".join(
|
|
|
|
"*." + extension for extension in sorted(itertools.chain(pics, audio))
|
|
|
|
)
|
2020-11-17 08:42:43 +01:00
|
|
|
key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def accept(file):
|
|
|
|
self.addMedia(file, canDelete=True)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-11-17 08:42:43 +01:00
|
|
|
file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media")
|
2012-12-21 08:51:59 +01:00
|
|
|
self.parentWindow.activateWindow()
|
|
|
|
|
|
|
|
def addMedia(self, path, canDelete=False):
|
2020-07-01 03:19:06 +02:00
|
|
|
try:
|
|
|
|
html = self._addMedia(path, canDelete)
|
|
|
|
except Exception as e:
|
|
|
|
showWarning(str(e))
|
|
|
|
return
|
2020-12-30 10:30:23 +01:00
|
|
|
self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def _addMedia(self, path, canDelete=False):
|
2013-07-11 10:21:16 +02:00
|
|
|
"Add to media folder and return local img or sound tag."
|
2012-12-21 08:51:59 +01:00
|
|
|
# copy to media folder
|
2013-07-11 10:21:16 +02:00
|
|
|
fname = self.mw.col.media.addFile(path)
|
2012-12-21 08:51:59 +01:00
|
|
|
# remove original?
|
2019-12-23 01:34:10 +01:00
|
|
|
if canDelete and self.mw.pm.profile["deleteMedia"]:
|
2013-07-11 10:21:16 +02:00
|
|
|
if os.path.abspath(fname) != os.path.abspath(path):
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
|
|
|
os.unlink(path)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
# 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:
|
2018-08-08 03:38:45 +02:00
|
|
|
return self.mw.col.media.writeData(fname, data)
|
2018-05-01 05:16:46 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def onRecSound(self):
|
2020-12-16 10:09:45 +01:00
|
|
|
aqt.sound.record_audio(
|
|
|
|
self.parentWindow,
|
|
|
|
self.mw,
|
|
|
|
True,
|
|
|
|
lambda file: self.addMedia(file, canDelete=True),
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2013-07-11 10:21:16 +02:00
|
|
|
# Media downloads
|
|
|
|
######################################################################
|
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def urlToLink(self, url: str) -> Optional[str]:
|
2013-07-11 10:21:16 +02:00
|
|
|
fname = self.urlToFile(url)
|
|
|
|
if not fname:
|
2018-09-23 13:39:04 +02:00
|
|
|
return None
|
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"))
|
2013-07-11 10:21:16 +02:00
|
|
|
return '<img src="%s">' % name
|
|
|
|
else:
|
2020-09-04 00:34:26 +02:00
|
|
|
av_player.play_file(fname)
|
2020-09-04 01:36:18 +02:00
|
|
|
return "[sound:%s]" % html.escape(fname, quote=False)
|
2013-07-11 10:21:16 +02:00
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def urlToFile(self, url: str) -> Optional[str]:
|
2013-07-11 10:21:16 +02:00
|
|
|
l = url.lower()
|
2019-12-23 01:34:10 +01:00
|
|
|
for suffix in pics + audio:
|
2017-10-18 13:58:36 +02:00
|
|
|
if l.endswith("." + 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
|
|
|
|
2013-07-18 13:32:41 +02:00
|
|
|
def isURL(self, s):
|
|
|
|
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"
|
2019-12-23 01:34:10 +01:00
|
|
|
return self._addPastedImage(data, "." + 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)
|
|
|
|
fname = "{}-{}{}".format("paste", csum, ext)
|
|
|
|
return self._addMediaFromData(fname, data)
|
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def _retrieveURL(self, url: str) -> Optional[str]:
|
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
|
2020-05-20 08:12:41 +02:00
|
|
|
error_msg: Optional[str] = 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:
|
2020-11-17 12:47:47 +01:00
|
|
|
error_msg = tr(
|
|
|
|
TR.QT_MISC_UNEXPECTED_RESPONSE_CODE,
|
|
|
|
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:
|
2020-11-17 12:47:47 +01:00
|
|
|
error_msg = tr(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"
|
2019-12-23 01:34:10 +01:00
|
|
|
self.web.eval(
|
2020-06-07 05:06:11 +02:00
|
|
|
"pasteHTML(%s, %s, %s);" % (json.dumps(html), json.dumps(internal), ext)
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
2020-10-03 16:52:41 +02:00
|
|
|
def doDrop(self, html: str, internal: bool, extended: bool = False) -> None:
|
2020-08-19 06:37:14 +02:00
|
|
|
def pasteIfField(ret):
|
|
|
|
if ret:
|
2020-10-03 16:52:41 +02:00
|
|
|
self.doPaste(html, internal, extended)
|
2020-08-19 06:37:14 +02:00
|
|
|
|
|
|
|
p = self.web.mapFromGlobal(QCursor.pos())
|
2020-08-25 16:23:34 +02:00
|
|
|
self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
def onPaste(self):
|
|
|
|
self.web.onPaste()
|
|
|
|
|
2017-09-02 05:48:03 +02:00
|
|
|
def onCutOrCopy(self):
|
|
|
|
self.web.flagAnkiText()
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Advanced menu
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def onAdvanced(self):
|
|
|
|
m = QMenu(self.mw)
|
2019-12-22 13:56:17 +01:00
|
|
|
|
|
|
|
for text, handler, shortcut in (
|
2020-11-17 08:42:43 +01:00
|
|
|
(tr(TR.EDITING_MATHJAX_INLINE), self.insertMathjaxInline, "Ctrl+M, M"),
|
|
|
|
(tr(TR.EDITING_MATHJAX_BLOCK), self.insertMathjaxBlock, "Ctrl+M, E"),
|
|
|
|
(
|
|
|
|
tr(TR.EDITING_MATHJAX_CHEMISTRY),
|
|
|
|
self.insertMathjaxChemistry,
|
|
|
|
"Ctrl+M, C",
|
|
|
|
),
|
|
|
|
(tr(TR.EDITING_LATEX), self.insertLatex, "Ctrl+T, T"),
|
|
|
|
(tr(TR.EDITING_LATEX_EQUATION), self.insertLatexEqn, "Ctrl+T, E"),
|
|
|
|
(tr(TR.EDITING_LATEX_MATH_ENV), self.insertLatexMathEnv, "Ctrl+T, M"),
|
|
|
|
(tr(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)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
m.exec_(QCursor.pos())
|
|
|
|
|
|
|
|
# LaTeX
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def insertLatex(self):
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('[latex]', '[/latex]');")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def insertLatexEqn(self):
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('[$]', '[/$]');")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def insertLatexMathEnv(self):
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('[$$]', '[/$$]');")
|
2017-09-08 11:20:37 +02:00
|
|
|
|
|
|
|
def insertMathjaxInline(self):
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('\\\\(', '\\\\)');")
|
2017-09-08 11:20:37 +02:00
|
|
|
|
|
|
|
def insertMathjaxBlock(self):
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('\\\\[', '\\\\]');")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2018-08-06 05:17:57 +02:00
|
|
|
def insertMathjaxChemistry(self):
|
2020-01-16 01:33:36 +01:00
|
|
|
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
2018-08-06 05:17:57 +02:00
|
|
|
|
2016-06-22 06:52:17 +02:00
|
|
|
# Links from HTML
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
_links = dict(
|
|
|
|
fields=onFields,
|
|
|
|
cards=onCardLayout,
|
|
|
|
bold=toggleBold,
|
|
|
|
italic=toggleItalic,
|
|
|
|
underline=toggleUnderline,
|
|
|
|
super=toggleSuper,
|
|
|
|
sub=toggleSub,
|
|
|
|
clear=removeFormat,
|
|
|
|
colour=onForeground,
|
|
|
|
changeCol=onChangeCol,
|
|
|
|
cloze=onCloze,
|
|
|
|
attach=onAddMedia,
|
|
|
|
record=onRecSound,
|
|
|
|
more=onAdvanced,
|
|
|
|
dupes=showDupes,
|
2016-12-15 09:14:47 +01:00
|
|
|
paste=onPaste,
|
2017-09-02 05:48:03 +02:00
|
|
|
cutOrCopy=onCutOrCopy,
|
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):
|
2012-12-21 08:51:59 +01:00
|
|
|
def __init__(self, parent, editor):
|
2020-02-12 21:03:11 +01:00
|
|
|
AnkiWebView.__init__(self, title="editor")
|
2012-12-21 08:51:59 +01:00
|
|
|
self.editor = editor
|
2019-12-23 01:34:10 +01:00
|
|
|
self.strip = self.editor.mw.pm.profile["stripHTML"]
|
2016-06-06 09:54:39 +02:00
|
|
|
self.setAcceptDrops(True)
|
2017-08-31 10:10:37 +02:00
|
|
|
self._markInternal = False
|
|
|
|
clip = self.editor.mw.app.clipboard()
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(clip.dataChanged, self._onClipboardChange)
|
2020-03-16 04:34:42 +01:00
|
|
|
gui_hooks.editor_web_view_did_init(self)
|
2017-08-31 10:10:37 +02:00
|
|
|
|
|
|
|
def _onClipboardChange(self):
|
|
|
|
if self._markInternal:
|
|
|
|
self._markInternal = False
|
|
|
|
self._flagAnkiText()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onCut(self):
|
2016-06-06 09:54:39 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.Cut)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onCopy(self):
|
2016-06-06 09:54:39 +02:00
|
|
|
self.triggerPageAction(QWebEnginePage.Copy)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-09-14 16:07:31 +02:00
|
|
|
def _wantsExtendedPaste(self) -> bool:
|
2019-12-06 04:37:50 +01:00
|
|
|
extended = not (self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier)
|
2020-01-16 03:36:04 +01:00
|
|
|
if self.editor.mw.pm.profile.get("pasteInvert", False):
|
|
|
|
extended = not extended
|
2020-09-14 16:07:31 +02:00
|
|
|
return extended
|
|
|
|
|
|
|
|
def _onPaste(self, mode: QClipboard.Mode) -> None:
|
|
|
|
extended = self._wantsExtendedPaste()
|
2018-03-02 02:16:02 +01:00
|
|
|
mime = self.editor.mw.app.clipboard().mimeData(mode=mode)
|
2020-10-03 16:52:41 +02:00
|
|
|
html, internal = self._processMime(mime, extended)
|
2016-12-15 09:14:47 +01:00
|
|
|
if not html:
|
|
|
|
return
|
2018-02-05 04:40:56 +01:00
|
|
|
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:
|
2018-03-02 02:16:02 +01:00
|
|
|
self._onPaste(QClipboard.Clipboard)
|
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def onMiddleClickPaste(self) -> None:
|
2018-03-02 02:16:02 +01:00
|
|
|
self._onPaste(QClipboard.Selection)
|
|
|
|
|
2020-08-19 06:37:14 +02:00
|
|
|
def dragEnterEvent(self, evt):
|
|
|
|
evt.accept()
|
|
|
|
|
2016-12-15 09:14:47 +01:00
|
|
|
def dropEvent(self, evt):
|
2020-10-03 16:52:41 +02:00
|
|
|
extended = self._wantsExtendedPaste()
|
2016-12-15 09:14:47 +01:00
|
|
|
mime = evt.mimeData()
|
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:
|
2020-10-03 16:52:41 +02:00
|
|
|
html, internal = self._processMime(mime, extended)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
if not html:
|
|
|
|
return
|
|
|
|
|
2020-10-03 16:52:41 +02:00
|
|
|
self.editor.doDrop(html, internal, extended)
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
# returns (html, isInternal)
|
2020-10-03 16:52:41 +02:00
|
|
|
def _processMime(self, mime: QMimeData, extended: bool = False) -> 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
|
|
|
|
|
|
|
# try various content types in turn
|
|
|
|
html, internal = self._processHtml(mime)
|
|
|
|
if html:
|
|
|
|
return html, internal
|
2018-08-08 04:46:51 +02:00
|
|
|
|
|
|
|
# favour url if it's a local link
|
|
|
|
if mime.hasUrls() and mime.urls()[0].toString().startswith("file://"):
|
|
|
|
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
|
|
|
|
2020-10-03 16:52:41 +02:00
|
|
|
def _processUrls(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
|
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]
|
|
|
|
buf += self.editor.urlToLink(url) or ""
|
|
|
|
|
|
|
|
return buf
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-10-03 16:52:41 +02:00
|
|
|
def _processText(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
|
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):
|
2020-07-24 04:51:36 +02:00
|
|
|
# if the user is pasting an image or sound link, convert it to local
|
|
|
|
link = self.editor.urlToLink(token)
|
|
|
|
if link:
|
2020-07-24 07:12:46 +02:00
|
|
|
processed.append(link)
|
2020-07-24 04:51:36 +02:00
|
|
|
else:
|
|
|
|
# not media; add it as a normal link
|
2020-10-11 15:09:56 +02:00
|
|
|
link = '<a href="{}">{}</a>'.format(
|
|
|
|
token, html.escape(urllib.parse.unquote(token))
|
|
|
|
)
|
2020-07-24 07:12:46 +02:00
|
|
|
processed.append(link)
|
2020-07-24 04:51:36 +02:00
|
|
|
else:
|
|
|
|
token = html.escape(token).replace("\t", " " * 4)
|
|
|
|
# if there's more than one consecutive space,
|
|
|
|
# use non-breaking spaces for the second one on
|
|
|
|
def repl(match):
|
|
|
|
return 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
|
|
|
|
2020-06-07 05:06:11 +02:00
|
|
|
def _processHtml(self, mime: QMimeData) -> Tuple[Optional[str], bool]:
|
2016-12-15 09:14:47 +01:00
|
|
|
if not mime.hasHtml():
|
|
|
|
return None, False
|
2012-12-21 08:51:59 +01:00
|
|
|
html = mime.html()
|
2016-12-15 09:14:47 +01:00
|
|
|
|
|
|
|
# no filtering required for internal pastes
|
|
|
|
if html.startswith("<!--anki-->"):
|
|
|
|
return html[11:], True
|
|
|
|
|
|
|
|
return html, False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-10-03 16:52:41 +02:00
|
|
|
def _processImage(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
|
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")
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.editor.mw.pm.profile.get("pastePNG", False):
|
|
|
|
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
|
|
|
|
2017-09-02 05:48:03 +02:00
|
|
|
def flagAnkiText(self):
|
|
|
|
# be ready to adjust when clipboard event fires
|
|
|
|
self._markInternal = True
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def _flagAnkiText(self):
|
|
|
|
# add a comment in the clipboard html so we can tell text is copied
|
|
|
|
# from us and doesn't need to be stripped
|
|
|
|
clip = self.editor.mw.app.clipboard()
|
2019-04-29 04:30:52 +02:00
|
|
|
if not isMac and not clip.ownsClipboard():
|
2019-01-26 04:09:29 +01:00
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
mime = clip.mimeData()
|
|
|
|
if not mime.hasHtml():
|
|
|
|
return
|
|
|
|
html = mime.html()
|
2017-08-31 10:10:37 +02:00
|
|
|
mime.setHtml("<!--anki-->" + html)
|
|
|
|
clip.setMimeData(mime)
|
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)
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(TR.EDITING_CUT))
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, self.onCut)
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(TR.ACTIONS_COPY))
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, self.onCopy)
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(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.
|
|
|
|
def fontMungeHack(font):
|
2018-11-15 05:04:08 +01:00
|
|
|
return re.sub(" L$", " Light", font)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
|
2020-08-08 23:29:28 +02:00
|
|
|
def munge_html(txt, editor):
|
|
|
|
return "" if txt in ("<br>", "<div><br></div>") else txt
|
|
|
|
|
2020-08-09 11:16:19 +02:00
|
|
|
|
2020-08-09 10:35:52 +02:00
|
|
|
def remove_null_bytes(txt, editor):
|
|
|
|
# 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
|
|
|
|
2020-08-09 10:37:38 +02:00
|
|
|
def reverse_url_quoting(txt, editor):
|
|
|
|
# reverse the url quoting we added to get images to display
|
2020-11-10 14:50:17 +01:00
|
|
|
return editor.mw.col.media.escape_media_filenames(txt, unescape=True)
|
2020-08-09 10:37:38 +02:00
|
|
|
|
2020-08-09 11:16:19 +02:00
|
|
|
|
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)
|
2020-08-09 10:37:38 +02:00
|
|
|
gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|