mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 03:27:34 +00:00
Libraries: Create top level directory for libraries.
Things were getting a little crowded in the project root, so this patch moves the Lib*/ directories into Libraries/.
This commit is contained in:
parent
63814ffebf
commit
04b9dc2d30
328 changed files with 36 additions and 36 deletions
153
Libraries/LibGUI/GAbstractButton.cpp
Normal file
153
Libraries/LibGUI/GAbstractButton.cpp
Normal file
|
@ -0,0 +1,153 @@
|
|||
#include <LibGUI/GAbstractButton.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
|
||||
GAbstractButton::GAbstractButton(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GAbstractButton::GAbstractButton(const StringView& text, GWidget* parent)
|
||||
: GWidget(parent)
|
||||
, m_text(text)
|
||||
{
|
||||
}
|
||||
|
||||
GAbstractButton::~GAbstractButton()
|
||||
{
|
||||
}
|
||||
|
||||
void GAbstractButton::set_text(const StringView& text)
|
||||
{
|
||||
if (m_text == text)
|
||||
return;
|
||||
m_text = text;
|
||||
update();
|
||||
}
|
||||
|
||||
void GAbstractButton::set_checked(bool checked)
|
||||
{
|
||||
if (m_checked == checked)
|
||||
return;
|
||||
m_checked = checked;
|
||||
|
||||
if (is_exclusive() && checked) {
|
||||
parent_widget()->for_each_child_of_type<GAbstractButton>([&](auto& sibling) {
|
||||
if (!sibling.is_exclusive() || !sibling.is_checkable() || !sibling.is_checked())
|
||||
return IterationDecision::Continue;
|
||||
sibling.m_checked = false;
|
||||
sibling.update();
|
||||
if (sibling.on_checked)
|
||||
sibling.on_checked(false);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
m_checked = true;
|
||||
}
|
||||
|
||||
update();
|
||||
if (on_checked)
|
||||
on_checked(checked);
|
||||
}
|
||||
|
||||
void GAbstractButton::set_checkable(bool checkable)
|
||||
{
|
||||
if (m_checkable == checkable)
|
||||
return;
|
||||
m_checkable = checkable;
|
||||
update();
|
||||
}
|
||||
|
||||
void GAbstractButton::mousemove_event(GMouseEvent& event)
|
||||
{
|
||||
bool is_over = rect().contains(event.position());
|
||||
m_hovered = is_over;
|
||||
if (event.buttons() & GMouseButton::Left) {
|
||||
if (is_enabled()) {
|
||||
bool being_pressed = is_over;
|
||||
if (being_pressed != m_being_pressed) {
|
||||
m_being_pressed = being_pressed;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
GWidget::mousemove_event(event);
|
||||
}
|
||||
|
||||
void GAbstractButton::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
#ifdef GABSTRACTBUTTON_DEBUG
|
||||
dbgprintf("GAbstractButton::mouse_down_event: x=%d, y=%d, button=%u\n", event.x(), event.y(), (unsigned)event.button());
|
||||
#endif
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
if (is_enabled()) {
|
||||
m_being_pressed = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
GWidget::mousedown_event(event);
|
||||
}
|
||||
|
||||
void GAbstractButton::mouseup_event(GMouseEvent& event)
|
||||
{
|
||||
#ifdef GABSTRACTBUTTON_DEBUG
|
||||
dbgprintf("GAbstractButton::mouse_up_event: x=%d, y=%d, button=%u\n", event.x(), event.y(), (unsigned)event.button());
|
||||
#endif
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
if (is_enabled()) {
|
||||
bool was_being_pressed = m_being_pressed;
|
||||
m_being_pressed = false;
|
||||
update();
|
||||
if (was_being_pressed)
|
||||
click();
|
||||
}
|
||||
}
|
||||
GWidget::mouseup_event(event);
|
||||
}
|
||||
|
||||
void GAbstractButton::enter_event(CEvent&)
|
||||
{
|
||||
m_hovered = true;
|
||||
update();
|
||||
}
|
||||
|
||||
void GAbstractButton::leave_event(CEvent&)
|
||||
{
|
||||
m_hovered = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void GAbstractButton::keydown_event(GKeyEvent& event)
|
||||
{
|
||||
if (event.key() == KeyCode::Key_Return)
|
||||
click();
|
||||
GWidget::keydown_event(event);
|
||||
}
|
||||
|
||||
void GAbstractButton::paint_text(GPainter& painter, const Rect& rect, const Font& font, TextAlignment text_alignment)
|
||||
{
|
||||
auto clipped_rect = rect.intersected(this->rect());
|
||||
|
||||
if (!is_enabled()) {
|
||||
painter.draw_text(clipped_rect.translated(1, 1), text(), font, text_alignment, Color::White, TextElision::Right);
|
||||
painter.draw_text(clipped_rect, text(), font, text_alignment, Color::from_rgb(0x808080), TextElision::Right);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text().is_empty())
|
||||
return;
|
||||
painter.draw_text(clipped_rect, text(), font, text_alignment, foreground_color(), TextElision::Right);
|
||||
if (is_focused())
|
||||
painter.draw_rect(clipped_rect.inflated(6, 4), Color(140, 140, 140));
|
||||
}
|
||||
|
||||
void GAbstractButton::change_event(GEvent& event)
|
||||
{
|
||||
if (event.type() == GEvent::Type::EnabledChange) {
|
||||
if (!is_enabled()) {
|
||||
bool was_being_pressed = m_being_pressed;
|
||||
m_being_pressed = false;
|
||||
if (was_being_pressed)
|
||||
update();
|
||||
}
|
||||
}
|
||||
GWidget::change_event(event);
|
||||
}
|
65
Libraries/LibGUI/GAbstractButton.h
Normal file
65
Libraries/LibGUI/GAbstractButton.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
#include <SharedGraphics/TextAlignment.h>
|
||||
|
||||
class GPainter;
|
||||
|
||||
class GAbstractButton : public GWidget {
|
||||
public:
|
||||
virtual ~GAbstractButton() override;
|
||||
|
||||
Function<void(bool)> on_checked;
|
||||
|
||||
void set_text(const StringView&);
|
||||
const String& text() const { return m_text; }
|
||||
|
||||
bool is_exclusive() const { return m_exclusive; }
|
||||
void set_exclusive(bool b) { m_exclusive = b; }
|
||||
|
||||
bool is_checked() const { return m_checked; }
|
||||
void set_checked(bool);
|
||||
|
||||
bool is_checkable() const { return m_checkable; }
|
||||
void set_checkable(bool);
|
||||
|
||||
bool is_hovered() const { return m_hovered; }
|
||||
bool is_being_pressed() const { return m_being_pressed; }
|
||||
|
||||
virtual void click() = 0;
|
||||
virtual const char* class_name() const override { return "GAbstractButton"; }
|
||||
virtual bool accepts_focus() const override { return true; }
|
||||
virtual bool supports_keyboard_activation() const { return true; }
|
||||
|
||||
protected:
|
||||
explicit GAbstractButton(GWidget* parent);
|
||||
GAbstractButton(const StringView&, GWidget* parent);
|
||||
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void enter_event(CEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
virtual void change_event(GEvent&) override;
|
||||
|
||||
void paint_text(GPainter&, const Rect&, const Font&, TextAlignment);
|
||||
|
||||
private:
|
||||
virtual bool is_abstract_button() const final { return true; }
|
||||
|
||||
String m_text;
|
||||
bool m_checked { false };
|
||||
bool m_checkable { false };
|
||||
bool m_hovered { false };
|
||||
bool m_being_pressed { false };
|
||||
bool m_exclusive { false };
|
||||
};
|
||||
|
||||
template<>
|
||||
inline bool is<GAbstractButton>(const CObject& object)
|
||||
{
|
||||
if (!is<GWidget>(object))
|
||||
return false;
|
||||
return to<GWidget>(object).is_abstract_button();
|
||||
}
|
105
Libraries/LibGUI/GAbstractView.cpp
Normal file
105
Libraries/LibGUI/GAbstractView.cpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
#include <Kernel/KeyCode.h>
|
||||
#include <LibGUI/GAbstractView.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <LibGUI/GModelEditingDelegate.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
#include <LibGUI/GTextBox.h>
|
||||
|
||||
GAbstractView::GAbstractView(GWidget* parent)
|
||||
: GScrollableWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GAbstractView::~GAbstractView()
|
||||
{
|
||||
delete m_edit_widget;
|
||||
}
|
||||
|
||||
void GAbstractView::set_model(RefPtr<GModel>&& model)
|
||||
{
|
||||
if (model == m_model)
|
||||
return;
|
||||
if (m_model)
|
||||
m_model->unregister_view({}, *this);
|
||||
m_model = move(model);
|
||||
if (m_model)
|
||||
m_model->register_view({}, *this);
|
||||
did_update_model();
|
||||
}
|
||||
|
||||
void GAbstractView::model_notification(const GModelNotification& notification)
|
||||
{
|
||||
if (on_model_notification)
|
||||
on_model_notification(notification);
|
||||
}
|
||||
|
||||
void GAbstractView::did_update_model()
|
||||
{
|
||||
if (!model() || model()->selected_index() != m_edit_index)
|
||||
stop_editing();
|
||||
model_notification(GModelNotification(GModelNotification::ModelUpdated));
|
||||
}
|
||||
|
||||
void GAbstractView::did_update_selection()
|
||||
{
|
||||
if (!model() || model()->selected_index() != m_edit_index)
|
||||
stop_editing();
|
||||
if (model() && on_selection && model()->selected_index().is_valid())
|
||||
on_selection(model()->selected_index());
|
||||
}
|
||||
|
||||
void GAbstractView::did_scroll()
|
||||
{
|
||||
update_edit_widget_position();
|
||||
}
|
||||
|
||||
void GAbstractView::update_edit_widget_position()
|
||||
{
|
||||
if (!m_edit_widget)
|
||||
return;
|
||||
m_edit_widget->set_relative_rect(m_edit_widget_content_rect.translated(-horizontal_scrollbar().value(), -vertical_scrollbar().value()));
|
||||
}
|
||||
|
||||
void GAbstractView::begin_editing(const GModelIndex& index)
|
||||
{
|
||||
ASSERT(is_editable());
|
||||
ASSERT(model());
|
||||
if (m_edit_index == index)
|
||||
return;
|
||||
if (!model()->is_editable(index))
|
||||
return;
|
||||
if (m_edit_widget)
|
||||
delete m_edit_widget;
|
||||
m_edit_index = index;
|
||||
|
||||
ASSERT(aid_create_editing_delegate);
|
||||
m_editing_delegate = aid_create_editing_delegate(index);
|
||||
m_editing_delegate->bind(*model(), index);
|
||||
m_editing_delegate->set_value(model()->data(index, GModel::Role::Display));
|
||||
m_edit_widget = m_editing_delegate->widget();
|
||||
add_child(*m_edit_widget);
|
||||
m_edit_widget->move_to_back();
|
||||
m_edit_widget_content_rect = content_rect(index).translated(frame_thickness(), frame_thickness());
|
||||
update_edit_widget_position();
|
||||
m_edit_widget->set_focus(true);
|
||||
m_editing_delegate->will_begin_editing();
|
||||
m_editing_delegate->on_commit = [this] {
|
||||
ASSERT(model());
|
||||
model()->set_data(m_edit_index, m_editing_delegate->value());
|
||||
stop_editing();
|
||||
};
|
||||
}
|
||||
|
||||
void GAbstractView::stop_editing()
|
||||
{
|
||||
m_edit_index = {};
|
||||
delete m_edit_widget;
|
||||
m_edit_widget = nullptr;
|
||||
}
|
||||
|
||||
void GAbstractView::activate(const GModelIndex& index)
|
||||
{
|
||||
if (on_activation)
|
||||
on_activation(index);
|
||||
}
|
58
Libraries/LibGUI/GAbstractView.h
Normal file
58
Libraries/LibGUI/GAbstractView.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <LibGUI/GScrollableWidget.h>
|
||||
|
||||
class GModelEditingDelegate;
|
||||
|
||||
class GAbstractView : public GScrollableWidget {
|
||||
friend class GModel;
|
||||
|
||||
public:
|
||||
explicit GAbstractView(GWidget* parent);
|
||||
virtual ~GAbstractView() override;
|
||||
|
||||
void set_model(RefPtr<GModel>&&);
|
||||
GModel* model() { return m_model.ptr(); }
|
||||
const GModel* model() const { return m_model.ptr(); }
|
||||
|
||||
bool is_editable() const { return m_editable; }
|
||||
void set_editable(bool editable) { m_editable = editable; }
|
||||
|
||||
virtual bool accepts_focus() const override { return true; }
|
||||
virtual void did_update_model();
|
||||
virtual void did_update_selection();
|
||||
|
||||
virtual Rect content_rect(const GModelIndex&) const { return {}; }
|
||||
void begin_editing(const GModelIndex&);
|
||||
void stop_editing();
|
||||
|
||||
void set_activates_on_selection(bool b) { m_activates_on_selection = b; }
|
||||
bool activates_on_selection() const { return m_activates_on_selection; }
|
||||
|
||||
Function<void(const GModelIndex&)> on_activation;
|
||||
Function<void(const GModelIndex&)> on_selection;
|
||||
Function<void(const GModelIndex&, const GContextMenuEvent&)> on_context_menu_request;
|
||||
Function<void(const GModelNotification&)> on_model_notification;
|
||||
|
||||
Function<OwnPtr<GModelEditingDelegate>(const GModelIndex&)> aid_create_editing_delegate;
|
||||
|
||||
virtual const char* class_name() const override { return "GAbstractView"; }
|
||||
|
||||
protected:
|
||||
virtual void model_notification(const GModelNotification&);
|
||||
virtual void did_scroll() override;
|
||||
void activate(const GModelIndex&);
|
||||
void update_edit_widget_position();
|
||||
|
||||
bool m_editable { false };
|
||||
GModelIndex m_edit_index;
|
||||
GWidget* m_edit_widget { nullptr };
|
||||
Rect m_edit_widget_content_rect;
|
||||
|
||||
private:
|
||||
RefPtr<GModel> m_model;
|
||||
OwnPtr<GModelEditingDelegate> m_editing_delegate;
|
||||
bool m_activates_on_selection { false };
|
||||
};
|
120
Libraries/LibGUI/GAction.cpp
Normal file
120
Libraries/LibGUI/GAction.cpp
Normal file
|
@ -0,0 +1,120 @@
|
|||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GApplication.h>
|
||||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GMenuItem.h>
|
||||
|
||||
GAction::GAction(const StringView& text, const StringView& custom_data, Function<void(GAction&)> on_activation_callback, GWidget* widget)
|
||||
: on_activation(move(on_activation_callback))
|
||||
, m_text(text)
|
||||
, m_custom_data(custom_data)
|
||||
, m_widget(widget ? widget->make_weak_ptr() : nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
GAction::GAction(const StringView& text, Function<void(GAction&)> on_activation_callback, GWidget* widget)
|
||||
: GAction(text, String(), move(on_activation_callback), widget)
|
||||
{
|
||||
}
|
||||
|
||||
GAction::GAction(const StringView& text, RefPtr<GraphicsBitmap>&& icon, Function<void(GAction&)> on_activation_callback, GWidget* widget)
|
||||
: on_activation(move(on_activation_callback))
|
||||
, m_text(text)
|
||||
, m_icon(move(icon))
|
||||
, m_widget(widget ? widget->make_weak_ptr() : nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
GAction::GAction(const StringView& text, const GShortcut& shortcut, Function<void(GAction&)> on_activation_callback, GWidget* widget)
|
||||
: GAction(text, shortcut, nullptr, move(on_activation_callback), widget)
|
||||
{
|
||||
}
|
||||
|
||||
GAction::GAction(const StringView& text, const GShortcut& shortcut, RefPtr<GraphicsBitmap>&& icon, Function<void(GAction&)> on_activation_callback, GWidget* widget)
|
||||
: on_activation(move(on_activation_callback))
|
||||
, m_text(text)
|
||||
, m_icon(move(icon))
|
||||
, m_shortcut(shortcut)
|
||||
, m_widget(widget ? widget->make_weak_ptr() : nullptr)
|
||||
{
|
||||
if (m_widget) {
|
||||
m_scope = ShortcutScope::WidgetLocal;
|
||||
m_widget->register_local_shortcut_action({}, *this);
|
||||
} else {
|
||||
m_scope = ShortcutScope::ApplicationGlobal;
|
||||
GApplication::the().register_global_shortcut_action({}, *this);
|
||||
}
|
||||
}
|
||||
|
||||
GAction::~GAction()
|
||||
{
|
||||
if (m_shortcut.is_valid() && m_scope == ShortcutScope::ApplicationGlobal)
|
||||
GApplication::the().unregister_global_shortcut_action({}, *this);
|
||||
if (m_widget && m_scope == ShortcutScope::WidgetLocal)
|
||||
m_widget->unregister_local_shortcut_action({}, *this);
|
||||
}
|
||||
|
||||
void GAction::activate()
|
||||
{
|
||||
if (on_activation)
|
||||
on_activation(*this);
|
||||
}
|
||||
|
||||
void GAction::register_button(Badge<GButton>, GButton& button)
|
||||
{
|
||||
m_buttons.set(&button);
|
||||
}
|
||||
|
||||
void GAction::unregister_button(Badge<GButton>, GButton& button)
|
||||
{
|
||||
m_buttons.remove(&button);
|
||||
}
|
||||
|
||||
void GAction::register_menu_item(Badge<GMenuItem>, GMenuItem& menu_item)
|
||||
{
|
||||
m_menu_items.set(&menu_item);
|
||||
}
|
||||
|
||||
void GAction::unregister_menu_item(Badge<GMenuItem>, GMenuItem& menu_item)
|
||||
{
|
||||
m_menu_items.remove(&menu_item);
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void GAction::for_each_toolbar_button(Callback callback)
|
||||
{
|
||||
for (auto& it : m_buttons)
|
||||
callback(*it);
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void GAction::for_each_menu_item(Callback callback)
|
||||
{
|
||||
for (auto& it : m_menu_items)
|
||||
callback(*it);
|
||||
}
|
||||
|
||||
void GAction::set_enabled(bool enabled)
|
||||
{
|
||||
if (m_enabled == enabled)
|
||||
return;
|
||||
m_enabled = enabled;
|
||||
for_each_toolbar_button([enabled](GButton& button) {
|
||||
button.set_enabled(enabled);
|
||||
});
|
||||
for_each_menu_item([enabled](GMenuItem& item) {
|
||||
item.set_enabled(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
void GAction::set_checked(bool checked)
|
||||
{
|
||||
if (m_checked == checked)
|
||||
return;
|
||||
m_checked = checked;
|
||||
for_each_toolbar_button([checked](GButton& button) {
|
||||
button.set_checked(checked);
|
||||
});
|
||||
for_each_menu_item([checked](GMenuItem& item) {
|
||||
item.set_checked(checked);
|
||||
});
|
||||
}
|
102
Libraries/LibGUI/GAction.h
Normal file
102
Libraries/LibGUI/GAction.h
Normal file
|
@ -0,0 +1,102 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <AK/Weakable.h>
|
||||
#include <LibGUI/GShortcut.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
|
||||
class GButton;
|
||||
class GMenuItem;
|
||||
class GWidget;
|
||||
|
||||
class GAction : public RefCounted<GAction>
|
||||
, public Weakable<GAction> {
|
||||
public:
|
||||
enum class ShortcutScope {
|
||||
None,
|
||||
ApplicationGlobal,
|
||||
WidgetLocal,
|
||||
};
|
||||
static NonnullRefPtr<GAction> create(const StringView& text, Function<void(GAction&)> callback, GWidget* widget = nullptr)
|
||||
{
|
||||
return adopt(*new GAction(text, move(callback), widget));
|
||||
}
|
||||
static NonnullRefPtr<GAction> create(const StringView& text, const StringView& custom_data, Function<void(GAction&)> callback, GWidget* widget = nullptr)
|
||||
{
|
||||
return adopt(*new GAction(text, custom_data, move(callback), widget));
|
||||
}
|
||||
static NonnullRefPtr<GAction> create(const StringView& text, RefPtr<GraphicsBitmap>&& icon, Function<void(GAction&)> callback, GWidget* widget = nullptr)
|
||||
{
|
||||
return adopt(*new GAction(text, move(icon), move(callback), widget));
|
||||
}
|
||||
static NonnullRefPtr<GAction> create(const StringView& text, const GShortcut& shortcut, Function<void(GAction&)> callback, GWidget* widget = nullptr)
|
||||
{
|
||||
return adopt(*new GAction(text, shortcut, move(callback), widget));
|
||||
}
|
||||
static NonnullRefPtr<GAction> create(const StringView& text, const GShortcut& shortcut, RefPtr<GraphicsBitmap>&& icon, Function<void(GAction&)> callback, GWidget* widget = nullptr)
|
||||
{
|
||||
return adopt(*new GAction(text, shortcut, move(icon), move(callback), widget));
|
||||
}
|
||||
~GAction();
|
||||
|
||||
GWidget* widget() { return m_widget.ptr(); }
|
||||
const GWidget* widget() const { return m_widget.ptr(); }
|
||||
|
||||
String text() const { return m_text; }
|
||||
GShortcut shortcut() const { return m_shortcut; }
|
||||
String custom_data() const { return m_custom_data; }
|
||||
const GraphicsBitmap* icon() const { return m_icon.ptr(); }
|
||||
|
||||
Function<void(GAction&)> on_activation;
|
||||
|
||||
void activate();
|
||||
|
||||
bool is_enabled() const { return m_enabled; }
|
||||
void set_enabled(bool);
|
||||
|
||||
bool is_checkable() const { return m_checkable; }
|
||||
void set_checkable(bool checkable) { m_checkable = checkable; }
|
||||
|
||||
bool is_checked() const
|
||||
{
|
||||
ASSERT(is_checkable());
|
||||
return m_checked;
|
||||
}
|
||||
void set_checked(bool);
|
||||
|
||||
void register_button(Badge<GButton>, GButton&);
|
||||
void unregister_button(Badge<GButton>, GButton&);
|
||||
void register_menu_item(Badge<GMenuItem>, GMenuItem&);
|
||||
void unregister_menu_item(Badge<GMenuItem>, GMenuItem&);
|
||||
|
||||
private:
|
||||
GAction(const StringView& text, Function<void(GAction&)> = nullptr, GWidget* = nullptr);
|
||||
GAction(const StringView& text, const GShortcut&, Function<void(GAction&)> = nullptr, GWidget* = nullptr);
|
||||
GAction(const StringView& text, const GShortcut&, RefPtr<GraphicsBitmap>&& icon, Function<void(GAction&)> = nullptr, GWidget* = nullptr);
|
||||
GAction(const StringView& text, RefPtr<GraphicsBitmap>&& icon, Function<void(GAction&)> = nullptr, GWidget* = nullptr);
|
||||
GAction(const StringView& text, const StringView& custom_data = StringView(), Function<void(GAction&)> = nullptr, GWidget* = nullptr);
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_toolbar_button(Callback);
|
||||
template<typename Callback>
|
||||
void for_each_menu_item(Callback);
|
||||
|
||||
String m_text;
|
||||
String m_custom_data;
|
||||
RefPtr<GraphicsBitmap> m_icon;
|
||||
GShortcut m_shortcut;
|
||||
bool m_enabled { true };
|
||||
bool m_checkable { false };
|
||||
bool m_checked { false };
|
||||
ShortcutScope m_scope { ShortcutScope::None };
|
||||
|
||||
HashTable<GButton*> m_buttons;
|
||||
HashTable<GMenuItem*> m_menu_items;
|
||||
WeakPtr<GWidget> m_widget;
|
||||
};
|
113
Libraries/LibGUI/GApplication.cpp
Normal file
113
Libraries/LibGUI/GApplication.cpp
Normal file
|
@ -0,0 +1,113 @@
|
|||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GApplication.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
#include <LibGUI/GLabel.h>
|
||||
#include <LibGUI/GMenuBar.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
#include <WindowServer/WSAPITypes.h>
|
||||
|
||||
static GApplication* s_the;
|
||||
|
||||
GApplication& GApplication::the()
|
||||
{
|
||||
ASSERT(s_the);
|
||||
return *s_the;
|
||||
}
|
||||
|
||||
GApplication::GApplication(int argc, char** argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
ASSERT(!s_the);
|
||||
s_the = this;
|
||||
m_event_loop = make<GEventLoop>();
|
||||
}
|
||||
|
||||
GApplication::~GApplication()
|
||||
{
|
||||
s_the = nullptr;
|
||||
}
|
||||
|
||||
int GApplication::exec()
|
||||
{
|
||||
int exit_code = m_event_loop->exec();
|
||||
// NOTE: Maybe it would be cool to return instead of exit()?
|
||||
// This would require cleaning up all the CObjects on the heap.
|
||||
exit(exit_code);
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
void GApplication::quit(int exit_code)
|
||||
{
|
||||
m_event_loop->quit(exit_code);
|
||||
}
|
||||
|
||||
void GApplication::set_menubar(OwnPtr<GMenuBar>&& menubar)
|
||||
{
|
||||
if (m_menubar)
|
||||
m_menubar->notify_removed_from_application({});
|
||||
m_menubar = move(menubar);
|
||||
if (m_menubar)
|
||||
m_menubar->notify_added_to_application({});
|
||||
}
|
||||
|
||||
void GApplication::register_global_shortcut_action(Badge<GAction>, GAction& action)
|
||||
{
|
||||
m_global_shortcut_actions.set(action.shortcut(), &action);
|
||||
}
|
||||
|
||||
void GApplication::unregister_global_shortcut_action(Badge<GAction>, GAction& action)
|
||||
{
|
||||
m_global_shortcut_actions.remove(action.shortcut());
|
||||
}
|
||||
|
||||
GAction* GApplication::action_for_key_event(const GKeyEvent& event)
|
||||
{
|
||||
auto it = m_global_shortcut_actions.find(GShortcut(event.modifiers(), (KeyCode)event.key()));
|
||||
if (it == m_global_shortcut_actions.end())
|
||||
return nullptr;
|
||||
return (*it).value;
|
||||
}
|
||||
|
||||
class GApplication::TooltipWindow final : public GWindow {
|
||||
public:
|
||||
TooltipWindow()
|
||||
{
|
||||
set_window_type(GWindowType::Tooltip);
|
||||
m_label = new GLabel;
|
||||
m_label->set_background_color(Color::from_rgb(0xdac7b5));
|
||||
m_label->set_fill_with_background_color(true);
|
||||
m_label->set_frame_thickness(1);
|
||||
m_label->set_frame_shape(FrameShape::Container);
|
||||
m_label->set_frame_shadow(FrameShadow::Plain);
|
||||
set_main_widget(m_label);
|
||||
}
|
||||
|
||||
void set_tooltip(const StringView& tooltip)
|
||||
{
|
||||
// FIXME: Add some kind of GLabel auto-sizing feature.
|
||||
int text_width = m_label->font().width(tooltip);
|
||||
set_rect(100, 100, text_width + 10, m_label->font().glyph_height() + 8);
|
||||
m_label->set_text(tooltip);
|
||||
}
|
||||
|
||||
GLabel* m_label { nullptr };
|
||||
};
|
||||
|
||||
void GApplication::show_tooltip(const StringView& tooltip, const Point& screen_location)
|
||||
{
|
||||
if (!m_tooltip_window) {
|
||||
m_tooltip_window = new TooltipWindow;
|
||||
m_tooltip_window->set_double_buffering_enabled(false);
|
||||
}
|
||||
m_tooltip_window->set_tooltip(tooltip);
|
||||
m_tooltip_window->move_to(screen_location);
|
||||
m_tooltip_window->show();
|
||||
}
|
||||
|
||||
void GApplication::hide_tooltip()
|
||||
{
|
||||
if (m_tooltip_window)
|
||||
m_tooltip_window->hide();
|
||||
}
|
38
Libraries/LibGUI/GApplication.h
Normal file
38
Libraries/LibGUI/GApplication.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibGUI/GShortcut.h>
|
||||
|
||||
class GAction;
|
||||
class GKeyEvent;
|
||||
class GEventLoop;
|
||||
class GMenuBar;
|
||||
class Point;
|
||||
|
||||
class GApplication {
|
||||
public:
|
||||
static GApplication& the();
|
||||
GApplication(int argc, char** argv);
|
||||
~GApplication();
|
||||
|
||||
int exec();
|
||||
void quit(int = 0);
|
||||
|
||||
void set_menubar(OwnPtr<GMenuBar>&&);
|
||||
GAction* action_for_key_event(const GKeyEvent&);
|
||||
|
||||
void register_global_shortcut_action(Badge<GAction>, GAction&);
|
||||
void unregister_global_shortcut_action(Badge<GAction>, GAction&);
|
||||
|
||||
void show_tooltip(const StringView&, const Point& screen_location);
|
||||
void hide_tooltip();
|
||||
|
||||
private:
|
||||
OwnPtr<GEventLoop> m_event_loop;
|
||||
OwnPtr<GMenuBar> m_menubar;
|
||||
HashMap<GShortcut, GAction*> m_global_shortcut_actions;
|
||||
class TooltipWindow;
|
||||
TooltipWindow* m_tooltip_window { nullptr };
|
||||
};
|
132
Libraries/LibGUI/GBoxLayout.cpp
Normal file
132
Libraries/LibGUI/GBoxLayout.cpp
Normal file
|
@ -0,0 +1,132 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
#include <stdio.h>
|
||||
|
||||
//#define GBOXLAYOUT_DEBUG
|
||||
|
||||
GBoxLayout::GBoxLayout(Orientation orientation)
|
||||
: m_orientation(orientation)
|
||||
{
|
||||
}
|
||||
|
||||
GBoxLayout::~GBoxLayout()
|
||||
{
|
||||
}
|
||||
|
||||
void GBoxLayout::run(GWidget& widget)
|
||||
{
|
||||
bool should_log = false;
|
||||
#ifdef GBOXLAYOUT_DEBUG
|
||||
should_log = true;
|
||||
#endif
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: running layout on %s{%p}, entry count: %d\n", widget.class_name(), &widget, m_entries.size());
|
||||
|
||||
if (m_entries.is_empty())
|
||||
return;
|
||||
|
||||
Size available_size = widget.size();
|
||||
int number_of_entries_with_fixed_size = 0;
|
||||
|
||||
int number_of_visible_entries = 0;
|
||||
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: Starting with available size: %s\n", available_size.to_string().characters());
|
||||
|
||||
for (auto& entry : m_entries) {
|
||||
if (entry.type == Entry::Type::Spacer) {
|
||||
++number_of_visible_entries;
|
||||
}
|
||||
if (!entry.widget)
|
||||
continue;
|
||||
|
||||
if (!entry.widget->is_visible())
|
||||
continue;
|
||||
++number_of_visible_entries;
|
||||
if (entry.widget && entry.widget->size_policy(orientation()) == SizePolicy::Fixed) {
|
||||
if (should_log) {
|
||||
dbgprintf("GBoxLayout: Subtracting for fixed %s{%p}, size: %s\n", entry.widget->class_name(), entry.widget.ptr(), entry.widget->preferred_size().to_string().characters());
|
||||
dbgprintf("GBoxLayout: Available size before: %s\n", available_size.to_string().characters());
|
||||
}
|
||||
available_size -= entry.widget->preferred_size();
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: Available size after: %s\n", available_size.to_string().characters());
|
||||
++number_of_entries_with_fixed_size;
|
||||
}
|
||||
available_size -= { spacing(), spacing() };
|
||||
}
|
||||
|
||||
available_size += { spacing(), spacing() };
|
||||
|
||||
available_size -= { margins().left() + margins().right(), margins().top() + margins().bottom() };
|
||||
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: Number of visible: %d/%d\n", number_of_visible_entries, m_entries.size());
|
||||
|
||||
int number_of_entries_with_automatic_size = number_of_visible_entries - number_of_entries_with_fixed_size;
|
||||
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: available_size=%s, fixed=%d, fill=%d\n", available_size.to_string().characters(), number_of_entries_with_fixed_size, number_of_entries_with_automatic_size);
|
||||
|
||||
Size automatic_size;
|
||||
|
||||
if (number_of_entries_with_automatic_size) {
|
||||
if (m_orientation == Orientation::Horizontal) {
|
||||
automatic_size.set_width(available_size.width() / number_of_entries_with_automatic_size);
|
||||
automatic_size.set_height(widget.height());
|
||||
} else {
|
||||
automatic_size.set_width(widget.width());
|
||||
automatic_size.set_height(available_size.height() / number_of_entries_with_automatic_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: automatic_size=%s\n", automatic_size.to_string().characters());
|
||||
|
||||
int current_x = margins().left();
|
||||
int current_y = margins().top();
|
||||
|
||||
for (auto& entry : m_entries) {
|
||||
if (entry.type == Entry::Type::Spacer) {
|
||||
current_x += automatic_size.width();
|
||||
current_y += automatic_size.height();
|
||||
}
|
||||
|
||||
if (!entry.widget)
|
||||
continue;
|
||||
if (!entry.widget->is_visible())
|
||||
continue;
|
||||
Rect rect(current_x, current_y, 0, 0);
|
||||
if (entry.layout) {
|
||||
// FIXME: Implement recursive layout.
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
ASSERT(entry.widget);
|
||||
rect.set_size(automatic_size);
|
||||
|
||||
if (entry.widget->size_policy(Orientation::Vertical) == SizePolicy::Fixed)
|
||||
rect.set_height(entry.widget->preferred_size().height());
|
||||
|
||||
if (entry.widget->size_policy(Orientation::Horizontal) == SizePolicy::Fixed)
|
||||
rect.set_width(entry.widget->preferred_size().width());
|
||||
|
||||
if (orientation() == Orientation::Horizontal) {
|
||||
if (entry.widget->size_policy(Orientation::Vertical) == SizePolicy::Fill)
|
||||
rect.set_height(widget.height() - margins().top() - margins().bottom());
|
||||
rect.center_vertically_within(widget.rect());
|
||||
} else {
|
||||
if (entry.widget->size_policy(Orientation::Horizontal) == SizePolicy::Fill)
|
||||
rect.set_width(widget.width() - margins().left() - margins().right());
|
||||
rect.center_horizontally_within(widget.rect());
|
||||
}
|
||||
|
||||
if (should_log)
|
||||
dbgprintf("GBoxLayout: apply, %s{%p} <- %s\n", entry.widget->class_name(), entry.widget.ptr(), rect.to_string().characters());
|
||||
entry.widget->set_relative_rect(rect);
|
||||
|
||||
if (orientation() == Orientation::Horizontal)
|
||||
current_x += rect.width() + spacing();
|
||||
else
|
||||
current_y += rect.height() + spacing();
|
||||
}
|
||||
}
|
17
Libraries/LibGUI/GBoxLayout.h
Normal file
17
Libraries/LibGUI/GBoxLayout.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GLayout.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GBoxLayout final : public GLayout {
|
||||
public:
|
||||
explicit GBoxLayout(Orientation);
|
||||
virtual ~GBoxLayout() override;
|
||||
|
||||
Orientation orientation() const { return m_orientation; }
|
||||
|
||||
virtual void run(GWidget&) override;
|
||||
|
||||
private:
|
||||
Orientation m_orientation;
|
||||
};
|
90
Libraries/LibGUI/GButton.cpp
Normal file
90
Libraries/LibGUI/GButton.cpp
Normal file
|
@ -0,0 +1,90 @@
|
|||
#include "GButton.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <Kernel/KeyCode.h>
|
||||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
GButton::GButton(GWidget* parent)
|
||||
: GAbstractButton(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GButton::GButton(const StringView& text, GWidget* parent)
|
||||
: GAbstractButton(text, parent)
|
||||
{
|
||||
}
|
||||
|
||||
GButton::~GButton()
|
||||
{
|
||||
if (m_action)
|
||||
m_action->unregister_button({}, *this);
|
||||
}
|
||||
|
||||
void GButton::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
StylePainter::paint_button(painter, rect(), m_button_style, is_being_pressed(), is_hovered(), is_checkable() && is_checked(), is_enabled());
|
||||
|
||||
if (text().is_empty() && !m_icon)
|
||||
return;
|
||||
|
||||
auto content_rect = rect().shrunken(10, 2);
|
||||
auto icon_location = m_icon ? content_rect.center().translated(-(m_icon->width() / 2), -(m_icon->height() / 2)) : Point();
|
||||
if (m_icon && !text().is_empty())
|
||||
icon_location.set_x(content_rect.x());
|
||||
if (is_being_pressed())
|
||||
painter.translate(1, 1);
|
||||
if (m_icon) {
|
||||
if (is_enabled())
|
||||
painter.blit(icon_location, *m_icon, m_icon->rect());
|
||||
else
|
||||
painter.blit_dimmed(icon_location, *m_icon, m_icon->rect());
|
||||
}
|
||||
auto& font = (is_checkable() && is_checked()) ? Font::default_bold_font() : this->font();
|
||||
if (m_icon && !text().is_empty()) {
|
||||
content_rect.move_by(m_icon->width() + 4, 0);
|
||||
content_rect.set_width(content_rect.width() - m_icon->width() - 4);
|
||||
}
|
||||
|
||||
Rect text_rect { 0, 0, font.width(text()), font.glyph_height() };
|
||||
if (text_rect.width() > content_rect.width())
|
||||
text_rect.set_width(content_rect.width());
|
||||
text_rect.align_within(content_rect, text_alignment());
|
||||
paint_text(painter, text_rect, font, TextAlignment::Center);
|
||||
}
|
||||
|
||||
void GButton::click()
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
if (is_checkable())
|
||||
set_checked(!is_checked());
|
||||
if (on_click)
|
||||
on_click(*this);
|
||||
}
|
||||
|
||||
bool GButton::supports_keyboard_activation() const
|
||||
{
|
||||
return is_enabled();
|
||||
}
|
||||
|
||||
void GButton::set_action(GAction& action)
|
||||
{
|
||||
m_action = action.make_weak_ptr();
|
||||
action.register_button({}, *this);
|
||||
set_enabled(action.is_enabled());
|
||||
set_checkable(action.is_checkable());
|
||||
if (action.is_checkable())
|
||||
set_checked(action.is_checked());
|
||||
}
|
||||
|
||||
void GButton::set_icon(RefPtr<GraphicsBitmap>&& icon)
|
||||
{
|
||||
if (m_icon == icon)
|
||||
return;
|
||||
m_icon = move(icon);
|
||||
update();
|
||||
}
|
49
Libraries/LibGUI/GButton.h
Normal file
49
Libraries/LibGUI/GButton.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <LibGUI/GAbstractButton.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
#include <SharedGraphics/TextAlignment.h>
|
||||
|
||||
class GAction;
|
||||
|
||||
class GButton : public GAbstractButton {
|
||||
public:
|
||||
GButton(const StringView& text, GWidget* parent);
|
||||
explicit GButton(GWidget* parent);
|
||||
virtual ~GButton() override;
|
||||
|
||||
void set_icon(RefPtr<GraphicsBitmap>&&);
|
||||
const GraphicsBitmap* icon() const { return m_icon.ptr(); }
|
||||
GraphicsBitmap* icon() { return m_icon.ptr(); }
|
||||
|
||||
void set_text_alignment(TextAlignment text_alignment) { m_text_alignment = text_alignment; }
|
||||
TextAlignment text_alignment() const { return m_text_alignment; }
|
||||
|
||||
Function<void(GButton&)> on_click;
|
||||
|
||||
void set_button_style(ButtonStyle style) { m_button_style = style; }
|
||||
ButtonStyle button_style() const { return m_button_style; }
|
||||
|
||||
virtual void click() override;
|
||||
|
||||
void set_action(GAction&);
|
||||
|
||||
virtual const char* class_name() const override { return "GButton"; }
|
||||
virtual bool accepts_focus() const override { return m_focusable; }
|
||||
virtual bool supports_keyboard_activation() const override;
|
||||
|
||||
void set_focusable(bool b) { m_focusable = b; }
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
private:
|
||||
RefPtr<GraphicsBitmap> m_icon;
|
||||
ButtonStyle m_button_style { ButtonStyle::Normal };
|
||||
TextAlignment m_text_alignment { TextAlignment::Center };
|
||||
WeakPtr<GAction> m_action;
|
||||
bool m_focusable { true };
|
||||
};
|
77
Libraries/LibGUI/GCheckBox.cpp
Normal file
77
Libraries/LibGUI/GCheckBox.cpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#include <Kernel/KeyCode.h>
|
||||
#include <LibGUI/GCheckBox.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <SharedGraphics/CharacterBitmap.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
static const char* s_checked_bitmap_data = {
|
||||
" "
|
||||
" # "
|
||||
" ## "
|
||||
" ### "
|
||||
" ## ### "
|
||||
" ##### "
|
||||
" ### "
|
||||
" # "
|
||||
" "
|
||||
};
|
||||
|
||||
static CharacterBitmap* s_checked_bitmap;
|
||||
static const int s_checked_bitmap_width = 9;
|
||||
static const int s_checked_bitmap_height = 9;
|
||||
static const int s_box_width = 13;
|
||||
static const int s_box_height = 13;
|
||||
|
||||
GCheckBox::GCheckBox(GWidget* parent)
|
||||
: GAbstractButton(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GCheckBox::GCheckBox(const StringView& text, GWidget* parent)
|
||||
: GAbstractButton(text, parent)
|
||||
{
|
||||
}
|
||||
|
||||
GCheckBox::~GCheckBox()
|
||||
{
|
||||
}
|
||||
|
||||
void GCheckBox::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
auto text_rect = rect();
|
||||
text_rect.set_left(s_box_width + 4);
|
||||
text_rect.set_width(font().width(text()));
|
||||
text_rect.set_top(height() / 2 - font().glyph_height() / 2);
|
||||
text_rect.set_height(font().glyph_height());
|
||||
|
||||
if (fill_with_background_color())
|
||||
painter.fill_rect(rect(), background_color());
|
||||
|
||||
Rect box_rect {
|
||||
0, height() / 2 - s_box_height / 2 - 1,
|
||||
s_box_width, s_box_height
|
||||
};
|
||||
painter.fill_rect(box_rect, Color::White);
|
||||
StylePainter::paint_frame(painter, box_rect, FrameShape::Container, FrameShadow::Sunken, 2);
|
||||
|
||||
if (is_being_pressed())
|
||||
painter.draw_rect(box_rect.shrunken(4, 4), Color::MidGray);
|
||||
|
||||
if (is_checked()) {
|
||||
if (!s_checked_bitmap)
|
||||
s_checked_bitmap = &CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref();
|
||||
painter.draw_bitmap(box_rect.shrunken(4, 4).location(), *s_checked_bitmap, foreground_color());
|
||||
}
|
||||
|
||||
paint_text(painter, text_rect, font(), TextAlignment::TopLeft);
|
||||
}
|
||||
|
||||
void GCheckBox::click()
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
set_checked(!is_checked());
|
||||
}
|
19
Libraries/LibGUI/GCheckBox.h
Normal file
19
Libraries/LibGUI/GCheckBox.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <LibGUI/GAbstractButton.h>
|
||||
|
||||
class GCheckBox : public GAbstractButton {
|
||||
public:
|
||||
GCheckBox(const StringView&, GWidget* parent);
|
||||
explicit GCheckBox(GWidget* parent);
|
||||
virtual ~GCheckBox() override;
|
||||
|
||||
virtual void click() override;
|
||||
|
||||
virtual const char* class_name() const override { return "GCheckBox"; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
};
|
55
Libraries/LibGUI/GClipboard.cpp
Normal file
55
Libraries/LibGUI/GClipboard.cpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#include <LibC/SharedBuffer.h>
|
||||
#include <LibGUI/GClipboard.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
#include <WindowServer/WSAPITypes.h>
|
||||
|
||||
GClipboard& GClipboard::the()
|
||||
{
|
||||
static GClipboard* s_the;
|
||||
if (!s_the)
|
||||
s_the = new GClipboard;
|
||||
return *s_the;
|
||||
}
|
||||
|
||||
GClipboard::GClipboard()
|
||||
{
|
||||
}
|
||||
|
||||
String GClipboard::data() const
|
||||
{
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::GetClipboardContents;
|
||||
auto response = GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidGetClipboardContents);
|
||||
if (response.clipboard.shared_buffer_id < 0)
|
||||
return {};
|
||||
auto shared_buffer = SharedBuffer::create_from_shared_buffer_id(response.clipboard.shared_buffer_id);
|
||||
if (!shared_buffer) {
|
||||
dbgprintf("GClipboard::data() failed to attach to the shared buffer\n");
|
||||
return {};
|
||||
}
|
||||
if (response.clipboard.contents_size > shared_buffer->size()) {
|
||||
dbgprintf("GClipboard::data() clipping contents size is greater than shared buffer size\n");
|
||||
return {};
|
||||
}
|
||||
return String((const char*)shared_buffer->data(), response.clipboard.contents_size);
|
||||
}
|
||||
|
||||
void GClipboard::set_data(const StringView& data)
|
||||
{
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::SetClipboardContents;
|
||||
auto shared_buffer = SharedBuffer::create(GEventLoop::current().server_pid(), data.length() + 1);
|
||||
if (!shared_buffer) {
|
||||
dbgprintf("GClipboard::set_data() failed to create a shared buffer\n");
|
||||
return;
|
||||
}
|
||||
if (!data.is_empty())
|
||||
memcpy(shared_buffer->data(), data.characters(), data.length() + 1);
|
||||
else
|
||||
((u8*)shared_buffer->data())[0] = '\0';
|
||||
shared_buffer->seal();
|
||||
request.clipboard.shared_buffer_id = shared_buffer->shared_buffer_id();
|
||||
request.clipboard.contents_size = data.length();
|
||||
auto response = GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidSetClipboardContents);
|
||||
ASSERT(response.clipboard.shared_buffer_id == shared_buffer->shared_buffer_id());
|
||||
}
|
14
Libraries/LibGUI/GClipboard.h
Normal file
14
Libraries/LibGUI/GClipboard.h
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
|
||||
class GClipboard {
|
||||
public:
|
||||
static GClipboard& the();
|
||||
|
||||
String data() const;
|
||||
void set_data(const StringView&);
|
||||
|
||||
private:
|
||||
GClipboard();
|
||||
};
|
118
Libraries/LibGUI/GComboBox.cpp
Normal file
118
Libraries/LibGUI/GComboBox.cpp
Normal file
|
@ -0,0 +1,118 @@
|
|||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GComboBox.h>
|
||||
#include <LibGUI/GListView.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
#include <LibGUI/GTextEditor.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
|
||||
GComboBox::GComboBox(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
m_editor = new GTextEditor(GTextEditor::Type::SingleLine, this);
|
||||
m_editor->on_change = [this] {
|
||||
if (on_change)
|
||||
on_change(m_editor->text());
|
||||
};
|
||||
m_editor->on_return_pressed = [this] {
|
||||
if (on_return_pressed)
|
||||
on_return_pressed();
|
||||
};
|
||||
m_open_button = new GButton(this);
|
||||
m_open_button->set_focusable(false);
|
||||
m_open_button->set_text("\xf7");
|
||||
m_open_button->on_click = [this](auto&) {
|
||||
if (m_list_window->is_visible())
|
||||
close();
|
||||
else
|
||||
open();
|
||||
};
|
||||
|
||||
m_list_window = new GWindow(this);
|
||||
// FIXME: This is obviously not a tooltip window, but it's the closest thing to what we want atm.
|
||||
m_list_window->set_window_type(GWindowType::Tooltip);
|
||||
m_list_window->set_should_destroy_on_close(false);
|
||||
|
||||
m_list_view = new GListView(nullptr);
|
||||
m_list_view->horizontal_scrollbar().set_visible(false);
|
||||
m_list_window->set_main_widget(m_list_view);
|
||||
|
||||
m_list_view->on_selection = [this](auto& index) {
|
||||
ASSERT(model());
|
||||
auto new_value = model()->data(index).to_string();
|
||||
m_editor->set_text(new_value);
|
||||
m_editor->select_all();
|
||||
close();
|
||||
deferred_invoke([this](auto&) {
|
||||
if (on_change)
|
||||
on_change(m_editor->text());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
GComboBox::~GComboBox()
|
||||
{
|
||||
}
|
||||
|
||||
void GComboBox::resize_event(GResizeEvent& event)
|
||||
{
|
||||
int frame_thickness = m_editor->frame_thickness();
|
||||
int button_height = event.size().height() - frame_thickness * 2;
|
||||
int button_width = 15;
|
||||
m_open_button->set_relative_rect(width() - button_width - frame_thickness, frame_thickness, button_width, button_height);
|
||||
m_editor->set_relative_rect(0, 0, width(), height());
|
||||
}
|
||||
|
||||
void GComboBox::set_model(NonnullRefPtr<GModel> model)
|
||||
{
|
||||
m_list_view->set_model(move(model));
|
||||
}
|
||||
|
||||
void GComboBox::select_all()
|
||||
{
|
||||
m_editor->select_all();
|
||||
}
|
||||
|
||||
void GComboBox::open()
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
|
||||
auto my_screen_rect = screen_relative_rect();
|
||||
|
||||
int longest_item_width = 0;
|
||||
for (int i = 0; i < model()->row_count(); ++i) {
|
||||
auto index = model()->index(i);
|
||||
auto item_text = model()->data(index).to_string();
|
||||
longest_item_width = max(longest_item_width, m_list_view->font().width(item_text));
|
||||
}
|
||||
Size size {
|
||||
max(width(), longest_item_width + m_list_view->width_occupied_by_vertical_scrollbar() + m_list_view->frame_thickness() * 2 + m_list_view->horizontal_padding()),
|
||||
model()->row_count() * m_list_view->item_height() + m_list_view->frame_thickness() * 2
|
||||
};
|
||||
|
||||
m_list_window->set_rect({ my_screen_rect.bottom_left(), size });
|
||||
m_list_window->show();
|
||||
}
|
||||
|
||||
void GComboBox::close()
|
||||
{
|
||||
m_list_window->hide();
|
||||
m_editor->set_focus(true);
|
||||
}
|
||||
|
||||
String GComboBox::text() const
|
||||
{
|
||||
return m_editor->text();
|
||||
}
|
||||
|
||||
void GComboBox::set_text(const String& text)
|
||||
{
|
||||
m_editor->set_text(text);
|
||||
}
|
||||
|
||||
void GComboBox::set_only_allow_values_from_model(bool b)
|
||||
{
|
||||
if (m_only_allow_values_from_model == b)
|
||||
return;
|
||||
m_editor->set_readonly(m_only_allow_values_from_model);
|
||||
}
|
42
Libraries/LibGUI/GComboBox.h
Normal file
42
Libraries/LibGUI/GComboBox.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GListView.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GButton;
|
||||
class GTextEditor;
|
||||
|
||||
class GComboBox : public GWidget {
|
||||
public:
|
||||
explicit GComboBox(GWidget* parent = nullptr);
|
||||
virtual ~GComboBox() override;
|
||||
|
||||
String text() const;
|
||||
void set_text(const String&);
|
||||
|
||||
void open();
|
||||
void close();
|
||||
void select_all();
|
||||
|
||||
GModel* model() { return m_list_view->model(); }
|
||||
const GModel* model() const { return m_list_view->model(); }
|
||||
void set_model(NonnullRefPtr<GModel>);
|
||||
|
||||
bool only_allow_values_from_model() const { return m_only_allow_values_from_model; }
|
||||
void set_only_allow_values_from_model(bool);
|
||||
|
||||
Function<void(const String&)> on_change;
|
||||
Function<void()> on_return_pressed;
|
||||
|
||||
virtual const char* class_name() const override { return "GComboBox"; }
|
||||
|
||||
protected:
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
|
||||
private:
|
||||
GTextEditor* m_editor { nullptr };
|
||||
GButton* m_open_button { nullptr };
|
||||
GWindow* m_list_window { nullptr };
|
||||
GListView* m_list_view { nullptr };
|
||||
bool m_only_allow_values_from_model { false };
|
||||
};
|
44
Libraries/LibGUI/GDesktop.cpp
Normal file
44
Libraries/LibGUI/GDesktop.cpp
Normal file
|
@ -0,0 +1,44 @@
|
|||
#include <LibGUI/GDesktop.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
GDesktop& GDesktop::the()
|
||||
{
|
||||
static GDesktop* the;
|
||||
if (!the)
|
||||
the = new GDesktop;
|
||||
return *the;
|
||||
}
|
||||
|
||||
GDesktop::GDesktop()
|
||||
{
|
||||
}
|
||||
|
||||
void GDesktop::did_receive_screen_rect(Badge<GEventLoop>, const Rect& rect)
|
||||
{
|
||||
if (m_rect == rect)
|
||||
return;
|
||||
m_rect = rect;
|
||||
if (on_rect_change)
|
||||
on_rect_change(rect);
|
||||
}
|
||||
|
||||
bool GDesktop::set_wallpaper(const StringView& path)
|
||||
{
|
||||
WSAPI_ClientMessage message;
|
||||
message.type = WSAPI_ClientMessage::Type::SetWallpaper;
|
||||
ASSERT(path.length() < (int)sizeof(message.text));
|
||||
strncpy(message.text, path.characters(), path.length());
|
||||
message.text_length = path.length();
|
||||
auto response = GEventLoop::current().sync_request(message, WSAPI_ServerMessage::Type::DidSetWallpaper);
|
||||
return response.value;
|
||||
}
|
||||
|
||||
String GDesktop::wallpaper() const
|
||||
{
|
||||
WSAPI_ClientMessage message;
|
||||
message.type = WSAPI_ClientMessage::Type::GetWallpaper;
|
||||
auto response = GEventLoop::current().sync_request(message, WSAPI_ServerMessage::Type::DidGetWallpaper);
|
||||
return String(response.text, response.text_length);
|
||||
}
|
25
Libraries/LibGUI/GDesktop.h
Normal file
25
Libraries/LibGUI/GDesktop.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/Function.h>
|
||||
#include <SharedGraphics/Rect.h>
|
||||
|
||||
class GEventLoop;
|
||||
|
||||
class GDesktop {
|
||||
public:
|
||||
static GDesktop& the();
|
||||
GDesktop();
|
||||
|
||||
String wallpaper() const;
|
||||
bool set_wallpaper(const StringView& path);
|
||||
|
||||
Rect rect() const { return m_rect; }
|
||||
void did_receive_screen_rect(Badge<GEventLoop>, const Rect&);
|
||||
|
||||
Function<void(const Rect&)> on_rect_change;
|
||||
|
||||
private:
|
||||
Rect m_rect;
|
||||
};
|
42
Libraries/LibGUI/GDialog.cpp
Normal file
42
Libraries/LibGUI/GDialog.cpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
#include <LibGUI/GDesktop.h>
|
||||
#include <LibGUI/GDialog.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
|
||||
GDialog::GDialog(CObject* parent)
|
||||
: GWindow(parent)
|
||||
{
|
||||
set_modal(true);
|
||||
set_should_exit_event_loop_on_close(true);
|
||||
}
|
||||
|
||||
GDialog::~GDialog()
|
||||
{
|
||||
}
|
||||
|
||||
int GDialog::exec()
|
||||
{
|
||||
ASSERT(!m_event_loop);
|
||||
m_event_loop = make<GEventLoop>();
|
||||
auto new_rect = rect();
|
||||
if (parent() && parent()->is_window()) {
|
||||
auto& parent_window = *static_cast<GWindow*>(parent());
|
||||
new_rect.center_within(parent_window.rect());
|
||||
} else {
|
||||
new_rect.center_within(GDesktop::the().rect());
|
||||
}
|
||||
set_rect(new_rect);
|
||||
show();
|
||||
auto result = m_event_loop->exec();
|
||||
m_event_loop = nullptr;
|
||||
dbgprintf("%s: event loop returned with result %d\n", class_name(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void GDialog::done(int result)
|
||||
{
|
||||
if (!m_event_loop)
|
||||
return;
|
||||
m_result = result;
|
||||
dbgprintf("%s: quit event loop with result %d\n", class_name(), result);
|
||||
m_event_loop->quit(result);
|
||||
}
|
29
Libraries/LibGUI/GDialog.h
Normal file
29
Libraries/LibGUI/GDialog.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
|
||||
class GDialog : public GWindow {
|
||||
public:
|
||||
enum ExecResult {
|
||||
ExecOK = 0,
|
||||
ExecCancel = 1,
|
||||
ExecAborted = 2
|
||||
};
|
||||
|
||||
virtual ~GDialog() override;
|
||||
|
||||
int exec();
|
||||
|
||||
int result() const { return m_result; }
|
||||
void done(int result);
|
||||
|
||||
virtual const char* class_name() const override { return "GDialog"; }
|
||||
|
||||
protected:
|
||||
explicit GDialog(CObject* parent);
|
||||
|
||||
private:
|
||||
OwnPtr<GEventLoop> m_event_loop;
|
||||
int m_result { ExecAborted };
|
||||
};
|
315
Libraries/LibGUI/GDirectoryModel.cpp
Normal file
315
Libraries/LibGUI/GDirectoryModel.cpp
Normal file
|
@ -0,0 +1,315 @@
|
|||
#include "GDirectoryModel.h"
|
||||
#include <AK/FileSystemPath.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCore/CDirIterator.h>
|
||||
#include <LibCore/CLock.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
#include <dirent.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static CLockable<HashMap<String, RefPtr<GraphicsBitmap>>>& thumbnail_cache()
|
||||
{
|
||||
static CLockable<HashMap<String, RefPtr<GraphicsBitmap>>>* s_map;
|
||||
if (!s_map)
|
||||
s_map = new CLockable<HashMap<String, RefPtr<GraphicsBitmap>>>();
|
||||
return *s_map;
|
||||
}
|
||||
|
||||
int thumbnail_thread(void* model_ptr)
|
||||
{
|
||||
auto& model = *(GDirectoryModel*)model_ptr;
|
||||
for (;;) {
|
||||
sleep(1);
|
||||
Vector<String> to_generate;
|
||||
{
|
||||
LOCKER(thumbnail_cache().lock());
|
||||
to_generate.ensure_capacity(thumbnail_cache().resource().size());
|
||||
for (auto& it : thumbnail_cache().resource()) {
|
||||
if (it.value)
|
||||
continue;
|
||||
to_generate.append(it.key);
|
||||
}
|
||||
}
|
||||
if (to_generate.is_empty())
|
||||
continue;
|
||||
for (int i = 0; i < to_generate.size(); ++i) {
|
||||
auto& path = to_generate[i];
|
||||
auto png_bitmap = GraphicsBitmap::load_from_file(path);
|
||||
if (!png_bitmap)
|
||||
continue;
|
||||
auto thumbnail = GraphicsBitmap::create(png_bitmap->format(), { 32, 32 });
|
||||
Painter painter(*thumbnail);
|
||||
painter.draw_scaled_bitmap(thumbnail->rect(), *png_bitmap, png_bitmap->rect());
|
||||
{
|
||||
LOCKER(thumbnail_cache().lock());
|
||||
thumbnail_cache().resource().set(path, move(thumbnail));
|
||||
}
|
||||
if (model.on_thumbnail_progress)
|
||||
model.on_thumbnail_progress(i + 1, to_generate.size());
|
||||
model.did_update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GDirectoryModel::GDirectoryModel()
|
||||
{
|
||||
create_thread(thumbnail_thread, this);
|
||||
|
||||
m_directory_icon = GIcon::default_icon("filetype-folder");
|
||||
m_file_icon = GIcon::default_icon("filetype-unknown");
|
||||
m_symlink_icon = GIcon::default_icon("filetype-symlink");
|
||||
m_socket_icon = GIcon::default_icon("filetype-socket");
|
||||
m_executable_icon = GIcon::default_icon("filetype-executable");
|
||||
m_filetype_image_icon = GIcon::default_icon("filetype-image");
|
||||
|
||||
setpwent();
|
||||
while (auto* passwd = getpwent())
|
||||
m_user_names.set(passwd->pw_uid, passwd->pw_name);
|
||||
endpwent();
|
||||
|
||||
setgrent();
|
||||
while (auto* group = getgrent())
|
||||
m_group_names.set(group->gr_gid, group->gr_name);
|
||||
endgrent();
|
||||
}
|
||||
|
||||
GDirectoryModel::~GDirectoryModel()
|
||||
{
|
||||
}
|
||||
|
||||
int GDirectoryModel::row_count(const GModelIndex&) const
|
||||
{
|
||||
return m_directories.size() + m_files.size();
|
||||
}
|
||||
|
||||
int GDirectoryModel::column_count(const GModelIndex&) const
|
||||
{
|
||||
return Column::__Count;
|
||||
}
|
||||
|
||||
String GDirectoryModel::column_name(int column) const
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Icon:
|
||||
return "";
|
||||
case Column::Name:
|
||||
return "Name";
|
||||
case Column::Size:
|
||||
return "Size";
|
||||
case Column::Owner:
|
||||
return "Owner";
|
||||
case Column::Group:
|
||||
return "Group";
|
||||
case Column::Permissions:
|
||||
return "Mode";
|
||||
case Column::Inode:
|
||||
return "Inode";
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
GModel::ColumnMetadata GDirectoryModel::column_metadata(int column) const
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Icon:
|
||||
return { 16, TextAlignment::Center };
|
||||
case Column::Name:
|
||||
return { 120, TextAlignment::CenterLeft };
|
||||
case Column::Size:
|
||||
return { 80, TextAlignment::CenterRight };
|
||||
case Column::Owner:
|
||||
return { 50, TextAlignment::CenterLeft };
|
||||
case Column::Group:
|
||||
return { 50, TextAlignment::CenterLeft };
|
||||
case Column::Permissions:
|
||||
return { 80, TextAlignment::CenterLeft };
|
||||
case Column::Inode:
|
||||
return { 80, TextAlignment::CenterRight };
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
GIcon GDirectoryModel::icon_for(const Entry& entry) const
|
||||
{
|
||||
if (S_ISDIR(entry.mode))
|
||||
return m_directory_icon;
|
||||
if (S_ISLNK(entry.mode))
|
||||
return m_symlink_icon;
|
||||
if (S_ISSOCK(entry.mode))
|
||||
return m_socket_icon;
|
||||
if (entry.mode & S_IXUSR)
|
||||
return m_executable_icon;
|
||||
if (entry.name.to_lowercase().ends_with(".png")) {
|
||||
if (!entry.thumbnail) {
|
||||
auto path = entry.full_path(*this);
|
||||
LOCKER(thumbnail_cache().lock());
|
||||
auto it = thumbnail_cache().resource().find(path);
|
||||
if (it != thumbnail_cache().resource().end()) {
|
||||
entry.thumbnail = (*it).value.copy_ref();
|
||||
} else {
|
||||
thumbnail_cache().resource().set(path, nullptr);
|
||||
}
|
||||
}
|
||||
if (!entry.thumbnail)
|
||||
return m_filetype_image_icon;
|
||||
return GIcon(m_filetype_image_icon.bitmap_for_size(16), *entry.thumbnail);
|
||||
}
|
||||
return m_file_icon;
|
||||
}
|
||||
|
||||
static String permission_string(mode_t mode)
|
||||
{
|
||||
StringBuilder builder;
|
||||
if (S_ISDIR(mode))
|
||||
builder.append("d");
|
||||
else if (S_ISLNK(mode))
|
||||
builder.append("l");
|
||||
else if (S_ISBLK(mode))
|
||||
builder.append("b");
|
||||
else if (S_ISCHR(mode))
|
||||
builder.append("c");
|
||||
else if (S_ISFIFO(mode))
|
||||
builder.append("f");
|
||||
else if (S_ISSOCK(mode))
|
||||
builder.append("s");
|
||||
else if (S_ISREG(mode))
|
||||
builder.append("-");
|
||||
else
|
||||
builder.append("?");
|
||||
|
||||
builder.appendf("%c%c%c%c%c%c%c%c",
|
||||
mode & S_IRUSR ? 'r' : '-',
|
||||
mode & S_IWUSR ? 'w' : '-',
|
||||
mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'),
|
||||
mode & S_IRGRP ? 'r' : '-',
|
||||
mode & S_IWGRP ? 'w' : '-',
|
||||
mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'),
|
||||
mode & S_IROTH ? 'r' : '-',
|
||||
mode & S_IWOTH ? 'w' : '-');
|
||||
|
||||
if (mode & S_ISVTX)
|
||||
builder.append("t");
|
||||
else
|
||||
builder.appendf("%c", mode & S_IXOTH ? 'x' : '-');
|
||||
return builder.to_string();
|
||||
}
|
||||
|
||||
String GDirectoryModel::name_for_uid(uid_t uid) const
|
||||
{
|
||||
auto it = m_user_names.find(uid);
|
||||
if (it == m_user_names.end())
|
||||
return String::number(uid);
|
||||
return (*it).value;
|
||||
}
|
||||
|
||||
String GDirectoryModel::name_for_gid(uid_t gid) const
|
||||
{
|
||||
auto it = m_user_names.find(gid);
|
||||
if (it == m_user_names.end())
|
||||
return String::number(gid);
|
||||
return (*it).value;
|
||||
}
|
||||
|
||||
GVariant GDirectoryModel::data(const GModelIndex& index, Role role) const
|
||||
{
|
||||
ASSERT(is_valid(index));
|
||||
auto& entry = this->entry(index.row());
|
||||
if (role == Role::Sort) {
|
||||
switch (index.column()) {
|
||||
case Column::Icon:
|
||||
return entry.is_directory() ? 0 : 1;
|
||||
case Column::Name:
|
||||
return entry.name;
|
||||
case Column::Size:
|
||||
return (int)entry.size;
|
||||
case Column::Owner:
|
||||
return name_for_uid(entry.uid);
|
||||
case Column::Group:
|
||||
return name_for_gid(entry.gid);
|
||||
case Column::Permissions:
|
||||
return permission_string(entry.mode);
|
||||
case Column::Inode:
|
||||
return (int)entry.inode;
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
if (role == Role::Display) {
|
||||
switch (index.column()) {
|
||||
case Column::Icon:
|
||||
return icon_for(entry);
|
||||
case Column::Name:
|
||||
return entry.name;
|
||||
case Column::Size:
|
||||
return (int)entry.size;
|
||||
case Column::Owner:
|
||||
return name_for_uid(entry.uid);
|
||||
case Column::Group:
|
||||
return name_for_gid(entry.gid);
|
||||
case Column::Permissions:
|
||||
return permission_string(entry.mode);
|
||||
case Column::Inode:
|
||||
return (int)entry.inode;
|
||||
}
|
||||
}
|
||||
if (role == Role::Icon) {
|
||||
return icon_for(entry);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void GDirectoryModel::update()
|
||||
{
|
||||
CDirIterator di(m_path, CDirIterator::SkipDots);
|
||||
if (di.has_error()) {
|
||||
fprintf(stderr, "CDirIterator: %s\n", di.error_string());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
m_directories.clear();
|
||||
m_files.clear();
|
||||
|
||||
m_bytes_in_files = 0;
|
||||
while (di.has_next()) {
|
||||
String name = di.next_path();
|
||||
Entry entry;
|
||||
entry.name = name;
|
||||
|
||||
struct stat st;
|
||||
int rc = lstat(String::format("%s/%s", m_path.characters(), name.characters()).characters(), &st);
|
||||
if (rc < 0) {
|
||||
perror("lstat");
|
||||
continue;
|
||||
}
|
||||
entry.size = st.st_size;
|
||||
entry.mode = st.st_mode;
|
||||
entry.uid = st.st_uid;
|
||||
entry.gid = st.st_gid;
|
||||
entry.inode = st.st_ino;
|
||||
auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files;
|
||||
entries.append(move(entry));
|
||||
|
||||
if (S_ISREG(entry.mode))
|
||||
m_bytes_in_files += st.st_size;
|
||||
}
|
||||
|
||||
did_update();
|
||||
}
|
||||
|
||||
void GDirectoryModel::open(const StringView& a_path)
|
||||
{
|
||||
FileSystemPath canonical_path(a_path);
|
||||
auto path = canonical_path.string();
|
||||
if (m_path == path)
|
||||
return;
|
||||
DIR* dirp = opendir(path.characters());
|
||||
if (!dirp)
|
||||
return;
|
||||
closedir(dirp);
|
||||
m_path = path;
|
||||
update();
|
||||
set_selected_index(index(0, 0));
|
||||
}
|
80
Libraries/LibGUI/GDirectoryModel.h
Normal file
80
Libraries/LibGUI/GDirectoryModel.h
Normal file
|
@ -0,0 +1,80 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
class GDirectoryModel final : public GModel {
|
||||
friend int thumbnail_thread(void*);
|
||||
|
||||
public:
|
||||
static NonnullRefPtr<GDirectoryModel> create() { return adopt(*new GDirectoryModel); }
|
||||
virtual ~GDirectoryModel() override;
|
||||
|
||||
enum Column {
|
||||
Icon = 0,
|
||||
Name,
|
||||
Size,
|
||||
Owner,
|
||||
Group,
|
||||
Permissions,
|
||||
Inode,
|
||||
__Count,
|
||||
};
|
||||
|
||||
virtual int row_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual int column_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual String column_name(int column) const override;
|
||||
virtual ColumnMetadata column_metadata(int column) const override;
|
||||
virtual GVariant data(const GModelIndex&, Role = Role::Display) const override;
|
||||
virtual void update() override;
|
||||
|
||||
String path() const { return m_path; }
|
||||
void open(const StringView& path);
|
||||
size_t bytes_in_files() const { return m_bytes_in_files; }
|
||||
|
||||
Function<void(int done, int total)> on_thumbnail_progress;
|
||||
|
||||
struct Entry {
|
||||
String name;
|
||||
size_t size { 0 };
|
||||
mode_t mode { 0 };
|
||||
uid_t uid { 0 };
|
||||
uid_t gid { 0 };
|
||||
ino_t inode { 0 };
|
||||
mutable RefPtr<GraphicsBitmap> thumbnail;
|
||||
bool is_directory() const { return S_ISDIR(mode); }
|
||||
bool is_executable() const { return mode & S_IXUSR; }
|
||||
String full_path(const GDirectoryModel& model) const { return String::format("%s/%s", model.path().characters(), name.characters()); }
|
||||
};
|
||||
|
||||
const Entry& entry(int index) const
|
||||
{
|
||||
if (index < m_directories.size())
|
||||
return m_directories[index];
|
||||
return m_files[index - m_directories.size()];
|
||||
}
|
||||
|
||||
private:
|
||||
GDirectoryModel();
|
||||
|
||||
String name_for_uid(uid_t) const;
|
||||
String name_for_gid(gid_t) const;
|
||||
|
||||
GIcon icon_for(const Entry& entry) const;
|
||||
|
||||
String m_path;
|
||||
Vector<Entry> m_files;
|
||||
Vector<Entry> m_directories;
|
||||
size_t m_bytes_in_files;
|
||||
|
||||
GIcon m_directory_icon;
|
||||
GIcon m_file_icon;
|
||||
GIcon m_symlink_icon;
|
||||
GIcon m_socket_icon;
|
||||
GIcon m_executable_icon;
|
||||
GIcon m_filetype_image_icon;
|
||||
|
||||
HashMap<uid_t, String> m_user_names;
|
||||
HashMap<gid_t, String> m_group_names;
|
||||
};
|
277
Libraries/LibGUI/GEvent.h
Normal file
277
Libraries/LibGUI/GEvent.h
Normal file
|
@ -0,0 +1,277 @@
|
|||
#pragma once
|
||||
|
||||
#include <Kernel/KeyCode.h>
|
||||
#include <LibCore/CEvent.h>
|
||||
#include <LibGUI/GWindowType.h>
|
||||
#include <SharedGraphics/Point.h>
|
||||
#include <SharedGraphics/Rect.h>
|
||||
|
||||
class CObject;
|
||||
|
||||
class GEvent : public CEvent {
|
||||
public:
|
||||
enum Type {
|
||||
Show = 1000,
|
||||
Hide,
|
||||
Paint,
|
||||
MultiPaint,
|
||||
Resize,
|
||||
MouseMove,
|
||||
MouseDown,
|
||||
MouseDoubleClick,
|
||||
MouseUp,
|
||||
MouseWheel,
|
||||
Enter,
|
||||
Leave,
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
WindowEntered,
|
||||
WindowLeft,
|
||||
WindowBecameInactive,
|
||||
WindowBecameActive,
|
||||
FocusIn,
|
||||
FocusOut,
|
||||
WindowCloseRequest,
|
||||
ContextMenu,
|
||||
EnabledChange,
|
||||
|
||||
__Begin_WM_Events,
|
||||
WM_WindowRemoved,
|
||||
WM_WindowStateChanged,
|
||||
WM_WindowRectChanged,
|
||||
WM_WindowIconChanged,
|
||||
__End_WM_Events,
|
||||
};
|
||||
|
||||
GEvent() {}
|
||||
explicit GEvent(Type type)
|
||||
: CEvent(type)
|
||||
{
|
||||
}
|
||||
virtual ~GEvent() {}
|
||||
|
||||
bool is_key_event() const { return type() == KeyUp || type() == KeyDown; }
|
||||
bool is_paint_event() const { return type() == Paint; }
|
||||
};
|
||||
|
||||
class GWMEvent : public GEvent {
|
||||
public:
|
||||
GWMEvent(Type type, int client_id, int window_id)
|
||||
: GEvent(type)
|
||||
, m_client_id(client_id)
|
||||
, m_window_id(window_id)
|
||||
{
|
||||
}
|
||||
|
||||
int client_id() const { return m_client_id; }
|
||||
int window_id() const { return m_window_id; }
|
||||
|
||||
private:
|
||||
int m_client_id { -1 };
|
||||
int m_window_id { -1 };
|
||||
};
|
||||
|
||||
class GWMWindowRemovedEvent : public GWMEvent {
|
||||
public:
|
||||
GWMWindowRemovedEvent(int client_id, int window_id)
|
||||
: GWMEvent(GEvent::Type::WM_WindowRemoved, client_id, window_id)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class GWMWindowStateChangedEvent : public GWMEvent {
|
||||
public:
|
||||
GWMWindowStateChangedEvent(int client_id, int window_id, const StringView& title, const Rect& rect, bool is_active, GWindowType window_type, bool is_minimized)
|
||||
: GWMEvent(GEvent::Type::WM_WindowStateChanged, client_id, window_id)
|
||||
, m_title(title)
|
||||
, m_rect(rect)
|
||||
, m_window_type(window_type)
|
||||
, m_active(is_active)
|
||||
, m_minimized(is_minimized)
|
||||
{
|
||||
}
|
||||
|
||||
String title() const { return m_title; }
|
||||
Rect rect() const { return m_rect; }
|
||||
bool is_active() const { return m_active; }
|
||||
GWindowType window_type() const { return m_window_type; }
|
||||
bool is_minimized() const { return m_minimized; }
|
||||
|
||||
private:
|
||||
String m_title;
|
||||
Rect m_rect;
|
||||
GWindowType m_window_type;
|
||||
bool m_active;
|
||||
bool m_minimized;
|
||||
};
|
||||
|
||||
class GWMWindowRectChangedEvent : public GWMEvent {
|
||||
public:
|
||||
GWMWindowRectChangedEvent(int client_id, int window_id, const Rect& rect)
|
||||
: GWMEvent(GEvent::Type::WM_WindowRectChanged, client_id, window_id)
|
||||
, m_rect(rect)
|
||||
{
|
||||
}
|
||||
|
||||
Rect rect() const { return m_rect; }
|
||||
|
||||
private:
|
||||
Rect m_rect;
|
||||
};
|
||||
|
||||
class GWMWindowIconChangedEvent : public GWMEvent {
|
||||
public:
|
||||
GWMWindowIconChangedEvent(int client_id, int window_id, const StringView& icon_path)
|
||||
: GWMEvent(GEvent::Type::WM_WindowIconChanged, client_id, window_id)
|
||||
, m_icon_path(icon_path)
|
||||
{
|
||||
}
|
||||
|
||||
String icon_path() const { return m_icon_path; }
|
||||
|
||||
private:
|
||||
String m_icon_path;
|
||||
};
|
||||
|
||||
class GMultiPaintEvent final : public GEvent {
|
||||
public:
|
||||
explicit GMultiPaintEvent(const Vector<Rect, 32>& rects, const Size& window_size)
|
||||
: GEvent(GEvent::MultiPaint)
|
||||
, m_rects(rects)
|
||||
, m_window_size(window_size)
|
||||
{
|
||||
}
|
||||
|
||||
const Vector<Rect, 32>& rects() const { return m_rects; }
|
||||
Size window_size() const { return m_window_size; }
|
||||
|
||||
private:
|
||||
Vector<Rect, 32> m_rects;
|
||||
Size m_window_size;
|
||||
};
|
||||
|
||||
class GPaintEvent final : public GEvent {
|
||||
public:
|
||||
explicit GPaintEvent(const Rect& rect, const Size& window_size = Size())
|
||||
: GEvent(GEvent::Paint)
|
||||
, m_rect(rect)
|
||||
, m_window_size(window_size)
|
||||
{
|
||||
}
|
||||
|
||||
Rect rect() const { return m_rect; }
|
||||
Size window_size() const { return m_window_size; }
|
||||
|
||||
private:
|
||||
Rect m_rect;
|
||||
Size m_window_size;
|
||||
};
|
||||
|
||||
class GResizeEvent final : public GEvent {
|
||||
public:
|
||||
explicit GResizeEvent(const Size& old_size, const Size& size)
|
||||
: GEvent(GEvent::Resize)
|
||||
, m_old_size(old_size)
|
||||
, m_size(size)
|
||||
{
|
||||
}
|
||||
|
||||
const Size& old_size() const { return m_old_size; }
|
||||
const Size& size() const { return m_size; }
|
||||
|
||||
private:
|
||||
Size m_old_size;
|
||||
Size m_size;
|
||||
};
|
||||
|
||||
class GContextMenuEvent final : public GEvent {
|
||||
public:
|
||||
explicit GContextMenuEvent(const Point& position, const Point& screen_position)
|
||||
: GEvent(GEvent::ContextMenu)
|
||||
, m_position(position)
|
||||
, m_screen_position(screen_position)
|
||||
{
|
||||
}
|
||||
|
||||
const Point& position() const { return m_position; }
|
||||
const Point& screen_position() const { return m_screen_position; }
|
||||
|
||||
private:
|
||||
Point m_position;
|
||||
Point m_screen_position;
|
||||
};
|
||||
|
||||
class GShowEvent final : public GEvent {
|
||||
public:
|
||||
GShowEvent()
|
||||
: GEvent(GEvent::Show)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class GHideEvent final : public GEvent {
|
||||
public:
|
||||
GHideEvent()
|
||||
: GEvent(GEvent::Hide)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
enum GMouseButton : u8 {
|
||||
None = 0,
|
||||
Left = 1,
|
||||
Right = 2,
|
||||
Middle = 4,
|
||||
};
|
||||
|
||||
class GKeyEvent final : public GEvent {
|
||||
public:
|
||||
GKeyEvent(Type type, int key, u8 modifiers)
|
||||
: GEvent(type)
|
||||
, m_key(key)
|
||||
, m_modifiers(modifiers)
|
||||
{
|
||||
}
|
||||
|
||||
int key() const { return m_key; }
|
||||
bool ctrl() const { return m_modifiers & Mod_Ctrl; }
|
||||
bool alt() const { return m_modifiers & Mod_Alt; }
|
||||
bool shift() const { return m_modifiers & Mod_Shift; }
|
||||
bool logo() const { return m_modifiers & Mod_Logo; }
|
||||
u8 modifiers() const { return m_modifiers; }
|
||||
String text() const { return m_text; }
|
||||
|
||||
private:
|
||||
friend class GEventLoop;
|
||||
int m_key { 0 };
|
||||
u8 m_modifiers { 0 };
|
||||
String m_text;
|
||||
};
|
||||
|
||||
class GMouseEvent final : public GEvent {
|
||||
public:
|
||||
GMouseEvent(Type type, const Point& position, unsigned buttons, GMouseButton button, unsigned modifiers, int wheel_delta)
|
||||
: GEvent(type)
|
||||
, m_position(position)
|
||||
, m_buttons(buttons)
|
||||
, m_button(button)
|
||||
, m_modifiers(modifiers)
|
||||
, m_wheel_delta(wheel_delta)
|
||||
{
|
||||
}
|
||||
|
||||
Point position() const { return m_position; }
|
||||
int x() const { return m_position.x(); }
|
||||
int y() const { return m_position.y(); }
|
||||
GMouseButton button() const { return m_button; }
|
||||
unsigned buttons() const { return m_buttons; }
|
||||
unsigned modifiers() const { return m_modifiers; }
|
||||
int wheel_delta() const { return m_wheel_delta; }
|
||||
|
||||
private:
|
||||
Point m_position;
|
||||
unsigned m_buttons { 0 };
|
||||
GMouseButton m_button { GMouseButton::None };
|
||||
unsigned m_modifiers { 0 };
|
||||
int m_wheel_delta { 0 };
|
||||
};
|
447
Libraries/LibGUI/GEventLoop.cpp
Normal file
447
Libraries/LibGUI/GEventLoop.cpp
Normal file
|
@ -0,0 +1,447 @@
|
|||
#include "GEventLoop.h"
|
||||
#include "GEvent.h"
|
||||
#include "GWindow.h"
|
||||
#include <LibC/errno.h>
|
||||
#include <LibC/fcntl.h>
|
||||
#include <LibC/stdio.h>
|
||||
#include <LibC/stdlib.h>
|
||||
#include <LibC/string.h>
|
||||
#include <LibC/sys/select.h>
|
||||
#include <LibC/sys/socket.h>
|
||||
#include <LibC/sys/time.h>
|
||||
#include <LibC/time.h>
|
||||
#include <LibC/unistd.h>
|
||||
#include <LibCore/CNotifier.h>
|
||||
#include <LibCore/CObject.h>
|
||||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GApplication.h>
|
||||
#include <LibGUI/GDesktop.h>
|
||||
#include <LibGUI/GMenu.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
//#define GEVENTLOOP_DEBUG
|
||||
//#define COALESCING_DEBUG
|
||||
|
||||
int GEventLoop::s_windowserver_fd = -1;
|
||||
int GEventLoop::s_my_client_id = -1;
|
||||
pid_t GEventLoop::s_server_pid = -1;
|
||||
|
||||
void GEventLoop::connect_to_server()
|
||||
{
|
||||
ASSERT(s_windowserver_fd == -1);
|
||||
s_windowserver_fd = socket(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (s_windowserver_fd < 0) {
|
||||
perror("socket");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
sockaddr_un address;
|
||||
address.sun_family = AF_LOCAL;
|
||||
strcpy(address.sun_path, "/tmp/wsportal");
|
||||
|
||||
int retries = 1000;
|
||||
int rc = 0;
|
||||
while (retries) {
|
||||
rc = connect(s_windowserver_fd, (const sockaddr*)&address, sizeof(address));
|
||||
if (rc == 0)
|
||||
break;
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("connect failed: %d, %s\n", errno, strerror(errno));
|
||||
#endif
|
||||
sleep(1);
|
||||
--retries;
|
||||
}
|
||||
if (rc < 0) {
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::Greeting;
|
||||
request.greeting.client_pid = getpid();
|
||||
auto response = sync_request(request, WSAPI_ServerMessage::Type::Greeting);
|
||||
handle_greeting(response);
|
||||
}
|
||||
|
||||
GEventLoop::GEventLoop()
|
||||
{
|
||||
static bool connected = false;
|
||||
if (!connected) {
|
||||
connect_to_server();
|
||||
connected = true;
|
||||
}
|
||||
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("(%u) GEventLoop constructed :)\n", getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
GEventLoop::~GEventLoop()
|
||||
{
|
||||
}
|
||||
|
||||
void GEventLoop::handle_paint_event(const WSAPI_ServerMessage& event, GWindow& window, const ByteBuffer& extra_data)
|
||||
{
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("WID=%x Paint [%d,%d %dx%d]\n", event.window_id, event.paint.rect.location.x, event.paint.rect.location.y, event.paint.rect.size.width, event.paint.rect.size.height);
|
||||
#endif
|
||||
Vector<Rect, 32> rects;
|
||||
for (int i = 0; i < min(WSAPI_ServerMessage::max_inline_rect_count, event.rect_count); ++i)
|
||||
rects.append(event.rects[i]);
|
||||
if (event.extra_size) {
|
||||
auto* extra_rects = reinterpret_cast<const WSAPI_Rect*>(extra_data.data());
|
||||
for (int i = 0; i < event.rect_count - WSAPI_ServerMessage::max_inline_rect_count; ++i)
|
||||
rects.append(extra_rects[i]);
|
||||
}
|
||||
post_event(window, make<GMultiPaintEvent>(rects, event.paint.window_size));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_resize_event(const WSAPI_ServerMessage& event, GWindow& window)
|
||||
{
|
||||
post_event(window, make<GResizeEvent>(event.window.old_rect.size, event.window.rect.size));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_window_activation_event(const WSAPI_ServerMessage& event, GWindow& window)
|
||||
{
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("WID=%x WindowActivation\n", event.window_id);
|
||||
#endif
|
||||
post_event(window, make<GEvent>(event.type == WSAPI_ServerMessage::Type::WindowActivated ? GEvent::WindowBecameActive : GEvent::WindowBecameInactive));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_window_close_request_event(const WSAPI_ServerMessage&, GWindow& window)
|
||||
{
|
||||
post_event(window, make<GEvent>(GEvent::WindowCloseRequest));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_window_entered_or_left_event(const WSAPI_ServerMessage& message, GWindow& window)
|
||||
{
|
||||
post_event(window, make<GEvent>(message.type == WSAPI_ServerMessage::Type::WindowEntered ? GEvent::WindowEntered : GEvent::WindowLeft));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_key_event(const WSAPI_ServerMessage& event, GWindow& window)
|
||||
{
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("WID=%x KeyEvent character=0x%b\n", event.window_id, event.key.character);
|
||||
#endif
|
||||
auto key_event = make<GKeyEvent>(event.type == WSAPI_ServerMessage::Type::KeyDown ? GEvent::KeyDown : GEvent::KeyUp, event.key.key, event.key.modifiers);
|
||||
if (event.key.character != '\0')
|
||||
key_event->m_text = String(&event.key.character, 1);
|
||||
|
||||
if (event.type == WSAPI_ServerMessage::Type::KeyDown) {
|
||||
if (auto* focused_widget = window.focused_widget()) {
|
||||
if (auto* action = focused_widget->action_for_key_event(*key_event)) {
|
||||
if (action->is_enabled()) {
|
||||
action->activate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto* action = GApplication::the().action_for_key_event(*key_event)) {
|
||||
if (action->is_enabled()) {
|
||||
action->activate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
post_event(window, move(key_event));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_mouse_event(const WSAPI_ServerMessage& event, GWindow& window)
|
||||
{
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("WID=%x MouseEvent %d,%d,%d\n", event.window_id, event.mouse.position.x, event.mouse.position.y, event.mouse.wheel_delta);
|
||||
#endif
|
||||
GMouseEvent::Type type;
|
||||
switch (event.type) {
|
||||
case WSAPI_ServerMessage::Type::MouseMove:
|
||||
type = GEvent::MouseMove;
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::MouseUp:
|
||||
type = GEvent::MouseUp;
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::MouseDown:
|
||||
type = GEvent::MouseDown;
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::MouseDoubleClick:
|
||||
type = GEvent::MouseDoubleClick;
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::MouseWheel:
|
||||
type = GEvent::MouseWheel;
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
break;
|
||||
}
|
||||
GMouseButton button { GMouseButton::None };
|
||||
switch (event.mouse.button) {
|
||||
case WSAPI_MouseButton::NoButton:
|
||||
button = GMouseButton::None;
|
||||
break;
|
||||
case WSAPI_MouseButton::Left:
|
||||
button = GMouseButton::Left;
|
||||
break;
|
||||
case WSAPI_MouseButton::Right:
|
||||
button = GMouseButton::Right;
|
||||
break;
|
||||
case WSAPI_MouseButton::Middle:
|
||||
button = GMouseButton::Middle;
|
||||
break;
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
break;
|
||||
}
|
||||
post_event(window, make<GMouseEvent>(type, event.mouse.position, event.mouse.buttons, button, event.mouse.modifiers, event.mouse.wheel_delta));
|
||||
}
|
||||
|
||||
void GEventLoop::handle_menu_event(const WSAPI_ServerMessage& event)
|
||||
{
|
||||
if (event.type == WSAPI_ServerMessage::Type::MenuItemActivated) {
|
||||
auto* menu = GMenu::from_menu_id(event.menu.menu_id);
|
||||
if (!menu) {
|
||||
dbgprintf("GEventLoop received event for invalid window ID %d\n", event.window_id);
|
||||
return;
|
||||
}
|
||||
if (auto* action = menu->action_at(event.menu.identifier))
|
||||
action->activate();
|
||||
return;
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
void GEventLoop::handle_wm_event(const WSAPI_ServerMessage& event, GWindow& window)
|
||||
{
|
||||
#ifdef GEVENTLOOP_DEBUG
|
||||
dbgprintf("GEventLoop: handle_wm_event: %d\n", (int)event.type);
|
||||
#endif
|
||||
if (event.type == WSAPI_ServerMessage::WM_WindowStateChanged)
|
||||
return post_event(window, make<GWMWindowStateChangedEvent>(event.wm.client_id, event.wm.window_id, String(event.text, event.text_length), event.wm.rect, event.wm.is_active, (GWindowType)event.wm.window_type, event.wm.is_minimized));
|
||||
if (event.type == WSAPI_ServerMessage::WM_WindowRectChanged)
|
||||
return post_event(window, make<GWMWindowRectChangedEvent>(event.wm.client_id, event.wm.window_id, event.wm.rect));
|
||||
if (event.type == WSAPI_ServerMessage::WM_WindowIconChanged)
|
||||
return post_event(window, make<GWMWindowIconChangedEvent>(event.wm.client_id, event.wm.window_id, String(event.text, event.text_length)));
|
||||
if (event.type == WSAPI_ServerMessage::WM_WindowRemoved)
|
||||
return post_event(window, make<GWMWindowRemovedEvent>(event.wm.client_id, event.wm.window_id));
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
void GEventLoop::process_unprocessed_bundles()
|
||||
{
|
||||
int coalesced_paints = 0;
|
||||
int coalesced_resizes = 0;
|
||||
auto unprocessed_bundles = move(m_unprocessed_bundles);
|
||||
|
||||
HashMap<int, Size> latest_size_for_window_id;
|
||||
for (auto& bundle : unprocessed_bundles) {
|
||||
auto& event = bundle.message;
|
||||
if (event.type == WSAPI_ServerMessage::Type::WindowResized) {
|
||||
latest_size_for_window_id.set(event.window_id, event.window.rect.size);
|
||||
}
|
||||
}
|
||||
|
||||
int paint_count = 0;
|
||||
HashMap<int, Size> latest_paint_size_for_window_id;
|
||||
for (auto& bundle : unprocessed_bundles) {
|
||||
auto& event = bundle.message;
|
||||
if (event.type == WSAPI_ServerMessage::Type::Paint) {
|
||||
++paint_count;
|
||||
#ifdef COALESCING_DEBUG
|
||||
dbgprintf(" %s (window: %s)\n", Rect(event.paint.rect).to_string().characters(), Size(event.paint.window_size).to_string().characters());
|
||||
#endif
|
||||
latest_paint_size_for_window_id.set(event.window_id, event.paint.window_size);
|
||||
}
|
||||
}
|
||||
#ifdef COALESCING_DEBUG
|
||||
dbgprintf("paint_count: %d\n", paint_count);
|
||||
#endif
|
||||
|
||||
for (auto& bundle : unprocessed_bundles) {
|
||||
auto& event = bundle.message;
|
||||
if (event.type == WSAPI_ServerMessage::Type::Greeting) {
|
||||
handle_greeting(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == WSAPI_ServerMessage::Type::ScreenRectChanged) {
|
||||
GDesktop::the().did_receive_screen_rect({}, event.screen.rect);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == WSAPI_ServerMessage::Error) {
|
||||
dbgprintf("GEventLoop got error message from server\n");
|
||||
dbgprintf(" - error message: %s\n", String(event.text, event.text_length).characters());
|
||||
quit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case WSAPI_ServerMessage::MenuItemActivated:
|
||||
handle_menu_event(event);
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
auto* window = GWindow::from_window_id(event.window_id);
|
||||
if (!window) {
|
||||
dbgprintf("GEventLoop received event for invalid window ID %d\n", event.window_id);
|
||||
continue;
|
||||
}
|
||||
switch (event.type) {
|
||||
case WSAPI_ServerMessage::Type::Paint:
|
||||
if (Size(event.paint.window_size) != latest_paint_size_for_window_id.get(event.window_id)) {
|
||||
++coalesced_paints;
|
||||
break;
|
||||
}
|
||||
handle_paint_event(event, *window, bundle.extra_data);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::MouseDown:
|
||||
case WSAPI_ServerMessage::Type::MouseDoubleClick:
|
||||
case WSAPI_ServerMessage::Type::MouseUp:
|
||||
case WSAPI_ServerMessage::Type::MouseMove:
|
||||
case WSAPI_ServerMessage::Type::MouseWheel:
|
||||
handle_mouse_event(event, *window);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::WindowActivated:
|
||||
case WSAPI_ServerMessage::Type::WindowDeactivated:
|
||||
handle_window_activation_event(event, *window);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::WindowCloseRequest:
|
||||
handle_window_close_request_event(event, *window);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::KeyDown:
|
||||
case WSAPI_ServerMessage::Type::KeyUp:
|
||||
handle_key_event(event, *window);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::WindowEntered:
|
||||
case WSAPI_ServerMessage::Type::WindowLeft:
|
||||
handle_window_entered_or_left_event(event, *window);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::WindowResized:
|
||||
if (Size(event.window.rect.size) != latest_size_for_window_id.get(event.window_id)) {
|
||||
++coalesced_resizes;
|
||||
break;
|
||||
}
|
||||
handle_resize_event(event, *window);
|
||||
break;
|
||||
case WSAPI_ServerMessage::Type::WM_WindowRemoved:
|
||||
case WSAPI_ServerMessage::Type::WM_WindowStateChanged:
|
||||
case WSAPI_ServerMessage::Type::WM_WindowIconChanged:
|
||||
handle_wm_event(event, *window);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef COALESCING_DEBUG
|
||||
if (coalesced_paints)
|
||||
dbgprintf("Coalesced %d paints\n", coalesced_paints);
|
||||
if (coalesced_resizes)
|
||||
dbgprintf("Coalesced %d resizes\n", coalesced_resizes);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GEventLoop::drain_messages_from_server()
|
||||
{
|
||||
for (;;) {
|
||||
WSAPI_ServerMessage message;
|
||||
ssize_t nread = recv(s_windowserver_fd, &message, sizeof(WSAPI_ServerMessage), MSG_DONTWAIT);
|
||||
if (nread < 0) {
|
||||
if (errno == EAGAIN) {
|
||||
return true;
|
||||
}
|
||||
perror("read");
|
||||
quit(1);
|
||||
return false;
|
||||
}
|
||||
if (nread == 0) {
|
||||
fprintf(stderr, "EOF on WindowServer fd\n");
|
||||
quit(1);
|
||||
exit(-1);
|
||||
return false;
|
||||
}
|
||||
ASSERT(nread == sizeof(message));
|
||||
ByteBuffer extra_data;
|
||||
if (message.extra_size) {
|
||||
extra_data = ByteBuffer::create_uninitialized(message.extra_size);
|
||||
int extra_nread = read(s_windowserver_fd, extra_data.data(), extra_data.size());
|
||||
if (extra_nread < 0) {
|
||||
perror("read");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
ASSERT((size_t)extra_nread == message.extra_size);
|
||||
}
|
||||
m_unprocessed_bundles.append({ move(message), move(extra_data) });
|
||||
}
|
||||
}
|
||||
|
||||
bool GEventLoop::post_message_to_server(const WSAPI_ClientMessage& message, const ByteBuffer& extra_data)
|
||||
{
|
||||
if (!extra_data.is_empty())
|
||||
const_cast<WSAPI_ClientMessage&>(message).extra_size = extra_data.size();
|
||||
|
||||
struct iovec iov[2];
|
||||
int iov_count = 1;
|
||||
iov[0].iov_base = const_cast<WSAPI_ClientMessage*>(&message);
|
||||
iov[0].iov_len = sizeof(message);
|
||||
|
||||
if (!extra_data.is_empty()) {
|
||||
iov[1].iov_base = const_cast<u8*>(extra_data.data());
|
||||
iov[1].iov_len = extra_data.size();
|
||||
++iov_count;
|
||||
}
|
||||
|
||||
int nwritten = writev(s_windowserver_fd, iov, iov_count);
|
||||
if (nwritten < 0) {
|
||||
perror("writev");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
ASSERT((size_t)nwritten == sizeof(message) + extra_data.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GEventLoop::wait_for_specific_event(WSAPI_ServerMessage::Type type, WSAPI_ServerMessage& event)
|
||||
{
|
||||
for (;;) {
|
||||
fd_set rfds;
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(s_windowserver_fd, &rfds);
|
||||
int rc = select(s_windowserver_fd + 1, &rfds, nullptr, nullptr, nullptr);
|
||||
if (rc < 0) {
|
||||
perror("select");
|
||||
}
|
||||
ASSERT(rc > 0);
|
||||
ASSERT(FD_ISSET(s_windowserver_fd, &rfds));
|
||||
bool success = drain_messages_from_server();
|
||||
if (!success)
|
||||
return false;
|
||||
for (ssize_t i = 0; i < m_unprocessed_bundles.size(); ++i) {
|
||||
if (m_unprocessed_bundles[i].message.type == type) {
|
||||
event = move(m_unprocessed_bundles[i].message);
|
||||
m_unprocessed_bundles.remove(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WSAPI_ServerMessage GEventLoop::sync_request(const WSAPI_ClientMessage& request, WSAPI_ServerMessage::Type response_type)
|
||||
{
|
||||
bool success = post_message_to_server(request);
|
||||
ASSERT(success);
|
||||
|
||||
WSAPI_ServerMessage response;
|
||||
success = wait_for_specific_event(response_type, response);
|
||||
ASSERT(success);
|
||||
return response;
|
||||
}
|
||||
|
||||
void GEventLoop::handle_greeting(WSAPI_ServerMessage& message)
|
||||
{
|
||||
s_server_pid = message.greeting.server_pid;
|
||||
s_my_client_id = message.greeting.your_client_id;
|
||||
GDesktop::the().did_receive_screen_rect({}, message.greeting.screen_rect);
|
||||
}
|
75
Libraries/LibGUI/GEventLoop.h
Normal file
75
Libraries/LibGUI/GEventLoop.h
Normal file
|
@ -0,0 +1,75 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibCore/CEventLoop.h>
|
||||
#include <LibGUI/GEvent.h>
|
||||
#include <WindowServer/WSAPITypes.h>
|
||||
|
||||
class GAction;
|
||||
class CObject;
|
||||
class CNotifier;
|
||||
class GWindow;
|
||||
|
||||
class GEventLoop final : public CEventLoop {
|
||||
public:
|
||||
GEventLoop();
|
||||
virtual ~GEventLoop() override;
|
||||
|
||||
static GEventLoop& current() { return static_cast<GEventLoop&>(CEventLoop::current()); }
|
||||
|
||||
static bool post_message_to_server(const WSAPI_ClientMessage&, const ByteBuffer& extra_data = {});
|
||||
bool wait_for_specific_event(WSAPI_ServerMessage::Type, WSAPI_ServerMessage&);
|
||||
WSAPI_ServerMessage sync_request(const WSAPI_ClientMessage& request, WSAPI_ServerMessage::Type response_type);
|
||||
|
||||
static pid_t server_pid() { return s_server_pid; }
|
||||
static int my_client_id() { return s_my_client_id; }
|
||||
|
||||
virtual void take_pending_events_from(CEventLoop& other) override
|
||||
{
|
||||
CEventLoop::take_pending_events_from(other);
|
||||
m_unprocessed_bundles.append(move(static_cast<GEventLoop&>(other).m_unprocessed_bundles));
|
||||
}
|
||||
|
||||
private:
|
||||
virtual void add_file_descriptors_for_select(fd_set& fds, int& max_fd_added) override
|
||||
{
|
||||
FD_SET(s_windowserver_fd, &fds);
|
||||
max_fd_added = s_windowserver_fd;
|
||||
}
|
||||
|
||||
virtual void process_file_descriptors_after_select(const fd_set& fds) override
|
||||
{
|
||||
if (FD_ISSET(s_windowserver_fd, &fds))
|
||||
drain_messages_from_server();
|
||||
}
|
||||
|
||||
virtual void do_processing() override
|
||||
{
|
||||
while (!m_unprocessed_bundles.is_empty())
|
||||
process_unprocessed_bundles();
|
||||
}
|
||||
|
||||
void wait_for_event();
|
||||
bool drain_messages_from_server();
|
||||
void process_unprocessed_bundles();
|
||||
void handle_paint_event(const WSAPI_ServerMessage&, GWindow&, const ByteBuffer& extra_data);
|
||||
void handle_resize_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_mouse_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_key_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_window_activation_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_window_close_request_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_menu_event(const WSAPI_ServerMessage&);
|
||||
void handle_window_entered_or_left_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_wm_event(const WSAPI_ServerMessage&, GWindow&);
|
||||
void handle_greeting(WSAPI_ServerMessage&);
|
||||
void connect_to_server();
|
||||
|
||||
struct IncomingWSMessageBundle {
|
||||
WSAPI_ServerMessage message;
|
||||
ByteBuffer extra_data;
|
||||
};
|
||||
|
||||
Vector<IncomingWSMessageBundle> m_unprocessed_bundles;
|
||||
static pid_t s_server_pid;
|
||||
static int s_my_client_id;
|
||||
static int s_windowserver_fd;
|
||||
};
|
191
Libraries/LibGUI/GFilePicker.cpp
Normal file
191
Libraries/LibGUI/GFilePicker.cpp
Normal file
|
@ -0,0 +1,191 @@
|
|||
#include <AK/FileSystemPath.h>
|
||||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GDirectoryModel.h>
|
||||
#include <LibGUI/GFilePicker.h>
|
||||
#include <LibGUI/GInputBox.h>
|
||||
#include <LibGUI/GLabel.h>
|
||||
#include <LibGUI/GMessageBox.h>
|
||||
#include <LibGUI/GSortingProxyModel.h>
|
||||
#include <LibGUI/GTextBox.h>
|
||||
#include <LibGUI/GToolBar.h>
|
||||
#include <SharedGraphics/PNGLoader.h>
|
||||
|
||||
GFilePicker::GFilePicker(const StringView& path, CObject* parent)
|
||||
: GDialog(parent)
|
||||
, m_model(GDirectoryModel::create())
|
||||
{
|
||||
set_title("GFilePicker");
|
||||
set_rect(200, 200, 700, 400);
|
||||
auto* horizontal_container = new GWidget;
|
||||
set_main_widget(horizontal_container);
|
||||
horizontal_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
horizontal_container->layout()->set_margins({ 4, 4, 4, 4 });
|
||||
horizontal_container->set_fill_with_background_color(true);
|
||||
horizontal_container->set_background_color(Color::WarmGray);
|
||||
|
||||
auto* vertical_container = new GWidget(horizontal_container);
|
||||
vertical_container->set_layout(make<GBoxLayout>(Orientation::Vertical));
|
||||
vertical_container->layout()->set_spacing(4);
|
||||
|
||||
auto* upper_container = new GWidget(vertical_container);
|
||||
upper_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
upper_container->layout()->set_spacing(4);
|
||||
upper_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
upper_container->set_preferred_size({ 0, 26 });
|
||||
|
||||
auto* toolbar = new GToolBar(upper_container);
|
||||
toolbar->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
|
||||
toolbar->set_preferred_size({ 60, 0 });
|
||||
toolbar->set_has_frame(false);
|
||||
|
||||
auto* location_textbox = new GTextBox(upper_container);
|
||||
location_textbox->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
location_textbox->set_preferred_size({ 0, 20 });
|
||||
|
||||
m_view = new GTableView(vertical_container);
|
||||
m_view->set_model(GSortingProxyModel::create(*m_model));
|
||||
m_view->set_column_hidden(GDirectoryModel::Column::Owner, true);
|
||||
m_view->set_column_hidden(GDirectoryModel::Column::Group, true);
|
||||
m_view->set_column_hidden(GDirectoryModel::Column::Permissions, true);
|
||||
m_view->set_column_hidden(GDirectoryModel::Column::Inode, true);
|
||||
m_model->open(path);
|
||||
|
||||
location_textbox->on_return_pressed = [&] {
|
||||
m_model->open(location_textbox->text());
|
||||
clear_preview();
|
||||
};
|
||||
|
||||
auto open_parent_directory_action = GAction::create("Open parent directory", { Mod_Alt, Key_Up }, GraphicsBitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"), [this](const GAction&) {
|
||||
m_model->open(String::format("%s/..", m_model->path().characters()));
|
||||
clear_preview();
|
||||
});
|
||||
toolbar->add_action(*open_parent_directory_action);
|
||||
|
||||
auto mkdir_action = GAction::create("New directory...", GraphicsBitmap::load_from_file("/res/icons/16x16/mkdir.png"), [this](const GAction&) {
|
||||
GInputBox input_box("Enter name:", "New directory", this);
|
||||
if (input_box.exec() == GInputBox::ExecOK && !input_box.text_value().is_empty()) {
|
||||
auto new_dir_path = FileSystemPath(String::format("%s/%s",
|
||||
m_model->path().characters(),
|
||||
input_box.text_value().characters()))
|
||||
.string();
|
||||
int rc = mkdir(new_dir_path.characters(), 0777);
|
||||
if (rc < 0) {
|
||||
GMessageBox::show(String::format("mkdir(\"%s\") failed: %s", new_dir_path.characters(), strerror(errno)), "Error", GMessageBox::Type::Error, this);
|
||||
} else {
|
||||
m_model->update();
|
||||
}
|
||||
}
|
||||
});
|
||||
toolbar->add_action(*mkdir_action);
|
||||
|
||||
auto* lower_container = new GWidget(vertical_container);
|
||||
lower_container->set_layout(make<GBoxLayout>(Orientation::Vertical));
|
||||
lower_container->layout()->set_spacing(4);
|
||||
lower_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
lower_container->set_preferred_size({ 0, 60 });
|
||||
|
||||
auto* filename_container = new GWidget(lower_container);
|
||||
filename_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
filename_container->set_preferred_size({ 0, 20 });
|
||||
filename_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
auto* filename_label = new GLabel("File name:", filename_container);
|
||||
filename_label->set_text_alignment(TextAlignment::CenterLeft);
|
||||
filename_label->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
|
||||
filename_label->set_preferred_size({ 60, 0 });
|
||||
auto* filename_textbox = new GTextBox(filename_container);
|
||||
|
||||
m_view->on_activation = [this, filename_textbox](auto& index) {
|
||||
auto& filter_model = (GSortingProxyModel&)*m_view->model();
|
||||
auto local_index = filter_model.map_to_target(index);
|
||||
const GDirectoryModel::Entry& entry = m_model->entry(local_index.row());
|
||||
|
||||
FileSystemPath path(String::format("%s/%s", m_model->path().characters(), entry.name.characters()));
|
||||
|
||||
clear_preview();
|
||||
|
||||
if (entry.is_directory()) {
|
||||
m_model->open(path.string());
|
||||
// NOTE: 'entry' is invalid from here on
|
||||
} else {
|
||||
filename_textbox->set_text(entry.name);
|
||||
set_preview(path);
|
||||
}
|
||||
};
|
||||
|
||||
auto* button_container = new GWidget(lower_container);
|
||||
button_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
button_container->set_preferred_size({ 0, 20 });
|
||||
button_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
button_container->layout()->set_spacing(4);
|
||||
button_container->layout()->add_spacer();
|
||||
|
||||
auto* cancel_button = new GButton(button_container);
|
||||
cancel_button->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
|
||||
cancel_button->set_preferred_size({ 80, 0 });
|
||||
cancel_button->set_text("Cancel");
|
||||
cancel_button->on_click = [this](auto&) {
|
||||
done(ExecCancel);
|
||||
};
|
||||
|
||||
auto* ok_button = new GButton(button_container);
|
||||
ok_button->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
|
||||
ok_button->set_preferred_size({ 80, 0 });
|
||||
ok_button->set_text("OK");
|
||||
ok_button->on_click = [this, filename_textbox](auto&) {
|
||||
FileSystemPath path(String::format("%s/%s", m_model->path().characters(), filename_textbox->text().characters()));
|
||||
m_selected_file = path;
|
||||
done(ExecOK);
|
||||
};
|
||||
|
||||
auto* preview_container = new GFrame(horizontal_container);
|
||||
preview_container->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
|
||||
preview_container->set_preferred_size({ 180, 0 });
|
||||
preview_container->set_frame_shape(FrameShape::Container);
|
||||
preview_container->set_frame_shadow(FrameShadow::Sunken);
|
||||
preview_container->set_frame_thickness(2);
|
||||
preview_container->set_layout(make<GBoxLayout>(Orientation::Vertical));
|
||||
preview_container->layout()->set_margins({ 8, 8, 8, 8 });
|
||||
|
||||
m_preview_image_label = new GLabel(preview_container);
|
||||
m_preview_image_label->set_should_stretch_icon(true);
|
||||
m_preview_image_label->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
m_preview_image_label->set_preferred_size({ 160, 160 });
|
||||
|
||||
m_preview_name_label = new GLabel(preview_container);
|
||||
m_preview_name_label->set_font(Font::default_bold_font());
|
||||
m_preview_name_label->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
m_preview_name_label->set_preferred_size({ 0, m_preview_name_label->font().glyph_height() });
|
||||
|
||||
m_preview_geometry_label = new GLabel(preview_container);
|
||||
m_preview_geometry_label->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
m_preview_geometry_label->set_preferred_size({ 0, m_preview_name_label->font().glyph_height() });
|
||||
}
|
||||
|
||||
GFilePicker::~GFilePicker()
|
||||
{
|
||||
}
|
||||
|
||||
void GFilePicker::set_preview(const FileSystemPath& path)
|
||||
{
|
||||
if (path.has_extension(".png")) {
|
||||
auto bitmap = load_png(path.string());
|
||||
if (!bitmap) {
|
||||
clear_preview();
|
||||
return;
|
||||
}
|
||||
bool should_stretch = bitmap->width() > m_preview_image_label->width() || bitmap->height() > m_preview_image_label->height();
|
||||
m_preview_name_label->set_text(path.basename());
|
||||
m_preview_geometry_label->set_text(bitmap->size().to_string());
|
||||
m_preview_image_label->set_should_stretch_icon(should_stretch);
|
||||
m_preview_image_label->set_icon(move(bitmap));
|
||||
}
|
||||
}
|
||||
|
||||
void GFilePicker::clear_preview()
|
||||
{
|
||||
m_preview_image_label->set_icon(nullptr);
|
||||
m_preview_name_label->set_text(String::empty());
|
||||
m_preview_geometry_label->set_text(String::empty());
|
||||
}
|
28
Libraries/LibGUI/GFilePicker.h
Normal file
28
Libraries/LibGUI/GFilePicker.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
#include <AK/FileSystemPath.h>
|
||||
#include <LibGUI/GDialog.h>
|
||||
#include <LibGUI/GTableView.h>
|
||||
|
||||
class GDirectoryModel;
|
||||
class GLabel;
|
||||
|
||||
class GFilePicker final : public GDialog {
|
||||
public:
|
||||
GFilePicker(const StringView& path = "/", CObject* parent = nullptr);
|
||||
virtual ~GFilePicker() override;
|
||||
|
||||
FileSystemPath selected_file() const { return m_selected_file; }
|
||||
|
||||
virtual const char* class_name() const override { return "GFilePicker"; }
|
||||
|
||||
private:
|
||||
void set_preview(const FileSystemPath&);
|
||||
void clear_preview();
|
||||
|
||||
GTableView* m_view { nullptr };
|
||||
NonnullRefPtr<GDirectoryModel> m_model;
|
||||
FileSystemPath m_selected_file;
|
||||
|
||||
GLabel* m_preview_image_label { nullptr };
|
||||
GLabel* m_preview_name_label { nullptr };
|
||||
GLabel* m_preview_geometry_label { nullptr };
|
||||
};
|
209
Libraries/LibGUI/GFileSystemModel.cpp
Normal file
209
Libraries/LibGUI/GFileSystemModel.cpp
Normal file
|
@ -0,0 +1,209 @@
|
|||
#include <AK/FileSystemPath.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCore/CDirIterator.h>
|
||||
#include <LibGUI/GFileSystemModel.h>
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct GFileSystemModel::Node {
|
||||
String name;
|
||||
Node* parent { nullptr };
|
||||
Vector<Node*> children;
|
||||
enum Type {
|
||||
Unknown,
|
||||
Directory,
|
||||
File
|
||||
};
|
||||
Type type { Unknown };
|
||||
|
||||
bool has_traversed { false };
|
||||
|
||||
GModelIndex index(const GFileSystemModel& model) const
|
||||
{
|
||||
if (!parent)
|
||||
return model.create_index(0, 0, const_cast<Node*>(this));
|
||||
for (int row = 0; row < parent->children.size(); ++row) {
|
||||
if (parent->children[row] == this)
|
||||
return model.create_index(row, 0, const_cast<Node*>(this));
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
void traverse_if_needed(const GFileSystemModel& model)
|
||||
{
|
||||
if (type != Node::Directory || has_traversed)
|
||||
return;
|
||||
has_traversed = true;
|
||||
|
||||
auto full_path = this->full_path(model);
|
||||
CDirIterator di(full_path, CDirIterator::SkipDots);
|
||||
if (di.has_error()) {
|
||||
fprintf(stderr, "CDirIterator: %s\n", di.error_string());
|
||||
return;
|
||||
}
|
||||
|
||||
while (di.has_next()) {
|
||||
String name = di.next_path();
|
||||
struct stat st;
|
||||
int rc = lstat(String::format("%s/%s", full_path.characters(), name.characters()).characters(), &st);
|
||||
if (rc < 0) {
|
||||
perror("lstat");
|
||||
continue;
|
||||
}
|
||||
if (model.m_mode == DirectoriesOnly && !S_ISDIR(st.st_mode))
|
||||
continue;
|
||||
auto* child = new Node;
|
||||
child->name = name;
|
||||
child->type = S_ISDIR(st.st_mode) ? Node::Type::Directory : Node::Type::File;
|
||||
child->parent = this;
|
||||
children.append(child);
|
||||
}
|
||||
}
|
||||
|
||||
void reify_if_needed(const GFileSystemModel& model)
|
||||
{
|
||||
traverse_if_needed(model);
|
||||
if (type != Node::Type::Unknown)
|
||||
return;
|
||||
struct stat st;
|
||||
auto full_path = this->full_path(model);
|
||||
int rc = lstat(full_path.characters(), &st);
|
||||
dbgprintf("lstat(%s) = %d\n", full_path.characters(), rc);
|
||||
if (rc < 0) {
|
||||
perror("lstat");
|
||||
return;
|
||||
}
|
||||
type = S_ISDIR(st.st_mode) ? Node::Type::Directory : Node::Type::File;
|
||||
}
|
||||
|
||||
String full_path(const GFileSystemModel& model) const
|
||||
{
|
||||
Vector<String, 32> lineage;
|
||||
for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) {
|
||||
lineage.append(ancestor->name);
|
||||
}
|
||||
StringBuilder builder;
|
||||
builder.append(model.root_path());
|
||||
for (int i = lineage.size() - 1; i >= 0; --i) {
|
||||
builder.append('/');
|
||||
builder.append(lineage[i]);
|
||||
}
|
||||
builder.append('/');
|
||||
builder.append(name);
|
||||
return FileSystemPath(builder.to_string()).string();
|
||||
}
|
||||
};
|
||||
|
||||
GModelIndex GFileSystemModel::index(const StringView& path) const
|
||||
{
|
||||
FileSystemPath canonical_path(path);
|
||||
const Node* node = m_root;
|
||||
if (canonical_path.string() == "/")
|
||||
return m_root->index(*this);
|
||||
for (int i = 0; i < canonical_path.parts().size(); ++i) {
|
||||
auto& part = canonical_path.parts()[i];
|
||||
bool found = false;
|
||||
for (auto& child : node->children) {
|
||||
if (child->name == part) {
|
||||
node = child;
|
||||
found = true;
|
||||
if (i == canonical_path.parts().size() - 1)
|
||||
return node->index(*this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
String GFileSystemModel::path(const GModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
auto& node = *(Node*)index.internal_data();
|
||||
node.reify_if_needed(*this);
|
||||
return node.full_path(*this);
|
||||
}
|
||||
|
||||
GFileSystemModel::GFileSystemModel(const StringView& root_path, Mode mode)
|
||||
: m_root_path(FileSystemPath(root_path).string())
|
||||
, m_mode(mode)
|
||||
{
|
||||
m_open_folder_icon = GIcon::default_icon("filetype-folder-open");
|
||||
m_closed_folder_icon = GIcon::default_icon("filetype-folder");
|
||||
m_file_icon = GIcon::default_icon("filetype-unknown");
|
||||
update();
|
||||
}
|
||||
|
||||
GFileSystemModel::~GFileSystemModel()
|
||||
{
|
||||
}
|
||||
|
||||
void GFileSystemModel::update()
|
||||
{
|
||||
// FIXME: Support refreshing the model!
|
||||
if (m_root)
|
||||
return;
|
||||
|
||||
m_root = new Node;
|
||||
m_root->name = m_root_path;
|
||||
m_root->reify_if_needed(*this);
|
||||
}
|
||||
|
||||
int GFileSystemModel::row_count(const GModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return 1;
|
||||
auto& node = *(Node*)index.internal_data();
|
||||
node.reify_if_needed(*this);
|
||||
if (node.type == Node::Type::Directory)
|
||||
return node.children.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
GModelIndex GFileSystemModel::index(int row, int column, const GModelIndex& parent) const
|
||||
{
|
||||
if (!parent.is_valid())
|
||||
return create_index(row, column, m_root);
|
||||
auto& node = *(Node*)parent.internal_data();
|
||||
return create_index(row, column, node.children[row]);
|
||||
}
|
||||
|
||||
GModelIndex GFileSystemModel::parent_index(const GModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
auto& node = *(const Node*)index.internal_data();
|
||||
if (!node.parent) {
|
||||
ASSERT(&node == m_root);
|
||||
return {};
|
||||
}
|
||||
return node.parent->index(*this);
|
||||
}
|
||||
|
||||
GVariant GFileSystemModel::data(const GModelIndex& index, Role role) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
auto& node = *(const Node*)index.internal_data();
|
||||
if (role == GModel::Role::Display)
|
||||
return node.name;
|
||||
if (role == GModel::Role::Icon) {
|
||||
if (node.type == Node::Directory) {
|
||||
if (selected_index() == index)
|
||||
return m_open_folder_icon;
|
||||
return m_closed_folder_icon;
|
||||
}
|
||||
return m_file_icon;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int GFileSystemModel::column_count(const GModelIndex&) const
|
||||
{
|
||||
return 1;
|
||||
}
|
44
Libraries/LibGUI/GFileSystemModel.h
Normal file
44
Libraries/LibGUI/GFileSystemModel.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GModel.h>
|
||||
|
||||
class GFileSystemModel : public GModel {
|
||||
friend class Node;
|
||||
|
||||
public:
|
||||
enum Mode {
|
||||
Invalid,
|
||||
DirectoriesOnly,
|
||||
FilesAndDirectories
|
||||
};
|
||||
|
||||
static NonnullRefPtr<GFileSystemModel> create(const StringView& root_path = "/", Mode mode = Mode::FilesAndDirectories)
|
||||
{
|
||||
return adopt(*new GFileSystemModel(root_path, mode));
|
||||
}
|
||||
virtual ~GFileSystemModel() override;
|
||||
|
||||
String root_path() const { return m_root_path; }
|
||||
String path(const GModelIndex&) const;
|
||||
GModelIndex index(const StringView& path) const;
|
||||
|
||||
virtual int row_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual int column_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual GVariant data(const GModelIndex&, Role = Role::Display) const override;
|
||||
virtual void update() override;
|
||||
virtual GModelIndex parent_index(const GModelIndex&) const override;
|
||||
virtual GModelIndex index(int row, int column = 0, const GModelIndex& parent = GModelIndex()) const override;
|
||||
|
||||
private:
|
||||
GFileSystemModel(const StringView& root_path, Mode);
|
||||
|
||||
String m_root_path;
|
||||
Mode m_mode { Invalid };
|
||||
|
||||
struct Node;
|
||||
Node* m_root { nullptr };
|
||||
|
||||
GIcon m_open_folder_icon;
|
||||
GIcon m_closed_folder_icon;
|
||||
GIcon m_file_icon;
|
||||
};
|
62
Libraries/LibGUI/GFontDatabase.cpp
Normal file
62
Libraries/LibGUI/GFontDatabase.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include <LibCore/CDirIterator.h>
|
||||
#include <LibGUI/GFontDatabase.h>
|
||||
#include <SharedGraphics/Font.h>
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static GFontDatabase* s_the;
|
||||
|
||||
GFontDatabase& GFontDatabase::the()
|
||||
{
|
||||
if (!s_the)
|
||||
s_the = new GFontDatabase;
|
||||
return *s_the;
|
||||
}
|
||||
|
||||
GFontDatabase::GFontDatabase()
|
||||
{
|
||||
CDirIterator di("/res/fonts", CDirIterator::SkipDots);
|
||||
if (di.has_error()) {
|
||||
fprintf(stderr, "CDirIterator: %s\n", di.error_string());
|
||||
exit(1);
|
||||
}
|
||||
while (di.has_next()) {
|
||||
String name = di.next_path();
|
||||
auto path = String::format("/res/fonts/%s", name.characters());
|
||||
if (auto font = Font::load_from_file(path)) {
|
||||
Metadata metadata;
|
||||
metadata.path = path;
|
||||
metadata.glyph_height = font->glyph_height();
|
||||
metadata.is_fixed_width = font->is_fixed_width();
|
||||
m_name_to_metadata.set(font->name(), move(metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GFontDatabase::~GFontDatabase()
|
||||
{
|
||||
}
|
||||
|
||||
void GFontDatabase::for_each_font(Function<void(const StringView&)> callback)
|
||||
{
|
||||
for (auto& it : m_name_to_metadata) {
|
||||
callback(it.key);
|
||||
}
|
||||
}
|
||||
|
||||
void GFontDatabase::for_each_fixed_width_font(Function<void(const StringView&)> callback)
|
||||
{
|
||||
for (auto& it : m_name_to_metadata) {
|
||||
if (it.value.is_fixed_width)
|
||||
callback(it.key);
|
||||
}
|
||||
}
|
||||
|
||||
RefPtr<Font> GFontDatabase::get_by_name(const StringView& name)
|
||||
{
|
||||
auto it = m_name_to_metadata.find(name);
|
||||
if (it == m_name_to_metadata.end())
|
||||
return nullptr;
|
||||
return Font::load_from_file((*it).value.path);
|
||||
}
|
33
Libraries/LibGUI/GFontDatabase.h
Normal file
33
Libraries/LibGUI/GFontDatabase.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
|
||||
class Font;
|
||||
|
||||
struct Metadata {
|
||||
String path;
|
||||
bool is_fixed_width;
|
||||
int glyph_height;
|
||||
};
|
||||
|
||||
class GFontDatabase {
|
||||
public:
|
||||
static GFontDatabase& the();
|
||||
|
||||
RefPtr<Font> get_by_name(const StringView&);
|
||||
void for_each_font(Function<void(const StringView&)>);
|
||||
void for_each_fixed_width_font(Function<void(const StringView&)>);
|
||||
|
||||
Metadata get_metadata_by_name(const StringView& name) const
|
||||
{
|
||||
return m_name_to_metadata.get(name);
|
||||
};
|
||||
|
||||
private:
|
||||
GFontDatabase();
|
||||
~GFontDatabase();
|
||||
|
||||
HashMap<String, Metadata> m_name_to_metadata;
|
||||
};
|
22
Libraries/LibGUI/GFrame.cpp
Normal file
22
Libraries/LibGUI/GFrame.cpp
Normal file
|
@ -0,0 +1,22 @@
|
|||
#include <LibGUI/GFrame.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
GFrame::GFrame(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GFrame::~GFrame()
|
||||
{
|
||||
}
|
||||
|
||||
void GFrame::paint_event(GPaintEvent& event)
|
||||
{
|
||||
if (m_shape == FrameShape::NoFrame)
|
||||
return;
|
||||
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
StylePainter::paint_frame(painter, rect(), m_shape, m_shadow, m_thickness, spans_entire_window_horizontally());
|
||||
}
|
32
Libraries/LibGUI/GFrame.h
Normal file
32
Libraries/LibGUI/GFrame.h
Normal file
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
class GFrame : public GWidget {
|
||||
public:
|
||||
explicit GFrame(GWidget* parent = nullptr);
|
||||
virtual ~GFrame() override;
|
||||
|
||||
int frame_thickness() const { return m_thickness; }
|
||||
void set_frame_thickness(int thickness) { m_thickness = thickness; }
|
||||
|
||||
FrameShadow frame_shadow() const { return m_shadow; }
|
||||
void set_frame_shadow(FrameShadow shadow) { m_shadow = shadow; }
|
||||
|
||||
FrameShape frame_shape() const { return m_shape; }
|
||||
void set_frame_shape(FrameShape shape) { m_shape = shape; }
|
||||
|
||||
Rect frame_inner_rect_for_size(const Size& size) const { return { m_thickness, m_thickness, size.width() - m_thickness * 2, size.height() - m_thickness * 2 }; }
|
||||
Rect frame_inner_rect() const { return frame_inner_rect_for_size(size()); }
|
||||
|
||||
virtual const char* class_name() const override { return "GFrame"; }
|
||||
|
||||
protected:
|
||||
void paint_event(GPaintEvent&) override;
|
||||
|
||||
private:
|
||||
int m_thickness { 0 };
|
||||
FrameShadow m_shadow { FrameShadow::Plain };
|
||||
FrameShape m_shape { FrameShape::NoFrame };
|
||||
};
|
39
Libraries/LibGUI/GGroupBox.cpp
Normal file
39
Libraries/LibGUI/GGroupBox.cpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
#include <LibGUI/GGroupBox.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
GGroupBox::GGroupBox(const StringView& title, GWidget* parent)
|
||||
: GWidget(parent)
|
||||
, m_title(title)
|
||||
{
|
||||
set_fill_with_background_color(true);
|
||||
set_background_color(Color::WarmGray);
|
||||
}
|
||||
|
||||
GGroupBox::~GGroupBox()
|
||||
{
|
||||
}
|
||||
|
||||
void GGroupBox::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
Rect frame_rect {
|
||||
0, font().glyph_height() / 2,
|
||||
width(), height() - font().glyph_height() / 2
|
||||
};
|
||||
StylePainter::paint_frame(painter, frame_rect, FrameShape::Box, FrameShadow::Sunken, 2);
|
||||
|
||||
Rect text_rect { 4, 0, font().width(m_title) + 6, font().glyph_height() };
|
||||
painter.fill_rect(text_rect, background_color());
|
||||
painter.draw_text(text_rect, m_title, TextAlignment::Center, foreground_color());
|
||||
}
|
||||
|
||||
void GGroupBox::set_title(const StringView& title)
|
||||
{
|
||||
if (m_title == title)
|
||||
return;
|
||||
m_title = title;
|
||||
update();
|
||||
}
|
20
Libraries/LibGUI/GGroupBox.h
Normal file
20
Libraries/LibGUI/GGroupBox.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GGroupBox : public GWidget {
|
||||
public:
|
||||
GGroupBox(const StringView& title, GWidget* parent);
|
||||
virtual ~GGroupBox() override;
|
||||
|
||||
String title() const { return m_title; }
|
||||
void set_title(const StringView&);
|
||||
|
||||
virtual const char* class_name() const override { return "GGroupBox"; }
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
private:
|
||||
String m_title;
|
||||
};
|
70
Libraries/LibGUI/GIcon.cpp
Normal file
70
Libraries/LibGUI/GIcon.cpp
Normal file
|
@ -0,0 +1,70 @@
|
|||
#include <LibGUI/GIcon.h>
|
||||
|
||||
GIcon::GIcon()
|
||||
: m_impl(GIconImpl::create())
|
||||
{
|
||||
}
|
||||
|
||||
GIcon::GIcon(const GIconImpl& impl)
|
||||
: m_impl(const_cast<GIconImpl&>(impl))
|
||||
{
|
||||
}
|
||||
|
||||
GIcon::GIcon(const GIcon& other)
|
||||
: m_impl(other.m_impl.copy_ref())
|
||||
{
|
||||
}
|
||||
|
||||
GIcon::GIcon(RefPtr<GraphicsBitmap>&& bitmap)
|
||||
: GIcon()
|
||||
{
|
||||
if (bitmap) {
|
||||
ASSERT(bitmap->width() == bitmap->height());
|
||||
int size = bitmap->width();
|
||||
set_bitmap_for_size(size, move(bitmap));
|
||||
}
|
||||
}
|
||||
|
||||
GIcon::GIcon(RefPtr<GraphicsBitmap>&& bitmap1, RefPtr<GraphicsBitmap>&& bitmap2)
|
||||
: GIcon(move(bitmap1))
|
||||
{
|
||||
if (bitmap2) {
|
||||
ASSERT(bitmap2->width() == bitmap2->height());
|
||||
int size = bitmap2->width();
|
||||
set_bitmap_for_size(size, move(bitmap2));
|
||||
}
|
||||
}
|
||||
|
||||
const GraphicsBitmap* GIconImpl::bitmap_for_size(int size) const
|
||||
{
|
||||
auto it = m_bitmaps.find(size);
|
||||
if (it != m_bitmaps.end())
|
||||
return it->value.ptr();
|
||||
|
||||
int best_diff_so_far = INT32_MAX;
|
||||
const GraphicsBitmap* best_fit = nullptr;
|
||||
for (auto& it : m_bitmaps) {
|
||||
int abs_diff = abs(it.key - size);
|
||||
if (abs_diff < best_diff_so_far) {
|
||||
best_diff_so_far = abs_diff;
|
||||
best_fit = it.value.ptr();
|
||||
}
|
||||
}
|
||||
return best_fit;
|
||||
}
|
||||
|
||||
void GIconImpl::set_bitmap_for_size(int size, RefPtr<GraphicsBitmap>&& bitmap)
|
||||
{
|
||||
if (!bitmap) {
|
||||
m_bitmaps.remove(size);
|
||||
return;
|
||||
}
|
||||
m_bitmaps.set(size, move(bitmap));
|
||||
}
|
||||
|
||||
GIcon GIcon::default_icon(const StringView& name)
|
||||
{
|
||||
auto bitmap16 = GraphicsBitmap::load_from_file(String::format("/res/icons/16x16/%s.png", name.characters()));
|
||||
auto bitmap32 = GraphicsBitmap::load_from_file(String::format("/res/icons/32x32/%s.png", name.characters()));
|
||||
return GIcon(move(bitmap16), move(bitmap32));
|
||||
}
|
43
Libraries/LibGUI/GIcon.h
Normal file
43
Libraries/LibGUI/GIcon.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
|
||||
class GIconImpl : public RefCounted<GIconImpl> {
|
||||
public:
|
||||
static NonnullRefPtr<GIconImpl> create() { return adopt(*new GIconImpl); }
|
||||
~GIconImpl() {}
|
||||
|
||||
const GraphicsBitmap* bitmap_for_size(int) const;
|
||||
void set_bitmap_for_size(int, RefPtr<GraphicsBitmap>&&);
|
||||
|
||||
private:
|
||||
GIconImpl() {}
|
||||
HashMap<int, RefPtr<GraphicsBitmap>> m_bitmaps;
|
||||
};
|
||||
|
||||
class GIcon {
|
||||
public:
|
||||
GIcon();
|
||||
explicit GIcon(RefPtr<GraphicsBitmap>&&);
|
||||
explicit GIcon(RefPtr<GraphicsBitmap>&&, RefPtr<GraphicsBitmap>&&);
|
||||
explicit GIcon(const GIconImpl&);
|
||||
GIcon(const GIcon&);
|
||||
~GIcon() {}
|
||||
|
||||
static GIcon default_icon(const StringView&);
|
||||
|
||||
GIcon& operator=(const GIcon& other)
|
||||
{
|
||||
m_impl = other.m_impl.copy_ref();
|
||||
return *this;
|
||||
}
|
||||
|
||||
const GraphicsBitmap* bitmap_for_size(int size) const { return m_impl->bitmap_for_size(size); }
|
||||
void set_bitmap_for_size(int size, RefPtr<GraphicsBitmap>&& bitmap) { m_impl->set_bitmap_for_size(size, move(bitmap)); }
|
||||
|
||||
const GIconImpl& impl() const { return *m_impl; }
|
||||
|
||||
private:
|
||||
NonnullRefPtr<GIconImpl> m_impl;
|
||||
};
|
80
Libraries/LibGUI/GInputBox.cpp
Normal file
80
Libraries/LibGUI/GInputBox.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GInputBox.h>
|
||||
#include <LibGUI/GLabel.h>
|
||||
#include <LibGUI/GTextEditor.h>
|
||||
#include <stdio.h>
|
||||
|
||||
GInputBox::GInputBox(const StringView& prompt, const StringView& title, CObject* parent)
|
||||
: GDialog(parent)
|
||||
, m_prompt(prompt)
|
||||
{
|
||||
set_title(title);
|
||||
build();
|
||||
}
|
||||
|
||||
GInputBox::~GInputBox()
|
||||
{
|
||||
}
|
||||
|
||||
void GInputBox::build()
|
||||
{
|
||||
auto* widget = new GWidget;
|
||||
set_main_widget(widget);
|
||||
|
||||
int text_width = widget->font().width(m_prompt);
|
||||
int title_width = widget->font().width(title()) + 24 /* icon, plus a little padding -- not perfect */;
|
||||
int max_width = AK::max(text_width, title_width);
|
||||
|
||||
set_rect(x(), y(), max_width + 80, 80);
|
||||
|
||||
widget->set_layout(make<GBoxLayout>(Orientation::Vertical));
|
||||
widget->set_fill_with_background_color(true);
|
||||
|
||||
widget->layout()->set_margins({ 8, 8, 8, 8 });
|
||||
widget->layout()->set_spacing(8);
|
||||
|
||||
auto* label = new GLabel(m_prompt, widget);
|
||||
label->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
label->set_preferred_size({ text_width, 16 });
|
||||
|
||||
m_text_editor = new GTextEditor(GTextEditor::SingleLine, widget);
|
||||
m_text_editor->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
m_text_editor->set_preferred_size({ 0, 19 });
|
||||
|
||||
auto* button_container_outer = new GWidget(widget);
|
||||
button_container_outer->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
button_container_outer->set_preferred_size({ 0, 20 });
|
||||
button_container_outer->set_layout(make<GBoxLayout>(Orientation::Vertical));
|
||||
|
||||
auto* button_container_inner = new GWidget(button_container_outer);
|
||||
button_container_inner->set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
button_container_inner->layout()->set_spacing(8);
|
||||
|
||||
m_cancel_button = new GButton(button_container_inner);
|
||||
m_cancel_button->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
m_cancel_button->set_preferred_size({ 0, 20 });
|
||||
m_cancel_button->set_text("Cancel");
|
||||
m_cancel_button->on_click = [this](auto&) {
|
||||
dbgprintf("GInputBox: Cancel button clicked\n");
|
||||
done(ExecCancel);
|
||||
};
|
||||
|
||||
m_ok_button = new GButton(button_container_inner);
|
||||
m_ok_button->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
m_ok_button->set_preferred_size({ 0, 20 });
|
||||
m_ok_button->set_text("OK");
|
||||
m_ok_button->on_click = [this](auto&) {
|
||||
dbgprintf("GInputBox: OK button clicked\n");
|
||||
m_text_value = m_text_editor->text();
|
||||
done(ExecOK);
|
||||
};
|
||||
|
||||
m_text_editor->on_return_pressed = [this] {
|
||||
m_ok_button->click();
|
||||
};
|
||||
m_text_editor->on_escape_pressed = [this] {
|
||||
m_cancel_button->click();
|
||||
};
|
||||
m_text_editor->set_focus(true);
|
||||
}
|
25
Libraries/LibGUI/GInputBox.h
Normal file
25
Libraries/LibGUI/GInputBox.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GDialog.h>
|
||||
|
||||
class GButton;
|
||||
class GTextEditor;
|
||||
|
||||
class GInputBox : public GDialog {
|
||||
public:
|
||||
explicit GInputBox(const StringView& prompt, const StringView& title, CObject* parent = nullptr);
|
||||
virtual ~GInputBox() override;
|
||||
|
||||
String text_value() const { return m_text_value; }
|
||||
|
||||
virtual const char* class_name() const override { return "GInputBox"; }
|
||||
|
||||
private:
|
||||
void build();
|
||||
String m_prompt;
|
||||
String m_text_value;
|
||||
|
||||
GButton* m_ok_button { nullptr };
|
||||
GButton* m_cancel_button { nullptr };
|
||||
GTextEditor* m_text_editor { nullptr };
|
||||
};
|
258
Libraries/LibGUI/GItemView.cpp
Normal file
258
Libraries/LibGUI/GItemView.cpp
Normal file
|
@ -0,0 +1,258 @@
|
|||
#include <Kernel/KeyCode.h>
|
||||
#include <LibGUI/GItemView.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
|
||||
GItemView::GItemView(GWidget* parent)
|
||||
: GAbstractView(parent)
|
||||
{
|
||||
set_frame_shape(FrameShape::Container);
|
||||
set_frame_shadow(FrameShadow::Sunken);
|
||||
set_frame_thickness(2);
|
||||
horizontal_scrollbar().set_visible(false);
|
||||
}
|
||||
|
||||
GItemView::~GItemView()
|
||||
{
|
||||
}
|
||||
|
||||
void GItemView::scroll_into_view(const GModelIndex& index, Orientation orientation)
|
||||
{
|
||||
GScrollableWidget::scroll_into_view(item_rect(index.row()), orientation);
|
||||
}
|
||||
|
||||
void GItemView::resize_event(GResizeEvent& event)
|
||||
{
|
||||
GAbstractView::resize_event(event);
|
||||
update_content_size();
|
||||
}
|
||||
|
||||
void GItemView::did_update_model()
|
||||
{
|
||||
GAbstractView::did_update_model();
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
void GItemView::update_content_size()
|
||||
{
|
||||
if (!model())
|
||||
return set_content_size({});
|
||||
|
||||
m_visual_column_count = available_size().width() / effective_item_size().width();
|
||||
if (m_visual_column_count)
|
||||
m_visual_row_count = ceil_div(model()->row_count(), m_visual_column_count);
|
||||
else
|
||||
m_visual_row_count = 0;
|
||||
|
||||
int content_width = available_size().width();
|
||||
int content_height = m_visual_row_count * effective_item_size().height();
|
||||
|
||||
set_content_size({ content_width, content_height });
|
||||
}
|
||||
|
||||
Rect GItemView::item_rect(int item_index) const
|
||||
{
|
||||
if (!m_visual_row_count || !m_visual_column_count)
|
||||
return {};
|
||||
int visual_row_index = item_index / m_visual_column_count;
|
||||
int visual_column_index = item_index % m_visual_column_count;
|
||||
return {
|
||||
visual_column_index * effective_item_size().width(),
|
||||
visual_row_index * effective_item_size().height(),
|
||||
effective_item_size().width(),
|
||||
effective_item_size().height()
|
||||
};
|
||||
}
|
||||
|
||||
void GItemView::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
// FIXME: Since all items are the same size, just compute the clicked item index
|
||||
// instead of iterating over everything.
|
||||
auto adjusted_position = event.position().translated(0, vertical_scrollbar().value());
|
||||
for (int i = 0; i < item_count(); ++i) {
|
||||
if (item_rect(i).contains(adjusted_position)) {
|
||||
model()->set_selected_index(model()->index(i, 0));
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
model()->set_selected_index({});
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void GItemView::doubleclick_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
mousedown_event(event);
|
||||
activate(model()->selected_index());
|
||||
}
|
||||
}
|
||||
|
||||
void GItemView::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GFrame::paint_event(event);
|
||||
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(widget_inner_rect());
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.fill_rect(event.rect(), Color::White);
|
||||
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
|
||||
|
||||
auto column_metadata = model()->column_metadata(m_model_column);
|
||||
const Font& font = column_metadata.font ? *column_metadata.font : this->font();
|
||||
|
||||
for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
|
||||
bool is_selected_item = item_index == model()->selected_index().row();
|
||||
Color background_color;
|
||||
if (is_selected_item) {
|
||||
background_color = is_focused() ? Color::from_rgb(0x84351a) : Color::from_rgb(0x606060);
|
||||
} else {
|
||||
background_color = Color::White;
|
||||
}
|
||||
|
||||
Rect item_rect = this->item_rect(item_index);
|
||||
auto model_index = model()->index(item_index, m_model_column);
|
||||
|
||||
auto icon = model()->data(model_index, GModel::Role::Icon);
|
||||
auto item_text = model()->data(model_index, GModel::Role::Display);
|
||||
|
||||
Rect icon_rect = { 0, 0, 32, 32 };
|
||||
icon_rect.center_within(item_rect);
|
||||
icon_rect.move_by(0, -font.glyph_height() - 6);
|
||||
|
||||
if (icon.is_icon()) {
|
||||
if (auto bitmap = icon.as_icon().bitmap_for_size(icon_rect.width()))
|
||||
painter.draw_scaled_bitmap(icon_rect, *bitmap, bitmap->rect());
|
||||
}
|
||||
|
||||
Rect text_rect { 0, icon_rect.bottom() + 6 + 1, font.width(item_text.to_string()), font.glyph_height() };
|
||||
text_rect.center_horizontally_within(item_rect);
|
||||
text_rect.inflate(6, 4);
|
||||
|
||||
Color text_color;
|
||||
if (is_selected_item)
|
||||
text_color = Color::White;
|
||||
else
|
||||
text_color = model()->data(model_index, GModel::Role::ForegroundColor).to_color(Color::Black);
|
||||
painter.fill_rect(text_rect, background_color);
|
||||
painter.draw_text(text_rect, item_text.to_string(), font, TextAlignment::Center, text_color);
|
||||
};
|
||||
}
|
||||
|
||||
int GItemView::item_count() const
|
||||
{
|
||||
if (!model())
|
||||
return 0;
|
||||
return model()->row_count();
|
||||
}
|
||||
|
||||
void GItemView::keydown_event(GKeyEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
if (!m_visual_row_count || !m_visual_column_count)
|
||||
return;
|
||||
|
||||
auto& model = *this->model();
|
||||
if (event.key() == KeyCode::Key_Return) {
|
||||
activate(model.selected_index());
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Home) {
|
||||
auto new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_End) {
|
||||
auto new_index = model.index(model.row_count() - 1, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Up) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() - m_visual_column_count, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Down) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() + m_visual_column_count, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Left) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() - 1, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Right) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() + 1, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageUp) {
|
||||
int items_per_page = (visible_content_rect().height() / effective_item_size().height()) * m_visual_column_count;
|
||||
auto new_index = model.index(max(0, model.selected_index().row() - items_per_page), model.selected_index().column());
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageDown) {
|
||||
int items_per_page = (visible_content_rect().height() / effective_item_size().height()) * m_visual_column_count;
|
||||
auto new_index = model.index(min(model.row_count() - 1, model.selected_index().row() + items_per_page), model.selected_index().column());
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
return GWidget::keydown_event(event);
|
||||
}
|
45
Libraries/LibGUI/GItemView.h
Normal file
45
Libraries/LibGUI/GItemView.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/GAbstractView.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
|
||||
class GScrollBar;
|
||||
class Painter;
|
||||
|
||||
class GItemView : public GAbstractView {
|
||||
public:
|
||||
explicit GItemView(GWidget* parent);
|
||||
virtual ~GItemView() override;
|
||||
|
||||
int content_width() const;
|
||||
int horizontal_padding() const { return m_horizontal_padding; }
|
||||
|
||||
void scroll_into_view(const GModelIndex&, Orientation);
|
||||
Size effective_item_size() const { return m_effective_item_size; }
|
||||
|
||||
int model_column() const { return m_model_column; }
|
||||
void set_model_column(int column) { m_model_column = column; }
|
||||
|
||||
virtual const char* class_name() const override { return "GItemView"; }
|
||||
|
||||
private:
|
||||
virtual void did_update_model() override;
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void doubleclick_event(GMouseEvent&) override;
|
||||
|
||||
int item_count() const;
|
||||
Rect item_rect(int item_index) const;
|
||||
void update_content_size();
|
||||
|
||||
int m_horizontal_padding { 5 };
|
||||
int m_model_column { 0 };
|
||||
int m_visual_column_count { 0 };
|
||||
int m_visual_row_count { 0 };
|
||||
|
||||
Size m_effective_item_size { 80, 80 };
|
||||
};
|
69
Libraries/LibGUI/GLabel.cpp
Normal file
69
Libraries/LibGUI/GLabel.cpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "GLabel.h"
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
|
||||
GLabel::GLabel(GWidget* parent)
|
||||
: GFrame(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GLabel::GLabel(const StringView& text, GWidget* parent)
|
||||
: GFrame(parent)
|
||||
, m_text(text)
|
||||
{
|
||||
}
|
||||
|
||||
GLabel::~GLabel()
|
||||
{
|
||||
}
|
||||
|
||||
void GLabel::set_icon(RefPtr<GraphicsBitmap>&& icon)
|
||||
{
|
||||
m_icon = move(icon);
|
||||
}
|
||||
|
||||
void GLabel::set_text(const StringView& text)
|
||||
{
|
||||
if (text == m_text)
|
||||
return;
|
||||
m_text = move(text);
|
||||
update();
|
||||
}
|
||||
|
||||
void GLabel::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GFrame::paint_event(event);
|
||||
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
if (m_icon) {
|
||||
if (m_should_stretch_icon) {
|
||||
painter.draw_scaled_bitmap(frame_inner_rect(), *m_icon, m_icon->rect());
|
||||
} else {
|
||||
auto icon_location = frame_inner_rect().center().translated(-(m_icon->width() / 2), -(m_icon->height() / 2));
|
||||
painter.blit(icon_location, *m_icon, m_icon->rect());
|
||||
}
|
||||
}
|
||||
if (text().is_empty())
|
||||
return;
|
||||
int indent = 0;
|
||||
if (frame_thickness() > 0)
|
||||
indent = font().glyph_width('x') / 2;
|
||||
auto text_rect = frame_inner_rect();
|
||||
text_rect.move_by(indent, 0);
|
||||
text_rect.set_width(text_rect.width() - indent * 2);
|
||||
|
||||
if (is_enabled()) {
|
||||
painter.draw_text(text_rect, text(), m_text_alignment, foreground_color(), TextElision::Right);
|
||||
} else {
|
||||
painter.draw_text(text_rect.translated(1, 1), text(), font(), text_alignment(), Color::White, TextElision::Right);
|
||||
painter.draw_text(text_rect, text(), font(), text_alignment(), Color::from_rgb(0x808080), TextElision::Right);
|
||||
}
|
||||
}
|
||||
|
||||
void GLabel::size_to_fit()
|
||||
{
|
||||
set_size_policy(SizePolicy::Fixed, SizePolicy::Fill);
|
||||
set_preferred_size({ font().width(m_text), 0 });
|
||||
}
|
38
Libraries/LibGUI/GLabel.h
Normal file
38
Libraries/LibGUI/GLabel.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GFrame.h>
|
||||
#include <SharedGraphics/TextAlignment.h>
|
||||
|
||||
class GraphicsBitmap;
|
||||
|
||||
class GLabel : public GFrame {
|
||||
public:
|
||||
explicit GLabel(GWidget* parent = nullptr);
|
||||
GLabel(const StringView& text, GWidget* parent = nullptr);
|
||||
virtual ~GLabel() override;
|
||||
|
||||
String text() const { return m_text; }
|
||||
void set_text(const StringView&);
|
||||
|
||||
void set_icon(RefPtr<GraphicsBitmap>&&);
|
||||
const GraphicsBitmap* icon() const { return m_icon.ptr(); }
|
||||
GraphicsBitmap* icon() { return m_icon.ptr(); }
|
||||
|
||||
TextAlignment text_alignment() const { return m_text_alignment; }
|
||||
void set_text_alignment(TextAlignment text_alignment) { m_text_alignment = text_alignment; }
|
||||
|
||||
bool should_stretch_icon() const { return m_should_stretch_icon; }
|
||||
void set_should_stretch_icon(bool b) { m_should_stretch_icon = b; }
|
||||
|
||||
void size_to_fit();
|
||||
|
||||
virtual const char* class_name() const override { return "GLabel"; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
String m_text;
|
||||
RefPtr<GraphicsBitmap> m_icon;
|
||||
TextAlignment m_text_alignment { TextAlignment::Center };
|
||||
bool m_should_stretch_icon { false };
|
||||
};
|
80
Libraries/LibGUI/GLayout.cpp
Normal file
80
Libraries/LibGUI/GLayout.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include <LibGUI/GLayout.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
GLayout::GLayout()
|
||||
{
|
||||
}
|
||||
|
||||
GLayout::~GLayout()
|
||||
{
|
||||
}
|
||||
|
||||
void GLayout::notify_adopted(Badge<GWidget>, GWidget& widget)
|
||||
{
|
||||
if (m_owner == &widget)
|
||||
return;
|
||||
m_owner = widget.make_weak_ptr();
|
||||
}
|
||||
|
||||
void GLayout::notify_disowned(Badge<GWidget>, GWidget& widget)
|
||||
{
|
||||
ASSERT(m_owner == &widget);
|
||||
m_owner.clear();
|
||||
}
|
||||
|
||||
void GLayout::add_entry(Entry&& entry)
|
||||
{
|
||||
m_entries.append(move(entry));
|
||||
if (m_owner)
|
||||
m_owner->notify_layout_changed({});
|
||||
}
|
||||
|
||||
void GLayout::add_spacer()
|
||||
{
|
||||
Entry entry;
|
||||
entry.type = Entry::Type::Spacer;
|
||||
add_entry(move(entry));
|
||||
}
|
||||
|
||||
void GLayout::add_layout(OwnPtr<GLayout>&& layout)
|
||||
{
|
||||
Entry entry;
|
||||
entry.type = Entry::Type::Layout;
|
||||
entry.layout = move(layout);
|
||||
add_entry(move(entry));
|
||||
}
|
||||
|
||||
void GLayout::add_widget(GWidget& widget)
|
||||
{
|
||||
Entry entry;
|
||||
entry.type = Entry::Type::Widget;
|
||||
entry.widget = widget.make_weak_ptr();
|
||||
add_entry(move(entry));
|
||||
}
|
||||
|
||||
void GLayout::remove_widget(GWidget& widget)
|
||||
{
|
||||
m_entries.remove_first_matching([&](auto& entry) {
|
||||
return entry.widget == &widget;
|
||||
});
|
||||
if (m_owner)
|
||||
m_owner->notify_layout_changed({});
|
||||
}
|
||||
|
||||
void GLayout::set_spacing(int spacing)
|
||||
{
|
||||
if (m_spacing == spacing)
|
||||
return;
|
||||
m_spacing = spacing;
|
||||
if (m_owner)
|
||||
m_owner->notify_layout_changed({});
|
||||
}
|
||||
|
||||
void GLayout::set_margins(const GMargins& margins)
|
||||
{
|
||||
if (m_margins == margins)
|
||||
return;
|
||||
m_margins = margins;
|
||||
if (m_owner)
|
||||
m_owner->notify_layout_changed({});
|
||||
}
|
53
Libraries/LibGUI/GLayout.h
Normal file
53
Libraries/LibGUI/GLayout.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibGUI/GMargins.h>
|
||||
|
||||
class GWidget;
|
||||
|
||||
class GLayout {
|
||||
public:
|
||||
GLayout();
|
||||
virtual ~GLayout();
|
||||
|
||||
void add_widget(GWidget&);
|
||||
void add_layout(OwnPtr<GLayout>&&);
|
||||
void add_spacer();
|
||||
|
||||
void remove_widget(GWidget&);
|
||||
|
||||
virtual void run(GWidget&) = 0;
|
||||
|
||||
void notify_adopted(Badge<GWidget>, GWidget&);
|
||||
void notify_disowned(Badge<GWidget>, GWidget&);
|
||||
|
||||
GMargins margins() const { return m_margins; }
|
||||
void set_margins(const GMargins&);
|
||||
|
||||
int spacing() const { return m_spacing; }
|
||||
void set_spacing(int);
|
||||
|
||||
protected:
|
||||
struct Entry {
|
||||
enum class Type {
|
||||
Invalid = 0,
|
||||
Widget,
|
||||
Layout,
|
||||
Spacer,
|
||||
};
|
||||
|
||||
Type type { Type::Invalid };
|
||||
WeakPtr<GWidget> widget;
|
||||
OwnPtr<GLayout> layout;
|
||||
};
|
||||
void add_entry(Entry&&);
|
||||
|
||||
WeakPtr<GWidget> m_owner;
|
||||
Vector<Entry> m_entries;
|
||||
|
||||
GMargins m_margins;
|
||||
int m_spacing { 4 };
|
||||
};
|
227
Libraries/LibGUI/GListView.cpp
Normal file
227
Libraries/LibGUI/GListView.cpp
Normal file
|
@ -0,0 +1,227 @@
|
|||
#include <Kernel/KeyCode.h>
|
||||
#include <LibGUI/GListView.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
|
||||
GListView::GListView(GWidget* parent)
|
||||
: GAbstractView(parent)
|
||||
{
|
||||
set_frame_shape(FrameShape::Container);
|
||||
set_frame_shadow(FrameShadow::Sunken);
|
||||
set_frame_thickness(2);
|
||||
}
|
||||
|
||||
GListView::~GListView()
|
||||
{
|
||||
}
|
||||
|
||||
void GListView::update_content_size()
|
||||
{
|
||||
if (!model())
|
||||
return set_content_size({});
|
||||
|
||||
int content_width = 0;
|
||||
for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) {
|
||||
auto text = model()->data(model()->index(row, m_model_column), GModel::Role::Display);
|
||||
content_width = max(content_width, font().width(text.to_string()));
|
||||
}
|
||||
|
||||
content_width = max(content_width, widget_inner_rect().width());
|
||||
|
||||
int content_height = item_count() * item_height();
|
||||
set_content_size({ content_width, content_height });
|
||||
}
|
||||
|
||||
void GListView::resize_event(GResizeEvent& event)
|
||||
{
|
||||
update_content_size();
|
||||
GAbstractView::resize_event(event);
|
||||
}
|
||||
|
||||
void GListView::did_update_model()
|
||||
{
|
||||
GAbstractView::did_update_model();
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
Rect GListView::content_rect(int row) const
|
||||
{
|
||||
return { 0, row * item_height(), content_width(), item_height() };
|
||||
}
|
||||
|
||||
Rect GListView::content_rect(const GModelIndex& index) const
|
||||
{
|
||||
return content_rect(index.row());
|
||||
}
|
||||
|
||||
Point GListView::adjusted_position(const Point& position)
|
||||
{
|
||||
return position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
|
||||
}
|
||||
|
||||
void GListView::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
|
||||
if (event.button() != GMouseButton::Left)
|
||||
return;
|
||||
|
||||
auto adjusted_position = this->adjusted_position(event.position());
|
||||
for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) {
|
||||
if (!content_rect(row).contains(adjusted_position))
|
||||
continue;
|
||||
model()->set_selected_index(model()->index(row, m_model_column));
|
||||
update();
|
||||
return;
|
||||
}
|
||||
model()->set_selected_index({});
|
||||
update();
|
||||
}
|
||||
|
||||
void GListView::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GFrame::paint_event(event);
|
||||
|
||||
if (!model())
|
||||
return;
|
||||
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(frame_inner_rect());
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
|
||||
|
||||
int exposed_width = max(content_size().width(), width());
|
||||
int painted_item_index = 0;
|
||||
|
||||
for (int row_index = 0; row_index < model()->row_count(); ++row_index) {
|
||||
bool is_selected_row = row_index == model()->selected_index().row();
|
||||
int y = painted_item_index * item_height();
|
||||
|
||||
Color background_color;
|
||||
if (is_selected_row) {
|
||||
background_color = is_focused() ? Color::from_rgb(0x84351a) : Color::from_rgb(0x606060);
|
||||
} else {
|
||||
if (alternating_row_colors() && (painted_item_index % 2))
|
||||
background_color = Color(210, 210, 210);
|
||||
else
|
||||
background_color = Color::White;
|
||||
}
|
||||
|
||||
auto column_metadata = model()->column_metadata(m_model_column);
|
||||
const Font& font = column_metadata.font ? *column_metadata.font : this->font();
|
||||
Rect row_rect(0, y, content_width(), item_height());
|
||||
painter.fill_rect(row_rect, background_color);
|
||||
auto index = model()->index(row_index, m_model_column);
|
||||
auto data = model()->data(index);
|
||||
if (data.is_bitmap()) {
|
||||
painter.blit(row_rect.location(), data.as_bitmap(), data.as_bitmap().rect());
|
||||
} else if (data.is_icon()) {
|
||||
if (auto bitmap = data.as_icon().bitmap_for_size(16))
|
||||
painter.blit(row_rect.location(), *bitmap, bitmap->rect());
|
||||
} else {
|
||||
Color text_color;
|
||||
if (is_selected_row)
|
||||
text_color = Color::White;
|
||||
else
|
||||
text_color = model()->data(index, GModel::Role::ForegroundColor).to_color(Color::Black);
|
||||
auto text_rect = row_rect;
|
||||
text_rect.move_by(horizontal_padding(), 0);
|
||||
text_rect.set_width(text_rect.width() - horizontal_padding() * 2);
|
||||
painter.draw_text(text_rect, data.to_string(), font, column_metadata.text_alignment, text_color);
|
||||
}
|
||||
|
||||
++painted_item_index;
|
||||
};
|
||||
|
||||
Rect unpainted_rect(0, painted_item_index * item_height(), exposed_width, height());
|
||||
painter.fill_rect(unpainted_rect, Color::White);
|
||||
}
|
||||
|
||||
int GListView::item_count() const
|
||||
{
|
||||
if (!model())
|
||||
return 0;
|
||||
return model()->row_count();
|
||||
}
|
||||
|
||||
void GListView::keydown_event(GKeyEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
if (event.key() == KeyCode::Key_Return) {
|
||||
activate(model.selected_index());
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Up) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() - 1, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Down) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() + 1, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageUp) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto new_index = model.index(max(0, model.selected_index().row() - items_per_page), model.selected_index().column());
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageDown) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto new_index = model.index(min(model.row_count() - 1, model.selected_index().row() + items_per_page), model.selected_index().column());
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
return GWidget::keydown_event(event);
|
||||
}
|
||||
|
||||
void GListView::scroll_into_view(const GModelIndex& index, Orientation orientation)
|
||||
{
|
||||
auto rect = content_rect(index.row());
|
||||
GScrollableWidget::scroll_into_view(rect, orientation);
|
||||
}
|
||||
|
||||
void GListView::doubleclick_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
if (model.selected_index().is_valid()) {
|
||||
if (is_editable())
|
||||
begin_editing(model.selected_index());
|
||||
else
|
||||
activate(model.selected_index());
|
||||
}
|
||||
}
|
||||
}
|
46
Libraries/LibGUI/GListView.h
Normal file
46
Libraries/LibGUI/GListView.h
Normal file
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/GAbstractView.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
|
||||
class GScrollBar;
|
||||
class Painter;
|
||||
|
||||
class GListView : public GAbstractView {
|
||||
public:
|
||||
explicit GListView(GWidget* parent);
|
||||
virtual ~GListView() override;
|
||||
|
||||
int item_height() const { return 16; }
|
||||
|
||||
bool alternating_row_colors() const { return m_alternating_row_colors; }
|
||||
void set_alternating_row_colors(bool b) { m_alternating_row_colors = b; }
|
||||
|
||||
int horizontal_padding() const { return m_horizontal_padding; }
|
||||
|
||||
void scroll_into_view(const GModelIndex&, Orientation);
|
||||
|
||||
Point adjusted_position(const Point&);
|
||||
|
||||
virtual Rect content_rect(const GModelIndex&) const override;
|
||||
|
||||
virtual const char* class_name() const override { return "GListView"; }
|
||||
|
||||
private:
|
||||
virtual void did_update_model() override;
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void doubleclick_event(GMouseEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
|
||||
Rect content_rect(int row) const;
|
||||
int item_count() const;
|
||||
void update_content_size();
|
||||
|
||||
int m_horizontal_padding { 2 };
|
||||
int m_model_column { 0 };
|
||||
bool m_alternating_row_colors { true };
|
||||
};
|
40
Libraries/LibGUI/GMargins.h
Normal file
40
Libraries/LibGUI/GMargins.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
class GMargins {
|
||||
public:
|
||||
GMargins() {}
|
||||
GMargins(int left, int top, int right, int bottom)
|
||||
: m_left(left)
|
||||
, m_top(top)
|
||||
, m_right(right)
|
||||
, m_bottom(bottom)
|
||||
{
|
||||
}
|
||||
~GMargins() {}
|
||||
|
||||
bool is_null() const { return !m_left && !m_top && !m_right && !m_bottom; }
|
||||
|
||||
int left() const { return m_left; }
|
||||
int top() const { return m_top; }
|
||||
int right() const { return m_right; }
|
||||
int bottom() const { return m_bottom; }
|
||||
|
||||
void set_left(int value) { m_left = value; }
|
||||
void set_top(int value) { m_top = value; }
|
||||
void set_right(int value) { m_right = value; }
|
||||
void set_bottom(int value) { m_bottom = value; }
|
||||
|
||||
bool operator==(const GMargins& other) const
|
||||
{
|
||||
return m_left == other.m_left
|
||||
&& m_top == other.m_top
|
||||
&& m_right == other.m_right
|
||||
&& m_bottom == other.m_bottom;
|
||||
}
|
||||
|
||||
private:
|
||||
int m_left { 0 };
|
||||
int m_top { 0 };
|
||||
int m_right { 0 };
|
||||
int m_bottom { 0 };
|
||||
};
|
140
Libraries/LibGUI/GMenu.cpp
Normal file
140
Libraries/LibGUI/GMenu.cpp
Normal file
|
@ -0,0 +1,140 @@
|
|||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
#include <LibGUI/GMenu.h>
|
||||
|
||||
//#define GMENU_DEBUG
|
||||
|
||||
static HashMap<int, GMenu*>& all_menus()
|
||||
{
|
||||
static HashMap<int, GMenu*>* map;
|
||||
if (!map)
|
||||
map = new HashMap<int, GMenu*>();
|
||||
return *map;
|
||||
}
|
||||
|
||||
GMenu* GMenu::from_menu_id(int menu_id)
|
||||
{
|
||||
auto it = all_menus().find(menu_id);
|
||||
if (it == all_menus().end())
|
||||
return nullptr;
|
||||
return (*it).value;
|
||||
}
|
||||
|
||||
GMenu::GMenu(const StringView& name)
|
||||
: m_name(name)
|
||||
{
|
||||
}
|
||||
|
||||
GMenu::~GMenu()
|
||||
{
|
||||
unrealize_menu();
|
||||
}
|
||||
|
||||
void GMenu::add_action(NonnullRefPtr<GAction> action)
|
||||
{
|
||||
m_items.append(make<GMenuItem>(m_menu_id, move(action)));
|
||||
#ifdef GMENU_DEBUG
|
||||
dbgprintf("GMenu::add_action(): MenuItem Menu ID: %d\n", m_menu_id);
|
||||
#endif
|
||||
}
|
||||
|
||||
void GMenu::add_separator()
|
||||
{
|
||||
m_items.append(make<GMenuItem>(m_menu_id, GMenuItem::Separator));
|
||||
}
|
||||
|
||||
void GMenu::popup(const Point& screen_position)
|
||||
{
|
||||
if (m_menu_id == -1)
|
||||
realize_menu();
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::PopupMenu;
|
||||
request.menu.menu_id = m_menu_id;
|
||||
request.menu.position = screen_position;
|
||||
GEventLoop::post_message_to_server(request);
|
||||
}
|
||||
|
||||
void GMenu::dismiss()
|
||||
{
|
||||
if (m_menu_id == -1)
|
||||
return;
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::DismissMenu;
|
||||
request.menu.menu_id = m_menu_id;
|
||||
GEventLoop::post_message_to_server(request);
|
||||
}
|
||||
|
||||
int GMenu::realize_menu()
|
||||
{
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::CreateMenu;
|
||||
ASSERT(m_name.length() < (ssize_t)sizeof(request.text));
|
||||
strcpy(request.text, m_name.characters());
|
||||
request.text_length = m_name.length();
|
||||
auto response = GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidCreateMenu);
|
||||
m_menu_id = response.menu.menu_id;
|
||||
|
||||
#ifdef GMENU_DEBUG
|
||||
dbgprintf("GMenu::realize_menu(): New menu ID: %d\n", m_menu_id);
|
||||
#endif
|
||||
ASSERT(m_menu_id > 0);
|
||||
for (int i = 0; i < m_items.size(); ++i) {
|
||||
auto& item = *m_items[i];
|
||||
item.set_menu_id({}, m_menu_id);
|
||||
item.set_identifier({}, i);
|
||||
if (item.type() == GMenuItem::Separator) {
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::AddMenuSeparator;
|
||||
request.menu.menu_id = m_menu_id;
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidAddMenuSeparator);
|
||||
continue;
|
||||
}
|
||||
if (item.type() == GMenuItem::Action) {
|
||||
auto& action = *item.action();
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::AddMenuItem;
|
||||
request.menu.menu_id = m_menu_id;
|
||||
request.menu.identifier = i;
|
||||
request.menu.enabled = action.is_enabled();
|
||||
request.menu.checkable = action.is_checkable();
|
||||
if (action.is_checkable())
|
||||
request.menu.checked = action.is_checked();
|
||||
ASSERT(action.text().length() < (ssize_t)sizeof(request.text));
|
||||
strcpy(request.text, action.text().characters());
|
||||
request.text_length = action.text().length();
|
||||
|
||||
if (action.shortcut().is_valid()) {
|
||||
auto shortcut_text = action.shortcut().to_string();
|
||||
ASSERT(shortcut_text.length() < (ssize_t)sizeof(request.menu.shortcut_text));
|
||||
strcpy(request.menu.shortcut_text, shortcut_text.characters());
|
||||
request.menu.shortcut_text_length = shortcut_text.length();
|
||||
} else {
|
||||
request.menu.shortcut_text_length = 0;
|
||||
}
|
||||
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidAddMenuItem);
|
||||
}
|
||||
}
|
||||
all_menus().set(m_menu_id, this);
|
||||
return m_menu_id;
|
||||
}
|
||||
|
||||
void GMenu::unrealize_menu()
|
||||
{
|
||||
if (m_menu_id == -1)
|
||||
return;
|
||||
all_menus().remove(m_menu_id);
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::DestroyMenu;
|
||||
request.menu.menu_id = m_menu_id;
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidDestroyMenu);
|
||||
m_menu_id = 0;
|
||||
}
|
||||
|
||||
GAction* GMenu::action_at(int index)
|
||||
{
|
||||
if (index >= m_items.size())
|
||||
return nullptr;
|
||||
return m_items[index]->action();
|
||||
}
|
39
Libraries/LibGUI/GMenu.h
Normal file
39
Libraries/LibGUI/GMenu.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/GMenuItem.h>
|
||||
|
||||
class GAction;
|
||||
class Point;
|
||||
|
||||
class GMenu {
|
||||
public:
|
||||
explicit GMenu(const StringView& name);
|
||||
~GMenu();
|
||||
|
||||
static GMenu* from_menu_id(int);
|
||||
|
||||
GAction* action_at(int);
|
||||
|
||||
void add_action(NonnullRefPtr<GAction>);
|
||||
void add_separator();
|
||||
|
||||
void popup(const Point& screen_position);
|
||||
void dismiss();
|
||||
|
||||
Function<void(unsigned)> on_item_activation;
|
||||
|
||||
private:
|
||||
friend class GMenuBar;
|
||||
|
||||
int menu_id() const { return m_menu_id; }
|
||||
int realize_menu();
|
||||
void unrealize_menu();
|
||||
|
||||
int m_menu_id { -1 };
|
||||
String m_name;
|
||||
Vector<OwnPtr<GMenuItem>> m_items;
|
||||
};
|
61
Libraries/LibGUI/GMenuBar.cpp
Normal file
61
Libraries/LibGUI/GMenuBar.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#include <LibGUI/GEventLoop.h>
|
||||
#include <LibGUI/GMenuBar.h>
|
||||
|
||||
GMenuBar::GMenuBar()
|
||||
{
|
||||
}
|
||||
|
||||
GMenuBar::~GMenuBar()
|
||||
{
|
||||
unrealize_menubar();
|
||||
}
|
||||
|
||||
void GMenuBar::add_menu(OwnPtr<GMenu>&& menu)
|
||||
{
|
||||
m_menus.append(move(menu));
|
||||
}
|
||||
|
||||
int GMenuBar::realize_menubar()
|
||||
{
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::CreateMenubar;
|
||||
WSAPI_ServerMessage response = GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidCreateMenubar);
|
||||
return response.menu.menubar_id;
|
||||
}
|
||||
|
||||
void GMenuBar::unrealize_menubar()
|
||||
{
|
||||
if (m_menubar_id == -1)
|
||||
return;
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::DestroyMenubar;
|
||||
request.menu.menubar_id = m_menubar_id;
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidDestroyMenubar);
|
||||
m_menubar_id = -1;
|
||||
}
|
||||
|
||||
void GMenuBar::notify_added_to_application(Badge<GApplication>)
|
||||
{
|
||||
ASSERT(m_menubar_id == -1);
|
||||
m_menubar_id = realize_menubar();
|
||||
ASSERT(m_menubar_id != -1);
|
||||
for (auto& menu : m_menus) {
|
||||
ASSERT(menu);
|
||||
int menu_id = menu->realize_menu();
|
||||
ASSERT(menu_id != -1);
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::AddMenuToMenubar;
|
||||
request.menu.menubar_id = m_menubar_id;
|
||||
request.menu.menu_id = menu_id;
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidAddMenuToMenubar);
|
||||
}
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::SetApplicationMenubar;
|
||||
request.menu.menubar_id = m_menubar_id;
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidSetApplicationMenubar);
|
||||
}
|
||||
|
||||
void GMenuBar::notify_removed_from_application(Badge<GApplication>)
|
||||
{
|
||||
unrealize_menubar();
|
||||
}
|
25
Libraries/LibGUI/GMenuBar.h
Normal file
25
Libraries/LibGUI/GMenuBar.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/GMenu.h>
|
||||
|
||||
class GApplication;
|
||||
|
||||
class GMenuBar {
|
||||
public:
|
||||
GMenuBar();
|
||||
~GMenuBar();
|
||||
|
||||
void add_menu(OwnPtr<GMenu>&&);
|
||||
|
||||
void notify_added_to_application(Badge<GApplication>);
|
||||
void notify_removed_from_application(Badge<GApplication>);
|
||||
|
||||
private:
|
||||
int realize_menubar();
|
||||
void unrealize_menubar();
|
||||
|
||||
int m_menubar_id { -1 };
|
||||
Vector<OwnPtr<GMenu>> m_menus;
|
||||
};
|
74
Libraries/LibGUI/GMenuItem.cpp
Normal file
74
Libraries/LibGUI/GMenuItem.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GEventLoop.h>
|
||||
#include <LibGUI/GMenuItem.h>
|
||||
#include <WindowServer/WSAPITypes.h>
|
||||
|
||||
GMenuItem::GMenuItem(unsigned menu_id, Type type)
|
||||
: m_type(type)
|
||||
, m_menu_id(menu_id)
|
||||
{
|
||||
}
|
||||
|
||||
GMenuItem::GMenuItem(unsigned menu_id, NonnullRefPtr<GAction>&& action)
|
||||
: m_type(Action)
|
||||
, m_menu_id(menu_id)
|
||||
, m_action(move(action))
|
||||
{
|
||||
m_action->register_menu_item({}, *this);
|
||||
m_enabled = m_action->is_enabled();
|
||||
m_checkable = m_action->is_checkable();
|
||||
if (m_checkable)
|
||||
m_checked = m_action->is_checked();
|
||||
}
|
||||
|
||||
GMenuItem::~GMenuItem()
|
||||
{
|
||||
if (m_action)
|
||||
m_action->unregister_menu_item({}, *this);
|
||||
}
|
||||
|
||||
void GMenuItem::set_enabled(bool enabled)
|
||||
{
|
||||
if (m_enabled == enabled)
|
||||
return;
|
||||
m_enabled = enabled;
|
||||
update_window_server();
|
||||
}
|
||||
|
||||
void GMenuItem::set_checked(bool checked)
|
||||
{
|
||||
ASSERT(is_checkable());
|
||||
if (m_checked == checked)
|
||||
return;
|
||||
m_checked = checked;
|
||||
update_window_server();
|
||||
}
|
||||
|
||||
void GMenuItem::update_window_server()
|
||||
{
|
||||
if (m_menu_id < 0)
|
||||
return;
|
||||
auto& action = *m_action;
|
||||
WSAPI_ClientMessage request;
|
||||
request.type = WSAPI_ClientMessage::Type::UpdateMenuItem;
|
||||
request.menu.menu_id = m_menu_id;
|
||||
request.menu.identifier = m_identifier;
|
||||
request.menu.enabled = action.is_enabled();
|
||||
request.menu.checkable = action.is_checkable();
|
||||
if (action.is_checkable())
|
||||
request.menu.checked = action.is_checked();
|
||||
ASSERT(action.text().length() < (ssize_t)sizeof(request.text));
|
||||
strcpy(request.text, action.text().characters());
|
||||
request.text_length = action.text().length();
|
||||
|
||||
if (action.shortcut().is_valid()) {
|
||||
auto shortcut_text = action.shortcut().to_string();
|
||||
ASSERT(shortcut_text.length() < (ssize_t)sizeof(request.menu.shortcut_text));
|
||||
strcpy(request.menu.shortcut_text, shortcut_text.characters());
|
||||
request.menu.shortcut_text_length = shortcut_text.length();
|
||||
} else {
|
||||
request.menu.shortcut_text_length = 0;
|
||||
}
|
||||
|
||||
GEventLoop::current().sync_request(request, WSAPI_ServerMessage::Type::DidUpdateMenuItem);
|
||||
}
|
49
Libraries/LibGUI/GMenuItem.h
Normal file
49
Libraries/LibGUI/GMenuItem.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Badge.h>
|
||||
|
||||
class GAction;
|
||||
class GMenu;
|
||||
|
||||
class GMenuItem {
|
||||
public:
|
||||
enum Type {
|
||||
Invalid,
|
||||
Action,
|
||||
Separator
|
||||
};
|
||||
|
||||
GMenuItem(unsigned menu_id, Type);
|
||||
GMenuItem(unsigned menu_id, NonnullRefPtr<GAction>&&);
|
||||
~GMenuItem();
|
||||
|
||||
Type type() const { return m_type; }
|
||||
String text() const;
|
||||
const GAction* action() const { return m_action.ptr(); }
|
||||
GAction* action() { return m_action.ptr(); }
|
||||
unsigned identifier() const { return m_identifier; }
|
||||
|
||||
bool is_checkable() const { return m_checkable; }
|
||||
void set_checkable(bool checkable) { m_checkable = checkable; }
|
||||
|
||||
bool is_checked() const { return m_checked; }
|
||||
void set_checked(bool);
|
||||
|
||||
bool is_enabled() const { return m_enabled; }
|
||||
void set_enabled(bool);
|
||||
|
||||
void set_menu_id(Badge<GMenu>, unsigned menu_id) { m_menu_id = menu_id; }
|
||||
void set_identifier(Badge<GMenu>, unsigned identifier) { m_identifier = identifier; }
|
||||
|
||||
private:
|
||||
void update_window_server();
|
||||
|
||||
Type m_type { Invalid };
|
||||
int m_menu_id { -1 };
|
||||
unsigned m_identifier { 0 };
|
||||
bool m_enabled { true };
|
||||
bool m_checkable { false };
|
||||
bool m_checked { false };
|
||||
RefPtr<GAction> m_action;
|
||||
};
|
83
Libraries/LibGUI/GMessageBox.cpp
Normal file
83
Libraries/LibGUI/GMessageBox.cpp
Normal file
|
@ -0,0 +1,83 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GLabel.h>
|
||||
#include <LibGUI/GMessageBox.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void GMessageBox::show(const StringView& text, const StringView& title, Type type, CObject* parent)
|
||||
{
|
||||
GMessageBox box(text, title, type, parent);
|
||||
box.exec();
|
||||
}
|
||||
|
||||
GMessageBox::GMessageBox(const StringView& text, const StringView& title, Type type, CObject* parent)
|
||||
: GDialog(parent)
|
||||
, m_text(text)
|
||||
, m_type(type)
|
||||
{
|
||||
set_title(title);
|
||||
build();
|
||||
}
|
||||
|
||||
GMessageBox::~GMessageBox()
|
||||
{
|
||||
}
|
||||
|
||||
RefPtr<GraphicsBitmap> GMessageBox::icon() const
|
||||
{
|
||||
switch (m_type) {
|
||||
case Type::Information:
|
||||
return GraphicsBitmap::load_from_file("/res/icons/32x32/msgbox-information.png");
|
||||
case Type::Warning:
|
||||
return GraphicsBitmap::load_from_file("/res/icons/32x32/msgbox-warning.png");
|
||||
case Type::Error:
|
||||
return GraphicsBitmap::load_from_file("/res/icons/32x32/msgbox-error.png");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void GMessageBox::build()
|
||||
{
|
||||
auto* widget = new GWidget;
|
||||
set_main_widget(widget);
|
||||
|
||||
int text_width = widget->font().width(m_text);
|
||||
int icon_width = 0;
|
||||
|
||||
widget->set_layout(make<GBoxLayout>(Orientation::Vertical));
|
||||
widget->set_fill_with_background_color(true);
|
||||
|
||||
widget->layout()->set_margins({ 0, 15, 0, 15 });
|
||||
widget->layout()->set_spacing(15);
|
||||
|
||||
GWidget* message_container = widget;
|
||||
if (m_type != Type::None) {
|
||||
message_container = new GWidget(widget);
|
||||
message_container->set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
message_container->layout()->set_margins({ 8, 0, 8, 0 });
|
||||
message_container->layout()->set_spacing(8);
|
||||
|
||||
auto* icon_label = new GLabel(message_container);
|
||||
icon_label->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
icon_label->set_preferred_size({ 32, 32 });
|
||||
icon_label->set_icon(icon());
|
||||
icon_width = icon_label->icon()->width();
|
||||
}
|
||||
|
||||
auto* label = new GLabel(m_text, message_container);
|
||||
label->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
label->set_preferred_size({ text_width, 16 });
|
||||
|
||||
auto* button = new GButton(widget);
|
||||
button->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
button->set_preferred_size({ 100, 20 });
|
||||
button->set_text("OK");
|
||||
button->on_click = [this](auto&) {
|
||||
dbgprintf("GMessageBox: OK button clicked\n");
|
||||
done(0);
|
||||
};
|
||||
|
||||
set_rect(x(), y(), text_width + icon_width + 80, 100);
|
||||
set_resizable(false);
|
||||
}
|
27
Libraries/LibGUI/GMessageBox.h
Normal file
27
Libraries/LibGUI/GMessageBox.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GDialog.h>
|
||||
|
||||
class GMessageBox : public GDialog {
|
||||
public:
|
||||
enum class Type {
|
||||
None,
|
||||
Information,
|
||||
Warning,
|
||||
Error,
|
||||
};
|
||||
|
||||
explicit GMessageBox(const StringView& text, const StringView& title, Type type = Type::None, CObject* parent = nullptr);
|
||||
virtual ~GMessageBox() override;
|
||||
|
||||
static void show(const StringView& text, const StringView& title, Type type = Type::None, CObject* parent = nullptr);
|
||||
|
||||
virtual const char* class_name() const override { return "GMessageBox"; }
|
||||
|
||||
private:
|
||||
void build();
|
||||
RefPtr<GraphicsBitmap> icon() const;
|
||||
|
||||
String m_text;
|
||||
Type m_type { Type::None };
|
||||
};
|
62
Libraries/LibGUI/GModel.cpp
Normal file
62
Libraries/LibGUI/GModel.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include <LibGUI/GAbstractView.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
|
||||
GModel::GModel()
|
||||
{
|
||||
}
|
||||
|
||||
GModel::~GModel()
|
||||
{
|
||||
}
|
||||
|
||||
void GModel::register_view(Badge<GAbstractView>, GAbstractView& view)
|
||||
{
|
||||
m_views.set(&view);
|
||||
}
|
||||
|
||||
void GModel::unregister_view(Badge<GAbstractView>, GAbstractView& view)
|
||||
{
|
||||
m_views.remove(&view);
|
||||
}
|
||||
|
||||
void GModel::for_each_view(Function<void(GAbstractView&)> callback)
|
||||
{
|
||||
for (auto* view : m_views)
|
||||
callback(*view);
|
||||
}
|
||||
|
||||
void GModel::did_update()
|
||||
{
|
||||
if (on_model_update)
|
||||
on_model_update(*this);
|
||||
for_each_view([](auto& view) {
|
||||
view.did_update_model();
|
||||
});
|
||||
}
|
||||
|
||||
void GModel::set_selected_index(const GModelIndex& index)
|
||||
{
|
||||
if (m_selected_index == index)
|
||||
return;
|
||||
m_selected_index = index;
|
||||
if (on_selection_changed)
|
||||
on_selection_changed(index);
|
||||
for_each_view([](auto& view) {
|
||||
view.did_update_selection();
|
||||
});
|
||||
}
|
||||
|
||||
GModelIndex GModel::create_index(int row, int column, void* data) const
|
||||
{
|
||||
return GModelIndex(*this, row, column, data);
|
||||
}
|
||||
|
||||
GModelIndex GModel::sibling(int row, int column, const GModelIndex& parent) const
|
||||
{
|
||||
if (!parent.is_valid())
|
||||
return {};
|
||||
int row_count = this->row_count(parent);
|
||||
if (row < 0 || row > row_count)
|
||||
return {};
|
||||
return index(row, column, parent);
|
||||
}
|
108
Libraries/LibGUI/GModel.h
Normal file
108
Libraries/LibGUI/GModel.h
Normal file
|
@ -0,0 +1,108 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <LibGUI/GModelIndex.h>
|
||||
#include <LibGUI/GVariant.h>
|
||||
#include <SharedGraphics/TextAlignment.h>
|
||||
|
||||
class Font;
|
||||
class GAbstractView;
|
||||
|
||||
enum class GSortOrder {
|
||||
None,
|
||||
Ascending,
|
||||
Descending
|
||||
};
|
||||
|
||||
class GModelNotification {
|
||||
public:
|
||||
enum Type {
|
||||
Invalid = 0,
|
||||
ModelUpdated,
|
||||
};
|
||||
|
||||
explicit GModelNotification(Type type, const GModelIndex& index = GModelIndex())
|
||||
: m_type(type)
|
||||
, m_index(index)
|
||||
{
|
||||
}
|
||||
|
||||
Type type() const { return m_type; }
|
||||
GModelIndex index() const { return m_index; }
|
||||
|
||||
private:
|
||||
Type m_type { Invalid };
|
||||
GModelIndex m_index;
|
||||
};
|
||||
|
||||
class GModel : public RefCounted<GModel> {
|
||||
public:
|
||||
struct ColumnMetadata {
|
||||
int preferred_width { 0 };
|
||||
TextAlignment text_alignment { TextAlignment::CenterLeft };
|
||||
const Font* font { nullptr };
|
||||
};
|
||||
|
||||
enum class Role {
|
||||
Display,
|
||||
Sort,
|
||||
Custom,
|
||||
ForegroundColor,
|
||||
BackgroundColor,
|
||||
Icon
|
||||
};
|
||||
|
||||
virtual ~GModel();
|
||||
|
||||
virtual int row_count(const GModelIndex& = GModelIndex()) const = 0;
|
||||
virtual int column_count(const GModelIndex& = GModelIndex()) const = 0;
|
||||
virtual String row_name(int) const { return {}; }
|
||||
virtual String column_name(int) const { return {}; }
|
||||
virtual ColumnMetadata column_metadata(int) const { return {}; }
|
||||
virtual GVariant data(const GModelIndex&, Role = Role::Display) const = 0;
|
||||
virtual void update() = 0;
|
||||
virtual GModelIndex parent_index(const GModelIndex&) const { return {}; }
|
||||
virtual GModelIndex index(int row, int column = 0, const GModelIndex& = GModelIndex()) const { return create_index(row, column); }
|
||||
virtual GModelIndex sibling(int row, int column, const GModelIndex& parent) const;
|
||||
virtual bool is_editable(const GModelIndex&) const { return false; }
|
||||
virtual void set_data(const GModelIndex&, const GVariant&) {}
|
||||
|
||||
bool is_valid(const GModelIndex& index) const
|
||||
{
|
||||
return index.row() >= 0 && index.row() < row_count() && index.column() >= 0 && index.column() < column_count();
|
||||
}
|
||||
|
||||
void set_selected_index(const GModelIndex&);
|
||||
GModelIndex selected_index() const { return m_selected_index; }
|
||||
|
||||
virtual int key_column() const { return -1; }
|
||||
virtual GSortOrder sort_order() const { return GSortOrder::None; }
|
||||
virtual void set_key_column_and_sort_order(int, GSortOrder) {}
|
||||
|
||||
void register_view(Badge<GAbstractView>, GAbstractView&);
|
||||
void unregister_view(Badge<GAbstractView>, GAbstractView&);
|
||||
|
||||
Function<void(GModel&)> on_model_update;
|
||||
Function<void(const GModelIndex&)> on_selection_changed;
|
||||
|
||||
protected:
|
||||
GModel();
|
||||
|
||||
void for_each_view(Function<void(GAbstractView&)>);
|
||||
void did_update();
|
||||
|
||||
GModelIndex create_index(int row, int column, void* data = nullptr) const;
|
||||
|
||||
private:
|
||||
HashTable<GAbstractView*> m_views;
|
||||
GModelIndex m_selected_index;
|
||||
};
|
||||
|
||||
inline GModelIndex GModelIndex::parent() const
|
||||
{
|
||||
return m_model ? m_model->parent_index(*this) : GModelIndex();
|
||||
}
|
60
Libraries/LibGUI/GModelEditingDelegate.h
Normal file
60
Libraries/LibGUI/GModelEditingDelegate.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <LibGUI/GTextBox.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GModelEditingDelegate {
|
||||
public:
|
||||
GModelEditingDelegate() {}
|
||||
virtual ~GModelEditingDelegate() {}
|
||||
|
||||
void bind(GModel& model, const GModelIndex& index)
|
||||
{
|
||||
if (m_model.ptr() == &model && m_index == index)
|
||||
return;
|
||||
m_model = model;
|
||||
m_index = index;
|
||||
m_widget = create_widget()->make_weak_ptr();
|
||||
}
|
||||
|
||||
GWidget* widget() { return m_widget; }
|
||||
const GWidget* widget() const { return m_widget; }
|
||||
|
||||
Function<void()> on_commit;
|
||||
|
||||
virtual GVariant value() const = 0;
|
||||
virtual void set_value(const GVariant&) = 0;
|
||||
|
||||
virtual void will_begin_editing() { }
|
||||
|
||||
protected:
|
||||
virtual GWidget* create_widget() = 0;
|
||||
void commit()
|
||||
{
|
||||
if (on_commit)
|
||||
on_commit();
|
||||
}
|
||||
|
||||
private:
|
||||
RefPtr<GModel> m_model;
|
||||
GModelIndex m_index;
|
||||
WeakPtr<GWidget> m_widget;
|
||||
};
|
||||
|
||||
class GStringModelEditingDelegate : public GModelEditingDelegate {
|
||||
public:
|
||||
GStringModelEditingDelegate() {}
|
||||
virtual ~GStringModelEditingDelegate() override {}
|
||||
|
||||
virtual GWidget* create_widget() override
|
||||
{
|
||||
auto* textbox = new GTextBox(nullptr);
|
||||
textbox->on_return_pressed = [this] {
|
||||
commit();
|
||||
};
|
||||
return textbox;
|
||||
}
|
||||
virtual GVariant value() const override { return static_cast<const GTextBox*>(widget())->text(); }
|
||||
virtual void set_value(const GVariant& value) override { static_cast<GTextBox*>(widget())->set_text(value.to_string()); }
|
||||
};
|
42
Libraries/LibGUI/GModelIndex.h
Normal file
42
Libraries/LibGUI/GModelIndex.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
class GModel;
|
||||
|
||||
class GModelIndex {
|
||||
friend class GModel;
|
||||
|
||||
public:
|
||||
GModelIndex() {}
|
||||
|
||||
bool is_valid() const { return m_row != -1 && m_column != -1; }
|
||||
int row() const { return m_row; }
|
||||
int column() const { return m_column; }
|
||||
|
||||
void* internal_data() const { return m_internal_data; }
|
||||
|
||||
GModelIndex parent() const;
|
||||
|
||||
bool operator==(const GModelIndex& other) const
|
||||
{
|
||||
return m_model == other.m_model && m_row == other.m_row && m_column == other.m_column && m_internal_data == other.m_internal_data;
|
||||
}
|
||||
|
||||
bool operator!=(const GModelIndex& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
private:
|
||||
GModelIndex(const GModel& model, int row, int column, void* internal_data)
|
||||
: m_model(&model)
|
||||
, m_row(row)
|
||||
, m_column(column)
|
||||
, m_internal_data(internal_data)
|
||||
{
|
||||
}
|
||||
|
||||
const GModel* m_model { nullptr };
|
||||
int m_row { -1 };
|
||||
int m_column { -1 };
|
||||
void* m_internal_data { nullptr };
|
||||
};
|
19
Libraries/LibGUI/GPainter.cpp
Normal file
19
Libraries/LibGUI/GPainter.cpp
Normal file
|
@ -0,0 +1,19 @@
|
|||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
|
||||
GPainter::GPainter(GraphicsBitmap& bitmap)
|
||||
: Painter(bitmap)
|
||||
{
|
||||
}
|
||||
|
||||
GPainter::GPainter(GWidget& widget)
|
||||
: Painter(*widget.window()->back_bitmap())
|
||||
{
|
||||
state().font = &widget.font();
|
||||
auto origin_rect = widget.window_relative_rect();
|
||||
state().translation = origin_rect.location();
|
||||
state().clip_rect = origin_rect;
|
||||
m_clip_origin = origin_rect;
|
||||
state().clip_rect.intersect(m_target->rect());
|
||||
}
|
12
Libraries/LibGUI/GPainter.h
Normal file
12
Libraries/LibGUI/GPainter.h
Normal file
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <SharedGraphics/Painter.h>
|
||||
|
||||
class GWidget;
|
||||
class GraphicsBitmap;
|
||||
|
||||
class GPainter : public Painter {
|
||||
public:
|
||||
explicit GPainter(GWidget&);
|
||||
explicit GPainter(GraphicsBitmap&);
|
||||
};
|
80
Libraries/LibGUI/GProgressBar.cpp
Normal file
80
Libraries/LibGUI/GProgressBar.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include <AK/StringBuilder.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GProgressBar.h>
|
||||
|
||||
GProgressBar::GProgressBar(GWidget* parent)
|
||||
: GFrame(parent)
|
||||
{
|
||||
set_frame_shape(FrameShape::Container);
|
||||
set_frame_shadow(FrameShadow::Sunken);
|
||||
set_frame_thickness(2);
|
||||
}
|
||||
|
||||
GProgressBar::~GProgressBar()
|
||||
{
|
||||
}
|
||||
|
||||
void GProgressBar::set_value(int value)
|
||||
{
|
||||
if (m_value == value)
|
||||
return;
|
||||
m_value = value;
|
||||
update();
|
||||
}
|
||||
|
||||
void GProgressBar::set_range(int min, int max)
|
||||
{
|
||||
ASSERT(min < max);
|
||||
m_min = min;
|
||||
m_max = max;
|
||||
if (m_value > m_max)
|
||||
m_value = m_max;
|
||||
if (m_value < m_min)
|
||||
m_value = m_min;
|
||||
}
|
||||
|
||||
void GProgressBar::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GFrame::paint_event(event);
|
||||
|
||||
GPainter painter(*this);
|
||||
auto rect = frame_inner_rect();
|
||||
painter.add_clip_rect(rect);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
// First we fill the entire widget with the gradient. This incurs a bit of
|
||||
// overdraw but ensures a consistent look throughout the progression.
|
||||
Color start_color(110, 34, 9);
|
||||
Color end_color(244, 202, 158);
|
||||
painter.fill_rect_with_gradient(rect, start_color, end_color);
|
||||
|
||||
float range_size = m_max - m_min;
|
||||
float progress = (m_value - m_min) / range_size;
|
||||
|
||||
String progress_text;
|
||||
if (m_format != Format::NoText) {
|
||||
// Then we draw the progress text over the gradient.
|
||||
// We draw it twice, once offset (1, 1) for a drop shadow look.
|
||||
StringBuilder builder;
|
||||
builder.append(m_caption);
|
||||
if (m_format == Format::Percentage)
|
||||
builder.appendf("%d%%", (int)(progress * 100));
|
||||
else if (m_format == Format::ValueSlashMax)
|
||||
builder.appendf("%d/%d", m_value, m_max);
|
||||
|
||||
progress_text = builder.to_string();
|
||||
|
||||
painter.draw_text(rect.translated(1, 1), progress_text, TextAlignment::Center, Color::Black);
|
||||
painter.draw_text(rect, progress_text, TextAlignment::Center, Color::White);
|
||||
}
|
||||
|
||||
// Then we carve out a hole in the remaining part of the widget.
|
||||
// We draw the text a third time, clipped and inverse, for sharp contrast.
|
||||
float progress_width = progress * width();
|
||||
Rect hole_rect { (int)progress_width, 0, (int)(width() - progress_width), height() };
|
||||
painter.add_clip_rect(hole_rect);
|
||||
painter.fill_rect(hole_rect, Color::White);
|
||||
|
||||
if (m_format != Format::NoText)
|
||||
painter.draw_text(rect.translated(0, 0), progress_text, TextAlignment::Center, Color::Black);
|
||||
}
|
39
Libraries/LibGUI/GProgressBar.h
Normal file
39
Libraries/LibGUI/GProgressBar.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GFrame.h>
|
||||
|
||||
class GProgressBar : public GFrame {
|
||||
public:
|
||||
explicit GProgressBar(GWidget* parent);
|
||||
virtual ~GProgressBar() override;
|
||||
|
||||
void set_range(int min, int max);
|
||||
void set_min(int min) { set_range(min, max()); }
|
||||
void set_max(int max) { set_range(min(), max); }
|
||||
void set_value(int);
|
||||
|
||||
int value() const { return m_value; }
|
||||
int min() const { return m_min; }
|
||||
int max() const { return m_max; }
|
||||
|
||||
String caption() const { return m_caption; }
|
||||
void set_caption(const StringView& caption) { m_caption = caption; }
|
||||
|
||||
enum Format {
|
||||
NoText,
|
||||
Percentage,
|
||||
ValueSlashMax
|
||||
};
|
||||
Format format() const { return m_format; }
|
||||
void set_format(Format format) { m_format = format; }
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
private:
|
||||
Format m_format { Percentage };
|
||||
int m_min { 0 };
|
||||
int m_max { 100 };
|
||||
int m_value { 0 };
|
||||
String m_caption;
|
||||
};
|
73
Libraries/LibGUI/GRadioButton.cpp
Normal file
73
Libraries/LibGUI/GRadioButton.cpp
Normal file
|
@ -0,0 +1,73 @@
|
|||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GRadioButton.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
|
||||
static RefPtr<GraphicsBitmap> s_unfilled_circle_bitmap;
|
||||
static RefPtr<GraphicsBitmap> s_filled_circle_bitmap;
|
||||
static RefPtr<GraphicsBitmap> s_changing_filled_circle_bitmap;
|
||||
static RefPtr<GraphicsBitmap> s_changing_unfilled_circle_bitmap;
|
||||
|
||||
GRadioButton::GRadioButton(const StringView& text, GWidget* parent)
|
||||
: GAbstractButton(text, parent)
|
||||
{
|
||||
if (!s_unfilled_circle_bitmap) {
|
||||
s_unfilled_circle_bitmap = GraphicsBitmap::load_from_file("/res/icons/unfilled-radio-circle.png");
|
||||
s_filled_circle_bitmap = GraphicsBitmap::load_from_file("/res/icons/filled-radio-circle.png");
|
||||
s_changing_filled_circle_bitmap = GraphicsBitmap::load_from_file("/res/icons/changing-filled-radio-circle.png");
|
||||
s_changing_unfilled_circle_bitmap = GraphicsBitmap::load_from_file("/res/icons/changing-unfilled-radio-circle.png");
|
||||
}
|
||||
}
|
||||
|
||||
GRadioButton::~GRadioButton()
|
||||
{
|
||||
}
|
||||
|
||||
Size GRadioButton::circle_size()
|
||||
{
|
||||
return s_unfilled_circle_bitmap->size();
|
||||
}
|
||||
|
||||
static const GraphicsBitmap& circle_bitmap(bool checked, bool changing)
|
||||
{
|
||||
if (changing)
|
||||
return checked ? *s_changing_filled_circle_bitmap : *s_changing_unfilled_circle_bitmap;
|
||||
return checked ? *s_filled_circle_bitmap : *s_unfilled_circle_bitmap;
|
||||
}
|
||||
|
||||
void GRadioButton::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
Rect circle_rect { { 2, 0 }, circle_size() };
|
||||
circle_rect.center_vertically_within(rect());
|
||||
|
||||
auto& bitmap = circle_bitmap(is_checked(), is_being_pressed());
|
||||
painter.blit(circle_rect.location(), bitmap, bitmap.rect());
|
||||
|
||||
Rect text_rect { circle_rect.right() + 4, 0, font().width(text()), font().glyph_height() };
|
||||
text_rect.center_vertically_within(rect());
|
||||
paint_text(painter, text_rect, font(), TextAlignment::TopLeft);
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void GRadioButton::for_each_in_group(Callback callback)
|
||||
{
|
||||
if (!parent())
|
||||
return;
|
||||
parent()->for_each_child_of_type<GRadioButton>([&](auto& child) {
|
||||
return callback(static_cast<GRadioButton&>(child));
|
||||
});
|
||||
}
|
||||
|
||||
void GRadioButton::click()
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
for_each_in_group([this](auto& button) {
|
||||
if (&button != this)
|
||||
button.set_checked(false);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
set_checked(true);
|
||||
}
|
31
Libraries/LibGUI/GRadioButton.h
Normal file
31
Libraries/LibGUI/GRadioButton.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GAbstractButton.h>
|
||||
|
||||
class GRadioButton : public GAbstractButton {
|
||||
public:
|
||||
GRadioButton(const StringView& text, GWidget* parent);
|
||||
virtual ~GRadioButton() override;
|
||||
|
||||
virtual const char* class_name() const override { return "GRadioButton"; }
|
||||
|
||||
virtual void click() override;
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
private:
|
||||
virtual bool is_radio_button() const final { return true; }
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_in_group(Callback);
|
||||
static Size circle_size();
|
||||
};
|
||||
|
||||
template<>
|
||||
inline bool is<GRadioButton>(const CObject& object)
|
||||
{
|
||||
if (!is<GWidget>(object))
|
||||
return false;
|
||||
return to<GWidget>(object).is_radio_button();
|
||||
}
|
46
Libraries/LibGUI/GResizeCorner.cpp
Normal file
46
Libraries/LibGUI/GResizeCorner.cpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GResizeCorner.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
#include <WindowServer/WSAPITypes.h>
|
||||
|
||||
GResizeCorner::GResizeCorner(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
set_preferred_size({ 16, 16 });
|
||||
m_bitmap = GraphicsBitmap::load_from_file("/res/icons/resize-corner.png");
|
||||
ASSERT(m_bitmap);
|
||||
}
|
||||
|
||||
GResizeCorner::~GResizeCorner()
|
||||
{
|
||||
}
|
||||
|
||||
void GResizeCorner::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.fill_rect(rect(), background_color());
|
||||
painter.blit({ 0, 0 }, *m_bitmap, m_bitmap->rect());
|
||||
GWidget::paint_event(event);
|
||||
}
|
||||
|
||||
void GResizeCorner::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() == GMouseButton::Left)
|
||||
window()->start_wm_resize();
|
||||
GWidget::mousedown_event(event);
|
||||
}
|
||||
|
||||
void GResizeCorner::enter_event(CEvent& event)
|
||||
{
|
||||
window()->set_override_cursor(GStandardCursor::ResizeDiagonalTLBR);
|
||||
GWidget::enter_event(event);
|
||||
}
|
||||
|
||||
void GResizeCorner::leave_event(CEvent& event)
|
||||
{
|
||||
window()->set_override_cursor(GStandardCursor::None);
|
||||
GWidget::leave_event(event);
|
||||
}
|
18
Libraries/LibGUI/GResizeCorner.h
Normal file
18
Libraries/LibGUI/GResizeCorner.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GResizeCorner : public GWidget {
|
||||
public:
|
||||
explicit GResizeCorner(GWidget* parent);
|
||||
virtual ~GResizeCorner() override;
|
||||
|
||||
virtual const char* class_name() const override { return "GResizeCorner"; }
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void enter_event(CEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
|
||||
private:
|
||||
RefPtr<GraphicsBitmap> m_bitmap;
|
||||
};
|
336
Libraries/LibGUI/GScrollBar.cpp
Normal file
336
Libraries/LibGUI/GScrollBar.cpp
Normal file
|
@ -0,0 +1,336 @@
|
|||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
#include <SharedGraphics/CharacterBitmap.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
static const char* s_up_arrow_bitmap_data = {
|
||||
" "
|
||||
" # "
|
||||
" ### "
|
||||
" ##### "
|
||||
" ####### "
|
||||
" ### "
|
||||
" ### "
|
||||
" ### "
|
||||
" "
|
||||
};
|
||||
|
||||
static const char* s_down_arrow_bitmap_data = {
|
||||
" "
|
||||
" ### "
|
||||
" ### "
|
||||
" ### "
|
||||
" ####### "
|
||||
" ##### "
|
||||
" ### "
|
||||
" # "
|
||||
" "
|
||||
};
|
||||
|
||||
static const char* s_left_arrow_bitmap_data = {
|
||||
" "
|
||||
" # "
|
||||
" ## "
|
||||
" ###### "
|
||||
" ####### "
|
||||
" ###### "
|
||||
" ## "
|
||||
" # "
|
||||
" "
|
||||
};
|
||||
|
||||
static const char* s_right_arrow_bitmap_data = {
|
||||
" "
|
||||
" # "
|
||||
" ## "
|
||||
" ###### "
|
||||
" ####### "
|
||||
" ###### "
|
||||
" ## "
|
||||
" # "
|
||||
" "
|
||||
};
|
||||
|
||||
static CharacterBitmap* s_up_arrow_bitmap;
|
||||
static CharacterBitmap* s_down_arrow_bitmap;
|
||||
static CharacterBitmap* s_left_arrow_bitmap;
|
||||
static CharacterBitmap* s_right_arrow_bitmap;
|
||||
|
||||
GScrollBar::GScrollBar(Orientation orientation, GWidget* parent)
|
||||
: GWidget(parent)
|
||||
, m_orientation(orientation)
|
||||
{
|
||||
if (!s_up_arrow_bitmap)
|
||||
s_up_arrow_bitmap = &CharacterBitmap::create_from_ascii(s_up_arrow_bitmap_data, 9, 9).leak_ref();
|
||||
if (!s_down_arrow_bitmap)
|
||||
s_down_arrow_bitmap = &CharacterBitmap::create_from_ascii(s_down_arrow_bitmap_data, 9, 9).leak_ref();
|
||||
if (!s_left_arrow_bitmap)
|
||||
s_left_arrow_bitmap = &CharacterBitmap::create_from_ascii(s_left_arrow_bitmap_data, 9, 9).leak_ref();
|
||||
if (!s_right_arrow_bitmap)
|
||||
s_right_arrow_bitmap = &CharacterBitmap::create_from_ascii(s_right_arrow_bitmap_data, 9, 9).leak_ref();
|
||||
|
||||
if (m_orientation == Orientation::Vertical) {
|
||||
set_preferred_size({ 15, 0 });
|
||||
} else {
|
||||
set_preferred_size({ 0, 15 });
|
||||
}
|
||||
|
||||
m_automatic_scrolling_timer.set_interval(100);
|
||||
m_automatic_scrolling_timer.on_timeout = [this] {
|
||||
on_automatic_scrolling_timer_fired();
|
||||
};
|
||||
}
|
||||
|
||||
GScrollBar::~GScrollBar()
|
||||
{
|
||||
}
|
||||
|
||||
void GScrollBar::set_range(int min, int max)
|
||||
{
|
||||
ASSERT(min <= max);
|
||||
if (m_min == min && m_max == max)
|
||||
return;
|
||||
|
||||
m_min = min;
|
||||
m_max = max;
|
||||
|
||||
int old_value = m_value;
|
||||
if (m_value < m_min)
|
||||
m_value = m_min;
|
||||
if (m_value > m_max)
|
||||
m_value = m_max;
|
||||
if (on_change && m_value != old_value)
|
||||
on_change(m_value);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void GScrollBar::set_value(int value)
|
||||
{
|
||||
if (value < m_min)
|
||||
value = m_min;
|
||||
if (value > m_max)
|
||||
value = m_max;
|
||||
if (value == m_value)
|
||||
return;
|
||||
m_value = value;
|
||||
if (on_change)
|
||||
on_change(value);
|
||||
update();
|
||||
}
|
||||
|
||||
Rect GScrollBar::decrement_button_rect() const
|
||||
{
|
||||
return { 0, 0, button_width(), button_height() };
|
||||
}
|
||||
|
||||
Rect GScrollBar::increment_button_rect() const
|
||||
{
|
||||
if (orientation() == Orientation::Vertical)
|
||||
return { 0, height() - button_height(), button_width(), button_height() };
|
||||
else
|
||||
return { width() - button_width(), 0, button_width(), button_height() };
|
||||
}
|
||||
|
||||
Rect GScrollBar::decrement_gutter_rect() const
|
||||
{
|
||||
if (orientation() == Orientation::Vertical)
|
||||
return { 0, button_height(), button_width(), scrubber_rect().top() - button_height() };
|
||||
else
|
||||
return { button_width(), 0, scrubber_rect().x() - button_width(), button_height() };
|
||||
}
|
||||
|
||||
Rect GScrollBar::increment_gutter_rect() const
|
||||
{
|
||||
auto scrubber_rect = this->scrubber_rect();
|
||||
if (orientation() == Orientation::Vertical)
|
||||
return { 0, scrubber_rect.bottom() + 1, button_width(), height() - button_height() - scrubber_rect.bottom() - 1 };
|
||||
else
|
||||
return { scrubber_rect.right() + 1, 0, width() - button_width() - scrubber_rect.right() - 1, button_width() };
|
||||
}
|
||||
|
||||
int GScrollBar::scrubbable_range_in_pixels() const
|
||||
{
|
||||
if (orientation() == Orientation::Vertical)
|
||||
return height() - button_height() * 2 - scrubber_size();
|
||||
else
|
||||
return width() - button_width() * 2 - scrubber_size();
|
||||
}
|
||||
|
||||
bool GScrollBar::has_scrubber() const
|
||||
{
|
||||
return m_max != m_min;
|
||||
}
|
||||
|
||||
int GScrollBar::scrubber_size() const
|
||||
{
|
||||
int pixel_range = length(orientation()) - button_size() * 2;
|
||||
int value_range = m_max - m_min;
|
||||
return ::max(pixel_range - value_range, button_size());
|
||||
}
|
||||
|
||||
Rect GScrollBar::scrubber_rect() const
|
||||
{
|
||||
if (!has_scrubber() || length(orientation()) <= (button_size() * 2) + scrubber_size())
|
||||
return {};
|
||||
float x_or_y;
|
||||
if (m_value == m_min)
|
||||
x_or_y = button_size();
|
||||
else if (m_value == m_max)
|
||||
x_or_y = (length(orientation()) - button_size() - scrubber_size()) + 1;
|
||||
else {
|
||||
float range_size = m_max - m_min;
|
||||
float available = scrubbable_range_in_pixels();
|
||||
float step = available / range_size;
|
||||
x_or_y = (button_size() + (step * m_value));
|
||||
}
|
||||
|
||||
if (orientation() == Orientation::Vertical)
|
||||
return { 0, (int)x_or_y, button_width(), scrubber_size() };
|
||||
else
|
||||
return { (int)x_or_y, 0, scrubber_size(), button_height() };
|
||||
}
|
||||
|
||||
void GScrollBar::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
painter.fill_rect(rect(), Color::from_rgb(0xd6d2ce));
|
||||
|
||||
StylePainter::paint_button(painter, decrement_button_rect(), ButtonStyle::Normal, false, m_hovered_component == Component::DecrementButton);
|
||||
StylePainter::paint_button(painter, increment_button_rect(), ButtonStyle::Normal, false, m_hovered_component == Component::IncrementButton);
|
||||
|
||||
if (length(orientation()) > default_button_size()) {
|
||||
painter.draw_bitmap(decrement_button_rect().location().translated(3, 3), orientation() == Orientation::Vertical ? *s_up_arrow_bitmap : *s_left_arrow_bitmap, has_scrubber() ? Color::Black : Color::MidGray);
|
||||
painter.draw_bitmap(increment_button_rect().location().translated(3, 3), orientation() == Orientation::Vertical ? *s_down_arrow_bitmap : *s_right_arrow_bitmap, has_scrubber() ? Color::Black : Color::MidGray);
|
||||
}
|
||||
|
||||
if (has_scrubber())
|
||||
StylePainter::paint_button(painter, scrubber_rect(), ButtonStyle::Normal, false, m_hovered_component == Component::Scrubber);
|
||||
}
|
||||
|
||||
void GScrollBar::on_automatic_scrolling_timer_fired()
|
||||
{
|
||||
if (m_automatic_scrolling_direction == AutomaticScrollingDirection::Decrement) {
|
||||
set_value(value() - m_step);
|
||||
return;
|
||||
}
|
||||
if (m_automatic_scrolling_direction == AutomaticScrollingDirection::Increment) {
|
||||
set_value(value() + m_step);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GScrollBar::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() != GMouseButton::Left)
|
||||
return;
|
||||
if (decrement_button_rect().contains(event.position())) {
|
||||
m_automatic_scrolling_direction = AutomaticScrollingDirection::Decrement;
|
||||
set_automatic_scrolling_active(true);
|
||||
return;
|
||||
}
|
||||
if (increment_button_rect().contains(event.position())) {
|
||||
m_automatic_scrolling_direction = AutomaticScrollingDirection::Increment;
|
||||
set_automatic_scrolling_active(true);
|
||||
return;
|
||||
}
|
||||
if (has_scrubber() && scrubber_rect().contains(event.position())) {
|
||||
m_scrubbing = true;
|
||||
m_scrub_start_value = value();
|
||||
m_scrub_origin = event.position();
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
if (has_scrubber()) {
|
||||
float range_size = m_max - m_min;
|
||||
float available = scrubbable_range_in_pixels();
|
||||
|
||||
float x = ::max(0, event.position().x() - button_width() - button_width() / 2);
|
||||
float y = ::max(0, event.position().y() - button_height() - button_height() / 2);
|
||||
|
||||
float rel_x = x / available;
|
||||
float rel_y = y / available;
|
||||
|
||||
if (orientation() == Orientation::Vertical)
|
||||
set_value(m_min + rel_y * range_size);
|
||||
else
|
||||
set_value(m_min + rel_x * range_size);
|
||||
|
||||
m_scrubbing = true;
|
||||
m_scrub_start_value = value();
|
||||
m_scrub_origin = event.position();
|
||||
}
|
||||
}
|
||||
|
||||
void GScrollBar::mouseup_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() != GMouseButton::Left)
|
||||
return;
|
||||
m_automatic_scrolling_direction = AutomaticScrollingDirection::None;
|
||||
set_automatic_scrolling_active(false);
|
||||
if (!m_scrubbing)
|
||||
return;
|
||||
m_scrubbing = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void GScrollBar::set_automatic_scrolling_active(bool active)
|
||||
{
|
||||
if (active) {
|
||||
on_automatic_scrolling_timer_fired();
|
||||
m_automatic_scrolling_timer.start();
|
||||
} else {
|
||||
m_automatic_scrolling_timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void GScrollBar::mousemove_event(GMouseEvent& event)
|
||||
{
|
||||
auto old_hovered_component = m_hovered_component;
|
||||
if (scrubber_rect().contains(event.position()))
|
||||
m_hovered_component = Component::Scrubber;
|
||||
else if (decrement_button_rect().contains(event.position()))
|
||||
m_hovered_component = Component::DecrementButton;
|
||||
else if (increment_button_rect().contains(event.position()))
|
||||
m_hovered_component = Component::IncrementButton;
|
||||
else if (rect().contains(event.position()))
|
||||
m_hovered_component = Component::Gutter;
|
||||
else
|
||||
m_hovered_component = Component::Invalid;
|
||||
if (old_hovered_component != m_hovered_component) {
|
||||
update();
|
||||
|
||||
if (m_automatic_scrolling_direction == AutomaticScrollingDirection::Decrement)
|
||||
set_automatic_scrolling_active(m_hovered_component == Component::DecrementButton);
|
||||
else if (m_automatic_scrolling_direction == AutomaticScrollingDirection::Increment)
|
||||
set_automatic_scrolling_active(m_hovered_component == Component::IncrementButton);
|
||||
}
|
||||
if (!m_scrubbing)
|
||||
return;
|
||||
float delta = orientation() == Orientation::Vertical ? (event.y() - m_scrub_origin.y()) : (event.x() - m_scrub_origin.x());
|
||||
float scrubbable_range = scrubbable_range_in_pixels();
|
||||
float value_steps_per_scrubbed_pixel = (m_max - m_min) / scrubbable_range;
|
||||
float new_value = m_scrub_start_value + (value_steps_per_scrubbed_pixel * delta);
|
||||
set_value(new_value);
|
||||
}
|
||||
|
||||
void GScrollBar::leave_event(CEvent&)
|
||||
{
|
||||
if (m_hovered_component != Component::Invalid) {
|
||||
m_hovered_component = Component::Invalid;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void GScrollBar::change_event(GEvent& event)
|
||||
{
|
||||
if (event.type() == GEvent::Type::EnabledChange) {
|
||||
if (!is_enabled())
|
||||
m_scrubbing = false;
|
||||
}
|
||||
return GWidget::change_event(event);
|
||||
}
|
83
Libraries/LibGUI/GScrollBar.h
Normal file
83
Libraries/LibGUI/GScrollBar.h
Normal file
|
@ -0,0 +1,83 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <LibCore/CTimer.h>
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GScrollBar final : public GWidget {
|
||||
public:
|
||||
explicit GScrollBar(Orientation, GWidget* parent);
|
||||
virtual ~GScrollBar() override;
|
||||
|
||||
Orientation orientation() const { return m_orientation; }
|
||||
|
||||
int value() const { return m_value; }
|
||||
int min() const { return m_min; }
|
||||
int max() const { return m_max; }
|
||||
int step() const { return m_step; }
|
||||
int big_step() const { return m_big_step; }
|
||||
|
||||
void set_min(int min) { set_range(min, max()); }
|
||||
void set_max(int max) { set_range(min(), max); }
|
||||
void set_range(int min, int max);
|
||||
void set_value(int value);
|
||||
void set_step(int step) { m_step = step; }
|
||||
void set_big_step(int big_step) { m_big_step = big_step; }
|
||||
bool has_scrubber() const;
|
||||
|
||||
Function<void(int)> on_change;
|
||||
|
||||
virtual const char* class_name() const override { return "GScrollBar"; }
|
||||
|
||||
enum Component {
|
||||
Invalid,
|
||||
DecrementButton,
|
||||
IncrementButton,
|
||||
Gutter,
|
||||
Scrubber,
|
||||
};
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
virtual void change_event(GEvent&) override;
|
||||
|
||||
int default_button_size() const { return 16; }
|
||||
int button_size() const { return length(orientation()) <= (default_button_size() * 2) ? length(orientation()) / 2 : default_button_size(); }
|
||||
int button_width() const { return orientation() == Orientation::Vertical ? width() : button_size(); }
|
||||
int button_height() const { return orientation() == Orientation::Horizontal ? height() : button_size(); }
|
||||
Rect decrement_button_rect() const;
|
||||
Rect increment_button_rect() const;
|
||||
Rect decrement_gutter_rect() const;
|
||||
Rect increment_gutter_rect() const;
|
||||
Rect scrubber_rect() const;
|
||||
int scrubber_size() const;
|
||||
int scrubbable_range_in_pixels() const;
|
||||
void on_automatic_scrolling_timer_fired();
|
||||
void set_automatic_scrolling_active(bool);
|
||||
|
||||
int m_min { 0 };
|
||||
int m_max { 0 };
|
||||
int m_value { 0 };
|
||||
int m_step { 1 };
|
||||
int m_big_step { 5 };
|
||||
|
||||
bool m_scrubbing { false };
|
||||
int m_scrub_start_value { 0 };
|
||||
Point m_scrub_origin;
|
||||
|
||||
Orientation m_orientation { Orientation::Vertical };
|
||||
Component m_hovered_component { Component::Invalid };
|
||||
|
||||
enum class AutomaticScrollingDirection {
|
||||
None = 0,
|
||||
Decrement,
|
||||
Increment,
|
||||
};
|
||||
|
||||
AutomaticScrollingDirection m_automatic_scrolling_direction { AutomaticScrollingDirection::None };
|
||||
CTimer m_automatic_scrolling_timer;
|
||||
};
|
163
Libraries/LibGUI/GScrollableWidget.cpp
Normal file
163
Libraries/LibGUI/GScrollableWidget.cpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#include <LibGUI/GScrollBar.h>
|
||||
#include <LibGUI/GScrollableWidget.h>
|
||||
|
||||
GScrollableWidget::GScrollableWidget(GWidget* parent)
|
||||
: GFrame(parent)
|
||||
{
|
||||
m_vertical_scrollbar = new GScrollBar(Orientation::Vertical, this);
|
||||
m_vertical_scrollbar->set_step(4);
|
||||
m_vertical_scrollbar->on_change = [this](int) {
|
||||
did_scroll();
|
||||
update();
|
||||
};
|
||||
|
||||
m_horizontal_scrollbar = new GScrollBar(Orientation::Horizontal, this);
|
||||
m_horizontal_scrollbar->set_step(4);
|
||||
m_horizontal_scrollbar->set_big_step(30);
|
||||
m_horizontal_scrollbar->on_change = [this](int) {
|
||||
did_scroll();
|
||||
update();
|
||||
};
|
||||
|
||||
m_corner_widget = new GWidget(this);
|
||||
m_corner_widget->set_fill_with_background_color(true);
|
||||
}
|
||||
|
||||
GScrollableWidget::~GScrollableWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void GScrollableWidget::mousewheel_event(GMouseEvent& event)
|
||||
{
|
||||
// FIXME: The wheel delta multiplier should probably come from... somewhere?
|
||||
vertical_scrollbar().set_value(vertical_scrollbar().value() + event.wheel_delta() * 20);
|
||||
}
|
||||
|
||||
void GScrollableWidget::resize_event(GResizeEvent& event)
|
||||
{
|
||||
auto inner_rect = frame_inner_rect_for_size(event.size());
|
||||
update_scrollbar_ranges();
|
||||
|
||||
int height_wanted_by_horizontal_scrollbar = m_horizontal_scrollbar->is_visible() ? m_horizontal_scrollbar->preferred_size().height() : 0;
|
||||
int width_wanted_by_vertical_scrollbar = m_vertical_scrollbar->is_visible() ? m_vertical_scrollbar->preferred_size().width() : 0;
|
||||
|
||||
m_vertical_scrollbar->set_relative_rect(inner_rect.right() + 1 - m_vertical_scrollbar->preferred_size().width(), inner_rect.top(), m_vertical_scrollbar->preferred_size().width(), inner_rect.height() - height_wanted_by_horizontal_scrollbar);
|
||||
m_horizontal_scrollbar->set_relative_rect(inner_rect.left(), inner_rect.bottom() + 1 - m_horizontal_scrollbar->preferred_size().height(), inner_rect.width() - m_vertical_scrollbar->preferred_size().width(), width_wanted_by_vertical_scrollbar);
|
||||
|
||||
m_corner_widget->set_visible(m_vertical_scrollbar->is_visible() && m_horizontal_scrollbar->is_visible());
|
||||
if (m_corner_widget->is_visible()) {
|
||||
Rect corner_rect { m_horizontal_scrollbar->relative_rect().right() + 1, m_vertical_scrollbar->relative_rect().bottom() + 1, width_occupied_by_vertical_scrollbar(), height_occupied_by_horizontal_scrollbar() };
|
||||
m_corner_widget->set_relative_rect(corner_rect);
|
||||
}
|
||||
}
|
||||
|
||||
Size GScrollableWidget::available_size() const
|
||||
{
|
||||
int available_width = frame_inner_rect().width() - m_size_occupied_by_fixed_elements.width() - width_occupied_by_vertical_scrollbar();
|
||||
int available_height = frame_inner_rect().height() - m_size_occupied_by_fixed_elements.height() - height_occupied_by_horizontal_scrollbar();
|
||||
return { available_width, available_height };
|
||||
}
|
||||
|
||||
void GScrollableWidget::update_scrollbar_ranges()
|
||||
{
|
||||
auto available_size = this->available_size();
|
||||
|
||||
int excess_height = max(0, m_content_size.height() - available_size.height());
|
||||
m_vertical_scrollbar->set_range(0, excess_height);
|
||||
|
||||
int excess_width = max(0, m_content_size.width() - available_size.width());
|
||||
m_horizontal_scrollbar->set_range(0, excess_width);
|
||||
|
||||
m_vertical_scrollbar->set_big_step(visible_content_rect().height() - m_vertical_scrollbar->step());
|
||||
}
|
||||
|
||||
void GScrollableWidget::set_content_size(const Size& size)
|
||||
{
|
||||
if (m_content_size == size)
|
||||
return;
|
||||
m_content_size = size;
|
||||
update_scrollbar_ranges();
|
||||
}
|
||||
|
||||
void GScrollableWidget::set_size_occupied_by_fixed_elements(const Size& size)
|
||||
{
|
||||
if (m_size_occupied_by_fixed_elements == size)
|
||||
return;
|
||||
m_size_occupied_by_fixed_elements = size;
|
||||
update_scrollbar_ranges();
|
||||
}
|
||||
|
||||
int GScrollableWidget::height_occupied_by_horizontal_scrollbar() const
|
||||
{
|
||||
return m_horizontal_scrollbar->is_visible() ? m_horizontal_scrollbar->height() : 0;
|
||||
}
|
||||
|
||||
int GScrollableWidget::width_occupied_by_vertical_scrollbar() const
|
||||
{
|
||||
return m_vertical_scrollbar->is_visible() ? m_vertical_scrollbar->width() : 0;
|
||||
}
|
||||
|
||||
Rect GScrollableWidget::visible_content_rect() const
|
||||
{
|
||||
return {
|
||||
m_horizontal_scrollbar->value(),
|
||||
m_vertical_scrollbar->value(),
|
||||
min(m_content_size.width(), frame_inner_rect().width() - width_occupied_by_vertical_scrollbar() - m_size_occupied_by_fixed_elements.width()),
|
||||
min(m_content_size.height(), frame_inner_rect().height() - height_occupied_by_horizontal_scrollbar() - m_size_occupied_by_fixed_elements.height())
|
||||
};
|
||||
}
|
||||
|
||||
void GScrollableWidget::scroll_into_view(const Rect& rect, Orientation orientation)
|
||||
{
|
||||
if (orientation == Orientation::Vertical)
|
||||
return scroll_into_view(rect, false, true);
|
||||
return scroll_into_view(rect, true, false);
|
||||
}
|
||||
|
||||
void GScrollableWidget::scroll_into_view(const Rect& rect, bool scroll_horizontally, bool scroll_vertically)
|
||||
{
|
||||
auto visible_content_rect = this->visible_content_rect();
|
||||
if (visible_content_rect.contains(rect))
|
||||
return;
|
||||
|
||||
if (scroll_vertically) {
|
||||
if (rect.top() < visible_content_rect.top())
|
||||
m_vertical_scrollbar->set_value(rect.top());
|
||||
else if (rect.bottom() > visible_content_rect.bottom())
|
||||
m_vertical_scrollbar->set_value(rect.bottom() - visible_content_rect.height());
|
||||
}
|
||||
if (scroll_horizontally) {
|
||||
if (rect.left() < visible_content_rect.left())
|
||||
m_horizontal_scrollbar->set_value(rect.left());
|
||||
else if (rect.right() > visible_content_rect.right())
|
||||
m_horizontal_scrollbar->set_value(rect.right() - visible_content_rect.width());
|
||||
}
|
||||
}
|
||||
|
||||
void GScrollableWidget::set_scrollbars_enabled(bool scrollbars_enabled)
|
||||
{
|
||||
if (m_scrollbars_enabled == scrollbars_enabled)
|
||||
return;
|
||||
m_scrollbars_enabled = scrollbars_enabled;
|
||||
m_vertical_scrollbar->set_visible(m_scrollbars_enabled);
|
||||
m_horizontal_scrollbar->set_visible(m_scrollbars_enabled);
|
||||
m_corner_widget->set_visible(m_scrollbars_enabled);
|
||||
}
|
||||
|
||||
void GScrollableWidget::scroll_to_top()
|
||||
{
|
||||
scroll_into_view({ 0, 0, 1, 1 }, Orientation::Vertical);
|
||||
}
|
||||
|
||||
void GScrollableWidget::scroll_to_bottom()
|
||||
{
|
||||
scroll_into_view({ 0, content_height(), 1, 1 }, Orientation::Vertical);
|
||||
}
|
||||
|
||||
Rect GScrollableWidget::widget_inner_rect() const
|
||||
{
|
||||
auto rect = frame_inner_rect();
|
||||
rect.set_width(rect.width() - width_occupied_by_vertical_scrollbar());
|
||||
rect.set_height(rect.height() - height_occupied_by_horizontal_scrollbar());
|
||||
return rect;
|
||||
}
|
60
Libraries/LibGUI/GScrollableWidget.h
Normal file
60
Libraries/LibGUI/GScrollableWidget.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GFrame.h>
|
||||
|
||||
class GScrollBar;
|
||||
|
||||
class GScrollableWidget : public GFrame {
|
||||
public:
|
||||
virtual ~GScrollableWidget() override;
|
||||
|
||||
Size content_size() const { return m_content_size; }
|
||||
int content_width() const { return m_content_size.width(); }
|
||||
int content_height() const { return m_content_size.height(); }
|
||||
|
||||
Rect visible_content_rect() const;
|
||||
|
||||
Rect widget_inner_rect() const;
|
||||
|
||||
void scroll_into_view(const Rect&, Orientation);
|
||||
void scroll_into_view(const Rect&, bool scroll_horizontally, bool scroll_vertically);
|
||||
|
||||
void set_scrollbars_enabled(bool);
|
||||
bool is_scrollbars_enabled() const { return m_scrollbars_enabled; }
|
||||
|
||||
Size available_size() const;
|
||||
|
||||
GScrollBar& vertical_scrollbar() { return *m_vertical_scrollbar; }
|
||||
const GScrollBar& vertical_scrollbar() const { return *m_vertical_scrollbar; }
|
||||
GScrollBar& horizontal_scrollbar() { return *m_horizontal_scrollbar; }
|
||||
const GScrollBar& horizontal_scrollbar() const { return *m_horizontal_scrollbar; }
|
||||
GWidget& corner_widget() { return *m_corner_widget; }
|
||||
const GWidget& corner_widget() const { return *m_corner_widget; }
|
||||
|
||||
void scroll_to_top();
|
||||
void scroll_to_bottom();
|
||||
|
||||
int width_occupied_by_vertical_scrollbar() const;
|
||||
int height_occupied_by_horizontal_scrollbar() const;
|
||||
|
||||
virtual const char* class_name() const override { return "GScrollableWidget"; }
|
||||
|
||||
protected:
|
||||
explicit GScrollableWidget(GWidget* parent);
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
virtual void mousewheel_event(GMouseEvent&) override;
|
||||
virtual void did_scroll() {}
|
||||
void set_content_size(const Size&);
|
||||
void set_size_occupied_by_fixed_elements(const Size&);
|
||||
|
||||
|
||||
private:
|
||||
void update_scrollbar_ranges();
|
||||
|
||||
GScrollBar* m_vertical_scrollbar { nullptr };
|
||||
GScrollBar* m_horizontal_scrollbar { nullptr };
|
||||
GWidget* m_corner_widget { nullptr };
|
||||
Size m_content_size;
|
||||
Size m_size_occupied_by_fixed_elements;
|
||||
bool m_scrollbars_enabled { true };
|
||||
};
|
243
Libraries/LibGUI/GShortcut.cpp
Normal file
243
Libraries/LibGUI/GShortcut.cpp
Normal file
|
@ -0,0 +1,243 @@
|
|||
#include <AK/StringBuilder.h>
|
||||
#include <LibGUI/GShortcut.h>
|
||||
|
||||
static String to_string(KeyCode key)
|
||||
{
|
||||
switch (key) {
|
||||
case Key_Escape:
|
||||
return "Escape";
|
||||
case Key_Tab:
|
||||
return "Tab";
|
||||
case Key_Backspace:
|
||||
return "Backspace";
|
||||
case Key_Return:
|
||||
return "Return";
|
||||
case Key_Insert:
|
||||
return "Insert";
|
||||
case Key_Delete:
|
||||
return "Delete";
|
||||
case Key_PrintScreen:
|
||||
return "PrintScreen";
|
||||
case Key_SysRq:
|
||||
return "SysRq";
|
||||
case Key_Home:
|
||||
return "Home";
|
||||
case Key_End:
|
||||
return "End";
|
||||
case Key_Left:
|
||||
return "Left";
|
||||
case Key_Up:
|
||||
return "Up";
|
||||
case Key_Right:
|
||||
return "Right";
|
||||
case Key_Down:
|
||||
return "Down";
|
||||
case Key_PageUp:
|
||||
return "PageUp";
|
||||
case Key_PageDown:
|
||||
return "PageDown";
|
||||
case Key_Shift:
|
||||
return "Shift";
|
||||
case Key_Control:
|
||||
return "Control";
|
||||
case Key_Alt:
|
||||
return "Alt";
|
||||
case Key_CapsLock:
|
||||
return "CapsLock";
|
||||
case Key_NumLock:
|
||||
return "NumLock";
|
||||
case Key_ScrollLock:
|
||||
return "ScrollLock";
|
||||
case Key_F1:
|
||||
return "F1";
|
||||
case Key_F2:
|
||||
return "F2";
|
||||
case Key_F3:
|
||||
return "F3";
|
||||
case Key_F4:
|
||||
return "F4";
|
||||
case Key_F5:
|
||||
return "F5";
|
||||
case Key_F6:
|
||||
return "F6";
|
||||
case Key_F7:
|
||||
return "F7";
|
||||
case Key_F8:
|
||||
return "F8";
|
||||
case Key_F9:
|
||||
return "F9";
|
||||
case Key_F10:
|
||||
return "F10";
|
||||
case Key_F11:
|
||||
return "F11";
|
||||
case Key_F12:
|
||||
return "F12";
|
||||
case Key_Space:
|
||||
return "Space";
|
||||
case Key_ExclamationPoint:
|
||||
return "!";
|
||||
case Key_DoubleQuote:
|
||||
return "\"";
|
||||
case Key_Hashtag:
|
||||
return "#";
|
||||
case Key_Dollar:
|
||||
return "$";
|
||||
case Key_Percent:
|
||||
return "%";
|
||||
case Key_Ampersand:
|
||||
return "&";
|
||||
case Key_Apostrophe:
|
||||
return "'";
|
||||
case Key_LeftParen:
|
||||
return "(";
|
||||
case Key_RightParen:
|
||||
return ")";
|
||||
case Key_Asterisk:
|
||||
return "*";
|
||||
case Key_Plus:
|
||||
return "+";
|
||||
case Key_Comma:
|
||||
return ",";
|
||||
case Key_Minus:
|
||||
return "-";
|
||||
case Key_Period:
|
||||
return ",";
|
||||
case Key_Slash:
|
||||
return "/";
|
||||
case Key_0:
|
||||
return "0";
|
||||
case Key_1:
|
||||
return "1";
|
||||
case Key_2:
|
||||
return "2";
|
||||
case Key_3:
|
||||
return "3";
|
||||
case Key_4:
|
||||
return "4";
|
||||
case Key_5:
|
||||
return "5";
|
||||
case Key_6:
|
||||
return "6";
|
||||
case Key_7:
|
||||
return "7";
|
||||
case Key_8:
|
||||
return "8";
|
||||
case Key_9:
|
||||
return "9";
|
||||
case Key_Colon:
|
||||
return ":";
|
||||
case Key_Semicolon:
|
||||
return ";";
|
||||
case Key_LessThan:
|
||||
return "<";
|
||||
case Key_Equal:
|
||||
return "=";
|
||||
case Key_GreaterThan:
|
||||
return ">";
|
||||
case Key_QuestionMark:
|
||||
return "?";
|
||||
case Key_AtSign:
|
||||
return "@";
|
||||
case Key_A:
|
||||
return "A";
|
||||
case Key_B:
|
||||
return "B";
|
||||
case Key_C:
|
||||
return "C";
|
||||
case Key_D:
|
||||
return "D";
|
||||
case Key_E:
|
||||
return "E";
|
||||
case Key_F:
|
||||
return "F";
|
||||
case Key_G:
|
||||
return "G";
|
||||
case Key_H:
|
||||
return "H";
|
||||
case Key_I:
|
||||
return "I";
|
||||
case Key_J:
|
||||
return "J";
|
||||
case Key_K:
|
||||
return "K";
|
||||
case Key_L:
|
||||
return "L";
|
||||
case Key_M:
|
||||
return "M";
|
||||
case Key_N:
|
||||
return "N";
|
||||
case Key_O:
|
||||
return "O";
|
||||
case Key_P:
|
||||
return "P";
|
||||
case Key_Q:
|
||||
return "Q";
|
||||
case Key_R:
|
||||
return "R";
|
||||
case Key_S:
|
||||
return "S";
|
||||
case Key_T:
|
||||
return "T";
|
||||
case Key_U:
|
||||
return "U";
|
||||
case Key_V:
|
||||
return "V";
|
||||
case Key_W:
|
||||
return "W";
|
||||
case Key_X:
|
||||
return "X";
|
||||
case Key_Y:
|
||||
return "Y";
|
||||
case Key_Z:
|
||||
return "Z";
|
||||
case Key_LeftBracket:
|
||||
return "[";
|
||||
case Key_RightBracket:
|
||||
return "]";
|
||||
case Key_Backslash:
|
||||
return "\\";
|
||||
case Key_Circumflex:
|
||||
return "^";
|
||||
case Key_Underscore:
|
||||
return "_";
|
||||
case Key_LeftBrace:
|
||||
return "{";
|
||||
case Key_RightBrace:
|
||||
return "}";
|
||||
case Key_Pipe:
|
||||
return "|";
|
||||
case Key_Tilde:
|
||||
return "~";
|
||||
case Key_Backtick:
|
||||
return "`";
|
||||
|
||||
case Key_Invalid:
|
||||
return "Invalid";
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
String GShortcut::to_string() const
|
||||
{
|
||||
Vector<String, 8> parts;
|
||||
|
||||
if (m_modifiers & Mod_Ctrl)
|
||||
parts.append("Ctrl");
|
||||
if (m_modifiers & Mod_Shift)
|
||||
parts.append("Shift");
|
||||
if (m_modifiers & Mod_Alt)
|
||||
parts.append("Alt");
|
||||
if (m_modifiers & Mod_Logo)
|
||||
parts.append("Logo");
|
||||
|
||||
parts.append(::to_string(m_key));
|
||||
|
||||
StringBuilder builder;
|
||||
for (int i = 0; i < parts.size(); ++i) {
|
||||
builder.append(parts[i]);
|
||||
if (i != parts.size() - 1)
|
||||
builder.append('+');
|
||||
}
|
||||
return builder.to_string();
|
||||
}
|
42
Libraries/LibGUI/GShortcut.h
Normal file
42
Libraries/LibGUI/GShortcut.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/Traits.h>
|
||||
#include <Kernel/KeyCode.h>
|
||||
|
||||
class GShortcut {
|
||||
public:
|
||||
GShortcut() {}
|
||||
GShortcut(u8 modifiers, KeyCode key)
|
||||
: m_modifiers(modifiers)
|
||||
, m_key(key)
|
||||
{
|
||||
}
|
||||
|
||||
bool is_valid() const { return m_key != KeyCode::Key_Invalid; }
|
||||
u8 modifiers() const { return m_modifiers; }
|
||||
KeyCode key() const { return m_key; }
|
||||
String to_string() const;
|
||||
|
||||
bool operator==(const GShortcut& other) const
|
||||
{
|
||||
return m_modifiers == other.m_modifiers
|
||||
&& m_key == other.m_key;
|
||||
}
|
||||
|
||||
private:
|
||||
u8 m_modifiers { 0 };
|
||||
KeyCode m_key { KeyCode::Key_Invalid };
|
||||
};
|
||||
|
||||
namespace AK {
|
||||
|
||||
template<>
|
||||
struct Traits<GShortcut> : public GenericTraits<GShortcut> {
|
||||
static unsigned hash(const GShortcut& shortcut)
|
||||
{
|
||||
return pair_int_hash(shortcut.modifiers(), shortcut.key());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
138
Libraries/LibGUI/GSlider.cpp
Normal file
138
Libraries/LibGUI/GSlider.cpp
Normal file
|
@ -0,0 +1,138 @@
|
|||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GSlider.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
GSlider::GSlider(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GSlider::~GSlider()
|
||||
{
|
||||
}
|
||||
|
||||
void GSlider::set_range(int min, int max)
|
||||
{
|
||||
ASSERT(min <= max);
|
||||
if (m_min == min && m_max == max)
|
||||
return;
|
||||
m_min = min;
|
||||
m_max = max;
|
||||
|
||||
if (m_value > max)
|
||||
m_value = max;
|
||||
if (m_value < min)
|
||||
m_value = min;
|
||||
update();
|
||||
}
|
||||
|
||||
void GSlider::set_value(int value)
|
||||
{
|
||||
if (value > m_max)
|
||||
value = m_max;
|
||||
if (value < m_min)
|
||||
value = m_min;
|
||||
if (m_value == value)
|
||||
return;
|
||||
m_value = value;
|
||||
update();
|
||||
|
||||
if (on_value_changed)
|
||||
on_value_changed(m_value);
|
||||
}
|
||||
|
||||
void GSlider::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
Rect track_rect { inner_rect().x(), 0, inner_rect().width(), track_height() };
|
||||
track_rect.center_vertically_within(inner_rect());
|
||||
|
||||
StylePainter::paint_frame(painter, track_rect, FrameShape::Panel, FrameShadow::Sunken, 1);
|
||||
StylePainter::paint_button(painter, knob_rect(), ButtonStyle::Normal, false, m_knob_hovered);
|
||||
}
|
||||
|
||||
Rect GSlider::knob_rect() const
|
||||
{
|
||||
auto inner_rect = this->inner_rect();
|
||||
float range_size = m_max - m_min;
|
||||
float adjusted_value = m_value - m_min;
|
||||
float relative_value = adjusted_value / range_size;
|
||||
Rect rect {
|
||||
inner_rect.x() + (int)(relative_value * inner_rect.width()) - knob_width() / 2,
|
||||
0,
|
||||
knob_width(),
|
||||
knob_height()
|
||||
};
|
||||
rect.center_vertically_within(inner_rect);
|
||||
return rect;
|
||||
}
|
||||
|
||||
void GSlider::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
if (knob_rect().contains(event.position())) {
|
||||
m_dragging = true;
|
||||
m_drag_origin = event.position();
|
||||
m_drag_origin_value = m_value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
return GWidget::mousedown_event(event);
|
||||
}
|
||||
|
||||
void GSlider::mousemove_event(GMouseEvent& event)
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
set_knob_hovered(knob_rect().contains(event.position()));
|
||||
if (m_dragging) {
|
||||
float delta = event.position().x() - m_drag_origin.x();
|
||||
float scrubbable_range = inner_rect().width();
|
||||
float value_steps_per_scrubbed_pixel = (m_max - m_min) / scrubbable_range;
|
||||
float new_value = m_drag_origin_value + (value_steps_per_scrubbed_pixel * delta);
|
||||
set_value((int)new_value);
|
||||
return;
|
||||
}
|
||||
return GWidget::mousemove_event(event);
|
||||
}
|
||||
|
||||
void GSlider::mouseup_event(GMouseEvent& event)
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
m_dragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
return GWidget::mouseup_event(event);
|
||||
}
|
||||
|
||||
void GSlider::leave_event(CEvent& event)
|
||||
{
|
||||
if (!is_enabled())
|
||||
return;
|
||||
set_knob_hovered(false);
|
||||
GWidget::leave_event(event);
|
||||
}
|
||||
|
||||
void GSlider::change_event(GEvent& event)
|
||||
{
|
||||
if (event.type() == GEvent::Type::EnabledChange) {
|
||||
if (!is_enabled())
|
||||
m_dragging = false;
|
||||
}
|
||||
GWidget::change_event(event);
|
||||
}
|
||||
|
||||
void GSlider::set_knob_hovered(bool hovered)
|
||||
{
|
||||
if (m_knob_hovered == hovered)
|
||||
return;
|
||||
m_knob_hovered = hovered;
|
||||
update(knob_rect());
|
||||
}
|
48
Libraries/LibGUI/GSlider.h
Normal file
48
Libraries/LibGUI/GSlider.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GSlider : public GWidget {
|
||||
public:
|
||||
explicit GSlider(GWidget*);
|
||||
virtual ~GSlider() override;
|
||||
|
||||
int value() const { return m_value; }
|
||||
int min() const { return m_min; }
|
||||
int max() const { return m_max; }
|
||||
|
||||
void set_range(int min, int max);
|
||||
void set_value(int);
|
||||
|
||||
void set_min(int min) { set_range(min, max()); }
|
||||
void set_max(int max) { set_range(min(), max); }
|
||||
|
||||
int track_height() const { return 2; }
|
||||
int knob_width() const { return 8; }
|
||||
int knob_height() const { return 20; }
|
||||
|
||||
Rect knob_rect() const;
|
||||
Rect inner_rect() const { return rect().shrunken(20, 0); }
|
||||
|
||||
Function<void(int)> on_value_changed;
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
virtual void change_event(GEvent&) override;
|
||||
|
||||
private:
|
||||
void set_knob_hovered(bool);
|
||||
|
||||
int m_value { 0 };
|
||||
int m_min { 0 };
|
||||
int m_max { 100 };
|
||||
|
||||
bool m_knob_hovered { false };
|
||||
bool m_dragging { false };
|
||||
int m_drag_origin_value { 0 };
|
||||
Point m_drag_origin;
|
||||
};
|
105
Libraries/LibGUI/GSortingProxyModel.cpp
Normal file
105
Libraries/LibGUI/GSortingProxyModel.cpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
#include <AK/QuickSort.h>
|
||||
#include <LibGUI/GSortingProxyModel.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
GSortingProxyModel::GSortingProxyModel(NonnullRefPtr<GModel>&& target)
|
||||
: m_target(move(target))
|
||||
, m_key_column(-1)
|
||||
{
|
||||
m_target->on_model_update = [this](GModel&) {
|
||||
resort();
|
||||
};
|
||||
}
|
||||
|
||||
GSortingProxyModel::~GSortingProxyModel()
|
||||
{
|
||||
}
|
||||
|
||||
int GSortingProxyModel::row_count(const GModelIndex& index) const
|
||||
{
|
||||
return target().row_count(index);
|
||||
}
|
||||
|
||||
int GSortingProxyModel::column_count(const GModelIndex& index) const
|
||||
{
|
||||
return target().column_count(index);
|
||||
}
|
||||
|
||||
GModelIndex GSortingProxyModel::map_to_target(const GModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
if (index.row() >= m_row_mappings.size() || index.column() >= column_count())
|
||||
return {};
|
||||
return target().index(m_row_mappings[index.row()], index.column());
|
||||
}
|
||||
|
||||
String GSortingProxyModel::row_name(int index) const
|
||||
{
|
||||
return target().row_name(index);
|
||||
}
|
||||
|
||||
String GSortingProxyModel::column_name(int index) const
|
||||
{
|
||||
return target().column_name(index);
|
||||
}
|
||||
|
||||
GModel::ColumnMetadata GSortingProxyModel::column_metadata(int index) const
|
||||
{
|
||||
return target().column_metadata(index);
|
||||
}
|
||||
|
||||
GVariant GSortingProxyModel::data(const GModelIndex& index, Role role) const
|
||||
{
|
||||
return target().data(map_to_target(index), role);
|
||||
}
|
||||
|
||||
void GSortingProxyModel::update()
|
||||
{
|
||||
target().update();
|
||||
}
|
||||
|
||||
void GSortingProxyModel::set_key_column_and_sort_order(int column, GSortOrder sort_order)
|
||||
{
|
||||
if (column == m_key_column && sort_order == m_sort_order)
|
||||
return;
|
||||
|
||||
ASSERT(column >= 0 && column < column_count());
|
||||
m_key_column = column;
|
||||
m_sort_order = sort_order;
|
||||
resort();
|
||||
}
|
||||
|
||||
void GSortingProxyModel::resort()
|
||||
{
|
||||
int previously_selected_target_row = map_to_target(selected_index()).row();
|
||||
int row_count = target().row_count();
|
||||
m_row_mappings.resize(row_count);
|
||||
for (int i = 0; i < row_count; ++i)
|
||||
m_row_mappings[i] = i;
|
||||
if (m_key_column == -1) {
|
||||
did_update();
|
||||
return;
|
||||
}
|
||||
quick_sort(m_row_mappings.begin(), m_row_mappings.end(), [&](auto row1, auto row2) -> bool {
|
||||
auto data1 = target().data(target().index(row1, m_key_column), GModel::Role::Sort);
|
||||
auto data2 = target().data(target().index(row2, m_key_column), GModel::Role::Sort);
|
||||
if (data1 == data2)
|
||||
return 0;
|
||||
bool is_less_than = data1 < data2;
|
||||
return m_sort_order == GSortOrder::Ascending ? is_less_than : !is_less_than;
|
||||
});
|
||||
if (previously_selected_target_row != -1) {
|
||||
// Preserve selection.
|
||||
ASSERT(m_row_mappings.size() == row_count);
|
||||
for (int i = 0; i < row_count; ++i) {
|
||||
if (m_row_mappings[i] == previously_selected_target_row) {
|
||||
set_selected_index(index(i, 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
did_update();
|
||||
}
|
36
Libraries/LibGUI/GSortingProxyModel.h
Normal file
36
Libraries/LibGUI/GSortingProxyModel.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GModel.h>
|
||||
|
||||
class GSortingProxyModel final : public GModel {
|
||||
public:
|
||||
static NonnullRefPtr<GSortingProxyModel> create(NonnullRefPtr<GModel>&& model) { return adopt(*new GSortingProxyModel(move(model))); }
|
||||
virtual ~GSortingProxyModel() override;
|
||||
|
||||
virtual int row_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual int column_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual String row_name(int) const override;
|
||||
virtual String column_name(int) const override;
|
||||
virtual ColumnMetadata column_metadata(int) const override;
|
||||
virtual GVariant data(const GModelIndex&, Role = Role::Display) const override;
|
||||
virtual void update() override;
|
||||
|
||||
virtual int key_column() const override { return m_key_column; }
|
||||
virtual GSortOrder sort_order() const override { return m_sort_order; }
|
||||
virtual void set_key_column_and_sort_order(int, GSortOrder) override;
|
||||
|
||||
GModelIndex map_to_target(const GModelIndex&) const;
|
||||
|
||||
private:
|
||||
explicit GSortingProxyModel(NonnullRefPtr<GModel>&&);
|
||||
|
||||
GModel& target() { return *m_target; }
|
||||
const GModel& target() const { return *m_target; }
|
||||
|
||||
void resort();
|
||||
|
||||
NonnullRefPtr<GModel> m_target;
|
||||
Vector<int> m_row_mappings;
|
||||
int m_key_column { -1 };
|
||||
GSortOrder m_sort_order { GSortOrder::Ascending };
|
||||
};
|
75
Libraries/LibGUI/GSpinBox.cpp
Normal file
75
Libraries/LibGUI/GSpinBox.cpp
Normal file
|
@ -0,0 +1,75 @@
|
|||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GSpinBox.h>
|
||||
#include <LibGUI/GTextEditor.h>
|
||||
|
||||
GSpinBox::GSpinBox(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
m_editor = new GTextEditor(GTextEditor::Type::SingleLine, this);
|
||||
m_editor->set_text("0");
|
||||
m_editor->on_change = [this] {
|
||||
bool ok;
|
||||
int value = m_editor->text().to_uint(ok);
|
||||
if (ok)
|
||||
set_value(value);
|
||||
else
|
||||
m_editor->set_text(String::number(m_value));
|
||||
};
|
||||
m_increment_button = new GButton(this);
|
||||
m_increment_button->set_focusable(false);
|
||||
m_increment_button->set_text("\xf6");
|
||||
m_increment_button->on_click = [this](GButton&) { set_value(m_value + 1); };
|
||||
m_decrement_button = new GButton(this);
|
||||
m_decrement_button->set_focusable(false);
|
||||
m_decrement_button->set_text("\xf7");
|
||||
m_decrement_button->on_click = [this](GButton&) { set_value(m_value - 1); };
|
||||
}
|
||||
|
||||
GSpinBox::~GSpinBox()
|
||||
{
|
||||
}
|
||||
|
||||
void GSpinBox::set_value(int value)
|
||||
{
|
||||
if (value < m_min)
|
||||
value = m_min;
|
||||
if (value > m_max)
|
||||
value = m_max;
|
||||
if (m_value == value)
|
||||
return;
|
||||
m_value = value;
|
||||
m_editor->set_text(String::number(value));
|
||||
update();
|
||||
if (on_change)
|
||||
on_change(value);
|
||||
}
|
||||
|
||||
void GSpinBox::set_range(int min, int max)
|
||||
{
|
||||
ASSERT(min <= max);
|
||||
if (m_min == min && m_max == max)
|
||||
return;
|
||||
|
||||
m_min = min;
|
||||
m_max = max;
|
||||
|
||||
int old_value = m_value;
|
||||
if (m_value < m_min)
|
||||
m_value = m_min;
|
||||
if (m_value > m_max)
|
||||
m_value = m_max;
|
||||
if (on_change && m_value != old_value)
|
||||
on_change(m_value);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void GSpinBox::resize_event(GResizeEvent& event)
|
||||
{
|
||||
int frame_thickness = m_editor->frame_thickness();
|
||||
int button_height = (event.size().height() / 2) - frame_thickness;
|
||||
int button_width = 15;
|
||||
m_increment_button->set_relative_rect(width() - button_width - frame_thickness, frame_thickness, button_width, button_height);
|
||||
m_decrement_button->set_relative_rect(width() - button_width - frame_thickness, frame_thickness + button_height, button_width, button_height);
|
||||
m_editor->set_relative_rect(0, 0, width(), height());
|
||||
}
|
37
Libraries/LibGUI/GSpinBox.h
Normal file
37
Libraries/LibGUI/GSpinBox.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GButton;
|
||||
class GTextEditor;
|
||||
|
||||
class GSpinBox : public GWidget {
|
||||
public:
|
||||
GSpinBox(GWidget* parent = nullptr);
|
||||
virtual ~GSpinBox() override;
|
||||
|
||||
int value() const { return m_value; }
|
||||
void set_value(int);
|
||||
|
||||
int min() const { return m_min; }
|
||||
int max() const { return m_max; }
|
||||
void set_min(int min) { set_range(min, max()); }
|
||||
void set_max(int max) { set_range(min(), max); }
|
||||
void set_range(int min, int max);
|
||||
|
||||
Function<void(int value)> on_change;
|
||||
|
||||
virtual const char* class_name() const override { return "GSpinBox"; }
|
||||
|
||||
protected:
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
|
||||
private:
|
||||
GTextEditor* m_editor { nullptr };
|
||||
GButton* m_increment_button { nullptr };
|
||||
GButton* m_decrement_button { nullptr };
|
||||
|
||||
int m_min { 0 };
|
||||
int m_max { 100 };
|
||||
int m_value { 0 };
|
||||
};
|
116
Libraries/LibGUI/GSplitter.cpp
Normal file
116
Libraries/LibGUI/GSplitter.cpp
Normal file
|
@ -0,0 +1,116 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GSplitter.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
|
||||
GSplitter::GSplitter(Orientation orientation, GWidget* parent)
|
||||
: GFrame(parent)
|
||||
, m_orientation(orientation)
|
||||
{
|
||||
set_layout(make<GBoxLayout>(orientation));
|
||||
set_fill_with_background_color(true);
|
||||
set_background_color(Color::WarmGray);
|
||||
layout()->set_spacing(4);
|
||||
}
|
||||
|
||||
GSplitter::~GSplitter()
|
||||
{
|
||||
}
|
||||
|
||||
void GSplitter::enter_event(CEvent&)
|
||||
{
|
||||
set_background_color(Color::from_rgb(0xd6d2ce));
|
||||
window()->set_override_cursor(m_orientation == Orientation::Horizontal ? GStandardCursor::ResizeHorizontal : GStandardCursor::ResizeVertical);
|
||||
update();
|
||||
}
|
||||
|
||||
void GSplitter::leave_event(CEvent&)
|
||||
{
|
||||
set_background_color(Color::WarmGray);
|
||||
if (!m_resizing)
|
||||
window()->set_override_cursor(GStandardCursor::None);
|
||||
update();
|
||||
}
|
||||
|
||||
void GSplitter::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() != GMouseButton::Left)
|
||||
return;
|
||||
m_resizing = true;
|
||||
int x_or_y = m_orientation == Orientation::Horizontal ? event.x() : event.y();
|
||||
GWidget* first_resizee { nullptr };
|
||||
GWidget* second_resizee { nullptr };
|
||||
int fudge = layout()->spacing();
|
||||
for_each_child_widget([&](auto& child) {
|
||||
int child_start = m_orientation == Orientation::Horizontal ? child.relative_rect().left() : child.relative_rect().top();
|
||||
int child_end = m_orientation == Orientation::Horizontal ? child.relative_rect().right() : child.relative_rect().bottom();
|
||||
if (x_or_y > child_end && (x_or_y - fudge) <= child_end)
|
||||
first_resizee = &child;
|
||||
if (x_or_y < child_start && (x_or_y + fudge) >= child_start)
|
||||
second_resizee = &child;
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
ASSERT(first_resizee && second_resizee);
|
||||
m_first_resizee = first_resizee->make_weak_ptr();
|
||||
m_second_resizee = second_resizee->make_weak_ptr();
|
||||
m_first_resizee_start_size = first_resizee->size();
|
||||
m_second_resizee_start_size = second_resizee->size();
|
||||
m_resize_origin = event.position();
|
||||
}
|
||||
|
||||
void GSplitter::mousemove_event(GMouseEvent& event)
|
||||
{
|
||||
if (!m_resizing)
|
||||
return;
|
||||
auto delta = event.position() - m_resize_origin;
|
||||
if (!m_first_resizee || !m_second_resizee) {
|
||||
// One or both of the resizees were deleted during an ongoing resize, screw this.
|
||||
m_resizing = false;
|
||||
return;
|
||||
;
|
||||
}
|
||||
int minimum_size = 0;
|
||||
auto new_first_resizee_size = m_first_resizee_start_size;
|
||||
auto new_second_resizee_size = m_second_resizee_start_size;
|
||||
if (m_orientation == Orientation::Horizontal) {
|
||||
new_first_resizee_size.set_width(new_first_resizee_size.width() + delta.x());
|
||||
new_second_resizee_size.set_width(new_second_resizee_size.width() - delta.x());
|
||||
|
||||
if (new_first_resizee_size.width() < minimum_size) {
|
||||
int correction = minimum_size - new_first_resizee_size.width();
|
||||
new_first_resizee_size.set_width(new_first_resizee_size.width() + correction);
|
||||
new_second_resizee_size.set_width(new_second_resizee_size.width() - correction);
|
||||
}
|
||||
if (new_second_resizee_size.width() < minimum_size) {
|
||||
int correction = minimum_size - new_second_resizee_size.width();
|
||||
new_second_resizee_size.set_width(new_second_resizee_size.width() + correction);
|
||||
new_first_resizee_size.set_width(new_first_resizee_size.width() - correction);
|
||||
}
|
||||
} else {
|
||||
new_first_resizee_size.set_height(new_first_resizee_size.height() + delta.y());
|
||||
new_second_resizee_size.set_height(new_second_resizee_size.height() - delta.y());
|
||||
|
||||
if (new_first_resizee_size.height() < minimum_size) {
|
||||
int correction = minimum_size - new_first_resizee_size.height();
|
||||
new_first_resizee_size.set_height(new_first_resizee_size.height() + correction);
|
||||
new_second_resizee_size.set_height(new_second_resizee_size.height() - correction);
|
||||
}
|
||||
if (new_second_resizee_size.height() < minimum_size) {
|
||||
int correction = minimum_size - new_second_resizee_size.height();
|
||||
new_second_resizee_size.set_height(new_second_resizee_size.height() + correction);
|
||||
new_first_resizee_size.set_height(new_first_resizee_size.height() - correction);
|
||||
}
|
||||
}
|
||||
m_first_resizee->set_preferred_size(new_first_resizee_size);
|
||||
m_second_resizee->set_preferred_size(new_second_resizee_size);
|
||||
|
||||
invalidate_layout();
|
||||
}
|
||||
|
||||
void GSplitter::mouseup_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() != GMouseButton::Left)
|
||||
return;
|
||||
m_resizing = false;
|
||||
if (!rect().contains(event.position()))
|
||||
window()->set_override_cursor(GStandardCursor::None);
|
||||
}
|
25
Libraries/LibGUI/GSplitter.h
Normal file
25
Libraries/LibGUI/GSplitter.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GFrame.h>
|
||||
|
||||
class GSplitter : public GFrame {
|
||||
public:
|
||||
GSplitter(Orientation, GWidget* parent);
|
||||
virtual ~GSplitter() override;
|
||||
|
||||
protected:
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void enter_event(CEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
|
||||
private:
|
||||
Orientation m_orientation;
|
||||
bool m_resizing { false };
|
||||
Point m_resize_origin;
|
||||
WeakPtr<GWidget> m_first_resizee;
|
||||
WeakPtr<GWidget> m_second_resizee;
|
||||
Size m_first_resizee_start_size;
|
||||
Size m_second_resizee_start_size;
|
||||
};
|
55
Libraries/LibGUI/GStackWidget.cpp
Normal file
55
Libraries/LibGUI/GStackWidget.cpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GStackWidget.h>
|
||||
|
||||
GStackWidget::GStackWidget(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GStackWidget::~GStackWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void GStackWidget::set_active_widget(GWidget* widget)
|
||||
{
|
||||
if (widget == m_active_widget)
|
||||
return;
|
||||
|
||||
if (m_active_widget)
|
||||
m_active_widget->set_visible(false);
|
||||
m_active_widget = widget;
|
||||
if (m_active_widget) {
|
||||
m_active_widget->set_relative_rect(rect());
|
||||
m_active_widget->set_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void GStackWidget::resize_event(GResizeEvent& event)
|
||||
{
|
||||
if (!m_active_widget)
|
||||
return;
|
||||
m_active_widget->set_relative_rect({ {}, event.size() });
|
||||
}
|
||||
|
||||
void GStackWidget::child_event(CChildEvent& event)
|
||||
{
|
||||
if (!event.child() || !is<GWidget>(*event.child()))
|
||||
return GWidget::child_event(event);
|
||||
auto& child = to<GWidget>(*event.child());
|
||||
if (event.type() == GEvent::ChildAdded) {
|
||||
if (!m_active_widget)
|
||||
set_active_widget(&child);
|
||||
else if (m_active_widget != &child)
|
||||
child.set_visible(false);
|
||||
} else if (event.type() == GEvent::ChildRemoved) {
|
||||
if (m_active_widget == &child) {
|
||||
GWidget* 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);
|
||||
}
|
||||
}
|
||||
GWidget::child_event(event);
|
||||
}
|
21
Libraries/LibGUI/GStackWidget.h
Normal file
21
Libraries/LibGUI/GStackWidget.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GStackWidget : public GWidget {
|
||||
public:
|
||||
explicit GStackWidget(GWidget* parent);
|
||||
virtual ~GStackWidget() override;
|
||||
|
||||
GWidget* active_widget() const { return m_active_widget; }
|
||||
void set_active_widget(GWidget*);
|
||||
|
||||
virtual const char* class_name() const override { return "GStackWidget"; }
|
||||
|
||||
protected:
|
||||
virtual void child_event(CChildEvent&) override;
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
|
||||
private:
|
||||
GWidget* m_active_widget { nullptr };
|
||||
};
|
44
Libraries/LibGUI/GStatusBar.cpp
Normal file
44
Libraries/LibGUI/GStatusBar.cpp
Normal file
|
@ -0,0 +1,44 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GLabel.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GResizeCorner.h>
|
||||
#include <LibGUI/GStatusBar.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
GStatusBar::GStatusBar(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
set_preferred_size({ 0, 20 });
|
||||
set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
layout()->set_margins({ 2, 2, 2, 2 });
|
||||
layout()->set_spacing(2);
|
||||
m_label = new GLabel(this);
|
||||
m_label->set_frame_shadow(FrameShadow::Sunken);
|
||||
m_label->set_frame_shape(FrameShape::Panel);
|
||||
m_label->set_frame_thickness(1);
|
||||
m_label->set_text_alignment(TextAlignment::CenterLeft);
|
||||
|
||||
m_corner = new GResizeCorner(this);
|
||||
}
|
||||
|
||||
GStatusBar::~GStatusBar()
|
||||
{
|
||||
}
|
||||
|
||||
void GStatusBar::set_text(const StringView& text)
|
||||
{
|
||||
m_label->set_text(text);
|
||||
}
|
||||
|
||||
String GStatusBar::text() const
|
||||
{
|
||||
return m_label->text();
|
||||
}
|
||||
|
||||
void GStatusBar::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
StylePainter::paint_surface(painter, rect(), !spans_entire_window_horizontally());
|
||||
}
|
23
Libraries/LibGUI/GStatusBar.h
Normal file
23
Libraries/LibGUI/GStatusBar.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GLabel;
|
||||
class GResizeCorner;
|
||||
|
||||
class GStatusBar : public GWidget {
|
||||
public:
|
||||
explicit GStatusBar(GWidget* parent);
|
||||
virtual ~GStatusBar() override;
|
||||
|
||||
String text() const;
|
||||
void set_text(const StringView&);
|
||||
|
||||
virtual const char* class_name() const override { return "GStatusBar"; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
GLabel* m_label { nullptr };
|
||||
GResizeCorner* m_corner { nullptr };
|
||||
};
|
176
Libraries/LibGUI/GTabWidget.cpp
Normal file
176
Libraries/LibGUI/GTabWidget.cpp
Normal file
|
@ -0,0 +1,176 @@
|
|||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GTabWidget.h>
|
||||
#include <SharedGraphics/StylePainter.h>
|
||||
|
||||
GTabWidget::GTabWidget(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
set_fill_with_background_color(true);
|
||||
set_background_color(Color::WarmGray);
|
||||
}
|
||||
|
||||
GTabWidget::~GTabWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void GTabWidget::add_widget(const StringView& title, GWidget* widget)
|
||||
{
|
||||
m_tabs.append({ title, widget });
|
||||
add_child(*widget);
|
||||
}
|
||||
|
||||
void GTabWidget::set_active_widget(GWidget* widget)
|
||||
{
|
||||
if (widget == m_active_widget)
|
||||
return;
|
||||
|
||||
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()));
|
||||
m_active_widget->set_visible(true);
|
||||
}
|
||||
|
||||
update_bar();
|
||||
}
|
||||
|
||||
void GTabWidget::resize_event(GResizeEvent& event)
|
||||
{
|
||||
if (!m_active_widget)
|
||||
return;
|
||||
m_active_widget->set_relative_rect(child_rect_for_size(event.size()));
|
||||
}
|
||||
|
||||
Rect GTabWidget::child_rect_for_size(const Size& size) const
|
||||
{
|
||||
return { { container_padding(), bar_height() + container_padding() }, { size.width() - container_padding() * 2, size.height() - bar_height() - container_padding() * 2 } };
|
||||
}
|
||||
|
||||
void GTabWidget::child_event(CChildEvent& event)
|
||||
{
|
||||
if (!event.child() || !is<GWidget>(*event.child()))
|
||||
return GWidget::child_event(event);
|
||||
auto& child = to<GWidget>(*event.child());
|
||||
if (event.type() == GEvent::ChildAdded) {
|
||||
if (!m_active_widget)
|
||||
set_active_widget(&child);
|
||||
else if (m_active_widget != &child)
|
||||
child.set_visible(false);
|
||||
} else if (event.type() == GEvent::ChildRemoved) {
|
||||
if (m_active_widget == &child) {
|
||||
GWidget* 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);
|
||||
}
|
||||
}
|
||||
GWidget::child_event(event);
|
||||
}
|
||||
|
||||
Rect GTabWidget::bar_rect() const
|
||||
{
|
||||
return { 0, 0, width(), bar_height() };
|
||||
}
|
||||
|
||||
void GTabWidget::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
Rect container_rect { 0, bar_height(), width(), height() - bar_height() };
|
||||
auto padding_rect = container_rect;
|
||||
for (int i = 0; i < container_padding(); ++i) {
|
||||
painter.draw_rect(padding_rect, background_color());
|
||||
padding_rect.shrink(2, 2);
|
||||
}
|
||||
|
||||
StylePainter::paint_frame(painter, container_rect, FrameShape::Container, FrameShadow::Raised, 2);
|
||||
|
||||
for (int i = 0; i < m_tabs.size(); ++i) {
|
||||
if (m_tabs[i].widget == m_active_widget)
|
||||
continue;
|
||||
bool hovered = i == m_hovered_tab_index;
|
||||
auto button_rect = this->button_rect(i);
|
||||
StylePainter::paint_tab_button(painter, button_rect, false, hovered, m_tabs[i].widget->is_enabled());
|
||||
painter.draw_text(button_rect.translated(0, 1), m_tabs[i].title, TextAlignment::Center);
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_tabs.size(); ++i) {
|
||||
if (m_tabs[i].widget != m_active_widget)
|
||||
continue;
|
||||
bool hovered = i == m_hovered_tab_index;
|
||||
auto button_rect = this->button_rect(i);
|
||||
StylePainter::paint_tab_button(painter, button_rect, true, hovered, m_tabs[i].widget->is_enabled());
|
||||
painter.draw_text(button_rect.translated(0, 1), m_tabs[i].title, TextAlignment::Center);
|
||||
painter.draw_line(button_rect.bottom_left().translated(1, 1), button_rect.bottom_right().translated(-1, 1), background_color());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Rect GTabWidget::button_rect(int index) const
|
||||
{
|
||||
int x_offset = 2;
|
||||
for (int i = 0; i < index; ++i)
|
||||
x_offset += m_tabs[i].width(font());
|
||||
Rect rect { x_offset, 0, m_tabs[index].width(font()), bar_height() };
|
||||
if (m_tabs[index].widget != m_active_widget) {
|
||||
rect.move_by(0, 2);
|
||||
rect.set_height(rect.height() - 2);
|
||||
} else {
|
||||
rect.move_by(-2, 0);
|
||||
rect.set_width(rect.width() + 4);
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
int GTabWidget::TabData::width(const Font& font) const
|
||||
{
|
||||
return 16 + font.width(title);
|
||||
}
|
||||
|
||||
void GTabWidget::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
for (int i = 0; i < m_tabs.size(); ++i) {
|
||||
auto button_rect = this->button_rect(i);
|
||||
if (!button_rect.contains(event.position()))
|
||||
continue;
|
||||
set_active_widget(m_tabs[i].widget);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GTabWidget::mousemove_event(GMouseEvent& event)
|
||||
{
|
||||
int hovered_tab = -1;
|
||||
for (int i = 0; i < m_tabs.size(); ++i) {
|
||||
auto button_rect = this->button_rect(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)
|
||||
return;
|
||||
m_hovered_tab_index = hovered_tab;
|
||||
update_bar();
|
||||
}
|
||||
|
||||
void GTabWidget::leave_event(CEvent&)
|
||||
{
|
||||
if (m_hovered_tab_index != -1) {
|
||||
m_hovered_tab_index = -1;
|
||||
update_bar();
|
||||
}
|
||||
}
|
||||
|
||||
void GTabWidget::update_bar()
|
||||
{
|
||||
auto invalidation_rect = bar_rect();
|
||||
invalidation_rect.set_height(invalidation_rect.height() + 1);
|
||||
update(invalidation_rect);
|
||||
}
|
44
Libraries/LibGUI/GTabWidget.h
Normal file
44
Libraries/LibGUI/GTabWidget.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GTabWidget : public GWidget {
|
||||
public:
|
||||
explicit GTabWidget(GWidget* parent);
|
||||
virtual ~GTabWidget() override;
|
||||
|
||||
GWidget* active_widget() const { return m_active_widget; }
|
||||
void set_active_widget(GWidget*);
|
||||
|
||||
int bar_height() const { return 21; }
|
||||
int container_padding() const { return 2; }
|
||||
|
||||
void add_widget(const StringView&, GWidget*);
|
||||
|
||||
virtual const char* class_name() const override { return "GTabWidget"; }
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void child_event(CChildEvent&) override;
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
|
||||
private:
|
||||
Rect child_rect_for_size(const Size&) const;
|
||||
Rect button_rect(int index) const;
|
||||
Rect bar_rect() const;
|
||||
void update_bar();
|
||||
|
||||
GWidget* m_active_widget { nullptr };
|
||||
|
||||
struct TabData {
|
||||
Rect rect(const Font&) const;
|
||||
int width(const Font&) const;
|
||||
String title;
|
||||
GWidget* widget { nullptr };
|
||||
};
|
||||
Vector<TabData> m_tabs;
|
||||
int m_hovered_tab_index { -1 };
|
||||
};
|
472
Libraries/LibGUI/GTableView.cpp
Normal file
472
Libraries/LibGUI/GTableView.cpp
Normal file
|
@ -0,0 +1,472 @@
|
|||
#include <AK/StringBuilder.h>
|
||||
#include <Kernel/KeyCode.h>
|
||||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GMenu.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
#include <LibGUI/GTableView.h>
|
||||
#include <LibGUI/GTextBox.h>
|
||||
#include <LibGUI/GWindow.h>
|
||||
|
||||
GTableView::GTableView(GWidget* parent)
|
||||
: GAbstractView(parent)
|
||||
{
|
||||
set_frame_shape(FrameShape::Container);
|
||||
set_frame_shadow(FrameShadow::Sunken);
|
||||
set_frame_thickness(2);
|
||||
}
|
||||
|
||||
GTableView::~GTableView()
|
||||
{
|
||||
}
|
||||
|
||||
void GTableView::update_content_size()
|
||||
{
|
||||
if (!model())
|
||||
return set_content_size({});
|
||||
|
||||
int content_width = 0;
|
||||
int column_count = model()->column_count();
|
||||
for (int i = 0; i < column_count; ++i) {
|
||||
if (!is_column_hidden(i))
|
||||
content_width += column_width(i) + horizontal_padding() * 2;
|
||||
}
|
||||
int content_height = item_count() * item_height();
|
||||
|
||||
set_content_size({ content_width, content_height });
|
||||
set_size_occupied_by_fixed_elements({ 0, header_height() });
|
||||
}
|
||||
|
||||
void GTableView::did_update_model()
|
||||
{
|
||||
GAbstractView::did_update_model();
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
Rect GTableView::content_rect(int row, int column) const
|
||||
{
|
||||
auto row_rect = this->row_rect(row);
|
||||
int x = 0;
|
||||
for (int i = 0; i < column; ++i)
|
||||
x += column_width(i) + horizontal_padding() * 2;
|
||||
|
||||
return { horizontal_padding() + row_rect.x() + x, row_rect.y(), column_width(column), item_height() };
|
||||
}
|
||||
|
||||
Rect GTableView::content_rect(const GModelIndex& index) const
|
||||
{
|
||||
return content_rect(index.row(), index.column());
|
||||
}
|
||||
|
||||
Rect GTableView::row_rect(int item_index) const
|
||||
{
|
||||
return { 0, header_height() + (item_index * item_height()), max(content_size().width(), width()), item_height() };
|
||||
}
|
||||
|
||||
int GTableView::column_width(int column_index) const
|
||||
{
|
||||
if (!model())
|
||||
return 0;
|
||||
auto& column_data = this->column_data(column_index);
|
||||
if (!column_data.has_initialized_width) {
|
||||
column_data.has_initialized_width = true;
|
||||
column_data.width = model()->column_metadata(column_index).preferred_width;
|
||||
}
|
||||
return column_data.width;
|
||||
}
|
||||
|
||||
Rect GTableView::header_rect(int column_index) const
|
||||
{
|
||||
if (!model())
|
||||
return {};
|
||||
if (is_column_hidden(column_index))
|
||||
return {};
|
||||
int x_offset = 0;
|
||||
for (int i = 0; i < column_index; ++i) {
|
||||
if (is_column_hidden(i))
|
||||
continue;
|
||||
x_offset += column_width(i) + horizontal_padding() * 2;
|
||||
}
|
||||
return { x_offset, 0, column_width(column_index) + horizontal_padding() * 2, header_height() };
|
||||
}
|
||||
|
||||
Point GTableView::adjusted_position(const Point& position) const
|
||||
{
|
||||
return position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
|
||||
}
|
||||
|
||||
Rect GTableView::column_resize_grabbable_rect(int column) const
|
||||
{
|
||||
if (!model())
|
||||
return {};
|
||||
auto header_rect = this->header_rect(column);
|
||||
return { header_rect.right() - 1, header_rect.top(), 4, header_rect.height() };
|
||||
}
|
||||
|
||||
void GTableView::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
|
||||
if (event.button() != GMouseButton::Left)
|
||||
return;
|
||||
|
||||
if (event.y() < header_height()) {
|
||||
for (int i = 0; i < model()->column_count(); ++i) {
|
||||
if (column_resize_grabbable_rect(i).contains(event.position())) {
|
||||
m_resizing_column = i;
|
||||
m_in_column_resize = true;
|
||||
m_column_resize_original_width = column_width(i);
|
||||
m_column_resize_origin = event.position();
|
||||
return;
|
||||
}
|
||||
auto header_rect = this->header_rect(i);
|
||||
if (header_rect.contains(event.position())) {
|
||||
auto new_sort_order = GSortOrder::Ascending;
|
||||
if (model()->key_column() == i)
|
||||
new_sort_order = model()->sort_order() == GSortOrder::Ascending
|
||||
? GSortOrder::Descending
|
||||
: GSortOrder::Ascending;
|
||||
model()->set_key_column_and_sort_order(i, new_sort_order);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
model()->set_selected_index(index_at_event_position(event.position()));
|
||||
update();
|
||||
}
|
||||
|
||||
GModelIndex GTableView::index_at_event_position(const Point& position) const
|
||||
{
|
||||
if (!model())
|
||||
return {};
|
||||
|
||||
auto adjusted_position = this->adjusted_position(position);
|
||||
for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) {
|
||||
if (!row_rect(row).contains(adjusted_position))
|
||||
continue;
|
||||
for (int column = 0, column_count = model()->column_count(); column < column_count; ++column) {
|
||||
if (!content_rect(row, column).contains(adjusted_position))
|
||||
continue;
|
||||
return model()->index(row, column);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void GTableView::mousemove_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
|
||||
if (m_in_column_resize) {
|
||||
auto delta = event.position() - m_column_resize_origin;
|
||||
int new_width = m_column_resize_original_width + delta.x();
|
||||
ASSERT(m_resizing_column >= 0 && m_resizing_column < model()->column_count());
|
||||
auto& column_data = this->column_data(m_resizing_column);
|
||||
if (column_data.width != new_width) {
|
||||
column_data.width = new_width;
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto adjusted_position = this->adjusted_position(event.position());
|
||||
if (event.buttons() == 0) {
|
||||
for (int i = 0; i < model()->column_count(); ++i) {
|
||||
if (column_resize_grabbable_rect(i).contains(adjusted_position)) {
|
||||
window()->set_override_cursor(GStandardCursor::ResizeHorizontal);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
window()->set_override_cursor(GStandardCursor::None);
|
||||
}
|
||||
|
||||
void GTableView::mouseup_event(GMouseEvent& event)
|
||||
{
|
||||
auto adjusted_position = this->adjusted_position(event.position());
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
if (m_in_column_resize) {
|
||||
if (!column_resize_grabbable_rect(m_resizing_column).contains(adjusted_position))
|
||||
window()->set_override_cursor(GStandardCursor::None);
|
||||
m_in_column_resize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GTableView::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GFrame::paint_event(event);
|
||||
|
||||
if (!model())
|
||||
return;
|
||||
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(frame_inner_rect());
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
|
||||
|
||||
int exposed_width = max(content_size().width(), width());
|
||||
int painted_item_index = 0;
|
||||
int y_offset = header_height();
|
||||
|
||||
for (int row_index = 0; row_index < model()->row_count(); ++row_index) {
|
||||
bool is_selected_row = row_index == model()->selected_index().row();
|
||||
int y = y_offset + painted_item_index * item_height();
|
||||
|
||||
Color background_color;
|
||||
Color key_column_background_color;
|
||||
if (is_selected_row) {
|
||||
background_color = is_focused() ? Color::from_rgb(0x84351a) : Color::from_rgb(0x606060);
|
||||
key_column_background_color = is_focused() ? Color::from_rgb(0x84351a) : Color::from_rgb(0x606060);
|
||||
} else {
|
||||
if (alternating_row_colors() && (painted_item_index % 2)) {
|
||||
background_color = Color(210, 210, 210);
|
||||
key_column_background_color = Color(180, 180, 180);
|
||||
} else {
|
||||
background_color = Color::White;
|
||||
key_column_background_color = Color(210, 210, 210);
|
||||
}
|
||||
}
|
||||
painter.fill_rect(row_rect(painted_item_index), background_color);
|
||||
|
||||
int x_offset = 0;
|
||||
for (int column_index = 0; column_index < model()->column_count(); ++column_index) {
|
||||
if (is_column_hidden(column_index))
|
||||
continue;
|
||||
auto column_metadata = model()->column_metadata(column_index);
|
||||
int column_width = this->column_width(column_index);
|
||||
const Font& font = column_metadata.font ? *column_metadata.font : this->font();
|
||||
bool is_key_column = model()->key_column() == column_index;
|
||||
Rect cell_rect(horizontal_padding() + x_offset, y, column_width, item_height());
|
||||
if (is_key_column) {
|
||||
auto cell_rect_for_fill = cell_rect.inflated(horizontal_padding() * 2, 0);
|
||||
painter.fill_rect(cell_rect_for_fill, key_column_background_color);
|
||||
}
|
||||
auto cell_index = model()->index(row_index, column_index);
|
||||
auto data = model()->data(cell_index);
|
||||
if (data.is_bitmap()) {
|
||||
painter.blit(cell_rect.location(), data.as_bitmap(), data.as_bitmap().rect());
|
||||
} else if (data.is_icon()) {
|
||||
if (auto bitmap = data.as_icon().bitmap_for_size(16))
|
||||
painter.blit(cell_rect.location(), *bitmap, bitmap->rect());
|
||||
} else {
|
||||
Color text_color;
|
||||
if (is_selected_row)
|
||||
text_color = Color::White;
|
||||
else
|
||||
text_color = model()->data(cell_index, GModel::Role::ForegroundColor).to_color(Color::Black);
|
||||
painter.draw_text(cell_rect, data.to_string(), font, column_metadata.text_alignment, text_color);
|
||||
}
|
||||
x_offset += column_width + horizontal_padding() * 2;
|
||||
}
|
||||
++painted_item_index;
|
||||
};
|
||||
|
||||
Rect unpainted_rect(0, header_height() + painted_item_index * item_height(), exposed_width, height());
|
||||
painter.fill_rect(unpainted_rect, Color::White);
|
||||
|
||||
// Untranslate the painter vertically and do the column headers.
|
||||
painter.translate(0, vertical_scrollbar().value());
|
||||
if (headers_visible())
|
||||
paint_headers(painter);
|
||||
}
|
||||
|
||||
void GTableView::paint_headers(Painter& painter)
|
||||
{
|
||||
int exposed_width = max(content_size().width(), width());
|
||||
painter.fill_rect({ 0, 0, exposed_width, header_height() }, Color::WarmGray);
|
||||
painter.draw_line({ 0, 0 }, { exposed_width - 1, 0 }, Color::White);
|
||||
painter.draw_line({ 0, header_height() - 1 }, { exposed_width - 1, header_height() - 1 }, Color::MidGray);
|
||||
int x_offset = 0;
|
||||
for (int column_index = 0; column_index < model()->column_count(); ++column_index) {
|
||||
if (is_column_hidden(column_index))
|
||||
continue;
|
||||
int column_width = this->column_width(column_index);
|
||||
bool is_key_column = model()->key_column() == column_index;
|
||||
Rect cell_rect(x_offset, 0, column_width + horizontal_padding() * 2, header_height());
|
||||
StylePainter::paint_button(painter, cell_rect, ButtonStyle::Normal, false);
|
||||
String text;
|
||||
if (is_key_column) {
|
||||
StringBuilder builder;
|
||||
builder.append(model()->column_name(column_index));
|
||||
auto sort_order = model()->sort_order();
|
||||
if (sort_order == GSortOrder::Ascending)
|
||||
builder.append(" \xf6");
|
||||
else if (sort_order == GSortOrder::Descending)
|
||||
builder.append(" \xf7");
|
||||
text = builder.to_string();
|
||||
} else {
|
||||
text = model()->column_name(column_index);
|
||||
}
|
||||
auto text_rect = cell_rect.translated(horizontal_padding(), 0);
|
||||
painter.draw_text(text_rect, text, Font::default_bold_font(), TextAlignment::CenterLeft, Color::Black);
|
||||
x_offset += column_width + horizontal_padding() * 2;
|
||||
}
|
||||
}
|
||||
|
||||
int GTableView::item_count() const
|
||||
{
|
||||
if (!model())
|
||||
return 0;
|
||||
return model()->row_count();
|
||||
}
|
||||
|
||||
void GTableView::keydown_event(GKeyEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
if (event.key() == KeyCode::Key_Return) {
|
||||
activate(model.selected_index());
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Up) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() - 1, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Down) {
|
||||
GModelIndex new_index;
|
||||
if (model.selected_index().is_valid())
|
||||
new_index = model.index(model.selected_index().row() + 1, model.selected_index().column());
|
||||
else
|
||||
new_index = model.index(0, 0);
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageUp) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto new_index = model.index(max(0, model.selected_index().row() - items_per_page), model.selected_index().column());
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageDown) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto new_index = model.index(min(model.row_count() - 1, model.selected_index().row() + items_per_page), model.selected_index().column());
|
||||
if (model.is_valid(new_index)) {
|
||||
model.set_selected_index(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
return GWidget::keydown_event(event);
|
||||
}
|
||||
|
||||
void GTableView::scroll_into_view(const GModelIndex& index, Orientation orientation)
|
||||
{
|
||||
auto rect = row_rect(index.row()).translated(0, -header_height());
|
||||
GScrollableWidget::scroll_into_view(rect, orientation);
|
||||
}
|
||||
|
||||
GTableView::ColumnData& GTableView::column_data(int column) const
|
||||
{
|
||||
if (column >= m_column_data.size())
|
||||
m_column_data.resize(column + 1);
|
||||
return m_column_data.at(column);
|
||||
}
|
||||
|
||||
bool GTableView::is_column_hidden(int column) const
|
||||
{
|
||||
return !column_data(column).visibility;
|
||||
}
|
||||
|
||||
void GTableView::set_column_hidden(int column, bool hidden)
|
||||
{
|
||||
auto& column_data = this->column_data(column);
|
||||
if (column_data.visibility == !hidden)
|
||||
return;
|
||||
column_data.visibility = !hidden;
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
void GTableView::doubleclick_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
if (event.y() < header_height())
|
||||
return;
|
||||
if (model.selected_index().is_valid()) {
|
||||
if (is_editable())
|
||||
begin_editing(model.selected_index());
|
||||
else
|
||||
activate(model.selected_index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GMenu& GTableView::ensure_header_context_menu()
|
||||
{
|
||||
// FIXME: This menu needs to be rebuilt if the model is swapped out,
|
||||
// or if the column count/names change.
|
||||
if (!m_header_context_menu) {
|
||||
ASSERT(model());
|
||||
m_header_context_menu = make<GMenu>("");
|
||||
|
||||
for (int column = 0; column < model()->column_count(); ++column) {
|
||||
auto& column_data = this->column_data(column);
|
||||
auto name = model()->column_name(column);
|
||||
column_data.visibility_action = GAction::create(name, [this, column](GAction& action) {
|
||||
action.set_checked(!action.is_checked());
|
||||
set_column_hidden(column, !action.is_checked());
|
||||
});
|
||||
column_data.visibility_action->set_checkable(true);
|
||||
column_data.visibility_action->set_checked(true);
|
||||
|
||||
m_header_context_menu->add_action(*column_data.visibility_action);
|
||||
}
|
||||
}
|
||||
return *m_header_context_menu;
|
||||
}
|
||||
|
||||
void GTableView::context_menu_event(GContextMenuEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
if (event.position().y() < header_height()) {
|
||||
ensure_header_context_menu().popup(event.screen_position());
|
||||
return;
|
||||
}
|
||||
|
||||
auto index = index_at_event_position(event.position());
|
||||
if (!index.is_valid())
|
||||
return;
|
||||
dbgprintf("context menu requested for index (%d,%d) '%s'\n", index.row(), index.column(), model()->data(index).to_string().characters());
|
||||
|
||||
model()->set_selected_index(index);
|
||||
update();
|
||||
if (on_context_menu_request)
|
||||
on_context_menu_request(index, event);
|
||||
}
|
||||
|
||||
void GTableView::leave_event(CEvent&)
|
||||
{
|
||||
window()->set_override_cursor(GStandardCursor::None);
|
||||
}
|
81
Libraries/LibGUI/GTableView.h
Normal file
81
Libraries/LibGUI/GTableView.h
Normal file
|
@ -0,0 +1,81 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/GAbstractView.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
|
||||
class GScrollBar;
|
||||
class Painter;
|
||||
|
||||
class GTableView : public GAbstractView {
|
||||
public:
|
||||
explicit GTableView(GWidget* parent);
|
||||
virtual ~GTableView() override;
|
||||
|
||||
int header_height() const { return m_headers_visible ? 16 : 0; }
|
||||
int item_height() const { return 16; }
|
||||
|
||||
bool headers_visible() const { return m_headers_visible; }
|
||||
void set_headers_visible(bool headers_visible) { m_headers_visible = headers_visible; }
|
||||
|
||||
bool alternating_row_colors() const { return m_alternating_row_colors; }
|
||||
void set_alternating_row_colors(bool b) { m_alternating_row_colors = b; }
|
||||
|
||||
int content_width() const;
|
||||
int horizontal_padding() const { return m_horizontal_padding; }
|
||||
|
||||
void scroll_into_view(const GModelIndex&, Orientation);
|
||||
|
||||
bool is_column_hidden(int) const;
|
||||
void set_column_hidden(int, bool);
|
||||
|
||||
Point adjusted_position(const Point&) const;
|
||||
|
||||
virtual Rect content_rect(const GModelIndex&) const override;
|
||||
|
||||
virtual const char* class_name() const override { return "GTableView"; }
|
||||
|
||||
private:
|
||||
virtual void did_update_model() override;
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void doubleclick_event(GMouseEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
virtual void context_menu_event(GContextMenuEvent&) override;
|
||||
|
||||
GModelIndex index_at_event_position(const Point&) const;
|
||||
|
||||
Rect content_rect(int row, int column) const;
|
||||
void paint_headers(Painter&);
|
||||
int item_count() const;
|
||||
Rect row_rect(int item_index) const;
|
||||
Rect header_rect(int) const;
|
||||
Rect column_resize_grabbable_rect(int) const;
|
||||
int column_width(int) const;
|
||||
void update_content_size();
|
||||
|
||||
struct ColumnData {
|
||||
int width { 0 };
|
||||
bool has_initialized_width { false };
|
||||
bool visibility { true };
|
||||
RefPtr<GAction> visibility_action;
|
||||
};
|
||||
ColumnData& column_data(int column) const;
|
||||
|
||||
mutable Vector<ColumnData> m_column_data;
|
||||
int m_horizontal_padding { 5 };
|
||||
bool m_headers_visible { true };
|
||||
bool m_alternating_row_colors { true };
|
||||
|
||||
bool m_in_column_resize { false };
|
||||
Point m_column_resize_origin;
|
||||
int m_column_resize_original_width { 0 };
|
||||
int m_resizing_column { -1 };
|
||||
|
||||
GMenu& ensure_header_context_menu();
|
||||
OwnPtr<GMenu> m_header_context_menu;
|
||||
};
|
10
Libraries/LibGUI/GTextBox.cpp
Normal file
10
Libraries/LibGUI/GTextBox.cpp
Normal file
|
@ -0,0 +1,10 @@
|
|||
#include <LibGUI/GTextBox.h>
|
||||
|
||||
GTextBox::GTextBox(GWidget* parent)
|
||||
: GTextEditor(GTextEditor::SingleLine, parent)
|
||||
{
|
||||
}
|
||||
|
||||
GTextBox::~GTextBox()
|
||||
{
|
||||
}
|
12
Libraries/LibGUI/GTextBox.h
Normal file
12
Libraries/LibGUI/GTextBox.h
Normal file
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <LibGUI/GTextEditor.h>
|
||||
|
||||
class GTextBox final : public GTextEditor {
|
||||
public:
|
||||
explicit GTextBox(GWidget* parent);
|
||||
virtual ~GTextBox() override;
|
||||
|
||||
virtual const char* class_name() const override { return "GTextBox"; }
|
||||
};
|
1074
Libraries/LibGUI/GTextEditor.cpp
Normal file
1074
Libraries/LibGUI/GTextEditor.cpp
Normal file
File diff suppressed because it is too large
Load diff
232
Libraries/LibGUI/GTextEditor.h
Normal file
232
Libraries/LibGUI/GTextEditor.h
Normal file
|
@ -0,0 +1,232 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/GScrollableWidget.h>
|
||||
#include <SharedGraphics/TextAlignment.h>
|
||||
|
||||
class GAction;
|
||||
class GMenu;
|
||||
class GScrollBar;
|
||||
class Painter;
|
||||
|
||||
class GTextPosition {
|
||||
public:
|
||||
GTextPosition() {}
|
||||
GTextPosition(int line, int column)
|
||||
: m_line(line)
|
||||
, m_column(column)
|
||||
{
|
||||
}
|
||||
|
||||
bool is_valid() const { return m_line >= 0 && m_column >= 0; }
|
||||
|
||||
int line() const { return m_line; }
|
||||
int column() const { return m_column; }
|
||||
|
||||
void set_line(int line) { m_line = line; }
|
||||
void set_column(int column) { m_column = column; }
|
||||
|
||||
bool operator==(const GTextPosition& other) const { return m_line == other.m_line && m_column == other.m_column; }
|
||||
bool operator!=(const GTextPosition& other) const { return m_line != other.m_line || m_column != other.m_column; }
|
||||
bool operator<(const GTextPosition& other) const { return m_line < other.m_line || (m_line == other.m_line && m_column < other.m_column); }
|
||||
|
||||
private:
|
||||
int m_line { -1 };
|
||||
int m_column { -1 };
|
||||
};
|
||||
|
||||
class GTextRange {
|
||||
public:
|
||||
GTextRange() {}
|
||||
GTextRange(const GTextPosition& start, const GTextPosition& end)
|
||||
: m_start(start)
|
||||
, m_end(end)
|
||||
{
|
||||
}
|
||||
|
||||
bool is_valid() const { return m_start.is_valid() && m_end.is_valid(); }
|
||||
void clear()
|
||||
{
|
||||
m_start = {};
|
||||
m_end = {};
|
||||
}
|
||||
|
||||
GTextPosition& start() { return m_start; }
|
||||
GTextPosition& end() { return m_end; }
|
||||
const GTextPosition& start() const { return m_start; }
|
||||
const GTextPosition& end() const { return m_end; }
|
||||
|
||||
GTextRange normalized() const { return GTextRange(normalized_start(), normalized_end()); }
|
||||
|
||||
void set_start(const GTextPosition& position) { m_start = position; }
|
||||
void set_end(const GTextPosition& position) { m_end = position; }
|
||||
|
||||
void set(const GTextPosition& start, const GTextPosition& end)
|
||||
{
|
||||
m_start = start;
|
||||
m_end = end;
|
||||
}
|
||||
|
||||
private:
|
||||
GTextPosition normalized_start() const { return m_start < m_end ? m_start : m_end; }
|
||||
GTextPosition normalized_end() const { return m_start < m_end ? m_end : m_start; }
|
||||
|
||||
GTextPosition m_start;
|
||||
GTextPosition m_end;
|
||||
};
|
||||
|
||||
class GTextEditor : public GScrollableWidget {
|
||||
public:
|
||||
enum Type {
|
||||
MultiLine,
|
||||
SingleLine
|
||||
};
|
||||
GTextEditor(Type, GWidget* parent);
|
||||
virtual ~GTextEditor() override;
|
||||
|
||||
bool is_readonly() const { return m_readonly; }
|
||||
void set_readonly(bool);
|
||||
|
||||
bool is_automatic_indentation() const { return m_automatic_indentation_enabled; }
|
||||
void set_automatic_indentation_enabled(bool enabled) { m_automatic_indentation_enabled = enabled; }
|
||||
|
||||
TextAlignment text_alignment() const { return m_text_alignment; }
|
||||
void set_text_alignment(TextAlignment);
|
||||
|
||||
Type type() const { return m_type; }
|
||||
bool is_single_line() const { return m_type == SingleLine; }
|
||||
bool is_multi_line() const { return m_type == MultiLine; }
|
||||
|
||||
bool is_ruler_visible() const { return m_ruler_visible; }
|
||||
void set_ruler_visible(bool b) { m_ruler_visible = b; }
|
||||
|
||||
Function<void()> on_cursor_change;
|
||||
Function<void()> on_selection_change;
|
||||
|
||||
void set_text(const StringView&);
|
||||
void scroll_cursor_into_view();
|
||||
int line_count() const { return m_lines.size(); }
|
||||
int line_spacing() const { return m_line_spacing; }
|
||||
int line_height() const { return font().glyph_height() + m_line_spacing; }
|
||||
GTextPosition cursor() const { return m_cursor; }
|
||||
GTextRange normalized_selection() const { return m_selection.normalized(); }
|
||||
// FIXME: This should take glyph spacing into account, no?
|
||||
int glyph_width() const { return font().glyph_width('x'); }
|
||||
|
||||
bool write_to_file(const StringView& path);
|
||||
|
||||
bool has_selection() const { return m_selection.is_valid(); }
|
||||
String selected_text() const;
|
||||
String text() const;
|
||||
|
||||
void clear();
|
||||
|
||||
void cut();
|
||||
void copy();
|
||||
void paste();
|
||||
void do_delete();
|
||||
void delete_current_line();
|
||||
void select_all();
|
||||
|
||||
Function<void()> on_change;
|
||||
Function<void()> on_return_pressed;
|
||||
Function<void()> on_escape_pressed;
|
||||
|
||||
virtual const char* class_name() const override { return "GTextEditor"; }
|
||||
|
||||
GAction& undo_action() { return *m_undo_action; }
|
||||
GAction& redo_action() { return *m_redo_action; }
|
||||
GAction& cut_action() { return *m_cut_action; }
|
||||
GAction& copy_action() { return *m_copy_action; }
|
||||
GAction& paste_action() { return *m_paste_action; }
|
||||
GAction& delete_action() { return *m_delete_action; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void mouseup_event(GMouseEvent&) override;
|
||||
virtual void mousemove_event(GMouseEvent&) override;
|
||||
virtual void doubleclick_event(GMouseEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void focusin_event(CEvent&) override;
|
||||
virtual void focusout_event(CEvent&) override;
|
||||
virtual void timer_event(CTimerEvent&) override;
|
||||
virtual bool accepts_focus() const override { return true; }
|
||||
virtual void enter_event(CEvent&) override;
|
||||
virtual void leave_event(CEvent&) override;
|
||||
virtual void context_menu_event(GContextMenuEvent&) override;
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
|
||||
void create_actions();
|
||||
void paint_ruler(Painter&);
|
||||
void update_content_size();
|
||||
void did_change();
|
||||
|
||||
class Line {
|
||||
friend class GTextEditor;
|
||||
|
||||
public:
|
||||
Line();
|
||||
explicit Line(const StringView&);
|
||||
|
||||
const char* characters() const { return m_text.data(); }
|
||||
int length() const { return m_text.size() - 1; }
|
||||
int width(const Font&) const;
|
||||
void set_text(const StringView&);
|
||||
void append(char);
|
||||
void prepend(char);
|
||||
void insert(int index, char);
|
||||
void remove(int index);
|
||||
void append(const char*, int);
|
||||
void truncate(int length);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
// NOTE: This vector is null terminated.
|
||||
Vector<char> m_text;
|
||||
};
|
||||
|
||||
Rect line_content_rect(int item_index) const;
|
||||
Rect line_widget_rect(int line_index) const;
|
||||
Rect cursor_content_rect() const;
|
||||
void update_cursor();
|
||||
void set_cursor(int line, int column);
|
||||
void set_cursor(const GTextPosition&);
|
||||
Line& current_line() { return *m_lines[m_cursor.line()]; }
|
||||
const Line& current_line() const { return *m_lines[m_cursor.line()]; }
|
||||
GTextPosition text_position_at(const Point&) const;
|
||||
void insert_at_cursor(char);
|
||||
void insert_at_cursor(const StringView&);
|
||||
int ruler_width() const;
|
||||
Rect ruler_content_rect(int line) const;
|
||||
void toggle_selection_if_needed_for_event(const GKeyEvent&);
|
||||
void insert_at_cursor_or_replace_selection(const StringView&);
|
||||
void delete_selection();
|
||||
void did_update_selection();
|
||||
int content_x_for_position(const GTextPosition&) const;
|
||||
|
||||
Type m_type { MultiLine };
|
||||
|
||||
Vector<OwnPtr<Line>> m_lines;
|
||||
GTextPosition m_cursor;
|
||||
TextAlignment m_text_alignment { TextAlignment::CenterLeft };
|
||||
bool m_cursor_state { true };
|
||||
bool m_in_drag_select { false };
|
||||
bool m_ruler_visible { false };
|
||||
bool m_have_pending_change_notification { false };
|
||||
bool m_automatic_indentation_enabled { false };
|
||||
bool m_readonly { false };
|
||||
int m_line_spacing { 4 };
|
||||
int m_soft_tab_width { 4 };
|
||||
int m_horizontal_content_padding { 2 };
|
||||
GTextRange m_selection;
|
||||
OwnPtr<GMenu> m_context_menu;
|
||||
RefPtr<GAction> m_undo_action;
|
||||
RefPtr<GAction> m_redo_action;
|
||||
RefPtr<GAction> m_cut_action;
|
||||
RefPtr<GAction> m_copy_action;
|
||||
RefPtr<GAction> m_paste_action;
|
||||
RefPtr<GAction> m_delete_action;
|
||||
CElapsedTimer m_triple_click_timer;
|
||||
};
|
89
Libraries/LibGUI/GToolBar.cpp
Normal file
89
Libraries/LibGUI/GToolBar.cpp
Normal file
|
@ -0,0 +1,89 @@
|
|||
#include <LibGUI/GAction.h>
|
||||
#include <LibGUI/GBoxLayout.h>
|
||||
#include <LibGUI/GButton.h>
|
||||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GToolBar.h>
|
||||
|
||||
GToolBar::GToolBar(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
|
||||
set_preferred_size({ 0, 28 });
|
||||
set_layout(make<GBoxLayout>(Orientation::Horizontal));
|
||||
layout()->set_spacing(0);
|
||||
layout()->set_margins({ 2, 2, 2, 2 });
|
||||
}
|
||||
|
||||
GToolBar::~GToolBar()
|
||||
{
|
||||
}
|
||||
|
||||
void GToolBar::add_action(NonnullRefPtr<GAction>&& action)
|
||||
{
|
||||
GAction* raw_action_ptr = action.ptr();
|
||||
auto item = make<Item>();
|
||||
item->type = Item::Action;
|
||||
item->action = move(action);
|
||||
|
||||
auto* button = new GButton(this);
|
||||
button->set_action(*item->action);
|
||||
button->set_tooltip(item->action->text());
|
||||
if (item->action->icon())
|
||||
button->set_icon(item->action->icon());
|
||||
else
|
||||
button->set_text(item->action->text());
|
||||
button->on_click = [raw_action_ptr](const GButton&) {
|
||||
raw_action_ptr->activate();
|
||||
};
|
||||
|
||||
button->set_button_style(ButtonStyle::CoolBar);
|
||||
button->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
ASSERT(button->size_policy(Orientation::Horizontal) == SizePolicy::Fixed);
|
||||
ASSERT(button->size_policy(Orientation::Vertical) == SizePolicy::Fixed);
|
||||
button->set_preferred_size({ 24, 24 });
|
||||
|
||||
m_items.append(move(item));
|
||||
}
|
||||
|
||||
class SeparatorWidget final : public GWidget {
|
||||
public:
|
||||
SeparatorWidget(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed);
|
||||
set_background_color(Color::White);
|
||||
set_preferred_size({ 8, 22 });
|
||||
}
|
||||
virtual ~SeparatorWidget() override {}
|
||||
|
||||
virtual void paint_event(GPaintEvent& event) override
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.translate(rect().center().x() - 1, 0);
|
||||
painter.draw_line({ 0, 0 }, { 0, rect().bottom() }, Color::MidGray);
|
||||
painter.draw_line({ 1, 0 }, { 1, rect().bottom() }, Color::White);
|
||||
}
|
||||
|
||||
private:
|
||||
virtual const char* class_name() const override { return "SeparatorWidget"; }
|
||||
};
|
||||
|
||||
void GToolBar::add_separator()
|
||||
{
|
||||
auto item = make<Item>();
|
||||
item->type = Item::Separator;
|
||||
new SeparatorWidget(this);
|
||||
m_items.append(move(item));
|
||||
}
|
||||
|
||||
void GToolBar::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
if (m_has_frame)
|
||||
StylePainter::paint_surface(painter, rect(), x() != 0, y() != 0);
|
||||
else
|
||||
painter.fill_rect(event.rect(), Color::WarmGray);
|
||||
}
|
34
Libraries/LibGUI/GToolBar.h
Normal file
34
Libraries/LibGUI/GToolBar.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
|
||||
class GAction;
|
||||
|
||||
class GToolBar : public GWidget {
|
||||
public:
|
||||
explicit GToolBar(GWidget* parent);
|
||||
virtual ~GToolBar() override;
|
||||
|
||||
void add_action(NonnullRefPtr<GAction>&&);
|
||||
void add_separator();
|
||||
|
||||
bool has_frame() const { return m_has_frame; }
|
||||
void set_has_frame(bool has_frame) { m_has_frame = has_frame; }
|
||||
|
||||
virtual const char* class_name() const override { return "GToolBar"; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
|
||||
struct Item {
|
||||
enum Type {
|
||||
Invalid,
|
||||
Separator,
|
||||
Action
|
||||
};
|
||||
Type type { Invalid };
|
||||
RefPtr<GAction> action;
|
||||
};
|
||||
Vector<OwnPtr<Item>> m_items;
|
||||
bool m_has_frame { true };
|
||||
};
|
299
Libraries/LibGUI/GTreeView.cpp
Normal file
299
Libraries/LibGUI/GTreeView.cpp
Normal file
|
@ -0,0 +1,299 @@
|
|||
#include <LibGUI/GPainter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
#include <LibGUI/GTreeView.h>
|
||||
|
||||
//#define DEBUG_ITEM_RECTS
|
||||
|
||||
struct GTreeView::MetadataForIndex {
|
||||
bool open { false };
|
||||
};
|
||||
|
||||
GTreeView::MetadataForIndex& GTreeView::ensure_metadata_for_index(const GModelIndex& index) const
|
||||
{
|
||||
ASSERT(index.is_valid());
|
||||
auto it = m_view_metadata.find(index.internal_data());
|
||||
if (it != m_view_metadata.end())
|
||||
return *it->value;
|
||||
auto new_metadata = make<MetadataForIndex>();
|
||||
auto& new_metadata_ref = *new_metadata;
|
||||
m_view_metadata.set(index.internal_data(), move(new_metadata));
|
||||
return new_metadata_ref;
|
||||
}
|
||||
|
||||
GTreeView::GTreeView(GWidget* parent)
|
||||
: GAbstractView(parent)
|
||||
{
|
||||
set_frame_shape(FrameShape::Container);
|
||||
set_frame_shadow(FrameShadow::Sunken);
|
||||
set_frame_thickness(2);
|
||||
|
||||
m_expand_bitmap = GraphicsBitmap::load_from_file("/res/icons/treeview-expand.png");
|
||||
m_collapse_bitmap = GraphicsBitmap::load_from_file("/res/icons/treeview-collapse.png");
|
||||
}
|
||||
|
||||
GTreeView::~GTreeView()
|
||||
{
|
||||
}
|
||||
|
||||
GModelIndex GTreeView::index_at_content_position(const Point& position, bool& is_toggle) const
|
||||
{
|
||||
is_toggle = false;
|
||||
if (!model())
|
||||
return {};
|
||||
GModelIndex result;
|
||||
traverse_in_paint_order([&](const GModelIndex& index, const Rect& rect, const Rect& toggle_rect, int) {
|
||||
if (rect.contains(position)) {
|
||||
result = index;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
if (toggle_rect.contains(position)) {
|
||||
result = index;
|
||||
is_toggle = true;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void GTreeView::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
auto adjusted_position = event.position().translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
|
||||
bool is_toggle;
|
||||
auto index = index_at_content_position(adjusted_position, is_toggle);
|
||||
if (!index.is_valid())
|
||||
return;
|
||||
|
||||
if (model.selected_index() != index) {
|
||||
model.set_selected_index(index);
|
||||
update();
|
||||
}
|
||||
|
||||
if (is_toggle && model.row_count(index)) {
|
||||
auto& metadata = ensure_metadata_for_index(index);
|
||||
metadata.open = !metadata.open;
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void GTreeView::traverse_in_paint_order(Callback callback) const
|
||||
{
|
||||
ASSERT(model());
|
||||
auto& model = *this->model();
|
||||
int indent_level = 0;
|
||||
int y_offset = 0;
|
||||
|
||||
Function<IterationDecision(const GModelIndex&)> traverse_index = [&](const GModelIndex& index) {
|
||||
int row_count_at_index = model.row_count(index);
|
||||
if (index.is_valid()) {
|
||||
auto& metadata = ensure_metadata_for_index(index);
|
||||
int x_offset = indent_level * indent_width_in_pixels();
|
||||
auto node_text = model.data(index, GModel::Role::Display).to_string();
|
||||
Rect rect = {
|
||||
x_offset, y_offset,
|
||||
icon_size() + icon_spacing() + text_padding() + font().width(node_text) + text_padding(), item_height()
|
||||
};
|
||||
Rect toggle_rect;
|
||||
if (row_count_at_index > 0) {
|
||||
int toggle_x = indent_width_in_pixels() * indent_level - icon_size() / 2 - 4;
|
||||
toggle_rect = { toggle_x, rect.y(), toggle_size(), toggle_size() };
|
||||
toggle_rect.center_vertically_within(rect);
|
||||
}
|
||||
if (callback(index, rect, toggle_rect, indent_level) == IterationDecision::Break)
|
||||
return IterationDecision::Break;
|
||||
y_offset += item_height();
|
||||
// NOTE: Skip traversing children if this index is closed!
|
||||
if (!metadata.open)
|
||||
return IterationDecision::Continue;
|
||||
}
|
||||
|
||||
++indent_level;
|
||||
int row_count = model.row_count(index);
|
||||
for (int i = 0; i < row_count; ++i) {
|
||||
if (traverse_index(model.index(i, 0, index)) == IterationDecision::Break)
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
--indent_level;
|
||||
return IterationDecision::Continue;
|
||||
};
|
||||
traverse_index(model.index(0, 0, GModelIndex()));
|
||||
}
|
||||
|
||||
void GTreeView::paint_event(GPaintEvent& event)
|
||||
{
|
||||
GFrame::paint_event(event);
|
||||
GPainter painter(*this);
|
||||
painter.add_clip_rect(frame_inner_rect());
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.fill_rect(event.rect(), Color::White);
|
||||
painter.translate(frame_inner_rect().location());
|
||||
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
|
||||
|
||||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
auto visible_content_rect = this->visible_content_rect();
|
||||
|
||||
traverse_in_paint_order([&](const GModelIndex& index, const Rect& rect, const Rect& toggle_rect, int indent_level) {
|
||||
if (!rect.intersects(visible_content_rect))
|
||||
return IterationDecision::Continue;
|
||||
#ifdef DEBUG_ITEM_RECTS
|
||||
painter.fill_rect(rect, Color::WarmGray);
|
||||
#endif
|
||||
|
||||
Color background_color = Color::from_rgb(0xffffff);
|
||||
Color text_color = Color::from_rgb(0x000000);
|
||||
|
||||
Rect icon_rect = { rect.x(), rect.y(), icon_size(), icon_size() };
|
||||
auto icon = model.data(index, GModel::Role::Icon);
|
||||
if (icon.is_icon()) {
|
||||
if (auto* bitmap = icon.as_icon().bitmap_for_size(icon_size()))
|
||||
painter.blit(icon_rect.location(), *bitmap, bitmap->rect());
|
||||
}
|
||||
Rect text_rect = {
|
||||
icon_rect.right() + 1 + icon_spacing(), rect.y(),
|
||||
rect.width() - icon_size() - icon_spacing(), rect.height()
|
||||
};
|
||||
if (index == model.selected_index()) {
|
||||
background_color = is_focused() ? Color::from_rgb(0x84351a) : Color::from_rgb(0x606060);
|
||||
text_color = Color::from_rgb(0xffffff);
|
||||
painter.fill_rect(text_rect, background_color);
|
||||
}
|
||||
auto node_text = model.data(index, GModel::Role::Display).to_string();
|
||||
painter.draw_text(text_rect, node_text, TextAlignment::Center, text_color);
|
||||
auto index_at_indent = index;
|
||||
for (int i = indent_level; i >= 0; --i) {
|
||||
auto parent_of_index_at_indent = index_at_indent.parent();
|
||||
bool index_at_indent_is_last_in_parent = index_at_indent.row() == model.row_count(parent_of_index_at_indent) - 1;
|
||||
Point a { indent_width_in_pixels() * i - icon_size() / 2, rect.y() - 2 };
|
||||
Point b { a.x(), a.y() + item_height() - 1 };
|
||||
if (index_at_indent_is_last_in_parent)
|
||||
b.set_y(rect.center().y());
|
||||
if (!(i != indent_level && index_at_indent_is_last_in_parent))
|
||||
painter.draw_line(a, b, Color::MidGray);
|
||||
|
||||
if (i == indent_level) {
|
||||
Point c { a.x(), rect.center().y() };
|
||||
Point d { c.x() + icon_size() / 2, c.y() };
|
||||
painter.draw_line(c, d, Color::MidGray);
|
||||
}
|
||||
index_at_indent = parent_of_index_at_indent;
|
||||
}
|
||||
|
||||
if (!toggle_rect.is_empty()) {
|
||||
auto& metadata = ensure_metadata_for_index(index);
|
||||
if (metadata.open)
|
||||
painter.blit(toggle_rect.location(), *m_collapse_bitmap, m_collapse_bitmap->rect());
|
||||
else
|
||||
painter.blit(toggle_rect.location(), *m_expand_bitmap, m_expand_bitmap->rect());
|
||||
}
|
||||
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
void GTreeView::scroll_into_view(const GModelIndex& a_index, Orientation orientation)
|
||||
{
|
||||
if (!a_index.is_valid())
|
||||
return;
|
||||
Rect found_rect;
|
||||
traverse_in_paint_order([&](const GModelIndex& index, const Rect& rect, const Rect&, int) {
|
||||
if (index == a_index) {
|
||||
found_rect = rect;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
GScrollableWidget::scroll_into_view(found_rect, orientation);
|
||||
}
|
||||
|
||||
void GTreeView::did_update_model()
|
||||
{
|
||||
GAbstractView::did_update_model();
|
||||
update_content_size();
|
||||
}
|
||||
|
||||
void GTreeView::did_update_selection()
|
||||
{
|
||||
ASSERT(model());
|
||||
auto& model = *this->model();
|
||||
auto index = model.selected_index();
|
||||
if (!index.is_valid())
|
||||
return;
|
||||
bool opened_any = false;
|
||||
auto& metadata_for_index = ensure_metadata_for_index(index);
|
||||
if (!metadata_for_index.open) {
|
||||
opened_any = true;
|
||||
metadata_for_index.open = true;
|
||||
}
|
||||
auto ancestor = index.parent();
|
||||
while (ancestor.is_valid()) {
|
||||
auto& metadata_for_ancestor = ensure_metadata_for_index(ancestor);
|
||||
if (!metadata_for_ancestor.open) {
|
||||
metadata_for_ancestor.open = true;
|
||||
opened_any = true;
|
||||
}
|
||||
ancestor = ancestor.parent();
|
||||
}
|
||||
if (opened_any)
|
||||
update_content_size();
|
||||
update();
|
||||
if (activates_on_selection())
|
||||
activate(index);
|
||||
}
|
||||
|
||||
void GTreeView::update_content_size()
|
||||
{
|
||||
int height = 0;
|
||||
int width = 0;
|
||||
traverse_in_paint_order([&](const GModelIndex&, const Rect& rect, const Rect&, int) {
|
||||
width = max(width, rect.right());
|
||||
height += rect.height();
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
set_content_size({ width, height });
|
||||
}
|
||||
|
||||
void GTreeView::keydown_event(GKeyEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return;
|
||||
auto cursor_index = model()->selected_index();
|
||||
if (event.key() == KeyCode::Key_Up) {
|
||||
GModelIndex previous_index;
|
||||
GModelIndex found_index;
|
||||
traverse_in_paint_order([&](const GModelIndex& index, const Rect&, const Rect&, int) {
|
||||
if (index == cursor_index) {
|
||||
found_index = previous_index;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
previous_index = index;
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
if (found_index.is_valid()) {
|
||||
model()->set_selected_index(found_index);
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Down) {
|
||||
GModelIndex previous_index;
|
||||
GModelIndex found_index;
|
||||
traverse_in_paint_order([&](const GModelIndex& index, const Rect&, const Rect&, int) {
|
||||
if (previous_index == cursor_index) {
|
||||
found_index = index;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
previous_index = index;
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
if (found_index.is_valid())
|
||||
model()->set_selected_index(found_index);
|
||||
return;
|
||||
}
|
||||
}
|
42
Libraries/LibGUI/GTreeView.h
Normal file
42
Libraries/LibGUI/GTreeView.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <LibGUI/GAbstractView.h>
|
||||
|
||||
class GTreeView : public GAbstractView {
|
||||
public:
|
||||
explicit GTreeView(GWidget*);
|
||||
virtual ~GTreeView() override;
|
||||
|
||||
virtual void scroll_into_view(const GModelIndex&, Orientation);
|
||||
virtual const char* class_name() const override { return "GTreeView"; }
|
||||
|
||||
protected:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
virtual void keydown_event(GKeyEvent&) override;
|
||||
virtual void did_update_selection() override;
|
||||
virtual void did_update_model() override;
|
||||
|
||||
private:
|
||||
GModelIndex index_at_content_position(const Point&, bool& is_toggle) const;
|
||||
int item_height() const { return 16; }
|
||||
int max_item_width() const { return frame_inner_rect().width(); }
|
||||
int indent_width_in_pixels() const { return 16; }
|
||||
int icon_size() const { return 16; }
|
||||
int icon_spacing() const { return 2; }
|
||||
int toggle_size() const { return 9; }
|
||||
int text_padding() const { return 2; }
|
||||
void update_content_size();
|
||||
|
||||
template<typename Callback>
|
||||
void traverse_in_paint_order(Callback) const;
|
||||
|
||||
struct MetadataForIndex;
|
||||
|
||||
MetadataForIndex& ensure_metadata_for_index(const GModelIndex&) const;
|
||||
|
||||
mutable HashMap<void*, OwnPtr<MetadataForIndex>> m_view_metadata;
|
||||
|
||||
RefPtr<GraphicsBitmap> m_expand_bitmap;
|
||||
RefPtr<GraphicsBitmap> m_collapse_bitmap;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue