Today, some Pharo users asked why we have lost a nice feature in Mac OS X. In this operating system, it is possible to Cmd-Click on the title of a window, if the window represents an open document, a nice menu showing all the path to it appears. Also, the user can select to open Finder (the file explorer) in any of this directory.
This feature was not available anymore in the latest Pharo 9 VM. What happened? Does the VM has a regression? Do we need to throw everything away and use another programming language :)? Let’s see that this is not the case. And also, it is a nice story of why we want the in-image development in the center of our life.
Where is the Window handled?
One of the main features introduced in the VM for Pharo 9 is that all the handling of the events and UI is done in the image side. The so called “headless” VM has no responsibility in how the world is presented to the user.
When the image starts, it detects if it is running in the “headless” VM. If it is the case, it knows it should take the responsibility to show a window to the user. Also, the image is now able to decide if we want to show a window or not and what kind of window we want. In this case, we want to show the Morphic-based world.
To handle the events, render and show a window, the image uses SDL as its backend. This is one of the possible backends to use, but we are not going to talk about other than SDL in this article. The creation of the window and its events is done through the OSWindow package, if you want to see it.
SDL provides a portable way of implementing a UI in a lot of different architectures and operating systems. Allowing us to use the same code in all of them. Also, the image is using the FFI bridge that does not present a major slowdown for managing events and redrawing.
But.. why is this important or better?
One of the key points is portability, so the same code can be executed in different platforms, but it is not the only one. Also, it allows the image to decide how to handle the UI. Doing so, it allows applications built on top of Pharo to create the UI experience they desire.
A final benefit, that in this case is more relevant for us, is the flexibility to modify it from the image, and to do it in a live programming fashion.
We think all these points give more ability to the users to invent their own future.
Solving the Issue
This issue is a simple one to resolve. We need to only take the Cocoa window (the backend used by all OSX applications) and send the message setTitleWithRepresentedFilename:, something like the following code will do the magic.
[pharoWindow setTitleWithRepresentedFilename: @'./Pharo.image']
But… this solution is not possible:
- We need to access the Cocoa window.
- This code is in ObjectiveC.
- We want it portable: we want the issue fix, but we want all the other platforms to continue working also.
Let’s solve all the problems from our nice lovely image.
Accessing the Window
The first point is easy to solve. SDL and the Pharo bindings expose a way of accessing the handler of the real Cocoa window that SDL is using. SDL exposes all the inner details of a window through the WMInfo struct.
wmInfo := aOSSDLWindow backendWindow getWMInfo. cocoaWindow := wmInfo info cocoa window.
Talking with the Cocoa Framework
The Cocoa Framework exposes all its API though the use of ObjectiveC or Swift. None of them we can use directly. Fortunately, there is a C bridge to communicate to the ObjectiveC objects. It is exposed through a series of C functions. And, we can use the Unified-FFI support of Pharo to call these functions without any problem. Here it is the description of this API.
We can use a wrapper of these functions that has been developed for Pharo: estebanlm/objcbridge. However, we only need to call a single message. So, let’s see if we can simplify it. We don’t want to have the whole project just for doing a single call. If you are interesting of a further implementation or using more Cocoa APIs, this a good project to check and it will ease your life.
As we want a reduced version of it, we are going to use just three functions, with its corresponding use through Unified FFI:
SDLOSXPlatform >> lookupClass: aString ^ self ffiCall: #(void* objc_lookUpClass(char *aString))
SDLOSXPlatform >> lookupSelector: aString ^ self ffiCall: #(void* sel_registerName(const char *aString))
SDLOSXPlatform >> sendMessage: sel to: rcv with: aParam ^ self ffiCall: #(void* objc_msgSend(void* rcv, void* sel, void* aParam))
The first two functions allows us to resolve an Objective C class and a selector to call. The third one allows us to send a message with a parameter.
As the parameter to the function “setTitleWithRepresentedFilename:” is expecting a NSString (a String in Objective-C), we need to create it with our utf-8 characters. So we have the following helper:
SDLOSXPlatform >> nsStringOf: aString | class selector encoded param | class := self lookupClass: 'NSString'. selector:= self lookupSelector: 'stringWithUTF8String:'. encoded := aString utf8Encoded. param := ByteArray new: encoded size + 1. param pinInMemory. LibC memCopy: encoded to: param size: encoded size. param at: encoded size + 1 put: 0. ^ self sendMessage: selector to: class with: param
So, we can set the file location just executing:
aParam := self nsStringOf: aString. wmInfo := aOSSDLWindow backendWindow getWMInfo. cocoaWindow := wmInfo info cocoa window. selector := self lookupSelector: 'setTitleWithRepresentedFilename:'. self sendMessage: selector to: cocoaWindow getHandle with: aParam. self release: aParam. "It sends the message #release to the objective-C object, important for the reference counting used by Obj-C"
Doing it portable
Of course this feature is heavy related with the current OS. If we are not in OSX, all this code should not be executed. To do so, the best alternative is to have a strategy per platform. This idea may look an overkill but it allows us better modularization and extension points for the future.
Also, it is a good moment to implement in the same way some specific code for OSX that was using an if clause to see if it was in OSX.
So, the following strategy by platform is implemented:
In the strategy, there is a Null implementation that does nothing. This is used by all other operating systems, and an implementation that is used by OSX. This implementation for OSX has all the custom code needed to change the file associated with the window.
This strategy is then accessed through extension methods in the OSPlatform subclasses. One important point is to do this through extension methods, as we don’t want to introduce a dependency from OSPlatform to SDL.
For the OSX platform:
MacOSXPlatform >> sdlPlatform
^ SDLOSXPlatform new
For the others:
^ SDLNullPlatform new
Presenting the solution to this issue was a good excuse to present the following points:
- How to introduce platform dependent code without bloating the system with Ifs.
- How to interact with the operating system through FFI.
- How we can take advantage of the image controlling the event handling and the UI.
We consider these points very important to allow developers to create portable and customizable applications while taking full advantage of the programming capabilities of Pharo.