Tear Off Palettes Using ContainerControls by Seth Willits
August 27, 2005




Using a New *PRO* Feature From REALbasic 2005!
In this tutorial, we're going to use a new feature from REALbasic 2005 which is the ContainerControl. The ContainerControl allows you to group controls into one single control which you can embed into windows either in the IDE or dynamically at runtime. They're quite cool (although still a bit buggy) and darn useful. In this example we'll create a font palette (we'll just use an image of the standard one instead of writing the whole thing) which can be dragged into and attached to multiple windows. It's pretty fancy. But yes, ContainerControls are REALbasic Pro feature ONLY. A major bummer, but that's the way the cookie crumbles. It is a rather advanced feature, so I'm not crying. The real bummer is that standard users can't use ContainerControl solutions made by Pro users. For those of you without Pro, you can download a Mac OS X binary and see what it's like. Anyway...



CCPanel Class
The first place we're going to start is the CCPanel class. This is the ContainerControl subclass which will be the base class of all of our panels (in this tutorial, there's only one). It has four properties and one constant; They are: FirstX as Integer, FirstY as Integer, InPalette as Boolean, Type as String, and the constant is: kPanelTypeFont = "Font". The FirstX and FirstY properties are used when dragging the panel, the InPalette property is true only if that particular instance of the font panel (remember, there can be many) is in a palette on its own, and the Type property is a string which identifies what kind of panel it is so we can use the CCPanel abstractly in some places.

Note that when creating ContainerControls in the IDE, it can be a little confusing. Like Windows, ContainerControls have two special kinds in the IDE, one with an interface, one without. As with the "Add Window" button in the project tab, the "Add ContainerControl" tab adds a ContainerControl with an interface. Only the last subclass of a ContainerControl in a single hierarchy can have an interface. To create a ContainerControl subclass without an interface, use "Add Class" and then set the superclass property to ContainerControl. The CCPanel class is a ContainerControl without an interface.

In the MouseDown and MosueDrag events we add code to offer a "Tear Off" contextual menu item when the panel is not being shown in its own palette.

Function MouseDown(X As Integer, Y As Integer) As Boolean

  // Workaround for a bug where ConstructContextualMenu isn't called
  if IsContextualClick then
    
    // We Don't Tear out from the Palette
    if not InPalette then
      
      // Show Contextual Menu for Tear Off
      dim base, hitItem as New MenuItem
      base.Append New MenuItem("Tear Off")
      hitItem = base.PopUp
      if hitItem = nil then return false
      if hitItem.Text = "Tear Off" then
        
        // Remove from Window and Show Palette
        WWindow(me.Window).DetachPanel me.Type
        PaletteManager.ShowPalette CCPanel.kPanelTypeFont
      end if
    end if
  end if
  
  
  FirstX = X
  FirstY = Y
  return true
End Function

Note that we have to work around a stupid bug in the above code. I'm not sure why it's happening in this particular project, but note that ConstructContextualMenu does sometimes work in ContainerControls, it just didn't in this case. Someone should file a bug report. :^)

Sub MouseDrag(X As Integer, Y As Integer)
    if FirstX <> X or FirstY <> Y then
    dim item as DragItem
    item = me.Window.NewDragItem(Me.Left, Me.Top, me.Width, me.Height)
    item.PrivateRawData("Plte") = "Font|" + Str(me.Window.Handle) // Palette Name|Window Pointer
    item.Drag
  end if
End Sub

Here, we create the drag item for the panel tear-off. Using the PrivateRawData method, we ensure that this data is only draggable within our own application. The data in the drag item is the type of panel it is, and the handle of the window it currently resides in. We use this to know which window the panel was dragged from.


CCFontPanel
The CCFontPanel class is a subclass of CCPanel and has an interface. To create it, click "Add ContainerControl" and set its super to CCPanel. There isn't any real interface in this control except that we specify a backdrop image to substitute for not having a real interface. Other than that, there's not even any code except for a very short Constructor method which simply assigns the Type value to CCPanel.kPanelTypeFont.


WPalette
This is the window class that will contain a single panel to display them individually in floating windows. Because of the way some code we'll see later on is written, we need to intercept the Close messages and actually hide the window instead. We do this like so:

Function CancelClose(appQuitting as Boolean) As Boolean
  
  // If we don't do this, we'll be released and the window
  // will permanently go bye bye which we don't want.
  if not appQuitting then
    self.Hide
    return true
  end if
End Function

That's the only code this class contains. The code to embed and detach panels into this window is elsewhere.


WWindow
This is where the real work begins, despite the bland name. WWindows will be able to contain multiple panels along with whatever the window is supposed to contain. Although in this tutorial we only show the code to display one kind of panel in a window, you could add another CCPanel subclass and a couple more lines of code and you could have multiple panels in the window.

Because WWindow can contain multiple panels, there's a Panels(-1) as CCPanel property in the class, a long with a GetPanel(type as String) as CCPanel method which searches the Panels array for the panel with the given type. (Remember, only one instance of each type of panel can be in a window at the same time.)

Function GetPanel(type as String) As CCPanel
  dim index as Integer
  
  // Just a Simple Search
  for index = UBound(Panels) DownTo 0
    if Panels(index).Type = type then
      return Panels(index)
    end if
  next
End Function

And because panels are dragged and dropped into the window we have to make sure we accept that special type:

Sub Open()
  
  // We Accept Palette Drops
  me.AcceptRawDataDrop("Plte")
End Sub

Panels are attatched and detached from the window by type using the two methods EmbedPanel and DetachPanel. The EmbedPanel method first checks to make sure that it doesn't already have a panel of the type requested to be embedded, then creates the panel, and embeds it within the window, adjusting the interface to make room.

Sub EmbedPanel(panelType as String)
  dim panel as CCPanel
  
  // Can't have more than one
  if GetPanel(panelType) <> nil then return
  
  // Handle Embedding
  Select Case panelType
  Case CCPanel.kPanelTypeFont
    dim fontPanel as New CCFontPanel
    fontPanel.InPalette = false
    fontPanel.EmbedWithin(self, 0, 183, 421, 271)
    fontPanel.Left = 0   // Workaround for
    fontPanel.Top = 183  // a stupid bug
    EditField1.Height = 157
    panel = fontPanel
  end Select

  // Add the Panel to the Array
  Panels.Append panel
End Sub

The DetachPanel method pretty much does the opposite. It finds the panel, removes it from the window by calling the Close method, and then adjusts the interface of the window so it fills the void left behind.

Sub DetachPanel(panelType as String)
  dim panel as CCPanel
  
  // Find Panel
  panel = GetPanel(panelType)
  
  // Handle Removal
  Select Case panelType
  Case CCPanel.kPanelTypeFont
    panel.Close
    EditField1.Height = 420
  end Select

  // Remove Panel
  panels.Remove panels.IndexOf(panel)
End Sub

Lastly, the DropObject code which accepts the panel drops and calls for the embedding in the window is:

Sub DropObject(obj As DragItem)
  
  if not obj.RawDataAvailable("Plte") then return
  if obj.PrivateRawData("Plte") = "" then return
  
  // Parse Data
  dim data, paletteType as String, windowHandle as Integer
  data = obj.PrivateRawData("Plte")
  paletteType = NthField(data, "|", 1)
  windowHandle = Val(NthField(data, "|", 2))
  
  // Embed Panel
  PaletteManager.EmbedPanel paletteType, self, windowHandle
End Sub

Notice that rather than using the EmbedPanel method of the WWindow class, we call the EmedPanel method of the PaletteManager module. The PaletteManager module does several things, the one above being the handling of finding the window the panel is currently in and detaching the panel from it (unless it is a WPalette window in which case it just hides the window) and then tells the new window to embed the panel. It does some more things which we'll see below.


PaletteManger
We're almost done! The PaletteManager modules manages the global palettes by creating them, showing them, and hiding them when appropriate. It also contains the EmbedPanel method which finds the old window (the one the panel is currently in), detaches it, and then embeds a new panel in the new window. While we're talking about it, here's the code:

Sub EmbedPanel(panelType as String, newWindow as WWindow, oldWindowHandle as Integer)
  dim i, count as Integer
  dim oldWindow as WWindow
  
  // That Would Be Silly
  if newWindow.Handle = oldWindowHandle then
    return
  end if
  
  // Detach Panel from Previous Owner
  if gFontPaletteWindow.Handle <> oldWindowHandle then
    for i = WindowCount - 1 DownTo 0
      if Window(i).Handle = oldWindowHandle then
        oldWindow = WWindow(Window(i))
        exit
      end if
    next
    oldWindow.DetachPanel panelType
  else
    HidePalette panelType
  end if
  
  // Embed Panel
  Select Case panelType
  Case CCPanel.kPanelTypeFont
    newWindow.EmbedPanel panelType
  end Select
End Sub

Not rocket science, I don't think. The one thing to note is that if the user drags a panel from a palette to a window, it does NOT detach the panel from the palette, it just hides it.

The Initialize method is called in App.Open and creates the panels and a dictionary which tracks visibility of the palettes.

Protected Sub Initialize()
  
  // New Vars
  gPaletteVisibility = New Dictionary
  
  // Font Palette
  gFontPalette = New CCFontPanel
  gFontPaletteWindow = New WPalette
  gFontPalette.EmbedWithin(gFontPaletteWindow, 0, 0, 421,  271)
  gFontPalette.InPalette = true
  gPaletteVisibility.Value(gFontPalette.Type) = false
End Sub

There are two simple methods for showing and hiding palettes, and then one to check the visiblity:

Sub ShowPalette(panelType as String)
  Select Case panelType
  Case CCPanel.kPanelTypeFont
    gFontPaletteWindow.Show
  end Select

  gPaletteVisibility.Value(panelType) = false
End Sub


Sub HidePalette(panelType as String)
  Select Case panelType
  Case CCPanel.kPanelTypeFont
    gFontPaletteWindow.Hide
  end Select

  gPaletteVisibility.Value(panelType) = false
End Sub


Function PaletteIsShowing(panelType as String) As Boolean
  return gPaletteVisibility.Value(panelType).BooleanValue
End Function

Again, very simple. But guess what? That's all the code!

Finished
Yikes! That was long! And the funny part is, that was pretty simple and isn't even as rhobust as it should be, but it does work and should suffice as a good starting point or at least give you good ideas (or at the very least, one method definitely not to copy if you think it's really that horrible) to add detachable panels to your programs. As always, you can download the project here.