Vadim Shpakovski way back on July 2011 released code for a popup window in the status bar, and now in August 2012 I have ported it over to MonoMac with a few tweaks! If you don’t care about the guide below, you can access the source at Github.
The implementation is largely the same as used in Vadim’s implementation, with a few tweaks to make it simpler, more .NET-like and more extensible. There’s still more work that needs to be done to make it truly modular architecture-wise, but I’ve made the classes themselves easily extensible with clear extension points.
High Level Overview
The entry point is the StatusPanelController, this controller creates the icon in the status bar. It takes in a controller that inherits from PanelController so that you can create your own custom views if necessary. When the user clicks on the icon, the controller will call OpenPanel() and ClosePanel() in the PanelController.
The PanelController is the controller for the panel that opens – it’s responsible for all rendering.
BackgroundView is the background for the panel we display, you can think of it as the panel itself. It renders the arrow, border, and background. For this reason the panel you use must have a BackgroundView, additionally the panel itself should be effectively non-styled, with all standard window chrome removed.
That’s basically it!
BackgroundView is a special view that represents the popup panel. At the top of the source file are various constants for controlling how the view should be rendered. ArrowX is necessary so that we know where the middle of the status bar icon is. The arrow will centre itself on this point.
To render the view we override DrawRect, and provide our own custom drawing using an NSBezierPath to do most of the work. Take note that the coords .NET use are reversed on the Y axis, .NET’s origin is in the top-left, while OS X’s origin is in the bottom left.
PanelController (and a little note on MonoMac Animation)
PanelController is the most substantial class in the entire project. This controller is responsible for the display of the panel itself, this means it needs to handle the animation of the panel, as well as updating the BackgroundView with the position of the status icon, should the window be resized. HandleWindowDidResize handles the arrow position, updating the BackgroundView whenever the panel is resized. Meanwhile we have two events, WillClose and DidResignKey that we use to know when to close the panel. We don’t want the panel just disappearing, we want a nice smooth animation!
OpenPanel() is responsible for displaying the panel in a smooth way. First of all it works out the position the panel needs to be, this is based on the icon position. Next we set up the panel for display by ensuring critical state, and adding a teeny bit of a debug Easter egg. If you press shift, or shift + option, you slow down the animation, and optionally output debug information relating to the upcoming display.
Finally we animate the panel into view. Note that we cast the Animator property of the window, this is because by default the Animator property is of type NSObject. The Animator is actually a proxy for the full object, so we can just cast it to the ‘real’ type, and set our new post-animation properties. We then finish our animations, and let OS X do the rest!
ClosePanel() is the opposite of OpenPanel() and just animates the panel away.
Panel is a very slightly customised implementation of NSPanel. We just add an extra property so that we can use controls in the panel that require the keyboard, such as text fields.
This is the view that will be displayed in the status bar. It’s fairly self-explanatory, with different images depending on the state of the button. There’s a GlobalRect property so that we can locate the icon in the screen and position the panel.
For rendering we override DrawRect, and do it largely ourselves. We first draw the background into the view, optionally highlighted. Next we draw the requisite image (the icon) in the centre of the view. We use an updated method call over the one used by Vadim because the original has been deprecated – it’s not even available in MonoMac!
Finally we track the MouseDown and MouseUp events, rather than just MouseUp. A ‘click’ is considered when the user both mouses down and up, in the same view. If the user does this, we fire the StatusItemClicked event.
Finally we have the controller that actually brings it all together. The controller could do with a little more work to make it genuinely re-usable, since it uses hard-coded values for the images, as well as the view used for the status icon. Fortunately re-factoring these out into dependencies would be fairly trivial.
At construction we create all of our required controllers and views, this creates the icon in the status bar.
We hook into the clicked event from StatusItemView, and toggle the panel as neccesary.
Finally we implement IDisposable so that we never leave an icon in the status bar by accident – that would be very bad! We also have a finalizer so that no matter what, the icon will end up removed from the status bar when the controller is garbage collected.