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