Verified Commit a905a8f4 authored by Valentin Ionita's avatar Valentin Ionita 🐳
Browse files

updated TODO, added RFC

parent 1c16be11
# GDL refactoring & new dialog system
## Intro
*[link to GSoC page](https://summerofcode.withgoogle.com/projects/#6404262747176960)*
This Google Summer of Code project has had the purpose of removing one major
Inkscape dependency, **Gnome Docking Library (GDL)**, in favor of a custom-built,
extensible dialog system, based visually on GIMP's own multi-panes and a number
of [demos from Tav](https://gitlab.com/Tavmjong/gtk_sandbox/-/tree/master/gtkmm_paned).
The value of the project consisted in modernization, standardization and
synchronization. While for completely achieving the latter two there is more
work to be done, based on future code quality project decisions, each of them
has been addressed when working on the current code.
## Goals completion
To remind the reader about the specific goals of the project, we will list them
below, covering their completion:
- removing GDL as a dependency: completed
- creating a **Multipane** widget that manages all types of dialogs: the
**DialogMultipaned** widget is focused on size allocation of its children while
the **DialogContainer** widget manages, along with a few others, the dialogs
themselves. This helps right now with the possibility of having complex dialog
arrangements, the segregation of concerns and the potential of resizing more
than dialogs in the future.
- saving dialog state: completed. Dialog state is saved and loaded for the last
active window and it recovers complex arrangements of dialogs and them being
docked or floating at shutdown time.
- update dialog state though a new system pushing forward Gio::Actions instead
of Gtk::Actions: not completed. This is a feature dependent on Inkscape's way
of storing data about dialogs (like icons, names, shortcuts) and, while using
Verbs, just like in the old system, is not the best solution, it tries to keep
the need of verbs into one place, specifically DialogContainer, in order to make
for a smooth future transition. More than this, Inkscape is currently having
similar transitions (standardizations) which might create a blueprint for the
mentioned potential project.
## Features
Following is a list of features implemented by the current project:
- DialogBase superclass for all dialogs
- *DialogContainer* dialog factory
- dockable dialogs inside Inkscape windows
- floating dialogs inside DialogWindows
- "multiplexing" dialogs by using *DialogNotebooks* to switch between dialogs sharing the same concrete screen space
- "parallelization" of dialogs vertically or horizontally by using nested *DialogMultipaned* widgets
- floating dialogs track the state of the last active Inkscape window
- dragging and dropping dialogs into regions called *DropZones* owned by *DialogMultipaned* widgets
- making dialogs floating by dragging tabs onto the canvas or outside the window
- automatic resizing (i.e. size allocation) of *DialogMultipaned* children
- manual resizing of *DialogMultipaned* children by dragging handles marked with a "three dots" icon
- completely hiding dialogs by resizing them to 0 on an axis
- *DialogNotebook* tabs with shortcuts on hover
- automatic removal of dialog names/labels in tabs if needed
- possibility of hiding dialog tab labels completely
- dialog tabs right click menu with options for closing the dialog, moving to a floating window or closing the notebook
- dialog tabs middle click (triple click on touchpads) closing a dialog
- floating dialogs resize to their natural size initially
- dialog configurations are saved when closing Inkscape, including any floating dialog configurations, and loaded when a new window is opened in the next session
- preventing multiple dialogs of the same type in the same *DialogContainer*
- scrolling inside *DialogNotebook* if the dialogs don't have enough space
## Implementation
A general description of how dialogs work in this project can be found in the
source code at `src/ui/dialog/README.md`. We will not reproduce the same content
here, but will describe important aspects of the implementation or potentially
not easy to grasp details. Each section below focuses on a single aspect of the
code.
### Basic configuration of the new dialog system
The following diagram shows how an InkscapeWindow looks like, including internal
widgets of custom Dialog* widgets.
```yaml
- DialogContainer():
- DialogMultipaned():
- MyDropZone()
- CanvasGrid()
- MyHandle()
- DialogMultipaned():
- MyDropZone()
- DialogNotebook():
- DialogBase()
- ...
- MyDropZone()
- MyDropZone()
```
### DialogBase::DialogBase()
A dialog internally holds a name, a verb number and a preferences path. The name
is trimmed of unerscores ("_") and ellipsis ("..."). More than this, each dialog
has a pointer to the concrete Inkscape application, used in tracking the
properties of the active Inkscape Window that it needs (selection, document, etc).
### DialogBase::update()
An essential method used for dialog state management. The method gets triggered
on each relevant change (document modified, selection changed, selection
modified) for all floating dialogs and all dialogs docked in the active window.
The recommended use for a dialog implementation is keeping a pointer to the
needed properties and, if a change is detected inside the update method, helper
functions are called with the new pointers, properties, etc. This pattern helps
with removing the need of signal connections to properties like
`desktop.selectionChanged()` or similar (which would also need to track the
object instance they attached the signal to, so they aren't any more efficient).
Signals are hard to debug and sometimes create problems on widget destruction,
so it helps a lot replacing them as much as possible.
### DialogBase::blink() and DialogBase::blink_off()
Utility methods that highlight the notebook a dialog is (and focus it on the
dialog), in order to give visual feedback to the user that a dialog of a
requested kind already exists.
### MyDropZone() and DialogMultipaned::set_dropzone_sizes()
Dropzones are oriented eventboxes placed at the margins of DialogMultipaned
widgets where you can drop dialogs in order to create a new DialogNotebook
holding the dropped dialog. They have default transversal sizes which can be set
from the pane if cuatom sizes are wanted (this is used in floating dialogs which
are smaller and don't need as proeminent dropzones). They highlight when
dragging a dialog tab over them.
### MyHandle()
DialogMultipaned handles are oriented eventboxes that are used to resize paned
children. Its own functionality is minimal (cursor icon change on hover), but
one thing to be mentioned is that we remove its icon under a specific size in
order to completely hide a handle along with its DialogMultipaned when resizing
to 0. Without doing this manually, we would have to stop at the size of the
icon itself.
### DialogMultipaned custom container
There are a number of methods overriden in order to build this custom
`Gtk::Container`, but there are two main mechanisms we have to mention. First if
the children size allocation algorithm in `on_size_allocate`.
To begin, it will only allocate custom sizes on the axis of its own orientation:
if it is a horizontal container, all the vertical sizes will be identical, the
full size of the container and the reverse. First, it will check if the
allocation was triggered by moving a handle or not. The former mechanism is
described in the next section, while the latter is described here. The main goal
of the algorithm is to assign enough pixels to satisfy each child's minimum
size, then natural size and then distribute the remainder between all the
expandable children (visible and which desire larger sizes). After computing the
new sizes, we next check if we actually need to resize children (if the next
total size is equal to the total current allocated size and all the size
constraints are followed, then we don't need to resize anything). Finally, we
use the computed sizes to compute a new allocation (sizes and positions on both
axis) for each child.
### DialogMultipaned::on_drag_begin and DialogMultipaned::on_drag_update
The other essential mechanism is based on dragging a handle. In the first
function we identify if the element at the position where the drag event starts
is a handle. If so, we save its index in the children array and its allocation,
along with the allocation of its neighbours, in order to compute the new
allocations. This is done in the second method. We compute the new allocations
based on the drag event offsets and the minimum sizes of the neighbours. A bias
of 1px is removed in order to cover for an erratic behavior when dragging fast
outside the window (hack). If the `hide_multipaned` flag is set, the allocation
event is not triggered, but if unset, `queue_allocate()` is called, which
triggers the first case of `on_size_allocate()`.
### DialogMultipaned _empty_widget
When a new instance is created, a placeholder widget is added. It is only inside
the DialogMultipaned, when there is no other external child in the container
(dropzones are always inside the container), and it's removed as soon as we add
one. In a previous step of the project, it also had an UI role, but for now it
only gets the unallocated space of an empty DialogMultipaned in the short
periods (a few frame updates) when an empty container exists.
### DialogContainer::DialogContainer()
A DialogContainer is a box that, first, holds an internal horizontal
DialogMultipaned child called as "columns" (because it's visually a single row
with multiple children on the horizontal axis) and second, holds a reference to
all the dialogs which are in any of its children. The first part helps with
complex arrangements and state saving, while the second is essential to
keeping a maximum of a single dialog of each type in a single window.
For the main column (and any secondary columns), it connects to the signals
related to prepending and appending children in order to prepare each dialog
and put into a new DialogNotebook on creation.
### DialogContainer::create_column()
Returns tracked vertical columns (i.e. DialogMultipaned instances) to add to the
main column. This design choice has been taken in order to be able to both
prepend and append new columns.
### DialogContainer::dialog_factory()
Is a factory method that returns a new dialog based on the verb code. Looking at
a specific dialog, you can find its verb number in its constructor. This design
choice has been taken, because at the moment of implementation there was a lot
of uncertainty between verbs/actions, names of the dialogs and duplication of
verbs (you can see that a multitude of calls refer to the preferences dialog).
This could be changed in the future along with the switch to actions.
### DialogContainer::new_dialog() and DialogContainer::new_floating_dialog()
Are the methods that create new dialogs in the current container or a floating
window, following pretty much the same steps, which are: identify the Verb
associated to the code, call the factory for the dialog itself, create its
notebook tab using a helper, find a notebook to which you can add this dialog to
(or create it, or use it from the call parameters) and add the dialog. For
floating dialogs, we make use of the DialogNotebook `pop_tab_callback()` to
reuse code.
### DialogContainer state saving and loading
The methods themselves are quite long and explained in the comments from the
source. We want to add that loading the state from a keyfile is made through two
calls in order to give the extensibility to load a different state and have
something like "profiles" where you work in different ways for different
projects. This can be subject to open discussion and a micro-project with UX focus.
### DialogContainer::on_unmap()
The method is mainly used to disconnect signals, but it also saves the state if
it's called from an Inkscape window (not a DialogWindow).
### DialogContainer::prepare_drop(), DialogContainer::prepend_drop() and DialogContainer::append_drop()
These are the callbacks that respond to DialogMultipaned prepend and append
signals and their scope is to extract the page from an old notebook, to append
it to a newly created one and to add the DialogNotebook as a child to the origin
DialogMultipaned, either prepended or appended. When we add to a horizontal
column (i.e. the main paned widget in a container), we also create a vertical
column to which we add first.
### DialogContainer::column_empty()
It is an essential method that does two things: deletes any secondary empty
column from the main columns, and closes a DialogWindow if empty.
### DialogNotebook::DialogNotebook()
More than setting up the needed basics for this widget, the constructor creates
a `Gtk::Menu` that will appear when you right-click a dialog tab. A DialogNotebook
has methods for adding, closing, moving and popping (making it float) a page,
which plainly means a dialog. However, we kept the nomenclature from the Gtk
notebook for consistency.
### DialogNotebook::on_drag_end()
Signal handler to pop a dragged tab into its own DialogWindow. This is not a
perfect method to drag and float behavior, but it has currently the best
compatibility between platforms. We try to pop a window whenever we drag a tab
outside any Inkscape window, but works inside the Inkscape window in X11.
We get the page from the drag origin noteboook and create a new DialogWindow
from it. This is not done on the current notebook in order to make sure the drag
works properly and. If the notebook remains empty, we close it.
### DialogNotebook::on_size_allocate_scroll()
This method has two purposes which are grouped together because they respond
to the same events (size allocate signal and adding/removing dialogs in the
notebook). The first one is to remove the internal state of the scrolbars under
a certain size in order to be able to snap a notebook to size 0 (and, as a
result, to hide a column). The second one is to hide dialog tab labels/names
if there is not enough horizontal space in the DialogNotebook. We don't want to
dictate the size of the notebook by the sum of the lengths of the labels, so
we only show the icons, if necessary. You can hide the labels completely by
using a global setting in the preferences or by using an independent notebook
radio setting in the notebook right-click menu.
### DialogWindow::DialogWindow()
It is simply a `Gtk::ApplicationWindow()` with a hint of type
`WINDOW_TYPE_HINT_DIALOG` that holds a DialogContainer and has the same dialog
arrangement capabilities. Its sole purpose is creating a floating dialog (or
configuration of dialogs) that tracks the active Inkscape window.
### DialogWindow::update_dialogs()
This is the method that keeps dialogs in sync, by calling the analogous method
from its inner container. However, one more thing is that it tracks if there is
only a single dialog or more and the name of the document it's connected to
and puts all that information in the title of the window.
### DialogWindow::update_window_size_to_fit_children()
This is an external-use method used to resize the window in order to fit the
size requirements of all inner dialogs (dialogs attached to its container). It
is currently only used when loading the dialog state, but a careful analysis
might show other good callsites.
......@@ -10,6 +10,14 @@ However, this might be only on my local machine, not even on the [Gitlab branch]
4. DialogNotebook should scroll content only (sticky header)
5. Adding dialogs below the canvas
6. Dragging notebook to reposition
7. Dockable canvas.
8. DialogMultipaned locked sizes
## Potential microprojects
1. Migrating from verbs to actions in the DialogContainer
2. Removing the empty_widget from DialogMultipaned if proven just a code creep with no future use (now that there is no moment when an empty column is visible). Here we could also destroy an empty DialogMultipaned that is the child of another (moved from DialogContainer::column_empty())
3. Adding dialog state profiles and loading them on the fly. (UI & functionality)
4. Saving dialog positions and sizes along with configurations.
## Known issues
1. The shortcuts to open a new dialog don't work when a floating window is focused
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment