Store field content instead of trying to mutate clipboard data

Hopefully this will finally address the clipboard issues some users
have been experiencing.

Tentatively closes #2349
This commit is contained in:
Damien Elmes 2023-07-02 22:23:31 +10:00
parent e63be7083c
commit d0798ae7e0

View File

@ -966,7 +966,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.web.onPaste()
def onCutOrCopy(self) -> None:
self.web.flagAnkiText()
self.web.user_cut_or_copied()
# Legacy editing routines
######################################################################
@ -1215,15 +1215,34 @@ class EditorWebView(AnkiWebView):
AnkiWebView.__init__(self, kind=AnkiWebViewKind.EDITOR)
self.editor = editor
self.setAcceptDrops(True)
self._markInternal = False
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
clip = self.editor.mw.app.clipboard()
qconnect(clip.dataChanged, self._onClipboardChange)
qconnect(clip.dataChanged, self._on_clipboard_change)
gui_hooks.editor_web_view_did_init(self)
def _onClipboardChange(self) -> None:
if self._markInternal:
self._markInternal = False
self._flagAnkiText()
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()
def onCut(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.Cut)
@ -1241,11 +1260,15 @@ class EditorWebView(AnkiWebView):
def _onPaste(self, mode: QClipboard.Mode) -> None:
extended = self._wantsExtendedPaste()
mime = self.editor.mw.app.clipboard().mimeData(mode=mode)
html, internal = self._processMime(mime, extended)
if not html:
return
self.editor.doPaste(html, internal, extended)
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)
def onPaste(self) -> None:
self._onPaste(QClipboard.Mode.Clipboard)
@ -1282,7 +1305,7 @@ class EditorWebView(AnkiWebView):
# print("urls", mime.urls())
# print("text", mime.text())
internal = mime.html().startswith("<!--anki-->")
internal = False
mime = gui_hooks.editor_will_process_mime(
mime, self, internal, extended, drop_event
@ -1379,35 +1402,6 @@ class EditorWebView(AnkiWebView):
return self.editor.fnameToLink(fname)
return None
def flagAnkiText(self) -> None:
# be ready to adjust when clipboard event fires
self._markInternal = True
def _flagAnkiText(self) -> None:
# 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()
if not is_mac and not clip.ownsClipboard():
return
mime = clip.mimeData()
if not mime.hasHtml():
return
html = f"<!--anki-->{mime.html()}"
def after_delay() -> None:
# utilities that modify the clipboard can invalidate our existing
# mime handle in the time it takes for the timer to fire, so we need
# to fetch the data again
mime = clip.mimeData()
mime.setHtml(html)
clip.setMimeData(mime)
# Mutter bugs out if the clipboard data is mutated in the clipboard change
# hook, so we need to do it after a delay. Initially 10ms appeared to be
# enough, but we've had a recent report than 175ms+ was required on their
# system.
aqt.mw.progress.timer(300 if is_lin else 10, after_delay, False, parent=self)
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
m = QMenu(self)
a = m.addAction(tr.editing_cut())