A cross platform popup window in Xojo code

There are lots of handy declares on macOS to make a nifty little popover window that behaves a lot like the color picker in things like Numbers

image1

This little tip is about how to make something that mimics that floater nice & simple and then you can extend it in whatever way you want.

I’m not going to go into how to make the entire color picker selector control but its not too difficult with a canvas. When you click the right hand color portion with the color wheel the normal color picker gets shown. And when you click the left hand side it brings up the little prefilled palette picker.

One of the nice things the Numbers Color Picker does is pop up and when you select a value it disappears. Or if you just select a different cell in the spreadsheet it also closes.

We can make a cross platform selector thats a lot like thins pretty simply in Xojo.

Start by creating a new desktop project
Add a new Window and rename it popupWindow
Make the frame type a plain box and turn off the close button, maximize & minimize buttons.
You can make this window resizable or not - thats entirely up to you
Also set the title to a blank string (no contents whatsoever)
Turn off Visible as we want the window to be invisible when created and we’ll show it later after positioning it
And turn implicit instance off (we’ll need it off for this to work decently)

Now on the original window in the project lets start putting some testing code in place
Lets add a push button to the window
The name of it really doesnt matter
In the pushbutton action we’re going to make the popup show up right below the popup button
Lets start with code like

  dim popup as new popupWindow
    
  // for the astute reader you'll note there is an issue with this code
  popup.top = me.top + me.height
  popup.left = me.left
   
  popup.Showmodal

And then save your code (save early and often!)
And give this a try - it should all run but probably wont behave quit the way you want just yet

There are a couple problems

  1. if you click outside the window it doesnt get dismissed as we really want. Its still there and open.
  2. the positioning is way off

Lets tackle the first one
To make the window close when we click else where OR when we click on “something” in the contents is pretty simple.
Lets implement the deactivated event for the case where someone clicks outside the window.
We’ll just close our little popup in that case.
Add the Deactivate event and in there put

  self.close

Now when you deactivate this window it will close itself. Exactly as we wanted.
And for the case where it reacts and closes when something is clicked we’ll use the MouseDown and MouseUp events
Add both of those events
In MouseDown put

    return true

And in MouseUp we once again have

  self.close

Save (always save !) and Run again
While the positioning is still off when you click either in the window or elsewhere things behave as expected.

At this point you can now popup that window - its just poorly positioned.
And you can get any values back from the window to know if a person selected a color od something else.
By adding a couple properties to our popupWindow you can easily make it so when it closes you can then
do some additional processing.
In the case of the color selector popup a click in the content area (the mouse down and up ) simply sets
a color property and a boolean - didSelectColor
The deactivate event simply sets the didSelectColor boolean to false

Lets change the popup to do that
Add a public boolean property, didSelectColor, to popUpWindow.
As well add a public color property, selectedColor, to popupWindow

Alter the deactivate event of popupWindow to

  didSelectColor = false
  self.close

And the mouseUp event to

  didSelectColor = true
  self.close

Eventually we’ll need to add some code to decide what color was selected etc but for now we’ll just
indicate that a color was selected and the mechanics of how to decide what color can be filled in later

Then in our pushButton we can alter the code to

  dim popup as new popupWindow
    
  // for the astute reader you'll note there is an issue with this code
  popup.top = me.top + me.height
  popup.left = me.left
   
  w.ShowModal
  If w.didSelectColor Then
    MsgBox "selected a color !!!!!!"
  Else
    MsgBox "couldnt decide ?"
  End If

Save (always save !) and Run again
Now when you deactivate the window but clicking outside its bounds you get the “Couldnt decide?”
message and when you click the window you get the “Selected a color” message

To make a grid of selectable colors we could use a grid of canvases and then in each canvas mouse up and mouse down do much the same as we have in the windows mouse down and mouse up events.
At this point its trivial to do.

The only big bugaboo is to fix the positioning - and this affects all kinds of things that require global coordinates and not local ones.
What do I mean by that ?

When you open and position a window on screen its “top and left” are in screen coordinates. They are
“global” to the entire screen area available.
But when you position a button or other control on a window layout those positions are relative to the top left of the window (more on this in a bit) This is handy for moving controls or layout as you dont need to, or want to, think about “this should be at position 500,500 from the top left of the screen” In fact doing so would make it impossible to work because as soon as you moved a window its controls would need to be adjusted in absolute terms.

But this also explains why our initially simple for didnt position our new popup window correctly.
When we wrote

  popup.top = me.top + me.height
  popup.left = me.left

the popup is a Window and needs GLOBAL coordinates and ME in this case is a control and those
coordinates are relative to the top left of the window. And here we dive into what do a Windows top,
left, width and height ACTUALLY refer to ?
Windows have a “bounding rectangle” - the rectangle that encloses all of the window and, on Windows,
the menu bar, the title bar and all the window frame. Tne same is true on macOS and Linux.

image2

In the image the red rectangle represents the bounding rectangle
And the inner light blue one the “content area”
When you put a control on a layout its position is relative to the top left of the CONTENT AREA
(starting at 0,0)
So in Xojo the TOP and LEFT of a Window refer to the CONTENT area, NOT the bounding rectangle.
Sometimes this doesn’t matter but when you’re trying to position a Window it can matter a lot.
If you set top = 0 then the title bar and other window adornments that are above that may be tucked up under the menu bar on macOS. Or they could be positioned offscreen on Windows.
Not really friendly.

So for positioning our little window we need to use the BOUNDS which is the enclosing frame rectangle NOT the content area. Lets alter our code a bit in the pushbutton to use this newfound knowledge.

  dim popup as new popupWindow
  
  dim popupBounds as REALbasic.Rect = popup.Bounds
    
  // for the astute reader you'll note there is an issue with this code
  popupBounds.top = me.top + me.height
  popupBounds.left = me.left
  
  popup.Bounds = popupBounds
   
  popup.Showmodal

Now ehen you run this you’ll see a different - but its still not right.
The new BOUNDS we’re calculating are still in local coordinates relative to the top left of
the window and the position of a window needs to be stated in global coordinates.
We need to take the controls position, and add the windows top and left and that gets where we wanted.
This time :slight_smile:
When you put a control inside a container control you have a similar issue and you have to account
for the fact that each level you embed is offset from the container as if it was 0, 0.
In fact to get the GLOBAL left and TOP for a control when its in a container many levels deep
you need to walk up through each parent accumulating the left and top as offsets from the Windows
left and top.

We can add a couple of extension methods to handle this simply.
Add a module and name it RectControlExtensions.
I chose names like this so its immediately obvious whats in them. This one would, in my world,
contain ONLY RectControl extension methods.

To this we’re going to add two methods - GlobalLeft and GlobalTop
Each will, for the control referenced, figure out the Global top or left by walking up the
containment hierarchy. Fortunately when you put one control inside another, like a button inside a canvas,
the positioning of the inner control is stil relative to the top left and not to the control it is parented in.

  Public Function GlobalLeft(extends r as rectControl) as double
    Dim retValue As Double = r.TrueWindow.Left + r.Left
    
    Dim p As RectControl = r.Parent
    While p <> Nil
      retValue = retValue + p.Left
      p = p.Parent
    Wend
    
    Return retValue
  End Function
  Public Function GlobalTop(extends r as rectControl) as double
    Dim retValue As Double = r.TrueWindow.Top + r.top
  
    Dim p As RectControl = r.Parent
    while p <> nil
      retValue = retValue + p.top
      p = p.Parent
    Wend
  
    Return retValue
  End Function

Now that we have added these to our application all our code, in any place, can look like

  Dim popup As New popupWindow

  Dim popupBounds As REALbasic.Rect = popup.Bounds
  Dim selfbounds As realbasic.rect = Self.bounds

  popupBounds.top = Me.Globaltop + Me.height
  popupBounds.Left = Me.GlobalLeft

  popup.Bounds = popupBounds

  popup.Showmodal

  If popup.didSelectColor Then
    MsgBox "selected a color !!!!!!"
  Else
    MsgBox "couldnt decide ?"
  End If

And, except for a weird issue on Windows, we’re done and this little popup window works cross platform.
To deal with the issue on Windows we need to make the self.close NOT happen in the events themselves but in a really short period timer.

The simplest way to do this is to, on our popupWindow, drag a timer onto it.
Rename it selfCloseTimer.
Set its Mode to “OFF” and its period to 1.
Add its action event and out this code in it

  self.close

Then popupWindows Deactivate needs to get changed to look like

  didSelectColor = False

  selfCloseTimer.Mode = Timer.ModeSingle

And popupWindow.MouseUp can look like

  didSelectColor = True

  selfCloseTimer.Mode = Timer.ModeSingle

And there you have a handy dandy starting point for a popup window that will work cross platform !

EDIT : Sample Project added

8 Likes

Example project? I’m lazy :sunglasses:

Link to it added to the end of the post

1 Like

I did a popover project a long time ago and while it worked great on Mac, it was a little subpar in Windows. It makes a shaped window rather than a plain box, so it can point at something like on ios. I ended up not using popovers, so I sort of let it die, but maybe I’ll see if I can clean it up. Or if you guys want to, LOL

Bitbucket Repository

@npalardy: Thanks!

I’m able to do a nice popover with TextAreas and a button. But with a listbox I got popover salad. The window closed fine. But on reopening I only got an empty window.

Yikes, it was 7 years ago. I don’t know if I want to look at the code I wrote back then. Here’s the xojo forum post.

https://forum.xojo.com/t/popovers-my-solution/12970

How can you make the menubar disappear? That’s on Xojo 2019r3 and some Catalina:

the menu bar is set to none in the popup window