Serenity Operating System
at master 310 lines 9.5 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <LibGUI/Button.h> 9#include <LibGUI/ComboBox.h> 10#include <LibGUI/Desktop.h> 11#include <LibGUI/Event.h> 12#include <LibGUI/ListView.h> 13#include <LibGUI/Model.h> 14#include <LibGUI/Scrollbar.h> 15#include <LibGUI/TextBox.h> 16#include <LibGUI/TextEditor.h> 17#include <LibGUI/Window.h> 18 19REGISTER_WIDGET(GUI, ComboBox) 20 21namespace GUI { 22 23class ComboBoxEditor final : public TextEditor { 24 C_OBJECT(ComboBoxEditor); 25 26public: 27 Function<void(int delta)> on_mousewheel; 28 Function<void(KeyEvent& event)> on_keypress; 29 30private: 31 ComboBoxEditor() 32 : TextEditor(TextEditor::SingleLine) 33 { 34 } 35 36 virtual void mousewheel_event(MouseEvent& event) override 37 { 38 if (!is_focused()) 39 set_focus(true); 40 if (on_mousewheel) 41 on_mousewheel(event.wheel_delta_y()); 42 event.accept(); 43 } 44 45 virtual void keydown_event(KeyEvent& event) override 46 { 47 if (event.key() == Key_Escape) { 48 if (is_focused()) 49 set_focus(false); 50 event.accept(); 51 } else { 52 on_keypress(event); 53 TextEditor::keydown_event(event); 54 } 55 } 56}; 57 58ComboBox::ComboBox() 59{ 60 REGISTER_STRING_PROPERTY("placeholder", editor_placeholder, set_editor_placeholder); 61 REGISTER_BOOL_PROPERTY("model_only", only_allow_values_from_model, set_only_allow_values_from_model); 62 REGISTER_INT_PROPERTY("max_visible_items", max_visible_items, set_max_visible_items); 63 64 set_min_size({ 40, 22 }); 65 set_preferred_size({ SpecialDimension::OpportunisticGrow, 22 }); 66 67 m_editor = add<ComboBoxEditor>(); 68 m_editor->set_frame_thickness(0); 69 m_editor->on_return_pressed = [this] { 70 if (on_return_pressed) 71 on_return_pressed(); 72 }; 73 m_editor->on_up_pressed = [this] { 74 navigate(AbstractView::CursorMovement::Up); 75 }; 76 m_editor->on_down_pressed = [this] { 77 navigate(AbstractView::CursorMovement::Down); 78 }; 79 m_editor->on_pageup_pressed = [this] { 80 navigate(AbstractView::CursorMovement::PageUp); 81 }; 82 m_editor->on_pagedown_pressed = [this] { 83 navigate(AbstractView::CursorMovement::PageDown); 84 }; 85 m_editor->on_mousewheel = [this](int delta) { 86 // Since we can only show one item at a time we don't want to 87 // skip any. So just move one item at a time. 88 navigate_relative(delta > 0 ? 1 : -1); 89 }; 90 m_editor->on_mousedown = [this] { 91 if (only_allow_values_from_model()) 92 m_open_button->click(); 93 }; 94 m_editor->on_keypress = [this](KeyEvent& event) { 95 if (!m_only_allow_values_from_model) 96 return; 97 if (!m_list_window->is_visible() && event.key() <= Key_Z && event.key() >= Key_A && event.modifiers() == Mod_None) { 98 open(); 99 m_list_window->event(event); 100 } 101 }; 102 103 m_open_button = add<Button>(); 104 m_open_button->set_button_style(Gfx::ButtonStyle::ThickCap); 105 m_open_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/downward-triangle.png"sv).release_value_but_fixme_should_propagate_errors()); 106 m_open_button->set_focus_policy(GUI::FocusPolicy::NoFocus); 107 m_open_button->on_click = [this](auto) { 108 if (!m_list_view->item_count()) 109 return; 110 if (m_list_window->is_visible()) 111 close(); 112 else 113 open(); 114 }; 115 116 m_list_window = add<Window>(window()); 117 m_list_window->set_window_type(GUI::WindowType::Popup); 118 119 m_list_view = m_list_window->set_main_widget<ListView>().release_value_but_fixme_should_propagate_errors(); 120 m_list_view->set_should_hide_unnecessary_scrollbars(true); 121 m_list_view->set_alternating_row_colors(false); 122 m_list_view->set_hover_highlighting(true); 123 m_list_view->set_frame_thickness(1); 124 m_list_view->set_frame_shadow(Gfx::FrameShadow::Plain); 125 m_list_view->set_activates_on_selection(true); 126 m_list_view->on_selection_change = [this] { 127 VERIFY(model()); 128 const auto& index = m_list_view->selection().first(); 129 if (m_updating_model) 130 selection_updated(index); 131 }; 132 133 m_list_view->on_activation = [this](auto& index) { 134 deferred_invoke([this, index] { 135 selection_updated(index); 136 if (on_change) 137 on_change(m_editor->text(), index); 138 }); 139 close(); 140 }; 141 142 m_list_view->on_escape_pressed = [this] { 143 close(); 144 }; 145} 146 147ComboBox::~ComboBox() = default; 148 149void ComboBox::set_editor_placeholder(StringView placeholder) 150{ 151 m_editor->set_placeholder(placeholder); 152} 153 154DeprecatedString const& ComboBox::editor_placeholder() const 155{ 156 return m_editor->placeholder(); 157} 158 159void ComboBox::navigate(AbstractView::CursorMovement cursor_movement) 160{ 161 auto previous_selected = m_list_view->cursor_index(); 162 m_list_view->move_cursor(cursor_movement, AbstractView::SelectionUpdate::Set); 163 auto current_selected = m_list_view->cursor_index(); 164 selection_updated(current_selected); 165 if (previous_selected.row() != current_selected.row() && on_change) 166 on_change(m_editor->text(), current_selected); 167} 168 169void ComboBox::navigate_relative(int delta) 170{ 171 auto previous_selected = m_list_view->cursor_index(); 172 m_list_view->move_cursor_relative(delta, AbstractView::SelectionUpdate::Set); 173 auto current_selected = m_list_view->cursor_index(); 174 selection_updated(current_selected); 175 if (previous_selected.row() != current_selected.row() && on_change) 176 on_change(m_editor->text(), current_selected); 177} 178 179void ComboBox::selection_updated(ModelIndex const& index) 180{ 181 if (index.is_valid()) 182 m_selected_index = index; 183 else 184 m_selected_index.clear(); 185 auto new_value = index.data().to_deprecated_string(); 186 m_editor->set_text(new_value); 187 if (!m_only_allow_values_from_model) 188 m_editor->select_all(); 189} 190 191void ComboBox::resize_event(ResizeEvent& event) 192{ 193 Widget::resize_event(event); 194 int button_height = event.size().height() - frame_thickness() * 2; 195 int button_width = 15; 196 m_open_button->set_relative_rect(width() - button_width - frame_thickness(), frame_thickness(), button_width, button_height); 197 auto editor_rect = frame_inner_rect(); 198 editor_rect.set_width(editor_rect.width() - button_width); 199 m_editor->set_relative_rect(editor_rect); 200} 201 202void ComboBox::set_model(NonnullRefPtr<Model> model) 203{ 204 TemporaryChange change(m_updating_model, true); 205 m_selected_index.clear(); 206 m_list_view->set_model(move(model)); 207} 208 209void ComboBox::clear_selection() 210{ 211 m_selected_index.clear(); 212 m_editor->clear_selection(); 213 m_editor->clear(); 214} 215 216void ComboBox::set_selected_index(size_t index, AllowCallback allow_callback) 217{ 218 if (!m_list_view->model()) 219 return; 220 size_t previous_index = selected_index(); 221 TemporaryChange change(m_updating_model, true); 222 auto model_index = m_list_view->model()->index(index, 0); 223 m_list_view->set_cursor(model_index, AbstractView::SelectionUpdate::Set); 224 selection_updated(model_index); 225 if (previous_index != selected_index() && on_change && allow_callback == AllowCallback::Yes) 226 on_change(m_editor->text(), m_list_view->cursor_index()); 227} 228 229size_t ComboBox::selected_index() const 230{ 231 return m_selected_index.has_value() ? m_selected_index.value().row() : 0; 232} 233 234void ComboBox::select_all() 235{ 236 m_editor->select_all(); 237} 238 239void ComboBox::open() 240{ 241 m_editor->set_focus(true); 242 243 // Force content size update while invisible 244 m_list_view->resize({}); 245 246 auto frame = m_list_view->frame_thickness() * 2; 247 auto max_height = min(m_list_view->item_height() * m_max_visible_items, m_list_view->content_height()); 248 Gfx::IntSize size { max(width(), m_list_view->content_width() + frame), max_height + frame }; 249 Gfx::IntRect rect { screen_relative_rect().bottom_left(), size }; 250 251 auto desktop = Desktop::the().rect().shrunken(0, 0, Desktop::the().taskbar_height(), 0); 252 auto min_height = 5 * m_list_view->item_height() + frame; 253 auto go_upwards_instead = rect.bottom() >= desktop.height() && rect.intersected(desktop).height() < min_height; 254 if (go_upwards_instead) { 255 auto origin = screen_relative_rect().top_left(); 256 rect = { Gfx::IntPoint { origin.x(), origin.y() - size.height() }, size }; 257 } 258 rect.intersect(desktop); 259 m_list_window->set_rect(rect); 260 261 if (m_selected_index.has_value()) 262 m_list_view->set_cursor(m_selected_index.value(), AbstractView::SelectionUpdate::Set); 263 264 m_list_window->show(); 265} 266 267void ComboBox::close() 268{ 269 m_list_window->hide(); 270} 271 272DeprecatedString ComboBox::text() const 273{ 274 return m_editor->text(); 275} 276 277void ComboBox::set_text(DeprecatedString const& text, AllowCallback allow_callback) 278{ 279 m_editor->set_text(text, allow_callback); 280} 281 282void ComboBox::set_only_allow_values_from_model(bool b) 283{ 284 if (m_only_allow_values_from_model == b) 285 return; 286 m_only_allow_values_from_model = b; 287 m_editor->set_mode(m_only_allow_values_from_model ? TextEditor::DisplayOnly : TextEditor::Editable); 288} 289 290Model* ComboBox::model() 291{ 292 return m_list_view->model(); 293} 294 295Model const* ComboBox::model() const 296{ 297 return m_list_view->model(); 298} 299 300int ComboBox::model_column() const 301{ 302 return m_list_view->model_column(); 303} 304 305void ComboBox::set_model_column(int column) 306{ 307 m_list_view->set_model_column(column); 308} 309 310}