https://bugs.kde.org/show_bug.cgi?id=446581

--- Comment #8 from nyanpasu64 <nyanpas...@tuta.io> ---
## Popping up menus upon copying URLs

At
https://invent.kde.org/plasma/plasma-workspace/-/blob/master/klipper/klipper.cpp#L825-833,
you call URLGrabber::checkNewData() (which pops up a menu if Klipper's Action
Menu is enabled and you're not copying from Firefox), *then* skip assigning
lastURLGrabberText if the text matches. I believe the intent was to skip
calling URLGrabber::checkNewData() if the text matches, but that would mean
copying the same text twice doesn't popup a menu the second time. Should this
be changed to the design intent (which changes clipboard behavior), or should
we remove m_lastURLGrabberTextSelection and m_lastURLGrabberTextClipboard
altogether?

## Fixing the bug through asynchronous work

On the topic of this bug, QMimeData::text() is blocking and there's no
continuation-based alternative (even though the underlying xcb is
asynchronous).

- The hard solution is to get text through raw xcb (and I'm not familiar with X
so I can't do this without a lot of new learning).
- The easiest solution I can think of, which minimizes learning unknown APIs
and code changes, is to move fetching clipboard contents (which can block) to
either a new thread or a job in QThreadPool::globalInstance().
- This job will call HistoryItem::create(clipData), and sends the data back to
a new Klipper::onClipData(HistoryItemPtr) method called in the main thread,
which inserts the resulting HistoryItemPtr into history (hopefully in the same
order as you copied text).
- How do we send a HistoryItemPtr back to Klipper on the main thread?
    - Add our own message queue which workers push onto, and having the Qt
event loop poll it and invoke Klipper (probably difficult to achieve).
    - The worker job holds a QPointer<Klipper> and sends a custom QEvent
subclass there. Overload QObject::event() in Klipper to dispatch on our new
event ID. (Creates a use-after-free if the main thread deletes Klipper after it
checks it's alive but before it sends a message.)
    - The worker job holds a QPointer<Klipper> and calls
QMetaObject::invokeMethod(Klipper*, &Klipper::onClipData, HistoryItemPtr)
(which internally sends a QMetaCallEvent, but Qt handles invoking the right
method for us). (Creates a use-after-free if the main thread deletes Klipper
after it checks it's alive but before it sends a message.)
    - Give the worker job a signal clipDataReady(HistoryItemPtr), and connect
it to Klipper::onClipData(HistoryItemPtr) with a QueuedConnection. (I don't
like dynamically adding connections, but it's immune to use-after-free I hope.
But QRunnable can't have signals unless you inherit from QObject *and*
QRunnable! https://forum.qt.io/post/357008)
    - Let the worker job update a QFutureInterface (stable but undocumented
internal Qt type, similar to the documented QPromise in Qt 6), wrap a
QFutureWatcher around the QFuture, and connect to
Klipper::onClipData(HistoryItemPtr). At this point it's so complicated I'd
rather inherit from QObject and QRunnable.
    - Should we wait for all worker threads to finish before destroying
Klipper? Probably doesn't matter, it's fine to destroy the signal connections
with Klipper, and let the background threads work until the process exits.
(Does QApplication::exec() wait for all QThreadPool tasks to finish? I don't
know. I sure hope that we don't get stuck background threads preventing
plasmashell termination.)

The least bad solution I've come up with is a worker object inheriting from
QObject and QRunnable, emitting a clipDataReady signal before exiting. Is there
a better solution? I hope so.

## Reentrancy

Also we will need to make sure that splitting functionality across asynchronous
callbacks does not interact incorrectly with Klipper::m_locklevel (apparently
designed to prevent reentrant calls?).

I'm not sure all the ways Klipper::m_locklevel is manipulated, but the check in
Klipper::applyClipChanges()
(https://invent.kde.org/plasma/plasma-workspace/-/blob/master/klipper/klipper.cpp#L657)
seems to be completely redundant. Klipper::newClipData()
(https://invent.kde.org/plasma/plasma-workspace/-/blob/master/klipper/klipper.cpp#L681)
returns early if m_locklevel is nonzero, before calling ->
Klipper::checkClipData -> Klipper::applyClipChanges, with no increments to
m_locklevel along the way. The latter two methods have no other callers or
references, but are bizarrely protected methods despite Klipper having no
subclasses in all of plasma-workspace (and I hope it has no external subclasses
in other Qt projects,
https://lxr.kde.org/search?%21v=kf5-qt5&_filestring=&_string=Klipper&_casesensitive=1
didn't return anything). Because there is no other path to call
Klipper::applyClipChanges() except through Klipper::newClipData() (which
returns if m_locklevel != 0), then m_locklevel must be 0 in
Klipper::applyClipChanges(). But I'd make this into an assertion to verify my
reasoning, rather than remove this check entirely.

So in any case I don't *think* Klipper::m_locklevel matters.

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to