diff --git a/LibGUI/GAbstractButton.cpp b/LibGUI/GAbstractButton.cpp index 27b0fefb32..61aaacfed3 100644 --- a/LibGUI/GAbstractButton.cpp +++ b/LibGUI/GAbstractButton.cpp @@ -31,7 +31,7 @@ void GAbstractButton::set_checked(bool checked) m_checked = checked; if (is_exclusive() && checked) { - parent_widget()->for_each_child_of_type([&] (auto& sibling) { + parent_widget()->for_each_child_of_type([&](auto& sibling) { if (!sibling.is_exclusive() || !sibling.is_checkable() || !sibling.is_checked()) return IterationDecision::Continue; sibling.m_checked = false; diff --git a/LibGUI/GAbstractButton.h b/LibGUI/GAbstractButton.h index 7ef205a209..340b0ee880 100644 --- a/LibGUI/GAbstractButton.h +++ b/LibGUI/GAbstractButton.h @@ -29,6 +29,7 @@ public: virtual void click() = 0; virtual const char* class_name() const override { return "GAbstractButton"; } virtual bool accepts_focus() const override { return true; } + virtual bool accepts_keyboard_select() const { return true; } protected: explicit GAbstractButton(GWidget* parent); diff --git a/LibGUI/GButton.cpp b/LibGUI/GButton.cpp index 6528131215..203ef664e6 100644 --- a/LibGUI/GButton.cpp +++ b/LibGUI/GButton.cpp @@ -66,6 +66,11 @@ void GButton::click() on_click(*this); } +bool GButton::accepts_keyboard_select() const +{ + return is_enabled(); +} + void GButton::set_action(GAction& action) { m_action = action.make_weak_ptr(); diff --git a/LibGUI/GButton.h b/LibGUI/GButton.h index d7284cbe0e..11eb68f9f8 100644 --- a/LibGUI/GButton.h +++ b/LibGUI/GButton.h @@ -33,6 +33,7 @@ public: virtual const char* class_name() const override { return "GButton"; } virtual bool accepts_focus() const override { return true; } + virtual bool accepts_keyboard_select() const; protected: virtual void paint_event(GPaintEvent&) override; diff --git a/LibGUI/GWidget.h b/LibGUI/GWidget.h index 67e6522809..c978f913c9 100644 --- a/LibGUI/GWidget.h +++ b/LibGUI/GWidget.h @@ -102,6 +102,7 @@ public: void update(const Rect&); virtual bool accepts_focus() const { return false; } + virtual bool accepts_keyboard_select() const { return false; } bool is_focused() const; void set_focus(bool); diff --git a/LibGUI/GWindow.cpp b/LibGUI/GWindow.cpp index 13aa292d34..cf13d8a24c 100644 --- a/LibGUI/GWindow.cpp +++ b/LibGUI/GWindow.cpp @@ -3,6 +3,7 @@ #include "GEventLoop.h" #include "GWidget.h" #include +#include #include #include #include @@ -241,6 +242,8 @@ void GWindow::event(CEvent& event) for (auto& rect : rects) m_main_widget->event(*make(rect)); + paint_keybinds(); + if (m_double_buffering_enabled) flip(rects); else if (created_new_backing_store) @@ -262,14 +265,53 @@ void GWindow::event(CEvent& event) } if (event.type() == GEvent::KeyUp || event.type() == GEvent::KeyDown) { - if (m_focused_widget) - return m_focused_widget->event(event); - if (m_main_widget) - return m_main_widget->event(event); + auto keyevent = static_cast(event); + if (keyevent.logo() && event.type() == GEvent::KeyUp) { + if (m_keybind_mode) { + m_keybind_mode = false; + } else { + m_keybind_mode = true; + find_keyboard_selectable(); + m_entered_keybind = ""; + } + update(); + return; + } + + if (m_keybind_mode) { + if (event.type() == GEvent::KeyUp) { + StringBuilder builder; + builder.append(m_entered_keybind); + builder.append(keyevent.text()); + m_entered_keybind = builder.to_string(); + + auto found_widget = m_hashed_potential_keybind_widgets.find(m_entered_keybind); + if (found_widget != m_hashed_potential_keybind_widgets.end()) { + m_keybind_mode = false; + auto event = make(GEvent::MouseDown, Point(), 0, GMouseButton::Left, 0, 0); + found_widget->value->event(*event); + event = make(GEvent::MouseUp, Point(), 0, GMouseButton::Left, 0, 0); + found_widget->value->event(*event); + } else if (m_entered_keybind.length() >= m_max_keybind_length) { + m_keybind_mode = false; + } + update(); + } + } else { + if (m_focused_widget) + return m_focused_widget->event(event); + if (m_main_widget) + return m_main_widget->event(event); + } return; } if (event.type() == GEvent::WindowBecameActive || event.type() == GEvent::WindowBecameInactive) { + if (event.type() == GEvent::WindowBecameInactive && m_keybind_mode) { + m_keybind_mode = false; + update(); + } + m_is_active = event.type() == GEvent::WindowBecameActive; if (m_main_widget) m_main_widget->event(event); @@ -307,6 +349,72 @@ void GWindow::event(CEvent& event) CObject::event(event); } +void GWindow::paint_keybinds() +{ + if (!m_keybind_mode) + return; + GPainter painter(*m_main_widget); + + for (auto& keypair : m_hashed_potential_keybind_widgets) { + auto widget = keypair.value; + bool could_be_keybind = true; + for (size_t i = 0; i < m_entered_keybind.length(); i++) { + if (keypair.key.characters()[i] != m_entered_keybind.characters()[i]) { + could_be_keybind = false; + } + } + + if (could_be_keybind) { + auto rect = Rect(widget->x() - 5, widget->y() - 5, 4 + Font::default_font().width(keypair.key), 16); + auto highlight_rect = Rect(widget->x() - 3, widget->y() - 5, 0, 16); + + painter.fill_rect(rect, Color::LightGray); + painter.draw_rect(rect, Color::Black, false); + painter.draw_text(rect, keypair.key.characters(), TextAlignment::Center, Color::Black); + painter.draw_text(highlight_rect, m_entered_keybind.characters(), TextAlignment::CenterLeft, Color::MidGray); + } + } +} + +void GWindow::find_keyboard_selectable() +{ + Vector potential_keybind_widgets; + m_hashed_potential_keybind_widgets.clear(); + find_keyboard_selectable_children(m_main_widget, potential_keybind_widgets); + + m_max_keybind_length = ceil_div(potential_keybind_widgets.size(), ('z' - 'a')); + size_t buffer_length = m_max_keybind_length + 1; + char keybind_buffer[buffer_length]; + for (size_t i = 0; i < buffer_length - 1; i++) { + keybind_buffer[i] = 'a'; + } + keybind_buffer[buffer_length - 1] = '\0'; + + for (auto& widget : potential_keybind_widgets) { + m_hashed_potential_keybind_widgets.set(String(keybind_buffer), widget); + + for (size_t i = 0; i < buffer_length - 1; i++) { + if (keybind_buffer[i] >= 'z') { + keybind_buffer[i] = 'a'; + } else { + keybind_buffer[i]++; + break; + } + } + } +} + +void GWindow::find_keyboard_selectable_children(GWidget* widget, Vector& potential_keybind_widgets) +{ + widget->for_each_child_widget([&](auto& child) { + if (child.accepts_keyboard_select()) { + potential_keybind_widgets.append(&child); + find_keyboard_selectable_children(&child, potential_keybind_widgets); + } + return IterationDecision::Continue; + }); +} + bool GWindow::is_visible() const { return false; @@ -316,6 +424,7 @@ void GWindow::update(const Rect& a_rect) { if (!m_window_id) return; + for (auto& pending_rect : m_pending_paint_event_rects) { if (pending_rect.contains(a_rect)) { #ifdef UPDATE_COALESCING_DEBUG diff --git a/LibGUI/GWindow.h b/LibGUI/GWindow.h index dddfc02474..988b81ffb1 100644 --- a/LibGUI/GWindow.h +++ b/LibGUI/GWindow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -130,6 +131,10 @@ protected: private: virtual bool is_window() const override final { return true; } + void paint_keybinds(); + + void find_keyboard_selectable(); + void find_keyboard_selectable_children(GWidget* widget, Vector& potential_keybind_widgets); Retained create_backing_bitmap(const Size&); void set_current_backing_bitmap(GraphicsBitmap&, bool flush_immediately = false); void flip(const Vector& dirty_rects); @@ -159,4 +164,8 @@ private: bool m_resizable { true }; bool m_fullscreen { false }; bool m_show_titlebar { true }; + bool m_keybind_mode { false }; + String m_entered_keybind; + size_t m_max_keybind_length { 0 }; + HashMap m_hashed_potential_keybind_widgets; };