From 6adaff68bd53a01ac6ea2eeb5bdddb27db7c2344 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 18:59:39 +0000 Subject: [PATCH] Designer: deal the section settings out into four tabs The editor pane was one column of fifteen groups -- what a section says, how its type is put together, where it sits, five text styles, eight panels and a table -- in a pane a third of a window wide. Folding the groups away made the column shorter without making it any less of a column: somebody working on the words of a list still scrolled past the geometry and the styles to reach the entry table. So the settings are dealt out into Content, Layout, Style and Background, which are the jobs somebody sits down to do rather than an arbitrary quartering: fill the section in, place it, ink it, put something behind it. A divider's piece stacks go where a list's entries do -- they are what a divider is made of, and a divider has no words to type. Nothing about the document changes; a tab is where a control is drawn and nothing else. The type picker, the label, the visible box and the advanced switch stay above the tab strip. They are not settings of any one job -- the type decides what every tab holds, and the advanced switch reaches rows on three of the four pages -- so leaving the tab you are working in to flip one of them would lose your place to change something that governs all of them. Each tab scrolls on its own, which is why the designer now adds the editor straight to the splitter instead of wrapping it in a QScrollArea: the scrolling moved inside, one area per page, so the header and the strip cannot be scrolled off the top and each page keeps the place it was left at. A tab with nothing on it goes away, since a Spacer has no words, no styles and no panels and three empty panes invite the reader to look for settings that are not there. That is asked of the groups on each page rather than of the section type, so it cannot disagree with the visibility pass that has just run. The tab the reader chose is remembered rather than forced: one going away under them takes the selection with it, and they are put back on it once the next section has one again. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019UvPd5wu54qwoNAHqN9U8o --- ARCHITECTURE.md | 64 ++++++++++--- data/locale/en-US.ini | 6 +- src/ui/DesignerDialog.cpp | 26 +++--- src/ui/DesignerDialog.hpp | 2 - src/ui/SectionEditor.cpp | 190 ++++++++++++++++++++++++++++++-------- src/ui/SectionEditor.hpp | 78 ++++++++++++++-- 6 files changed, 291 insertions(+), 75 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3fb2d0e..a04a8ed 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1181,18 +1181,53 @@ taking a type apart is a property of the type table rather than of this window, mapping that has to be exactly reversible is worth a test that needs no window on screen. Nothing about persistence changes: the document still carries all twenty ids. -**The rows are gathered into named groups that fold away** — *Content Properties*, the type's own -settings, *Layout Properties* — with the middle one titled after the type in it, since -"Bridge settings" and "Divider settings" are never on screen at once. A `CollapsibleGroup` rather -than a checkable `QGroupBox`: a checkbox on a group reads as switching the group *off*, which is -what the checkable groups already in this editor mean. A group whose every row is hidden goes away -with them, which is asked of the layout rather than of the widgets in it — a child of a window that -has not been shown yet reads as hidden whether or not anything hid it. +**The settings are dealt out into four tabs rather than stacked in one column** — *Content*, +*Layout*, *Style*, *Background* (`EditorTab`). Fifteen groups one under the next was everything +about a section at once in a pane a third of a window wide; folding the groups away made the column +shorter without making it any less of a column, and somebody working on the words still scrolled +past the geometry to reach them. The four are the jobs somebody sits down to do — fill the section +in, place it, ink it, put something behind it — and each is a whole answer on its own. The document +is untouched by this: a tab is where a control is drawn and nothing else. + +*Content* holds what the section says, and the tables it says it through: the entry table for a +list, the divider's three piece stacks for a divider (a divider has no words, so those stacks *are* +its content, and its Content tab holds them alone). *Layout* holds the type's own settings and the +placement rows. *Style* holds the up-to-five `StyleEditor`s. *Background* holds the eight panel +slots. + +**The type picker, the label, the visible box and the advanced switch stay above the tab strip.** +They are not settings of any one job: the type decides what every tab holds, the name is how the +section is found again in the list, and the advanced switch reaches rows on three of the four +pages. A reader who had to leave the tab they were working in to flip one of them would lose their +place to change something that governs all of them. + +**Each tab scrolls on its own**, so the header and the strip cannot be scrolled off the top and +each page keeps the place it was left at — going back to *Style* should be going back to where you +were in it. That is why the designer adds the editor straight to the splitter rather than wrapping +it in a `QScrollArea` as it used to: the scrolling moved inside, one area per page. + +**A tab with nothing on it goes away.** A Spacer has no words, no styles and no panels, so three of +the four would otherwise be empty panes inviting the reader to look for settings that are not +there. Asked of the groups on each page rather than of the section type — `applyTypeVisibility` has +just settled what applies, and counting what it left on screen cannot disagree with it the way a +second list of conditions could. It asks `isHidden` rather than `isVisible`, because the page of a +tab nobody is looking at is itself hidden and everything on it would otherwise answer no. The tab +the reader chose is remembered rather than forced: a tab going away under them takes the selection +with it, and `desiredTab` is what brings them back to it once the next section has one again, while +`restoringTab` keeps that shuffle from being read as a choice of theirs. + +**Within a tab the rows are still gathered into named groups that fold away** — *Content +Properties*, the type's own settings, *Placement on the Canvas* — with the middle one titled after +the type in it, since "Bridge settings" and "Divider settings" are never on screen at once. A +`CollapsibleGroup` rather than a checkable `QGroupBox`: a checkbox on a group reads as switching the +group *off*, which is what the checkable groups already in this editor mean. A group whose every row +is hidden goes away with them, which is asked of the layout rather than of the widgets in it — a +child of a window that has not been shown yet reads as hidden whether or not anything hid it. **The style groups fold away too, and the two that switch off carry both controls in one header.** A `StyleEditor` is the tallest thing in the pane — a font, a size, a fill with its stops, an -outline, a shadow, an alignment — and a bridged row with subtitles stacks five of them between the -placement rows and the entry table, which is the scroll the folding groups were added to end. So +outline, a shadow, an alignment — and a bridged row with subtitles stacks five of them one under the +next, which is the length that made the editor feel endless before they had a tab of their own. So `CollapsibleGroup` grew the checkbox it was originally written to avoid: it sits *beside* the title rather than replacing the disclosure triangle, and the two readings stay apart — the checkbox says whether the style applies, the triangle says whether it is on screen, and neither moves the @@ -1215,11 +1250,12 @@ bridged row's *subtitles* switch adds and takes away, so the editor remembers th was built with (`tableType`, `tableRowSubtitles`) and reads through that rather than through the switch: the same cell means a different field on each side of the rebuild, and reading a two-column table as a four-column one put every row's right-hand name into the left-hand subtitle the moment -subtitles were switched on. A trailing spacer takes whatever height -is left over: a `QVBoxLayout` with nothing to give its slack to shares it out between the items -it has, which spread a short type's handful of rows down the pane with gaps between them. The -entry table is the one thing worth growing, so it takes the slack instead whenever the selected -type has one, and asks for enough height to read a run of entries at a glance. +subtitles were switched on. Each tab's page ends in a trailing spacer that takes whatever height is +left over: a `QVBoxLayout` with nothing to give its slack to shares it out between the items it +has, which spread a short type's handful of rows down the page with gaps between them. Whichever +table the *Content* page is showing is the one thing on it worth growing, so it takes that page's +slack instead whenever the selected type has one, and asks for enough height to read a run of +entries at a glance. **A sticky block's children are branches off it, and fold away.** The list stays a `QListWidget`: a roll is a run of sections with one shallow exception in it, and a tree would trade the diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 7bb0462..e5f36c5 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -85,9 +85,13 @@ Designer.ListContent.Pairs="A line and a smaller one under it" Designer.ListContent.Logos="Images" Designer.ShowAdvanced="Show every setting" Designer.ShowAdvanced.Tip="Reveals the settings that are rarely needed — where the section sits on the canvas, the fine spacing of a divider, the ordering of a pair. Nothing is switched off while they are hidden." +Designer.Tab.Content="Content" +Designer.Tab.Layout="Layout" +Designer.Tab.Style="Style" +Designer.Tab.Background="Background" Designer.Group.Content="Content Properties" Designer.Group.TypeSettings="%1 Settings" -Designer.Group.Placement="Layout Properties" +Designer.Group.Placement="Placement on the Canvas" Designer.TypeHelp.title="The largest heading. One line, on its own." Designer.TypeHelp.title_with_subtitle="A large heading with a second, smaller line under it." Designer.TypeHelp.title_with_logo="A large heading with an image beside it." diff --git a/src/ui/DesignerDialog.cpp b/src/ui/DesignerDialog.cpp index dbab7bc..2b3a6bc 100644 --- a/src/ui/DesignerDialog.cpp +++ b/src/ui/DesignerDialog.cpp @@ -39,7 +39,6 @@ with this program. If not, see #include #include #include -#include #include #include #include @@ -464,12 +463,15 @@ DesignerDialog::DesignerDialog(obs_source_t *source, QWidget *parent) : QDialog( splitter->addWidget(listPane); - /* --- middle: the editor for whichever section is selected --- */ - editorScroll = new QScrollArea(splitter); - editorScroll->setWidgetResizable(true); - editor = new SectionEditor(editorScroll); - editorScroll->setWidget(editor); - splitter->addWidget(editorScroll); + /* + * --- middle: the editor for whichever section is selected --- + * + * Added straight to the splitter rather than wrapped in a scroll area: the editor is tabbed + * and scrolls each of its pages itself, which is what keeps its type picker and its tab strip + * on screen while a long page is scrolled. See SectionEditor::addTab. + */ + editor = new SectionEditor(splitter); + splitter->addWidget(editor); /* --- right: live preview of the whole strip --- */ previewPane = new QWidget(splitter); @@ -986,7 +988,7 @@ void DesignerDialog::refreshSectionList(int selectRow) if (const Section *section = sectionAt(currentPath)) editor->setSection(*section); - editorScroll->setEnabled(row >= 0); + editor->setEnabled(row >= 0); } DesignerDialog::DocumentSnapshot DesignerDialog::snapshot() const @@ -1128,8 +1130,8 @@ void DesignerDialog::setPaneFolded(QWidget *pane, QToolButton *button, int *reme pane->setMaximumWidth(QWIDGETSIZE_MAX); const int width = *rememberedWidth > 0 ? *rememberedWidth : kDefaultPaneSizes.value(index); - const int editor = splitter->indexOf(editorScroll); - if (index == editor || editor < 0 || sizes.size() != splitter->count()) { + const int editorIndex = splitter->indexOf(editor); + if (index == editorIndex || editorIndex < 0 || sizes.size() != splitter->count()) { splitter->setSizes(kDefaultPaneSizes); return; } @@ -1139,7 +1141,7 @@ void DesignerDialog::setPaneFolded(QWidget *pane, QToolButton *button, int *reme * where the user last put it. The editor is the pane with room to spare -- and never let * below nothing, which the splitter would only hand back at the next resize anyway. */ - sizes[editor] = std::max(0, sizes.value(editor) - (width - sizes.value(index))); + sizes[editorIndex] = std::max(0, sizes.value(editorIndex) - (width - sizes.value(index))); sizes[index] = width; splitter->setSizes(sizes); } @@ -1161,7 +1163,7 @@ void DesignerDialog::onSelectionChanged() const int row = sectionList->currentRow(); currentRow = row; currentPath = row >= 0 && row < rowPaths.size() ? rowPaths.at(row) : SectionPath{}; - editorScroll->setEnabled(row >= 0); + editor->setEnabled(row >= 0); preview->setHighlightedSection(currentPath.isValid() ? highlightFor(currentPath) : -1); if (const Section *section = sectionAt(currentPath)) diff --git a/src/ui/DesignerDialog.hpp b/src/ui/DesignerDialog.hpp index 483336d..39bd7f4 100644 --- a/src/ui/DesignerDialog.hpp +++ b/src/ui/DesignerDialog.hpp @@ -35,7 +35,6 @@ class QDialogButtonBox; class QFileSystemWatcher; class QLabel; class QPushButton; -class QScrollArea; class QSplitter; class QTimer; class QToolButton; @@ -377,7 +376,6 @@ class DesignerDialog : public QDialog { int listExpandedWidth = 0; SectionEditor *editor = nullptr; - QScrollArea *editorScroll = nullptr; PreviewWidget *preview = nullptr; QWidget *previewPane = nullptr; QLabel *previewLabel = nullptr; diff --git a/src/ui/SectionEditor.cpp b/src/ui/SectionEditor.cpp index 536016b..fda37ae 100644 --- a/src/ui/SectionEditor.cpp +++ b/src/ui/SectionEditor.cpp @@ -37,6 +37,7 @@ with this program. If not, see #include #include #include +#include #include #include #include @@ -692,10 +693,17 @@ void StyleEditor::onFillChanged() SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) { auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(0, 0, 0, 0); - outerLayout = outer; + /* + * A margin of its own, small but not nothing: the editor now sits straight against the + * splitter rather than inside a scroll area whose frame used to hold it clear of the handle. + */ + outer->setContentsMargins(4, 4, 4, 4); + /* + * The rows above the tab strip, which every tab is read under. See `headerForm`. + */ form = new QFormLayout(); + headerForm = form; outer->addLayout(form); const auto notify = [this] { @@ -754,15 +762,38 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) addRow(QString(), showAdvanced); /* - * From here the rows go into named groups that fold away. The order is the order the work is - * done in: what the section says, then how this kind of section is put together, then where - * it sits on the canvas -- with the styles and the tables under them in groups of their own. + * From here everything goes into one of the four tabs, in named groups that fold away. See + * EditorTab for why the settings are dealt out rather than stacked. + * + * All four are built up front and in reading order, so a group only has to say which tab it + * belongs to. Which of them are on show follows what is left visible on each page; see + * refreshTabVisibility. + */ + tabs = new QTabWidget(this); + outer->addWidget(tabs, 1); + + addTab(EditorTab::Content, moduleText("Designer.Tab.Content")); + addTab(EditorTab::Layout, moduleText("Designer.Tab.Layout")); + addTab(EditorTab::Style, moduleText("Designer.Tab.Style")); + addTab(EditorTab::Background, moduleText("Designer.Tab.Background")); + + connect(tabs, &QTabWidget::currentChanged, this, [this](int index) { + if (restoringTab || index < 0) + return; + + desiredTab = index; + }); + + /* + * What the section says: its words, and the artwork that stands beside or instead of them. + * The table of entries and the divider's piece stacks join it further down -- a list of two + * hundred credits is this section's content as much as a heading's one line is. */ contentGroup = new CollapsibleGroup(moduleText("Designer.Group.Content"), this); form = new QFormLayout(); contentForm = form; contentGroup->addLayout(form); - outer->addWidget(contentGroup); + tabLayout(EditorTab::Content)->addWidget(contentGroup); textEdit = new QPlainTextEdit(this); textEdit->setMaximumHeight(80); @@ -825,7 +856,7 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) form = new QFormLayout(); typeSettingsForm = form; typeSettingsGroup->addLayout(form); - outer->addWidget(typeSettingsGroup); + tabLayout(EditorTab::Layout)->addWidget(typeSettingsGroup); logoPlacement = new QComboBox(this); logoPlacement->addItem(moduleText("Designer.LogoPlacement.Hug"), static_cast(LogoPlacement::Hug)); @@ -1101,7 +1132,7 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) form = new QFormLayout(); placementForm = form; placementGroup->addLayout(form); - outer->addWidget(placementGroup); + tabLayout(EditorTab::Layout)->addWidget(placementGroup); paddingTop = new QSpinBox(this); paddingTop->setRange(0, 20000); @@ -1136,28 +1167,28 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) addRow(moduleText("Designer.SectionAlign"), sectionAlign); /* - * The style groups fold away like the three above them. + * The styles get a tab to themselves. * * A StyleEditor is the tallest thing in this pane -- a font, a size, a fill with its stops, - * an outline, a shadow, an alignment -- and a bridged row with subtitles puts five of them - * one under the next. Somebody working on the words of a section scrolls past all of it to - * reach the entry table, which is the whole complaint the folding groups were added to - * answer; these were simply left out of that first pass. + * an outline, a shadow, an alignment -- and a bridged row with subtitles puts five of them one + * under the next. Stacked under the settings above they were the length that made the editor + * feel endless; behind a tab they are simply the answer to "what does this look like", which + * is a question somebody asks on purpose rather than one they scroll through. * - * Two of them keep their checkbox, which is now carried in the fold header beside the title - * rather than by a QGroupBox: the checkbox still says whether the style applies, and the - * triangle beside it says only whether it is on screen. See CollapsibleGroup. + * Two of them keep their checkbox, which is carried in the fold header beside the title rather + * than by a QGroupBox: the checkbox still says whether the style applies, and the triangle + * beside it says only whether it is on screen. See CollapsibleGroup. */ styleGroup = new CollapsibleGroup(moduleText("Designer.TextStyle"), this); primaryStyle = new StyleEditor(styleGroup->content()); styleGroup->addWidget(primaryStyle); - outer->addWidget(styleGroup); + tabLayout(EditorTab::Style)->addWidget(styleGroup); secondaryGroup = new CollapsibleGroup(moduleText("Designer.SecondaryStyle"), this); secondaryGroup->setCheckable(true); secondaryStyle = new StyleEditor(secondaryGroup->content()); secondaryGroup->addWidget(secondaryStyle); - outer->addWidget(secondaryGroup); + tabLayout(EditorTab::Style)->addWidget(secondaryGroup); /* * Unchecked, the bridge is drawn in the section's own style, which is what makes a leader @@ -1170,7 +1201,7 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) bridgeStyle = new StyleEditor(bridgeStyleGroup->content()); bridgeStyle->setInkOnly(true); bridgeStyleGroup->addWidget(bridgeStyle); - outer->addWidget(bridgeStyleGroup); + tabLayout(EditorTab::Style)->addWidget(bridgeStyleGroup); /* * The two subtitles of a bridged row. Two style groups rather than one, because the two @@ -1182,17 +1213,18 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) rowSubtitleStyleGroup = new CollapsibleGroup(moduleText("Designer.RowSubtitleStyle"), this); rowSubtitleStyle = new StyleEditor(rowSubtitleStyleGroup->content()); rowSubtitleStyleGroup->addWidget(rowSubtitleStyle); - outer->addWidget(rowSubtitleStyleGroup); + tabLayout(EditorTab::Style)->addWidget(rowSubtitleStyleGroup); rowSecondarySubtitleStyleGroup = new CollapsibleGroup(moduleText("Designer.RowSecondarySubtitleStyle"), this); rowSecondarySubtitleStyle = new StyleEditor(rowSecondarySubtitleStyleGroup->content()); rowSecondarySubtitleStyleGroup->addWidget(rowSecondarySubtitleStyle); - outer->addWidget(rowSecondarySubtitleStyleGroup); + tabLayout(EditorTab::Style)->addWidget(rowSecondarySubtitleStyleGroup); /* * A folding group per background slot, built from the slot table so a slot added to the model - * turns up here with nothing to write. They sit after the styles because a panel is the thing - * behind the words: the reader who has come this far down the pane has already set them. + * turns up here with nothing to write. Eight of them, one for each thing a section can put a + * panel behind, which is why they get a tab rather than sharing one with the styles: a panel is + * a different question from the ink on the words in front of it, and eight groups is a page. */ for (const BackgroundSlot slot : allBackgroundSlots()) { const QString title = moduleText(QStringLiteral("Designer.Background.Slot.%1") @@ -1217,7 +1249,7 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) auto *editor = new BackgroundEditor(group->content()); group->addWidget(editor); - outer->addWidget(group); + tabLayout(EditorTab::Background)->addWidget(group); backgroundGroups.insert(slot, group); backgroundEditors.insert(slot, editor); @@ -1267,7 +1299,7 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) addButton(makeLabelledButton(entriesGroup, moduleText("Designer.ImportCsv")), &SectionEditor::importCsv); entriesLayout->addLayout(buttons); - outer->addWidget(entriesGroup, 1); + tabLayout(EditorTab::Content)->addWidget(entriesGroup, 1); /* * The divider's three piece stacks: its two ends and its middle. @@ -1359,16 +1391,26 @@ SectionEditor::SectionEditor(QWidget *parent) : QWidget(parent) connect(table, &QTableWidget::itemChanged, this, [this] { emitChanged(); }); } - outer->addWidget(dividerPiecesGroup, 1); + /* + * The piece stacks are what a divider is made of, so they go where a list's entries do. A + * divider has no words to type, which leaves its Content tab holding these alone -- which is + * exactly right: this is the thing somebody opens a divider to work on. + */ + tabLayout(EditorTab::Content)->addWidget(dividerPiecesGroup, 1); /* - * Whatever height is left over when the editor is shorter than the pane it sits in. A - * QVBoxLayout with nothing to give the slack to shares it out between the items it has, so - * without this a type carrying few fields -- Title especially -- has its handful of rows - * spread down the pane with gaps between them rather than sitting one under the next. + * Whatever height is left over when a page is shorter than the tab it sits in. A QVBoxLayout + * with nothing to give the slack to shares it out between the items it has, so without this a + * type carrying few fields -- Title especially -- has its handful of rows spread down the page + * with gaps between them rather than sitting one under the next. + * + * One per page, and added last, so each page packs its own groups at the top independently of + * what the others hold. */ - outer->addStretch(); - trailingStretchIndex = outer->count() - 1; + for (int index = 0; index < kEditorTabCount; ++index) { + tabLayouts[index]->addStretch(); + tabStretchIndex[index] = tabLayouts[index]->count() - 1; + } /* * The settings held back until the reader asks for everything. @@ -1850,6 +1892,74 @@ void SectionEditor::markAdvanced(QWidget *field) advancedRows.insert(field); } +QVBoxLayout *SectionEditor::addTab(EditorTab tab, const QString &title) +{ + const int index = static_cast(tab); + + /* + * A scroll area per page rather than one around the whole editor. The header and the tab strip + * are then outside every one of them and cannot be scrolled off the top, and each page keeps + * the place the reader left it at -- which is the point of tabs: going back to one should be + * going back to where you were, not to the top of it. + */ + auto *scroll = new QScrollArea(tabs); + scroll->setWidgetResizable(true); + /* The tab already draws a frame around the page; a second one inside it is a box in a box. */ + scroll->setFrameShape(QFrame::NoFrame); + + auto *page = new QWidget(scroll); + auto *layout = new QVBoxLayout(page); + layout->setContentsMargins(0, 6, 0, 0); + scroll->setWidget(page); + + tabs->addTab(scroll, title); + + tabLayouts[index] = layout; + + return layout; +} + +bool SectionEditor::tabHasVisibleGroup(EditorTab tab) const +{ + const QVBoxLayout *layout = tabLayouts[static_cast(tab)]; + + for (int item = 0; item < layout->count(); ++item) { + const QWidget *widget = layout->itemAt(item)->widget(); + /* + * `isHidden` rather than `isVisible`, because a page on a tab nobody is looking at is + * itself hidden and every widget on it would answer no. What is being asked here is + * whether the group was hidden on purpose, which is the flag `isHidden` carries. + */ + if (widget && !widget->isHidden()) + return true; + } + + return false; +} + +void SectionEditor::refreshTabVisibility() +{ + /* + * Hiding the tab somebody is on moves the selection, and that move is Qt tidying up rather + * than the reader choosing -- so it must not be recorded as the tab to come back to. + */ + restoringTab = true; + + for (int index = 0; index < kEditorTabCount; ++index) + tabs->setTabVisible(index, tabHasVisibleGroup(static_cast(index))); + + /* + * Back to the tab the reader picked, now that it has something on it again. When it still has + * not, whatever Qt fell back to is left alone: the choice is remembered, not forced, so + * clicking through a run of sections that have no styles does not keep dragging them to a tab + * that is empty for the one they are looking at. + */ + if (desiredTab >= 0 && desiredTab < kEditorTabCount && tabs->isTabVisible(desiredTab)) + tabs->setCurrentIndex(desiredTab); + + restoringTab = false; +} + SectionType SectionEditor::composedType() const { SectionTypeSwitches switches; @@ -2195,11 +2305,6 @@ void SectionEditor::applyTypeVisibility(SectionType type) dividerPiecesGroup->setVisible(divider); - /* - * Whichever table is on show is the one thing here worth growing, so it takes the leftover - * height. With no table at all the trailing spacer takes it instead, which is what keeps the - * rows packed at the top rather than spread down the pane. - */ /* * A group with every row hidden is a heading over nothing, so it goes away with them. Asked * of the form rather than tracked alongside it: the rows have just been set, and counting @@ -2209,7 +2314,16 @@ void SectionEditor::applyTypeVisibility(SectionType type) typeSettingsGroup->setVisible(formHasVisibleRow(typeSettingsForm)); placementGroup->setVisible(formHasVisibleRow(placementForm)); - outerLayout->setStretch(trailingStretchIndex, hasEntries || divider ? 0 : 1); + /* + * Whichever table the Content tab is showing is the one thing on it worth growing, so it takes + * that page's leftover height. With no table at all the trailing spacer takes it instead, + * which is what keeps the rows packed at the top rather than spread down the page. + */ + tabLayout(EditorTab::Content) + ->setStretch(tabStretchIndex[static_cast(EditorTab::Content)], hasEntries || divider ? 0 : 1); + + /* Last, so it reads the groups this pass has just settled. */ + refreshTabVisibility(); } void SectionEditor::rebuildEntryTable(SectionType type, bool rowSubtitles) diff --git a/src/ui/SectionEditor.hpp b/src/ui/SectionEditor.hpp index c1fb801..9103df7 100644 --- a/src/ui/SectionEditor.hpp +++ b/src/ui/SectionEditor.hpp @@ -37,6 +37,7 @@ class QLabel; class QLineEdit; class QPlainTextEdit; class QPushButton; +class QScrollArea; class QSpinBox; class QTableWidget; class QTabWidget; @@ -45,6 +46,23 @@ class QVBoxLayout; namespace closingtime { +/* + * The four tabs the section's settings are dealt out into. + * + * The editor used to be one column of fifteen groups: what a section says, how its type is put + * together, where it sits, five text styles, eight panels and a table -- everything about a section + * at once, in a pane a third of a window wide. Folding the groups away made the column shorter + * without making it any less of a column; the reader still had to scroll past the geometry to reach + * the words. + * + * These four are the jobs somebody actually sits down to do, and each is a whole answer on its own: + * fill the section in, place it, ink it, put something behind it. The order is the order the work + * tends to happen in. + */ +enum class EditorTab { Content, Layout, Style, Background }; + +constexpr int kEditorTabCount = 4; + /* * Which of a divider's three piece stacks an editor is acting on. * @@ -234,6 +252,33 @@ class SectionEditor : public QWidget { */ void markAdvanced(QWidget *field); + /* + * Builds one tab and returns the layout its groups go into. + * + * Each tab scrolls on its own, so the header above the tab strip and the strip itself stay + * put however tall the settings under them get, and each tab keeps its own scroll position -- + * coming back to Style leaves the reader where they left off in it rather than at the top of + * a column they have to find their place in again. + */ + QVBoxLayout *addTab(EditorTab tab, const QString &title); + + /* The layout a tab's groups are added to. */ + QVBoxLayout *tabLayout(EditorTab tab) const { return tabLayouts[static_cast(tab)]; } + + /* + * Takes away the tabs with nothing on them and gives them back. + * + * A Spacer has no words, no styles and no panels, so three of the four would be empty panes + * inviting the reader to look for settings that are not there. Asked of the groups on each + * page rather than of the section type: applyTypeVisibility has just decided what applies, and + * counting what it left on screen cannot disagree with it the way a second list of conditions + * could. + */ + void refreshTabVisibility(); + + /* True when any group on a tab's page is still on screen. */ + bool tabHasVisibleGroup(EditorTab tab) const; + /* * The section type the picker and its switches add up to, and the reverse: which base type * and switches stand for a given section type. @@ -320,6 +365,31 @@ class SectionEditor : public QWidget { /* The rows held back until the reader asks for everything. */ QSet advancedRows; + /* + * The handful of rows above the tab strip: what kind of section this is, what it is called, + * whether it is drawn, and whether the rarely-wanted settings are on show. + * + * They stay out of the tabs because they are not settings of one job -- the type decides what + * every tab holds, the name is how the section is found again, and the advanced switch reaches + * rows on three of the four pages. A reader who had to leave the tab they were working in to + * flip one of these would lose their place to change something that governs all of them. + */ + QFormLayout *headerForm = nullptr; + + QTabWidget *tabs = nullptr; + /* The layout each page's groups are added to, in EditorTab order. */ + QVBoxLayout *tabLayouts[kEditorTabCount] = {}; + /* Where each page's trailing spacer sits in its layout; see the note beside the one it added. */ + int tabStretchIndex[kEditorTabCount] = {}; + /* + * The tab the reader last chose, which is not always the one on screen: a tab going away under + * them takes the selection with it, and this is what brings them back to it when the next + * section has one again. + */ + int desiredTab = 0; + /* Set while tabs are being taken away and given back, so that shuffle is not read as a choice. */ + bool restoringTab = false; + CollapsibleGroup *contentGroup = nullptr; CollapsibleGroup *typeSettingsGroup = nullptr; CollapsibleGroup *placementGroup = nullptr; @@ -467,14 +537,6 @@ class SectionEditor : public QWidget { /* Hidden for the entry types that have no logo path to set. */ QToolButton *setLogoButton = nullptr; - /* - * Absorbs whatever height is left once the visible rows have been laid out. Without it a - * type carrying few fields has its rows spread down the pane by the leftover space rather - * than sitting one under the next. - */ - QVBoxLayout *outerLayout = nullptr; - int trailingStretchIndex = -1; - QVector presets; QVector backgroundPresets; /*