mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 15:12:45 +00:00 
			
		
		
		
	LibGUI: Move most of mouse event handling to GAbstractView
This deduplicates existing code, and also gives all the model-backed widgets selection manipulation and drag support for free :^)
This commit is contained in:
		
							parent
							
								
									fd11c96e8e
								
							
						
					
					
						commit
						d3ce7ae0e3
					
				
					 9 changed files with 197 additions and 185 deletions
				
			
		|  | @ -251,7 +251,7 @@ int GAbstractColumnView::column_width(int column_index) const | |||
| void GAbstractColumnView::mousemove_event(GMouseEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
|         return GAbstractView::mousemove_event(event); | ||||
| 
 | ||||
|     if (m_in_column_resize) { | ||||
|         auto delta = event.position() - m_column_resize_origin; | ||||
|  | @ -301,6 +301,8 @@ void GAbstractColumnView::mousemove_event(GMouseEvent& event) | |||
|             set_hovered_header_index(-1); | ||||
|     } | ||||
|     window()->set_override_cursor(GStandardCursor::None); | ||||
| 
 | ||||
|     GAbstractView::mousemove_event(event); | ||||
| } | ||||
| 
 | ||||
| void GAbstractColumnView::mouseup_event(GMouseEvent& event) | ||||
|  | @ -311,6 +313,7 @@ void GAbstractColumnView::mouseup_event(GMouseEvent& event) | |||
|             if (!column_resize_grabbable_rect(m_resizing_column).contains(adjusted_position)) | ||||
|                 window()->set_override_cursor(GStandardCursor::None); | ||||
|             m_in_column_resize = false; | ||||
|             return; | ||||
|         } | ||||
|         if (m_pressed_column_header_index != -1) { | ||||
|             auto header_rect = this->header_rect(m_pressed_column_header_index); | ||||
|  | @ -325,17 +328,20 @@ void GAbstractColumnView::mouseup_event(GMouseEvent& event) | |||
|             m_pressed_column_header_index = -1; | ||||
|             m_pressed_column_header_is_pressed = false; | ||||
|             update_headers(); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     GAbstractView::mouseup_event(event); | ||||
| } | ||||
| 
 | ||||
| void GAbstractColumnView::mousedown_event(GMouseEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
|         return GAbstractView::mousedown_event(event); | ||||
| 
 | ||||
|     if (event.button() != GMouseButton::Left) | ||||
|         return; | ||||
|         return GAbstractView::mousedown_event(event); | ||||
| 
 | ||||
|     if (event.y() < header_height()) { | ||||
|         int column_count = model()->column_count(); | ||||
|  | @ -361,19 +367,13 @@ void GAbstractColumnView::mousedown_event(GMouseEvent& event) | |||
| 
 | ||||
|     bool is_toggle; | ||||
|     auto index = index_at_event_position(event.position(), is_toggle); | ||||
|     if (!index.is_valid()) { | ||||
|         selection().clear(); | ||||
|         return; | ||||
|     } | ||||
|     if (is_toggle && model()->row_count(index)) { | ||||
| 
 | ||||
|     if (index.is_valid() && is_toggle && model()->row_count(index)) { | ||||
|         toggle_index(index); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (event.modifiers() & Mod_Ctrl) | ||||
|         selection().toggle(index); | ||||
|     else | ||||
|         selection().set(index); | ||||
|     GAbstractView::mousedown_event(event); | ||||
| } | ||||
| 
 | ||||
| GModelIndex GAbstractColumnView::index_at_event_position(const Point& position, bool& is_toggle) const | ||||
|  |  | |||
|  | @ -24,8 +24,10 @@ | |||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #include <AK/StringBuilder.h> | ||||
| #include <Kernel/KeyCode.h> | ||||
| #include <LibGUI/GAbstractView.h> | ||||
| #include <LibGUI/GDragOperation.h> | ||||
| #include <LibGUI/GModel.h> | ||||
| #include <LibGUI/GModelEditingDelegate.h> | ||||
| #include <LibGUI/GPainter.h> | ||||
|  | @ -171,3 +173,151 @@ NonnullRefPtr<Font> GAbstractView::font_for_index(const GModelIndex& index) cons | |||
|         return *column_metadata.font; | ||||
|     return font(); | ||||
| } | ||||
| 
 | ||||
| void GAbstractView::mousedown_event(GMouseEvent& event) | ||||
| { | ||||
|     GScrollableWidget::mousedown_event(event); | ||||
| 
 | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|     if (event.button() == GMouseButton::Left) | ||||
|         m_left_mousedown_position = event.position(); | ||||
| 
 | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     m_might_drag = false; | ||||
| 
 | ||||
|     if (!index.is_valid()) { | ||||
|         m_selection.clear(); | ||||
|     } else if (event.modifiers() & Mod_Ctrl) { | ||||
|         m_selection.toggle(index); | ||||
|     } else if (event.button() == GMouseButton::Left) { | ||||
|         // We might be starting a drag, so don't throw away other selected items yet.
 | ||||
|         m_might_drag = true; | ||||
|         m_selection.add(index); | ||||
|     } else { | ||||
|         m_selection.set(index); | ||||
|     } | ||||
| 
 | ||||
|     update(); | ||||
| } | ||||
| 
 | ||||
| void GAbstractView::mousemove_event(GMouseEvent& event) | ||||
| { | ||||
|     if (!model() || !m_might_drag) | ||||
|         return GScrollableWidget::mousemove_event(event); | ||||
| 
 | ||||
|     if (!(event.buttons() & GMouseButton::Left) || m_selection.is_empty()) { | ||||
|         m_might_drag = false; | ||||
|         return GScrollableWidget::mousemove_event(event); | ||||
|     } | ||||
| 
 | ||||
|     auto diff = event.position() - m_left_mousedown_position; | ||||
|     auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y(); | ||||
|     constexpr int drag_distance_threshold = 5; | ||||
| 
 | ||||
|     if (distance_travelled_squared <= drag_distance_threshold) | ||||
|         return GScrollableWidget::mousemove_event(event); | ||||
| 
 | ||||
|     dbg() << "Initiate drag!"; | ||||
|     auto drag_operation = GDragOperation::construct(); | ||||
| 
 | ||||
|     RefPtr<GraphicsBitmap> bitmap; | ||||
| 
 | ||||
|     StringBuilder text_builder; | ||||
|     StringBuilder data_builder; | ||||
|     bool first = true; | ||||
|     m_selection.for_each_index([&](auto& index) { | ||||
|         auto text_data = m_model->data(index); | ||||
|         if (!first) | ||||
|             text_builder.append(", "); | ||||
|         text_builder.append(text_data.to_string()); | ||||
| 
 | ||||
|         auto drag_data = m_model->data(index, GModel::Role::DragData); | ||||
|         data_builder.append(drag_data.to_string()); | ||||
|         data_builder.append('\n'); | ||||
| 
 | ||||
|         first = false; | ||||
| 
 | ||||
|         if (!bitmap) { | ||||
|             GVariant icon_data = model()->data(index, GModel::Role::Icon); | ||||
|             if (icon_data.is_icon()) | ||||
|                 bitmap = icon_data.as_icon().bitmap_for_size(32); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     drag_operation->set_text(text_builder.to_string()); | ||||
|     drag_operation->set_bitmap(bitmap); | ||||
|     drag_operation->set_data("url-list", data_builder.to_string()); | ||||
| 
 | ||||
|     auto outcome = drag_operation->exec(); | ||||
| 
 | ||||
|     switch (outcome) { | ||||
|     case GDragOperation::Outcome::Accepted: | ||||
|         dbg() << "Drag was accepted!"; | ||||
|         break; | ||||
|     case GDragOperation::Outcome::Cancelled: | ||||
|         dbg() << "Drag was cancelled!"; | ||||
|         break; | ||||
|     default: | ||||
|         ASSERT_NOT_REACHED(); | ||||
|         break; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GAbstractView::mouseup_event(GMouseEvent& event) | ||||
| { | ||||
|     GScrollableWidget::mouseup_event(event); | ||||
| 
 | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|     if (m_might_drag) { | ||||
|         // We were unsure about unselecting items other than the current one
 | ||||
|         // in mousedown_event(), because we could be seeing a start of a drag.
 | ||||
|         // Since we're here, it was not that; so fix up the selection now.
 | ||||
|         auto index = index_at_event_position(event.position()); | ||||
|         if (index.is_valid()) | ||||
|             m_selection.set(index); | ||||
|         else | ||||
|             m_selection.clear(); | ||||
|         m_might_drag = false; | ||||
|         update(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GAbstractView::doubleclick_event(GMouseEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|     if (event.button() != GMouseButton::Left) | ||||
|         return; | ||||
| 
 | ||||
|     m_might_drag = false; | ||||
| 
 | ||||
|     auto index = index_at_event_position(event.position()); | ||||
| 
 | ||||
|     if (!index.is_valid()) | ||||
|         m_selection.clear(); | ||||
|     else if (!m_selection.contains(index)) | ||||
|         m_selection.set(index); | ||||
| 
 | ||||
|     activate_selected(); | ||||
| } | ||||
| 
 | ||||
| void GAbstractView::context_menu_event(GContextMenuEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|     auto index = index_at_event_position(event.position()); | ||||
| 
 | ||||
|     if (index.is_valid()) | ||||
|         m_selection.add(index); | ||||
|     else | ||||
|         selection().clear(); | ||||
| 
 | ||||
|     if (on_context_menu_request) | ||||
|         on_context_menu_request(index, event); | ||||
| } | ||||
|  |  | |||
|  | @ -76,6 +76,12 @@ protected: | |||
|     explicit GAbstractView(GWidget* parent); | ||||
|     virtual ~GAbstractView() 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 context_menu_event(GContextMenuEvent&) override; | ||||
| 
 | ||||
|     virtual void did_scroll() override; | ||||
|     void activate(const GModelIndex&); | ||||
|     void activate_selected(); | ||||
|  | @ -86,6 +92,9 @@ protected: | |||
|     RefPtr<GWidget> m_edit_widget; | ||||
|     Rect m_edit_widget_content_rect; | ||||
| 
 | ||||
|     Point m_left_mousedown_position; | ||||
|     bool m_might_drag { false }; | ||||
| 
 | ||||
| private: | ||||
|     RefPtr<GModel> m_model; | ||||
|     OwnPtr<GModelEditingDelegate> m_editing_delegate; | ||||
|  |  | |||
|  | @ -216,6 +216,8 @@ GModelIndex GColumnsView::index_at_event_position(const Point& a_position) const | |||
| 
 | ||||
| void GColumnsView::mousedown_event(GMouseEvent& event) | ||||
| { | ||||
|     GAbstractView::mousedown_event(event); | ||||
| 
 | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|  | @ -223,15 +225,7 @@ void GColumnsView::mousedown_event(GMouseEvent& event) | |||
|         return; | ||||
| 
 | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     if (!index.is_valid()) { | ||||
|         selection().clear(); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (event.modifiers() & Mod_Ctrl) { | ||||
|         selection().toggle(index); | ||||
|     } else { | ||||
|         selection().set(index); | ||||
|     if (index.is_valid() && !(event.modifiers() & Mod_Ctrl)) { | ||||
|         if (model()->row_count(index)) | ||||
|             push_column(index); | ||||
|     } | ||||
|  | @ -250,34 +244,6 @@ void GColumnsView::did_update_model() | |||
|     update(); | ||||
| } | ||||
| 
 | ||||
| void GColumnsView::doubleclick_event(GMouseEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|     if (event.button() != GMouseButton::Left) | ||||
|         return; | ||||
| 
 | ||||
|     mousedown_event(event); | ||||
|     activate_selected(); | ||||
| } | ||||
| 
 | ||||
| void GColumnsView::context_menu_event(GContextMenuEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
| 
 | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     if (index.is_valid()) { | ||||
|         if (!selection().contains(index)) | ||||
|             selection().set(index); | ||||
|     } else { | ||||
|         selection().clear(); | ||||
|     } | ||||
|     if (on_context_menu_request) | ||||
|         on_context_menu_request(index, event); | ||||
| } | ||||
| 
 | ||||
| void GColumnsView::keydown_event(GKeyEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|  |  | |||
|  | @ -51,8 +51,6 @@ private: | |||
|     virtual void did_update_model() override; | ||||
|     virtual void paint_event(GPaintEvent&) override; | ||||
|     virtual void mousedown_event(GMouseEvent& event) override; | ||||
|     virtual void doubleclick_event(GMouseEvent& event) override; | ||||
|     virtual void context_menu_event(GContextMenuEvent& event) override; | ||||
|     virtual void keydown_event(GKeyEvent& event) override; | ||||
| 
 | ||||
|     struct Column { | ||||
|  |  | |||
|  | @ -138,11 +138,20 @@ GModelIndex GItemView::index_at_event_position(const Point& position) const | |||
| 
 | ||||
| void GItemView::mousedown_event(GMouseEvent& event) | ||||
| { | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     if (!model()) | ||||
|         return GAbstractView::mousedown_event(event); | ||||
| 
 | ||||
|     if (event.button() != GMouseButton::Left) | ||||
|         return GAbstractView::mousedown_event(event); | ||||
| 
 | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     if (index.is_valid()) { | ||||
|         // We might start dragging this item, but not rubber-banding.
 | ||||
|         return GAbstractView::mousedown_event(event); | ||||
|     } | ||||
| 
 | ||||
|     ASSERT(m_rubber_band_remembered_selection.is_empty()); | ||||
| 
 | ||||
|     if (event.button() == GMouseButton::Left) { | ||||
|         m_left_mousedown_position = event.position(); | ||||
|         if (!index.is_valid()) { | ||||
|     if (event.modifiers() & Mod_Ctrl) { | ||||
|         selection().for_each_index([&](auto& index) { | ||||
|             m_rubber_band_remembered_selection.append(index); | ||||
|  | @ -150,20 +159,11 @@ void GItemView::mousedown_event(GMouseEvent& event) | |||
|     } else { | ||||
|         selection().clear(); | ||||
|     } | ||||
| 
 | ||||
|     m_might_drag = false; | ||||
|     m_rubber_banding = true; | ||||
|     m_rubber_band_origin = event.position(); | ||||
|     m_rubber_band_current = event.position(); | ||||
|         } else { | ||||
|             if (event.modifiers() & Mod_Ctrl) | ||||
|                 selection().toggle(index); | ||||
|             else if (selection().size() > 1) | ||||
|                 m_might_drag = true; | ||||
|             else | ||||
|                 selection().set(index); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     GAbstractView::mousedown_event(event); | ||||
| } | ||||
| 
 | ||||
| void GItemView::mouseup_event(GMouseEvent& event) | ||||
|  | @ -172,14 +172,6 @@ void GItemView::mouseup_event(GMouseEvent& event) | |||
|         m_rubber_banding = false; | ||||
|         m_rubber_band_remembered_selection.clear(); | ||||
|         update(); | ||||
|         return; | ||||
|     } | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     if (index.is_valid()) { | ||||
|         if ((selection().size() > 1) & m_might_drag) { | ||||
|             selection().set(index); | ||||
|             m_might_drag = false; | ||||
|         } | ||||
|     } | ||||
|     GAbstractView::mouseup_event(event); | ||||
| } | ||||
|  | @ -207,86 +199,9 @@ void GItemView::mousemove_event(GMouseEvent& event) | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (event.buttons() & GMouseButton::Left && !selection().is_empty()) { | ||||
|         auto diff = event.position() - m_left_mousedown_position; | ||||
|         auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y(); | ||||
|         constexpr int drag_distance_threshold = 5; | ||||
|         if (distance_travelled_squared > (drag_distance_threshold)) { | ||||
|             dbg() << "Initiate drag!"; | ||||
|             auto drag_operation = GDragOperation::construct(); | ||||
| 
 | ||||
|             RefPtr<GraphicsBitmap> bitmap; | ||||
| 
 | ||||
|             StringBuilder text_builder; | ||||
|             StringBuilder data_builder; | ||||
|             int index_iterations = 0; | ||||
|             selection().for_each_index([&](auto& index) { | ||||
|                 index_iterations++; | ||||
|                 auto text_data = model()->data(index); | ||||
|                 if (index_iterations == 0) | ||||
|                     text_builder.append(" "); | ||||
|                 text_builder.append(text_data.to_string()); | ||||
|                 if (!(index_iterations == selection().size())) | ||||
|                     text_builder.append(", "); | ||||
| 
 | ||||
|                 auto drag_data = model()->data(index, GModel::Role::DragData); | ||||
|                 data_builder.append(drag_data.to_string()); | ||||
|                 data_builder.append('\n'); | ||||
| 
 | ||||
|                 if (!bitmap) { | ||||
|                     GVariant icon_data = model()->data(index, GModel::Role::Icon); | ||||
|                     if (icon_data.is_icon()) | ||||
|                         bitmap = icon_data.as_icon().bitmap_for_size(32); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             drag_operation->set_text(text_builder.to_string()); | ||||
|             drag_operation->set_bitmap(bitmap); | ||||
|             drag_operation->set_data("url-list", data_builder.to_string()); | ||||
|             auto outcome = drag_operation->exec(); | ||||
|             switch (outcome) { | ||||
|             case GDragOperation::Outcome::Accepted: | ||||
|                 dbg() << "Drag was accepted!"; | ||||
|                 break; | ||||
|             case GDragOperation::Outcome::Cancelled: | ||||
|                 dbg() << "Drag was cancelled!"; | ||||
|                 break; | ||||
|             default: | ||||
|                 ASSERT_NOT_REACHED(); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     GAbstractView::mousemove_event(event); | ||||
| } | ||||
| 
 | ||||
| void GItemView::context_menu_event(GContextMenuEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
|     auto index = index_at_event_position(event.position()); | ||||
|     if (index.is_valid()) { | ||||
|         if (!selection().contains(index)) | ||||
|             selection().set(index); | ||||
|     } else { | ||||
|         selection().clear(); | ||||
|     } | ||||
|     if (on_context_menu_request) | ||||
|         on_context_menu_request(index, event); | ||||
|     GAbstractView::context_menu_event(event); | ||||
| } | ||||
| 
 | ||||
| void GItemView::doubleclick_event(GMouseEvent& event) | ||||
| { | ||||
|     if (!model()) | ||||
|         return; | ||||
|     if (event.button() == GMouseButton::Left) { | ||||
|         mousedown_event(event); | ||||
|         activate_selected(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GItemView::get_item_rects(int item_index, const Font& font, const GVariant& item_text, Rect& item_rect, Rect& icon_rect, Rect& text_rect) const | ||||
| { | ||||
|     item_rect = this->item_rect(item_index); | ||||
|  |  | |||
|  | @ -61,8 +61,6 @@ private: | |||
|     virtual void mousemove_event(GMouseEvent&) override; | ||||
|     virtual void mouseup_event(GMouseEvent&) override; | ||||
|     virtual void keydown_event(GKeyEvent&) override; | ||||
|     virtual void doubleclick_event(GMouseEvent&) override; | ||||
|     virtual void context_menu_event(GContextMenuEvent&) override; | ||||
| 
 | ||||
|     int item_count() const; | ||||
|     Rect item_rect(int item_index) const; | ||||
|  | @ -75,10 +73,6 @@ private: | |||
|     int m_visual_column_count { 0 }; | ||||
|     int m_visual_row_count { 0 }; | ||||
| 
 | ||||
|     bool m_might_drag { false }; | ||||
| 
 | ||||
|     Point m_left_mousedown_position; | ||||
| 
 | ||||
|     Size m_effective_item_size { 80, 80 }; | ||||
| 
 | ||||
|     bool m_rubber_banding { false }; | ||||
|  |  | |||
|  | @ -102,25 +102,6 @@ Point GListView::adjusted_position(const Point& position) const | |||
|     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 index = index_at_event_position(event.position()); | ||||
|     if (index.is_valid()) { | ||||
|         if (event.modifiers() & Mod_Ctrl) | ||||
|             selection().toggle(index); | ||||
|         else | ||||
|             selection().set(index); | ||||
|     } else { | ||||
|         selection().clear(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GListView::paint_event(GPaintEvent& event) | ||||
| { | ||||
|     GFrame::paint_event(event); | ||||
|  |  | |||
|  | @ -60,7 +60,6 @@ public: | |||
| 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; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Sergey Bugaev
						Sergey Bugaev