From 3c8cab64fdffdf9eb009722abf6c72034ba7ed74 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 13 Jul 2026 21:06:50 -0400 Subject: [PATCH 1/8] Prioritize items that begin with search string Items that begin with the search string should be prioritized before those that simply contain the search string. --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index ee88baa1c63..1df02052ea6 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -1608,11 +1608,18 @@ void sexp_tree_view::startOperatorQuickSearch(QTreeWidgetItem* item, const QStri void sexp_tree_view::filterOperatorPopup(const QString& text) { _opList->clear(); + SCP_set found_list; + if (text.isEmpty()) { _opList->addItems(_opAll); } else { for (const auto& s : _opAll) { - if (s.contains(text, Qt::CaseInsensitive)) + if (s.startsWith(text, Qt::CaseInsensitive)) + _opList->addItem(s); + } + + for (const auto& s : _opAll) { + if (s.contains(text, Qt::CaseInsensitive) && _opList->findItems(s, Qt::MatchExactly).isEmpty()) _opList->addItem(s); } } From de012bfa62b80aa4afe68d997953c4bac060d353 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 14 Jul 2026 13:43:38 -0400 Subject: [PATCH 2/8] Add Search Operators option --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index 1df02052ea6..a12853b8321 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -1143,6 +1143,8 @@ std::unique_ptr sexp_tree_view::buildContextMenu(QTreeWidgetItem* h) { popup_menu->addSeparator(); auto replace_op_menu = popup_menu->addMenu(tr("Replace Operator")); + + popup_menu->addAction(tr("Search Operators"), this, [this]() { editActionHandlerHelper(); }); auto replace_data_menu = popup_menu->addMenu(tr("Replace Data")); auto replace_number_act = From 1497aac45310c5b08fdfb1ee52104c5326c3ce64 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 14 Jul 2026 18:16:31 -0400 Subject: [PATCH 3/8] Add Safety to Action Handler I didn't see any crashes, but this matches the pattern of other keyboard shortcuts. --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index a12853b8321..175ea26bf9b 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -1996,7 +1996,14 @@ void sexp_tree_view::handleDoubleClick() { // Helper function for making sure that search and edit are initiated consistently void sexp_tree_view::editActionHandlerHelper(){ - item_index = get_node(currentItem()); + auto item = currentItem(); + + // Just to be extra safe, early exit when an item is not selected + if (item == nullptr || !_interface) { + return; + } + + item_index = get_node(item); if (_model.compute_context_menu_state().can_edit_text) { editDataActionHandler(); From 9ca7bb51d30d56b8e4ad4b8e52ecb24f8c556608 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 Jul 2026 14:54:23 -0400 Subject: [PATCH 4/8] Allow search for numeric nodes The working code existed already --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index 175ea26bf9b..13e63b03cbe 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -2004,12 +2004,8 @@ void sexp_tree_view::editActionHandlerHelper(){ } item_index = get_node(item); - - if (_model.compute_context_menu_state().can_edit_text) { - editDataActionHandler(); - } else { - openNodeEditor(currentItem()); - } + // No longer gated to just certain types. + openNodeEditor(currentItem()); } // Public entry point for deleting the currently selected item. Simply delegates to deleteActionHandler(). From 575977d8d1d523aeefc2f3f995092583ac8dd932 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 Jul 2026 16:01:41 -0400 Subject: [PATCH 5/8] Limit matches on operator search This limits matches on operator searches by setting a minimum size that a string match has to meet. Function returns -1 on failure, and the warning has been removed to allow this behavior. --- code/missioneditor/sexp_tree_model.cpp | 3 +-- code/parse/sexp.cpp | 9 ++++++--- code/parse/sexp.h | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/code/missioneditor/sexp_tree_model.cpp b/code/missioneditor/sexp_tree_model.cpp index 4d409f126e9..a92f3d5ee20 100644 --- a/code/missioneditor/sexp_tree_model.cpp +++ b/code/missioneditor/sexp_tree_model.cpp @@ -952,9 +952,8 @@ SCP_string SexpTreeModel::match_closest_operator(const SCP_string& str, int node opf = query_operator_argument_type(op, arg_num); // find the best operator - int best = sexp_match_closest_operator(str, opf); + int best = sexp_match_closest_operator(str, opf, 10); if (best < 0) { - Warning(LOCATION, "Unable to find an operator match for string '%s' and argument type %d", str.c_str(), opf); return str; } return Operators[best].text; diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 539fa03406c..9a90dcd8a81 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -35315,12 +35315,15 @@ bool sexp_query_type_match(int opf, int opr) * Finds the operator that is the best textual match for the input string, given the required OPF type. For equal matches, * the alphabetically earliest operator is returned. * + * min defaults to SCP_string::npos, when specified to another value, this function will not always return a match + * * Note: Returns the operator index, not the operator value. */ -int sexp_match_closest_operator(const SCP_string &str, int opf) +int sexp_match_closest_operator(const SCP_string &str, int opf, size_t min) { + // Cyborg - This bool setup helps with readability + bool return_any = (min == SCP_string::npos); int best = -1; - size_t min = SCP_string::npos; for (int op_index : Sorted_operator_indexes) { @@ -35330,7 +35333,7 @@ int sexp_match_closest_operator(const SCP_string &str, int opf) if (sexp_query_type_match(opf, opr)) { size_t cost = stringcost(op_text, str, Max_operator_length, stringcost_tolower_equal); - if (best < 0 || cost < min) + if (cost < min || (return_any && best == -1) ) { min = cost; best = op_index; diff --git a/code/parse/sexp.h b/code/parse/sexp.h index 68299082412..456bc8b767f 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -1494,7 +1494,7 @@ extern std::pair query_referenced_in_sexp(sexp_ref_type type, con extern void stuff_sexp_text_string(SCP_string &dest, int node, int mode); extern int build_sexp_string(SCP_string &accumulator, int cur_node, int level, int mode); extern bool sexp_query_type_match(int opf, int opr); -extern int sexp_match_closest_operator(const SCP_string &str, int opf); +extern int sexp_match_closest_operator(const SCP_string &str, int opf, size_t min = SCP_string::npos); extern bool sexp_recoverable_error(int num); extern const char *sexp_error_message(int num); extern int count_free_sexp_nodes(); From 34e4af761c4f85d7e34e9c7be6625ada8d7657d9 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 Jul 2026 19:01:26 -0400 Subject: [PATCH 6/8] Try this option name better --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index 13e63b03cbe..b743930ac8e 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -1144,9 +1144,9 @@ std::unique_ptr sexp_tree_view::buildContextMenu(QTreeWidgetItem* h) { auto replace_op_menu = popup_menu->addMenu(tr("Replace Operator")); - popup_menu->addAction(tr("Search Operators"), this, [this]() { editActionHandlerHelper(); }); - auto replace_data_menu = popup_menu->addMenu(tr("Replace Data")); + popup_menu->addAction(tr("Search for Replacement"), QKeySequence(Qt::CTRL | Qt::Key_S), this, [this]() { editActionHandlerHelper(); }); + auto replace_number_act = replace_data_menu->addAction(tr("Number"), this, [this]() { replaceNumberDataHandler(); }); replace_number_act->setEnabled(false); From 3f775bf0390918836926ffe1159342fa7d9c503f Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 Jul 2026 21:31:53 -0400 Subject: [PATCH 7/8] Add keyboard shortcut for search This allows ctrl+s (which can be listed instead of spacebar) to initiate search. There is a new menu item added as well. --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index b743930ac8e..22a677e8c3f 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -260,6 +260,12 @@ sexp_tree_view::sexp_tree_view(QWidget* parent) : QTreeWidget(parent), _actions( installShortcut(QKeySequence::Delete, [](const SexpContextMenuState& s) { return s.can_delete; }, [this]() { deleteActionHandler(); }); + + // This keyboard shortcut does not need as much state checking because the helper + // handles security checks and what to call, and any node should work here. + auto* shortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_S), this); + shortcut->setContext(Qt::WidgetShortcut); + connect(shortcut, &QShortcut::activated, this, &sexp_tree_view::editActionHandlerHelper); } @@ -1999,7 +2005,7 @@ void sexp_tree_view::editActionHandlerHelper(){ auto item = currentItem(); // Just to be extra safe, early exit when an item is not selected - if (item == nullptr || !_interface) { + if (_currently_editing || _opPopupActive || item == nullptr || !_interface) { return; } From d46fd2aac596fc449d8e09f13790c78ae66bd2d2 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 Jul 2026 22:38:33 -0400 Subject: [PATCH 8/8] Put this in the correct place --- qtfred/src/ui/widgets/sexp_tree_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/widgets/sexp_tree_view.cpp b/qtfred/src/ui/widgets/sexp_tree_view.cpp index 22a677e8c3f..10ea09b578c 100644 --- a/qtfred/src/ui/widgets/sexp_tree_view.cpp +++ b/qtfred/src/ui/widgets/sexp_tree_view.cpp @@ -1151,7 +1151,6 @@ std::unique_ptr sexp_tree_view::buildContextMenu(QTreeWidgetItem* h) { auto replace_op_menu = popup_menu->addMenu(tr("Replace Operator")); auto replace_data_menu = popup_menu->addMenu(tr("Replace Data")); - popup_menu->addAction(tr("Search for Replacement"), QKeySequence(Qt::CTRL | Qt::Key_S), this, [this]() { editActionHandlerHelper(); }); auto replace_number_act = replace_data_menu->addAction(tr("Number"), this, [this]() { replaceNumberDataHandler(); }); @@ -1161,6 +1160,7 @@ std::unique_ptr sexp_tree_view::buildContextMenu(QTreeWidgetItem* h) { replace_string_act->setEnabled(false); replace_data_menu->addSeparator(); + popup_menu->addAction(tr("Search for Replacement"), QKeySequence(Qt::CTRL | Qt::Key_S), this, [this]() { editActionHandlerHelper(); }); popup_menu->addSection("Variables"); auto modify_variable_act = popup_menu->addAction(tr("Add/Modify Variable"), this, [this]() {