Thursday 25 November 2010

Multiple Layout Frame issue

Whilst the blog has been quiet of late, work is progressing on the DUIM port. Ongoing work currently includes focus and keyboard handling, mnemonics and accelerators, and making use of GTK stock items where possible.

As usual, I've been attempting to fix failing tests too. The most recent one to be resolved is the "multiple layout frame". In DUIM (and CLIM) it's possible to define multiple layouts and to switch between them. This test verifies that the layout-switching functionality works and needless to say it didn't work initially. The frame was mapped correctly and the first time the layout was switched worked too, but the next time the layout was switched DUIM would blow out with a "no mirrored ancestor" error for one of the drawing areas.

After much head-scratching the clouds of mystery evaporated; it is probably useful to see parts of the macroexpansion of the <MULTIPLE-LAYOUT-FRAME> definition form:


(DEFINE-FRAME-CLASS <MULTIPLE-LAYOUT-FRAME> (<SIMPLE-FRAME>)
((CURRENT-LAYOUT :INITFORM :DEBUGGING :ACCESSOR CURRENT-LAYOUT)
(:PANE FILE-MENU (FRAME) ...
(:PANE SWITCH-BUTTON (FRAME)
(MAKE-PANE '<MENU-BUTTON>
:LABEL "Switch layouts"
:ACTIVATE-CALLBACK
#'(LAMBDA (BUTTON)
(ECASE (CURRENT-LAYOUT FRAME)
(:DEBUGGING
(SETF (CURRENT-LAYOUT FRAME) :INTERACTING)
(SETF (FRAME-LAYOUT FRAME)
(INTERACTING-LAYOUT FRAME)))
(:INTERACTING
(SETF (CURRENT-LAYOUT FRAME) :DEBUGGING)
(SETF (FRAME-LAYOUT FRAME)
(DEBUGGING-LAYOUT FRAME)))))))
(:PANE EXIT-BUTTON (FRAME) ...
(:PANE CONTEXT-PANE (FRAME) ...
(:PANE STACK-PANE (FRAME) ...
(:PANE SOURCE-PANE (FRAME) ...
(:PANE INTERACTOR-PANE (FRAME) ...
(:PANE MESSAGE-PANE (FRAME) ...
(:PANE DEBUGGING-LAYOUT (FRAME)
(VERTICALLY NIL
(WITH-BORDER (:TYPE :SUNKEN)
(CONTEXT-PANE FRAME))
(HORIZONTALLY NIL
(WITH-BORDER (:TYPE :SUNKEN)
(STACK-PANE FRAME))
(WITH-BORDER (:TYPE :SUNKEN)
(SOURCE-PANE FRAME)))
(WITH-BORDER (:TYPE :SUNKEN)
(INTERACTOR-PANE FRAME))
(WITH-BORDER (:TYPE :SUNKEN)
(MESSAGE-PANE FRAME))))
(:PANE INTERACTING-LAYOUT (FRAME)
(VERTICALLY NIL
(WITH-BORDER (:TYPE :SUNKEN)
(INTERACTOR-PANE FRAME))
(WITH-BORDER (:TYPE :SUNKEN)
(MESSAGE-PANE FRAME))))
(:LAYOUT (FRAME) (DEBUGGING-LAYOUT FRAME))
(:MENU-BAR (FRAME) ...
(:DEFAULT-INITARGS :WIDTH 300 :HEIGHT 400))

(DEFMETHOD FILE-MENU ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD SWITCH-BUTTON ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD EXIT-BUTTON ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD CONTEXT-PANE ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD STACK-PANE ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD SOURCE-PANE ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD INTERACTOR-PANE ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD MESSAGE-PANE ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...

(DEFMETHOD DEBUGGING-LAYOUT ((FRAME <MULTIPLE-LAYOUT-FRAME>))
(LET ((DUIM-FRAMES-INTERNALS::_FRAMEM (FRAME-MANAGER FRAME)))
(OR (DEBUGGING-LAYOUT-PANE FRAME)
(SETF (DEBUGGING-LAYOUT-PANE FRAME)
(WITH-FRAME-MANAGER (DUIM-FRAMES-INTERNALS::_FRAMEM)
(VERTICALLY NIL
(WITH-BORDER (:TYPE :SUNKEN)
(CONTEXT-PANE FRAME))
(HORIZONTALLY NIL
(WITH-BORDER (:TYPE :SUNKEN)
(STACK-PANE FRAME))
(WITH-BORDER (:TYPE :SUNKEN)
(SOURCE-PANE FRAME)))
(WITH-BORDER (:TYPE :SUNKEN)
(INTERACTOR-PANE FRAME))
(WITH-BORDER (:TYPE :SUNKEN)
(MESSAGE-PANE FRAME))))))))

(DEFMETHOD INTERACTING-LAYOUT ((FRAME <MULTIPLE-LAYOUT-FRAME>))
(LET ((DUIM-FRAMES-INTERNALS::_FRAMEM (FRAME-MANAGER FRAME)))
(OR (INTERACTING-LAYOUT-PANE FRAME)
(SETF (INTERACTING-LAYOUT-PANE FRAME)
(WITH-FRAME-MANAGER (DUIM-FRAMES-INTERNALS::_FRAMEM)
(VERTICALLY NIL
(WITH-BORDER (:TYPE :SUNKEN)
(INTERACTOR-PANE FRAME))
(WITH-BORDER (:TYPE :SUNKEN)
(MESSAGE-PANE FRAME))))))))

(DEFMETHOD FRAME-LAYOUT ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...
(DEFMETHOD FRAME-MENU-BAR ((FRAME <MULTIPLE-LAYOUT-FRAME>)) ...



The defining form starts with the frame class itself. There's nothing particularly interesting in this part of the code so much of the code is elided. The parts of interest have been left in. Initially the frame layout is the "debugging layout" which shows a bunch of panes:



This layout is generated when the DEBUGGING-LAYOUT method is invoked, which returns either the DEBUGGING-LAYOUT-PANE if the method has been invoked before (i.e. the layout has already been shown), or makes all the panes in that layout and then returns them (when the layout has not been shown).
The INTERACTING-LAYOUT method behaves similarly, and creates a layout that looks like this:



The "switch layouts" button unmaps the sheets included in the frame's current layout, and maps the sheets included in the layout being switched to.

The thing to note is that both these layouts refer to the some of the same child panes (i.e. the "interactor pane" and the "message pane").

So what happens is:-

1. The frame is initially shown with the debugging layout. The DEBUGGING-LAYOUT method is invoked to return the layout. Since the panes haven't been constructed yet, the layout is constructed and stored in the DEBUGGING-LAYOUT-PANE slot.

After this step, the pane hierarchy is similar to the following:


DEBUGGING-LAYOUT
VBOX
... more panes ...
BORDER-PANE D1
INTERACTOR-PANE P1
BORDER-PANE D2
MESSAGE-PANE P2


At this point all parent - child relationships are correct. In particular, the INTERACTOR-PANE P1 and MESSAGE-PANE P2 are added as children of their respective border panes and the parents of P1 and P2 are the border panes D1 and D2 respectively.


2. Switch layouts; the INTERACTING-LAYOUT method is invoked to return the layout. Since the layout hasn't been constructed yet, it is constructed and stored in the INTERACTING-LAYOUT-PANE slot.

After this step, the pane hierarchy is similar to the following:


INTERACTING-LAYOUT
VBOX
BORDER-PANE I1
INTERACTOR-PANE P1
BORDER-PANE I2
MESSAGE-PANE P2


At this point all parent - child relationships in this layout are correct. Note that whilst the VBOX and BORDER-PANEs are all new sheets and play no part in the DEBUGGING-LAYOUT, the panes P1 and P2 are the same instances that are also in the DEBUGGING-LAYOUT.

Since sheets are constructed from the top-down, the parent of P1 is now I1 and the parent of P2 is now I2.

Note that when children are added to a sheet, both the children and the new parent are modified, but any old parent of the child sheets is not informed of any changes (which is fortunate, or this issue wouldn't be as easily fixed).


3. Switch layouts a second time; the DEBUGGING-LAYOUT method is invoked to return the layout. Since the layout has already been constructed, the layout stored in the DEBUGGING-LAYOUT-PANE slot in step 1 is returned:


DEBUGGING-LAYOUT
VBOX
... more panes ...
BORDER-PANE D1
INTERACTOR-PANE P1
BORDER-PANE D2
MESSAGE-PANE P2


Unfortunately, whilst the border panes D1 and D2 have panes P1 and P2 as children, the parents of panes P1 and P2 were changed in step 2 to be the border panes I1 and I2, so when Silica tries to map P1 or P2 it complains that they don't have a mirrored parent.


To fix this the code now checks, when the frame layout is changed (the methods involved in the backend are actually UPDATE-FRAME-WRAPPER, and TOP-LEVEL-LAYOUT-CHILD), that all the sheets involved in the layout have the correct parents set. The parent has to be changed where they're wrong using the "internal" slot writer to prevent the border pane (which already has the child to be updated as a child) from complaining that it already has a child.

I'm not sure I like the fix (using a slot-writer that isn't exported feels wrong to me, and I'm not 100% that the fix won't introduce really subtle bugs in the future if the right circumstances should be met) so I'll be keeping this on my todo list (and reminding myself of it with this blog posting).

2 comments:

  1. Could you link to things you are talking about? This sounds interesting, but I have no idea what DUIM is and a very brief Google didn't enlighten me...

    ReplyDelete
  2. I think it's the Dylan's UI manager:
    http://wiki.opendylan.org/wiki/view.dsp?title=DUIM

    ReplyDelete

Followers