Container Merges - GimmickNG/AIRDock GitHub Wiki

IContainer instances are not usually added directly to other IContainer instances as their children. In fact, the default addContainer() function is provided as a convenience method to provide compatibility with other interfaces; in AIRDock, and its default implementations, however, containers are never added as subcontainers of other containers.

Instead, container contents are moved. The end result is the same, but there's a subtle difference between the two.

Suppose there's 2 containers, A and B. They are currently discrete containers, each part of a separate root container (such that the two are disjoint); now suppose A is to be added as a subcontainer of B, such that:

         BEFORE
      X          Y
     / \        / \
    A   *      B   *
    |          |
  [d,e]      [g,h]

           AFTER
      X             Y
      |           /   \
      *         B      *
               /   \
              A   [g,h]
              |
            [d,e]

Where upper case characters are containers (non-terminal), lower case characters in brackets are panels (terminals/leaves), and * indicates an empty container which has been left out for brevity (alternatively represented as a non-terminal with an empty set of terminals below it, but * is more concise).

The default implementation, DefaultContainer, is internally represented as a full binary tree, where each container can have either 0 or 2 containers below it; if, and only if, the container has 0 containers below it, can it have any number N of panels and function properly (undefined behavior occurs when a container has both panels and containers below it.)

A simple sequence of steps is shown for the above scenario, in two ways: how it can be done, and how it is done.

How it can be done:

  1. Remove A from X.

             BEFORE
          X          Y
         / \        / \
        A   *      B   * 
        |          |
      [d,e]      [g,h]
    
                     AFTER
          X            Y           A
          |          /   \         |
          *         B     *      [d,e]
                    |
                  [g,h]
    
  2. Add A as a child of B.

                BEFORE
      X            Y           A
      |          /   \         |
      *         B     *      [d,e]
                |
              [g,h]
      
           AFTER
      X            Y
      |          /   \
      *         B     *
              /   \
             A   [g,h]
             |
           [d,e]
    

However, this isn't how it's done! The reasons for that will be explained later.

How it's actually done:

  1. B creates a new container, C, but it's not added to B just yet (the : denotes a weak link between two containers, unlike /, | or \ which denotes a strong link between two containers)

             BEFORE
          X          Y
         / \        / \
        A   *      B   *
        |          |
      [d,e]      [g,h]
    
              AFTER
          X           Y
         / \        /   \
        A   *      B     *
        |          : \
      [d,e]        C  [g,h]
    
    
  2. Move the contents of A to C. This is the crucial part of the default IContainer implementation, and indeed the implementation that is preferred by AIRDock: containers are never removed from their parents except under certain circumstances (more on that later).

          X           Y
         / \        /   \
        A   *      B     *
                   : \
                   C  [g,h]
                   |
                 [d,e]
    
  3. Create a new container, D, under B; move B's contents to D, but don't add D to B just yet (aka, make a weak link between B and D)

          X           Y
         / \        /   \
        A   *      B     *
                :     :
                C     D 
                |     |
              [d,e] [g,h]
    
  4. Commit changes by making C and D strong links.

          X           Y
         / \        /   \
        A   *      B     *
                 /   \
                C     D 
                |     |
              [d,e] [g,h]
    

You might have noticed that the key difference is that since the containers' contents are always moved into a new container (be it A -> C or B -> D), the original containers are still part of the same roots (A's root is still X, B's root is still Y, and A and B are still disjoint).

How?

The mergeIntoContainer() function defined in the IContainer interface is the primary method used to merge containers. It accepts one parameter, which is another IContainer instance into which the calling container has to merge into (i.e. A.mergeIntoContainer(B) merges A's contents into B's)

Merging is performed recursively in postorder. That is, if A has subcontainers below it and B also has subcontainers below it, then first A's subcontainers are merged with B's subcontainers, propagating upward until finally A's contents are merged with B's.

Why?

Remember the earlier bit about parked containers? Parked containers are rooted containers, and more importantly, are never removed from their panel's nativeWindow's stage. If the first method of moving containers (that is, directly adding them) were used, then the following would occur:

         BEFORE
     X           Y
     |           |
    [a]          *
         AFTER
           Y
           |
           X
           |
          [a]

As a result, X would lose the original reference to its panel's nativeWindow! There'd be no way for the panel to detect if it were docked or integrated, because recollect that a panel is docked when it's part of its nativeWindow's container - however, that container's part of another container which may or may not be visible, and may or may not be in a nativeWindow created by the IBasicDocker! As a result, a panel may appear to AIRDock to be docked, but in reality it could still be part of a user-created root container (which would otherwise yield integrated).

By using the second method of merging containers instead, the following occurs:

         BEFORE
     X           Y
     |           |
    [a]          *
         AFTER
     X           Y
     |           |
     *           Z
                 |
                [a]

It's now clear to AIRDock that the panel a is integrated and not docked, since it's not part of container X.

So when are containers actually removed?

The only times when containers are removed are when either of the following events occur:

  1. A container is emptied - either a merge has been requested into another container, or all the children have been manually removed, or
  2. A container requests to be removed from its parent.

Both of the above are cancelable events (in fact, #1 is a consequence of #2 - the DefaultContainer requests to be removed when it gets emptied.) However, the above doesn't really apply to root containers, since they do not have any parent containers in any case, and so parked containers are not affected by a merge operation.