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;
/*