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

528 lines
13 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 "DebriefingEditorDlg.h"
#include "FREDDoc.h"
#include "management.h"
#include "mission/missionbriefcommon.h"
#include "mission/missionparse.h"
#include "globalincs/linklist.h"
#include "parse/sexp.h"
#include "gamesnd/eventmusic.h"
#include "cfile/cfile.h"
#include "sound/audiostr.h"
#include "localization/localize.h"
#include "jumpnode/jumpnode.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// debriefing_editor_dlg dialog
debriefing_editor_dlg::debriefing_editor_dlg(CWnd* pParent /*=NULL*/)
: CDialog(debriefing_editor_dlg::IDD, pParent)
{
//{{AFX_DATA_INIT(debriefing_editor_dlg)
m_text = _T("");
m_voice = _T("");
m_stage_title = _T("");
m_rec_text = _T("");
m_debriefPass_music = 0;
m_debriefAvg_music = 0;
m_debriefFail_music = 0;
m_current_debriefing = -1;
//}}AFX_DATA_INIT
modified = 0;
m_cur_stage = 0;
m_last_stage = -1;
m_voice_id = -1;
select_sexp_node = -1;
m_play_icon = nullptr;
}
void debriefing_editor_dlg::create()
{
CDialog::Create(IDD);
theApp.init_window(&Debriefing_wnd_data, this);
}
void debriefing_editor_dlg::focus_sexp(int node)
{
int i, t, n;
n = m_tree.select_sexp_node = node;
if (n == -1)
return;
for (t = 0; t < Num_teams; t++) {
for (i = 0; i < Debriefings[t].num_stages; i++) {
if (query_node_in_sexp(n, Debriefings[t].stages[i].formula)) {
m_current_debriefing = t;
Debriefing = &Debriefings[t];
m_cur_stage = i;
update_data();
GetDlgItem(IDC_TREE)->SetFocus();
m_tree.hilite_item(m_tree.select_sexp_node);
return;
}
}
}
}
void debriefing_editor_dlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(debriefing_editor_dlg)
DDX_Control(pDX, IDC_TREE, m_tree);
DDX_Text(pDX, IDC_TEXT, m_text);
DDX_Text(pDX, IDC_VOICE, m_voice);
DDV_MaxChars(pDX, m_voice, MAX_FILENAME_LEN - 1);
DDX_Text(pDX, IDC_STAGE_TITLE, m_stage_title);
DDX_Text(pDX, IDC_REC_TEXT, m_rec_text);
DDX_CBIndex(pDX, IDC_SUCCESSFUL_MISSION_TRACK, m_debriefPass_music);
DDX_CBIndex(pDX, IDC_DEBRIEFING_TRACK, m_debriefAvg_music);
DDX_CBIndex(pDX, IDC_FAILED_MISSION_TRACK, m_debriefFail_music);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(debriefing_editor_dlg, CDialog)
//{{AFX_MSG_MAP(debriefing_editor_dlg)
ON_BN_CLICKED(IDC_NEXT, OnNext)
ON_BN_CLICKED(IDC_PREV, OnPrev)
ON_BN_CLICKED(IDC_BROWSE, OnBrowse)
ON_BN_CLICKED(IDC_ADD_STAGE, OnAddStage)
ON_BN_CLICKED(IDC_DELETE_STAGE, OnDeleteStage)
ON_BN_CLICKED(IDC_INSERT_STAGE, OnInsertStage)
ON_NOTIFY(NM_RCLICK, IDC_TREE, OnRclickTree)
ON_NOTIFY(TVN_BEGINLABELEDIT, IDC_TREE, OnBeginlabeleditTree)
ON_NOTIFY(TVN_ENDLABELEDIT, IDC_TREE, OnEndlabeleditTree)
ON_WM_CLOSE()
ON_WM_INITMENU()
ON_BN_CLICKED(IDC_PLAY, OnPlay)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// debriefing_editor_dlg message handlers
void debriefing_editor_dlg::OnInitMenu(CMenu* pMenu)
{
int i;
CMenu *m;
// disable any items we should disable
m = pMenu->GetSubMenu(0);
// uncheck all menu items
for (i = 0; i < Num_teams; i++ )
m->CheckMenuItem( i, MF_BYPOSITION | MF_UNCHECKED );
for ( i = Num_teams; i < MAX_TVT_TEAMS; i++ )
m->EnableMenuItem(i, MF_BYPOSITION | MF_GRAYED);
// put a check next to the currently selected item
m->CheckMenuItem(m_current_debriefing, MF_BYPOSITION | MF_CHECKED );
// Karajorma - it might be nice to autobalance the briefings and debriefings but I'm not doing anything till I
// understand how the how system works better. disabling the option for now.
m = pMenu->GetSubMenu(1);
m->EnableMenuItem(ID_AUTOBALANCE, MF_GRAYED);
CDialog::OnInitMenu(pMenu);
}
BOOL debriefing_editor_dlg::OnInitDialog()
{
CDialog::OnInitDialog();
m_play_icon = load_button_icon(IDB_PLAY, RGB(192, 192, 192));
((CButton *) GetDlgItem(IDC_PLAY)) -> SetIcon(m_play_icon);
CComboBox *box;
box = (CComboBox *) GetDlgItem(IDC_ICON_IMAGE);
m_current_debriefing = 0;
UpdateData(FALSE);
Debriefing = &Debriefings[m_current_debriefing];
box = (CComboBox *) GetDlgItem(IDC_SUCCESSFUL_MISSION_TRACK);
box->AddString("None");
for (auto &sm: Spooled_music)
box->AddString(sm.name);
box = (CComboBox *) GetDlgItem(IDC_DEBRIEFING_TRACK);
box->AddString("None");
for (auto &sm: Spooled_music)
box->AddString(sm.name);
box = (CComboBox *) GetDlgItem(IDC_FAILED_MISSION_TRACK);
box->AddString("None");
for (auto &sm: Spooled_music)
box->AddString(sm.name);
m_debriefPass_music = Mission_music[SCORE_DEBRIEFING_SUCCESS] + 1;
m_debriefAvg_music = Mission_music[SCORE_DEBRIEFING_AVERAGE] + 1;
m_debriefFail_music = Mission_music[SCORE_DEBRIEFING_FAILURE] + 1;
m_tree._model.modified = &modified; // provide way to indicate trees are modified in dialog
CDialog::OnInitDialog();
update_data();
if (select_sexp_node != -1) {
focus_sexp(select_sexp_node);
select_sexp_node = -1;
return FALSE;
}
return TRUE;
}
void debriefing_editor_dlg::update_data(int update)
{
int enable, save_debriefing;
debrief_stage *ptr;
save_debriefing = m_current_debriefing;
if (update)
UpdateData(TRUE);
// based on the game type, enable the multiple briefings combo box (or disable it)
// set up the pointer to the briefing that we are editing
if ( save_debriefing != m_current_debriefing )
Debriefing = &Debriefings[save_debriefing];
else
Debriefing = &Debriefings[m_current_debriefing];
if (m_last_stage >= 0) {
ptr = &Debriefing->stages[m_last_stage];
if (ptr->formula >= 0)
free_sexp2(ptr->formula);
ptr->formula = m_tree._model.save_tree();
SCP_string new_text, new_rec_text;
deconvert_multiline_string(new_text, m_text);
lcl_fred_replace_stuff(new_text);
deconvert_multiline_string(new_rec_text, m_rec_text);
lcl_fred_replace_stuff(new_rec_text);
if (modified || ptr->text != new_text || ptr->recommendation_text != new_rec_text
|| stricmp(ptr->voice, m_voice) != 0)
set_modified();
modified = 0;
ptr->text = new_text;
ptr->recommendation_text = new_rec_text;
string_copy(ptr->voice, m_voice, MAX_FILENAME_LEN - 1);
}
// now get new stage data
if ((m_cur_stage >= 0) && (m_cur_stage < Debriefing->num_stages)) {
ptr = &Debriefing->stages[m_cur_stage];
m_stage_title.Format("Stage %d of %d", m_cur_stage + 1, Debriefing->num_stages);
m_tree.load_tree(ptr->formula);
convert_multiline_string(m_text, ptr->text);
convert_multiline_string(m_rec_text, ptr->recommendation_text);
m_voice = ptr->voice;
enable = TRUE;
} else {
m_stage_title = _T("No stages");
m_tree.clear_tree();
m_text = _T("");
m_rec_text = _T("");
m_voice = _T("");
enable = FALSE;
m_cur_stage = -1;
}
if (m_cur_stage == Debriefing->num_stages - 1)
GetDlgItem(IDC_NEXT) -> EnableWindow(FALSE);
else
GetDlgItem(IDC_NEXT) -> EnableWindow(enable);
if (m_cur_stage)
GetDlgItem(IDC_PREV) -> EnableWindow(enable);
else
GetDlgItem(IDC_PREV) -> EnableWindow(FALSE);
if (Debriefing->num_stages >= MAX_DEBRIEF_STAGES)
GetDlgItem(IDC_ADD_STAGE) -> EnableWindow(FALSE);
else
GetDlgItem(IDC_ADD_STAGE) -> EnableWindow(TRUE);
if (Debriefing->num_stages) {
GetDlgItem(IDC_DELETE_STAGE) -> EnableWindow(enable);
GetDlgItem(IDC_INSERT_STAGE) -> EnableWindow(enable);
} else {
GetDlgItem(IDC_DELETE_STAGE) -> EnableWindow(FALSE);
GetDlgItem(IDC_INSERT_STAGE) -> EnableWindow(FALSE);
}
GetDlgItem(IDC_VOICE) -> EnableWindow(enable);
GetDlgItem(IDC_BROWSE) -> EnableWindow(enable);
GetDlgItem(IDC_TEXT) -> EnableWindow(enable);
GetDlgItem(IDC_REC_TEXT) -> EnableWindow(enable);
GetDlgItem(IDC_TREE) -> EnableWindow(enable);
m_last_stage = m_cur_stage;
UpdateData(FALSE);
#ifndef NDEBUG
count_free_sexp_nodes();
#endif
}
void debriefing_editor_dlg::OnNext()
{
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
m_cur_stage++;
update_data();
}
void debriefing_editor_dlg::OnPrev()
{
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
m_cur_stage--;
update_data();
}
void debriefing_editor_dlg::OnBrowse()
{
int z;
CString name;
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
UpdateData(TRUE);
if (The_mission.game_type & MISSION_TYPE_TRAINING)
z = cfile_push_chdir(CF_TYPE_VOICE_TRAINING);
else
z = cfile_push_chdir(CF_TYPE_VOICE_DEBRIEFINGS);
CFileDialog dlg(TRUE, "wav", NULL, 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_voice = dlg.GetFileName();
UpdateData(FALSE);
}
if (!z)
cfile_pop_dir();
}
void debriefing_editor_dlg::OnAddStage()
{
int i;
if (Debriefing->num_stages >= MAX_DEBRIEF_STAGES)
return;
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
m_cur_stage = i = Debriefing->num_stages++;
copy_stage(i - 1, i, 1);
update_data(1);
}
void debriefing_editor_dlg::OnDeleteStage()
{
int i, z;
if (m_cur_stage < 0)
return;
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
Assert(Debriefing->num_stages);
z = m_cur_stage;
m_cur_stage = -1;
update_data(1);
for (i=z+1; i<Debriefing->num_stages; i++) {
copy_stage(i, i - 1);
}
Debriefing->num_stages--;
m_cur_stage = z;
if (m_cur_stage >= Debriefing->num_stages)
m_cur_stage = Debriefing->num_stages - 1;
update_data(0);
}
void debriefing_editor_dlg::OnInsertStage()
{
int i, z;
if (Debriefing->num_stages >= MAX_DEBRIEF_STAGES)
return;
if (!Debriefing->num_stages) {
OnAddStage();
return;
}
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
z = m_cur_stage;
m_cur_stage = -1;
update_data(1);
for (i=Debriefing->num_stages; i>z; i--) {
copy_stage(i - 1, i);
}
Debriefing->num_stages++;
copy_stage(z, z + 1);
Debriefing->stages[z].formula = -1;
m_cur_stage = z;
update_data(0);
}
void debriefing_editor_dlg::copy_stage(int from, int to, int clear_formula)
{
if ((from < 0) || (from >= Debriefing->num_stages)) {
Debriefing->stages[to].text = "<Text here>";
strcpy_s(Debriefing->stages[to].voice, "none.wav");
Debriefing->stages[to].formula = -1;
return;
}
if (clear_formula)
Debriefing->stages[to].formula = -1;
else
Debriefing->stages[to].formula = Debriefing->stages[from].formula;
Debriefing->stages[to].text = Debriefing->stages[from].text;
strcpy_s( Debriefing->stages[to].voice, Debriefing->stages[from].voice );
Debriefing->stages[to].recommendation_text = Debriefing->stages[from].recommendation_text;
}
void debriefing_editor_dlg::OnRclickTree(NMHDR* pNMHDR, LRESULT* pResult)
{
m_tree.right_clicked();
*pResult = 0;
}
void debriefing_editor_dlg::OnBeginlabeleditTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
if (m_tree.edit_label(pTVDispInfo->item.hItem) == 1) {
*pResult = 0;
modified = 1;
} else
*pResult = 1;
}
void debriefing_editor_dlg::OnEndlabeleditTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
*pResult = m_tree.end_label_edit(pTVDispInfo->item);
}
void debriefing_editor_dlg::OnClose()
{
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
m_cur_stage = -1;
update_data(1);
Mission_music[SCORE_DEBRIEFING_SUCCESS] = m_debriefPass_music - 1;
Mission_music[SCORE_DEBRIEFING_AVERAGE] = m_debriefAvg_music - 1;
Mission_music[SCORE_DEBRIEFING_FAILURE] = m_debriefFail_music - 1;
theApp.record_window_data(&Debriefing_wnd_data, this);
debriefing_editor_dlg *ptr = Debriefing_dialog;
Debriefing_dialog = nullptr;
delete ptr;
FREDDoc_ptr->autosave("debriefing editor");
}
void debriefing_editor_dlg::OnOK()
{
}
BOOL debriefing_editor_dlg::OnCommand(WPARAM wParam, LPARAM lParam)
{
int id;
// deal with figuring out menu stuff
id = LOWORD(wParam);
if ( (id >= ID_TEAM_1) && (id < ID_TEAM_3) ) {
update_data(1);
// set the current debriefing
m_current_debriefing = id - ID_TEAM_1;
// put user back at first stage for this team (or no current stage is there are none).
Debriefing = &Debriefings[m_current_debriefing];
if ( Debriefing->num_stages > 0 )
m_cur_stage = 0;
else
m_cur_stage = -1;
m_last_stage = -1;
update_data(0);
}
return CDialog::OnCommand(wParam, lParam);
}
BOOL debriefing_editor_dlg::DestroyWindow()
{
Debriefing_dialog = nullptr;
audiostream_close_file(m_voice_id, 0);
if (m_play_icon) DestroyIcon(m_play_icon);
return CDialog::DestroyWindow();
}
void debriefing_editor_dlg::OnPlay()
{
GetDlgItem(IDC_VOICE)->GetWindowText(m_voice);
if (m_voice_id >= 0) {
audiostream_close_file(m_voice_id, 0);
m_voice_id = -1;
return;
}
// we use ASF_EVENTMUSIC here so that it will keep the extension in place
m_voice_id = audiostream_open((char *)(LPCSTR) m_voice, ASF_EVENTMUSIC);
if (m_voice_id >= 0) {
audiostream_play(m_voice_id, 1.0f, 0);
}
}