Переглянути джерело

LibGUI: Add a GUI::Tray widget for the FilePicker common locations

The FilePicker has implemented its common locations tray as a composite
widget built from a GUI::Frame with a bunch of GUI::Button inside it.
The problem with that is that it creates a long and annoying chain of
keyboard-focusable widgets.

This patch adds GUI::Tray, which is a dedicated single widget that
implements the same UI element, but without child widgets.
Andreas Kling 3 роки тому
батько
коміт
ae2579d8b5

+ 1 - 0
Userland/Libraries/LibGUI/CMakeLists.txt

@@ -98,6 +98,7 @@ set(SOURCES
     TextEditor.cpp
     TextEditor.cpp
     Toolbar.cpp
     Toolbar.cpp
     ToolbarContainer.cpp
     ToolbarContainer.cpp
+    Tray.cpp
     TreeView.cpp
     TreeView.cpp
     UndoStack.cpp
     UndoStack.cpp
     ValueSlider.cpp
     ValueSlider.cpp

+ 214 - 0
Userland/Libraries/LibGUI/Tray.cpp

@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibGUI/Painter.h>
+#include <LibGUI/Tray.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/StylePainter.h>
+
+REGISTER_WIDGET(GUI, Tray);
+
+namespace GUI {
+
+Tray::Tray()
+{
+    set_fill_with_background_color(true);
+    set_background_role(Gfx::ColorRole::Tray);
+    set_focus_policy(GUI::FocusPolicy::TabFocus);
+}
+
+Tray::~Tray()
+{
+}
+
+Gfx::IntRect Tray::Item::rect(Tray const& tray) const
+{
+    static constexpr int item_height = 22;
+    return Gfx::IntRect {
+        tray.frame_thickness(),
+        tray.frame_thickness() + static_cast<int>(index) * item_height,
+        tray.frame_inner_rect().width(),
+        item_height,
+    };
+}
+
+size_t Tray::add_item(String text, RefPtr<Gfx::Bitmap> bitmap, String custom_data)
+{
+    auto new_index = m_items.size();
+
+    m_items.append(Item {
+        .text = move(text),
+        .bitmap = move(bitmap),
+        .custom_data = move(custom_data),
+        .index = new_index,
+    });
+    update();
+
+    return new_index;
+}
+
+void Tray::set_item_checked(size_t index, bool checked)
+{
+    if (checked) {
+        m_checked_item_index = index;
+    } else {
+        if (m_checked_item_index == index)
+            m_checked_item_index = {};
+    }
+    update();
+}
+
+void Tray::paint_event(GUI::PaintEvent& event)
+{
+    GUI::Frame::paint_event(event);
+
+    GUI::Painter painter(*this);
+    painter.add_clip_rect(event.rect());
+
+    for (auto& item : m_items) {
+        auto rect = item.rect(*this);
+        bool is_pressed = item.index == m_pressed_item_index;
+        bool is_hovered = item.index == m_hovered_item_index;
+        bool is_checked = item.index == m_checked_item_index;
+        Gfx::StylePainter::paint_button(painter, rect, palette(), Gfx::ButtonStyle::Tray, is_pressed && is_hovered, is_hovered, is_checked, is_enabled());
+
+        Gfx::IntRect icon_rect {
+            rect.x() + 4,
+            0,
+            16,
+            16,
+        };
+        icon_rect.center_vertically_within(rect);
+
+        Gfx::IntRect text_rect {
+            icon_rect.right() + 5,
+            rect.y(),
+            rect.width(),
+            rect.height(),
+        };
+        text_rect.intersect(rect);
+
+        if (is_pressed && is_hovered) {
+            icon_rect.translate_by(1, 1);
+            text_rect.translate_by(1, 1);
+        }
+
+        if (item.bitmap)
+            painter.blit(icon_rect.location(), *item.bitmap, item.bitmap->rect());
+
+        auto const& font = is_checked ? this->font().bold_variant() : this->font();
+        painter.draw_text(text_rect, item.text, font, Gfx::TextAlignment::CenterLeft, palette().color(Gfx::ColorRole::TrayText));
+    }
+}
+
+void Tray::mousemove_event(GUI::MouseEvent& event)
+{
+    auto* hovered_item = item_at(event.position());
+    if (!hovered_item) {
+        if (m_hovered_item_index.has_value())
+            update();
+        m_hovered_item_index = {};
+        return;
+    }
+    if (m_hovered_item_index != hovered_item->index) {
+        m_hovered_item_index = hovered_item->index;
+        update();
+    }
+}
+
+void Tray::mousedown_event(GUI::MouseEvent& event)
+{
+    if (event.button() != GUI::MouseButton::Left)
+        return;
+
+    auto* pressed_item = item_at(event.position());
+    if (!pressed_item)
+        return;
+
+    if (m_pressed_item_index != pressed_item->index) {
+        m_pressed_item_index = pressed_item->index;
+        update();
+    }
+}
+
+void Tray::mouseup_event(GUI::MouseEvent& event)
+{
+    if (event.button() != GUI::MouseButton::Left)
+        return;
+
+    if (auto* pressed_item = item_at(event.position()); pressed_item && m_pressed_item_index == pressed_item->index) {
+        on_item_activation(pressed_item->custom_data);
+    }
+
+    m_pressed_item_index = {};
+    update();
+}
+
+void Tray::leave_event(Core::Event&)
+{
+    m_hovered_item_index = {};
+    update();
+}
+
+Tray::Item* Tray::item_at(Gfx::IntPoint const& position)
+{
+    for (auto& item : m_items) {
+        if (item.rect(*this).contains(position))
+            return &item;
+    }
+    return nullptr;
+}
+
+void Tray::focusin_event(GUI::FocusEvent&)
+{
+    if (m_items.is_empty())
+        return;
+    m_hovered_item_index = 0;
+    update();
+}
+
+void Tray::focusout_event(GUI::FocusEvent&)
+{
+    if (m_items.is_empty())
+        return;
+    m_hovered_item_index = {};
+    update();
+}
+
+void Tray::keydown_event(GUI::KeyEvent& event)
+{
+    if (m_items.is_empty() || event.modifiers())
+        return Frame::keydown_event(event);
+
+    if (event.key() == KeyCode::Key_Down) {
+        if (!m_hovered_item_index.has_value())
+            m_hovered_item_index = 0;
+        else
+            m_hovered_item_index = (*m_hovered_item_index + 1) % m_items.size();
+        update();
+        return;
+    }
+
+    if (event.key() == KeyCode::Key_Up) {
+        if (!m_hovered_item_index.has_value() || m_hovered_item_index == 0u)
+            m_hovered_item_index = m_items.size() - 1;
+        else
+            m_hovered_item_index = *m_hovered_item_index - 1;
+        update();
+        return;
+    }
+
+    if (event.key() == KeyCode::Key_Return) {
+        if (m_hovered_item_index.has_value())
+            on_item_activation(m_items[*m_hovered_item_index].custom_data);
+        return;
+    }
+
+    Frame::keydown_event(event);
+}
+
+}

+ 56 - 0
Userland/Libraries/LibGUI/Tray.h

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+#include <LibGfx/Bitmap.h>
+
+namespace GUI {
+
+class Tray : public GUI::Frame {
+    C_OBJECT(Tray);
+
+public:
+    virtual ~Tray() override;
+
+    size_t add_item(String text, RefPtr<Gfx::Bitmap>, String custom_data);
+
+    void set_item_checked(size_t index, bool);
+
+    Function<void(String const&)> on_item_activation;
+
+protected:
+    virtual void paint_event(GUI::PaintEvent&) override;
+    virtual void mousemove_event(GUI::MouseEvent&) override;
+    virtual void mousedown_event(GUI::MouseEvent&) override;
+    virtual void mouseup_event(GUI::MouseEvent&) override;
+    virtual void leave_event(Core::Event&) override;
+    virtual void focusin_event(GUI::FocusEvent&) override;
+    virtual void focusout_event(GUI::FocusEvent&) override;
+    virtual void keydown_event(GUI::KeyEvent&) override;
+
+private:
+    Tray();
+
+    struct Item {
+        String text;
+        RefPtr<Gfx::Bitmap> bitmap;
+        String custom_data;
+        size_t index { 0 };
+        Gfx::IntRect rect(Tray const&) const;
+    };
+
+    Item* item_at(Gfx::IntPoint const&);
+
+    Vector<Item> m_items;
+
+    Optional<size_t> m_pressed_item_index;
+    Optional<size_t> m_hovered_item_index;
+    Optional<size_t> m_checked_item_index;
+};
+
+}