Mattias did an good job in the move towards a dockable IDE :-)
There exist a few problems with the form creation, that prevent proper operation of drag-dock. At best the IDE should not try to dock or otherwise place dockable windows.
A general problem is the update of the Caption of docked forms - their Caption is invisible! As a simple solution every change of the Caption of a docked control should be sent to its HostDockSite, so that it can update the DockCaption. An according UpdateDockCaption method should be added to TWinControl, which does nothing by default; TCustomForm instead updates its Caption, so that all (floathost...) docksites will always show the proper caption.
See http://www.hu.freepascal.org/fpcircbot/cgipastebin?msgid=18989
Monday, May 31, 2010
Monday, April 26, 2010
Flicker
Some flicker and other nasty things can be caused by excessive or inappropriate coordinate re-calculations while docking. This is partially caused by the flawed Delphi implementation, and is made even worse in further LCL hacks. Since the TDockManager interface has been improved already, much code can be removed from the LCL.
First we should agree that managed and unmanaged docksites require different handling of docking coordinates and other details. When a DockManager is installed in the target site, it will manage the DockRect (visual feedback) and finally will adjust the placement of the dropped control, including dock headers etc., eventually overriding user changes in event handlers. Without an DockManager, some calculations should be performed and reflected in the DockRect, and no final adjustment should be performed - this would be fooling the user. Also the calculation of BoundsForNewParent is questionable, at least for managed sites. Premature changes of the BoundsRect are a bad idea at all.
I'll outline the required changes to the TControl and TWinControl classes in futher comments to this entry, based on Lazarus revision 24970.
First we should agree that managed and unmanaged docksites require different handling of docking coordinates and other details. When a DockManager is installed in the target site, it will manage the DockRect (visual feedback) and finally will adjust the placement of the dropped control, including dock headers etc., eventually overriding user changes in event handlers. Without an DockManager, some calculations should be performed and reflected in the DockRect, and no final adjustment should be performed - this would be fooling the user. Also the calculation of BoundsForNewParent is questionable, at least for managed sites. Premature changes of the BoundsRect are a bad idea at all.
I'll outline the required changes to the TControl and TWinControl classes in futher comments to this entry, based on Lazarus revision 24970.
Docking Forms
Since most widgetsets do not (yet) allow to dock windows in the LCL, a commonly usable workaround must be found. Affected are all controls that eat up mouse events themselves, or which do not create events for the non-client area.
A stable and widgetset-independent solution requires a dedicated "dock grip" component, integrated into the forms or controls. That component can react on mouse moves with the left button down, by starting the dragging of the associated control; this can be the control's Parent (form, toolbar), the page of a notebook, or the control in a dock-zone.
My first approach used a red pin icon in the client area of a form, but this turned out to be very sensitive to changes in the layout (Z-order...) at runtime. It also eats up some space in an otherwise tabular layout, where it should occupy a cell or entire row or column of its own. E.g. a (editor) notebook has no room left for an additional control inside its tabs or editor page.
The next (current) approach uses dockheaders, as already implemented in a docktree or dockbook, to start dragging of the associated client control or notebook page. For that purpose all floating controls (forms...) have to be wrapped into a FloatHostSite, whose DockManager already implements all the functionality of the dockheader. When the dockheader is styled like the grip of a toolbar, this approach fits nicely into any GUI. The DockManager also allows to dock further controls into the same site, eliminating the need for additional ConJoinDockHost sites etc.
Another approach could use dedicated dock-grip controls, as bars with the appearance of commonly used grips (in toolbars...). These tiny bars could be added to a side of any form, just like elastic sites are added, leaving the form layout intact except for a minor reduction of the client width or height. But these controls should be hidden whenever the form is docked, since then the dockheader of the docksite would play the role of the dock-grip. Furthermore such controls would not allow to dock forms together, so that I won't consider this approach right now.
There remain still at least two major issues with wrapping dockable controls into a floating site, the automatic creation of the appropriate FloatHost sites, and the handling of these sites in the store/reload logic of a layout. I already tested the used of the FloatingDockSiteClass property, and found it working in the LCL even with forms, that otherwise float for themselves; perhaps somebody anticipated my idea already?
A minor issue with such floating sites is the visibility of the docked control, when it was just created in invisible state, to reduce flicker. The solution is simple, since it won't do any harm to make the docked client visible in TControl.DoFloatMsg.
Another issue is the exact FloatHost class, that also is used in the Floating property (GetFloating). The meaning or implementation of [Get]Floating had to be changed somehow, when multiple clients can be docked together in the same FloatHost, or in a different FloatHost class. Eventually a dedicated DefaultFloatHostClass can be added to the LCL, that is used with every dockable control (DragKind=dkDock) that has no dedicated FloatingDockSiteClass(=nil).
The currently last issue may deserve more changes to the LCL, related to storing and reloading layouts. Currently persistent layouts are implemented in the DockMaster, which owns all temporary docksites like floating sites, dockbooks and elastic panels. That's why the use of a dedicated DefaultFloatHostClass should be implemented, that is owned by the DockMaster, instead of by Application. At least it should be possible to change the Owner of the floating sites when these are created, but this only would defer the problem, not solve it.
The current implementation of the EasyDockTree and DockMaster try to reduce the dependencies on other (non-LCL) classes, but this is feasable only to some degree. The DockManager with notebook docking capabilities already requires an according notebook component. The DockMaster introduces further classes for floating dock sites and elastic panels, which support the layout streaming, and introduces a layout storage format. All these classes are not required in applications that do not use docking at all, or don't use persistent layouts. But when used, the LCL has to know about some of them.
Minimal impact on the LCL could consist of added global variables, or Application properties, describing the DefaultFloatHostClass and the DockMaster instance (of type TComponent).
A more comfortable implementation would add a TDockMaster or TDesktopLayout base class to the already defined TDockManager class, with methods to stream (and manage?) desktop layouts. The default implementation would do nothing, and an application can install an instance of any derived class. More details will be found out in the practical integration of an DockMaster into the IDE.
Please let me know what you think about my ideas, and what complications I may have missed.
A stable and widgetset-independent solution requires a dedicated "dock grip" component, integrated into the forms or controls. That component can react on mouse moves with the left button down, by starting the dragging of the associated control; this can be the control's Parent (form, toolbar), the page of a notebook, or the control in a dock-zone.
My first approach used a red pin icon in the client area of a form, but this turned out to be very sensitive to changes in the layout (Z-order...) at runtime. It also eats up some space in an otherwise tabular layout, where it should occupy a cell or entire row or column of its own. E.g. a (editor) notebook has no room left for an additional control inside its tabs or editor page.
The next (current) approach uses dockheaders, as already implemented in a docktree or dockbook, to start dragging of the associated client control or notebook page. For that purpose all floating controls (forms...) have to be wrapped into a FloatHostSite, whose DockManager already implements all the functionality of the dockheader. When the dockheader is styled like the grip of a toolbar, this approach fits nicely into any GUI. The DockManager also allows to dock further controls into the same site, eliminating the need for additional ConJoinDockHost sites etc.
Another approach could use dedicated dock-grip controls, as bars with the appearance of commonly used grips (in toolbars...). These tiny bars could be added to a side of any form, just like elastic sites are added, leaving the form layout intact except for a minor reduction of the client width or height. But these controls should be hidden whenever the form is docked, since then the dockheader of the docksite would play the role of the dock-grip. Furthermore such controls would not allow to dock forms together, so that I won't consider this approach right now.
There remain still at least two major issues with wrapping dockable controls into a floating site, the automatic creation of the appropriate FloatHost sites, and the handling of these sites in the store/reload logic of a layout. I already tested the used of the FloatingDockSiteClass property, and found it working in the LCL even with forms, that otherwise float for themselves; perhaps somebody anticipated my idea already?
A minor issue with such floating sites is the visibility of the docked control, when it was just created in invisible state, to reduce flicker. The solution is simple, since it won't do any harm to make the docked client visible in TControl.DoFloatMsg.
Another issue is the exact FloatHost class, that also is used in the Floating property (GetFloating). The meaning or implementation of [Get]Floating had to be changed somehow, when multiple clients can be docked together in the same FloatHost, or in a different FloatHost class. Eventually a dedicated DefaultFloatHostClass can be added to the LCL, that is used with every dockable control (DragKind=dkDock) that has no dedicated FloatingDockSiteClass(=nil).
The currently last issue may deserve more changes to the LCL, related to storing and reloading layouts. Currently persistent layouts are implemented in the DockMaster, which owns all temporary docksites like floating sites, dockbooks and elastic panels. That's why the use of a dedicated DefaultFloatHostClass should be implemented, that is owned by the DockMaster, instead of by Application. At least it should be possible to change the Owner of the floating sites when these are created, but this only would defer the problem, not solve it.
The current implementation of the EasyDockTree and DockMaster try to reduce the dependencies on other (non-LCL) classes, but this is feasable only to some degree. The DockManager with notebook docking capabilities already requires an according notebook component. The DockMaster introduces further classes for floating dock sites and elastic panels, which support the layout streaming, and introduces a layout storage format. All these classes are not required in applications that do not use docking at all, or don't use persistent layouts. But when used, the LCL has to know about some of them.
Minimal impact on the LCL could consist of added global variables, or Application properties, describing the DefaultFloatHostClass and the DockMaster instance (of type TComponent).
A more comfortable implementation would add a TDockMaster or TDesktopLayout base class to the already defined TDockManager class, with methods to stream (and manage?) desktop layouts. The default implementation would do nothing, and an application can install an instance of any derived class. More details will be found out in the practical integration of an DockMaster into the IDE.
Please let me know what you think about my ideas, and what complications I may have missed.
Monday, February 8, 2010
More Flexible Docking
The Delphi docking model happens to implement (by accident?) a feature, which I'd call top-level docking. When no DropOn control is found by the stupid DragManager/TControl methods, the dropped control is docked into the root zone of the docktree. This can happen on e.g. an drop onto an zone header, for which only the DockManager can determine the associated control, or on an drop outside the target DockSite, but within its InfluenceRect.
I've already tried to implement docking relative to dockzones, instead of relative to already docked controls, but didn't follow this approach further. The idea was to take into account the distance of the drop position, relative to the center of a dockzone. From this approach only the notebook-docking has survived, forced on an drop near the center of an already docked control. A similar override could be made for positions close to the boundary of a docked control, but I couldn't find a simple implementation with obvious DockRect feedback.
Now the idea comes up again, with drop positions near the boundaries of an entire DockSite. The existing code only allows for extending the root (or any other) zone only in its established horizontal/vertical orientation (isogonal), and this behaviour now should be extended to orthogonal docking at least for the root zone.
Let's take the case of DropOnControl=Nil to force an drop into the root zone. This condition must be checked at least twice, once in GetDockEdge and later in InsertControl. In both methods a search for the closest already docked control must be skipped, when the drop position is out of (or close to) the boundaries of the DockSite. In InsertControl another check may be required, to distinguish an drop into an empty DockSite from an drop into the root zone of an already populated DockSite. Everything were much easier to implement if the dragged control would not try to determine the DropOnControl and DockRect itself, without any knowledge about the DockManager's dockzones :-(
I've already tried to implement docking relative to dockzones, instead of relative to already docked controls, but didn't follow this approach further. The idea was to take into account the distance of the drop position, relative to the center of a dockzone. From this approach only the notebook-docking has survived, forced on an drop near the center of an already docked control. A similar override could be made for positions close to the boundary of a docked control, but I couldn't find a simple implementation with obvious DockRect feedback.
Now the idea comes up again, with drop positions near the boundaries of an entire DockSite. The existing code only allows for extending the root (or any other) zone only in its established horizontal/vertical orientation (isogonal), and this behaviour now should be extended to orthogonal docking at least for the root zone.
Let's take the case of DropOnControl=Nil to force an drop into the root zone. This condition must be checked at least twice, once in GetDockEdge and later in InsertControl. In both methods a search for the closest already docked control must be skipped, when the drop position is out of (or close to) the boundaries of the DockSite. In InsertControl another check may be required, to distinguish an drop into an empty DockSite from an drop into the root zone of an already populated DockSite. Everything were much easier to implement if the dragged control would not try to determine the DropOnControl and DockRect itself, without any knowledge about the DockManager's dockzones :-(
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.
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.
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.
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.
Subscribe to:
Posts (Atom)