Files
fs2open.github.com/fred2/messageeditordlg.cpp
T
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

580 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 "MessageEditorDlg.h"
#include "FREDDoc.h"
#include "Management.h"
#include "sexp_tree_view.h"
#include "EventEditor.h"
#include "mission/missionmessage.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CMessageEditorDlg *Message_editor_dlg = NULL;
/////////////////////////////////////////////////////////////////////////////
// CMessageEditorDlg dialog
CMessageEditorDlg::CMessageEditorDlg(CWnd* pParent /*=NULL*/)
: CDialog(CMessageEditorDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CMessageEditorDlg)
m_avi_filename = _T("");
m_wave_filename = _T("");
m_message_text = _T("");
m_message_name = _T("");
m_cur_msg = -1;
m_priority = -1;
m_sender = -1;
m_persona = -1;
//}}AFX_DATA_INIT
m_tree._model.modified = &modified;
modified = 0;
m_event_num = -1;
}
void CMessageEditorDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMessageEditorDlg)
DDX_Control(pDX, IDC_TREE, m_tree);
DDX_CBString(pDX, IDC_AVI_FILENAME, m_avi_filename);
DDV_MaxChars(pDX, m_avi_filename, MAX_FILENAME_LEN - 1);
DDX_CBString(pDX, IDC_WAVE_FILENAME, m_wave_filename);
DDV_MaxChars(pDX, m_wave_filename, MAX_FILENAME_LEN - 1);
DDX_Text(pDX, IDC_MESSAGE_TEXT, m_message_text);
DDV_MaxChars(pDX, m_message_text, MESSAGE_LENGTH - 1);
DDX_Text(pDX, IDC_NAME, m_message_name);
DDV_MaxChars(pDX, m_message_name, NAME_LENGTH - 1);
DDX_LBIndex(pDX, IDC_MESSAGE_LIST, m_cur_msg);
DDX_CBIndex(pDX, IDC_PRIORITY, m_priority);
DDX_CBIndex(pDX, IDC_SENDER, m_sender);
DDX_CBIndex(pDX, IDC_PERSONA_NAME, m_persona);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CMessageEditorDlg, CDialog)
//{{AFX_MSG_MAP(CMessageEditorDlg)
ON_LBN_SELCHANGE(IDC_MESSAGE_LIST, OnSelchangeMessageList)
ON_EN_UPDATE(IDC_NAME, OnUpdateName)
ON_BN_CLICKED(IDC_DELETE, OnDelete)
ON_BN_CLICKED(IDC_NEW, OnNew)
ON_WM_CLOSE()
ON_BN_CLICKED(IDC_BROWSE_AVI, OnBrowseAvi)
ON_BN_CLICKED(IDC_BROWSE_WAVE, OnBrowseWave)
ON_NOTIFY(NM_RCLICK, IDC_TREE, OnRclickTree)
ON_NOTIFY(TVN_BEGINLABELEDIT, IDC_TREE, OnBeginlabeleditTree)
ON_NOTIFY(TVN_ENDLABELEDIT, IDC_TREE, OnEndlabeleditTree)
ON_BN_CLICKED(ID_OK, OnButtonOk)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMessageEditorDlg message handlers
BOOL CMessageEditorDlg::OnInitDialog()
{
int i;
CListBox *list;
CComboBox *box;
CDialog::OnInitDialog();
theApp.init_window(&Messages_wnd_data, this);
m_tree.setup();
((CEdit *) GetDlgItem(IDC_NAME))->LimitText(NAME_LENGTH - 1);
((CEdit *) GetDlgItem(IDC_MESSAGE_TEXT))->LimitText(MESSAGE_LENGTH - 1);
((CComboBox *) GetDlgItem(IDC_AVI_FILENAME))->LimitText(MAX_FILENAME_LEN - 1);
((CComboBox *) GetDlgItem(IDC_WAVE_FILENAME))->LimitText(MAX_FILENAME_LEN - 1);
list = (CListBox *) GetDlgItem(IDC_MESSAGE_LIST);
list->ResetContent();
for (i=0; i<Num_messages; i++) {
//Assert(list->FindStringExact(-1, Messages[i].name) == CB_ERR);
// mwa we should probably not include builtin messages into this list!
list->AddString(Messages[i].name);
}
box = (CComboBox *) GetDlgItem(IDC_AVI_FILENAME);
for (i=0; i<Num_messages; i++)
if (Messages[i].avi_info.name)
if (box->FindStringExact(-1, Messages[i].avi_info.name) == CB_ERR)
box->AddString(Messages[i].avi_info.name);
box = (CComboBox *) GetDlgItem(IDC_WAVE_FILENAME);
for (i=0; i<Num_messages; i++)
if (Messages[i].wave_info.name)
if (box->FindStringExact(i, Messages[i].wave_info.name) == CB_ERR)
box->AddString(Messages[i].wave_info.name);
// add the persona names into the combo box
box = (CComboBox *)GetDlgItem(IDC_PERSONA_NAME);
box->ResetContent();
box->AddString("<None>");
for (const auto &persona: Personas)
box->AddString(persona.name);
box = (CComboBox *) GetDlgItem(IDC_SENDER);
for (i=0; i<MAX_SHIPS; i++)
if ((Ships[i].objnum >= 0) && (Objects[Ships[i].objnum].type == OBJ_SHIP))
box->AddString(Ships[i].ship_name);
for (i=0; i<MAX_WINGS; i++)
if (Wings[i].wave_count)
box->AddString(Wings[i].name);
box->AddString("<Any wingman>");
// MWA 4/7/98 -- removed any allied
//box->AddString("<Any allied>");
// set the first message to be the first non-builtin message (if it exists)
if ( Num_messages > Num_builtin_messages )
m_cur_msg = Num_builtin_messages;
else if (Num_messages)
m_cur_msg = 0;
else
m_cur_msg = -1;
update_cur_message();
return TRUE;
}
int CMessageEditorDlg::query_modified()
{
char *ptr, buf[MESSAGE_LENGTH];
int i;
UpdateData(TRUE);
if (modified)
return 1;
if (m_cur_msg < 0)
return 0;
ptr = (char *) (LPCTSTR) m_message_name;
for (i=0; i<Num_messages; i++)
if ((i != m_cur_msg) && (!stricmp(ptr, Messages[i].name)))
break;
if (i < Num_messages)
if (stricmp(ptr, Messages[m_cur_msg].name))
return 1; // name is different and allowed to update
string_copy(buf, m_message_text, MESSAGE_LENGTH - 1);
if (stricmp(buf, Messages[m_cur_msg].message))
return 1;
ptr = (char *) (LPCTSTR) m_avi_filename;
if (advanced_stricmp(ptr, Messages[m_cur_msg].avi_info.name))
return 1;
ptr = (char *) (LPCTSTR) m_wave_filename;
if (advanced_stricmp(ptr, Messages[m_cur_msg].wave_info.name))
return 1;
// check to see if persona changed. use -1 since we stuck a "None" for persona
// at the beginning of the list.
if ( (m_persona - 1 ) != Messages[m_cur_msg].persona_index )
return 1;
if (m_tree._model.query_false()) {
if (m_event_num >= 0)
return 1;
} else {
if (m_event_num < 0)
return 1;
}
return 0;
}
void CMessageEditorDlg::OnOK()
{
}
void CMessageEditorDlg::OnButtonOk()
{
update(m_cur_msg);
theApp.record_window_data(&Messages_wnd_data, this);
delete Message_editor_dlg;
Message_editor_dlg = NULL;
FREDDoc_ptr->autosave("message editor");
}
void CMessageEditorDlg::OnCancel()
{
theApp.record_window_data(&Messages_wnd_data, this);
delete Message_editor_dlg;
Message_editor_dlg = NULL;
}
// load controls with structure data
void CMessageEditorDlg::update_cur_message()
{
int node, enable = TRUE, enable2 = TRUE;
if (m_cur_msg < 0)
{
enable = enable2 = FALSE;
m_message_name = _T("");
m_message_text = _T("");
m_avi_filename = _T("");
m_wave_filename = _T("");
m_tree.clear_tree();
m_persona = 0;
m_sender = m_priority = -1;
} else {
m_message_name = Messages[m_cur_msg].name;
m_message_text = Messages[m_cur_msg].message;
if (Messages[m_cur_msg].avi_info.name)
m_avi_filename = _T(Messages[m_cur_msg].avi_info.name);
else
m_avi_filename = _T("");
if (Messages[m_cur_msg].wave_info.name)
m_wave_filename = _T(Messages[m_cur_msg].wave_info.name);
else
m_wave_filename = _T("");
// add persona id
if ( Messages[m_cur_msg].persona_index != -1 )
m_persona = Messages[m_cur_msg].persona_index + 1; // add one for the "none" at the beginning of the list
else
m_persona = 0;
m_event_num = find_event();
if (m_event_num < 0) {
node = -1;
m_sender = m_priority = 0;
} else
node = CADR(Mission_events[m_event_num].formula);
m_tree.load_tree(node, "false");
}
if (m_cur_msg < Num_builtin_messages)
enable = FALSE;
GetDlgItem(IDC_NAME)->EnableWindow(enable);
GetDlgItem(IDC_MESSAGE_TEXT)->EnableWindow(enable);
GetDlgItem(IDC_AVI_FILENAME)->EnableWindow(enable);
GetDlgItem(IDC_WAVE_FILENAME)->EnableWindow(enable);
GetDlgItem(IDC_DELETE)->EnableWindow(enable);
GetDlgItem(IDC_TREE)->EnableWindow(enable2);
GetDlgItem(IDC_SENDER)->EnableWindow(enable2);
GetDlgItem(IDC_PRIORITY)->EnableWindow(enable2);
GetDlgItem(IDC_PERSONA_NAME)->EnableWindow(enable);
UpdateData(FALSE);
}
int CMessageEditorDlg::find_event()
{
const char *str;
int i, formula, node;
CComboBox *box;
for (i=0; i<(int)Mission_events.size(); i++) {
node = Mission_events[i].formula;
Assertion(node >= 0, "Can't have a formula point to sexp node -1!");
int op_const = get_operator_const(node);
if (op_const == OP_WHEN || op_const == OP_EVERY_TIME
|| op_const == OP_WHEN_ARGUMENT || op_const == OP_EVERY_TIME_ARGUMENT
|| op_const == OP_IF_THEN_ELSE || op_const == OP_SWITCH
|| op_const == OP_PERFORM_ACTIONS_BOOL_FIRST || op_const == OP_PERFORM_ACTIONS_BOOL_LAST )
{
// Goober5000 - the bool part of the *-argument conditional starts at the second, not first, argument
if (is_when_argument_op(op_const))
node = CDR(node);
node = CDR(node);
formula = CAR(node); // bool conditional
if (CDR(CDR(node)) == -1) { // only 1 action
node = CADR(node);
if ((get_operator_const(node) == OP_SEND_MESSAGE) && !stricmp(CTEXT(CDDDR(node)), m_message_name)) {
box = (CComboBox *) GetDlgItem(IDC_SENDER);
str = CTEXT(CDR(node));
m_sender = box->FindStringExact(-1, str);
if (m_sender == CB_ERR)
m_sender = 0;
box = (CComboBox *) GetDlgItem(IDC_PRIORITY);
str = CTEXT(CDDR(node));
m_priority = box->FindStringExact(-1, str);
if (m_priority == CB_ERR)
m_priority = 0;
return i;
}
}
}
}
m_sender = m_priority = 0;
return -1;
}
void CMessageEditorDlg::OnSelchangeMessageList()
{
int old = m_cur_msg;
static int flag = 0;
if (flag)
return;
if (update(m_cur_msg)) {
flag = 1;
((CListBox *) GetDlgItem(IDC_MESSAGE_LIST)) -> SetCurSel(old);
m_cur_msg = old;
flag = 0;
return;
}
update_cur_message();
}
void CMessageEditorDlg::OnUpdateName()
{
}
int CMessageEditorDlg::update(int num)
{
char *ptr;
int i, node, fnode;
CString buf;
CListBox *list;
UpdateData(TRUE);
if (num >= 0)
{
ptr = (char *) (LPCTSTR) m_message_name;
for (i=0; i<Num_messages; i++)
if ((i != num) && (!stricmp(m_message_name, Messages[i].name)))
break;
if (i == Num_messages) { // update name if no conflicts, otherwise keep old name
update_sexp_references(Messages[num].name, ptr, OPF_MESSAGE);
update_sexp_references(Messages[num].name, ptr, OPF_MESSAGE_OR_STRING);
string_copy(Messages[num].name, m_message_name, NAME_LENGTH - 1);
list = (CListBox *) GetDlgItem(IDC_MESSAGE_LIST);
list->DeleteString(num);
list->InsertString(num, m_message_name);
}
string_copy(Messages[num].message, m_message_text, MESSAGE_LENGTH - 1);
if (Messages[num].avi_info.name)
free(Messages[num].avi_info.name);
ptr = (char *) (LPCTSTR) m_avi_filename;
if (!ptr || !strlen(ptr))
Messages[num].avi_info.name = NULL;
else
Messages[num].avi_info.name = strdup(ptr);
if (Messages[num].wave_info.name)
free(Messages[num].wave_info.name);
ptr = (char *) (LPCTSTR) m_wave_filename;
if (!ptr || !strlen(ptr))
Messages[num].wave_info.name = NULL;
else
Messages[num].wave_info.name = strdup(ptr);
// update the persona to the message. We subtract 1 for the "None" at the beginning of the combo
// box list.
Messages[num].persona_index = m_persona - 1;
if (m_tree._model.query_false()) {
if (m_event_num >= 0) { // need to delete event
i = m_event_num;
free_sexp2(Mission_events[i].formula);
Assert(i < (int)Mission_events.size());
Mission_events.erase(Mission_events.begin() + i);
m_event_num = -1;
}
} else {
if (m_event_num >= 0)
free_sexp2(Mission_events[m_event_num].formula);
else {
Mission_events.emplace_back();
m_event_num = (int)Mission_events.size() - 1;
Mission_events[m_event_num].name = m_message_name;
}
fnode = m_tree._model.save_tree();
ptr = (char *) (LPCTSTR) m_message_name;
node = alloc_sexp(ptr, SEXP_ATOM, SEXP_ATOM_STRING, -1, -1);
((CComboBox *) GetDlgItem(IDC_PRIORITY))->GetLBText(m_priority, buf);
node = alloc_sexp((char *) (LPCTSTR) buf, SEXP_ATOM, SEXP_ATOM_STRING, -1, node);
((CComboBox *) GetDlgItem(IDC_SENDER))->GetLBText(m_sender, buf);
node = alloc_sexp((char *) (LPCTSTR) buf, SEXP_ATOM, SEXP_ATOM_STRING, -1, node);
node = alloc_sexp("send-message", SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, node);
node = alloc_sexp("", SEXP_LIST, SEXP_ATOM_LIST, node, -1);
node = alloc_sexp("", SEXP_LIST, SEXP_ATOM_LIST, fnode, node);
node = alloc_sexp("when", SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, node);
Mission_events[m_event_num].formula = node;
}
}
if (query_modified())
set_modified();
modified = 0;
return 0;
}
void CMessageEditorDlg::OnDelete()
{
char buf[256];
int i;
Assert((m_cur_msg >= 0) && (m_cur_msg < Num_messages));
message_free_media_names(Messages[m_cur_msg]);
((CListBox *) GetDlgItem(IDC_MESSAGE_LIST))->DeleteString(m_cur_msg);
sprintf(buf, "<%s>", Messages[m_cur_msg].name);
update_sexp_references(Messages[m_cur_msg].name, buf, OPF_MESSAGE);
update_sexp_references(Messages[m_cur_msg].name, buf, OPF_MESSAGE_OR_STRING);
for (i=m_cur_msg; i<Num_messages-1; i++)
Messages[i] = std::move(Messages[i + 1]);
Num_messages--;
if (m_cur_msg >= Num_messages)
m_cur_msg = Num_messages - 1;
GetDlgItem(IDC_NEW)->EnableWindow(TRUE);
modified = 1;
update_cur_message();
}
void CMessageEditorDlg::OnNew()
{
if (update(m_cur_msg))
return;
strcpy_s(Messages[Num_messages].name, "<new message>");
((CListBox *) GetDlgItem(IDC_MESSAGE_LIST))->AddString("<new message>");
strcpy_s(Messages[Num_messages].message, "<put description here>");
Messages[Num_messages].avi_info.name = NULL;
Messages[Num_messages].wave_info.name = NULL;
Messages[Num_messages].persona_index = -1;
m_cur_msg = Num_messages++;
modified = 1;
update_cur_message();
}
BOOL CMessageEditorDlg::DestroyWindow()
{
Message_editor_dlg = nullptr;
return CDialog::DestroyWindow();
}
void CMessageEditorDlg::OnClose()
{
int z;
modified = query_modified();
if (modified) {
z = MessageBox("Do you want to keep your changes?", "Close", MB_ICONQUESTION | MB_YESNOCANCEL);
if (z == IDCANCEL)
return;
if (z == IDYES) {
OnButtonOk();
return;
}
}
theApp.record_window_data(&Messages_wnd_data, this);
delete Message_editor_dlg;
Message_editor_dlg = NULL;
}
void CMessageEditorDlg::OnBrowseAvi()
{
CString name;
UpdateData(TRUE);
CFileDialog dlg(TRUE, "ani", m_avi_filename, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR,
"Ani Files (*.ani)|*.ani|Eff Files (*.eff)|*.eff|APNG Files (*.png)|*.png|All Anims (*.ani, *.eff, *.png)|*.ani;*.eff;*.png||");
if (dlg.DoModal() == IDOK)
{
m_avi_filename = dlg.GetFileName();
UpdateData(FALSE);
modified = 1;
}
}
void CMessageEditorDlg::OnBrowseWave()
{
CString name;
UpdateData(TRUE);
CFileDialog dlg(TRUE, "wav", m_wave_filename, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR,
"Voice Files (*.ogg, *.wav)|*.ogg;*.wav|Ogg Vorbis Files (*.ogg)|*.ogg|Wave Files (*.wav)|*.wav||");
if (dlg.DoModal() == IDOK)
{
m_wave_filename = dlg.GetFileName();
UpdateData(FALSE);
modified = 1;
}
}
void CMessageEditorDlg::OnRclickTree(NMHDR* pNMHDR, LRESULT* pResult)
{
m_tree.right_clicked();
*pResult = 0;
}
void CMessageEditorDlg::OnBeginlabeleditTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
CEdit *edit;
if (m_tree.edit_label(pTVDispInfo->item.hItem) == 1) {
*pResult = 0;
modified = 1;
edit = m_tree.GetEditControl();
Assert(edit);
edit->SetLimitText(NAME_LENGTH - 1);
} else
*pResult = 1;
}
void CMessageEditorDlg::OnEndlabeleditTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
*pResult = m_tree.end_label_edit(pTVDispInfo->item);
}