Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions qtfred/source_groups.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,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/sexp_tree_view.cpp
src/ui/widgets/sexp_tree_view.h
src/ui/widgets/ShipFlagCheckbox.h
Expand Down
7 changes: 7 additions & 0 deletions qtfred/src/mission/EditorViewport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ void EditorViewport::loadSettings() {
Show_sexp_help_wing_editor = settings.value("show_sexp_help_wing_editor", Show_sexp_help_wing_editor).toBool();
// 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<int>(Sexp_data_menu_style)).toInt();
if (rawStyle >= 0 && rawStyle <= static_cast<int>(SexpDataMenuStyle::Searchable)) {
Sexp_data_menu_style = static_cast<SexpDataMenuStyle>(rawStyle);
}
}

view.Universal_heading = settings.value("view_universal_heading", view.Universal_heading).toBool();
view.Show_stars = settings.value("view_show_stars", view.Show_stars).toBool();
Expand Down Expand Up @@ -192,6 +198,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<int>(Sexp_data_menu_style));

settings.setValue("view_universal_heading", view.Universal_heading);
settings.setValue("view_show_stars", view.Show_stars);
Expand Down
8 changes: 8 additions & 0 deletions qtfred/src/mission/EditorViewport.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ enum class OtherKind {
JumpNode,
};

enum class SexpDataMenuStyle {
Auto = 0,
Columns = 1,
Searchable = 2,
};

struct ViewSettings {
bool Universal_heading = false;
bool Show_stars = true;
Expand Down Expand Up @@ -232,6 +238,8 @@ class EditorViewport {

ThemeMode Theme_mode = ThemeMode::System;

SexpDataMenuStyle Sexp_data_menu_style = SexpDataMenuStyle::Auto;

void saveSettings() const;

Editor* editor = nullptr;
Expand Down
5 changes: 5 additions & 0 deletions qtfred/src/mission/dialogs/PreferencesDialogModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +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)
, _toolbarIconSize(viewport->toolbar_icon_size)
, _outlineLod(viewport->view.Outline_lod)
, _invertOrbitX(viewport->camera.getInvertOrbitX())
Expand Down Expand Up @@ -64,6 +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->toolbar_icon_size = _toolbarIconSize;
_viewport->view.Outline_lod = _outlineLod;
_viewport->camera.setInvertOrbitX(_invertOrbitX);
Expand Down Expand Up @@ -169,6 +171,9 @@ 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); }

int PreferencesDialogModel::getToolbarIconSize() const { return _toolbarIconSize; }
void PreferencesDialogModel::setToolbarIconSize(int size) { modify(_toolbarIconSize, size); }

Expand Down
4 changes: 4 additions & 0 deletions qtfred/src/mission/dialogs/PreferencesDialogModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class PreferencesDialogModel : public AbstractDialogModel {
ThemeMode getThemeMode() const;
void setThemeMode(ThemeMode value);

SexpDataMenuStyle getSexpDataMenuStyle() const;
void setSexpDataMenuStyle(SexpDataMenuStyle value);

int getToolbarIconSize() const;
void setToolbarIconSize(int size);

Expand Down Expand Up @@ -102,6 +105,7 @@ class PreferencesDialogModel : public AbstractDialogModel {
bool _showSexpHelpShipEditor;
bool _showSexpHelpWingEditor;
ThemeMode _themeMode;
SexpDataMenuStyle _sexpDataMenuStyle;
int _toolbarIconSize;
int _outlineLod;

Expand Down
5 changes: 5 additions & 0 deletions qtfred/src/ui/dialogs/PreferencesDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +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<int>(_model->getSexpDataMenuStyle()));

const int iconSize = _model->getToolbarIconSize();
ui->toolbarIconSizeCombo->setCurrentIndex(iconSize <= 16 ? 0 : iconSize >= 32 ? 2 : 1);
Expand Down Expand Up @@ -201,6 +202,10 @@ void PreferencesDialog::on_themeCombo_currentIndexChanged(int index) {
_model->setThemeMode(themeModeFromIndex(index));
}

void PreferencesDialog::on_sexpDataMenuStyleCombo_currentIndexChanged(int index) {
_model->setSexpDataMenuStyle(static_cast<SexpDataMenuStyle>(index));
}

void PreferencesDialog::on_showSexpHelpMissionEvents_toggled(bool checked) {
_model->setShowSexpHelpMissionEvents(checked);
}
Expand Down
1 change: 1 addition & 0 deletions qtfred/src/ui/dialogs/PreferencesDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +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_showSexpHelpMissionEvents_toggled(bool checked);
void on_showSexpHelpMissionGoals_toggled(bool checked);
void on_showSexpHelpMissionCutscenes_toggled(bool checked);
Expand Down
197 changes: 197 additions & 0 deletions qtfred/src/ui/widgets/sexp_data_menu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#include "sexp_data_menu.h"

#include <QApplication>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListView>
#include <QMenu>
#include <QScreen>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidgetAction>

namespace fso::fred {
namespace {

constexpr int kDataIdxRole = Qt::UserRole + 1;

void dismissAndActivate(int dataIdx, const std::function<void(int)>& 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); });
}

class SearchableMenuWidget : public QWidget {
public:
SearchableMenuWidget(const std::vector<SexpDataMenuItem>& items,
std::function<void(int)> onActivate)
: QWidget(nullptr)
, _onActivate(std::move(onActivate))
{
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 4, 4, 4);
layout->setSpacing(4);

_filter = new QLineEdit(this);
_filter->setPlaceholderText(tr("Type to filter..."));
_filter->setClearButtonEnabled(true);
layout->addWidget(_filter);

_model = new QStandardItemModel(0, 1, this);
for (const auto& it : items) {
auto* row = new QStandardItem(it.text);
row->setData(it.dataIdx, kDataIdxRole);
row->setEditable(false);
_model->appendRow(row);
}

_proxy = new QSortFilterProxyModel(this);
_proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
_proxy->setSourceModel(_model);

_list = new QListView(this);
_list->setModel(_proxy);
_list->setEditTriggers(QAbstractItemView::NoEditTriggers);
_list->setUniformItemSizes(true);
_list->setSelectionMode(QAbstractItemView::SingleSelection);
_list->setFrameShape(QFrame::NoFrame);

const QFontMetrics fm(_list->font());
const int rowHeight = fm.height() + 6;
int maxTextWidth = 0;
for (const auto& it : items) {
maxTextWidth = std::max(maxTextWidth, fm.horizontalAdvance(it.text));
}
_list->setFixedWidth(std::clamp(maxTextWidth + 32, 220, 480));
_list->setMinimumHeight(rowHeight * 13);
layout->addWidget(_list);

QObject::connect(_filter, &QLineEdit::textChanged, this, [this](const QString& text) {
_proxy->setFilterFixedString(text);
if (_proxy->rowCount() > 0) {
_list->setCurrentIndex(_proxy->index(0, 0));
}
});
QObject::connect(_filter, &QLineEdit::returnPressed, this, [this]() { activateCurrent(); });
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));
}
}

QLineEdit* filterEdit() { return _filter; }

protected:
void keyPressEvent(QKeyEvent* event) override {
switch (event->key()) {
case Qt::Key_Down:
case Qt::Key_Up:
case Qt::Key_PageDown:
case Qt::Key_PageUp:
QApplication::sendEvent(_list, event);
return;
case Qt::Key_Escape:
while (auto* p = QApplication::activePopupWidget()) {
p->close();
}
return;
default:
break;
}
QWidget::keyPressEvent(event);
}

private:
void activateCurrent() {
const QModelIndex idx = _list->currentIndex();
if (!idx.isValid()) {
return;
}
dismissAndActivate(idx.data(kDataIdxRole).toInt(), _onActivate);
}

std::function<void(int)> _onActivate;
QLineEdit* _filter = nullptr;
QListView* _list = nullptr;
QStandardItemModel* _model = nullptr;
QSortFilterProxyModel* _proxy = nullptr;
};

void appendActions(QMenu* menu,
const std::vector<SexpDataMenuItem>& items,
const std::function<void(int)>& onActivate)
{
for (const auto& item : items) {
const int idx = item.dataIdx;
menu->addAction(item.text, menu, [onActivate, idx]() { onActivate(idx); });
}
}

// 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;
const QFontMetrics fm(menu->font());
const int rowHeight = fm.height() + 6;
const int estimatedHeight = (itemCount + fixedRows) * rowHeight;

const QScreen* screen = menu->screen();
if (screen == nullptr) {
screen = QGuiApplication::primaryScreen();
}
if (screen == nullptr) {
return SexpDataMenuStyle::Columns;
}

const int available = screen->availableGeometry().height();
return estimatedHeight > available / 2 ? SexpDataMenuStyle::Searchable
: SexpDataMenuStyle::Columns;
}

} // namespace

void populateSexpDataSubmenu(QMenu* menu,
const std::vector<SexpDataMenuItem>& items,
SexpDataMenuStyle style,
std::function<void(int)> onActivate)
{
if (!menu || items.empty()) {
return;
}

if (style == SexpDataMenuStyle::Auto) {
style = resolveAutoStyle(menu, static_cast<int>(items.size()));
}

switch (style) {
case SexpDataMenuStyle::Auto:
case SexpDataMenuStyle::Columns:
// Native menu: the platform tiles a too-tall list into columns.
appendActions(menu, items, onActivate);
break;
case SexpDataMenuStyle::Searchable: {
auto* widget = new SearchableMenuWidget(items, std::move(onActivate));
auto* action = new QWidgetAction(menu);
action->setDefaultWidget(widget);
menu->addAction(action);
// Focus the filter once the menu is fully shown.
QObject::connect(menu, &QMenu::aboutToShow, widget, [widget]() {
widget->filterEdit()->setFocus();
});
break;
}
}
}

} // namespace fso::fred
30 changes: 30 additions & 0 deletions qtfred/src/ui/widgets/sexp_data_menu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include "mission/EditorViewport.h"

#include <QString>
#include <functional>
#include <vector>

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<SexpDataMenuItem>& items,
SexpDataMenuStyle style,
std::function<void(int)> onActivate);

} // namespace fso::fred
Loading
Loading