Mac Full Screen Windows / Tabs

I’ve just been testing my application in full screen mode and I’ve made a few interesting / odd discoveries. First when in full screen mode opening a new window results in a tab being opened in the full screen “window”. The toolbar appears at the top of the screen followed by a tab bar and then my window contents.

Single window

Now with tabs

It makes sense and is really quite helpful, however, when I switch back to standard window mode the tabs remain. There is even control over them in as much as I can right click on a tab and send it to a new window, close it etc.

If I move it to a new window the content doesn’t get redrawn at the new correct height, leaving a trench at the bottom of the window. Dragging the window causes the content to snap back into place.

So going into and out of fullscreen mode can be detected by having a global property “wasFullscreen” and setting it in any Window.Resized event from Self.Fullscreen as follows:

sub Resized
  If App.WasFullscreen And Not FullScreen Then
    ' Split all tabs
  ElseIf Not App.WasFullscreen And FullScreen Then
    ' Join all windows
  End If
  App.WasFullscreen = FullScreen
end sub

However the issue is then how do I split the tabs into windows and conversely join all open document windows into a tabbed window.

I notice that there is a Cocoa “mergeAllWindows:” api and an “moveTabToNewWindow:” API, but if I have the Declares right I’m messing up some other way:

Declare Sub moveTabToNewWindow Lib "Cocoa" Selector "moveTabToNewWindow:" ( Any As Ptr )
Declare Sub mergeAllWindows Lib "Cocoa" Selector "mergeAllWindows:" ( Any As Ptr )

Calling either of these functions hard crashes the application. I’ve tried:

moveTabToNewWindow Self.Handle
mergeAllWindows( Self.Handle )

Both from within a Resized event and in Button Pressed events. Has anyone successfully managed this?

If you have the MBS plugins, you can handle that. For instance, this turns off MacOS tabs:

I don’t I’m afraid but will take a look.

courtesy of DaveS on another forum:

No need to invest in expensive plugins to solve this

// Disable auto tabbing in Sierra
#If TargetCocoa
  Declare Function NSClassFromString Lib "Cocoa" (s As CFStringRef) As Ptr
  Declare Function NSSelectorFromString Lib "Cocoa" (s As CFStringRef) As Ptr
  Declare Sub setAllowsAutomaticWindowTabbing Lib "Cocoa" selector "setAllowsAutomaticWindowTabbing:" (cls As Ptr, ena As Boolean)
  Declare Function respondsToSelector Lib "Cocoa" selector "respondsToSelector:" (p As Ptr, sel As Ptr) As Boolean
  Dim nswCls As Ptr = NSClassFromString ("NSWindow")
  If respondsToSelector (nswCls, NSSelectorFromString ("setAllowsAutomaticWindowTabbing:")) Then
    setAllowsAutomaticWindowTabbing (nswCls, False)
  End If
1 Like

Thanks for that, however, it provides a really bad full screen experience. I would prefer to merge all windows into tabs when they enter full screen and move them back into windows when they switch out again.

I was kinda hoping that “mergeAllWindows” and “moveTabToNewWindow” would do that. I suspect I’m just not calling them correctly.

Full screen is a really bad experience anyway.
A simple little dialog with a handful of controls occupies the whole screen with acres of space.

1 Like

Not for my App. Dialogs / Sheets don’t appear in full screen windows they appear as dialogs / sheets in the usual way. Only Document windows show in full screen at full size as far as I can see. At least for my usage.

Im sure I’ve had them do that.
Best check the window types again…

Maybe it’s an API2 thing, or a Monterey thing. My app is now converted to API2. Fullscreen works pretty well as far as I can tell. New window becomes new tab, dialogs operate as they do in window mode etc. The only issue is that when I move into fullscreen mode the Windows do not collapse down to tabs and when I leave full screen they don’t revert to windows.

Alternatively, I could add functions to support tabs in windowed mode too. But then I would need to be able to call mergeAllWindows and other elements like it. As far as my code is concerned everything just remains the same.

There is a setting in System prefs to General to “Prefer tabs:” which can be set to always, which would allow you to test without any app changes at all.

Bingo. I’ve worked it out:

Using MacOSLib NSWindow class, which I converted to API2 and the following code allows me to pop all windows into one.

Declare Sub mergeAllWindows Lib "Cocoa" Selector "mergeAllWindows:" ( Any As Ptr )
Var oNSWindow As New NSWindow( Self )
mergeAllWindows oNSWindow

The following code will split the tabs into separate windows when you return from fullscreen (or any other time you want):

Declare Sub moveTabToNewWindow Lib "Cocoa" Selector "moveTabToNewWindow:" ( Any As Ptr )

For iWindow As Integer = 0 To App.WindowCount - 1
  If App.Window( iWindow ) IsA Window1 Then
    Var oNSWindow As New NSWindow( App.Window( iWindow ) )
    moveTabToNewWindow oNSWindow
  End If

The “IsA Window1” part is to identify the class of the document windows you have. It could equally be any other code you wish to distinguish your documents from any other form of window, that likely wouldn’t be in a tab.

It doesn’t seem to work in the resize event. Likely because it is called from a window being resized.

Equally the merge code could be used to implement the Window menu “Merge All Windows” item you see in Finder.

AFAIK, this is NOT standard behavior, so I would recommend having an option to either enable this or disable it in the preferences for those that find it too jarring.

Just my 2¢

Agreed, I’ve been looking into apps that support it. It would seem that it would be better to make tabs fully available using the menu items.

File > Add Tab
Window > Show Previous Tab
Window > Show Next Tab
Window > Move Tab to New Window
Window > Merge All Window

All of which would seem to be as easily implemented.

So in the end I decided to implement tabs are they are in Finder.

I added extension methods to MacOSLib NSWindow class as follows:

Public Sub AddTabbedWindowOrdered(tabbedWindow as NSWindow, orderingMode as NSWindowOrderingMode = NSWindowOrderingMode.NSWindowAbove)
  #If TargetCocoa
    Declare Sub addTabbedWindow Lib CocoaLib Selector "addTabbedWindow:ordered:" _
    (obj_id As Ptr, tabbedWindow As Ptr, orderingMode As NSWindowOrderingMode)
    Dim tabbedWindowRef As Ptr
    If tabbedWindow <> Nil Then
      tabbedWindowRef = tabbedWindow
    End If
    addTabbedWindow Self, tabbedWindowRef, orderingMode
    #Pragma unused tabbedWindow
    #Pragma unused orderingMode
End Sub

Public Sub MoveTabToNewWindow()
  Declare Sub moveTabToNewWindow Lib "Cocoa" Selector "moveTabToNewWindow:" ( Any As Ptr, Sender As Ptr )
  moveTabToNewWindow Self, Nil
End Sub

Public Sub SelectNextTab()
  Declare Sub selectNextTab Lib "Cocoa" Selector "selectNextTab:" ( Any As Ptr, Sender As Ptr )
  selectNextTab Self, Nil
End Sub

Public Sub SelectPreviousTab()
  Declare Sub SelectPreviousTab Lib "Cocoa" Selector "selectPreviousTab:" ( Any As Ptr, Sender As Ptr )
  SelectPreviousTab Self, Nil
End Sub

Public Function TabbedWindows() As NSWindow()
  #If TargetCocoa
    Declare Function tabbedWindows Lib CocoaLib Selector "tabbedWindows" ( obj_id As Ptr ) As Ptr
    Dim retArray() As NSWindow
    Dim arrayRef As Ptr = tabbedWindows( Self )
    If arrayRef <> Nil Then
      Dim ns_array As New NSArray(arrayRef)
      Dim arrayRange As Cocoa.NSRange = Cocoa.NSMakeRange( 0, ns_array.Count )
      Dim m As MemoryBlock = ns_array.ValuesArray( arrayRange )
      Dim n As Integer = arrayRange.length - 1
      For i As Integer = 0 To n
        retArray.append New NSWindow( Ptr( m.UInt64Value( i * SizeOfPointer ) ) )
    End If
    Return retArray
End Function

Public Sub MergeAllWindows()
  Declare Sub mergeAllWindows Lib "Cocoa" Selector "mergeAllWindows:" ( Window As Ptr, any As Ptr )
  mergeAllWindows Self, Nil
End Sub

Usage was as follows:

Function FileNewTab() As Boolean
  ' Menu handler for New Tab
  Var oOwner As NSWindow = Self
  Var oCSW As New CSW
  Var oTab As NSWindow = oCSW
  oOwner.AddTabbedWindowOrdered oTab
End Function

Function WindowMergeAllWindows() As Boolean
  ' Menu handler for Merge All Windows
  Var oCSW As CSW
  Var oNSWindow As NSWindow = Self
  Return True
End Function

Function WindowMoveTabToNewWindow() As Boolean
  ' Menu handler for Move Tab to New window
  Var oNSWindow As NSWindow = Self
  Return True
End Function

Function WindowShowNextTab() As Boolean
  ' Menu handler for Show Next Tab
  Var oNSWindow As NSWIndow = Self
  Return True
End Function

Function WindowShowPreviousTab() As Boolean
  ' Menu handler for Show Previous Tab
  Var oNSWindow As NSWIndow = Self
  Return True
End Function

Enabling the right menus at the right time, in MenuBarSelected (API2) or EnableMenuItems (API1).
Set the tab menus to have AutoEnable = False

Sub MenuBarSelected() Handles MenuBarSelected

  ' Tabs only work in 10.12 and above
  If System.Version > "10.11" Then
    FileNewTab.Enabled = True

   ' This is a structure in my app that keeps track of how many open documents there are.
   ' You will need to adjust accordingly.
    If App.OpenWindows.NumWindows > 1 Then
       WindowMergeAllWindows.Enabled = True
    End If

    Var oNSWindow As NSWindow = Self
    Var TabbedWindows() As NSWindow = oNSWindow.TabbedWindows
    ' If there are tabs on the window
    If TabbedWindows.LastIndex > -1 Then
        WindowMoveTabToNewWindow.Enabled = True
        WindowShowPreviousTab.Enabled = True
        WindowShowNextTab.Enabled = True
    End If
  End If

End Sub

There’s only one more issue. When you move a window out of Tab mode into normal Window mode there seems to be a gap developed at the bottom of the window. If you have controls locked to there you may have to adjust their sizes or locations. Also windows entering Tabbed get shorter so you have to play with that.

Hope this helps someone.

PS I would add these additions into MacOSLib however, I only have a Light license and so can only save in binary. If someone would like to add them I’m more than happy to assist.

Also windows entering Tabbed get shorter so you have to play with that.

To this day I do not know why moving to a tab does not trigger a resized event.

Probably because Xojo doesn’t the system tabs, so they don’t experience this issue themselves.