LibGUI: Add hover highlighting and keyboard controls to ComboBox
Adds a new highlighting effect to the actively selected row in ComboBox ListView. ComboBoxEditor can now be controlled with page up, page down, and the up and down arrow keys. ESC and loss of focus now cause comboboxes to close. Now activates on mouseup as well as return.
This commit is contained in:
parent
b2783a234a
commit
6a78db07f1
Notes:
sideshowbarker
2024-07-19 04:48:58 +09:00
Author: https://github.com/thankyouverycool Commit: https://github.com/SerenityOS/serenity/commit/6a78db07f11 Pull-request: https://github.com/SerenityOS/serenity/pull/2794 Reviewed-by: https://github.com/awesomekling
7 changed files with 142 additions and 15 deletions
|
@ -225,6 +225,16 @@ void AbstractView::set_hovered_index(const ModelIndex& index)
|
|||
if (m_hovered_index == index)
|
||||
return;
|
||||
m_hovered_index = index;
|
||||
if (m_hovered_index.is_valid())
|
||||
m_last_valid_hovered_index = m_hovered_index;
|
||||
update();
|
||||
}
|
||||
|
||||
void AbstractView::set_last_valid_hovered_index(const ModelIndex& index)
|
||||
{
|
||||
if (m_last_valid_hovered_index == index)
|
||||
return;
|
||||
m_last_valid_hovered_index = index;
|
||||
update();
|
||||
}
|
||||
|
||||
|
@ -325,6 +335,9 @@ void AbstractView::mouseup_event(MouseEvent& event)
|
|||
m_might_drag = false;
|
||||
update();
|
||||
}
|
||||
|
||||
if (activates_on_selection())
|
||||
activate_selected();
|
||||
}
|
||||
|
||||
void AbstractView::doubleclick_event(MouseEvent& event)
|
||||
|
|
|
@ -74,6 +74,8 @@ public:
|
|||
|
||||
NonnullRefPtr<Gfx::Font> font_for_index(const ModelIndex&) const;
|
||||
|
||||
void set_last_valid_hovered_index(const ModelIndex&);
|
||||
|
||||
protected:
|
||||
AbstractView();
|
||||
virtual ~AbstractView() override;
|
||||
|
@ -107,6 +109,7 @@ protected:
|
|||
bool m_might_drag { false };
|
||||
|
||||
ModelIndex m_hovered_index;
|
||||
ModelIndex m_last_valid_hovered_index;
|
||||
|
||||
private:
|
||||
RefPtr<Model> m_model;
|
||||
|
|
|
@ -49,6 +49,8 @@ private:
|
|||
|
||||
virtual void mousewheel_event(MouseEvent& event) override
|
||||
{
|
||||
if (!is_focused())
|
||||
set_focus(true);
|
||||
if (on_mousewheel)
|
||||
on_mousewheel(event.wheel_delta());
|
||||
}
|
||||
|
@ -57,6 +59,7 @@ private:
|
|||
ComboBox::ComboBox()
|
||||
{
|
||||
m_editor = add<ComboBoxEditor>();
|
||||
m_editor->set_has_open_button(true);
|
||||
m_editor->on_change = [this] {
|
||||
if (on_change)
|
||||
on_change(m_editor->text(), m_list_view->selection().first());
|
||||
|
@ -65,6 +68,27 @@ ComboBox::ComboBox()
|
|||
if (on_return_pressed)
|
||||
on_return_pressed();
|
||||
};
|
||||
m_editor->on_up_pressed = [this] {
|
||||
m_list_view->move_selection(-1);
|
||||
};
|
||||
m_editor->on_down_pressed = [this] {
|
||||
m_list_view->move_selection(1);
|
||||
};
|
||||
m_editor->on_pageup_pressed = [this] {
|
||||
m_list_view->move_selection(-m_list_view->selection().first().row());
|
||||
};
|
||||
m_editor->on_pagedown_pressed = [this] {
|
||||
if (model())
|
||||
m_list_view->move_selection((model()->row_count() - 1) - m_list_view->selection().first().row());
|
||||
};
|
||||
m_editor->on_mousewheel = [this](int delta) {
|
||||
m_list_view->move_selection(delta);
|
||||
};
|
||||
m_editor->on_mousedown = [this] {
|
||||
if (only_allow_values_from_model())
|
||||
m_open_button->click();
|
||||
};
|
||||
|
||||
m_open_button = add<Button>();
|
||||
m_open_button->set_focusable(false);
|
||||
m_open_button->set_text("\xE2\xAC\x87"); // DOWNWARDS BLACK ARROW
|
||||
|
@ -77,25 +101,38 @@ ComboBox::ComboBox()
|
|||
|
||||
m_list_window = add<Window>();
|
||||
m_list_window->set_frameless(true);
|
||||
m_list_window->on_activity_change = [this](const bool is_active) {
|
||||
if (!is_active) {
|
||||
m_open_button->set_enabled(false);
|
||||
close();
|
||||
}
|
||||
m_open_button->set_enabled(true);
|
||||
};
|
||||
|
||||
m_list_view = m_list_window->set_main_widget<ListView>();
|
||||
m_list_view->horizontal_scrollbar().set_visible(false);
|
||||
|
||||
m_list_view->set_alternating_row_colors(false);
|
||||
m_list_view->set_hover_highlighting(true);
|
||||
m_list_view->set_frame_thickness(1);
|
||||
m_list_view->set_frame_shadow(Gfx::FrameShadow::Plain);
|
||||
m_list_view->on_selection = [this](auto& index) {
|
||||
ASSERT(model());
|
||||
m_list_view->set_activates_on_selection(true);
|
||||
auto new_value = model()->data(index).to_string();
|
||||
m_editor->set_text(new_value);
|
||||
if (!m_only_allow_values_from_model)
|
||||
m_editor->select_all();
|
||||
close();
|
||||
deferred_invoke([this, index](auto&) {
|
||||
if (on_change)
|
||||
on_change(m_editor->text(), index);
|
||||
});
|
||||
};
|
||||
|
||||
m_editor->on_mousewheel = [this](int delta) {
|
||||
m_list_view->move_selection(delta);
|
||||
m_list_view->on_activation = [this](auto&) {
|
||||
m_list_view->set_activates_on_selection(false);
|
||||
close();
|
||||
};
|
||||
m_list_view->on_escape_pressed = [this] {
|
||||
close();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -152,6 +189,11 @@ void ComboBox::open()
|
|||
Gfx::IntRect list_window_rect { my_screen_rect.bottom_left(), size };
|
||||
list_window_rect.intersect(Desktop::the().rect().shrunken(0, 128));
|
||||
|
||||
if (m_list_view->hover_highlighting())
|
||||
m_list_view->set_last_valid_hovered_index({});
|
||||
|
||||
m_editor->set_has_visible_list(true);
|
||||
m_editor->set_focus(true);
|
||||
m_list_window->set_rect(list_window_rect);
|
||||
m_list_window->show();
|
||||
}
|
||||
|
@ -159,6 +201,7 @@ void ComboBox::open()
|
|||
void ComboBox::close()
|
||||
{
|
||||
m_list_window->hide();
|
||||
m_editor->set_has_visible_list(false);
|
||||
m_editor->set_focus(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,12 @@ void ListView::paint_event(PaintEvent& event)
|
|||
int painted_item_index = 0;
|
||||
|
||||
for (int row_index = 0; row_index < model()->row_count(); ++row_index) {
|
||||
bool is_selected_row = selection().contains_row(row_index);
|
||||
bool is_selected_row;
|
||||
if (hover_highlighting() && m_last_valid_hovered_index.is_valid())
|
||||
is_selected_row = row_index == m_last_valid_hovered_index.row();
|
||||
else
|
||||
is_selected_row = selection().contains_row(row_index);
|
||||
|
||||
int y = painted_item_index * item_height();
|
||||
|
||||
Color background_color;
|
||||
|
@ -187,15 +192,29 @@ void ListView::move_selection(int steps)
|
|||
auto& model = *this->model();
|
||||
ModelIndex new_index;
|
||||
if (!selection().is_empty()) {
|
||||
auto old_index = selection().first();
|
||||
new_index = model.index(old_index.row() + steps, old_index.column());
|
||||
if (hover_highlighting() && m_last_valid_hovered_index.is_valid()) {
|
||||
new_index = model.index(m_last_valid_hovered_index.row() + steps, m_last_valid_hovered_index.column());
|
||||
} else {
|
||||
auto old_index = selection().first();
|
||||
new_index = model.index(old_index.row() + steps, old_index.column());
|
||||
}
|
||||
} else {
|
||||
new_index = model.index(0, 0);
|
||||
if (hover_highlighting() && m_last_valid_hovered_index.is_valid()) {
|
||||
new_index = model.index(m_last_valid_hovered_index.row() + steps, m_last_valid_hovered_index.column());
|
||||
} else {
|
||||
new_index = model.index(0, 0);
|
||||
}
|
||||
}
|
||||
if (model.is_valid(new_index)) {
|
||||
set_last_valid_hovered_index({});
|
||||
selection().set(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
update();
|
||||
} else {
|
||||
if (hover_highlighting() && m_last_valid_hovered_index.is_valid()) {
|
||||
new_index = model.index(m_last_valid_hovered_index.row(), m_last_valid_hovered_index.column());
|
||||
selection().set(new_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,7 +223,12 @@ void ListView::keydown_event(KeyEvent& event)
|
|||
if (!model())
|
||||
return;
|
||||
auto& model = *this->model();
|
||||
ModelIndex new_index;
|
||||
if (event.key() == KeyCode::Key_Return) {
|
||||
if (hover_highlighting() && m_last_valid_hovered_index.is_valid()) {
|
||||
auto new_index = model.index(m_last_valid_hovered_index.row(), m_last_valid_hovered_index.column());
|
||||
selection().set(new_index);
|
||||
}
|
||||
activate_selected();
|
||||
return;
|
||||
}
|
||||
|
@ -217,9 +241,15 @@ void ListView::keydown_event(KeyEvent& event)
|
|||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageUp) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto old_index = selection().first();
|
||||
auto new_index = model.index(max(0, old_index.row() - items_per_page), old_index.column());
|
||||
if (hover_highlighting())
|
||||
set_last_valid_hovered_index({});
|
||||
if (!selection().is_empty()) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto old_index = selection().first();
|
||||
new_index = model.index(max(0, old_index.row() - items_per_page), old_index.column());
|
||||
} else {
|
||||
new_index = model.index(0, 0);
|
||||
}
|
||||
if (model.is_valid(new_index)) {
|
||||
selection().set(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
|
@ -228,9 +258,15 @@ void ListView::keydown_event(KeyEvent& event)
|
|||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_PageDown) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto old_index = selection().first();
|
||||
auto new_index = model.index(min(model.row_count() - 1, old_index.row() + items_per_page), old_index.column());
|
||||
if (hover_highlighting())
|
||||
set_last_valid_hovered_index({});
|
||||
if (!selection().is_empty()) {
|
||||
int items_per_page = visible_content_rect().height() / item_height();
|
||||
auto old_index = selection().first();
|
||||
new_index = model.index(min(model.row_count() - 1, old_index.row() + items_per_page), old_index.column());
|
||||
} else {
|
||||
new_index = model.index(0, 0);
|
||||
}
|
||||
if (model.is_valid(new_index)) {
|
||||
selection().set(new_index);
|
||||
scroll_into_view(new_index, Orientation::Vertical);
|
||||
|
@ -238,6 +274,11 @@ void ListView::keydown_event(KeyEvent& event)
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Escape) {
|
||||
if (on_escape_pressed)
|
||||
on_escape_pressed();
|
||||
return;
|
||||
}
|
||||
return Widget::keydown_event(event);
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,9 @@ public:
|
|||
bool alternating_row_colors() const { return m_alternating_row_colors; }
|
||||
void set_alternating_row_colors(bool b) { m_alternating_row_colors = b; }
|
||||
|
||||
bool hover_highlighting() const { return m_hover_highlighting; }
|
||||
void set_hover_highlighting(bool b) { m_hover_highlighting = b; }
|
||||
|
||||
int horizontal_padding() const { return m_horizontal_padding; }
|
||||
|
||||
void scroll_into_view(const ModelIndex&, Orientation);
|
||||
|
@ -56,6 +59,8 @@ public:
|
|||
|
||||
void move_selection(int steps);
|
||||
|
||||
Function<void()> on_escape_pressed;
|
||||
|
||||
private:
|
||||
ListView();
|
||||
|
||||
|
@ -72,6 +77,7 @@ private:
|
|||
int m_horizontal_padding { 2 };
|
||||
int m_model_column { 0 };
|
||||
bool m_alternating_row_colors { true };
|
||||
bool m_hover_highlighting { false };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -700,6 +700,10 @@ void TextEditor::keydown_event(KeyEvent& event)
|
|||
}
|
||||
}
|
||||
return;
|
||||
} else if (event.key() == KeyCode::Key_Up) {
|
||||
if (on_up_pressed)
|
||||
on_up_pressed();
|
||||
return;
|
||||
}
|
||||
if (is_multi_line() && event.key() == KeyCode::Key_Down) {
|
||||
if (m_cursor.line() < (line_count() - 1)) {
|
||||
|
@ -717,6 +721,10 @@ void TextEditor::keydown_event(KeyEvent& event)
|
|||
}
|
||||
}
|
||||
return;
|
||||
} else if (event.key() == KeyCode::Key_Down) {
|
||||
if (on_down_pressed)
|
||||
on_down_pressed();
|
||||
return;
|
||||
}
|
||||
if (is_multi_line() && event.key() == KeyCode::Key_PageUp) {
|
||||
if (m_cursor.line() > 0) {
|
||||
|
@ -731,6 +739,10 @@ void TextEditor::keydown_event(KeyEvent& event)
|
|||
}
|
||||
}
|
||||
return;
|
||||
} else if (event.key() == KeyCode::Key_PageUp) {
|
||||
if (on_pageup_pressed)
|
||||
on_pageup_pressed();
|
||||
return;
|
||||
}
|
||||
if (is_multi_line() && event.key() == KeyCode::Key_PageDown) {
|
||||
if (m_cursor.line() < (line_count() - 1)) {
|
||||
|
@ -744,6 +756,10 @@ void TextEditor::keydown_event(KeyEvent& event)
|
|||
}
|
||||
}
|
||||
return;
|
||||
} else if (event.key() == KeyCode::Key_PageDown) {
|
||||
if (on_pagedown_pressed)
|
||||
on_pagedown_pressed();
|
||||
return;
|
||||
}
|
||||
if (event.key() == KeyCode::Key_Left) {
|
||||
if (event.ctrl()) {
|
||||
|
|
|
@ -127,8 +127,13 @@ public:
|
|||
void redo() { document().redo(); }
|
||||
|
||||
Function<void()> on_change;
|
||||
Function<void()> on_mousedown;
|
||||
Function<void()> on_return_pressed;
|
||||
Function<void()> on_escape_pressed;
|
||||
Function<void()> on_up_pressed;
|
||||
Function<void()> on_down_pressed;
|
||||
Function<void()> on_pageup_pressed;
|
||||
Function<void()> on_pagedown_pressed;
|
||||
|
||||
Action& undo_action() { return *m_undo_action; }
|
||||
Action& redo_action() { return *m_redo_action; }
|
||||
|
|
Loading…
Add table
Reference in a new issue