Hide fullscreen chrome at the TabWidget level instead of hiding the current tab's toolbar child directly. This lets the toolbar container and reserved verticaltabs space stop contributing to layout while fullscreen content is shown. Also keep the exit fullscreen button above native web content on macOS and center it using the fullscreen screen geometry rather than a potentially stale parent widget size.
2029 lines
70 KiB
C++
2029 lines
70 KiB
C++
/*
|
|
* Copyright (c) 2024-2026, Tim Flynn <trflynn89@ladybird.org>
|
|
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
|
|
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/StdLibExtras.h>
|
|
#include <UI/Qt/Application.h>
|
|
#include <UI/Qt/BrowserWindow.h>
|
|
#include <UI/Qt/ChromeLayout.h>
|
|
#include <UI/Qt/ChromeStyle.h>
|
|
#include <UI/Qt/Icon.h>
|
|
#if defined(AK_OS_MACOS)
|
|
# include <UI/Qt/MacWindow.h>
|
|
#endif
|
|
#include <UI/Qt/Menu.h>
|
|
#include <UI/Qt/Settings.h>
|
|
#include <UI/Qt/Tab.h>
|
|
#include <UI/Qt/TabBar.h>
|
|
#include <UI/Qt/WindowControlButton.h>
|
|
|
|
#include <QApplication>
|
|
#include <QContextMenuEvent>
|
|
#include <QCursor>
|
|
#include <QDrag>
|
|
#include <QDragEnterEvent>
|
|
#include <QDragLeaveEvent>
|
|
#include <QDragMoveEvent>
|
|
#include <QDropEvent>
|
|
#include <QEasingCurve>
|
|
#include <QEvent>
|
|
#include <QFontMetrics>
|
|
#include <QHBoxLayout>
|
|
#include <QLayoutItem>
|
|
#include <QLinearGradient>
|
|
#include <QMimeData>
|
|
#include <QMouseEvent>
|
|
#include <QPaintEvent>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QPixmap>
|
|
#include <QStyle>
|
|
#include <QTimer>
|
|
#include <QToolButton>
|
|
#include <QVBoxLayout>
|
|
#include <QVariant>
|
|
#include <QVariantAnimation>
|
|
#include <QWheelEvent>
|
|
#include <QWindow>
|
|
|
|
namespace Ladybird {
|
|
|
|
static constexpr auto LADYBIRD_TAB_MIME_TYPE = "application/x-ladybird-tab";
|
|
static constexpr int HORIZONTAL_TAB_STRIP_HEIGHT = 44;
|
|
static constexpr int HORIZONTAL_TAB_HEIGHT = 38;
|
|
static constexpr int HORIZONTAL_TAB_MIN_WIDTH = 128;
|
|
static constexpr int HORIZONTAL_TAB_MAX_WIDTH = 240;
|
|
static constexpr int VERTICAL_TAB_HEIGHT = 38;
|
|
static constexpr int VERTICAL_TABS_COLLAPSED_WIDTH = browser_chrome_layout_policy().collapsed_sidebar_width;
|
|
static constexpr int VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH = browser_chrome_layout_policy().expanded_sidebar_width;
|
|
static constexpr int VERTICAL_TABS_MIN_EXPANDED_WIDTH = 190;
|
|
static constexpr int VERTICAL_TABS_MAX_EXPANDED_WIDTH = 400;
|
|
static constexpr int VERTICAL_TABS_RESIZE_HIT_AREA_WIDTH = 5;
|
|
static constexpr int VERTICAL_TABS_COLLAPSED_SIDE_MARGIN = 6;
|
|
static constexpr int VERTICAL_TABS_EXPANDED_SIDE_MARGIN = 5;
|
|
static constexpr int VERTICAL_TABS_TOP_MARGIN = 8;
|
|
static constexpr int TAB_CARD_SHAPE_HORIZONTAL_INSET = 5;
|
|
static constexpr qreal HORIZONTAL_TAB_CARD_SHAPE_HORIZONTAL_INSET = 4.0;
|
|
static constexpr int TAB_CARD_SHAPE_VERTICAL_INSET = 3;
|
|
static constexpr int HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE = 32;
|
|
static constexpr int TAB_CONTENT_HORIZONTAL_INSET = 8;
|
|
static constexpr int VERTICAL_TABS_HOVER_COLLAPSE_POLL_INTERVAL_MS = 250;
|
|
static constexpr int TAB_BUTTON_SIZE = 22;
|
|
static constexpr int TAB_ICON_SIZE = 16;
|
|
static constexpr int COLLAPSED_VERTICAL_TAB_BUTTON_SIZE = 16;
|
|
static constexpr int COLLAPSED_VERTICAL_TAB_ICON_SIZE = 12;
|
|
static constexpr auto COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY = "collapsedVerticalTabButton";
|
|
static constexpr auto FULL_WIDTH_TOOLBAR_PROPERTY = "fullWidthToolbar";
|
|
static constexpr auto VERTICAL_TABS_EXPANDED_PROPERTY = "verticalTabsExpanded";
|
|
static constexpr auto VERTICAL_TABS_BUTTON_PROPERTY = "verticalTabsButton";
|
|
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY = "hovered";
|
|
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY = "active";
|
|
|
|
static void set_dynamic_property_if_needed(QWidget& widget, char const* property, QVariant const& value)
|
|
{
|
|
if (widget.property(property) == value)
|
|
return;
|
|
|
|
widget.setProperty(property, value);
|
|
widget.style()->unpolish(&widget);
|
|
widget.style()->polish(&widget);
|
|
widget.update();
|
|
}
|
|
|
|
static QPointer<TabWidget> s_active_tab_drag_source;
|
|
static QPointer<Tab> s_active_tab_dragged_tab;
|
|
static QPointer<TabWidget> s_pending_tab_drop_target;
|
|
static int s_pending_tab_drop_index { -1 };
|
|
|
|
static bool window_uses_client_side_decorations(QWidget& widget)
|
|
{
|
|
auto* top_level_window = qobject_cast<BrowserWindow*>(widget.window());
|
|
return !top_level_window || top_level_window->uses_client_side_decorations();
|
|
}
|
|
|
|
static QPainterPath tab_shape_path(QRectF const& rect, qreal top_radius, qreal bottom_radius)
|
|
{
|
|
top_radius = min(top_radius, rect.height() / 2.0);
|
|
bottom_radius = min(bottom_radius, rect.height() / 2.0);
|
|
|
|
QPainterPath path;
|
|
path.moveTo(rect.left() + top_radius, rect.top());
|
|
path.lineTo(rect.right() - top_radius, rect.top());
|
|
path.quadTo(rect.right(), rect.top(), rect.right(), rect.top() + top_radius);
|
|
path.lineTo(rect.right(), rect.bottom() - bottom_radius);
|
|
path.quadTo(rect.right(), rect.bottom(), rect.right() - bottom_radius, rect.bottom());
|
|
path.lineTo(rect.left() + bottom_radius, rect.bottom());
|
|
path.quadTo(rect.left(), rect.bottom(), rect.left(), rect.bottom() - bottom_radius);
|
|
path.lineTo(rect.left(), rect.top() + top_radius);
|
|
path.quadTo(rect.left(), rect.top(), rect.left() + top_radius, rect.top());
|
|
path.closeSubpath();
|
|
return path;
|
|
}
|
|
|
|
static void clear_layout(QLayout& layout)
|
|
{
|
|
while (auto* item = layout.takeAt(0))
|
|
delete item;
|
|
}
|
|
|
|
static constexpr int vertical_tabs_side_margin(bool expanded)
|
|
{
|
|
return expanded ? VERTICAL_TABS_EXPANDED_SIDE_MARGIN : VERTICAL_TABS_COLLAPSED_SIDE_MARGIN;
|
|
}
|
|
|
|
static constexpr int vertical_tabs_horizontal_margin_width(TabLayout tab_layout)
|
|
{
|
|
return vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed) * 2;
|
|
}
|
|
|
|
static constexpr int vertical_tab_width(int available_width, TabLayout tab_layout)
|
|
{
|
|
if (available_width > 0)
|
|
return available_width;
|
|
auto width = tab_layout == TabLayout::VerticalCollapsed ? VERTICAL_TABS_COLLAPSED_WIDTH : VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH;
|
|
return width - vertical_tabs_horizontal_margin_width(tab_layout);
|
|
}
|
|
|
|
static constexpr int clamp_vertical_tabs_expanded_width(int width)
|
|
{
|
|
return clamp(width, VERTICAL_TABS_MIN_EXPANDED_WIDTH, VERTICAL_TABS_MAX_EXPANDED_WIDTH);
|
|
}
|
|
|
|
static QRectF tab_card_shape_rect(QRectF const& rect)
|
|
{
|
|
return rect.adjusted(TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
|
|
}
|
|
|
|
static QRectF horizontal_tab_card_shape_rect(QRectF const& rect)
|
|
{
|
|
return rect.adjusted(HORIZONTAL_TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -HORIZONTAL_TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
|
|
}
|
|
|
|
static QRectF horizontal_new_tab_button_shape_rect(QRectF const& rect)
|
|
{
|
|
auto x = rect.left() + (rect.width() - HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE) / 2.0;
|
|
auto y = rect.top() + (rect.height() - HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE) / 2.0;
|
|
return { x, y, HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE, HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE };
|
|
}
|
|
|
|
static QRect tab_card_shape_rect(QRect const& rect)
|
|
{
|
|
return rect.adjusted(TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
|
|
}
|
|
|
|
static QColor tab_hover_surface(QPalette const& palette, qreal hover_progress)
|
|
{
|
|
auto dark = ChromeStyle::is_dark(palette);
|
|
auto color = dark ? QColor(255, 255, 255) : QColor(0, 0, 0);
|
|
color.setAlpha(static_cast<int>((dark ? 24 : 16) * hover_progress));
|
|
return color;
|
|
}
|
|
|
|
static QColor selected_tab_border(QPalette const& palette, bool collapsed)
|
|
{
|
|
auto dark = ChromeStyle::is_dark(palette);
|
|
auto color = dark ? QColor(255, 255, 255) : QColor(0, 0, 0);
|
|
color.setAlpha(collapsed ? (dark ? 32 : 30) : (dark ? 26 : 24));
|
|
return color;
|
|
}
|
|
|
|
static QColor selected_tab_shadow(QPalette const& palette, int layer)
|
|
{
|
|
auto dark = ChromeStyle::is_dark(palette);
|
|
auto color = QColor(0, 0, 0);
|
|
if (layer == 0)
|
|
color.setAlpha(dark ? 112 : 22);
|
|
else
|
|
color.setAlpha(dark ? 50 : 10);
|
|
return color;
|
|
}
|
|
|
|
static QRectF collapsed_vertical_tab_shape_rect(QRectF const& rect)
|
|
{
|
|
return rect.adjusted(4.0, 3.0, -4.0, -3.0);
|
|
}
|
|
|
|
static QRect collapsed_vertical_tab_shape_rect(QRect const& rect)
|
|
{
|
|
return rect.adjusted(4, 3, -4, -3);
|
|
}
|
|
|
|
class NewTabButton final : public QToolButton {
|
|
public:
|
|
explicit NewTabButton(TabBar& tab_bar, QWidget* parent)
|
|
: QToolButton(parent)
|
|
, m_tab_bar(tab_bar)
|
|
{
|
|
}
|
|
|
|
private:
|
|
bool is_hovered() const
|
|
{
|
|
return rect().contains(mapFromGlobal(QCursor::pos()));
|
|
}
|
|
|
|
virtual void enterEvent(QEnterEvent* event) override
|
|
{
|
|
QToolButton::enterEvent(event);
|
|
update();
|
|
}
|
|
|
|
virtual void leaveEvent(QEvent* event) override
|
|
{
|
|
QToolButton::leaveEvent(event);
|
|
update();
|
|
}
|
|
|
|
virtual void paintEvent(QPaintEvent* event) override
|
|
{
|
|
if (!property(VERTICAL_TABS_BUTTON_PROPERTY).toBool()) {
|
|
QToolButton::paintEvent(event);
|
|
return;
|
|
}
|
|
|
|
auto expanded = property(VERTICAL_TABS_EXPANDED_PROPERTY).toBool();
|
|
|
|
QPainter painter(this);
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
auto dark = ChromeStyle::is_dark(palette());
|
|
auto is_horizontal_new_tab_button = m_tab_bar.tab_layout() == TabLayout::Horizontal;
|
|
auto shape_rect = expanded
|
|
? tab_card_shape_rect(QRectF(rect()))
|
|
: (is_horizontal_new_tab_button
|
|
? horizontal_new_tab_button_shape_rect(QRectF(rect()))
|
|
: QRectF(rect()).adjusted(4.0, 3.0, -4.0, -3.0));
|
|
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
|
|
auto hovered = is_hovered();
|
|
|
|
if (isDown()) {
|
|
painter.setBrush(ChromeStyle::chrome_surface_pressed(palette()));
|
|
painter.setPen(QPen(ChromeStyle::chrome_control_border(palette()), 1));
|
|
painter.drawPath(tab_path);
|
|
} else if (hovered) {
|
|
painter.setBrush(tab_hover_surface(palette(), 1.0));
|
|
painter.setPen(Qt::NoPen);
|
|
painter.drawPath(tab_path);
|
|
}
|
|
|
|
auto contents_rect = shape_rect.toAlignedRect().adjusted(TAB_CONTENT_HORIZONTAL_INSET, 0, -TAB_CONTENT_HORIZONTAL_INSET, 0);
|
|
auto icon_size = QSize(TAB_ICON_SIZE, TAB_ICON_SIZE);
|
|
QRect icon_rect {
|
|
expanded ? contents_rect.left() - ((icon_size.width() - TAB_ICON_SIZE) / 2) : rect().center().x() - (icon_size.width() / 2),
|
|
contents_rect.top() + ((contents_rect.height() - icon_size.height()) / 2),
|
|
icon_size.width(),
|
|
icon_size.height(),
|
|
};
|
|
if (is_horizontal_new_tab_button) {
|
|
icon_rect.translate(0, 1);
|
|
painter.translate(0.5, 0);
|
|
} else if (!expanded) {
|
|
painter.translate(1.0, 0);
|
|
}
|
|
icon().paint(&painter, icon_rect);
|
|
if (!expanded)
|
|
return;
|
|
|
|
contents_rect.setLeft(icon_rect.right() + 8);
|
|
|
|
auto text_color = ChromeStyle::mix(ChromeStyle::chrome_button_text(palette()), ChromeStyle::chrome_muted_text(palette()), dark ? 0.26 : 0.40);
|
|
if (hovered || isDown())
|
|
text_color = ChromeStyle::chrome_button_text(palette());
|
|
painter.setPen(text_color);
|
|
auto text_font = m_tab_bar.font();
|
|
painter.setFont(text_font);
|
|
|
|
QFontMetrics font_metrics(text_font);
|
|
auto title = font_metrics.elidedText(text(), Qt::ElideRight, max(0, contents_rect.width()));
|
|
contents_rect.translate(0, -1);
|
|
painter.drawText(contents_rect, Qt::AlignLeft | Qt::AlignVCenter, title);
|
|
}
|
|
|
|
TabBar& m_tab_bar;
|
|
};
|
|
|
|
TabBar::TabBar(TabWidget* tab_widget)
|
|
: QTabBar(tab_widget)
|
|
, m_tab_widget(tab_widget)
|
|
{
|
|
setMouseTracking(true);
|
|
setAcceptDrops(true);
|
|
setFocusPolicy(Qt::NoFocus);
|
|
setIconSize({ 16, 16 });
|
|
setMinimumHeight(39);
|
|
recreate_icons();
|
|
|
|
m_hover_animation = new QVariantAnimation(this);
|
|
m_hover_animation->setDuration(120);
|
|
m_hover_animation->setEasingCurve(QEasingCurve::OutCubic);
|
|
connect(m_hover_animation, &QVariantAnimation::valueChanged, this, [this](QVariant const& value) {
|
|
m_hover_progress = value.toReal();
|
|
update();
|
|
});
|
|
connect(m_hover_animation, &QVariantAnimation::finished, this, [this] {
|
|
if (m_hover_progress <= 0.0)
|
|
m_hover_animation_tab_index = -1;
|
|
});
|
|
connect(this, &QTabBar::currentChanged, this, [this](int index) {
|
|
ensure_tab_visible(index);
|
|
update_tab_button_geometry();
|
|
});
|
|
}
|
|
|
|
void TabBar::set_available_width(int width)
|
|
{
|
|
if (m_available_width != width) {
|
|
m_available_width = width;
|
|
refresh_tab_layout();
|
|
}
|
|
}
|
|
|
|
void TabBar::set_tab_layout(TabLayout tab_layout)
|
|
{
|
|
if (m_tab_layout == tab_layout)
|
|
return;
|
|
|
|
m_tab_layout = tab_layout;
|
|
|
|
if (m_tab_layout == TabLayout::Horizontal) {
|
|
setShape(QTabBar::RoundedNorth);
|
|
setMinimumSize({ 0, HORIZONTAL_TAB_HEIGHT });
|
|
setMaximumWidth(QWIDGETSIZE_MAX);
|
|
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
|
setUsesScrollButtons(true);
|
|
set_vertical_scroll_offset(0);
|
|
} else {
|
|
setShape(QTabBar::RoundedWest);
|
|
setMinimumSize({ 0, 0 });
|
|
setMaximumWidth(QWIDGETSIZE_MAX);
|
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
setUsesScrollButtons(false);
|
|
}
|
|
|
|
refresh_tab_layout();
|
|
}
|
|
|
|
void TabBar::refresh_tab_layout()
|
|
{
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset);
|
|
updateGeometry();
|
|
update_tab_button_geometry();
|
|
update();
|
|
}
|
|
|
|
void TabBar::recreate_icons()
|
|
{
|
|
m_fallback_tab_icon = create_chrome_icon(ChromeIcon::Globe, palette());
|
|
update();
|
|
}
|
|
|
|
QSize TabBar::sizeHint() const
|
|
{
|
|
if (tab_layout() != TabLayout::Horizontal)
|
|
return vertical_size_hint(count());
|
|
return QTabBar::sizeHint();
|
|
}
|
|
|
|
QSize TabBar::minimumSizeHint() const
|
|
{
|
|
if (tab_layout() != TabLayout::Horizontal)
|
|
return vertical_size_hint(count() > 0 ? 1 : 0);
|
|
return QTabBar::minimumSizeHint();
|
|
}
|
|
|
|
QSize TabBar::tabSizeHint(int index) const
|
|
{
|
|
auto hint = QTabBar::tabSizeHint(index);
|
|
|
|
if (tab_layout() != TabLayout::Horizontal) {
|
|
hint.setWidth(vertical_tab_width(m_available_width, tab_layout()));
|
|
hint.setHeight(VERTICAL_TAB_HEIGHT);
|
|
return hint;
|
|
}
|
|
|
|
if (auto count = this->count(); count > 0) {
|
|
auto width = (m_available_width > 0 ? m_available_width : this->width()) / count;
|
|
width = min(HORIZONTAL_TAB_MAX_WIDTH, width);
|
|
width = max(HORIZONTAL_TAB_MIN_WIDTH, width);
|
|
|
|
hint.setWidth(width);
|
|
}
|
|
|
|
hint.setHeight(HORIZONTAL_TAB_HEIGHT);
|
|
return hint;
|
|
}
|
|
|
|
void TabBar::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QTabBar::resizeEvent(event);
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset);
|
|
ensure_tab_visible(currentIndex());
|
|
update_tab_button_geometry();
|
|
}
|
|
|
|
void TabBar::tabLayoutChange()
|
|
{
|
|
QTabBar::tabLayoutChange();
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset);
|
|
update_tab_button_geometry();
|
|
}
|
|
|
|
void TabBar::paintEvent(QPaintEvent*)
|
|
{
|
|
QPainter painter(this);
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
auto border = ChromeStyle::chrome_border(palette());
|
|
auto dark = ChromeStyle::is_dark(palette());
|
|
auto text_color = ChromeStyle::chrome_text(palette());
|
|
auto is_vertical = tab_layout() != TabLayout::Horizontal;
|
|
auto is_collapsed_vertical = tab_layout() == TabLayout::VerticalCollapsed;
|
|
|
|
for (int index = 0; index < count(); ++index) {
|
|
auto tab_rect = visual_tab_rect(index);
|
|
if (!tab_rect.isValid())
|
|
continue;
|
|
if (is_vertical && (tab_rect.bottom() < 0 || tab_rect.top() > height()))
|
|
continue;
|
|
if (!is_vertical && (tab_rect.right() < 0 || tab_rect.left() > width()))
|
|
continue;
|
|
|
|
bool is_selected = index == currentIndex();
|
|
auto hover_progress = index == m_hover_animation_tab_index ? m_hover_progress : (index == m_hovered_tab_index ? 1.0 : 0.0);
|
|
bool is_hovered = hover_progress > 0.0;
|
|
|
|
QRectF shape_rect;
|
|
if (is_collapsed_vertical)
|
|
shape_rect = collapsed_vertical_tab_shape_rect(QRectF(tab_rect));
|
|
else if (m_tab_layout == TabLayout::Horizontal)
|
|
shape_rect = horizontal_tab_card_shape_rect(QRectF(tab_rect));
|
|
else
|
|
shape_rect = tab_card_shape_rect(QRectF(tab_rect));
|
|
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
|
|
|
|
if (is_selected) {
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(selected_tab_shadow(palette(), 1));
|
|
painter.drawPath(tab_path.translated(0, 2));
|
|
painter.setBrush(selected_tab_shadow(palette(), 0));
|
|
painter.drawPath(tab_path.translated(0, 1));
|
|
|
|
auto selected_gradient = QLinearGradient(shape_rect.topLeft(), shape_rect.bottomLeft());
|
|
selected_gradient.setColorAt(0.0, ChromeStyle::chrome_active_tab_surface_top(palette()));
|
|
selected_gradient.setColorAt(1.0, ChromeStyle::chrome_active_tab_surface_bottom(palette()));
|
|
painter.setBrush(selected_gradient);
|
|
painter.setPen(QPen(selected_tab_border(palette(), is_collapsed_vertical), 1));
|
|
painter.drawPath(tab_path);
|
|
} else if (is_hovered) {
|
|
painter.setBrush(tab_hover_surface(palette(), hover_progress));
|
|
painter.setPen(Qt::NoPen);
|
|
painter.drawPath(tab_path);
|
|
}
|
|
|
|
if (!is_selected && !is_hovered && index > 0 && index != currentIndex() + 1 && !is_collapsed_vertical) {
|
|
auto separator = border;
|
|
separator.setAlpha(dark ? 24 : 20);
|
|
painter.setPen(separator);
|
|
if (is_vertical)
|
|
painter.drawLine(QPoint(tab_rect.left() + 16, tab_rect.top()), QPoint(tab_rect.right() - 16, tab_rect.top()));
|
|
else
|
|
painter.drawLine(QPoint(tab_rect.left(), 15), QPoint(tab_rect.left(), height() - 15));
|
|
}
|
|
|
|
auto icon = tabIcon(index);
|
|
if (icon.isNull())
|
|
icon = m_fallback_tab_icon;
|
|
|
|
if (is_collapsed_vertical) {
|
|
QRect icon_rect {
|
|
tab_rect.center().x() - (TAB_ICON_SIZE / 2) + 1,
|
|
tab_rect.center().y() - (TAB_ICON_SIZE / 2) + 1,
|
|
TAB_ICON_SIZE,
|
|
TAB_ICON_SIZE,
|
|
};
|
|
if (!is_selected)
|
|
painter.setOpacity(is_hovered ? 0.92 : (dark ? 0.88 : 0.84));
|
|
icon.paint(&painter, icon_rect);
|
|
painter.setOpacity(1.0);
|
|
continue;
|
|
}
|
|
|
|
auto contents_rect = shape_rect.toAlignedRect().adjusted(TAB_CONTENT_HORIZONTAL_INSET, 0, -TAB_CONTENT_HORIZONTAL_INSET, 0);
|
|
if (auto* left_button = tabButton(index, QTabBar::LeftSide); left_button && left_button->isVisible())
|
|
contents_rect.setLeft(max(contents_rect.left(), left_button->geometry().right() + 6));
|
|
if (auto* right_button = tabButton(index, QTabBar::RightSide); right_button && right_button->isVisible())
|
|
contents_rect.setRight(min(contents_rect.right(), right_button->geometry().left() - 6));
|
|
|
|
if (contents_rect.width() > 26) {
|
|
QRect icon_rect {
|
|
contents_rect.left(),
|
|
contents_rect.top() + ((contents_rect.height() - TAB_ICON_SIZE) / 2),
|
|
TAB_ICON_SIZE,
|
|
TAB_ICON_SIZE,
|
|
};
|
|
if (!is_selected)
|
|
painter.setOpacity(is_hovered ? 0.92 : (dark ? 0.88 : 0.84));
|
|
icon.paint(&painter, icon_rect);
|
|
painter.setOpacity(1.0);
|
|
contents_rect.setLeft(icon_rect.right() + 8);
|
|
}
|
|
|
|
QFont tab_font = font();
|
|
if (is_selected)
|
|
tab_font.setWeight(QFont::DemiBold);
|
|
painter.setFont(tab_font);
|
|
|
|
QFontMetrics font_metrics(tab_font);
|
|
auto title = font_metrics.elidedText(tabText(index), Qt::ElideRight, max(0, contents_rect.width()));
|
|
auto tab_text_color = text_color;
|
|
if (!is_selected)
|
|
tab_text_color.setAlpha(is_hovered ? (dark ? 236 : 228) : (dark ? 226 : 216));
|
|
painter.setPen(tab_text_color);
|
|
painter.drawText(contents_rect, Qt::AlignLeft | Qt::AlignVCenter, title);
|
|
}
|
|
|
|
if (m_drop_indicator_index >= 0 && count() > 0) {
|
|
auto indicator_color = ChromeStyle::chrome_accent(palette());
|
|
indicator_color.setAlpha(220);
|
|
painter.setPen(QPen(indicator_color, 3, Qt::SolidLine, Qt::RoundCap));
|
|
|
|
if (is_vertical) {
|
|
auto indicator_y = m_drop_indicator_index >= count()
|
|
? visual_tab_rect(count() - 1).bottom() + 3
|
|
: visual_tab_rect(m_drop_indicator_index).top() + 1;
|
|
indicator_y = max(3, min(height() - 3, indicator_y));
|
|
painter.drawLine(QPointF(10, indicator_y), QPointF(width() - 10, indicator_y));
|
|
return;
|
|
}
|
|
|
|
auto indicator_x = m_drop_indicator_index >= count()
|
|
? visual_tab_rect(count() - 1).right() + 3
|
|
: visual_tab_rect(m_drop_indicator_index).left() + 1;
|
|
indicator_x = max(2, min(width() - 3, indicator_x));
|
|
|
|
painter.drawLine(QPointF(indicator_x, 8), QPointF(indicator_x, height() - 6));
|
|
}
|
|
}
|
|
|
|
void TabBar::contextMenuEvent(QContextMenuEvent* event)
|
|
{
|
|
if (!m_tab_widget)
|
|
return;
|
|
|
|
auto tab_index = tab_index_at(event->pos());
|
|
if (tab_index < 0)
|
|
return;
|
|
|
|
if (auto* tab = m_tab_widget->tab(tab_index))
|
|
tab->context_menu()->exec(event->globalPos());
|
|
}
|
|
|
|
void TabBar::dragEnterEvent(QDragEnterEvent* event)
|
|
{
|
|
if (!m_tab_widget || !s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
event->accept();
|
|
}
|
|
|
|
void TabBar::dragLeaveEvent(QDragLeaveEvent* event)
|
|
{
|
|
m_drop_indicator_index = -1;
|
|
update();
|
|
QTabBar::dragLeaveEvent(event);
|
|
}
|
|
|
|
void TabBar::dragMoveEvent(QDragMoveEvent* event)
|
|
{
|
|
if (!m_tab_widget || !s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
auto drop_indicator_index = drop_indicator_index_for_insertion_index(insertion_index_at(event->position().toPoint()));
|
|
if (m_drop_indicator_index != drop_indicator_index) {
|
|
m_drop_indicator_index = drop_indicator_index;
|
|
update();
|
|
}
|
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
event->accept();
|
|
}
|
|
|
|
void TabBar::dropEvent(QDropEvent* event)
|
|
{
|
|
if (!m_tab_widget || !s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
s_pending_tab_drop_target = m_tab_widget;
|
|
s_pending_tab_drop_index = insertion_index_at(event->position().toPoint());
|
|
m_drop_indicator_index = -1;
|
|
update();
|
|
event->setDropAction(Qt::MoveAction);
|
|
event->accept();
|
|
}
|
|
|
|
bool TabBar::eventFilter(QObject* watched, QEvent* event)
|
|
{
|
|
auto hovered_tab_for_button = [this](QObject* button) {
|
|
for (int index = 0; index < count(); ++index) {
|
|
if (tabButton(index, QTabBar::LeftSide) == button || tabButton(index, QTabBar::RightSide) == button)
|
|
return index;
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
if (auto hovered_tab = hovered_tab_for_button(watched); hovered_tab >= 0) {
|
|
if (event->type() == QEvent::Enter || event->type() == QEvent::MouseMove) {
|
|
set_hovered_tab_index(hovered_tab);
|
|
} else if (event->type() == QEvent::Leave) {
|
|
auto cursor_position = mapFromGlobal(QCursor::pos());
|
|
set_hovered_tab_index(tab_index_at(cursor_position));
|
|
}
|
|
}
|
|
|
|
return QTabBar::eventFilter(watched, event);
|
|
}
|
|
|
|
void TabBar::mousePressEvent(QMouseEvent* event)
|
|
{
|
|
auto pressed_tab = tab_index_at(event->pos());
|
|
m_pressed_tab = (m_tab_widget && pressed_tab >= 0) ? m_tab_widget->tab(pressed_tab) : nullptr;
|
|
|
|
if (pressed_tab >= 0) {
|
|
auto rect_of_current_tab = visual_tab_rect(pressed_tab);
|
|
m_position_in_selected_tab_while_dragging = event->pos() - rect_of_current_tab.topLeft();
|
|
m_drag_start_position = event->pos();
|
|
}
|
|
|
|
if (tab_layout() != TabLayout::Horizontal) {
|
|
if (pressed_tab >= 0) {
|
|
setCurrentIndex(pressed_tab);
|
|
} else if (event->button() == Qt::LeftButton && start_window_move()) {
|
|
event->accept();
|
|
return;
|
|
}
|
|
} else {
|
|
if (pressed_tab < 0 && event->button() == Qt::LeftButton && start_window_move()) {
|
|
event->accept();
|
|
return;
|
|
}
|
|
QTabBar::mousePressEvent(event);
|
|
}
|
|
|
|
if (m_pressed_tab)
|
|
event->accept();
|
|
else
|
|
event->ignore();
|
|
}
|
|
|
|
void TabBar::mouseMoveEvent(QMouseEvent* event)
|
|
{
|
|
set_hovered_tab_index(tab_index_at(event->pos()));
|
|
|
|
if (count() == 0) {
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
QTabBar::mouseMoveEvent(event);
|
|
return;
|
|
}
|
|
|
|
if (m_pressed_tab && event->buttons().testFlag(Qt::LeftButton)) {
|
|
auto pressed_tab_index = m_tab_widget ? m_tab_widget->index_of(m_pressed_tab) : -1;
|
|
if (pressed_tab_index < 0) {
|
|
m_pressed_tab = nullptr;
|
|
} else if ((event->pos() - m_drag_start_position).manhattanLength() >= QApplication::startDragDistance()) {
|
|
start_tab_drag(pressed_tab_index);
|
|
event->accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_pressed_tab)
|
|
event->accept();
|
|
else
|
|
event->ignore();
|
|
}
|
|
|
|
void TabBar::leaveEvent(QEvent* event)
|
|
{
|
|
set_hovered_tab_index(-1);
|
|
QTabBar::leaveEvent(event);
|
|
}
|
|
|
|
void TabBar::mouseReleaseEvent(QMouseEvent* event)
|
|
{
|
|
auto pressed_tab = m_pressed_tab;
|
|
m_pressed_tab = nullptr;
|
|
|
|
if (event->button() == Qt::MiddleButton && pressed_tab && m_tab_widget) {
|
|
if (auto index = m_tab_widget->index_of(pressed_tab); index >= 0 && tab_index_at(event->pos()) == index) {
|
|
emit tabCloseRequested(index);
|
|
event->accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
QTabBar::mouseReleaseEvent(event);
|
|
|
|
if (pressed_tab)
|
|
event->accept();
|
|
}
|
|
|
|
void TabBar::mouseDoubleClickEvent(QMouseEvent* event)
|
|
{
|
|
if (window_uses_client_side_decorations(*this) && tab_index_at(event->pos()) < 0 && event->button() == Qt::LeftButton) {
|
|
toggle_window_maximized();
|
|
event->accept();
|
|
return;
|
|
}
|
|
|
|
QTabBar::mouseDoubleClickEvent(event);
|
|
}
|
|
|
|
void TabBar::wheelEvent(QWheelEvent* event)
|
|
{
|
|
if (tab_layout() == TabLayout::Horizontal || max_vertical_scroll_offset() <= 0) {
|
|
QTabBar::wheelEvent(event);
|
|
return;
|
|
}
|
|
|
|
auto old_offset = m_vertical_scroll_offset;
|
|
auto pixel_delta = event->pixelDelta().y();
|
|
auto angle_delta = event->angleDelta().y();
|
|
|
|
if (pixel_delta != 0) {
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset - pixel_delta);
|
|
} else if (angle_delta != 0) {
|
|
auto scroll_delta = angle_delta * VERTICAL_TAB_HEIGHT / 120;
|
|
if (scroll_delta == 0)
|
|
scroll_delta = angle_delta > 0 ? 1 : -1;
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset - scroll_delta);
|
|
}
|
|
|
|
if (m_vertical_scroll_offset != old_offset)
|
|
event->accept();
|
|
else
|
|
event->ignore();
|
|
}
|
|
|
|
int TabBar::insertion_index_at(QPoint const& position) const
|
|
{
|
|
if (count() == 0)
|
|
return 0;
|
|
|
|
if (tab_layout() != TabLayout::Horizontal) {
|
|
auto unscrolled_y = position.y() + m_vertical_scroll_offset;
|
|
if (unscrolled_y <= 0)
|
|
return 0;
|
|
if (unscrolled_y >= count() * VERTICAL_TAB_HEIGHT)
|
|
return count();
|
|
|
|
auto index = unscrolled_y / VERTICAL_TAB_HEIGHT;
|
|
if (unscrolled_y > (index * VERTICAL_TAB_HEIGHT) + (VERTICAL_TAB_HEIGHT / 2))
|
|
return index + 1;
|
|
return index;
|
|
}
|
|
|
|
auto index = tab_index_at(position);
|
|
if (index < 0)
|
|
return position.x() < visual_tab_rect(0).center().x() ? 0 : count();
|
|
|
|
if (position.x() > visual_tab_rect(index).center().x())
|
|
return index + 1;
|
|
return index;
|
|
}
|
|
|
|
int TabBar::drop_indicator_index_for_insertion_index(int insertion_index) const
|
|
{
|
|
if (s_active_tab_drag_source == m_tab_widget && s_active_tab_dragged_tab) {
|
|
auto dragged_tab_index = m_tab_widget->index_of(s_active_tab_dragged_tab);
|
|
if (dragged_tab_index >= 0 && (insertion_index == dragged_tab_index || insertion_index == dragged_tab_index + 1))
|
|
return -1;
|
|
}
|
|
|
|
return insertion_index;
|
|
}
|
|
|
|
QPixmap TabBar::render_tab_drag_pixmap(int index) const
|
|
{
|
|
auto tab_rect = visual_tab_rect(index);
|
|
QPixmap pixmap(tab_rect.size() * devicePixelRatioF());
|
|
pixmap.setDevicePixelRatio(devicePixelRatioF());
|
|
pixmap.fill(Qt::transparent);
|
|
|
|
QPainter painter(&pixmap);
|
|
painter.setOpacity(0.75);
|
|
const_cast<TabBar&>(*this).render(&painter, QPoint(), QRegion(tab_rect), QWidget::DrawChildren);
|
|
return pixmap;
|
|
}
|
|
|
|
void TabBar::start_tab_drag(int index)
|
|
{
|
|
if (!m_tab_widget)
|
|
return;
|
|
|
|
auto* tab = m_tab_widget->tab(index);
|
|
if (!tab)
|
|
return;
|
|
|
|
QPointer<Tab> dragged_tab = tab;
|
|
auto* drag = new QDrag(this);
|
|
auto* mime_data = new QMimeData;
|
|
mime_data->setData(LADYBIRD_TAB_MIME_TYPE, QByteArray {});
|
|
mime_data->setText(tab->title());
|
|
drag->setMimeData(mime_data);
|
|
drag->setPixmap(render_tab_drag_pixmap(index));
|
|
drag->setHotSpot(m_position_in_selected_tab_while_dragging);
|
|
|
|
s_active_tab_drag_source = m_tab_widget;
|
|
s_active_tab_dragged_tab = dragged_tab;
|
|
s_pending_tab_drop_target = nullptr;
|
|
s_pending_tab_drop_index = -1;
|
|
|
|
auto action = drag->exec(Qt::MoveAction, Qt::MoveAction);
|
|
|
|
auto* source_window = qobject_cast<BrowserWindow*>(window());
|
|
if (source_window && dragged_tab) {
|
|
auto current_index = source_window->tab_index(dragged_tab);
|
|
if (current_index >= 0) {
|
|
if (action == Qt::MoveAction && s_pending_tab_drop_target) {
|
|
if (auto* target_window = qobject_cast<BrowserWindow*>(s_pending_tab_drop_target->window()))
|
|
source_window->move_tab_to_window(current_index, *target_window, s_pending_tab_drop_index);
|
|
} else if (action == Qt::IgnoreAction) {
|
|
if (!m_tab_widget->tab_strip_global_rect().contains(QCursor::pos()) && m_tab_widget->count() > 1)
|
|
source_window->detach_tab_to_new_window(current_index, QCursor::pos());
|
|
}
|
|
}
|
|
}
|
|
|
|
s_active_tab_drag_source = nullptr;
|
|
s_active_tab_dragged_tab = nullptr;
|
|
s_pending_tab_drop_target = nullptr;
|
|
s_pending_tab_drop_index = -1;
|
|
m_pressed_tab = nullptr;
|
|
}
|
|
|
|
void TabBar::start_hover_animation(int tab_index, qreal target_progress)
|
|
{
|
|
if (!m_hover_animation)
|
|
return;
|
|
|
|
if (tab_index < 0) {
|
|
m_hover_animation_tab_index = -1;
|
|
m_hover_progress = 0.0;
|
|
update();
|
|
return;
|
|
}
|
|
|
|
m_hover_animation->stop();
|
|
auto start_progress = (m_hover_animation_tab_index == tab_index) ? m_hover_progress : 0.0;
|
|
m_hover_animation_tab_index = tab_index;
|
|
m_hover_animation->setStartValue(start_progress);
|
|
m_hover_animation->setEndValue(target_progress);
|
|
m_hover_animation->start();
|
|
}
|
|
|
|
QRect TabBar::visual_tab_rect(int index) const
|
|
{
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
return tabRect(index);
|
|
|
|
if (index < 0 || index >= count())
|
|
return {};
|
|
|
|
auto tab_width = width();
|
|
if (tab_width <= 0)
|
|
tab_width = vertical_size_hint(count()).width();
|
|
|
|
return { 0, (index * VERTICAL_TAB_HEIGHT) - m_vertical_scroll_offset, tab_width, VERTICAL_TAB_HEIGHT };
|
|
}
|
|
|
|
int TabBar::tab_index_at(QPoint const& position) const
|
|
{
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
return tabAt(position);
|
|
|
|
if (position.x() < 0 || position.x() >= width() || position.y() < 0 || position.y() >= height())
|
|
return -1;
|
|
|
|
auto index = (position.y() + m_vertical_scroll_offset) / VERTICAL_TAB_HEIGHT;
|
|
if (index < 0 || index >= count())
|
|
return -1;
|
|
|
|
if (!visual_tab_rect(index).contains(position))
|
|
return -1;
|
|
|
|
return index;
|
|
}
|
|
|
|
QSize TabBar::vertical_size_hint(int tab_count) const
|
|
{
|
|
return { vertical_tab_width(m_available_width, tab_layout()), tab_count * VERTICAL_TAB_HEIGHT };
|
|
}
|
|
|
|
int TabBar::max_vertical_scroll_offset() const
|
|
{
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
return 0;
|
|
return max(0, (count() * VERTICAL_TAB_HEIGHT) - height());
|
|
}
|
|
|
|
void TabBar::set_vertical_scroll_offset(int offset)
|
|
{
|
|
auto clamped_offset = max(0, min(offset, max_vertical_scroll_offset()));
|
|
if (m_vertical_scroll_offset == clamped_offset)
|
|
return;
|
|
|
|
m_vertical_scroll_offset = clamped_offset;
|
|
update_tab_button_geometry();
|
|
update();
|
|
}
|
|
|
|
void TabBar::ensure_tab_visible(int index)
|
|
{
|
|
if (tab_layout() == TabLayout::Horizontal || index < 0 || index >= count())
|
|
return;
|
|
|
|
auto tab_top = index * VERTICAL_TAB_HEIGHT;
|
|
auto tab_bottom = tab_top + VERTICAL_TAB_HEIGHT;
|
|
|
|
if (tab_top < m_vertical_scroll_offset)
|
|
set_vertical_scroll_offset(tab_top);
|
|
else if (tab_bottom > m_vertical_scroll_offset + height())
|
|
set_vertical_scroll_offset(tab_bottom - height());
|
|
}
|
|
|
|
void TabBar::set_hovered_tab_index(int index)
|
|
{
|
|
if (m_hovered_tab_index == index)
|
|
return;
|
|
|
|
auto previous_hovered_tab = m_hovered_tab_index;
|
|
m_hovered_tab_index = index;
|
|
update_tab_button_geometry();
|
|
start_hover_animation(index >= 0 ? index : previous_hovered_tab, index >= 0 ? 1.0 : 0.0);
|
|
update();
|
|
}
|
|
|
|
void TabBar::update_tab_button_geometry()
|
|
{
|
|
auto set_button_collapsed_overlay = [](QWidget* button, bool enabled) {
|
|
if (auto* tab_bar_button = qobject_cast<TabBarButton*>(button))
|
|
tab_bar_button->set_collapsed_vertical_overlay(enabled);
|
|
};
|
|
|
|
auto prepare_button = [&](QWidget* button, bool collapsed_overlay) {
|
|
if (!button)
|
|
return;
|
|
button->installEventFilter(this);
|
|
set_button_collapsed_overlay(button, collapsed_overlay);
|
|
};
|
|
|
|
if (tab_layout() == TabLayout::Horizontal) {
|
|
for (int index = 0; index < count(); ++index) {
|
|
prepare_button(tabButton(index, QTabBar::LeftSide), false);
|
|
prepare_button(tabButton(index, QTabBar::RightSide), false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Keep this out of paintEvent(): setVisible(), setGeometry(), and raise()
|
|
// dirty child widgets, and doing that while painting can schedule another
|
|
// tab bar repaint before the current one has finished flushing.
|
|
auto place_expanded_button = [&](int index, QTabBar::ButtonPosition position, QRect shape_rect) {
|
|
auto* button = tabButton(index, position);
|
|
if (!button)
|
|
return;
|
|
prepare_button(button, false);
|
|
|
|
auto should_be_visible = position != QTabBar::RightSide || index == currentIndex() || index == m_hovered_tab_index;
|
|
bool did_update_button = false;
|
|
if (button->isVisible() != should_be_visible) {
|
|
button->setVisible(should_be_visible);
|
|
did_update_button = true;
|
|
}
|
|
|
|
auto button_size = button->size();
|
|
if (button_size.isEmpty())
|
|
button_size = button->sizeHint();
|
|
if (button_size.isEmpty())
|
|
return;
|
|
|
|
auto x = position == QTabBar::RightSide ? shape_rect.right() - button_size.width() - 6 : shape_rect.left() + 6;
|
|
auto y = shape_rect.top() + ((shape_rect.height() - button_size.height()) / 2);
|
|
QRect button_geometry { { x, y }, button_size };
|
|
if (button->geometry() != button_geometry) {
|
|
button->setGeometry(button_geometry);
|
|
did_update_button = true;
|
|
}
|
|
|
|
if (did_update_button)
|
|
button->raise();
|
|
};
|
|
|
|
auto place_collapsed_button = [&](int index, QTabBar::ButtonPosition position, QRect tab_rect, QRect shape_rect) {
|
|
auto* button = tabButton(index, position);
|
|
if (!button)
|
|
return;
|
|
prepare_button(button, true);
|
|
|
|
auto should_be_visible = position == QTabBar::LeftSide || index == m_hovered_tab_index;
|
|
bool did_update_button = false;
|
|
if (button->isVisible() != should_be_visible) {
|
|
button->setVisible(should_be_visible);
|
|
did_update_button = true;
|
|
}
|
|
|
|
auto button_size = button->size();
|
|
if (button_size.isEmpty())
|
|
button_size = button->sizeHint();
|
|
if (button_size.isEmpty())
|
|
return;
|
|
|
|
QPoint button_position;
|
|
if (position == QTabBar::RightSide) {
|
|
button_position = {
|
|
max(tab_rect.left(), shape_rect.left() - 5),
|
|
max(tab_rect.top(), shape_rect.top() - 5),
|
|
};
|
|
} else {
|
|
auto x = min(tab_rect.right() - button_size.width() + 1, shape_rect.right() - button_size.width() + 5);
|
|
auto y = min(tab_rect.bottom() - button_size.height() + 1, shape_rect.bottom() - button_size.height() + 5);
|
|
button_position = { x, y };
|
|
}
|
|
|
|
QRect button_geometry { button_position, button_size };
|
|
if (button->geometry() != button_geometry) {
|
|
button->setGeometry(button_geometry);
|
|
did_update_button = true;
|
|
}
|
|
|
|
if (did_update_button)
|
|
button->raise();
|
|
};
|
|
|
|
for (int index = 0; index < count(); ++index) {
|
|
auto tab_rect = visual_tab_rect(index);
|
|
if (!tab_rect.isValid())
|
|
continue;
|
|
|
|
if (tab_layout() == TabLayout::VerticalCollapsed) {
|
|
auto shape_rect = collapsed_vertical_tab_shape_rect(tab_rect);
|
|
place_collapsed_button(index, QTabBar::LeftSide, tab_rect, shape_rect);
|
|
place_collapsed_button(index, QTabBar::RightSide, tab_rect, shape_rect);
|
|
} else {
|
|
auto shape_rect = tab_card_shape_rect(tab_rect);
|
|
place_expanded_button(index, QTabBar::LeftSide, shape_rect);
|
|
place_expanded_button(index, QTabBar::RightSide, shape_rect);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TabBar::toggle_window_maximized()
|
|
{
|
|
auto* top_level_window = window();
|
|
if (top_level_window->isMaximized())
|
|
top_level_window->showNormal();
|
|
else
|
|
top_level_window->showMaximized();
|
|
}
|
|
|
|
bool TabBar::start_window_move()
|
|
{
|
|
auto* handle = window()->windowHandle();
|
|
if (!handle)
|
|
return false;
|
|
#if defined(AK_OS_MACOS)
|
|
if (start_appkit_window_drag(*this))
|
|
return true;
|
|
#endif
|
|
return handle->startSystemMove();
|
|
}
|
|
|
|
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);
|
|
|
|
m_tab_bar = new TabBar(this);
|
|
setAcceptDrops(true);
|
|
|
|
m_tab_bar->setDocumentMode(true);
|
|
m_tab_bar->setElideMode(Qt::TextElideMode::ElideRight);
|
|
m_tab_bar->setMovable(false);
|
|
m_tab_bar->setTabsClosable(true);
|
|
m_tab_bar->setExpanding(false);
|
|
m_tab_bar->setUsesScrollButtons(true);
|
|
m_tab_bar->setDrawBase(false);
|
|
m_tab_bar->installEventFilter(this);
|
|
|
|
m_toolbar_container = new QStackedWidget(this);
|
|
m_toolbar_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
|
m_toolbar_container->installEventFilter(this);
|
|
m_stacked_widget = new QStackedWidget(this);
|
|
m_page_column = new QWidget(this);
|
|
m_page_column_layout = new QVBoxLayout(m_page_column);
|
|
m_page_column_layout->setSpacing(0);
|
|
m_page_column_layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
m_new_tab_button = new NewTabButton(*m_tab_bar, this);
|
|
m_new_tab_button->setObjectName("LadybirdNewTabButton");
|
|
m_new_tab_button->setIconSize(QSize(18, 18));
|
|
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);
|
|
|
|
auto window_control_buttons = create_window_control_buttons(*this, "LadybirdTabStripWindowControls", { 18, 18 }, { 40, 40 });
|
|
m_window_controls = window_control_buttons.container;
|
|
m_minimize_window_button = window_control_buttons.minimize;
|
|
m_maximize_window_button = window_control_buttons.maximize;
|
|
m_close_window_button = window_control_buttons.close;
|
|
|
|
recreate_icons();
|
|
|
|
m_tab_bar_row = new QWidget(this);
|
|
m_tab_bar_row->setObjectName("LadybirdTabStrip");
|
|
m_tab_bar_row_layout = new QHBoxLayout(m_tab_bar_row);
|
|
m_tab_bar_row->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_content->installEventFilter(this);
|
|
|
|
m_vertical_tab_bar_column = new QWidget(this);
|
|
m_vertical_tab_bar_column->setObjectName("LadybirdVerticalTabBar");
|
|
#if defined(AK_OS_MACOS)
|
|
m_vertical_tab_bar_column->setAttribute(Qt::WA_NativeWindow);
|
|
#endif
|
|
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_separator = new QWidget(this);
|
|
m_vertical_tabs_content_separator->setObjectName("LadybirdVerticalTabsContentSeparator");
|
|
m_vertical_tabs_content_separator->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
m_vertical_tabs_content_separator->hide();
|
|
|
|
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(this);
|
|
m_vertical_tabs_resize_handle->setObjectName("LadybirdVerticalTabsResizeHandle");
|
|
#if defined(AK_OS_MACOS)
|
|
m_vertical_tabs_resize_handle->setAttribute(Qt::WA_NativeWindow);
|
|
#endif
|
|
m_vertical_tabs_resize_handle->setCursor(Qt::SizeHorCursor);
|
|
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);
|
|
rebuild_layout();
|
|
update_chrome_style();
|
|
|
|
connect(m_tab_bar, &QTabBar::currentChanged, this, [this](int index) {
|
|
if (index >= 0 && index < m_stacked_widget->count())
|
|
m_stacked_widget->setCurrentIndex(index);
|
|
|
|
emit current_tab_changed(index);
|
|
m_toolbar_container->setCurrentIndex(index);
|
|
update_vertical_tabs_overlay_geometry();
|
|
update_vertical_tabs_resize_handle();
|
|
});
|
|
|
|
connect(m_tab_bar, &QTabBar::tabCloseRequested, this, &TabWidget::tab_close_requested);
|
|
|
|
connect(m_tab_bar, &QTabBar::tabMoved, this, [this](int from, int to) {
|
|
ScopeGuard guard { [&]() { m_stacked_widget->blockSignals(false); } };
|
|
m_stacked_widget->blockSignals(true);
|
|
|
|
auto* widget = m_stacked_widget->widget(from);
|
|
m_stacked_widget->removeWidget(widget);
|
|
m_stacked_widget->insertWidget(to, widget);
|
|
|
|
auto* toolbar = as<Tab>(widget)->toolbar_container();
|
|
if (auto toolbar_index = m_toolbar_container->indexOf(toolbar); toolbar_index != -1) {
|
|
m_toolbar_container->removeWidget(toolbar);
|
|
m_toolbar_container->insertWidget(to, toolbar);
|
|
}
|
|
m_stacked_widget->setCurrentIndex(m_tab_bar->currentIndex());
|
|
});
|
|
|
|
connect(m_minimize_window_button, &QToolButton::clicked, this, [this] {
|
|
window()->showMinimized();
|
|
});
|
|
connect(m_maximize_window_button, &QToolButton::clicked, this, [this] {
|
|
toggle_window_maximized();
|
|
});
|
|
connect(m_close_window_button, &QToolButton::clicked, this, [this] {
|
|
window()->close();
|
|
});
|
|
}
|
|
|
|
void TabWidget::add_tab(Tab* widget, QString const& label)
|
|
{
|
|
insert_tab(m_tab_bar->count(), widget, label);
|
|
}
|
|
|
|
void TabWidget::insert_tab(int index, Tab* widget, QString const& label)
|
|
{
|
|
m_stacked_widget->insertWidget(index, widget);
|
|
m_tab_bar->insertTab(index, label);
|
|
widget->set_toolbar_container_in_tab_layout(false);
|
|
m_toolbar_container->insertWidget(index, widget->toolbar_container());
|
|
|
|
widget->set_vertical_tabs_enabled(m_vertical_tabs_enabled);
|
|
|
|
update_toolbar_placement();
|
|
update_tab_layout();
|
|
update_tab_button_visibility();
|
|
}
|
|
|
|
void TabWidget::remove_tab(int index)
|
|
{
|
|
take_tab(index);
|
|
}
|
|
|
|
Tab* TabWidget::take_tab(int index)
|
|
{
|
|
auto* widget = m_stacked_widget->widget(index);
|
|
if (!widget)
|
|
return nullptr;
|
|
|
|
m_stacked_widget->removeWidget(widget);
|
|
auto* toolbar = as<Tab>(widget)->toolbar_container();
|
|
if (m_toolbar_container->indexOf(toolbar) != -1)
|
|
m_toolbar_container->removeWidget(toolbar);
|
|
as<Tab>(widget)->set_toolbar_container_in_tab_layout(true);
|
|
|
|
m_tab_bar->removeTab(index);
|
|
|
|
if (m_tab_bar->count() > 0 && m_tab_bar->currentIndex() >= 0)
|
|
m_stacked_widget->setCurrentIndex(m_tab_bar->currentIndex());
|
|
|
|
update_tab_layout();
|
|
update_tab_button_visibility();
|
|
return as<Tab>(widget);
|
|
}
|
|
|
|
void TabWidget::set_current_tab(Tab* widget)
|
|
{
|
|
if (auto index = m_stacked_widget->indexOf(widget); index >= 0)
|
|
set_current_index(index);
|
|
}
|
|
|
|
int TabWidget::index_of(Tab* widget) const
|
|
{
|
|
return m_stacked_widget->indexOf(widget);
|
|
}
|
|
|
|
void TabWidget::set_new_tab_action(QAction* action)
|
|
{
|
|
disconnect(m_new_tab_button, &QToolButton::clicked, nullptr, nullptr);
|
|
|
|
if (!action)
|
|
return;
|
|
|
|
connect(m_new_tab_button, &QToolButton::clicked, action, &QAction::trigger);
|
|
}
|
|
|
|
void TabWidget::set_tab_bar_visible(bool visible)
|
|
{
|
|
if (m_tab_bar_visible == visible)
|
|
return;
|
|
|
|
m_tab_bar_visible = visible;
|
|
update_tab_chrome_visibility();
|
|
update_tab_layout();
|
|
}
|
|
|
|
void TabWidget::set_window_controls_visible(bool visible)
|
|
{
|
|
m_window_controls_visible = visible;
|
|
update_tab_toolbar_window_controls_visibility();
|
|
update_tab_chrome_visibility();
|
|
update_tab_layout();
|
|
}
|
|
|
|
void TabWidget::set_vertical_tabs_enabled(bool enabled)
|
|
{
|
|
if (m_vertical_tabs_enabled == 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_vertical_tabs_enabled(enabled);
|
|
}
|
|
rebuild_layout();
|
|
}
|
|
|
|
void TabWidget::set_vertical_tabs_expanded(bool expanded)
|
|
{
|
|
if (m_vertical_tabs_expanded == 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();
|
|
}
|
|
|
|
bool TabWidget::event(QEvent* event)
|
|
{
|
|
if (auto type = event->type(); type == QEvent::PaletteChange) {
|
|
recreate_icons();
|
|
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);
|
|
}
|
|
|
|
void TabWidget::dragEnterEvent(QDragEnterEvent* event)
|
|
{
|
|
accept_tab_drag(event);
|
|
}
|
|
|
|
void TabWidget::dragMoveEvent(QDragMoveEvent* event)
|
|
{
|
|
accept_tab_drag(event);
|
|
}
|
|
|
|
void TabWidget::dropEvent(QDropEvent* event)
|
|
{
|
|
accept_tab_drop(event, m_tab_bar->count());
|
|
}
|
|
|
|
bool TabWidget::eventFilter(QObject* watched, QEvent* event)
|
|
{
|
|
if (watched == window() && event->type() == QEvent::Leave)
|
|
defer_update_vertical_tabs_hover_expanded();
|
|
|
|
if (watched == m_vertical_tabs_resize_handle) {
|
|
auto reset_resize_handle = [this] {
|
|
m_is_resizing_vertical_tabs = false;
|
|
m_vertical_tabs_resize_handle->releaseMouse();
|
|
QApplication::restoreOverrideCursor();
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false);
|
|
};
|
|
|
|
if (event->type() == QEvent::Enter) {
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, true);
|
|
} else if (event->type() == QEvent::Leave) {
|
|
if (!m_is_resizing_vertical_tabs)
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, false);
|
|
} else if (event->type() == QEvent::MouseButtonDblClick) {
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
if (mouse_event->button() == Qt::LeftButton && m_vertical_tabs_expanded) {
|
|
apply_vertical_tabs_expanded_width(VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH);
|
|
persist_vertical_tabs_expanded_width();
|
|
return true;
|
|
}
|
|
} else if (event->type() == QEvent::MouseButtonPress) {
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
if (mouse_event->button() != Qt::LeftButton)
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
m_is_resizing_vertical_tabs = true;
|
|
m_vertical_tabs_resize_start_global_x = mouse_event->globalPosition().toPoint().x();
|
|
m_vertical_tabs_resize_start_width = m_vertical_tabs_expanded_width;
|
|
m_vertical_tabs_resize_handle->grabMouse();
|
|
QApplication::setOverrideCursor(Qt::SizeHorCursor);
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, true);
|
|
return true;
|
|
} else if (event->type() == QEvent::MouseMove) {
|
|
if (!m_is_resizing_vertical_tabs)
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
auto delta = mouse_event->globalPosition().toPoint().x() - m_vertical_tabs_resize_start_global_x;
|
|
apply_vertical_tabs_expanded_width(m_vertical_tabs_resize_start_width + delta);
|
|
return true;
|
|
} else if (event->type() == QEvent::MouseButtonRelease) {
|
|
if (!m_is_resizing_vertical_tabs)
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
if (mouse_event->button() != Qt::LeftButton)
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
persist_vertical_tabs_expanded_width();
|
|
reset_resize_handle();
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, m_vertical_tabs_resize_handle->underMouse());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (watched == m_vertical_tabs_content && event->type() == QEvent::Leave)
|
|
defer_update_vertical_tabs_hover_expanded();
|
|
|
|
if (watched == m_toolbar_container && event->type() == QEvent::Resize)
|
|
update_tab_layout();
|
|
|
|
auto is_vertical_tabs_hover_target = watched == m_vertical_tab_bar_column
|
|
|| watched == m_tab_bar
|
|
|| watched == m_new_tab_button;
|
|
|
|
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) && window_uses_client_side_decorations(*this)) {
|
|
auto is_empty_chrome_area = [this, watched](QMouseEvent const& mouse_event) {
|
|
if (watched == m_vertical_tab_bar_column) {
|
|
auto* child = m_vertical_tab_bar_column->childAt(mouse_event.pos());
|
|
return child == nullptr;
|
|
}
|
|
|
|
return m_tab_bar_row->childAt(mouse_event.pos()) == nullptr;
|
|
};
|
|
|
|
if (event->type() == QEvent::MouseButtonDblClick) {
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
if (mouse_event->button() == Qt::LeftButton && is_empty_chrome_area(*mouse_event)) {
|
|
toggle_window_maximized();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
if (mouse_event->button() == Qt::LeftButton && is_empty_chrome_area(*mouse_event) && start_window_move())
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return QWidget::eventFilter(watched, event);
|
|
}
|
|
|
|
void TabWidget::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QWidget::resizeEvent(event);
|
|
update_tab_layout();
|
|
}
|
|
|
|
void TabWidget::accept_tab_drag(QDragMoveEvent* event)
|
|
{
|
|
if (!s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
auto* drag_area = tab_drag_area_widget();
|
|
auto position_in_drag_area = drag_area->mapFrom(this, event->position().toPoint());
|
|
if (!drag_area->rect().contains(position_in_drag_area)) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
event->accept();
|
|
}
|
|
|
|
void TabWidget::accept_tab_drop(QDropEvent* event, int index)
|
|
{
|
|
if (!s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
s_pending_tab_drop_target = this;
|
|
s_pending_tab_drop_index = index;
|
|
event->setDropAction(Qt::MoveAction);
|
|
event->accept();
|
|
}
|
|
|
|
QRect TabWidget::tab_strip_global_rect() const
|
|
{
|
|
if (auto* widget = tab_drag_area_widget())
|
|
return { widget->mapToGlobal(QPoint(0, 0)), widget->size() };
|
|
return {};
|
|
}
|
|
|
|
TabLayout TabWidget::current_tab_layout() const
|
|
{
|
|
if (!m_vertical_tabs_enabled)
|
|
return TabLayout::Horizontal;
|
|
return vertical_tabs_effectively_expanded() ? TabLayout::VerticalExpanded : TabLayout::VerticalCollapsed;
|
|
}
|
|
|
|
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_tabs_content->isVisible())
|
|
return false;
|
|
|
|
auto vertical_tabs_rect = QRect {
|
|
m_vertical_tabs_content->mapToGlobal(QPoint { 0, 0 }),
|
|
QSize { current_vertical_tabs_width(), m_vertical_tabs_content->height() }
|
|
};
|
|
return vertical_tabs_rect.contains(QCursor::pos());
|
|
}
|
|
|
|
int TabWidget::vertical_tabs_layout_width() const
|
|
{
|
|
if (!m_tab_bar_visible)
|
|
return 0;
|
|
return m_vertical_tabs_expanded ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH;
|
|
}
|
|
|
|
bool TabWidget::should_show_window_controls_in_tab_toolbar() const
|
|
{
|
|
return m_window_controls_visible && m_tab_bar->tab_layout() != TabLayout::Horizontal;
|
|
}
|
|
|
|
void TabWidget::update_toolbar_placement()
|
|
{
|
|
auto use_full_width_toolbar = current_tab_layout() != TabLayout::Horizontal;
|
|
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index) {
|
|
auto* current_tab = tab(index);
|
|
auto* toolbar = current_tab->toolbar_container();
|
|
set_dynamic_property_if_needed(*toolbar, FULL_WIDTH_TOOLBAR_PROPERTY, use_full_width_toolbar);
|
|
}
|
|
|
|
m_toolbar_container->setCurrentIndex(m_tab_bar->currentIndex());
|
|
update_tab_toolbar_window_controls_visibility();
|
|
}
|
|
|
|
void TabWidget::update_tab_toolbar_window_controls_visibility()
|
|
{
|
|
auto show_controls = should_show_window_controls_in_tab_toolbar();
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index)
|
|
tab(index)->set_toolbar_window_controls_visible(show_controls);
|
|
}
|
|
|
|
void TabWidget::rebuild_layout()
|
|
{
|
|
clear_layout(*m_main_layout);
|
|
clear_layout(*m_tab_bar_row_layout);
|
|
clear_layout(*m_page_column_layout);
|
|
clear_layout(*m_vertical_tab_bar_column_layout);
|
|
clear_layout(*m_vertical_tabs_content_layout);
|
|
|
|
m_tab_bar->set_tab_layout(current_tab_layout());
|
|
update_toolbar_placement();
|
|
|
|
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
|
|
rebuild_layout_for_vertical_tabs();
|
|
m_main_layout->addWidget(m_toolbar_container);
|
|
m_page_column->hide();
|
|
|
|
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();
|
|
} else {
|
|
rebuild_layout_for_horizontal_tabs();
|
|
rebuild_page_column();
|
|
|
|
m_main_layout->addWidget(m_tab_bar_row);
|
|
m_main_layout->addWidget(m_page_column, 1);
|
|
m_page_column->show();
|
|
m_vertical_tabs_content->hide();
|
|
}
|
|
|
|
update_tab_button_visibility();
|
|
update_tab_chrome_visibility();
|
|
update_tab_layout();
|
|
}
|
|
|
|
void TabWidget::rebuild_layout_for_horizontal_tabs()
|
|
{
|
|
m_tab_bar_row->setMinimumHeight(HORIZONTAL_TAB_STRIP_HEIGHT);
|
|
m_tab_bar_row_layout->setSpacing(0);
|
|
m_tab_bar_row_layout->setContentsMargins(12, 2, 4, 1);
|
|
|
|
m_new_tab_button->setText({});
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_EXPANDED_PROPERTY, false);
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_BUTTON_PROPERTY, true);
|
|
m_new_tab_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
|
|
m_new_tab_button->setFixedSize(HORIZONTAL_TAB_HEIGHT, HORIZONTAL_TAB_HEIGHT);
|
|
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
|
if (use_left_traffic_light_window_controls()) {
|
|
m_tab_bar_row_layout->addWidget(m_window_controls, 0, Qt::AlignVCenter);
|
|
m_tab_bar_row_layout->addSpacing(16);
|
|
m_tab_bar_row_layout->addWidget(m_tab_bar);
|
|
m_tab_bar_row_layout->addWidget(m_new_tab_button, 0, Qt::AlignVCenter);
|
|
m_tab_bar_row_layout->addStretch(1);
|
|
return;
|
|
}
|
|
|
|
m_tab_bar_row_layout->addWidget(m_tab_bar);
|
|
m_tab_bar_row_layout->addWidget(m_new_tab_button, 0, Qt::AlignVCenter);
|
|
m_tab_bar_row_layout->addStretch(1);
|
|
m_tab_bar_row_layout->addWidget(m_window_controls);
|
|
}
|
|
|
|
void TabWidget::rebuild_layout_for_vertical_tabs()
|
|
{
|
|
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(0);
|
|
auto tab_layout = m_tab_bar->tab_layout();
|
|
auto side_margin = vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed);
|
|
m_window_controls->layout()->setContentsMargins(0, 0, 0, 0);
|
|
m_vertical_tab_bar_column_layout->setContentsMargins(side_margin, VERTICAL_TABS_TOP_MARGIN, side_margin, 8);
|
|
|
|
update_vertical_tabs_button_layout();
|
|
|
|
m_vertical_tab_bar_column_layout->addWidget(m_tab_bar);
|
|
m_vertical_tab_bar_column_layout->addWidget(m_new_tab_button);
|
|
m_vertical_tab_bar_column_layout->addStretch(1);
|
|
}
|
|
|
|
void TabWidget::rebuild_page_column()
|
|
{
|
|
m_page_column_layout->addWidget(m_toolbar_container);
|
|
m_page_column_layout->addWidget(m_stacked_widget, 1);
|
|
}
|
|
|
|
int TabWidget::current_vertical_tabs_width() const
|
|
{
|
|
return vertical_tabs_effectively_expanded() ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH;
|
|
}
|
|
|
|
void TabWidget::apply_vertical_tabs_expanded_width(int width)
|
|
{
|
|
auto clamped_width = clamp_vertical_tabs_expanded_width(width);
|
|
if (m_vertical_tabs_expanded_width == clamped_width)
|
|
return;
|
|
|
|
m_vertical_tabs_expanded_width = clamped_width;
|
|
update_tab_layout();
|
|
}
|
|
|
|
void TabWidget::persist_vertical_tabs_expanded_width()
|
|
{
|
|
Settings::the()->set_vertical_tabs_expanded_width(m_vertical_tabs_expanded_width);
|
|
}
|
|
|
|
void TabWidget::set_resize_handle_property(char const* property, bool enabled)
|
|
{
|
|
set_dynamic_property_if_needed(*m_vertical_tab_bar_column, property, enabled);
|
|
}
|
|
|
|
void TabWidget::update_vertical_tabs_resize_handle()
|
|
{
|
|
auto is_vertical = m_tab_bar->tab_layout() != TabLayout::Horizontal;
|
|
auto show_resize_handle = m_tab_bar_visible && is_vertical && m_vertical_tabs_expanded;
|
|
m_vertical_tabs_resize_handle->setVisible(show_resize_handle);
|
|
if (!show_resize_handle) {
|
|
m_vertical_tabs_resize_handle->releaseMouse();
|
|
if (m_is_resizing_vertical_tabs)
|
|
QApplication::restoreOverrideCursor();
|
|
m_is_resizing_vertical_tabs = false;
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, false);
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false);
|
|
return;
|
|
}
|
|
|
|
auto handle_width = VERTICAL_TABS_RESIZE_HIT_AREA_WIDTH;
|
|
auto divider_x = vertical_tabs_layout_width() - 1;
|
|
auto chrome_rect = vertical_tabs_chrome_rect();
|
|
m_vertical_tabs_resize_handle->setGeometry(
|
|
divider_x - (handle_width / 2),
|
|
chrome_rect.y(),
|
|
handle_width,
|
|
chrome_rect.height());
|
|
m_vertical_tabs_resize_handle->raise();
|
|
}
|
|
|
|
void TabWidget::update_vertical_tabs_content_separator()
|
|
{
|
|
auto show_content_separator = m_tab_bar_visible && m_tab_bar->tab_layout() != TabLayout::Horizontal;
|
|
m_vertical_tabs_content_separator->setVisible(show_content_separator);
|
|
if (!show_content_separator)
|
|
return;
|
|
|
|
auto separator_x = max(0, current_vertical_tabs_width() - 1);
|
|
auto separator_y = max(0, m_toolbar_container->height() - 1);
|
|
m_vertical_tabs_content_separator->setGeometry(separator_x, separator_y, max(0, width() - separator_x), 1);
|
|
m_vertical_tabs_content_separator->raise();
|
|
}
|
|
|
|
void TabWidget::update_vertical_tabs_action_labels()
|
|
{
|
|
auto text = vertical_tabs_effectively_expanded() ? "New Tab" : QString {};
|
|
if (m_new_tab_button->text() != text)
|
|
m_new_tab_button->setText(text);
|
|
}
|
|
|
|
void TabWidget::update_vertical_tabs_hover_layout()
|
|
{
|
|
m_tab_bar->set_tab_layout(current_tab_layout());
|
|
|
|
auto tab_layout = m_tab_bar->tab_layout();
|
|
auto side_margin = vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed);
|
|
|
|
m_vertical_tab_bar_column_layout->setContentsMargins(side_margin, VERTICAL_TABS_TOP_MARGIN, side_margin, 8);
|
|
update_vertical_tabs_button_layout();
|
|
|
|
update_tab_button_visibility();
|
|
update_tab_layout();
|
|
}
|
|
|
|
QRect TabWidget::vertical_tabs_chrome_rect() const
|
|
{
|
|
auto chrome_height = m_toolbar_container->height();
|
|
return {
|
|
0,
|
|
chrome_height,
|
|
current_vertical_tabs_width(),
|
|
max(0, height() - chrome_height)
|
|
};
|
|
}
|
|
|
|
int TabWidget::vertical_tabs_tab_width() const
|
|
{
|
|
return max(0, current_vertical_tabs_width() - vertical_tabs_horizontal_margin_width(m_tab_bar->tab_layout()));
|
|
}
|
|
|
|
void TabWidget::update_vertical_tabs_button_layout()
|
|
{
|
|
auto expanded = vertical_tabs_effectively_expanded();
|
|
auto tool_button_style = expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly;
|
|
if (m_new_tab_button->toolButtonStyle() != tool_button_style)
|
|
m_new_tab_button->setToolButtonStyle(tool_button_style);
|
|
update_vertical_tabs_action_labels();
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_BUTTON_PROPERTY, true);
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_EXPANDED_PROPERTY, expanded);
|
|
auto tab_width = vertical_tabs_tab_width();
|
|
auto button_size = QSize { tab_width, VERTICAL_TAB_HEIGHT };
|
|
if (m_new_tab_button->minimumSize() != button_size || m_new_tab_button->maximumSize() != button_size)
|
|
m_new_tab_button->setFixedSize(button_size);
|
|
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
}
|
|
|
|
void TabWidget::update_tab_layout()
|
|
{
|
|
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
|
|
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);
|
|
update_vertical_tabs_button_layout();
|
|
update_vertical_tabs_overlay_geometry();
|
|
m_vertical_tabs_content_layout->activate();
|
|
m_tab_bar->set_available_width(vertical_tabs_tab_width());
|
|
update_vertical_tabs_resize_handle();
|
|
update_vertical_tabs_content_separator();
|
|
return;
|
|
}
|
|
|
|
update_vertical_tabs_resize_handle();
|
|
update_vertical_tabs_content_separator();
|
|
|
|
auto controls_width = m_new_tab_button->width();
|
|
if (m_window_controls->isVisible())
|
|
controls_width += m_window_controls->width();
|
|
|
|
auto available_for_tabs = width() - controls_width - 36;
|
|
|
|
m_tab_bar->set_available_width(available_for_tabs);
|
|
|
|
auto tab_bar_width = min(available_for_tabs, m_tab_bar->count() * HORIZONTAL_TAB_MAX_WIDTH);
|
|
m_tab_bar->setFixedWidth(max(0, tab_bar_width));
|
|
}
|
|
|
|
void TabWidget::update_tab_button_visibility()
|
|
{
|
|
if (m_tab_bar->tab_layout() == TabLayout::VerticalCollapsed) {
|
|
for (int index = 0; index < m_tab_bar->count(); ++index) {
|
|
if (auto* button = m_tab_bar->tabButton(index, QTabBar::RightSide))
|
|
button->hide();
|
|
}
|
|
|
|
m_tab_bar->refresh_tab_layout();
|
|
return;
|
|
}
|
|
|
|
for (int index = 0; index < m_tab_bar->count(); ++index) {
|
|
if (auto* button = m_tab_bar->tabButton(index, QTabBar::LeftSide))
|
|
button->setVisible(true);
|
|
if (auto* button = m_tab_bar->tabButton(index, QTabBar::RightSide))
|
|
button->setVisible(true);
|
|
}
|
|
|
|
m_tab_bar->refresh_tab_layout();
|
|
}
|
|
|
|
void TabWidget::update_tab_chrome_visibility()
|
|
{
|
|
auto tab_layout = m_tab_bar->tab_layout();
|
|
auto is_horizontal = tab_layout == TabLayout::Horizontal;
|
|
auto show_top_row = m_tab_bar_visible && is_horizontal;
|
|
m_tab_bar_row->setVisible(show_top_row);
|
|
m_toolbar_container->setVisible(m_tab_bar_visible);
|
|
m_vertical_tab_bar_column->setVisible(m_tab_bar_visible && !is_horizontal);
|
|
update_vertical_tabs_content_separator();
|
|
|
|
auto show_window_controls = m_window_controls_visible
|
|
&& m_tab_bar_visible
|
|
&& is_horizontal;
|
|
m_window_controls->setVisible(show_window_controls);
|
|
}
|
|
|
|
void TabWidget::recreate_icons()
|
|
{
|
|
m_tab_bar->recreate_icons();
|
|
m_new_tab_button->setIcon(create_chrome_icon(ChromeIcon::NewTab, palette()));
|
|
update_vertical_tabs_action_labels();
|
|
update_window_button_icons();
|
|
}
|
|
|
|
void TabWidget::update_chrome_style()
|
|
{
|
|
if (m_is_updating_chrome_style)
|
|
return;
|
|
|
|
m_is_updating_chrome_style = true;
|
|
auto style_sheet = ChromeStyle::tab_widget_style_sheet(palette());
|
|
m_tab_bar_row->setStyleSheet(style_sheet);
|
|
m_vertical_tab_bar_column->setStyleSheet(style_sheet);
|
|
m_vertical_tabs_content_separator->setStyleSheet(style_sheet);
|
|
m_vertical_tabs_resize_handle->setStyleSheet(style_sheet);
|
|
m_is_updating_chrome_style = false;
|
|
}
|
|
|
|
void TabWidget::update_vertical_tabs_overlay_geometry()
|
|
{
|
|
if (!m_tab_bar_visible || m_tab_bar->tab_layout() == TabLayout::Horizontal) {
|
|
m_vertical_tab_bar_column->hide();
|
|
return;
|
|
}
|
|
|
|
m_vertical_tab_bar_column->setGeometry(vertical_tabs_chrome_rect());
|
|
m_vertical_tab_bar_column->show();
|
|
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();
|
|
update_vertical_tabs_hover_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();
|
|
m_minimize_window_button->setIcon(create_chrome_icon(ChromeIcon::WindowMinimize, palette()));
|
|
m_maximize_window_button->setIcon(create_chrome_icon(is_maximized ? ChromeIcon::WindowRestore : ChromeIcon::WindowMaximize, palette()));
|
|
m_maximize_window_button->setToolTip(is_maximized ? "Restore" : "Maximize");
|
|
m_close_window_button->setIcon(create_chrome_icon(ChromeIcon::WindowClose, palette()));
|
|
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index)
|
|
tab(index)->update_window_control_icons();
|
|
}
|
|
|
|
void TabWidget::toggle_window_maximized()
|
|
{
|
|
auto* top_level_window = window();
|
|
if (top_level_window->isMaximized())
|
|
top_level_window->showNormal();
|
|
else
|
|
top_level_window->showMaximized();
|
|
update_window_button_icons();
|
|
}
|
|
|
|
bool TabWidget::start_window_move()
|
|
{
|
|
auto* handle = window()->windowHandle();
|
|
if (!handle)
|
|
return false;
|
|
#if defined(AK_OS_MACOS)
|
|
if (start_appkit_window_drag(*this))
|
|
return true;
|
|
#endif
|
|
return handle->startSystemMove();
|
|
}
|
|
|
|
void TabBarButton::set_collapsed_vertical_overlay(bool enabled)
|
|
{
|
|
if (property(COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY).toBool() == enabled)
|
|
return;
|
|
|
|
setProperty(COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY, enabled);
|
|
|
|
QSize button_size { TAB_BUTTON_SIZE, TAB_BUTTON_SIZE };
|
|
QSize icon_size { TAB_ICON_SIZE, TAB_ICON_SIZE };
|
|
if (enabled) {
|
|
button_size = { COLLAPSED_VERTICAL_TAB_BUTTON_SIZE, COLLAPSED_VERTICAL_TAB_BUTTON_SIZE };
|
|
icon_size = { COLLAPSED_VERTICAL_TAB_ICON_SIZE, COLLAPSED_VERTICAL_TAB_ICON_SIZE };
|
|
}
|
|
|
|
setFixedSize(button_size);
|
|
setIconSize(icon_size);
|
|
style()->unpolish(this);
|
|
style()->polish(this);
|
|
update();
|
|
}
|
|
|
|
TabBarButton::TabBarButton(QIcon const& icon, QWidget* parent)
|
|
: QPushButton(icon, {}, parent)
|
|
{
|
|
setObjectName("LadybirdTabButton");
|
|
setFixedSize({ TAB_BUTTON_SIZE, TAB_BUTTON_SIZE });
|
|
setIconSize({ TAB_ICON_SIZE, TAB_ICON_SIZE });
|
|
setFocusPolicy(Qt::NoFocus);
|
|
setFlat(true);
|
|
hide();
|
|
}
|
|
|
|
bool TabBarButton::event(QEvent* event)
|
|
{
|
|
if (event->type() == QEvent::Enter)
|
|
setFlat(false);
|
|
if (event->type() == QEvent::Leave)
|
|
setFlat(true);
|
|
|
|
return QPushButton::event(event);
|
|
}
|
|
|
|
}
|