
Previously it would only do this if the mouse was over the close button. If you released the mouse outside the close button, the close button would appear permanently depressed afterwards.
530 lines
17 KiB
C++
530 lines
17 KiB
C++
/*
|
|
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonValue.h>
|
|
#include <LibGUI/BoxLayout.h>
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGUI/TabWidget.h>
|
|
#include <LibGUI/Window.h>
|
|
#include <LibGfx/Bitmap.h>
|
|
#include <LibGfx/Font.h>
|
|
#include <LibGfx/Palette.h>
|
|
#include <LibGfx/StylePainter.h>
|
|
|
|
REGISTER_WIDGET(GUI, TabWidget)
|
|
|
|
namespace GUI {
|
|
|
|
TabWidget::TabWidget()
|
|
{
|
|
set_focus_policy(FocusPolicy::NoFocus);
|
|
|
|
REGISTER_MARGINS_PROPERTY("container_margins", container_margins, set_container_margins);
|
|
REGISTER_BOOL_PROPERTY("uniform_tabs", uniform_tabs, set_uniform_tabs);
|
|
|
|
register_property(
|
|
"text_alignment",
|
|
[this] { return Gfx::to_string(text_alignment()); },
|
|
[this](auto& value) {
|
|
auto alignment = Gfx::text_alignment_from_string(value.to_string());
|
|
if (alignment.has_value()) {
|
|
set_text_alignment(alignment.value());
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
TabWidget::~TabWidget()
|
|
{
|
|
}
|
|
|
|
void TabWidget::add_widget(const StringView& title, Widget& widget)
|
|
{
|
|
m_tabs.append({ title, nullptr, &widget });
|
|
add_child(widget);
|
|
update_focus_policy();
|
|
if (on_tab_count_change)
|
|
on_tab_count_change(m_tabs.size());
|
|
}
|
|
|
|
void TabWidget::remove_widget(Widget& widget)
|
|
{
|
|
VERIFY(widget.parent() == this);
|
|
if (active_widget() == &widget)
|
|
activate_next_tab();
|
|
m_tabs.remove_first_matching([&widget](auto& entry) { return &widget == entry.widget; });
|
|
remove_child(widget);
|
|
update_focus_policy();
|
|
if (on_tab_count_change)
|
|
on_tab_count_change(m_tabs.size());
|
|
}
|
|
|
|
void TabWidget::update_focus_policy()
|
|
{
|
|
FocusPolicy policy;
|
|
if (is_bar_visible() && !m_tabs.is_empty())
|
|
policy = FocusPolicy::TabFocus;
|
|
else
|
|
policy = FocusPolicy::NoFocus;
|
|
set_focus_policy(policy);
|
|
}
|
|
|
|
void TabWidget::set_active_widget(Widget* widget)
|
|
{
|
|
if (widget == m_active_widget)
|
|
return;
|
|
|
|
bool active_widget_had_focus = m_active_widget && m_active_widget->has_focus_within();
|
|
|
|
if (m_active_widget)
|
|
m_active_widget->set_visible(false);
|
|
m_active_widget = widget;
|
|
if (m_active_widget) {
|
|
m_active_widget->set_relative_rect(child_rect_for_size(size()));
|
|
if (active_widget_had_focus)
|
|
m_active_widget->set_focus(true);
|
|
m_active_widget->set_visible(true);
|
|
deferred_invoke([this](auto&) {
|
|
if (on_change)
|
|
on_change(*m_active_widget);
|
|
});
|
|
}
|
|
|
|
update_bar();
|
|
}
|
|
|
|
void TabWidget::set_tab_index(int index)
|
|
{
|
|
if (m_tabs.at(index).widget == m_active_widget)
|
|
return;
|
|
set_active_widget(m_tabs.at(index).widget);
|
|
|
|
update_bar();
|
|
}
|
|
|
|
void TabWidget::resize_event(ResizeEvent& event)
|
|
{
|
|
if (!m_active_widget)
|
|
return;
|
|
m_active_widget->set_relative_rect(child_rect_for_size(event.size()));
|
|
}
|
|
|
|
Gfx::IntRect TabWidget::child_rect_for_size(const Gfx::IntSize& size) const
|
|
{
|
|
Gfx::IntRect rect;
|
|
switch (m_tab_position) {
|
|
case TabPosition::Top:
|
|
rect = { { m_container_margins.left(), bar_height() + m_container_margins.top() }, { size.width() - m_container_margins.left() - m_container_margins.right(), size.height() - bar_height() - m_container_margins.top() - m_container_margins.bottom() } };
|
|
break;
|
|
case TabPosition::Bottom:
|
|
rect = { { m_container_margins.left(), m_container_margins.top() }, { size.width() - m_container_margins.left() - m_container_margins.right(), size.height() - bar_height() - m_container_margins.top() - m_container_margins.bottom() } };
|
|
break;
|
|
}
|
|
if (rect.is_empty())
|
|
return {};
|
|
return rect;
|
|
}
|
|
|
|
void TabWidget::child_event(Core::ChildEvent& event)
|
|
{
|
|
if (!event.child() || !is<Widget>(*event.child()))
|
|
return Widget::child_event(event);
|
|
auto& child = verify_cast<Widget>(*event.child());
|
|
if (event.type() == Event::ChildAdded) {
|
|
if (!m_active_widget)
|
|
set_active_widget(&child);
|
|
else if (m_active_widget != &child)
|
|
child.set_visible(false);
|
|
} else if (event.type() == Event::ChildRemoved) {
|
|
if (m_active_widget == &child) {
|
|
Widget* new_active_widget = nullptr;
|
|
for_each_child_widget([&](auto& new_child) {
|
|
new_active_widget = &new_child;
|
|
return IterationDecision::Break;
|
|
});
|
|
set_active_widget(new_active_widget);
|
|
}
|
|
}
|
|
Widget::child_event(event);
|
|
}
|
|
|
|
Gfx::IntRect TabWidget::bar_rect() const
|
|
{
|
|
switch (m_tab_position) {
|
|
case TabPosition::Top:
|
|
return { 0, 0, width(), bar_height() };
|
|
case TabPosition::Bottom:
|
|
return { 0, height() - bar_height(), width(), bar_height() };
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
Gfx::IntRect TabWidget::container_rect() const
|
|
{
|
|
switch (m_tab_position) {
|
|
case TabPosition::Top:
|
|
return { 0, bar_height(), width(), height() - bar_height() };
|
|
case TabPosition::Bottom:
|
|
return { 0, 0, width(), height() - bar_height() };
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
void TabWidget::paint_event(PaintEvent& event)
|
|
{
|
|
if (!m_bar_visible)
|
|
return;
|
|
|
|
Painter painter(*this);
|
|
painter.add_clip_rect(event.rect());
|
|
|
|
painter.fill_rect(event.rect(), palette().button());
|
|
|
|
if (!m_container_margins.is_null()) {
|
|
Gfx::StylePainter::paint_frame(painter, container_rect(), palette(), Gfx::FrameShape::Container, Gfx::FrameShadow::Raised, 2);
|
|
}
|
|
|
|
auto paint_tab_icon_if_needed = [&](auto& icon, auto& button_rect, auto& text_rect) {
|
|
if (!icon)
|
|
return;
|
|
Gfx::IntRect icon_rect { button_rect.x(), button_rect.y(), 16, 16 };
|
|
icon_rect.translate_by(4, 3);
|
|
painter.draw_scaled_bitmap(icon_rect, *icon, icon->rect());
|
|
text_rect.set_x(icon_rect.right() + 1 + 4);
|
|
text_rect.intersect(button_rect);
|
|
};
|
|
|
|
for (size_t i = 0; i < m_tabs.size(); ++i) {
|
|
if (m_tabs[i].widget == m_active_widget)
|
|
continue;
|
|
bool hovered = static_cast<int>(i) == m_hovered_tab_index;
|
|
auto button_rect = this->button_rect(i);
|
|
Gfx::StylePainter::paint_tab_button(painter, button_rect, palette(), false, hovered, m_tabs[i].widget->is_enabled(), m_tab_position == TabPosition::Top);
|
|
auto tab_button_content_rect = button_rect.translated(4, m_tab_position == TabPosition::Top ? 1 : 0);
|
|
|
|
paint_tab_icon_if_needed(m_tabs[i].icon, button_rect, tab_button_content_rect);
|
|
tab_button_content_rect.set_width(tab_button_content_rect.width() - (m_close_button_enabled ? 16 : 2));
|
|
|
|
Gfx::IntRect text_rect { 0, 0, min(tab_button_content_rect.width(), font().width(m_tabs[i].title)), font().glyph_height() };
|
|
text_rect.inflate(6, 4);
|
|
text_rect.align_within(tab_button_content_rect, m_text_alignment);
|
|
text_rect.intersect(tab_button_content_rect);
|
|
|
|
painter.draw_text(text_rect, m_tabs[i].title, Gfx::TextAlignment::CenterLeft, palette().button_text(), Gfx::TextElision::Right);
|
|
}
|
|
|
|
for (size_t i = 0; i < m_tabs.size(); ++i) {
|
|
if (m_tabs[i].widget != m_active_widget)
|
|
continue;
|
|
bool hovered = static_cast<int>(i) == m_hovered_tab_index;
|
|
auto button_rect = this->button_rect(i);
|
|
Gfx::StylePainter::paint_tab_button(painter, button_rect, palette(), true, hovered, m_tabs[i].widget->is_enabled(), m_tab_position == TabPosition::Top);
|
|
auto tab_button_content_rect = button_rect.translated(4, m_tab_position == TabPosition::Top ? 1 : 0);
|
|
paint_tab_icon_if_needed(m_tabs[i].icon, button_rect, tab_button_content_rect);
|
|
tab_button_content_rect.set_width(tab_button_content_rect.width() - (m_close_button_enabled ? 16 : 2));
|
|
|
|
Gfx::IntRect text_rect { 0, 0, min(tab_button_content_rect.width(), font().width(m_tabs[i].title)), font().glyph_height() };
|
|
text_rect.inflate(6, 4);
|
|
text_rect.align_within(tab_button_content_rect, m_text_alignment);
|
|
text_rect.intersect(tab_button_content_rect);
|
|
|
|
painter.draw_text(text_rect, m_tabs[i].title, Gfx::TextAlignment::CenterLeft, palette().button_text(), Gfx::TextElision::Right);
|
|
|
|
if (is_focused()) {
|
|
painter.draw_focus_rect(text_rect, palette().focus_outline());
|
|
}
|
|
|
|
if (m_tab_position == TabPosition::Top) {
|
|
painter.draw_line(button_rect.bottom_left().translated(1, 1), button_rect.bottom_right().translated(-1, 1), palette().button());
|
|
} else if (m_tab_position == TabPosition::Bottom) {
|
|
painter.set_pixel(button_rect.top_left().translated(0, -1), palette().threed_highlight());
|
|
painter.set_pixel(button_rect.top_right().translated(-1, -1), palette().threed_shadow1());
|
|
painter.draw_line(button_rect.top_left().translated(1, -1), button_rect.top_right().translated(-2, -1), palette().button());
|
|
painter.draw_line(button_rect.top_left().translated(1, -2), button_rect.top_right().translated(-2, -2), palette().button());
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!m_close_button_enabled)
|
|
return;
|
|
|
|
for (size_t i = 0; i < m_tabs.size(); ++i) {
|
|
bool hovered_close_button = static_cast<int>(i) == m_hovered_close_button_index;
|
|
bool pressed_close_button = static_cast<int>(i) == m_pressed_close_button_index;
|
|
auto close_button_rect = this->close_button_rect(i);
|
|
|
|
if (hovered_close_button)
|
|
Gfx::StylePainter::paint_frame(painter, close_button_rect, palette(), Gfx::FrameShape::Box, pressed_close_button ? Gfx::FrameShadow::Sunken : Gfx::FrameShadow::Raised, 1);
|
|
|
|
Gfx::IntRect icon_rect { close_button_rect.x() + 3, close_button_rect.y() + 3, 6, 6 };
|
|
painter.draw_line(icon_rect.top_left(), icon_rect.bottom_right(), palette().button_text());
|
|
painter.draw_line(icon_rect.top_right(), icon_rect.bottom_left(), palette().button_text());
|
|
}
|
|
}
|
|
|
|
int TabWidget::uniform_tab_width() const
|
|
{
|
|
int minimum_tab_width = 24;
|
|
int maximum_tab_width = 160;
|
|
int total_tab_width = m_tabs.size() * maximum_tab_width;
|
|
int tab_width = maximum_tab_width;
|
|
int available_width = width() - bar_margin() * 2;
|
|
if (total_tab_width > available_width)
|
|
tab_width = available_width / m_tabs.size();
|
|
return max(tab_width, minimum_tab_width);
|
|
}
|
|
|
|
void TabWidget::set_bar_visible(bool bar_visible)
|
|
{
|
|
m_bar_visible = bar_visible;
|
|
if (m_active_widget)
|
|
m_active_widget->set_relative_rect(child_rect_for_size(size()));
|
|
update_bar();
|
|
}
|
|
|
|
Gfx::IntRect TabWidget::button_rect(int index) const
|
|
{
|
|
int x_offset = bar_margin();
|
|
int close_button_offset = m_close_button_enabled ? 16 : 0;
|
|
|
|
for (int i = 0; i < index; ++i) {
|
|
auto tab_width = m_uniform_tabs ? uniform_tab_width() : m_tabs[i].width(font()) + close_button_offset;
|
|
x_offset += tab_width;
|
|
}
|
|
Gfx::IntRect rect { x_offset, 0, m_uniform_tabs ? uniform_tab_width() : m_tabs[index].width(font()) + close_button_offset, bar_height() };
|
|
if (m_tabs[index].widget != m_active_widget) {
|
|
rect.translate_by(0, m_tab_position == TabPosition::Top ? 2 : 0);
|
|
rect.set_height(rect.height() - 2);
|
|
} else {
|
|
rect.translate_by(-2, 0);
|
|
rect.set_width(rect.width() + 4);
|
|
}
|
|
rect.translate_by(bar_rect().location());
|
|
return rect;
|
|
}
|
|
|
|
Gfx::IntRect TabWidget::close_button_rect(int index) const
|
|
{
|
|
auto rect = button_rect(index);
|
|
Gfx::IntRect close_button_rect { 0, 0, 12, 12 };
|
|
|
|
if (m_tabs[index].widget == m_active_widget)
|
|
close_button_rect.translate_by(rect.right() - 16, rect.top() + (m_tab_position == TabPosition::Top ? 5 : 4));
|
|
else
|
|
close_button_rect.translate_by(rect.right() - 15, rect.top() + (m_tab_position == TabPosition::Top ? 4 : 3));
|
|
|
|
return close_button_rect;
|
|
}
|
|
|
|
int TabWidget::TabData::width(const Gfx::Font& font) const
|
|
{
|
|
return 16 + font.width(title) + (icon ? (16 + 4) : 0);
|
|
}
|
|
|
|
void TabWidget::mousedown_event(MouseEvent& event)
|
|
{
|
|
for (size_t i = 0; i < m_tabs.size(); ++i) {
|
|
auto button_rect = this->button_rect(i);
|
|
auto close_button_rect = this->close_button_rect(i);
|
|
|
|
if (!button_rect.contains(event.position()))
|
|
continue;
|
|
|
|
if (event.button() == MouseButton::Left) {
|
|
if (m_close_button_enabled && close_button_rect.contains(event.position())) {
|
|
m_pressed_close_button_index = i;
|
|
update_bar();
|
|
return;
|
|
}
|
|
set_active_widget(m_tabs[i].widget);
|
|
} else if (event.button() == MouseButton::Middle) {
|
|
auto* widget = m_tabs[i].widget;
|
|
deferred_invoke([this, widget](auto&) {
|
|
if (on_middle_click && widget)
|
|
on_middle_click(*widget);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
void TabWidget::mouseup_event(MouseEvent& event)
|
|
{
|
|
if (event.button() != MouseButton::Left)
|
|
return;
|
|
|
|
if (!m_close_button_enabled || m_pressed_close_button_index == -1)
|
|
return;
|
|
|
|
auto close_button_rect = this->close_button_rect(m_pressed_close_button_index);
|
|
m_pressed_close_button_index = -1;
|
|
|
|
if (close_button_rect.contains(event.position())) {
|
|
auto* widget = m_tabs[m_pressed_close_button_index].widget;
|
|
deferred_invoke([this, widget](auto&) {
|
|
if (on_tab_close_click && widget)
|
|
on_tab_close_click(*widget);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
void TabWidget::mousemove_event(MouseEvent& event)
|
|
{
|
|
int hovered_tab = -1;
|
|
int hovered_close_button = -1;
|
|
|
|
for (size_t i = 0; i < m_tabs.size(); ++i) {
|
|
auto button_rect = this->button_rect(i);
|
|
auto close_button_rect = this->close_button_rect(i);
|
|
|
|
if (close_button_rect.contains(event.position()))
|
|
hovered_close_button = i;
|
|
|
|
if (!button_rect.contains(event.position()))
|
|
continue;
|
|
hovered_tab = i;
|
|
if (m_tabs[i].widget == m_active_widget)
|
|
break;
|
|
}
|
|
if (hovered_tab == m_hovered_tab_index && hovered_close_button == m_hovered_close_button_index)
|
|
return;
|
|
m_hovered_tab_index = hovered_tab;
|
|
m_hovered_close_button_index = hovered_close_button;
|
|
update_bar();
|
|
}
|
|
|
|
void TabWidget::leave_event(Core::Event&)
|
|
{
|
|
if (m_hovered_tab_index != -1 || m_hovered_close_button_index != -1) {
|
|
m_hovered_tab_index = -1;
|
|
m_hovered_close_button_index = -1;
|
|
update_bar();
|
|
}
|
|
}
|
|
|
|
void TabWidget::update_bar()
|
|
{
|
|
auto invalidation_rect = bar_rect();
|
|
invalidation_rect.set_height(invalidation_rect.height() + 1);
|
|
invalidation_rect.set_right(button_rect(static_cast<int>(m_tabs.size() - 1)).right());
|
|
update(invalidation_rect);
|
|
}
|
|
|
|
void TabWidget::set_tab_position(TabPosition tab_position)
|
|
{
|
|
if (m_tab_position == tab_position)
|
|
return;
|
|
m_tab_position = tab_position;
|
|
if (m_active_widget)
|
|
m_active_widget->set_relative_rect(child_rect_for_size(size()));
|
|
update();
|
|
}
|
|
|
|
int TabWidget::active_tab_index() const
|
|
{
|
|
for (size_t i = 0; i < m_tabs.size(); i++) {
|
|
if (m_tabs.at(i).widget == m_active_widget)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void TabWidget::set_tab_title(Widget& tab, const StringView& title)
|
|
{
|
|
for (auto& t : m_tabs) {
|
|
if (t.widget == &tab) {
|
|
if (t.title != title) {
|
|
t.title = title;
|
|
update();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TabWidget::set_tab_icon(Widget& tab, const Gfx::Bitmap* icon)
|
|
{
|
|
for (auto& t : m_tabs) {
|
|
if (t.widget == &tab) {
|
|
t.icon = icon;
|
|
update();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TabWidget::activate_next_tab()
|
|
{
|
|
if (m_tabs.size() <= 1)
|
|
return;
|
|
int index = active_tab_index();
|
|
++index;
|
|
if (index >= (int)m_tabs.size())
|
|
index = 0;
|
|
set_active_widget(m_tabs.at(index).widget);
|
|
}
|
|
|
|
void TabWidget::activate_previous_tab()
|
|
{
|
|
if (m_tabs.size() <= 1)
|
|
return;
|
|
int index = active_tab_index();
|
|
--index;
|
|
if (index < 0)
|
|
index = m_tabs.size() - 1;
|
|
set_active_widget(m_tabs.at(index).widget);
|
|
}
|
|
|
|
void TabWidget::keydown_event(KeyEvent& event)
|
|
{
|
|
if (event.ctrl() && event.key() == Key_Tab) {
|
|
if (event.shift())
|
|
activate_previous_tab();
|
|
else
|
|
activate_next_tab();
|
|
event.accept();
|
|
return;
|
|
}
|
|
if (is_focused()) {
|
|
if (!event.modifiers() && event.key() == Key_Left) {
|
|
activate_previous_tab();
|
|
event.accept();
|
|
return;
|
|
}
|
|
if (!event.modifiers() && event.key() == Key_Right) {
|
|
activate_next_tab();
|
|
event.accept();
|
|
return;
|
|
}
|
|
}
|
|
Widget::keydown_event(event);
|
|
}
|
|
|
|
void TabWidget::context_menu_event(ContextMenuEvent& context_menu_event)
|
|
{
|
|
for (size_t i = 0; i < m_tabs.size(); ++i) {
|
|
auto button_rect = this->button_rect(i);
|
|
if (!button_rect.contains(context_menu_event.position()))
|
|
continue;
|
|
auto* widget = m_tabs[i].widget;
|
|
deferred_invoke([this, widget, context_menu_event](auto&) {
|
|
if (on_context_menu_request && widget)
|
|
on_context_menu_request(*widget, context_menu_event);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
void TabWidget::set_container_margins(GUI::Margins const& margins)
|
|
{
|
|
m_container_margins = margins;
|
|
update();
|
|
}
|
|
|
|
}
|