Monday, June 29, 2009

Dock Sites

Yesterday I came about problems with complex host dock sites, caused by a stupid copy of Delphi VCL code. When a control adds and removes *itself* to the FDockClients of the dock sites, this behaviour is not proper OO design. Instead the control should *ask* the dock sites to add or remove itself.

The consequences of that misdesign become obvious with complex dock sites, like notebooks for themselves or as part of an dock tree. In this case we have two nested dock sites, the notebook and its container dock site. On a ManualDock into the container dock site, the controls will add themselves to the dock clients of that dock site, what certainly is wrong. In an drag-dock it depends on the proper determination of the Z-order, whether a control is docked into the notebook or into its enclosing container dock site. But in either case the dropped control must become a dock client of the notebook, so that undocking the notebook will not leave references to the notebook clients in the old host dock site.

Now a redesign of the docking procedures and methods is required, which eliminates any guesses about the structure, contents and management of dock sites. The most important change must remove all external references to the DockClients list of a dock site. Effectively DockClients should be managed by an DockManager only. Without an DockManager there exists no need for handling dock clients different from other components in a dock site, and the related methods (GetDockClientCount...) can be removed entirely. When a DockManager is removed, the existing DockClients must become ordinary controls of the dock site. In order to prevent the introduction of new methods, for adding and removing dock clients, the operations have to be moved into the appropriate methods. In that move also the currently unconditional undocking has to be removed, that leads to inconsistencies when a control is re-docked within the same dock site.

Furthermore the drop alignment and target controls have to be determined by the dock manager of a dock site, what already is required for notebook docking, and also is required for docking models other than tree-docking. In fact the DropOn control and DropAlign are applicable *only* to tree docking, and no control, drag manager or drag performer should assume anything about the docking process (DockRect...), at least when DropAlign=alCustom.

Wednesday, June 24, 2009

Dragging Deferred

Sometimes it's desireable to start a dragging operation in code, for a specific control. One such case is the dragging of forms from a menu or button, when dragging forms is not supported by the widgedset, or for dragging a docked control from a dockzone header.

Now it looks to me as if the widgetsets behave differently, when Control.BeginDrag is invoked. With gtk2, which does not (yet) allow to drag forms, dragging a control from the zone header starts and completes as expected - the docked control can be undocked and docked again somewhere else. With the win32 widgetset the GUI hangs, and when it becomes responsive again, after some clicking, the wrong control may have become floating. How that?

This behaviour may result from the capture and handling of mouse events, where the LButtonDown on the zone header will set csLButtonDown in the ControlState of the dock site (container TWinControl), in TControl.WndProc. This flag should be cleared during the start of dragging, but there exists no information about *which* control has this flag set. When the DragManager assumes that the flag was set for the control to be dragged, this assumption is not always correct. In Delphi the this flag is not set when the DragManager is invoked by BeginAutoDrag (see the lengthy comment in TControl.WndProc, case LM_LBUTTONDOWN, in control.inc).

Now I need some more detailed information about the handling of mouse button events: what will happen when a button is pressed over one control, and is released over a different control?

At least the mouse buttons and states would be much easier to handle, when the target and mouse position of a button press would be stored globally, so that the button-down flag can be cleared in any case, in the control with this flag set. Then it also were possible to delay the start of a dragging operaion in the TControl.WndProc, when the dragging threshold is exceeded by a mouse move, relative to the (globally) stored mouse-down position. The mouse button flag(s) IMO should be removed from the controls' state, and should be moved into the Mouse object instead. Then all clicks and moves could be handled inside the mouse object, and only the resulting Click events etc. should be sent to the affected control.

Monday, June 22, 2009

Anchor Docking

Anchor docking has nothing in common with drag-dock. It allows to glue forms together, in a dedicated host window. This requirement for a host window can lead to unwanted effects, when e.g. an explorer or message window is docked to an editor window - the editor window will become a child of the host window. In the Delphi docking model instead the editor window will stay a top level window, and the docked window would become a child of the editor window, When used to create a monolithic (SDI) IDE, the IDE main bar would become a child of another host window. The IDE window can not be a dock site itself, because it's impossible to dock another (single) window into an empty dock site.

The user currently is more restricted in the arrangement of docked windows than in the Delphi docking model, due to some flaws in the anchor controls. This might be improved by an extended anchor editor, as already used in the form designer. The model has more flaws and bugs, e.g. the forms tend to jump over the screen when forms are docked or undocked, and host windows cannot be merged or docked together. The dock headers have inconsistent alignment, and the anchors are not properly constructed. I don't want to go into details now, just before the next official Lazarus release.

Docking forms seems not to be a good idea, when their menus are inaccessible in docked state and further complications occur with various widgetsets. IMO we should collect all experiences with the pro's and con's of the implemented (and other) docking managers, and implement a better docking model at least for use in the IDE.

Saturday, June 20, 2009

Sizeable Zones

A dock tree is subdivided into zones, which contain either a docked control or child zones. When the zones shall be resizeable, splitters have to be inserted into the layout. In the EasyDockTree the splitter areas are part of the zones, not controls between zones as in an anchored layout. In the former implementation the splitter was part of the zone header, so that only zones with a child control and according header could have an splitter. A new model should allow for splitters also between zones without headers.

When splitters in a dock tree can reside at the bottom or right of a zone, every zone has an splitter area when its bottom/right is less than the root zone bottom/right. That's easy :-)

But which zone does a splitter area belong to? A zone can have splitter areas at both its bottom and right, at the same time, and at most one of these can belong to the zone splitter itself, the other area(s) belong to the splitters of some parent zone. Also easy: when a zone has an next visible sibling, it also must have an splitter in front of that sibling.

How are these splitters involved in coordinate calculations?

Currently every zone has stored its bottom and right coordinates. Either coordinate has to be decremented by the width of an splitter when a splitter exists in that direction, i.e. when the coordinate is less than the corresponding root zone coordinate. The remaining area can contain child zones, or is subdivided into a client (control) area and a header area, which again is subdivided into button areas and the caption.

When a zone is resized, and it is a leaf zone (contains a control), then the control occupies the zone's client area, otherwise the child zones occupy the full zone extent and the zone has no header.

When a header has to be drawn for a leaf zone, the splitters have to be excluded from the zone rectangle, then the width or height has to be adjusted to the header size.

A hit test can be performed top-down, excluding zones to the top or left of the mouse position, then recursing down into the children of the matching zone. Before the child zones are checked, a hit on the zone splitters has to be detected, so that a splitter of the topmost zone is found immediately. When the right mouse button is pressed on a splitter, the site's splitter control is positioned to the zone's splitter area. Since the splitter areas can overlap at the bottom right of a zone, the zone's orientation has to be checked in order to give precedence to the splitter between the top level siblings. The same considerations apply to the placement of splitters in an anchored tree layout.

Now I'll try to implement this model in the EasyDockTree. Then some thoughts on the handling of invisible controls are required, and about chances for collapsing zones, i.e. hiding a child control while still showing the zone header.

Tuesday, June 16, 2009

Capturing Input

What's special about mouse (and keyboard) capture?

IMO several objects can capture input, like a designer, drag manager, or any control. A designer is a special case, because it's bound to a special component (form) and its child controls, so that every control must check whether it's in csDesigning state itself, or its parent is in csDesigning state. Since further actions will differ between normal and designing state, a flag in every child control may be the best solution. All other captures are global, so that a check for a non-nil capture object will allow to forward all messages. Hereby the type of the capturing object seems not to play a role, the object captures all system messages from all receiving (windowed) components. In a very general model a reference to a WndProc can be used to allow capturing by objects of any kind. When the capture has to be released, a WM_CANCELMODE can be sent to the WndProc, and then the WndProc is reset to nil.

The global capture should be exclusive, the target can change only when the current target agrees to release the capture, or when the capture inevitably has to be released.

The drag manager should only capture input when the preconditions (threshold) are met. A delayed drag start, or competition between clicks and dragging, can be handled in the receiver of the uncaptured messages. A global variable can hold the position of the last button down message, for the threshold check. With the above convention, that an active capture is signaled by a non-nil WndProc, the messages can be routed immediately to the drag object (Delphi compatible). Then the different behaviour of drag-drop and drag-dock can be implemented in the according drag objects.

Monday, June 15, 2009

Who Manages Docking?

The default (unmanaged) Delphi behaviour drops a control into a docksite as is, at the current position. This mode can be used e.g. in designers (CAD...), where a control is selected and placed on a drawing surface.

Docking managers will organize the layout of a docksite, usually by filling the entire site with the dropped controls (tiling).

Other docksites can implement their own docking, as e.g. a sophisticated version of beforementioned designers. They also can implement docking of pages in a TPageControl, and provide tabs for every dropped page.

What's essential for these various docking models?

The interaction between the dragmanager and the source and target controls occurs in dsDrag... messages. dsDragEnter and dsDragLeave notify the target site, when the dragged control enters or leaves the docksite's area. A site can prepare for an eventual drop, signal preferred locations to the user, or whatever may be desireable. dsDragMove then shall determine an concrete docking place and provide visual feedback, by modifications to the cursor and the docking rectangle. Like in drag-drop, a non-accepting situation should be signaled clearly.

On a drop the docksite receives another dsDragLeave, so that it can be reset into "normal" (inactive) mode. Then the control is dropped and can be placed according to the information in the dockobject, resulting from the last dsDragMove. In ManualDock no dsDragMove occurs, so that all information must be filled into the dockobject before. The required information can differ widely, depending on the docksite management. The standard DropOnControl and DropAlign are applicable almost only to tree-docking, worthless in unmanaged docking.

The docking information should be handled by the site manager, i.e. either by DockManager, or in the overridden methods of a specialized docksite (TWinControl). The DragManager only has to determine the target site, send the drag messages to it, and then let the dockobject do the visual feedback. Please note that not only the docksite can be designed freely, it also can provide special docking objects, which can contain any additional information for special site layouts and feedback. We already have such cases, e.g. the anchored docking doesn't fit into the tree docking model, because it deserves more and different information. Therefore the DockObject should be passed to the target site, and from there to the DockManager (if used), instead of the mostly useless tree docking information. This way the site and the DockManager have full control over the information in the DockObject.

Unless we agree about some special default (unmanaged) behaviour, different from the Delphi model, the involved docksites and controls should do nothing themselves. They either pass the drag messages to their DockManager, or to the user provided message handlers. If neither is present, only the floating rectangle has to be moved.

The DragManager only has to distinguish between drag-drop and drag-dock in the determination of the drop target. This can be done in the different drag- and dock-objects, as currently is done in the drag performers. IMO such performers are not needed at all, because they cannot know what a special docksite or docking objects have implemented.

Sunday, June 14, 2009

Workarounds for gtk2

In the last day I found two workarounds for incooperative window managers. The first one is about minimized applications, where all windows are minimized together with the main form, but are not restored when the main form is restored later. Programmatical restoration in an event handler of the main form seems not to work, but an entry "Restore All" in the Window menu will do the job.

The next workaround is about docking forms. Assuming that we want to dock windows rarely, a (checkable) menu entry "Dock" in the Window menu can make visible (or hide) an dockable frame in every dockable application window. This frame can be docked in all widgetsets, and when finally an UnDock of the dropped frame is denied, the *form* is docked instead of the frame! This certainly is not expected behaviour, but it will do exactly what we need for now :-)

The docking behaviour of the SynEdit component has become worse in the last revisions. While a SynEdit still refuses to dock in a given location, with a given size, it now also refuses to drag at all. Before it had been possible to drag it from the gutter area, which now doesn't work any more. This may be related to some general change in the behaviour of (windowed?) controls, which seem to reject any BeginDrag requests, by possibly changing the request from the start of a drag-dock into a drag-drop.

Tuesday, June 9, 2009

The Empire Strikes Back

The last changes to the LCL and my docking manager have made docking much more useful, across all platforms. The lack of dockable forms, on some platforms, lead me to a new notebook, whose pages can be undocked by dragging their tabs. But now a lurking bug started to manifest, and this one deserves another change to the LCL :-(

The woes started with the flawed Delphi model, that did not initialize the DropAlign before invoking PositionDockRect; everything happened to work only because the DropAlign was determined later, and then was used in the next invocation of PositionDockRect. Next came the docking manager, that was ignored in the entire determination of the DropAlign. After some experiments I found an patch, that finally looked acceptable to Paul, and was added to the LCL. Now the docking manager had a chance in TDockManager.PositionDockRect to determine the drop zone and alignment, and to store these values in the docking object.

Unfortunately that patch results in a wrong DropAlign in the very last step of docking, when a control is re-docked within its host docksite. Then it can happen that the already taken removal of the control leads to a changed site (tree) layout, with a new DockRect and DropAlign.

Now it's time again to revert to one of my earlier suggestions, to let the docking manager determine the DropAlign in GetDockEdge, so that PositionDockRect will find the DropAlign already set properly and must not do any new arbitration. TControl.GetDockEdge only has to find out, whether its HostDockSite uses an DockManger, and invoke its new GetDockEdge method instead of the standard procedure. Remember that GetDockEdge can return nothing but left/right or top/bottom alignment, because it has no idea of dock zones, notebook docking or other special docking modes, implemented in a specialized docking manager.

The new TDockManager.GetDockEdge method can implement the same algorithm as used in TControl.GetDockEdge, or any other algorithm in an overridden method. One problem is the mouse position, relative to the DropOnControl. When a docksite contains dock headers, in addition to the docked controls, no DropOnControl can be found when the mouse hovers over an header. And when a control is found, its provided BoundsRect only covers the control, not the dock zone. This means that the drag manager should immediately invoke the dockmanager's GetDockEdge, and supply it with the mouse position within the dock site, regardless of any DropOnControl. OTOH a usable default implementation of the new TDockManager.GetDockEdge will fall back to the TControl.GetDockEdge method. Consequently the dockmanager should receive the docking object, that contains all required information for all possible cases.