2

I've been building a 64 bit Python 3.4.2 app on OS X 10.10, which I bundle as a Mac app using py2app 0.9. I have adapted the app's Info.plist file, so that OS X knows that files with a certain file name suffix can be opened by my app.

When a user double-clicks a file with a certain file name suffix in the Finder, this opens the app and sends the name of the double-clicked file as an argument to the app.

If the app is already running, though, and I double-click a second file with a matching file name suffix, this file name doesn't seem to be handed over to my app.

And this is exactly what I try to implement: no matter if my app is already running, if I double-click a matching file in the Finder, it should be opened in my app.

I have seen that py2app creates the file Contents/Resources/boot.py, which seems to catch the odoc Apple Event sent by the Finder, and sends it to my Python app.

I added some logging to the boot.py file and saw that boot.py doesn't seem to be invoked when my py2app bundled Python app is already running and I double-click a file in the Finder.

Any input would be appreciated.

Thanks a lot in advance,

André

André Aulich
  • 190
  • 10
  • What kind of app is this, GUI or command line? And, if GUI, what framework are you using? – abarnert Dec 05 '14 at 23:21
  • The reason I ask is that if you're just using `--argv-emulation` or equivalent, there are severe limitations (also, 10.9 broke argv emulation completely until py2app 0.8 fixed it, so I wouldn't be surprised if 10.10 broke it again…). If you make your program exit as soon as it finishes processing the arguments, or you set it up so multiple copies can run in parallel, you can get everything to work fine, but neither is acceptable for a typical Cocoa app, so you have to actually add handlers (whether through PyObjC or whatever framework you're using). – abarnert Dec 05 '14 at 23:22
  • Also, for a non-GUI app, how would you _expect_ the names to get handed over, even if the argv-emulator were, e.g., looping over events? You'd need to provide some "hook" function that it could call, right? Which means you need to write an event loop. – abarnert Dec 05 '14 at 23:23
  • Thanks for your quick reply. The app is using tkinter to display a UI. I want to open a single window for each document I open from the Finder, and each window would run in its own thread. – André Aulich Dec 05 '14 at 23:30
  • Well, first, "each window would run in its own thread" doesn't work in Tkinter. All windows have to run in the main thread, period. So, you're going to have to come up with another design. – abarnert Dec 05 '14 at 23:33
  • Second, Tkinter is unfortunately just about the only cross-platform GUI framework that doesn't wrap up standard-events handling, so you're going to have to write your own event loop (maybe using PyObjC) and drive the Tkinter loop manually if you want to do this. – abarnert Dec 05 '14 at 23:35
  • My main challenge is that I need to receive Apple Events, which I need to process ("odoc" in this case). In Python 3 it seems the only way would be PyObjC to do that, but you need version 3.0.4 to install it on OS X 10.10, which is not available on Pypi, yet. I saw that py2app adds AppleEvent handling to my app and somehow hoped I could catch these events when my app is already running. I guess I have to either wait for PyObjC 3.0.4 or use Python 2.7.x in my app to use the same Apple Event handlers like py2app. – André Aulich Dec 05 '14 at 23:39
  • First, PyObjC isn't the _only_ way to process AppleEvents, but it probably is the _easiest_ way if you're already using a GUI framework that can't do it. Second, just switching to Python 2.7.x won't help, because you still need PyObjC 3.0.4, unless you specifically use Apple's pre-installed version—and if you do that, `py2app` can't give you a standalone application. – abarnert Dec 05 '14 at 23:44
  • Also, you don't really need to wait for 3.0.4, you can just `pip install` it off Bitbucket instead of off PyPI (something like `pip install hg+https://bitbucket.org/ronaldoussoren/pyobjc/` should work—but notice that the docs show that it's easier to just clone the repo and run `python3 pyobjc/install.py` instead). – abarnert Dec 05 '14 at 23:44
  • Actually, one last thing to try: IDLE has (or at least used to have) code that integrates standard event handling into Tkinter (so you can drop files on IDLE's dock icon). I remember the old code based on Carbon Events (lots of fun debugging that on 10.5…), but at some point they must have changed to Cocoa APIs, and they must be doing it using only stuff that's included in the stdlib, so… maybe look at how they're doing it? (I'm pretty sure it does still involve driving a loop over `master.dooneevent` instead of calling `master.mainloop`…) – abarnert Dec 05 '14 at 23:47
  • 1
    I just learned, that Tk on a Mac can process AppleEvents. There's a good example at http://code.activestate.com/lists/pythonmac-sig/23079/ and the Mac specific Tk functions are listed at https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm. Basically you just add something like `tk.createcommand("::tk::mac::OpenDocument", doOpenFile)` to your Tk event loop, where doOpenFile needs to point to a function like this: `def doOpenFile(*args): for f in args: do something` – André Aulich Dec 16 '14 at 13:27
  • By the way, this works in both Python 2.7.x and Python 3.4.2 – André Aulich Dec 16 '14 at 13:32
  • Great, I didn't realize that! Post an answer to your own question and accept it, so anyone else who has your question will know what to do. – abarnert Dec 17 '14 at 22:14

2 Answers2

3

I just learned, that Tk on a Mac can process some AppleEvents, like e.g. opening documents.

There's a good example at code.activestate.com/lists/pythonmac-sig/23079 and the Mac specific Tk functions are listed at tcl.tk/man/tcl/TkCmd/tk_mac.htm. Basically you just add something like

tk.createcommand("::tk::mac::OpenDocument", doOpenFile)

to your Tk event loop, where doOpenFile needs to point to a function like this:

def doOpenFile(*args):
     for f in args:
         do something

This works well in Python 2.7 and Python 3.4.2 (haven't tested other versions).

André Aulich
  • 190
  • 10
1

If someone is wondering how to do this with PyQt and py2app:

from PyQt6.QtWidgets import QApplication, QMainWindow, QPlainTextEdit
from PyQt6.QtCore import QEvent


class MyApp(QApplication): # subclass the QApplication class
    def __init__(self, argv):
        super().__init__(argv)

        self.mainWindow = QMainWindow()
        self.textEdit = QPlainTextEdit()
        self.mainWindow.setCentralWidget(self.textEdit)
        self.mainWindow.show()

    def event(self, event: QEvent): # override the event method
        if event.type() == QEvent.Type.FileOpen: # filter the File Open event
            filePath = event.file() # get the file name from the event

            with open(filePath, 'r') as f:
                text = f.read()
            self.textEdit.setPlainText(text)

        return super().event(event)


if __name__ == "__main__":
    app = MyApp([])
    app.exec()

Refer: https://doc.qt.io/qtforpython-5/PySide2/QtGui/QFileOpenEvent.html

Tried this on PyQt6 and python3.11.