Crossword Editor Panel Components
Status: Deprecated | October 2023 Reviewer: @federico
Summary
The crossword editor has migrated to over to use libpanel. As a
result, we have a large collection of PanelWidgets
planned to go in
the frames around the outside of the dock, as well as some central
widgets. This document describes what they are and how they
interoperate.
Scope
This is focused on the interface for Standard, Cryptic, and Barred Crosswords. We’ll use a similar approach but have a different set of widgets for the other crossword types.
Proposal
As of 0.3.11, we have a new user interface for EditWindow
.
Mockup of EditWindow
Visually, the window consists of a grid (1) in the center
displaying the current puzzle. There are PanelFrames
on the left
(2) and bottom (3) of the window. Each frame contains a number
of PanelWidgets
, depending on the mode and puzzle type. On the top,
there is a GtkStackSwitcher
(4) to switch between the different
stages of editing a puzzle, represented by
PuzzleStackStage
. Currently, four stages are planned and all except
STYLE have some of its implementation written.
The first three stages (GRID, EDIT, and STYLE) all have a similar looking grid in the middle. There’s a slight change in background color to give a quick visual hint to the current stage. The final stage (METADATA) looks very different and doesn’t use either of the side frames.
The left frame has tools for mutating the puzzle — primarily editors and navigation controls. It currently has a primary and secondary area, and lets the user switch between different tools.
On the other hand, the bottom grid is purely informational. It
contains information about the current state, such as the definition
of the current word, or details about the grid. There may be many of
these during the CLUE
stage, as they can be extremely useful for
writing clues.
Editor components
Widget |
Stage |
Location |
Description |
---|---|---|---|
EditGrid |
GRID |
Center |
Displays the current puzzle grid and edit answers. Changes cursor |
EditGrid |
EDIT |
Center |
Displays the current puzzle grid. Changes cursor |
EditGrid |
STYLE |
Center |
Displays the current puzzle grid. Changes cursor |
EditWordList |
GRID |
Left |
Shows potential words that fit in the grid |
EditAutofill |
GRID |
Left |
Autofill the grid depending on |
EditClueList |
CLUE |
Left |
Selects the list of answers to set a clue |
EditSymmetry |
GRID |
Left |
Sets the symmetry of bars and blocks. Changes symmetry |
EditBars |
GRID & IpuzBarred |
Left |
Sets barred lines between cells |
EditCellType |
GRID |
Left |
Selects a cell type and shape for the current cell |
EditClueDetails |
CLUE |
Left |
Edits the details of the currently selected clue. changes clue substring |
EditStyleList |
STYLE |
Left |
Shows the list of current named styles |
EditStyleDetails |
STYLE |
Left |
Edits the style of the current cell |
EditGridInfo |
GRID |
Bottom |
Shows statistics about the current grid, such as distribution of clue lengths and letters. |
EditDict |
CLUE |
Bottom |
Gives a definition of the selected clue |
EditDict |
GRID |
Bottom |
Gives a definition of the clue substring |
EditAnagram |
CLUE |
Bottom |
Shows anagrams of the selected clue or subclue |
EditThesaurus |
CLUE |
Bottom |
Gives a synonym of the selected clue or subclue |
EditHint |
CLUE |
Bottom |
Shows potential clue fragments for selected clue or subclue |
NOTE: In the table above, Widgets written as bold/italic are just proposed and not written yet (as of Sept. 2023)
Implementation Notes
With this shift, our old approach of using the PuzzleStack
to
contain the full state is no longer sufficient. We need other
transient state — such as the cursor position — in order for
everything to update correctly. Different PanelWidgets
can affect
the behaviors of others, as can the central dock widget. This change
is correct: we don’t expect the undo command to undo each movement
around the grid or change tools.
As an example of this transient state, EditSymmetry
modifies the
current selected symmetry of the puzzle, and other widgets can read
from it for making changes. Similarly, EditClueDetails
produces a
substring of a clue that can be queried. The main grid’s cursor will
also affect things.
To handle this at scale and keep things straight forward in the code, we’ve taken the following approach:
All state for a given
EditWindow
is stored in theEditState
struct. This is the cannonical state of the application and crossword. It has a few constraints that are checked with theVALIDATE_EDIT_STATE()
macro.Each
PanelWidget
does three things:It can update its display when given state from
EditState
It will emit events when aspect of the
EditState
need changing.It can optionally push transient state on the puzzle-stack. This is ppredmoninantly focus data and cursor postition.
In addition, each widget can be told to commit any unsaved changes due to external events. As an example, widgets are committed before we save the puzzle to disk.
The only object that modifies
EditState
is theEditWindow
. This happens exclusively in callbacks for edit-window-controls.h. EachPanelWidget
is relatively simple as a result.The only widget that deals with the undo/redo stack is
EditWindow
. This also happens in edit-window-controls.h. The exception to this is thatPanelWidgets
are asked to save/restore transient state but the window.Each panel widget is defined in the
EditWindow.ui
, but not put in a widget tree. TheEditWindow
has an additional ref count for each widget beyond what the widget tree contains. This is because we have to add/remove the widgets to the panel when the Editor switches modes.PanelFrame
does not handle widgets being hidden and shown gracefully, so we have to create it from scratch on each stage change. See libpanel issue #31Finally, during the course of writing this doc, it’s become clear that
XwordState
is named incorrectly. We will rename it to beGridState
in the future.
This approach is an example of a pattern we’ve adopted in writing Crosswords: Concentrate the complexity. Where possible, we put all the complexity in one area. That lets us test that particular area, and limit side-effects across the code base.
Open Questions
Some open questions about this approach:
[X] There is no common base class or interface for the
PanelWidgets
. I started writing anEditPanelWidget
for that role, and realized that other than sharing the PuzzleStack, it didn’t have a lot of value. Different widgets need different things, and I didn’t want to add a union of property types – mostly empty. However, the result of this is a lot of duplicate code as each widget has to add its own property handling for the stack, at a minimum.This base type doesn’t have a ton of value, but we can centralize an
::update()
function as well as thepuzzle-stack
andgrid
properties. It also gives us flexibility in case we need it for future libpanel interactions.UPDATE: With the shared
puzzle-stack
not being used, we don’t needEditPanelWidget
. This can go away for good.
[ ] One of the main strengths of libpanel and gnome-builder is that you can reorder / resize the frames and panel widgets within the application. This breaks pretty badly with the stages, and is currently fully disabled. That means that we’re not taking advantage of libpanel.
UPDATE: After talking with Christian, it’s not clear libpanel will be able to provide us what we need. It’s a small enough amount of work that its worth looking to see if we can modify it, but it may be more work than its worth.
AdwSplitView
may be a better approach.