From 72a19a87b6c1c2fd1f32e6166ac717afc3e18ea5 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 7 Jan 2026 11:37:39 +0100 Subject: [PATCH] Qt: Add mulit selection to game grid --- rpcs3/rpcs3qt/flow_layout.h | 2 + rpcs3/rpcs3qt/flow_widget.cpp | 171 +++++++++++++++++++++++++----- rpcs3/rpcs3qt/flow_widget.h | 12 ++- rpcs3/rpcs3qt/game_list_frame.cpp | 51 +++++---- rpcs3/rpcs3qt/game_list_grid.cpp | 8 +- 5 files changed, 188 insertions(+), 56 deletions(-) diff --git a/rpcs3/rpcs3qt/flow_layout.h b/rpcs3/rpcs3qt/flow_layout.h index c04fa8ab6d..ba0ec201f6 100644 --- a/rpcs3/rpcs3qt/flow_layout.h +++ b/rpcs3/rpcs3qt/flow_layout.h @@ -61,6 +61,8 @@ public: { int row{}; int col{}; + + operator bool() const { return row >= 0 && col >= 0; } }; explicit flow_layout(QWidget* parent, int margin = -1, bool dynamic_spacing = false, int hSpacing = -1, int vSpacing = -1); diff --git a/rpcs3/rpcs3qt/flow_widget.cpp b/rpcs3/rpcs3qt/flow_widget.cpp index 13754f6c66..bf00b3fa4e 100644 --- a/rpcs3/rpcs3qt/flow_widget.cpp +++ b/rpcs3/rpcs3qt/flow_widget.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "flow_widget.h" +#include #include #include #include @@ -48,14 +49,24 @@ void flow_widget::clear() m_flow_layout->clear(); } -flow_widget_item* flow_widget::selected_item() const +std::set flow_widget::selected_items() const { - if (m_selected_index >= 0 && static_cast(m_selected_index) < m_widgets.size()) + std::set items; + + for (s64 index : m_selected_items) { - return ::at32(m_widgets, m_selected_index); + if (index >= 0 && static_cast(index) < m_widgets.size()) + { + items.insert(::at32(m_widgets, index)); + + if (!m_allow_multi_selection) + { + return items; + } + } } - return nullptr; + return items; } void flow_widget::paintEvent(QPaintEvent* /*event*/) @@ -69,7 +80,7 @@ void flow_widget::paintEvent(QPaintEvent* /*event*/) s64 flow_widget::find_item(const flow_layout::position& pos) { - if (pos.row < 0 || pos.col < 0) + if (!pos) { return -1; } @@ -109,7 +120,7 @@ flow_layout::position flow_widget::find_item(flow_widget_item* item) flow_layout::position flow_widget::find_next_item(flow_layout::position current_pos, flow_navigation value) { - if (current_pos.row >= 0 && current_pos.col >= 0 && m_flow_layout->rows() > 0 && m_flow_layout->cols() > 0) + if (current_pos && m_flow_layout->rows() > 0 && m_flow_layout->cols() > 0) { switch (value) { @@ -183,54 +194,160 @@ flow_layout::position flow_widget::find_next_item(flow_layout::position current_ return current_pos; } -void flow_widget::select_item(flow_widget_item* item) +void flow_widget::select_items(const std::set& selected_items, flow_widget_item* current_item) { - const flow_layout::position selected_pos = find_item(item); - const s64 selected_index = find_item(selected_pos); + m_selected_items.clear(); - if (selected_index < 0 || static_cast(selected_index) >= items().size()) + for (flow_widget_item* item : selected_items) { - m_selected_index = -1; - return; - } + const flow_layout::position selected_pos = find_item(item); + const s64 selected_index = find_item(selected_pos); - m_selected_index = selected_index; - Q_EMIT ItemSelectionChanged(m_selected_index); + if (selected_index < 0 || static_cast(selected_index) >= items().size()) + { + continue; + } + + m_selected_items.insert(selected_index); + + if (!m_allow_multi_selection) + { + break; + } + } for (usz i = 0; i < items().size(); i++) { if (flow_widget_item* item = items().at(i)) { // We need to polish the widgets in order to re-apply any stylesheet changes for the selected property. - item->selected = m_selected_index >= 0 && i == static_cast(m_selected_index); + item->selected = m_selected_items.contains(i); item->polish_style(); } } - // Make sure we see the focused widget - m_scroll_area->ensureWidgetVisible(::at32(items(), m_selected_index)); + if (m_selected_items.empty()) + { + m_last_selected_item = -1; + } + else + { + if (!m_selected_items.contains(m_last_selected_item)) + { + m_last_selected_item = *m_selected_items.cbegin(); + } + + s64 selected_item = m_last_selected_item; + s64 focused_item = selected_item; + + if (current_item) + { + if (const flow_layout::position selected_pos = find_item(current_item)) + { + focused_item = find_item(selected_pos); + + if (current_item->selected) + { + selected_item = focused_item; + } + } + } + + // Make sure we see the focused widget + m_scroll_area->ensureWidgetVisible(::at32(items(), focused_item)); + + Q_EMIT ItemSelectionChanged(selected_item); + } +} + +void flow_widget::update_selection(flow_widget_item* current_item) +{ + std::set selected_items; + const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); + + if (current_item) + { + if (!m_allow_multi_selection || !current_item->selected || modifiers != Qt::ControlModifier) + { + selected_items.insert(current_item); + } + } + + if (m_allow_multi_selection) + { + flow_layout::position selected_pos; + flow_layout::position last_selected_pos; + + if (modifiers == Qt::ShiftModifier && m_last_selected_item >= 0 && static_cast(m_last_selected_item) < items().size()) + { + selected_pos = find_item(current_item); + last_selected_pos = find_item(::at32(m_widgets, m_last_selected_item)); + } + + flow_layout::position pos_min = last_selected_pos; + flow_layout::position pos_max = selected_pos; + + if (pos_min.row > pos_max.row) + { + std::swap(pos_min, pos_max); + } + + // Check if the item is between the last and the current selection + const auto item_between = [this, &pos_min, &pos_max](flow_widget_item* item) + { + if (!pos_min || !pos_max) return false; + + const flow_layout::position pos = find_item(item); + if (!pos) return false; // pos invalid + if (pos.row < pos_min.row || pos.row > pos_max.row) return false; // not in any relevant row + if (pos.row > pos_min.row && pos.row < pos_max.row) return true; // in a row between the items -> match + + const bool in_min_row = pos.row == pos_min.row && pos.col >= pos_min.col; + const bool in_max_row = pos.row == pos_max.row && pos.col <= pos_max.col; + + if (pos_min.row == pos_max.row) return in_min_row && in_max_row; // in the only row at a relevant col -> match + + return in_min_row || in_max_row; // in either min or max row at a relevant col -> match + }; + + for (usz i = 0; i < items().size(); i++) + { + if (flow_widget_item* item = items().at(i)) + { + if (item == current_item) continue; + + if (modifiers == Qt::ControlModifier && item->selected) + { + selected_items.insert(item); + } + else if (modifiers == Qt::ShiftModifier && item_between(item)) + { + selected_items.insert(item); + } + } + } + } + + select_items(selected_items, current_item); } void flow_widget::on_item_focus() { - select_item(static_cast(QObject::sender())); + update_selection(static_cast(QObject::sender())); } void flow_widget::on_navigate(flow_navigation value) { const flow_layout::position selected_pos = find_next_item(find_item(static_cast(QObject::sender())), value); const s64 selected_index = find_item(selected_pos); - if (selected_index < 0 || static_cast(selected_index) >= items().size()) - { - return; - } - if (flow_widget_item* item = items().at(selected_index)) + if (selected_index >= 0 && static_cast(selected_index) < items().size()) { - item->setFocus(); + if (flow_widget_item* item = items().at(selected_index)) + { + item->setFocus(); + } } - - m_selected_index = selected_index; } void flow_widget::mouseDoubleClickEvent(QMouseEvent* ev) diff --git a/rpcs3/rpcs3qt/flow_widget.h b/rpcs3/rpcs3qt/flow_widget.h index 433d05ad0e..4f0a7dd453 100644 --- a/rpcs3/rpcs3qt/flow_widget.h +++ b/rpcs3/rpcs3qt/flow_widget.h @@ -19,8 +19,10 @@ public: void add_widget(flow_widget_item* widget); void clear(); + void set_multi_selection_enabled(bool enabled) { m_allow_multi_selection = enabled; } + std::vector& items() { return m_widgets; } - flow_widget_item* selected_item() const; + std::set selected_items() const; QScrollArea* scroll_area() const { return m_scroll_area; } void paintEvent(QPaintEvent* event) override; @@ -33,7 +35,7 @@ private Q_SLOTS: void on_navigate(flow_navigation value); protected: - void select_item(flow_widget_item* item); + void select_items(const std::set& selected_items, flow_widget_item* current_item = nullptr); void mouseDoubleClickEvent(QMouseEvent* event) override; private: @@ -41,8 +43,12 @@ private: flow_layout::position find_item(flow_widget_item* item); flow_layout::position find_next_item(flow_layout::position current_pos, flow_navigation value); + void update_selection(flow_widget_item* current_item); + flow_layout* m_flow_layout{}; QScrollArea* m_scroll_area{}; std::vector m_widgets; - s64 m_selected_index = -1; + std::set m_selected_items; + s64 m_last_selected_item = -1; + bool m_allow_multi_selection = false; }; diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index f8b48e8508..9563c46fb5 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -961,32 +961,31 @@ void game_list_frame::ShowContextMenu(const QPoint& pos) QPoint global_pos; std::vector games; - // NOTE: Currently, only m_game_list supports rows multi selection! - // - // TODO: Add support to rows multi selection to m_game_grid - if (m_is_list_layout) { global_pos = m_game_list->viewport()->mapToGlobal(pos); - const auto item_list = m_game_list->selectedItems(); - game_info gameinfo; - - for (const auto& item : item_list) + for (const QTableWidgetItem* item : m_game_list->selectedItems()) { - if (item->column() != static_cast(gui::game_list_columns::icon)) + if (!item || item->column() != static_cast(gui::game_list_columns::icon)) continue; - if (gameinfo = GetGameInfoFromItem(item); gameinfo) + if (game_info gameinfo = GetGameInfoFromItem(item)) games.push_back(gameinfo); } } - else if (game_list_grid_item* item = static_cast(m_game_grid->selected_item())) + else if (m_game_grid) { global_pos = m_game_grid->mapToGlobal(pos); - if (game_info gameinfo = item->game(); gameinfo) - games.push_back(gameinfo); + for (const flow_widget_item* selected_item : m_game_grid->selected_items()) + { + if (const game_list_grid_item* item = static_cast(selected_item)) + { + if (game_info gameinfo = item->game()) + games.push_back(gameinfo); + } + } } if (!games.empty()) @@ -1154,16 +1153,19 @@ bool game_list_frame::eventFilter(QObject *object, QEvent *event) if (object == m_game_list) { - QTableWidgetItem* item = m_game_list->item(m_game_list->currentRow(), static_cast(gui::game_list_columns::icon)); + const QTableWidgetItem* item = m_game_list->item(m_game_list->currentRow(), static_cast(gui::game_list_columns::icon)); - if (!item || !item->isSelected()) - return false; - - gameinfo = GetGameInfoFromItem(item); + if (item && item->isSelected()) + { + gameinfo = GetGameInfoFromItem(item); + } } - else if (game_list_grid_item* item = static_cast(m_game_grid->selected_item())) + else if (const auto items = m_game_grid->selected_items(); !items.empty()) { - gameinfo = item->game(); + if (const game_list_grid_item* item = static_cast(*items.begin())) + { + gameinfo = item->game(); + } } if (!gameinfo) @@ -1279,11 +1281,14 @@ std::set game_list_frame::CurrentSelectionPaths() } else if (m_game_grid) { - if (const game_list_grid_item* item = static_cast(m_game_grid->selected_item())) + for (const flow_widget_item* selected_item : m_game_grid->selected_items()) { - if (const game_info& game = item->game()) + if (const game_list_grid_item* item = static_cast(selected_item)) { - selection.insert(game->info.path + game->info.icon_path); + if (const game_info& game = item->game()) + { + selection.insert(game->info.path + game->info.icon_path); + } } } } diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index e60a832569..a20e67da73 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -12,6 +12,8 @@ game_list_grid::game_list_grid() setObjectName("game_list_grid"); setContextMenuPolicy(Qt::CustomContextMenu); + set_multi_selection_enabled(true); + m_icon_ready_callback = [this](const game_info& game, const movie_item_base* item) { Q_EMIT IconReady(game, item); @@ -45,7 +47,7 @@ void game_list_grid::populate( { clear_list(); - game_list_grid_item* selected_item = nullptr; + std::set selected_items; blockSignals(true); @@ -112,7 +114,7 @@ void game_list_grid::populate( if (selected_item_ids.contains(game->info.path + game->info.icon_path)) { - selected_item = item; + selected_items.insert(item); } add_widget(item); @@ -125,7 +127,7 @@ void game_list_grid::populate( QApplication::processEvents(); - select_item(selected_item); + select_items(selected_items); } void game_list_grid::repaint_icons(std::vector& game_data, const QColor& icon_color, const QSize& icon_size, qreal device_pixel_ratio)