Files
Mike Nelson 4d8538e5cc Qtfred sexp tree refactor (#7301)
* long project will be long

* add shared foundation files

* unify some shared types across both editors

* sexptreemodel framework

* move opf functions to the model

* move shared functions into the model

* move variable/container stuff to the model

* sexptreeactions and ui interface

* wire the model into the ui

* move some shared logic to the model

* move more methods to the model

* move clipboard to model

* create new right click menu model

* begin connecting the new right click menu

* wire up the new right click menu in fred

* wire up the new right click menu in qtfred

* move help text and visuals to model

* move menu option enable disable to model

* wire up sexp_tree to the model

* rename sexp_tree files to sexp_tree_ui

* fix build errors

* fix warning

* remove all opf forwarders

* remove some forwarder methods

* connect qtfred events editor directly to model

* wite plan for unifying tree types

* replace legacy m_mode with treeflags

* fix qtfred campaign dialog

* fix qtfred ship/wing dialog trees

* build error

* documentation

* more documentation

* even more documentation

* move more opf functions to opf file

* clean up context menu function

* move default argument to opf file

* build errors

* fix qtfred dialogs and annoations

* new shared annotations model

* sexp_tree_ui to sexp_tree_view

* documentation pass

* document annotations model

* delete the plan

* move event annotations to the new model

* generic cleanup

* remove some old defines

* cleanup includes

* more cleanup

* parenthesis

* more parenthesis

* variable collisions

* too many parenthesis this time

* clang modernization

* variable names again

* more clang and assertions

* static cast

* clang again

* fix some bugs and issues

* Refactor sexp tree action duplication into SexpTreeActions

* fix a few more minor issues

* avoid clang false positive

* rebase tweaks

* fix annotations issue

* fix typo and two small bugs claude found

* fix event editor tree widget loading

* fix ship and wing sexp tree editors in qtfred

* fix qtfred briefing and debriefing trees

* fix a regression Claude found

* address feedback

* fix annotations for root items in qtfred by copying fred's method

* address feedback

* rescope variables

* clang
2026-07-04 02:12:34 +02:00

628 lines
15 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include "stdafx.h"
#include "FRED.h"
#include "FREDDoc.h"
#include "FREDView.h"
#include "globalincs/linklist.h"
#include "parse/sexp.h"
#include "MissionGoalsDlg.h"
#include "Management.h"
#include "OperatorArgTypeSelect.h"
#define ID_ADD_SHIPS 9000
#define ID_REPLACE_SHIPS 11000
#define ID_ADD_WINGS 13000
#define ID_REPLACE_WINGS 15000
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CMissionGoalsDlg *Goal_editor_dlg; // global reference needed by sexp_tree_view class
/////////////////////////////////////////////////////////////////////////////
// sexp_goal_tree class member functions
/////////////////////////////////////////////////////////////////////////////
// CMissionGoalsDlg dialog class member functions
CMissionGoalsDlg::CMissionGoalsDlg(CWnd* pParent /*=NULL*/)
: CDialog(CMissionGoalsDlg::IDD, pParent),
SexpTreeEditorInterface({ TreeFlags::LabeledRoot, TreeFlags::RootDeletable })
{
//{{AFX_DATA_INIT(CMissionGoalsDlg)
m_goal_desc = _T("");
m_goal_type = -1;
m_display_goal_types = 0;
m_name = _T("");
m_goal_invalid = FALSE;
m_goal_score = 0;
m_no_music = FALSE;
m_team = -1;
//}}AFX_DATA_INIT
m_goals_tree._model._interface = this;
m_goals_tree._model.modified = &modified;
modified = 0;
select_sexp_node = -1;
}
int CMissionGoalsDlg::onRootDeleted(int formula_node)
{
int i;
for (i = 0; i < (int)m_goals.size(); i++) {
if (m_goals[i].formula == formula_node) {
break;
}
}
Assert(i < (int)m_goals.size());
m_goals.erase(m_goals.begin() + i);
m_sig.erase(m_sig.begin() + i);
return formula_node;
}
void CMissionGoalsDlg::onRootInserted(int old_formula, int new_formula) { insert_handler(old_formula, new_formula); }
void CMissionGoalsDlg::onRootMoved(int node1, int node2, bool insert_before) { move_handler(node1, node2, insert_before); }
BOOL CMissionGoalsDlg::OnInitDialog()
{
int i, adjust = 0;
CDialog::OnInitDialog(); // let the base class do the default work
if (!Show_sexp_help)
{
CRect rect;
GetDlgItem(IDC_HELP_BOX)->GetWindowRect(rect);
adjust = rect.top - rect.bottom - 20;
}
theApp.init_window(&Mission_goals_wnd_data, this, adjust);
m_goals_tree.setup((CEdit *) GetDlgItem(IDC_HELP_BOX));
load_tree();
create_tree();
Goal_editor_dlg = this;
i = m_goals_tree.select_sexp_node;
if (i != -1) {
GetDlgItem(IDC_GOALS_TREE) -> SetFocus();
m_goals_tree.hilite_item(i);
return FALSE;
}
return TRUE;
}
void CMissionGoalsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMissionGoalsDlg)
DDX_Control(pDX, IDC_GOALS_TREE, m_goals_tree);
DDX_Text(pDX, IDC_GOAL_DESC, m_goal_desc);
DDX_CBIndex(pDX, IDC_GOAL_TYPE_DROP, m_goal_type);
DDX_CBIndex(pDX, IDC_DISPLAY_GOAL_TYPES_DROP, m_display_goal_types);
DDX_Text(pDX, IDC_GOAL_NAME, m_name);
DDV_MaxChars(pDX, m_name, NAME_LENGTH - 1);
DDX_Check(pDX, IDC_GOAL_INVALID, m_goal_invalid);
DDX_Text(pDX, IDC_GOAL_SCORE, m_goal_score);
DDX_Check(pDX, IDC_NO_MUSIC, m_no_music);
DDX_CBIndex(pDX, IDC_OBJ_TEAM, m_team);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CMissionGoalsDlg, CDialog)
//{{AFX_MSG_MAP(CMissionGoalsDlg)
ON_CBN_SELCHANGE(IDC_DISPLAY_GOAL_TYPES_DROP, OnSelchangeDisplayGoalTypesDrop)
ON_NOTIFY(TVN_SELCHANGED, IDC_GOALS_TREE, OnSelchangedGoalsTree)
ON_NOTIFY(NM_RCLICK, IDC_GOALS_TREE, OnRclickGoalsTree)
ON_NOTIFY(TVN_ENDLABELEDIT, IDC_GOALS_TREE, OnEndlabeleditGoalsTree)
ON_NOTIFY(TVN_BEGINLABELEDIT, IDC_GOALS_TREE, OnBeginlabeleditGoalsTree)
ON_BN_CLICKED(IDC_BUTTON_NEW_GOAL, OnButtonNewGoal)
ON_EN_CHANGE(IDC_GOAL_DESC, OnChangeGoalDesc)
ON_EN_CHANGE(IDC_GOAL_RATING, OnChangeGoalRating)
ON_CBN_SELCHANGE(IDC_GOAL_TYPE_DROP, OnSelchangeGoalTypeDrop)
ON_EN_CHANGE(IDC_GOAL_NAME, OnChangeGoalName)
ON_BN_CLICKED(ID_OK, OnButtonOk)
ON_WM_CLOSE()
ON_BN_CLICKED(IDC_GOAL_INVALID, OnGoalInvalid)
ON_EN_CHANGE(IDC_GOAL_SCORE, OnChangeGoalScore)
ON_BN_CLICKED(IDC_NO_MUSIC, OnNoMusic)
ON_CBN_SELCHANGE(IDC_OBJ_TEAM, OnSelchangeTeam)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMissionGoalsDlg message handlers
// Initialization: sets up internal working copy of mission goals and goal trees.
void CMissionGoalsDlg::load_tree()
{
int i;
m_goals_tree.select_sexp_node = select_sexp_node;
select_sexp_node = -1;
m_goals_tree.clear_tree();
m_goals.clear();
m_sig.clear();
for (i=0; i<(int)Mission_goals.size(); i++) {
m_goals.push_back(Mission_goals[i]);
m_sig.push_back(i);
if (m_goals[i].name.empty())
m_goals[i].name = "<Unnamed>";
m_goals[i].formula = m_goals_tree._model.load_sub_tree(Mission_goals[i].formula, true, "true");
}
m_goals_tree._model.post_load();
cur_goal = -1;
update_cur_goal();
}
// create the CTreeCtrl tree from the goal tree, filtering based on m_display_goal_types
void CMissionGoalsDlg::create_tree()
{
int i;
HTREEITEM h;
m_goals_tree.DeleteAllItems();
m_goals_tree.reset_handles();
for (i=0; i<(int)m_goals.size(); i++) {
if ( (m_goals[i].type & GOAL_TYPE_MASK) != m_display_goal_types)
continue;
h = m_goals_tree.insert(m_goals[i].name.c_str());
m_goals_tree.SetItemData(h, m_goals[i].formula);
m_goals_tree.add_sub_tree(m_goals[i].formula, h);
}
cur_goal = -1;
update_cur_goal();
}
// Display goal types selection changed, so update the display
void CMissionGoalsDlg::OnSelchangeDisplayGoalTypesDrop()
{
UpdateData(TRUE);
create_tree();
}
// New tree item selected. Because goal info is displayed for the selected tree item,
// we need to update the display when this occurs.
void CMissionGoalsDlg::OnSelchangedGoalsTree(NMHDR* pNMHDR, LRESULT* pResult)
{
int i, z;
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
HTREEITEM h, h2;
h = pNMTreeView->itemNew.hItem;
if (!h)
return;
m_goals_tree.update_help(h);
while ((h2 = m_goals_tree.GetParentItem(h)) != 0)
h = h2;
z = (int)m_goals_tree.GetItemData(h);
for (i=0; i<(int)m_goals.size(); i++)
if (m_goals[i].formula == z)
break;
Assert(i < (int)m_goals.size());
cur_goal = i;
update_cur_goal();
*pResult = 0;
}
// update display info to reflect the currently selected goal.
void CMissionGoalsDlg::update_cur_goal()
{
if (cur_goal < 0) {
m_name = _T("");
m_goal_desc = _T("");
m_goal_type = -1;
m_team = 0;
UpdateData(FALSE);
GetDlgItem(IDC_GOAL_TYPE_DROP) -> EnableWindow(FALSE);
GetDlgItem(IDC_GOAL_NAME) -> EnableWindow(FALSE);
GetDlgItem(IDC_GOAL_DESC) -> EnableWindow(FALSE);
GetDlgItem(IDC_GOAL_INVALID)->EnableWindow(FALSE);
GetDlgItem(IDC_GOAL_SCORE)->EnableWindow(FALSE);
GetDlgItem(IDC_NO_MUSIC)->EnableWindow(FALSE);
GetDlgItem(IDC_OBJ_TEAM)->EnableWindow(FALSE);
UpdateData(FALSE);
return;
}
m_name = _T(m_goals[cur_goal].name.c_str());
m_goal_desc = _T(m_goals[cur_goal].message.c_str());
m_goal_type = m_goals[cur_goal].type & GOAL_TYPE_MASK;
if ( m_goals[cur_goal].type & INVALID_GOAL ){
m_goal_invalid = 1;
} else {
m_goal_invalid = 0;
}
if ( m_goals[cur_goal].flags & MGF_NO_MUSIC ){
m_no_music = 1;
} else {
m_no_music = 0;
}
m_goal_score = m_goals[cur_goal].score;
m_team = m_goals[cur_goal].team;
UpdateData(FALSE);
GetDlgItem(IDC_GOAL_TYPE_DROP) -> EnableWindow(TRUE);
GetDlgItem(IDC_GOAL_NAME) -> EnableWindow(TRUE);
GetDlgItem(IDC_GOAL_DESC) -> EnableWindow(TRUE);
// GetDlgItem(IDC_GOAL_RATING) -> EnableWindow(TRUE);
GetDlgItem(IDC_GOAL_INVALID)->EnableWindow(TRUE);
GetDlgItem(IDC_GOAL_SCORE)->EnableWindow(TRUE);
GetDlgItem(IDC_NO_MUSIC)->EnableWindow(TRUE);
GetDlgItem(IDC_OBJ_TEAM)->EnableWindow(FALSE);
if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ){
GetDlgItem(IDC_OBJ_TEAM)->EnableWindow(TRUE);
}
UpdateData(FALSE);
}
// handler for context menu (i.e. a right mouse button click).
void CMissionGoalsDlg::OnRclickGoalsTree(NMHDR* pNMHDR, LRESULT* pResult)
{
m_goals_tree.right_clicked();
*pResult = 0;
}
// goal tree item label editing is requested. Determine if it should be allowed.
void CMissionGoalsDlg::OnBeginlabeleditGoalsTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
if (m_goals_tree.edit_label(pTVDispInfo->item.hItem) == 1) {
*pResult = 0;
modified = 1;
} else {
*pResult = 1;
}
}
// Once we finish editing, we need to clean up, which we do here.
void CMissionGoalsDlg::OnEndlabeleditGoalsTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
*pResult = m_goals_tree.end_label_edit(pTVDispInfo->item);
}
void CMissionGoalsDlg::OnOK()
{
HWND h;
CWnd *w;
w = GetFocus();
if (w) {
h = w->m_hWnd;
GetDlgItem(IDC_GOALS_TREE)->SetFocus();
::SetFocus(h);
}
}
int CMissionGoalsDlg::query_modified()
{
int i;
if (modified)
return 1;
if (Mission_goals.size() != m_goals.size())
return 1;
for (i=0; i<(int)Mission_goals.size(); i++) {
if (!lcase_equal(Mission_goals[i].name, m_goals[i].name))
return 1;
if (!lcase_equal(Mission_goals[i].message, m_goals[i].message))
return 1;
if (Mission_goals[i].type != m_goals[i].type)
return 1;
if ( Mission_goals[i].score != m_goals[i].score )
return 1;
if ( Mission_goals[i].team != m_goals[i].team )
return 1;
}
return 0;
}
void CMissionGoalsDlg::OnButtonOk()
{
SCP_vector<std::pair<SCP_string, SCP_string>> names;
int i;
UpdateData(TRUE);
if (query_modified())
set_modified();
for (auto &goal: Mission_goals) {
free_sexp2(goal.formula);
goal.satisfied = 0; // use this as a processed flag
}
// rename all sexp references to old goals
for (i=0; i<(int)m_goals.size(); i++) {
if (m_sig[i] >= 0) {
names.emplace_back(Mission_goals[m_sig[i]].name, m_goals[i].name);
Mission_goals[m_sig[i]].satisfied = 1;
}
}
// invalidate all sexp references to deleted goals.
for (const auto &goal: Mission_goals) {
if (!goal.satisfied) {
SCP_string buf = "<" + goal.name + ">";
// force it to not be too long
if (SCP_truncate(buf, NAME_LENGTH - 1))
buf.back() = '>';
names.emplace_back(goal.name, buf);
}
}
// copy all dialog goals to the mission
Mission_goals.clear();
for (const auto &dialog_goal: m_goals) {
Mission_goals.push_back(dialog_goal);
Mission_goals.back().formula = m_goals_tree._model.save_tree(dialog_goal.formula);
if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) {
Assert( dialog_goal.team != -1 );
}
}
// now update all sexp references
for (const auto &name_pair: names)
update_sexp_references(name_pair.first.c_str(), name_pair.second.c_str(), OPF_GOAL_NAME);
theApp.record_window_data(&Mission_goals_wnd_data, this);
CDialog::OnOK();
FREDDoc_ptr->autosave("goal editor");
}
void CMissionGoalsDlg::OnButtonNewGoal()
{
int index;
HTREEITEM h;
m_goals.emplace_back();
m_sig.push_back(-1);
m_goals.back().type = m_display_goal_types; // this also marks the goal as valid since bit not set
m_goals.back().name = "Goal name";
m_goals.back().message = "Mission goal text";
h = m_goals_tree.insert(m_goals.back().name.c_str());
m_goals_tree.item_index = -1;
m_goals_tree.add_operator("true", h);
index = m_goals.back().formula = m_goals_tree.item_index;
m_goals_tree.SetItemData(h, index);
m_goals_tree.SelectItem(h);
}
void CMissionGoalsDlg::OnChangeGoalDesc()
{
if (cur_goal < 0){
return;
}
UpdateData(TRUE);
lcl_fred_replace_stuff(m_goal_desc);
string_copy(m_goals[cur_goal].message, m_goal_desc);
}
void CMissionGoalsDlg::OnChangeGoalRating()
{
if (cur_goal < 0){
return;
}
UpdateData(TRUE);
}
void CMissionGoalsDlg::OnSelchangeGoalTypeDrop()
{
HTREEITEM h, h2;
int otype;
if (cur_goal < 0){
return;
}
UpdateData(TRUE);
UpdateData(TRUE); // doesn't seem to update unless we do it twice..
// change the type being sure to keep the invalid bit if set
otype = m_goals[cur_goal].type;
m_goals[cur_goal].type = m_goal_type;
if ( otype & INVALID_GOAL ){
m_goals[cur_goal].type |= INVALID_GOAL;
}
h = m_goals_tree.GetSelectedItem();
Assert(h);
while ((h2 = m_goals_tree.GetParentItem(h)) != 0){
h = h2;
}
m_goals_tree.DeleteItem(h);
cur_goal = -1;
update_cur_goal();
}
void CMissionGoalsDlg::OnChangeGoalName()
{
HTREEITEM h, h2;
if (cur_goal < 0){
return;
}
UpdateData(TRUE);
h = m_goals_tree.GetSelectedItem();
if (!h){
return;
}
while ((h2 = m_goals_tree.GetParentItem(h)) != 0){
h = h2;
}
m_goals_tree.SetItemText(h, m_name);
string_copy(m_goals[cur_goal].name, m_name);
}
void CMissionGoalsDlg::OnCancel()
{
theApp.record_window_data(&Messages_wnd_data, this);
CDialog::OnCancel();
}
BOOL CMissionGoalsDlg::DestroyWindow()
{
Goal_editor_dlg = nullptr;
return CDialog::DestroyWindow();
}
void CMissionGoalsDlg::OnClose()
{
int z;
if (query_modified()) {
z = MessageBox("Do you want to keep your changes?", "Close", MB_ICONQUESTION | MB_YESNOCANCEL);
if (z == IDCANCEL)
return;
if (z == IDYES) {
OnButtonOk();
return;
}
}
CDialog::OnClose();
}
void CMissionGoalsDlg::insert_handler(int old, int node)
{
int i;
for (i=0; i<(int)m_goals.size(); i++){
if (m_goals[i].formula == old){
break;
}
}
Assert(i < (int)m_goals.size());
m_goals[i].formula = node;
return;
}
void CMissionGoalsDlg::OnGoalInvalid()
{
if ( cur_goal < 0 ){
return;
}
m_goal_invalid = !m_goal_invalid;
m_goals[cur_goal].type ^= INVALID_GOAL;
UpdateData(TRUE);
}
void CMissionGoalsDlg::OnNoMusic()
{
if (cur_goal < 0){
return;
}
m_no_music = !m_no_music;
m_goals[cur_goal].flags ^= MGF_NO_MUSIC;
UpdateData(TRUE);
}
void CMissionGoalsDlg::move_handler(int node1, int node2, bool insert_before)
{
int index1, index2, s;
mission_goal g;
for (index1=0; index1<(int)m_goals.size(); index1++){
if (m_goals[index1].formula == node1){
break;
}
}
Assert(index1 < (int)m_goals.size());
for (index2=0; index2<(int)m_goals.size(); index2++){
if (m_goals[index2].formula == node2){
break;
}
}
Assert(index2 < (int)m_goals.size());
g = m_goals[index1];
s = m_sig[index1];
int offset = insert_before ? -1 : 0;
while (index1 < index2 + offset) {
m_goals[index1] = m_goals[index1 + 1];
m_sig[index1] = m_sig[index1 + 1];
index1++;
}
while (index1 > index2 + offset + 1) {
m_goals[index1] = m_goals[index1 - 1];
m_sig[index1] = m_sig[index1 - 1];
index1--;
}
m_goals[index1] = g;
m_sig[index1] = s;
}
void CMissionGoalsDlg::OnChangeGoalScore()
{
if (cur_goal < 0){
return;
}
UpdateData(TRUE);
m_goals[cur_goal].score = m_goal_score;
}
// code when the "team" selection in the combo box changes
void CMissionGoalsDlg::OnSelchangeTeam()
{
if ( cur_goal < 0 ){
return;
}
UpdateData(TRUE);
m_goals[cur_goal].team = m_team;
}