10

I'm wanting to detect when the user has pasted something in ANY application, so I can follow it up with copying a new item into the clipboard (Use case: I have a list of items I'm copying from a database one-by-one into a web-page, and would like to automatically put the next one in the clipboard once I've finished pasting.)

Currently I have a button using Tkinter that copies a field when pressed using the following code.

self.root.clipboard_clear()
self.root.clipboard_append(text)

What I need then would be some way to detect when a paste has been performed in another application, so I can then load in the next item into the clipboard. I would like it to work on Win/Mac/Linux as I work across all three. Any ideas?

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
AdmiralJonB
  • 2,038
  • 3
  • 23
  • 27
  • I don't know if you can easily detect other application paste, but you sure would be able to periodically check if the clipboard has changed or not. – Nae Jan 29 '18 at 14:25
  • @Nae I thought about doing that, but as it's my application adding things into the clipboard, it wouldn't particularly change as what's in there doesn't get removed on paste? – AdmiralJonB Jan 29 '18 at 14:28
  • [This answer in SO should cover you](https://stackoverflow.com/questions/14685999/trigger-an-event-when-clipboard-content-changes) - it tracks changes in clipboard content – rodcoelho Jan 29 '18 at 15:16
  • 5
    @rodcoelho: this isn't about detecting someone doing a _copy_, it's about detecting when someone does a _paste_. – Bryan Oakley Jan 29 '18 at 16:03
  • Instead of copying, use cut and paste. If I'm not mistaken, cut will let you know when the copy has been pasted. If you're in control of the source end, you should have no trouble setting up the next item programmatically. – Mad Physicist Apr 13 '20 at 18:56
  • @MadPhysicist I don't think that cut and paste is any different from copy and paste. In any case the clipboard serves as the buffer holding the data and it doesn't care where that data came from and how it was obtained. You may be misled by how cut&paste behaves within the same application (in which case it can clear the clipboard immediately after the cut item was pasted). – Leon Apr 13 '20 at 19:05
  • @Leon. Seems that you're right. The solution appears to be called "delayed rendering" on Windows. Not sure yet if there's an equivalent on Linux – Mad Physicist Apr 13 '20 at 19:25
  • @AdmiralJonB. Are you trying to stay OS agnostic, or do you have a preference as to where this needs to work? I am pretty sure I can help you on Windows. Still researching the UNIX options. – Mad Physicist Apr 13 '20 at 20:39

4 Answers4

5

As Leon's answer points out, under standard conditions, it is unlikely that you will be able to detect any use of copied objects once you've released them into the wild. However, most modern OSes support something called "delayed rendering". Not only can the format of the selection be negotiated between host and destination, but it is not advisable to copy large pieces of memory without first knowing where they are going. Both Windows and X provide a way of doing exactly what you want through this mechanism.

Rather than go into the details of how each OS implements their clipboard API, let's look at a fairly standard cross-plarform package: PyQt5. Clipboard access is implemented through the QtGui.QClipBoard class. You can trigger delayed rendering by avoiding the convenience methods and using setMimeData. In particular, you would create a custom QtCore.QMimeData subclass that implements retreiveData to fetch upon request rather than just storing the data in the clipboard. You will also have to set up your own implementations of hasFormat and formats, which should not be a problem. This will allow you to dynamically negotiate the available formats upon request, which is how delayed rendering is normally implemented.

So now let's take a look at a small application. You mentioned in the question that you have a list of items that you would like to copy successively once the first one has been copied. Let's do exactly that. Our custom retrieveData implementation will convert the current selection to a string, encode it in UTF-8 or whatever, move the selection forward, and copy that into the clipboard:

from PyQt5.QtCore import Qt, QMimeData, QStringListModel, QTimer, QVariant
from PyQt5.QtGui import QClipboard
from PyQt5.QtWidgets import QAbstractItemView, QApplication, QListView

class MyMimeData(QMimeData):
    FORMATS = {'text/plain'}

    def __init__(self, item, hook=None):
        super().__init__()
        self.item = item
        self.hook = hook

    def hasFormat(self, fmt):
        return fmt in self.FORMATS

    def formats(self):
        # Ensure copy
        return list(self.FORMATS)

    def retrieveData(self, mime, type):
        if self.hasFormat(mime):
            if self.hook:
                self.hook()
            return self.item
        return QVariant()

class MyListView(QListView):
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
            self.copy()
        else:
            super().keyPressEvent(event)

    def nextRow(self):
        current = self.selectedIndexes()[0]
        row = None
        if current:
            row = self.model().index(current.row() + 1, current.column())
        if row is None or row.row() == -1:
            row = self.model().index(0, current.column())
        self.setCurrentIndex(row)
        QTimer.singleShot(1, self.copy)

    def copy(self, row=None):
        if row is None:
            row = self.selectedIndexes()[0]
        data = MyMimeData(row.data(), self.nextRow)
        QApplication.clipboard().setMimeData(data, QClipboard.Clipboard)

model = QStringListModel([
    "First", "Second", "Third", "Fourth", "Fifth",
    "Sixth", "Seventh", "Eighth", "Ninth", "Tenth",
])

app = QApplication([])

view = MyListView()
view.setSelectionMode(QAbstractItemView.SingleSelection)
view.setModel(model)
view.show()

app.exec_()

The QTimer object here is just a hack to quickly get a separate thread to run the copy. Attempting to copy outside Qt space triggers some thread-related problems.

The key here is that you can not simply copy text to the clipboard, but rather create a placeholder object. You will not be able to use a simple interface like pyperclip, and likely tkinter.

On the bright side, the above example hopefully shows you that PyQt5 is not too complex for a simple application (and definitely a good choice for the non-simple kind). It is also nice that most operating systems support some form of delayed rendering in some form that Qt can latch on to.

Keep in mind that delayed rendering only lets you know when an object is read from the clipboard by some application, not necessarily the one you want. In fact it doesn't have to be a "paste": the application could just be peeking into the clipboard. For the simple operation described in the question, this will likely be fine. If you want better control over the communication, use something more advanced, like OS-specific monitoring of who reads the copied data, or a more robust solution like shared memory.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • +1. However it would be worth to note that this solution still reacts only to the act of reading the clipboard contents rather than pasting from it. A [third party clipboard manager](https://www.geckoandfly.com/31857/clipboard-manager/) or an imaginary antivirus monitoring the clipboard will trick this code. – Leon Apr 14 '20 at 07:08
  • @Leon. Correct. However, if your goal is to just be able to hit copy once and paste multiple times, this will do it. I think that in Windows you may be able to detect who read your clipboard, but I have no desire to find out how to do it. – Mad Physicist Apr 14 '20 at 07:26
  • Thanks for this in-depth answer. Unfortunately, it doesn't appear to work (on Windows 10). When I double click on any item (say the First) and press Ctrl+C (as used in your code) It copies the first item, and then I am able to repeatedly paste it. However it doesn't seem to copy any new items in to the clipboard, even after 0.5 seconds. – Adam Griffiths Apr 14 '20 at 11:35
  • @AdamGriffiths How about creating a thread to listen the keyboard?When thread detect user pressed `Ctrl+V`,it will do some work. – jizhihaoSAMA Apr 14 '20 at 14:41
  • @jizhihaoSAMA. You can't listen to another app's keystrokes without jumping through some massive hoops – Mad Physicist Apr 14 '20 at 14:54
  • @AdamGriffiths. It appears to be a mix of Qt bug and C++ object lifetime issue. Somehow, the memory handle is not properly released after copy, or it allocates a handle of the wrong type, or locks it, or something. I'll figure it out. This is a proof of concept to show that you can react to paste events more than anything. – Mad Physicist Apr 14 '20 at 15:00
  • @MadPhysicist I don't know whether it could be used in linux,but it could be used in Chrome,windows 10(When I press `Ctrl+V`,it will do something). – jizhihaoSAMA Apr 14 '20 at 15:20
  • @jizhihaoSAMA. Neat. How can you harness that in Python? – Mad Physicist Apr 14 '20 at 15:22
  • @AdamGriffiths. Let's see if this works: https://stackoverflow.com/q/61211987/2988730 – Mad Physicist Apr 14 '20 at 16:00
  • @AdamGriffiths. All done! I was trying to modify Qt objects outside Qt space. All working now. And it was because of the linked question. – Mad Physicist Apr 14 '20 at 17:39
  • Thanks for your hard work on this :) This is definitely beyond my skill level. It still doesn't appear to be working. I am getting the same behaviour as before? – Adam Griffiths Apr 15 '20 at 15:49
  • @AdamGriffiths. Are you on windows? What error are you seeing? I'm on win 10 and the lastest version was working when I pasted it into a script and ran from the command line. It did have issues on Spyder, but that's a known miscommunication between Spyder and Qt – Mad Physicist Apr 15 '20 at 15:56
  • It doesn't bubble any errors up to the python console itself. I am just getting to grips with Qt now, and I'm unsure of how to get logs from Qt itself the same way you did with your question. I am using windows 10 version 1903 – Adam Griffiths Apr 15 '20 at 15:58
  • @AdamGriffiths The error I had was printed directly to the console. What version of Qt and PyQt do you use? – Mad Physicist Apr 15 '20 at 16:56
  • Python 3.8.0,PyQt5-5.14.2 and PyQt5-sip-12.7.2 – Adam Griffiths Apr 15 '20 at 16:59
  • @AdamGriffiths. How does the error manifest itself if not a printout? – Mad Physicist Apr 15 '20 at 17:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/211739/discussion-between-adam-griffiths-and-mad-physicist). – Adam Griffiths Apr 15 '20 at 17:43
1

Disclaimer: I am not an expert in clipboards. This answer is my understanding how they work. It can be totally wrong.

Hardly there is a platform specific way to solve this, let alone do it in a cross-platform way. The act of pasting from a clipboard consists of two disconnected steps:

  1. Peek at/read the clipboard contents
  2. Use that data in an application specific way

During the second step the application may check the type of the data read from the clipboard and may ignore it if doesn't match the type of the data that can be pasted in the active context (e.g. an image cannot be pasted in a plain text editor). If pasting happens, it happens in the user space and each application may do it differently. Detecting all possible implementations under all platforms simply doesn't makes any sense.

At best you can monitor the acts of peeking at the clipboard contents, yet any application (consider a third party clipboard manager) can examine the clipboard eagerly without any explicit actions from the user, hence those events are not necessarily followed by any observable utilization of that data.

The following loose analogy from the real world will probably convince you to abandon looking for a solution. Suppose that you apply for and are granted a patent to some recipe. The patent is published and can be read by anyone. Could you ask for an effective way to detect any instances of a dish being cooked according to the patented recipe?

Leon
  • 31,443
  • 4
  • 72
  • 97
  • Also, I could see many reasons why one application shouldn't be allowed to read when another has looked at something in a read-only way. – Mad Physicist Apr 13 '20 at 18:54
  • Thank you for your answer and your analogy, if it turns out that it is indeed impossible, I will award you the bounty. – Adam Griffiths Apr 13 '20 at 19:17
  • A solution working on windows alone, and not other platforms, would be preferable to no solution though. – Adam Griffiths Apr 13 '20 at 19:18
  • @AdamGriffiths. I didn't realize you were the one placing the bounty. Do you get to decide who gets it regardless of OP's selection? – Mad Physicist Apr 13 '20 at 20:46
  • @AdamGriffiths. I'm asking because I'm very close to having a Windows solution. It's definitely possible, if not super elegant, but basically the same as what Excel does. Might be worth wrapping it in a small package if it also happens to work on Linux later. – Mad Physicist Apr 13 '20 at 20:47
  • @AdamGriffiths. Hope you like Qt! – Mad Physicist Apr 14 '20 at 00:13
0

On ms-windows, it seems you should be able to use a global hook (using SetWindowsHookExA) to intercept the relevant WM_PASTE message.

Roland Smith
  • 42,427
  • 3
  • 64
  • 94
0

Well,I used to use this to make a global hotkeys for my tools.(In pynput official document,it also support linux/mac OS.)

A minimal example:

from pynput.keyboard import GlobalHotKeys
import platform

# platform.system() can detect the device.(macOS/Windows/Linux)

def yourfunction():
    print("detect paste...") # each time when you pressed "Ctrl+V",it will call the function

if platform.system() == 'Windows':
    with GlobalHotKeys({"<ctrl>+v":yourfunction}) as listener:
        listener.join()

PS:It could used in Chrome,Edge(Most of application).But it couldn't used in games.

jizhihaoSAMA
  • 12,336
  • 9
  • 27
  • 49