Implement a Custom Protocol on Mac
-
We want to be able to call our application from a browser link (as i.e. MS teams does when you open a link to a teams meeting).
The principal is quite simple and exists since quite a while. Under Windows this consists just of a few registry entries (https://stackoverflow.com/questions/80650/how-do-i-register-a-custom-url-protocol-in-windows).
But anything i tried on the Mac does not work at all.
See i.e. https://stackoverflow.com/questions/15721047/mac-custom-protocol-fails-on-some-machines or https://web.archive.org/web/20091215155410/http://www.xmldatabases.org/WK/blog/1154_Handling_URL_schemes_in_Cocoa.item (very old...)I learned that we need:
a) to add CFBundleURLTypes to pInfo.list
b) Add a handler that shall be called by MacOs.But i dont get it to work at all. Of course some native coding has to be done in Objective-C.
Does anyone have a working sample for that within a qt Application?
-
Hi,
You are looking for QDesktopServices::setUrlHandler.
-
I think @stefanwoe is asking a related but different question.
How do you make an arbitrary browser on Mac open your application and pass the URL as argument when you give the browser a URL like
myapp://some/arguments
? On Windows this is done with registry magic (and offerings to the deity of your choice).This is not the same thing as making a custom scheme for use from within a Qt program, which is what QDesktopServices does in a non-persistent fashion.
-
@stefanwoe can you share a minimal compilable sample application so we can investigate ?
-
@stefanwoe said in Implement a Custom Protocol on Mac:
@SGaist if i had this i would not have asked this question.
The goal is not that you provide a working solution. Having a minimal project will allow us to check your issue and help you debug it.
-
@SGaist This can be done with any sample application. i.e. the MDI Example https://doc.qt.io/qt-6.2/qtwidgets-mainwindows-mdi-example.html.
If we us a URL in a browser such usmayprotocol://mydata
A handler within this application shall be called.
AND: if the application is not running it shall be started. That works fine on windows (no programming in the required - its just called with a command line parameter) see the link in my question), but i dont get it to work on the mac. -
After spending several days in total with this I now had success and the custom protocol works as expected.
Basically its correct, that only a CFBundleURLTypes entry in Info.plist is needed but there are some caveats that were so hard to find out:
- Updating Info.plist in the QCreator project will not immediately update the Info.plist file in the created application boundle. This remains unchanged until a full rebuild is made.
- But even if we modify the file inside the Application boundle (MyApp.app/Contents/Info.plist) a modification in this file will not always be noticed by the macOS after you once start the application. I have not found the details of this. I created a sample project in XCode and there all changes were registered immediately. Also if you download a dmg file this is registered immediately.
- The Key to get all this to work during development is the lsregister utility described here:
https://eclecticlight.co/2019/03/25/lsregister-a-valuable-undocumented-command-for-launchservices/
Command to remove all custom protocols:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain u -domain s -domain l -v
If you do that, as soon as you start your application, the custom protocol defined in MyApp.app/Contents/Info.plist will be registered
To find if a protocol is registered:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep myprotocol
Where myprotocol is the name of the protocol searched. You may pipe the dump to less or a file to see which application is registered for the protocol. The dump returns a huge amount of data.
Also you can tell lsregister to add the protocol(s) from a boundle:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -R -f pathname
The linked article states "You shouldn’t ever have to do this" - but this seems invalid to me.
Or tell lsregister to forget about a application:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -R -f -u pathname code_text
where pathname is the path and name of the app.
So the whole process is:
- Add CFBundleURLTypes to your Info.plist
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Viewer</string> <key>CFBundleURLName</key> <string>com.mydomain</string> <key>CFBundleURLSchemes</key> <array> <string>myProtocol</string> </array> </dict> </array>
And then take care about the registering as described above.
If the protocol IS registered on the Mac, then you'll get a nice clean QEvent::FileOpen in the event callback of your application class derived from QApplication:
class CMyApplication : public QApplication { public: CMyApplication(int &argc, char **argv) : QApplication(argc, argv) { } bool event(QEvent *event) override { if (event->type() == QEvent::FileOpen) { QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(event); QString urlString = openEvent->url().toString(); // Processs urlString ... } return QApplication::event(event); } };
Now you can open a browser and type in the address bar:
myProtocol://?param1=value1¶m2=value2
And the browser will ask you if you want to start the registered application:
If you accept this, openEvent->url().toString() will return the complete string you typed into your browser. If your Application is not running it will be started by the macOS.
-
Thanks for the thorough feedback !