diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 1bac875a5ac..6718b68a21a 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -331,8 +331,8 @@ add_file_folder("Source/UI/Widgets" src/ui/widgets/PersonaColorComboBox.h src/ui/widgets/renderwidget.cpp src/ui/widgets/renderwidget.h - src/ui/widgets/sexp_data_menu.cpp - src/ui/widgets/sexp_data_menu.h + src/ui/widgets/data_list_menu.cpp + src/ui/widgets/data_list_menu.h src/ui/widgets/sexp_tree_view.cpp src/ui/widgets/sexp_tree_view.h src/ui/widgets/ShipFlagCheckbox.h diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp index 8e155e7c413..5fe85107f75 100644 --- a/qtfred/src/mission/EditorViewport.cpp +++ b/qtfred/src/mission/EditorViewport.cpp @@ -146,9 +146,11 @@ void EditorViewport::loadSettings() { // Handles its own group, since main.cpp reads it before the viewport exists. Theme_mode = readThemeModeSetting(); { - const int rawStyle = settings.value("sexp_data_menu_style", static_cast(Sexp_data_menu_style)).toInt(); - if (rawStyle >= 0 && rawStyle <= static_cast(SexpDataMenuStyle::Searchable)) { - Sexp_data_menu_style = static_cast(rawStyle); + // Fall back to the pre-rename key so an existing choice carries over. + const int legacyStyle = settings.value("sexp_data_menu_style", static_cast(Data_menu_style)).toInt(); + const int rawStyle = settings.value("data_menu_style", legacyStyle).toInt(); + if (rawStyle >= 0 && rawStyle <= static_cast(DataMenuStyle::Searchable)) { + Data_menu_style = static_cast(rawStyle); } } @@ -198,7 +200,7 @@ void EditorViewport::saveSettings() const { settings.setValue("show_sexp_help_ship_editor", Show_sexp_help_ship_editor); settings.setValue("show_sexp_help_wing_editor", Show_sexp_help_wing_editor); writeThemeModeSetting(Theme_mode); - settings.setValue("sexp_data_menu_style", static_cast(Sexp_data_menu_style)); + settings.setValue("data_menu_style", static_cast(Data_menu_style)); settings.setValue("view_universal_heading", view.Universal_heading); settings.setValue("view_show_stars", view.Show_stars); diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index f313661d78f..26962561525 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -29,7 +29,7 @@ enum class OtherKind { JumpNode, }; -enum class SexpDataMenuStyle { +enum class DataMenuStyle { Auto = 0, Columns = 1, Searchable = 2, @@ -239,7 +239,7 @@ class EditorViewport { ThemeMode Theme_mode = ThemeMode::System; - SexpDataMenuStyle Sexp_data_menu_style = SexpDataMenuStyle::Auto; + DataMenuStyle Data_menu_style = DataMenuStyle::Auto; void saveSettings() const; diff --git a/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp b/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp index 707be3baa79..c674f26744e 100644 --- a/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp +++ b/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp @@ -23,7 +23,7 @@ PreferencesDialogModel::PreferencesDialogModel(QObject* parent, EditorViewport* , _showSexpHelpShipEditor(viewport->Show_sexp_help_ship_editor) , _showSexpHelpWingEditor(viewport->Show_sexp_help_wing_editor) , _themeMode(viewport->Theme_mode) - , _sexpDataMenuStyle(viewport->Sexp_data_menu_style) + , _dataMenuStyle(viewport->Data_menu_style) , _toolbarIconSize(viewport->toolbar_icon_size) , _outlineLod(viewport->view.Outline_lod) , _invertOrbitX(viewport->camera.getInvertOrbitX()) @@ -65,7 +65,7 @@ bool PreferencesDialogModel::apply() { _viewport->Show_sexp_help_ship_editor = _showSexpHelpShipEditor; _viewport->Show_sexp_help_wing_editor = _showSexpHelpWingEditor; _viewport->Theme_mode = _themeMode; - _viewport->Sexp_data_menu_style = _sexpDataMenuStyle; + _viewport->Data_menu_style = _dataMenuStyle; _viewport->toolbar_icon_size = _toolbarIconSize; _viewport->view.Outline_lod = _outlineLod; _viewport->camera.setInvertOrbitX(_invertOrbitX); @@ -171,8 +171,8 @@ void PreferencesDialogModel::setShowSexpHelpWingEditor(bool value) { modify(_sho ThemeMode PreferencesDialogModel::getThemeMode() const { return _themeMode; } void PreferencesDialogModel::setThemeMode(ThemeMode value) { modify(_themeMode, value); } -SexpDataMenuStyle PreferencesDialogModel::getSexpDataMenuStyle() const { return _sexpDataMenuStyle; } -void PreferencesDialogModel::setSexpDataMenuStyle(SexpDataMenuStyle value) { modify(_sexpDataMenuStyle, value); } +DataMenuStyle PreferencesDialogModel::getDataMenuStyle() const { return _dataMenuStyle; } +void PreferencesDialogModel::setDataMenuStyle(DataMenuStyle value) { modify(_dataMenuStyle, value); } int PreferencesDialogModel::getToolbarIconSize() const { return _toolbarIconSize; } void PreferencesDialogModel::setToolbarIconSize(int size) { modify(_toolbarIconSize, size); } diff --git a/qtfred/src/mission/dialogs/PreferencesDialogModel.h b/qtfred/src/mission/dialogs/PreferencesDialogModel.h index 8687e125e60..9fcc5b50884 100644 --- a/qtfred/src/mission/dialogs/PreferencesDialogModel.h +++ b/qtfred/src/mission/dialogs/PreferencesDialogModel.h @@ -58,8 +58,8 @@ class PreferencesDialogModel : public AbstractDialogModel { ThemeMode getThemeMode() const; void setThemeMode(ThemeMode value); - SexpDataMenuStyle getSexpDataMenuStyle() const; - void setSexpDataMenuStyle(SexpDataMenuStyle value); + DataMenuStyle getDataMenuStyle() const; + void setDataMenuStyle(DataMenuStyle value); int getToolbarIconSize() const; void setToolbarIconSize(int size); @@ -105,7 +105,7 @@ class PreferencesDialogModel : public AbstractDialogModel { bool _showSexpHelpShipEditor; bool _showSexpHelpWingEditor; ThemeMode _themeMode; - SexpDataMenuStyle _sexpDataMenuStyle; + DataMenuStyle _dataMenuStyle; int _toolbarIconSize; int _outlineLod; diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 45a2d4ee7e1..b02135b1bc7 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -76,6 +76,7 @@ #include "missioneditor/missionsave.h" #include "widgets/ObjectComboBox.h" +#include "widgets/data_list_menu.h" #include "util.h" #include "mission/object.h" @@ -1971,21 +1972,20 @@ void FredView::initializePopupMenus() { _createSubmenu = new QMenu(tr("Create"), _viewPopup); + // Rebuilt on every open so a changed menu style preference takes effect. _createShipSubmenu = new QMenu(tr("Ship"), _createSubmenu); _createShipSubmenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); connect(_createShipSubmenu, &QMenu::aboutToShow, this, [this]() { - if (_createShipSubmenu->actions().isEmpty()) { - populateCreateShipSubmenu(); - } + _createShipSubmenu->clear(); + populateCreateShipSubmenu(); }); _createSubmenu->addMenu(_createShipSubmenu); _createPropSubmenu = new QMenu(tr("Prop"), _createSubmenu); _createPropSubmenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); connect(_createPropSubmenu, &QMenu::aboutToShow, this, [this]() { - if (_createPropSubmenu->actions().isEmpty()) { - populateCreatePropSubmenu(); - } + _createPropSubmenu->clear(); + populateCreatePropSubmenu(); }); _createSubmenu->addMenu(_createPropSubmenu); @@ -2080,32 +2080,32 @@ void FredView::initializePopupMenus() { } void FredView::populateCreateShipSubmenu() { + std::vector entries; for (int i = 0; i < (int)Ship_info.size(); ++i) { if (Ship_info[i].flags[Ship::Info_Flags::No_fred]) { continue; } - auto* action = new QAction(QString::fromUtf8(Ship_info[i].name), _createShipSubmenu); - connect(action, &QAction::triggered, this, [this, i]() { - _viewport->createShipAtScreenPos(_lastContextMenuLocalPos.x() * this->devicePixelRatio(), - _lastContextMenuLocalPos.y() * this->devicePixelRatio(), i); - }); - _createShipSubmenu->addAction(action); + entries.push_back({QString::fromUtf8(Ship_info[i].name), i}); } + populateDataListMenu(_createShipSubmenu, entries, _viewport->Data_menu_style, [this](int shipClass) { + _viewport->createShipAtScreenPos(_lastContextMenuLocalPos.x() * this->devicePixelRatio(), + _lastContextMenuLocalPos.y() * this->devicePixelRatio(), shipClass); + }); } void FredView::populateCreatePropSubmenu() { + std::vector entries; for (int i = 0; i < prop_info_size(); ++i) { if (Prop_info[i].flags[Prop::Info_Flags::No_fred]) { continue; } - auto* action = new QAction(QString::fromStdString(Prop_info[i].name), _createPropSubmenu); - connect(action, &QAction::triggered, this, [this, i]() { - _viewport->createPropAtScreenPos(_lastContextMenuLocalPos.x() * this->devicePixelRatio(), - _lastContextMenuLocalPos.y() * this->devicePixelRatio(), - i); - }); - _createPropSubmenu->addAction(action); + entries.push_back({QString::fromStdString(Prop_info[i].name), i}); } + populateDataListMenu(_createPropSubmenu, entries, _viewport->Data_menu_style, [this](int propClass) { + _viewport->createPropAtScreenPos(_lastContextMenuLocalPos.x() * this->devicePixelRatio(), + _lastContextMenuLocalPos.y() * this->devicePixelRatio(), + propClass); + }); } void FredView::populateMoveToLayerMenu(int targetObject, QMenu* targetMenu) { diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp index f1b334fcf2a..ff93b42ca29 100644 --- a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp @@ -38,6 +38,7 @@ JumpNodeEditorDialog::JumpNodeEditorDialog(FredView* parent, EditorViewport* vie Editor* editor = viewport->editor; util::installSelectMenu( this, + viewport, []() { std::vector entries; for (const auto& jn : Jump_nodes) { diff --git a/qtfred/src/ui/dialogs/PreferencesDialog.cpp b/qtfred/src/ui/dialogs/PreferencesDialog.cpp index 110e926ad7f..07db9e06d92 100644 --- a/qtfred/src/ui/dialogs/PreferencesDialog.cpp +++ b/qtfred/src/ui/dialogs/PreferencesDialog.cpp @@ -120,7 +120,7 @@ void PreferencesDialog::updateUi() { ui->checkPotentialIssues->setChecked(_model->getCheckPotentialIssues()); ui->applyAutoCorrections->setChecked(_model->getApplyAutoCorrections()); ui->themeCombo->setCurrentIndex(themeModeToIndex(_model->getThemeMode())); - ui->sexpDataMenuStyleCombo->setCurrentIndex(static_cast(_model->getSexpDataMenuStyle())); + ui->dataMenuStyleCombo->setCurrentIndex(static_cast(_model->getDataMenuStyle())); const int iconSize = _model->getToolbarIconSize(); ui->toolbarIconSizeCombo->setCurrentIndex(iconSize <= 16 ? 0 : iconSize >= 32 ? 2 : 1); @@ -202,8 +202,8 @@ void PreferencesDialog::on_themeCombo_currentIndexChanged(int index) { _model->setThemeMode(themeModeFromIndex(index)); } -void PreferencesDialog::on_sexpDataMenuStyleCombo_currentIndexChanged(int index) { - _model->setSexpDataMenuStyle(static_cast(index)); +void PreferencesDialog::on_dataMenuStyleCombo_currentIndexChanged(int index) { + _model->setDataMenuStyle(static_cast(index)); } void PreferencesDialog::on_showSexpHelpMissionEvents_toggled(bool checked) { diff --git a/qtfred/src/ui/dialogs/PreferencesDialog.h b/qtfred/src/ui/dialogs/PreferencesDialog.h index 0a88e68f4df..e4e8266866e 100644 --- a/qtfred/src/ui/dialogs/PreferencesDialog.h +++ b/qtfred/src/ui/dialogs/PreferencesDialog.h @@ -34,7 +34,7 @@ private slots: void on_toolbarIconSizeCombo_currentIndexChanged(int index); void on_outlineLodCombo_currentIndexChanged(int index); void on_themeCombo_currentIndexChanged(int index); - void on_sexpDataMenuStyleCombo_currentIndexChanged(int index); + void on_dataMenuStyleCombo_currentIndexChanged(int index); void on_showSexpHelpMissionEvents_toggled(bool checked); void on_showSexpHelpMissionGoals_toggled(bool checked); void on_showSexpHelpMissionCutscenes_toggled(bool checked); diff --git a/qtfred/src/ui/dialogs/PropEditorDialog.cpp b/qtfred/src/ui/dialogs/PropEditorDialog.cpp index b71b04c2a76..769f7625628 100644 --- a/qtfred/src/ui/dialogs/PropEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/PropEditorDialog.cpp @@ -48,6 +48,7 @@ PropEditorDialog::PropEditorDialog(FredView* parent, EditorViewport* viewport) Editor* editor = viewport->editor; util::installSelectMenu( this, + viewport, []() { std::vector entries; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index 620d42f5a98..7e67401b0dc 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -58,6 +58,7 @@ ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) Editor* editor = viewport->editor; util::installSelectMenu( this, + viewport, []() { std::vector entries; for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp index 0d85be900b6..82f59b86a81 100644 --- a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp @@ -40,6 +40,7 @@ WaypointEditorDialog::WaypointEditorDialog(FredView* parent, EditorViewport* vie auto* model = _model.get(); util::installSelectMenu( this, + viewport, []() { std::vector entries; entries.reserve(Waypoint_lists.size()); diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.cpp b/qtfred/src/ui/dialogs/WingEditorDialog.cpp index dd40fdea28e..c459b668900 100644 --- a/qtfred/src/ui/dialogs/WingEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WingEditorDialog.cpp @@ -49,6 +49,7 @@ WingEditorDialog::WingEditorDialog(FredView* parent, EditorViewport* viewport) Editor* editor = viewport->editor; util::installSelectMenu( this, + viewport, []() { std::vector entries; for (int i = 0; i < MAX_WINGS; i++) { diff --git a/qtfred/src/ui/util/menu.cpp b/qtfred/src/ui/util/menu.cpp index cb26c51f23f..3ca5430bff4 100644 --- a/qtfred/src/ui/util/menu.cpp +++ b/qtfred/src/ui/util/menu.cpp @@ -3,8 +3,9 @@ #include "menu.h" +#include + #include -#include #include #include #include @@ -36,6 +37,7 @@ int propagate_disabled_status(QMenu* top) { } void installSelectMenu(QWidget* dialog, + EditorViewport* viewport, std::function()> gather, std::function currentId, std::function onChosen, @@ -58,26 +60,11 @@ void installSelectMenu(QWidget* dialog, // Rebuild the list from the live scene every time the menu opens. QObject::connect(menu, &QMenu::aboutToShow, menu, - [menu, gather = std::move(gather), currentId = std::move(currentId), onChosen = std::move(onChosen)]() { + [menu, viewport, gather = std::move(gather), currentId = std::move(currentId), onChosen = std::move(onChosen)]() { menu->clear(); const int current = currentId ? currentId() : -1; - QAction* currentAct = nullptr; - for (const auto& entry : gather()) { - QAction* act = menu->addAction(entry.name); - if (entry.id == current) { - // Highlight the current object with a bold font - QFont font = act->font(); - font.setBold(true); - act->setFont(font); - currentAct = act; - } - const int id = entry.id; - QObject::connect(act, &QAction::triggered, menu, [onChosen, id]() { onChosen(id); }); - } - // Open with the current item pre-highlighted. - if (currentAct != nullptr) { - menu->setActiveAction(currentAct); - } + const DataMenuStyle style = viewport ? viewport->Data_menu_style : DataMenuStyle::Columns; + populateDataListMenu(menu, gather(), style, onChosen, 0, current); }); } diff --git a/qtfred/src/ui/util/menu.h b/qtfred/src/ui/util/menu.h index 41b7e11a32b..682e011011a 100644 --- a/qtfred/src/ui/util/menu.h +++ b/qtfred/src/ui/util/menu.h @@ -10,6 +10,9 @@ class QWidget; namespace fso { namespace fred { + +class EditorViewport; + namespace util { int propagate_disabled_status(QMenu* top); @@ -21,8 +24,10 @@ struct SelectMenuEntry { // Adds a "Select" menu to an object-editor dialog's menu bar, creating a slim // menu bar via the dialog's top-level layout if one isn't already present and -// reusing an existing bar otherwise. +// reusing an existing bar otherwise. The list is presented with the viewport's +// preferred data menu style (columns vs. searchable). void installSelectMenu(QWidget* dialog, + EditorViewport* viewport, std::function()> gather, std::function currentId, std::function onChosen, diff --git a/qtfred/src/ui/widgets/sexp_data_menu.cpp b/qtfred/src/ui/widgets/data_list_menu.cpp similarity index 60% rename from qtfred/src/ui/widgets/sexp_data_menu.cpp rename to qtfred/src/ui/widgets/data_list_menu.cpp index 83e5d28f5a3..47505bbbe43 100644 --- a/qtfred/src/ui/widgets/sexp_data_menu.cpp +++ b/qtfred/src/ui/widgets/data_list_menu.cpp @@ -1,4 +1,4 @@ -#include "sexp_data_menu.h" +#include "data_list_menu.h" #include #include @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -17,21 +18,22 @@ namespace fso::fred { namespace { -constexpr int kDataIdxRole = Qt::UserRole + 1; +constexpr int kIdRole = Qt::UserRole + 1; -void dismissAndActivate(int dataIdx, const std::function& onActivate) { +void dismissAndActivate(int id, const std::function& onActivate) { while (auto* p = QApplication::activePopupWidget()) { p->close(); } // Defer the actual edit so it runs after the menu close cascade has finished // processing — avoids reentrancy issues if the edit triggers more UI changes. - QTimer::singleShot(0, [onActivate, dataIdx]() { onActivate(dataIdx); }); + QTimer::singleShot(0, [onActivate, id]() { onActivate(id); }); } class SearchableMenuWidget : public QWidget { public: - SearchableMenuWidget(const std::vector& items, - std::function onActivate) + SearchableMenuWidget(const std::vector& items, + std::function onActivate, + int currentId) : QWidget(nullptr) , _onActivate(std::move(onActivate)) { @@ -46,9 +48,16 @@ class SearchableMenuWidget : public QWidget { _model = new QStandardItemModel(0, 1, this); for (const auto& it : items) { - auto* row = new QStandardItem(it.text); - row->setData(it.dataIdx, kDataIdxRole); + auto* row = new QStandardItem(it.name); + row->setData(it.id, kIdRole); row->setEditable(false); + if (currentId >= 0 && it.id == currentId) { + // Highlight the current object with a bold font + QFont font = row->font(); + font.setBold(true); + row->setFont(font); + _currentSourceRow = _model->rowCount(); + } _model->appendRow(row); } @@ -67,7 +76,7 @@ class SearchableMenuWidget : public QWidget { const int rowHeight = fm.height() + 6; int maxTextWidth = 0; for (const auto& it : items) { - maxTextWidth = std::max(maxTextWidth, fm.horizontalAdvance(it.text)); + maxTextWidth = std::max(maxTextWidth, fm.horizontalAdvance(it.name)); } _list->setFixedWidth(std::clamp(maxTextWidth + 32, 220, 480)); _list->setMinimumHeight(rowHeight * 13); @@ -83,14 +92,21 @@ class SearchableMenuWidget : public QWidget { QObject::connect(_list, &QListView::activated, this, [this](const QModelIndex&) { activateCurrent(); }); QObject::connect(_list, &QListView::clicked, this, [this](const QModelIndex&) { activateCurrent(); }); - if (_proxy->rowCount() > 0) { - _list->setCurrentIndex(_proxy->index(0, 0)); - } + selectDefaultRow(); } QLineEdit* filterEdit() { return _filter; } protected: + void showEvent(QShowEvent* event) override { + QWidget::showEvent(event); + // Reset from any previous opening, land on the current item, and make + // sure the filter has keyboard focus rather than the menu itself. + _filter->clear(); + selectDefaultRow(); + _filter->setFocus(); + } + void keyPressEvent(QKeyEvent* event) override { switch (event->key()) { case Qt::Key_Down: @@ -111,12 +127,26 @@ class SearchableMenuWidget : public QWidget { } private: + void selectDefaultRow() { + if (_currentSourceRow >= 0) { + const QModelIndex idx = _proxy->mapFromSource(_model->index(_currentSourceRow, 0)); + if (idx.isValid()) { + _list->setCurrentIndex(idx); + _list->scrollTo(idx); + return; + } + } + if (_proxy->rowCount() > 0) { + _list->setCurrentIndex(_proxy->index(0, 0)); + } + } + void activateCurrent() { const QModelIndex idx = _list->currentIndex(); if (!idx.isValid()) { return; } - dismissAndActivate(idx.data(kDataIdxRole).toInt(), _onActivate); + dismissAndActivate(idx.data(kIdRole).toInt(), _onActivate); } std::function _onActivate; @@ -124,24 +154,36 @@ class SearchableMenuWidget : public QWidget { QListView* _list = nullptr; QStandardItemModel* _model = nullptr; QSortFilterProxyModel* _proxy = nullptr; + int _currentSourceRow = -1; }; void appendActions(QMenu* menu, - const std::vector& items, - const std::function& onActivate) + const std::vector& items, + const std::function& onActivate, + int currentId) { + QAction* currentAct = nullptr; for (const auto& item : items) { - const int idx = item.dataIdx; - menu->addAction(item.text, menu, [onActivate, idx]() { onActivate(idx); }); + const int id = item.id; + QAction* act = menu->addAction(item.name, menu, [onActivate, id]() { onActivate(id); }); + if (currentId >= 0 && id == currentId) { + // Highlight the current object with a bold font + QFont font = act->font(); + font.setBold(true); + act->setFont(font); + currentAct = act; + } + } + // Open with the current item pre-highlighted. + if (currentAct != nullptr) { + menu->setActiveAction(currentAct); } } // Resolves Auto to a concrete style: the native column menu while the list fits // comfortably, then the searchable popup once it would grow past half the screen. // The menu is not shown yet, so height is estimated from the item count. -SexpDataMenuStyle resolveAutoStyle(const QMenu* menu, int itemCount) { - // Extra rows for the Number / String / separator entries already in the menu. - constexpr int fixedRows = 3; +DataMenuStyle resolveAutoStyle(const QMenu* menu, int itemCount, int fixedRows) { const QFontMetrics fm(menu->font()); const int rowHeight = fm.height() + 6; const int estimatedHeight = (itemCount + fixedRows) * rowHeight; @@ -151,41 +193,46 @@ SexpDataMenuStyle resolveAutoStyle(const QMenu* menu, int itemCount) { screen = QGuiApplication::primaryScreen(); } if (screen == nullptr) { - return SexpDataMenuStyle::Columns; + return DataMenuStyle::Columns; } const int available = screen->availableGeometry().height(); - return estimatedHeight > available / 2 ? SexpDataMenuStyle::Searchable - : SexpDataMenuStyle::Columns; + return estimatedHeight > available / 2 ? DataMenuStyle::Searchable + : DataMenuStyle::Columns; } } // namespace -void populateSexpDataSubmenu(QMenu* menu, - const std::vector& items, - SexpDataMenuStyle style, - std::function onActivate) +void populateDataListMenu(QMenu* menu, + const std::vector& items, + DataMenuStyle style, + std::function onActivate, + int fixedRows, + int currentId) { if (!menu || items.empty()) { return; } - if (style == SexpDataMenuStyle::Auto) { - style = resolveAutoStyle(menu, static_cast(items.size())); + if (style == DataMenuStyle::Auto) { + style = resolveAutoStyle(menu, static_cast(items.size()), fixedRows); } switch (style) { - case SexpDataMenuStyle::Auto: - case SexpDataMenuStyle::Columns: + case DataMenuStyle::Auto: + case DataMenuStyle::Columns: // Native menu: the platform tiles a too-tall list into columns. - appendActions(menu, items, onActivate); + appendActions(menu, items, onActivate, currentId); break; - case SexpDataMenuStyle::Searchable: { - auto* widget = new SearchableMenuWidget(items, std::move(onActivate)); + case DataMenuStyle::Searchable: { + auto* widget = new SearchableMenuWidget(items, std::move(onActivate), currentId); auto* action = new QWidgetAction(menu); action->setDefaultWidget(widget); menu->addAction(action); - // Focus the filter once the menu is fully shown. + // Focus the filter once the menu is fully shown. When we're already + // inside aboutToShow (the Select menus rebuild there), this connect + // fires too late for the current opening; the widget's showEvent + // covers that path. QObject::connect(menu, &QMenu::aboutToShow, widget, [widget]() { widget->filterEdit()->setFocus(); }); diff --git a/qtfred/src/ui/widgets/data_list_menu.h b/qtfred/src/ui/widgets/data_list_menu.h new file mode 100644 index 00000000000..8a6a97fb6d2 --- /dev/null +++ b/qtfred/src/ui/widgets/data_list_menu.h @@ -0,0 +1,30 @@ +#pragma once + +#include "mission/EditorViewport.h" + +#include + +#include +#include +#include + +class QMenu; + +namespace fso::fred { + +// Populates a menu with a flat list of name/id entries using the chosen style. +// Pre-existing items in `menu` (e.g. the sexp Number/String entries) are left +// intact; the list is appended after them. `fixedRows` is the count of those +// pre-existing rows, used when estimating whether an Auto-style menu would +// outgrow the screen. If `currentId` matches an entry's id, that entry is +// bolded and pre-highlighted. +// +// onActivate is invoked with the chosen entry's id when the user selects one. +void populateDataListMenu(QMenu* menu, + const std::vector& items, + DataMenuStyle style, + std::function onActivate, + int fixedRows = 0, + int currentId = -1); + +} // namespace fso::fred diff --git a/qtfred/src/ui/widgets/sexp_data_menu.h b/qtfred/src/ui/widgets/sexp_data_menu.h deleted file mode 100644 index ffc2ddf59dd..00000000000 --- a/qtfred/src/ui/widgets/sexp_data_menu.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "mission/EditorViewport.h" - -#include -#include -#include - -class QMenu; - -namespace fso::fred { - -struct SexpDataMenuItem { - QString text; - int dataIdx; -}; - -// Populates the OPF data list portion of an "Add Data" / "Replace Data" submenu -// using the chosen style. Pre-existing items in `menu` (Number, String, -// separator) are left intact; the data list is appended after them. -// -// onActivate is invoked with the chosen item's dataIdx when the user selects -// an entry. Callers are responsible for resolving that index against the -// original sexp_list_item linked list. -void populateSexpDataSubmenu(QMenu* menu, - const std::vector& items, - SexpDataMenuStyle style, - std::function onActivate); - -} // namespace fso::fred diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index aaa234fb473..a6d0e304dca 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include @@ -1327,10 +1327,10 @@ std::unique_ptr sexp_tree_view::buildContextMenu(QTreeWidgetItem* h) { replace_number_act->setEnabled(state.can_replace_number); replace_string_act->setEnabled(state.can_replace_string); - const SexpDataMenuStyle dataMenuStyle = _viewport ? _viewport->Sexp_data_menu_style : SexpDataMenuStyle::Columns; + const DataMenuStyle dataMenuStyle = _viewport ? _viewport->Data_menu_style : DataMenuStyle::Columns; auto collectDataItems = [](sexp_list_item* head) { - std::vector out; + std::vector out; sexp_list_item* ptr = head; int data_idx = 0; while (ptr) { @@ -1343,14 +1343,17 @@ std::unique_ptr sexp_tree_view::buildContextMenu(QTreeWidgetItem* h) { return out; }; + // The data menus already hold the Number / String / separator rows. + constexpr int dataMenuFixedRows = 3; + if (state.add_data_list) { - populateSexpDataSubmenu(add_data_menu, collectDataItems(state.add_data_list), dataMenuStyle, - [this](int data_idx) { addReplaceTypedDataHandler(data_idx, false); }); + populateDataListMenu(add_data_menu, collectDataItems(state.add_data_list), dataMenuStyle, + [this](int data_idx) { addReplaceTypedDataHandler(data_idx, false); }, dataMenuFixedRows); } if (state.replace_data_list) { - populateSexpDataSubmenu(replace_data_menu, collectDataItems(state.replace_data_list), dataMenuStyle, - [this](int data_idx) { addReplaceTypedDataHandler(data_idx, true); }); + populateDataListMenu(replace_data_menu, collectDataItems(state.replace_data_list), dataMenuStyle, + [this](int data_idx) { addReplaceTypedDataHandler(data_idx, true); }, dataMenuFixedRows); } // Clipboard and copy operations diff --git a/qtfred/ui/PreferencesDialog.ui b/qtfred/ui/PreferencesDialog.ui index 7fbea1dc657..c45e1a9ecb4 100644 --- a/qtfred/ui/PreferencesDialog.ui +++ b/qtfred/ui/PreferencesDialog.ui @@ -128,16 +128,16 @@ - + - SEXP data menu style: + Long list menu style: - + - Controls how the "Add Data" / "Replace Data" submenus present long lists like ships and wings. Auto uses Columns for short lists and Searchable once a list would fill more than half the screen. + Controls how menus with long lists are presented: SEXP "Add Data" / "Replace Data" submenus, object Select menus, and the Create Ship/Prop submenus. Auto uses Columns for short lists and Searchable once a list would fill more than half the screen.