diff --git a/CHANGELOG.md b/CHANGELOG.md index f41f802a..94622c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Major: Added multi-channel searching to search dialog via keyboard shortcut. (Ctrl+Shift+F by default) (#3694) +- Minor: Added option to display tabs on the right and bottom. (#3847) - Minor: Added `is:first-msg` search option. (#3700) - Minor: Added quotation marks in the permitted/blocked Automod messages for clarity. (#3654) - Minor: Added a `Scroll to top` keyboard shortcut for splits. (#3802) diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 53b076a0..439cd432 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -25,7 +25,7 @@ class IgnorePhrase; class FilterRecord; class Nickname; -/// Settings which are availlable for reading on all threads. +/// Settings which are available for reading on all threads. class ConcurrentSettings { public: @@ -98,8 +98,8 @@ public: "/appearance/messages/usernameDisplayMode", UsernameDisplayMode::UsernameAndLocalizedName}; - EnumSetting tabDirection = { - "/appearance/tabDirection", NotebookTabDirection::Horizontal}; + EnumSetting tabDirection = {"/appearance/tabDirection", + NotebookTabLocation::Top}; // BoolSetting collapseLongMessages = // {"/appearance/messages/collapseLongMessages", false}; diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 4a6f1256..4ed2e2c1 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -49,6 +49,11 @@ Notebook::Notebook(QWidget *parent) }); this->addNotebookActionsToMenu(&this->menu_); + + // Manually resize the add button so the initial paint uses the correct + // width when computing the maximum width occupied per column in vertical + // tab rendering. + this->resizeAddButton(); } NotebookTab *Notebook::addPage(QWidget *page, QString title, bool select) @@ -402,12 +407,15 @@ void Notebook::setShowAddButton(bool value) this->addButton_->setHidden(!value); } -void Notebook::scaleChangedEvent(float scale) +void Notebook::resizeAddButton() { float h = (NOTEBOOK_TAB_HEIGHT - 1) * this->scale(); - this->addButton_->setFixedSize(h, h); +} +void Notebook::scaleChangedEvent(float) +{ + this->resizeAddButton(); for (auto &i : this->items_) { i.tab->updateSize(); @@ -422,16 +430,22 @@ void Notebook::resizeEvent(QResizeEvent *) void Notebook::performLayout(bool animated) { const auto left = int(2 * this->scale()); + const auto right = width(); + const auto bottom = height(); const auto scale = this->scale(); const auto tabHeight = int(NOTEBOOK_TAB_HEIGHT * scale); const auto minimumTabAreaSpace = int(tabHeight * 0.5); const auto addButtonWidth = this->showAddButton_ ? tabHeight : 0; + const auto lineThickness = int(2 * scale); - if (this->tabDirection_ == NotebookTabDirection::Horizontal) + const auto buttonWidth = tabHeight; + const auto buttonHeight = tabHeight - 1; + + if (this->tabLocation_ == NotebookTabLocation::Top) { auto x = left; auto y = 0; - auto buttonHeight = 0; + auto consumedButtonHeights = 0; // set size of custom buttons (settings, user, ...) for (auto *btn : this->customButtons_) @@ -441,11 +455,11 @@ void Notebook::performLayout(bool animated) continue; } - btn->setFixedSize(tabHeight, tabHeight - 1); + btn->setFixedSize(buttonWidth, buttonHeight); btn->move(x, 0); - x += tabHeight; + x += buttonWidth; - buttonHeight = tabHeight; + consumedButtonHeights = tabHeight; } if (this->showTabs_) @@ -495,20 +509,9 @@ void Notebook::performLayout(bool animated) } y += tabHeight; - - // raise elements - for (auto &i : this->items_) - { - i.tab->raise(); - } - - if (this->showAddButton_) - { - this->addButton_->raise(); - } } - y = std::max({y, buttonHeight, minimumTabAreaSpace}); + y = std::max({y, consumedButtonHeights, minimumTabAreaSpace}); if (this->lineOffset_ != y) { @@ -527,9 +530,8 @@ void Notebook::performLayout(bool animated) this->selectedPage_->raise(); } } - else + else if (this->tabLocation_ == NotebookTabLocation::Left) { - const int lineThickness = int(2 * scale); auto x = left; auto y = 0; @@ -541,54 +543,55 @@ void Notebook::performLayout(bool animated) continue; } - btn->setFixedSize(tabHeight, tabHeight - 1); + btn->setFixedSize(buttonWidth, buttonHeight); btn->move(x, y); - x += tabHeight; + x += buttonWidth; } - if (this->customButtons_.size() > 0) + if (this->visibleButtonCount() > 0) y = tabHeight; - int buttonWidth = x; + int totalButtonWidths = x; int top = y; x = left; // zneix: if we were to remove buttons when tabs are hidden // stuff below to "set page bounds" part should be in conditional statement - int verticalRowSpace = (this->height() - top) / tabHeight; - if (verticalRowSpace == 0) // window hasn't properly rendered yet + int tabsPerColumn = (this->height() - top) / tabHeight; + if (tabsPerColumn == 0) // window hasn't properly rendered yet { return; } int count = this->items_.size() + (this->showAddButton_ ? 1 : 0); - int columnCount = ceil((float)count / verticalRowSpace); + int columnCount = ceil((float)count / tabsPerColumn); // only add width of all the tabs if they are not hidden if (this->showTabs_) { for (int col = 0; col < columnCount; col++) { + bool isLastColumn = col == columnCount - 1; auto largestWidth = 0; - int colStart = col * verticalRowSpace; - int colEnd = - std::min((col + 1) * verticalRowSpace, this->items_.size()); + int tabStart = col * tabsPerColumn; + int tabEnd = + std::min((col + 1) * tabsPerColumn, this->items_.size()); - for (int i = colStart; i < colEnd; i++) + for (int i = tabStart; i < tabEnd; i++) { largestWidth = std::max( this->items_.at(i).tab->normalTabWidth(), largestWidth); } - if (col == columnCount - 1 && this->showAddButton_ && - largestWidth == 0) + if (isLastColumn && this->showAddButton_) { - largestWidth = this->addButton_->width(); + largestWidth = + std::max(largestWidth, this->addButton_->width()); } - if (largestWidth + x < buttonWidth && col == columnCount - 1) - largestWidth = buttonWidth - x; + if (isLastColumn && largestWidth + x < totalButtonWidths) + largestWidth = totalButtonWidths - x; - for (int i = colStart; i < colEnd; i++) + for (int i = tabStart; i < tabEnd; i++) { auto item = this->items_.at(i); @@ -598,7 +601,7 @@ void Notebook::performLayout(bool animated) y += tabHeight; } - if (col == columnCount - 1 && this->showAddButton_) + if (isLastColumn && this->showAddButton_) { this->addButton_->move(x, y); } @@ -608,7 +611,7 @@ void Notebook::performLayout(bool animated) } } - x = std::max({x, buttonWidth, minimumTabAreaSpace}); + x = std::max({x, totalButtonWidths, minimumTabAreaSpace}); if (this->lineOffset_ != x - lineThickness) { @@ -616,6 +619,216 @@ void Notebook::performLayout(bool animated) this->update(); } + // set page bounds + if (this->selectedPage_ != nullptr) + { + this->selectedPage_->move(x, 0); + this->selectedPage_->resize(width() - x, height()); + this->selectedPage_->raise(); + } + } + else if (this->tabLocation_ == NotebookTabLocation::Right) + { + auto x = right; + auto y = 0; + + // set size of custom buttons (settings, user, ...) + for (auto btnIt = this->customButtons_.rbegin(); + btnIt != this->customButtons_.rend(); ++btnIt) + { + auto btn = *btnIt; + if (!btn->isVisible()) + { + continue; + } + + x -= buttonWidth; + btn->setFixedSize(buttonWidth, buttonHeight); + btn->move(x, y); + } + + if (this->visibleButtonCount() > 0) + y = tabHeight; + + int consumedButtonWidths = right - x; + int top = y; + x = right; + + // zneix: if we were to remove buttons when tabs are hidden + // stuff below to "set page bounds" part should be in conditional statement + int tabsPerColumn = (this->height() - top) / tabHeight; + if (tabsPerColumn == 0) // window hasn't properly rendered yet + { + return; + } + int count = this->items_.size() + (this->showAddButton_ ? 1 : 0); + int columnCount = ceil((float)count / tabsPerColumn); + + // only add width of all the tabs if they are not hidden + if (this->showTabs_) + { + for (int col = 0; col < columnCount; col++) + { + bool isLastColumn = col == columnCount - 1; + auto largestWidth = 0; + int tabStart = col * tabsPerColumn; + int tabEnd = + std::min((col + 1) * tabsPerColumn, this->items_.size()); + + for (int i = tabStart; i < tabEnd; i++) + { + largestWidth = std::max( + this->items_.at(i).tab->normalTabWidth(), largestWidth); + } + + if (isLastColumn && this->showAddButton_) + { + largestWidth = + std::max(largestWidth, this->addButton_->width()); + } + + int distanceFromRight = width() - x; + + if (isLastColumn && + largestWidth + distanceFromRight < consumedButtonWidths) + largestWidth = consumedButtonWidths - distanceFromRight; + + x -= largestWidth + lineThickness; + + for (int i = tabStart; i < tabEnd; i++) + { + auto item = this->items_.at(i); + + /// Layout tab + item.tab->growWidth(largestWidth); + item.tab->moveAnimated(QPoint(x, y), animated); + y += tabHeight; + } + + if (isLastColumn && this->showAddButton_) + { + this->addButton_->move(x, y); + } + + y = top; + } + } + + // subtract another lineThickness to account for vertical divider + x -= lineThickness; + int consumedRightSpace = + std::max({right - x, consumedButtonWidths, minimumTabAreaSpace}); + int tabsStart = right - consumedRightSpace; + + if (this->lineOffset_ != tabsStart) + { + this->lineOffset_ = tabsStart; + this->update(); + } + + // set page bounds + if (this->selectedPage_ != nullptr) + { + this->selectedPage_->move(0, 0); + this->selectedPage_->resize(tabsStart, height()); + this->selectedPage_->raise(); + } + } + else if (this->tabLocation_ == NotebookTabLocation::Bottom) + { + auto x = left; + auto y = bottom; + auto consumedButtonHeights = 0; + + // set size of custom buttons (settings, user, ...) + for (auto *btn : this->customButtons_) + { + if (!btn->isVisible()) + { + continue; + } + + // move upward to place button below location (x, y) + y = bottom - tabHeight; + + btn->setFixedSize(buttonWidth, buttonHeight); + btn->move(x, y); + x += buttonWidth; + + consumedButtonHeights = tabHeight; + } + + if (this->showTabs_) + { + // reset vertical position regardless + y = bottom - tabHeight; + + // layout tabs + /// Notebook tabs need to know if they are in the last row. + auto firstInBottomRow = + this->items_.size() ? &this->items_.front() : nullptr; + + for (auto &item : this->items_) + { + /// Break line if element doesn't fit. + auto isFirst = &item == &this->items_.front(); + auto isLast = &item == &this->items_.back(); + + auto fitsInLine = ((isLast ? addButtonWidth : 0) + x + + item.tab->width()) <= width(); + + if (!isFirst && !fitsInLine) + { + y -= item.tab->height(); + x = left; + firstInBottomRow = &item; + } + + /// Layout tab + item.tab->growWidth(0); + item.tab->moveAnimated(QPoint(x, y), animated); + x += item.tab->width() + std::max(1, int(scale * 1)); + } + + /// Update which tabs are in the last row + auto inLastRow = false; + for (const auto &item : this->items_) + { + if (&item == firstInBottomRow) + { + inLastRow = true; + } + item.tab->setInLastRow(inLastRow); + } + + // move misc buttons + if (this->showAddButton_) + { + this->addButton_->move(x, y); + } + } + + int consumedBottomSpace = + std::max({bottom - y, consumedButtonHeights, minimumTabAreaSpace}); + int tabsStart = bottom - consumedBottomSpace; + + if (this->lineOffset_ != tabsStart) + { + this->lineOffset_ = tabsStart; + this->update(); + } + + // set page bounds + if (this->selectedPage_ != nullptr) + { + this->selectedPage_->move(0, 0); + this->selectedPage_->resize(width(), tabsStart); + this->selectedPage_->raise(); + } + } + + if (this->showTabs_) + { // raise elements for (auto &i : this->items_) { @@ -626,14 +839,6 @@ void Notebook::performLayout(bool animated) { this->addButton_->raise(); } - - // set page bounds - if (this->selectedPage_ != nullptr) - { - this->selectedPage_->move(x, 0); - this->selectedPage_->resize(width() - x, height()); - this->selectedPage_->raise(); - } } } @@ -651,11 +856,11 @@ void Notebook::mousePressEvent(QMouseEvent *event) } } -void Notebook::setTabDirection(NotebookTabDirection direction) +void Notebook::setTabLocation(NotebookTabLocation location) { - if (direction != this->tabDirection_) + if (location != this->tabLocation_) { - this->tabDirection_ = direction; + this->tabLocation_ = location; this->performLayout(); } } @@ -666,19 +871,31 @@ void Notebook::paintEvent(QPaintEvent *event) auto scale = this->scale(); QPainter painter(this); - if (this->tabDirection_ == NotebookTabDirection::Horizontal) + if (this->tabLocation_ == NotebookTabLocation::Top || + this->tabLocation_ == NotebookTabLocation::Bottom) { /// horizontal line painter.fillRect(0, this->lineOffset_, this->width(), int(2 * scale), this->theme->tabs.dividerLine); } - else + else if (this->tabLocation_ == NotebookTabLocation::Left || + this->tabLocation_ == NotebookTabLocation::Right) { - if (this->customButtons_.size() > 0) + if (this->visibleButtonCount() > 0) { - painter.fillRect(0, int(NOTEBOOK_TAB_HEIGHT * scale), - this->lineOffset_, int(2 * scale), - this->theme->tabs.dividerLine); + if (this->tabLocation_ == NotebookTabLocation::Left) + { + painter.fillRect(0, int(NOTEBOOK_TAB_HEIGHT * scale), + this->lineOffset_, int(2 * scale), + this->theme->tabs.dividerLine); + } + else + { + painter.fillRect(this->lineOffset_, + int(NOTEBOOK_TAB_HEIGHT * scale), + width() - this->lineOffset_, int(2 * scale), + this->theme->tabs.dividerLine); + } } /// vertical line @@ -739,6 +956,19 @@ NotebookTab *Notebook::getTabFromPage(QWidget *page) return nullptr; } +size_t Notebook::visibleButtonCount() const +{ + size_t i = 0; + for (auto *btn : this->customButtons_) + { + if (btn->isVisible()) + { + ++i; + } + } + return i; +} + SplitNotebook::SplitNotebook(Window *parent) : Notebook(parent) { diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 42b05fc3..f69b90b8 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -17,7 +17,7 @@ class NotebookButton; class NotebookTab; class SplitContainer; -enum NotebookTabDirection { Horizontal = 0, Vertical = 1 }; +enum NotebookTabLocation { Top = 0, Left = 1, Right = 2, Bottom = 3 }; class Notebook : public BaseWidget { @@ -58,7 +58,7 @@ public: void performLayout(bool animate = false); - void setTabDirection(NotebookTabDirection direction); + void setTabLocation(NotebookTabLocation location); bool isNotebookLayoutLocked() const; void setLockNotebookLayout(bool value); @@ -86,12 +86,17 @@ protected: } private: + void resizeAddButton(); + bool containsPage(QWidget *page); Item &findItem(QWidget *page); static bool containsChild(const QObject *obj, const QObject *child); NotebookTab *getTabFromPage(QWidget *page); + // Returns the number of buttons in `customButtons_` that are visible + size_t visibleButtonCount() const; + QList items_; QMenu menu_; QWidget *selectedPage_ = nullptr; @@ -104,7 +109,7 @@ private: bool showAddButton_ = false; int lineOffset_ = 20; bool lockNotebookLayout_ = false; - NotebookTabDirection tabDirection_ = NotebookTabDirection::Horizontal; + NotebookTabLocation tabLocation_ = NotebookTabLocation::Top; QAction *lockNotebookLayoutAction_; }; diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index d06a0bc9..74f02499 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -79,7 +79,7 @@ Window::Window(WindowType type, QWidget *parent) if (type == WindowType::Main || type == WindowType::Popup) { getSettings()->tabDirection.connect([this](int val) { - this->notebook_->setTabDirection(NotebookTabDirection(val)); + this->notebook_->setTabLocation(NotebookTabLocation(val)); }); } } diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 85b13a17..8655b6c4 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -145,28 +145,40 @@ void GeneralPage::initLayout(GeneralPageView &layout) return fuzzyToFloat(args.value, 1.f); }); ComboBox *tabDirectionDropdown = - layout.addDropdown::type>( - "Tab layout", {"Horizontal", "Vertical"}, s.tabDirection, + layout.addDropdown::type>( + "Tab layout", {"Top", "Left", "Right", "Bottom"}, s.tabDirection, [](auto val) { switch (val) { - case NotebookTabDirection::Horizontal: - return "Horizontal"; - case NotebookTabDirection::Vertical: - return "Vertical"; + case NotebookTabLocation::Top: + return "Top"; + case NotebookTabLocation::Left: + return "Left"; + case NotebookTabLocation::Right: + return "Right"; + case NotebookTabLocation::Bottom: + return "Bottom"; } return ""; }, [](auto args) { - if (args.value == "Vertical") + if (args.value == "Bottom") { - return NotebookTabDirection::Vertical; + return NotebookTabLocation::Bottom; + } + else if (args.value == "Left") + { + return NotebookTabLocation::Left; + } + else if (args.value == "Right") + { + return NotebookTabLocation::Right; } else { - // default to horizontal - return NotebookTabDirection::Horizontal; + // default to top + return NotebookTabLocation::Top; } }, false);