diff --git a/Base/res/ladybird/about-pages/settings.html b/Base/res/ladybird/about-pages/settings.html index 28b925b2c9..64c786f8c1 100644 --- a/Base/res/ladybird/about-pages/settings.html +++ b/Base/res/ladybird/about-pages/settings.html @@ -388,6 +388,10 @@ + diff --git a/Base/res/ladybird/about-pages/settings/tabs.js b/Base/res/ladybird/about-pages/settings/tabs.js index bc37f6e99a..d541f4a566 100644 --- a/Base/res/ladybird/about-pages/settings/tabs.js +++ b/Base/res/ladybird/about-pages/settings/tabs.js @@ -1,4 +1,5 @@ const enableVerticalTabs = document.querySelector("#enable-vertical-tabs"); +const verticalTabsExpandOnHover = document.querySelector("#vertical-tabs-expand-on-hover"); let TAB_SETTINGS = {}; @@ -9,10 +10,16 @@ const loadFeatures = features => { tabSettingsCard.classList.toggle("hidden", !features?.verticalTabs); }; +const updateVerticalTabsDependentSettings = () => { + verticalTabsExpandOnHover.parentElement.classList.toggle("hidden", !enableVerticalTabs.checked); +}; + const loadSettings = settings => { TAB_SETTINGS = settings.tabs || {}; enableVerticalTabs.checked = !!TAB_SETTINGS.verticalTabsEnabled; + verticalTabsExpandOnHover.checked = !!TAB_SETTINGS.verticalTabsExpandOnHover; + updateVerticalTabsDependentSettings(); }; function addChangeHandler(input, name) { @@ -23,6 +30,7 @@ function addChangeHandler(input, name) { } addChangeHandler(enableVerticalTabs, "verticalTabsEnabled"); +addChangeHandler(verticalTabsExpandOnHover, "verticalTabsExpandOnHover"); document.addEventListener("WebUIMessage", event => { if (event.detail.name === "loadFeatures") { diff --git a/Libraries/LibWebView/Settings.cpp b/Libraries/LibWebView/Settings.cpp index c8f5182452..387bb7d18d 100644 --- a/Libraries/LibWebView/Settings.cpp +++ b/Libraries/LibWebView/Settings.cpp @@ -25,6 +25,7 @@ static constexpr auto NEW_TAB_PAGE_URL_KEY = "newTabPageURL"sv; static constexpr auto TAB_SETTINGS_KEY = "tabs"sv; static constexpr auto VERTICAL_TABS_ENABLED_KEY = "verticalTabsEnabled"sv; static constexpr auto VERTICAL_TABS_EXPANDED_KEY = "verticalTabsExpanded"sv; +static constexpr auto VERTICAL_TABS_EXPAND_ON_HOVER_KEY = "verticalTabsExpandOnHover"sv; static constexpr auto SHOW_BOOKMARKS_BAR_KEY = "showBookmarksBar"sv; static constexpr auto DEFAULT_SHOW_BOOKMARKS_BAR = true; @@ -286,6 +287,7 @@ JsonValue Settings::serialize_json() const JsonObject tab_settings; tab_settings.set(VERTICAL_TABS_ENABLED_KEY, m_tab_settings.vertical_tabs_enabled); tab_settings.set(VERTICAL_TABS_EXPANDED_KEY, m_tab_settings.vertical_tabs_expanded); + tab_settings.set(VERTICAL_TABS_EXPAND_ON_HOVER_KEY, m_tab_settings.vertical_tabs_expand_on_hover); settings.set(TAB_SETTINGS_KEY, move(tab_settings)); settings.set(SHOW_BOOKMARKS_BAR_KEY, m_show_bookmarks_bar); @@ -415,6 +417,8 @@ TabSettings Settings::parse_tab_settings(JsonValue const& settings) tab_settings.vertical_tabs_enabled = *vertical_tabs_enabled; if (auto vertical_tabs_expanded = settings.as_object().get_bool(VERTICAL_TABS_EXPANDED_KEY); vertical_tabs_expanded.has_value()) tab_settings.vertical_tabs_expanded = *vertical_tabs_expanded; + if (auto vertical_tabs_expand_on_hover = settings.as_object().get_bool(VERTICAL_TABS_EXPAND_ON_HOVER_KEY); vertical_tabs_expand_on_hover.has_value()) + tab_settings.vertical_tabs_expand_on_hover = *vertical_tabs_expand_on_hover; return tab_settings; } diff --git a/Libraries/LibWebView/Settings.h b/Libraries/LibWebView/Settings.h index d179979809..92bcdb51ef 100644 --- a/Libraries/LibWebView/Settings.h +++ b/Libraries/LibWebView/Settings.h @@ -25,6 +25,7 @@ namespace WebView { struct TabSettings { bool vertical_tabs_enabled { false }; bool vertical_tabs_expanded { true }; + bool vertical_tabs_expand_on_hover { false }; }; struct BrowsingBehavior { diff --git a/UI/Qt/BrowserWindow.cpp b/UI/Qt/BrowserWindow.cpp index 7dba90baf7..45160ce356 100644 --- a/UI/Qt/BrowserWindow.cpp +++ b/UI/Qt/BrowserWindow.cpp @@ -503,6 +503,7 @@ void BrowserWindow::update_tabs_display() auto const& settings = Application::settings().tab_settings(); m_tabs_container->set_vertical_tabs_enabled(settings.vertical_tabs_enabled); m_tabs_container->set_vertical_tabs_expanded(settings.vertical_tabs_expanded); + m_tabs_container->set_vertical_tabs_expand_on_hover(settings.vertical_tabs_expand_on_hover); } void BrowserWindow::rebuild_bookmarks_menu() diff --git a/UI/Qt/TabBar.cpp b/UI/Qt/TabBar.cpp index ae90167822..44e77a13b9 100644 --- a/UI/Qt/TabBar.cpp +++ b/UI/Qt/TabBar.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ static constexpr int VERTICAL_TABS_COLLAPSED_SIDE_MARGIN = 6; static constexpr int VERTICAL_TABS_EXPANDED_SIDE_MARGIN = 5; static constexpr int VERTICAL_TAB_SHAPE_HORIZONTAL_INSET = 5; static constexpr int VERTICAL_TAB_CONTENT_HORIZONTAL_INSET = 8; +static constexpr int VERTICAL_TABS_HOVER_COLLAPSE_POLL_INTERVAL_MS = 250; static constexpr int TAB_ICON_SIZE = 16; static constexpr auto VERTICAL_TABS_EXPANDED_PROPERTY = "verticalTabsExpanded"; static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY = "hovered"; @@ -904,6 +906,9 @@ bool TabBar::start_window_move() TabWidget::TabWidget(QWidget* parent) : QWidget(parent) { + if (auto* top_level_window = window(); top_level_window != this) + top_level_window->installEventFilter(this); + m_vertical_tabs_expanded_width = Settings::the()->vertical_tabs_expanded_width().value_or(VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH); m_vertical_tabs_expanded_width = clamp_vertical_tabs_expanded_width(m_vertical_tabs_expanded_width); @@ -917,6 +922,7 @@ TabWidget::TabWidget(QWidget* parent) m_tab_bar->setExpanding(false); m_tab_bar->setUsesScrollButtons(true); m_tab_bar->setDrawBase(false); + m_tab_bar->installEventFilter(this); m_stacked_widget = new QStackedWidget(this); @@ -926,6 +932,7 @@ TabWidget::TabWidget(QWidget* parent) m_new_tab_button->setFixedSize(32, 32); m_new_tab_button->setFocusPolicy(Qt::NoFocus); m_new_tab_button->setToolTip("New Tab"); + m_new_tab_button->installEventFilter(this); m_minimize_window_button = new WindowControlButton("LadybirdWindowButton", "Minimize", { 18, 18 }, { 40, 40 }, this); m_maximize_window_button = new WindowControlButton("LadybirdWindowButton", "Maximize", { 18, 18 }, { 40, 40 }, this); @@ -935,6 +942,7 @@ TabWidget::TabWidget(QWidget* parent) m_vertical_tabs_new_tab_separator->setObjectName("LadybirdVerticalTabsSeparator"); m_vertical_tabs_new_tab_separator->setFixedHeight(1); m_vertical_tabs_new_tab_separator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_vertical_tabs_new_tab_separator->installEventFilter(this); recreate_icons(); @@ -943,17 +951,21 @@ TabWidget::TabWidget(QWidget* parent) m_tab_bar_row_layout = new QHBoxLayout(m_tab_bar_row); m_tab_bar_row->installEventFilter(this); - m_vertical_tab_bar_column = new QWidget(this); + m_vertical_tabs_content = new QWidget(this); + m_vertical_tabs_content_layout = new QHBoxLayout(m_vertical_tabs_content); + m_vertical_tabs_content_layout->setSpacing(0); + m_vertical_tabs_content_layout->setContentsMargins(0, 0, 0, 0); + m_vertical_tabs_content->installEventFilter(this); + + m_vertical_tab_bar_column = new QWidget(m_vertical_tabs_content); m_vertical_tab_bar_column->setObjectName("LadybirdVerticalTabBar"); m_vertical_tab_bar_column_layout = new QVBoxLayout(m_vertical_tab_bar_column); m_vertical_tab_bar_column->setProperty(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, false); m_vertical_tab_bar_column->setProperty(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false); m_vertical_tab_bar_column->installEventFilter(this); - m_vertical_tabs_content = new QWidget(this); - m_vertical_tabs_content_layout = new QHBoxLayout(m_vertical_tabs_content); - m_vertical_tabs_content_layout->setSpacing(0); - m_vertical_tabs_content_layout->setContentsMargins(0, 0, 0, 0); + m_vertical_tabs_reserved_space = new QWidget(m_vertical_tabs_content); + m_vertical_tabs_reserved_space->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); m_vertical_tabs_resize_handle = new QWidget(m_vertical_tabs_content); m_vertical_tabs_resize_handle->setObjectName("LadybirdVerticalTabsResizeHandle"); @@ -961,6 +973,12 @@ TabWidget::TabWidget(QWidget* parent) m_vertical_tabs_resize_handle->installEventFilter(this); m_vertical_tabs_resize_handle->hide(); + m_vertical_tabs_hover_collapse_timer = new QTimer(this); + m_vertical_tabs_hover_collapse_timer->setInterval(VERTICAL_TABS_HOVER_COLLAPSE_POLL_INTERVAL_MS); + connect(m_vertical_tabs_hover_collapse_timer, &QTimer::timeout, this, [this]() { + update_vertical_tabs_hover_expanded(); + }); + m_main_layout = new QVBoxLayout(this); m_main_layout->setSpacing(0); m_main_layout->setContentsMargins(0, 0, 0, 0); @@ -1082,6 +1100,8 @@ void TabWidget::set_vertical_tabs_enabled(bool enabled) return; m_vertical_tabs_enabled = enabled; + if (!m_vertical_tabs_enabled) + set_vertical_tabs_hover_expanded(false); for (int index = 0; index < m_stacked_widget->count(); ++index) { tab(index)->set_toolbar_window_controls_visible(m_window_controls_visible && enabled); tab(index)->set_vertical_tabs_enabled(enabled); @@ -1095,6 +1115,18 @@ void TabWidget::set_vertical_tabs_expanded(bool expanded) return; m_vertical_tabs_expanded = expanded; + set_vertical_tabs_hover_expanded(false); + rebuild_layout(); +} + +void TabWidget::set_vertical_tabs_expand_on_hover(bool expand_on_hover) +{ + if (m_vertical_tabs_expand_on_hover == expand_on_hover) + return; + + m_vertical_tabs_expand_on_hover = expand_on_hover; + if (!m_vertical_tabs_expand_on_hover) + set_vertical_tabs_hover_expanded(false); rebuild_layout(); } @@ -1105,6 +1137,8 @@ bool TabWidget::event(QEvent* event) update_chrome_style(); } else if (type == QEvent::WindowStateChange) { update_window_button_icons(); + } else if (type == QEvent::Leave) { + defer_update_vertical_tabs_hover_expanded(); } return QWidget::event(event); @@ -1127,6 +1161,9 @@ void TabWidget::dropEvent(QDropEvent* event) bool TabWidget::eventFilter(QObject* watched, QEvent* event) { + if (watched == window() && event->type() == QEvent::Leave) + set_vertical_tabs_hover_expanded(false); + if (watched == m_vertical_tabs_resize_handle) { auto reset_resize_handle = [this] { m_is_resizing_vertical_tabs = false; @@ -1178,6 +1215,22 @@ bool TabWidget::eventFilter(QObject* watched, QEvent* event) } } + if (watched == m_vertical_tabs_content && event->type() == QEvent::Leave) + defer_update_vertical_tabs_hover_expanded(); + + auto is_vertical_tabs_hover_target = watched == m_vertical_tab_bar_column + || watched == m_tab_bar + || watched == m_new_tab_button + || watched == m_vertical_tabs_new_tab_separator; + + if (is_vertical_tabs_hover_target) { + if (event->type() == QEvent::Enter) { + set_vertical_tabs_hover_expanded(true); + } else if (event->type() == QEvent::Leave) { + defer_update_vertical_tabs_hover_expanded(); + } + } + if (watched == m_tab_bar_row || watched == m_vertical_tab_bar_column) { auto is_empty_chrome_area = [this, watched](QMouseEvent const& mouse_event) { if (watched == m_vertical_tab_bar_column) { @@ -1254,7 +1307,7 @@ TabLayout TabWidget::current_tab_layout() const { if (!m_vertical_tabs_enabled) return TabLayout::Horizontal; - return m_vertical_tabs_expanded ? TabLayout::VerticalExpanded : TabLayout::VerticalCollapsed; + return vertical_tabs_effectively_expanded() ? TabLayout::VerticalExpanded : TabLayout::VerticalCollapsed; } QWidget* TabWidget::tab_drag_area_widget() const @@ -1262,6 +1315,36 @@ QWidget* TabWidget::tab_drag_area_widget() const return m_tab_bar->tab_layout() == TabLayout::Horizontal ? m_tab_bar_row : m_vertical_tab_bar_column; } +bool TabWidget::vertical_tabs_effectively_expanded() const +{ + return m_vertical_tabs_expanded || m_vertical_tabs_hover_expanded; +} + +bool TabWidget::can_expand_vertical_tabs_on_hover() const +{ + return m_vertical_tabs_enabled && m_vertical_tabs_expand_on_hover && !m_vertical_tabs_expanded; +} + +bool TabWidget::cursor_is_over_vertical_tabs() const +{ + if (!m_vertical_tab_bar_column->isVisible()) + return false; + + auto cursor_position = m_vertical_tab_bar_column->mapFromGlobal(QCursor::pos()); + if (m_vertical_tab_bar_column->rect().contains(cursor_position)) + return true; + + return m_vertical_tab_bar_column->underMouse() + || m_tab_bar->underMouse() + || m_new_tab_button->underMouse() + || m_vertical_tabs_new_tab_separator->underMouse(); +} + +int TabWidget::vertical_tabs_layout_width() const +{ + return m_vertical_tabs_expanded ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH; +} + void TabWidget::rebuild_layout() { clear_layout(*m_main_layout); @@ -1274,7 +1357,7 @@ void TabWidget::rebuild_layout() if (m_tab_bar->tab_layout() != TabLayout::Horizontal) { rebuild_layout_for_vertical_tabs(); - m_vertical_tabs_content_layout->addWidget(m_vertical_tab_bar_column); + m_vertical_tabs_content_layout->addWidget(m_vertical_tabs_reserved_space); m_vertical_tabs_content_layout->addWidget(m_stacked_widget, 1); m_main_layout->addWidget(m_vertical_tabs_content, 1); m_vertical_tabs_content->show(); @@ -1313,18 +1396,21 @@ void TabWidget::rebuild_layout_for_horizontal_tabs() void TabWidget::rebuild_layout_for_vertical_tabs() { + auto expanded = vertical_tabs_effectively_expanded(); + auto reserved_width = vertical_tabs_layout_width(); auto side_bar_width = current_vertical_tabs_width(); + m_vertical_tabs_reserved_space->setFixedWidth(reserved_width); m_vertical_tab_bar_column->setFixedWidth(side_bar_width); - m_vertical_tab_bar_column_layout->setSpacing(m_vertical_tabs_expanded ? 0 : 4); - auto side_margin = vertical_tabs_side_margin(m_vertical_tabs_expanded); + m_vertical_tab_bar_column_layout->setSpacing(expanded ? 0 : 4); + auto side_margin = vertical_tabs_side_margin(expanded); m_vertical_tab_bar_column_layout->setContentsMargins(side_margin, 8, side_margin, 8); - m_new_tab_button->setToolButtonStyle(m_vertical_tabs_expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly); + m_new_tab_button->setToolButtonStyle(expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly); update_vertical_tabs_action_labels(); - m_new_tab_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, m_vertical_tabs_expanded); - m_vertical_tabs_new_tab_separator->setVisible(!m_vertical_tabs_expanded); - if (m_vertical_tabs_expanded) { + m_new_tab_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, expanded); + m_vertical_tabs_new_tab_separator->setVisible(!expanded); + if (expanded) { m_new_tab_button->setFixedHeight(VERTICAL_TAB_HEIGHT); m_new_tab_button->setMinimumWidth(32); m_new_tab_button->setMaximumWidth(QWIDGETSIZE_MAX); @@ -1335,7 +1421,7 @@ void TabWidget::rebuild_layout_for_vertical_tabs() } Qt::Alignment new_tab_button_alignment {}; - if (!m_vertical_tabs_expanded) + if (!expanded) new_tab_button_alignment = Qt::AlignHCenter; m_vertical_tab_bar_column_layout->addWidget(m_tab_bar); m_vertical_tab_bar_column_layout->addWidget(m_vertical_tabs_new_tab_separator); @@ -1345,7 +1431,7 @@ void TabWidget::rebuild_layout_for_vertical_tabs() int TabWidget::current_vertical_tabs_width() const { - return m_vertical_tabs_expanded ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH; + return vertical_tabs_effectively_expanded() ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH; } void TabWidget::apply_vertical_tabs_expanded_width(int width) @@ -1387,22 +1473,26 @@ void TabWidget::update_vertical_tabs_resize_handle() } auto handle_width = VERTICAL_TABS_RESIZE_HIT_AREA_WIDTH; - auto divider_x = m_vertical_tab_bar_column->geometry().right(); + auto divider_x = vertical_tabs_layout_width() - 1; m_vertical_tabs_resize_handle->setGeometry(divider_x - (handle_width / 2), 0, handle_width, m_vertical_tabs_content->height()); m_vertical_tabs_resize_handle->raise(); } void TabWidget::update_vertical_tabs_action_labels() { - m_new_tab_button->setText(m_vertical_tabs_expanded ? "New Tab" : QString {}); + m_new_tab_button->setText(vertical_tabs_effectively_expanded() ? "New Tab" : QString {}); } void TabWidget::update_tab_layout() { if (m_tab_bar->tab_layout() != TabLayout::Horizontal) { + auto expanded = vertical_tabs_effectively_expanded(); + auto reserved_width = vertical_tabs_layout_width(); auto side_bar_width = current_vertical_tabs_width(); + m_vertical_tabs_reserved_space->setFixedWidth(reserved_width); m_vertical_tab_bar_column->setFixedWidth(side_bar_width); - m_tab_bar->set_available_width(side_bar_width - (vertical_tabs_side_margin(m_vertical_tabs_expanded) * 2)); + update_vertical_tabs_overlay_geometry(); + m_tab_bar->set_available_width(side_bar_width - (vertical_tabs_side_margin(expanded) * 2)); update_vertical_tabs_resize_handle(); return; } @@ -1471,6 +1561,45 @@ void TabWidget::update_chrome_style() m_is_updating_chrome_style = false; } +void TabWidget::update_vertical_tabs_overlay_geometry() +{ + if (m_tab_bar->tab_layout() == TabLayout::Horizontal) + return; + + m_vertical_tab_bar_column->setGeometry(0, 0, current_vertical_tabs_width(), m_vertical_tabs_content->height()); + m_vertical_tab_bar_column->raise(); +} + +void TabWidget::set_vertical_tabs_hover_expanded(bool expanded) +{ + expanded &= can_expand_vertical_tabs_on_hover(); + if (m_vertical_tabs_hover_expanded == expanded) + return; + + m_vertical_tabs_hover_expanded = expanded; + if (m_vertical_tabs_hover_expanded) + m_vertical_tabs_hover_collapse_timer->start(); + else + m_vertical_tabs_hover_collapse_timer->stop(); + rebuild_layout(); +} + +void TabWidget::defer_update_vertical_tabs_hover_expanded() +{ + QTimer::singleShot(0, this, [this]() { + update_vertical_tabs_hover_expanded(); + }); +} + +void TabWidget::update_vertical_tabs_hover_expanded() +{ + if (!m_vertical_tabs_hover_expanded) + return; + + if (!cursor_is_over_vertical_tabs()) + set_vertical_tabs_hover_expanded(false); +} + void TabWidget::update_window_button_icons() { auto is_maximized = window()->isMaximized(); diff --git a/UI/Qt/TabBar.h b/UI/Qt/TabBar.h index b0b0e4b39c..a040bcc7ab 100644 --- a/UI/Qt/TabBar.h +++ b/UI/Qt/TabBar.h @@ -30,6 +30,7 @@ class QPaintEvent; class QPixmap; class QResizeEvent; class QToolButton; +class QTimer; class QVariantAnimation; class QWheelEvent; @@ -148,6 +149,7 @@ public: void update_window_button_icons(); void set_vertical_tabs_enabled(bool); void set_vertical_tabs_expanded(bool); + void set_vertical_tabs_expand_on_hover(bool); void update_tab_button_visibility(); signals: @@ -165,6 +167,10 @@ protected: private: TabLayout current_tab_layout() const; QWidget* tab_drag_area_widget() const; + bool vertical_tabs_effectively_expanded() const; + bool can_expand_vertical_tabs_on_hover() const; + bool cursor_is_over_vertical_tabs() const; + int vertical_tabs_layout_width() const; void rebuild_layout(); void rebuild_layout_for_horizontal_tabs(); @@ -179,6 +185,10 @@ private: void update_tab_chrome_visibility(); void recreate_icons(); void update_chrome_style(); + void update_vertical_tabs_overlay_geometry(); + void set_vertical_tabs_hover_expanded(bool); + void defer_update_vertical_tabs_hover_expanded(); + void update_vertical_tabs_hover_expanded(); void toggle_window_maximized(); bool start_window_move(); void accept_tab_drag(QDragMoveEvent*); @@ -192,9 +202,11 @@ private: QToolButton* m_close_window_button { nullptr }; QWidget* m_tab_bar_row { nullptr }; QWidget* m_vertical_tabs_new_tab_separator { nullptr }; + QWidget* m_vertical_tabs_reserved_space { nullptr }; QWidget* m_vertical_tab_bar_column { nullptr }; QWidget* m_vertical_tabs_resize_handle { nullptr }; QWidget* m_vertical_tabs_content { nullptr }; + QTimer* m_vertical_tabs_hover_collapse_timer { nullptr }; QBoxLayout* m_main_layout { nullptr }; QBoxLayout* m_tab_bar_row_layout { nullptr }; QBoxLayout* m_vertical_tab_bar_column_layout { nullptr }; @@ -203,6 +215,8 @@ private: bool m_window_controls_visible { true }; bool m_vertical_tabs_enabled { false }; bool m_vertical_tabs_expanded { true }; + bool m_vertical_tabs_expand_on_hover { false }; + bool m_vertical_tabs_hover_expanded { false }; bool m_is_resizing_vertical_tabs { false }; bool m_is_updating_chrome_style { false }; int m_vertical_tabs_expanded_width { 0 };