Saturday, July 11, 2009

Effectful Docking

In the last days I've been playing with several visual effects in docking. One such effect are initially invisible docking sites, which will occupy space only while controls are docked into them. Here the enter/leave events can be used to temporarily show the hidden site. Very nice to see, but it revealed more problems in the LCL layout manager.

Another attempt were docking outside the dock site itself, like side-by-side docking. This is already used by the LCL persistent docking approach, only without visual feedback. Here again the visual effects revealed problems in the LCL, with the resizing of the dock sites, and also with the dragmanager. It looks as if the layout manager has problems with shrinking forms or controls. The drag-dock implementation does not really allow for target locations outside the reported docking site extent (GetSiteInfo).

After all these experiments it seems to be the best and simplest solution, for a dockable IDE, when all parts are made dockable, including the initially fixed parts of e.g. the editor window(s). Such a dedicated window can have a menu, toolbar and statusbar, but even the edit notebook should initially be docked into the form, so that no special code is required for distinct docking sites within the form, and the user has really full control over the layout.

Another idea are rearrangeable elements, which are not intended to be moved outside their original site. This were the first useful application for a last-minute cancelled docking operation, that otherwise would make the dragged control floating, when the user attempts to move it out of its dedicated container.

Inspired by the hierarchical OS-X browser I came across more docking variations. With dockable lists a hierarchy can have a dynamic layout, not restricted to only vertical or horizontal orientation, and even with a mix of both elements. Then it might also make sense to minimize docked elements, implementing something like the drop-down list of a combobox, with the option to also show the list permanently, not only during a list selection.

It also turned out that the dockheader placement is not always related to the dock zone orientation. The first case became obvious with the tabbed notebook docking, where the dockheaders effectively reside in their own (toolbar) area, on top of the client area, by default. In a more grid-like layout the dockheaders could become column headers, where normally the headers would be part of the docked controls' separators. Finally something like the new Delphi component palette could be implemented, with a vertical list of dockheaders, where every such sub-list can be expanded on demand. The irregular case, when no child zone is expanded at all, can either be prevented in code (like an AllowAllUp=False property of a toolbar), or by adding a dummy zone at the end of the list, that is expanded by default, when all other zones are minimized.

A docking manager also could allow for switching the site layout, from e.g. tabbed into a list, so that both the old and new component palette could be implemented without the need for two distinct (notebook and list) container controls.

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.

Wednesday, May 27, 2009

Compatibility

Okay, FreePascal and Lazarus have to be compatible with Delphi, so that existing Delphi code can be used immediately. But what about new applications?

The Delphi VCL is dedicated to Windows, and for multi-platform applications the CLX was derived. So I wonder why Lazarus should try to cover all widgetsets with a model, that obviously is not suited for widgetsets other than Win32.

What would really change, when a LCX were added to the LCL? Not really much, I think. The LCL could stay compatible by emulating controls, that are not immediately available in a Win32 compatible form on other widgetsets, as mono does, or fpGUI. The multi-platform LCX instead could change the component model, in a way that allows to use existing components of other widgetsets almost immediately.

A different model were required e.g. for drag&drop support, where easy integration into other widgetsets could be achieved, and at the same time we could get rid of the flaws in the VCL model.

Layout management is another candidate for a different model. The Delphi docking managers are layout managers in the first place, with only an added interface to the drag manager. I cannot imagine any reason, why a layout manager has to be built into TWinControl, instead of making it linkable like an docking manager. Then we could have dedicated layout mangers for e.g. toolbars and dock trees, without a need to squeeze all layout features into one basic (anchored) layout model, that has no counterpart in the VCL.

Thursday, May 21, 2009

A Great Day

This morning I could make Paul fix some bugs, related to docking, and then I could make work a notebook for docking! A milestone, worth to remember :-)

We had a discussion about mouse messages and events, and we disagreed, as usual . Here my thoughts about this topic:

Pressing an mouse button (in detail the left one) can mean many things. Most obvious is the begin of a sequence of clicks, where Lazarus for some reason supports up to quadruple clicks. If only one event shall be signaled, then the handling of the mouse up and down messages must be delayed, until the timeout or maximum sequence number is reached. When a mouse down also can start dragging, processing can be inlined into that delayed handling. As the word says, "drag" means *moving* something, so that an automatic dragging operation always should start only after an significant move of the mouse. When the dragging threshold was not reached on mouse up, no dragging actions or messages should have occured, and the click processing should continue.

Such processing would eliminate the long standing note on WM_MOUSEDOWN in TControl.WndProc, where Delphi stops further processing after starting the dragmanager, and the LCL continues to process the message. IMO it would be much more transparent to stop handling of the message, and to leave it to the drag manager to delay further processing, until it's clear whether a drag should start, or normal click handling should occur. Or the drag manager could be invoked only in that place, where a mouse move would break a sequence of clicks. This latter solution would concentrate all decisions in one place, so that conflicts with already started dragging operations can no more occur. The TControl.IsDragging method could be based on a flag in the control's state, that steers the handling of all mouse messages. The control states then become:


IDLE <down ---> DOWN /move ---> DRAGGING <up ---> DROPPED
\ up ---> 1CLICK - - - > 4CLICK


Then also the dragging threshold could reflect the system threshold, not an application specific (and consequently unexpected) value.

Tuesday, May 19, 2009

Messages from the Mouse

The handling of mouse button messages is a mess :-(

An attempt to start dragging of a control, from a click on the zone header, seems to block proper processing of further messages. We should re-think the handling of WM_LBUTTONDOWN, with regards to multiple widgetsets and dragging.

When the left button is pressed over a control with DragMode=dmAutomatic, the drag manager should handle this and further messages. It seems not to be a problem to capture input, but the drag manager is responsible for the distinction of immedately started dragging, delayed started dragging (using threshold), and simple clicks on mouse up before dragging starts (threshold not reached). When dragging starts, the dragged control should either not have processed the button down message, or must be reset into unpressed state (CancelMode). When no drag really starts, the drag manager can send both an down and up message to the control, so that it can react on an click in the usual way.

Effectively a mouse button down message should be processed either by the drag manager or by the control, but not by both at the same time!

BTW, dragging from the zone header now works on Linux/gtk2, but no more on Windows/win32 widgetset.

Monday, May 18, 2009

Back to the Roots

Now I've tried to get the best out of three worlds, and could make docking work again. The dock helpers moved into the dock site unit, and multiple or configurable dock headers will come soon. Unfortunately some features got lost, for sometimes obscure reasons. At least the layout still works more stable than LDockTree, when multiple zones come into play.

Friday, May 15, 2009

How fragile a simple model can be...

In the last updates of the EasyTree I "only" changed the zone orientation meaning, and moved the splitter from the zone header to the opposite side of the zone - and now the layout seems to be broken :-(

While everything looked fine at the first glance, when docking and undocking components, some zones refused to resize properly. The more I tried to find the reason for that strange behaviour, the less I understood what really was going on. When I finally tried to redesign the entire handling of the zone bounds, I learned more about the subtle design of my tree model, and that it was not a good idea to change it at all.

Now I have to revert to the last working state, long before the addition of the notebook zones, and must verify that in this state all zones resize properly. Then I can introduce the notebooks again, and hope that then they will resize properly.

Wednesday, May 13, 2009

What will happen on the end of a drag?

When a drag-drop operation ends, the target control is notified of the drop. The entire operation can be canceled by pressing the ESC button.

But when a docking operation ends, we should have more options. The "normal" end will dock the dragged component into the target host site, or the component will become (or stay) floating on the screen, or the entire operation will be aborted. The user should get enough visible feedback, so that he can know what will happen when he releases the mouse button. The following changes to the LCL are suggested:

A NoDrop cursor should be used, when nothing will happen on a drop. This kind of feedback is essential when the dragged component would be dropped onto itself, whereupon nothing should happen at all. This feature is not part of the Delphi/Lazarus drag-drop implementation, where the default docking object does not support state specific cursors, and the drag object has no property for an NoDrop state at all.

The Lazarus implementation also doesn't handle the CTRL key, for making a component float even if it moves over a dock site.

Now I hope that the according patches are added to the LCL soon...

Fundamentals

Dragging GUI elements is a widely used technique, available on most platforms. Delphi has rudimentary support for both drag-drop (copy contents) and drag-dock (move forms or components), but the support for docking is a mess :-(

Lazarus already has slightly improved support for docking, but still lacks some features, and the support is platform dependent. While the platform specific support will be improved over time, by the famous Lazarus team, I feel a need to discuss the general implementation of dragging (DragManager etc.), and the available or missing features.

For some documentation on dragging visit the Lazarus wiki, and for a live example run the Lazarus\examples\dockmanager\easydocking project. More example projects are in preparation.