Files
Goober5000andClaude Fable 5.1 acf613c13c Fix FastDebug builds linking the non-redistributable debug CRT (#7919)
* fix FastDebug QtFRED linking the debug CRT

FastDebug builds in the DLL-runtime tree (used whenever qtFRED is
enabled) compiled with /MDd and, since #7450, linked Qt's debug
libraries.  Both import ucrtbased.dll, MSVCP140D.dll and
VCRUNTIME140D.dll, which Microsoft does not license for redistribution,
so end users could not run the FastDebug qtFRED shipped in nightlies
and releases.  Static Qt is ruled out by the LGPL, so the only shippable
shape is the release CRT plus release Qt.

On MSVC, _DEBUG is the CRT's own contract: /MTd and /MDd define it
themselves, and the STL headers select msvcprtd.lib and
_ITERATOR_DEBUG_LEVEL=2 from it.  Stop force-defining it for FastDebug
on MSVC; the engine keys its debug behaviour on NDEBUG, and the few
_DEBUG guards in code/ are switched to NDEBUG so they keep working.
The static tree is unchanged: /MTd still defines _DEBUG there.

Fixes #7915

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* derive the MSVC runtime choice once

The "DLL runtime or not" condition was spelled out in the toolchain and
twice in fred2 with "keep in sync" comments, and gtest_force_shared_crt
and antlr4's WITH_STATIC_CRT had already drifted: they only looked at
MSVC_USE_RUNTIME_DLL, so a qtFRED tree with tests enabled would have
asked both for the static CRT.  That is harmless today only because our
flag strings carry no literal /MD for them to rewrite.

Compute FSO_MSVC_RUNTIME_DLL once in toolchain-msvc.cmake and use it
everywhere.  InstallRequiredSystemLibraries deliberately stays keyed on
MSVC_USE_RUNTIME_DLL: the qtFRED deploy ships no compiler runtime.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* remove FastDebug runtime leftovers

The /MD -> /MDd substitution on the FastDebug flag strings has done
nothing since the runtime moved to CMAKE_MSVC_RUNTIME_LIBRARY: the
Release flags it copies contain no runtime switch.  The commented-out
_ITERATOR_DEBUG_LEVEL=0 lines for gtest and the OpenXR loader belong to
the scheme #7450 removed from the toolchain.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* use the release CRT for all FastDebug builds

Microsoft's documentation marks the static debug libraries (libcmtd,
libucrtd, libvcruntimed) "Not redistributable" and states that debug
versions of an application may only be copied to machines used for
testing, and the license grants distribution rights only for code on
the REDIST list, which contains no debug library in any form.  So the
statically linked FastDebug game and FRED2 are no more shippable than
the /MDd qtFRED was.

Link every FastDebug build against the release runtime.  The engine's
debug behaviour keys on NDEBUG, which FastDebug still leaves undefined,
so asserts, warnings and mprintf are unaffected; only the debug CRT's
heap checking goes, and that remains available in Debug builds.  FRED2's
static MFC library follows the CRT: the debug variant requires the debug
runtime.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* keep the debug icon, version flag and FRED2 asserts without _DEBUG

The resource compiler only ever sees the explicit definitions, so with
_DEBUG no longer defined for FastDebug the game lost its debug icon and
both editors lost the VS_FF_DEBUG version flag.  Key those on NDEBUG,
which FastDebug still leaves undefined.

FRED2 also used MFC's ASSERT, VERIFY, ASSERT_VALID and TRACE0, which are
all compiled out without _DEBUG.  Convert them to the FSO equivalents
FRED2 already uses nearly everywhere else: Assert, Verify, a null check
in place of ASSERT_VALID (AssertValid only exists in debug MFC), and
mprintf.  The AssertValid/Dump overrides and DEBUG_NEW stay under
_DEBUG, as they must.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-24 03:09:57 -04:00

2110 lines
56 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 "EventEditor.h"
#include "FREDView.h"
#include "Management.h"
#include "sexp_tree_view.h"
#include "textviewdlg.h"
#include "mission/missionmessage.h"
#include "cfile/cfile.h"
#include "sound/audiostr.h"
#include "localization/localize.h"
#include "mod_table/mod_table.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
static int annotation_key_for_item(const event_sexp_tree *tree, HTREEITEM h);
BEGIN_MESSAGE_MAP(event_sexp_tree, sexp_tree_view)
//{{AFX_MSG_MAP(event_sexp_tree)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipText)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipText)
ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomDraw)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
event_editor *Event_editor_dlg = NULL; // global reference needed by event tree class
// this is just useful for comparing modified EAs to unmodified ones
int safe_stricmp(const char* one, const char* two)
{
if (!one && !two)
return 0;
if (!one)
return -1;
if (!two)
return 1;
return stricmp(one, two);
}
/////////////////////////////////////////////////////////////////////////////
// event_editor dialog
int event_editor::getRootReturnType() const
{
return OPR_NULL;
}
int event_editor::onRootDeleted(int formula_node)
{
int i;
for (i = 0; i < (int)m_events.size(); i++) {
if (m_events[i].formula == formula_node) {
break;
}
}
Assert(i < (int)m_events.size());
m_events.erase(m_events.begin() + i);
m_sig.erase(m_sig.begin() + i);
if (i >= (int)m_events.size()) {
// if we have deleted the last event, i will be set to -1 which is what we want
i--;
}
cur_event = i;
update_cur_event();
return formula_node;
}
void event_editor::onRootRenamed(int formula_node, const char* new_name)
{
int i;
for (i = 0; i < (int)m_events.size(); i++) {
if (m_events[i].formula == formula_node) {
break;
}
}
Assert(i < (int)m_events.size());
m_events[i].name = new_name;
}
void event_editor::onRootInserted(int old_formula, int new_formula)
{
insert_handler(old_formula, new_formula);
}
void event_editor::onRootMoved(int node1, int node2, bool insert_before)
{
move_handler(node1, node2, insert_before);
}
event_editor::event_editor(CWnd* pParent /*=NULL*/)
: CDialog(event_editor::IDD, pParent),
SexpTreeEditorInterface({ TreeFlags::LabeledRoot, TreeFlags::RootDeletable, TreeFlags::RootEditable, TreeFlags::AnnotationsAllowed })
{
//{{AFX_DATA_INIT(event_editor)
m_repeat_count = 0;
m_trigger_count = 0;
m_interval = 0;
m_event_score = 0;
m_chain_delay = 0;
m_chained = FALSE;
m_use_msecs = FALSE;
m_obj_text = _T("");
m_obj_key_text = _T("");
m_avi_filename = _T("");
m_message_name = _T("");
m_message_note = _T("");
m_message_text = _T("");
m_persona = -1;
m_wave_filename = _T("");
m_cur_msg = -1;
m_cur_msg_old = -1;
m_team = -1;
m_message_team = -1;
m_last_message_node = -1;
//}}AFX_DATA_INIT
m_event_tree._model._interface = this;
m_event_tree._model.modified = &modified;
modified = 0;
select_sexp_node = -1;
m_wave_id = -1;
m_log_true = 0;
m_log_false = 0;
m_log_always_false = 0;
m_log_1st_repeat = 0;
m_log_last_repeat = 0;
m_log_1st_trigger = 0;
m_log_last_trigger = 0;
m_log_state_change = 0;
m_play_icon = nullptr;
m_move_to_top_icon = nullptr;
m_move_up_icon = nullptr;
m_move_down_icon = nullptr;
m_move_to_bottom_icon = nullptr;
}
void event_editor::DoDataExchange(CDataExchange* pDX)
{
if (m_cur_msg >= 0)
m_cur_msg_old = m_cur_msg;
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(event_editor)
DDX_Control(pDX, IDC_EVENT_TREE, m_event_tree);
DDX_Text(pDX, IDC_REPEAT_COUNT, m_repeat_count);
DDX_Text(pDX, IDC_TRIGGER_COUNT, m_trigger_count);
DDX_Text(pDX, IDC_INTERVAL_TIME, m_interval);
DDX_Text(pDX, IDC_EVENT_SCORE, m_event_score);
DDX_Text(pDX, IDC_CHAIN_DELAY, m_chain_delay);
DDX_Check(pDX, IDC_CHAINED, m_chained);
DDX_Check(pDX, IDC_USE_MSECS, m_use_msecs);
DDX_Text(pDX, IDC_OBJ_TEXT, m_obj_text);
DDV_MaxChars(pDX, m_obj_text, NAME_LENGTH - 1);
DDX_Text(pDX, IDC_OBJ_KEY_TEXT, m_obj_key_text);
DDV_MaxChars(pDX, m_obj_key_text, NAME_LENGTH - 1);
DDX_CBString(pDX, IDC_AVI_FILENAME, m_avi_filename);
DDV_MaxChars(pDX, m_avi_filename, MAX_FILENAME_LEN - 1);
DDX_Text(pDX, IDC_MESSAGE_NAME, m_message_name);
DDV_MaxChars(pDX, m_message_name, NAME_LENGTH - 1);
DDX_Text(pDX, IDC_MESSAGE_TEXT, m_message_text);
DDV_MaxChars(pDX, m_message_text, MESSAGE_LENGTH - 1);
DDX_CBIndex(pDX, IDC_PERSONA_NAME, m_persona);
DDX_CBString(pDX, IDC_WAVE_FILENAME, m_wave_filename);
DDV_MaxChars(pDX, m_wave_filename, MAX_FILENAME_LEN - 1);
DDX_LBIndex(pDX, IDC_MESSAGE_LIST, m_cur_msg);
DDX_Check(pDX, IDC_MISSION_LOG_TRUE, m_log_true);
DDX_Check(pDX, IDC_MISSION_LOG_FALSE, m_log_false);
DDX_Check(pDX, IDC_MISSION_LOG_ALWAYS_FALSE, m_log_always_false);
DDX_Check(pDX, IDC_MISSION_LOG_1ST_REPEAT, m_log_1st_repeat);
DDX_Check(pDX, IDC_MISSION_LOG_LAST_REPEAT, m_log_last_repeat);
DDX_Check(pDX, IDC_MISSION_LOG_1ST_TRIGGER, m_log_1st_trigger);
DDX_Check(pDX, IDC_MISSION_LOG_LAST_TRIGGER, m_log_last_trigger);
DDX_Check(pDX, IDC_MISSION_LOG_STATE_CHANGE, m_log_state_change);
// m_team == -1 maps to 2
if(m_team == -1){
m_team = MAX_TVT_TEAMS;
}
DDX_CBIndex(pDX, IDC_EVENT_TEAM, m_team);
// m_message_team == -1 maps to 2
if(m_message_team == -1){
m_message_team = MAX_TVT_TEAMS;
}
DDX_CBIndex(pDX, IDC_MESSAGE_TEAM, m_message_team);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(event_editor, CDialog)
//{{AFX_MSG_MAP(event_editor)
ON_NOTIFY(NM_RCLICK, IDC_EVENT_TREE, OnRclickEventTree)
ON_NOTIFY(TVN_BEGINLABELEDIT, IDC_EVENT_TREE, OnBeginlabeleditEventTree)
ON_NOTIFY(TVN_ENDLABELEDIT, IDC_EVENT_TREE, OnEndlabeleditEventTree)
ON_BN_CLICKED(IDC_BUTTON_NEW_EVENT, OnButtonNewEvent)
ON_BN_CLICKED(IDC_DELETE, OnDelete)
ON_BN_CLICKED(ID_OK, OnButtonOk)
ON_WM_CLOSE()
ON_NOTIFY(TVN_SELCHANGED, IDC_EVENT_TREE, OnSelchangedEventTree)
ON_EN_UPDATE(IDC_REPEAT_COUNT, OnUpdateRepeatCount)
ON_EN_UPDATE(IDC_TRIGGER_COUNT, OnUpdateTriggerCount)
ON_BN_CLICKED(IDC_CHAINED, OnChained)
ON_BN_CLICKED(IDC_INSERT, OnInsert)
ON_LBN_SELCHANGE(IDC_MESSAGE_LIST, OnSelchangeMessageList)
ON_BN_CLICKED(IDC_NEW_MSG, OnNewMsg)
ON_BN_CLICKED(IDC_INSERT_MSG, OnInsertMsg)
ON_BN_CLICKED(IDC_DELETE_MSG, OnDeleteMsg)
ON_BN_CLICKED(IDC_NEW_NOTE, OnMsgNote)
ON_BN_CLICKED(IDC_BROWSE_AVI, OnBrowseAvi)
ON_BN_CLICKED(IDC_BROWSE_WAVE, OnBrowseWave)
ON_CBN_SELCHANGE(IDC_WAVE_FILENAME, OnSelchangeWaveFilename)
ON_BN_CLICKED(IDC_PLAY, OnPlay)
ON_BN_CLICKED(IDC_UPDATE, OnUpdateStuff)
ON_BN_CLICKED(ID_CANCEL, OnButtonCancel)
ON_CBN_SELCHANGE(IDC_EVENT_TEAM, OnSelchangeTeam)
ON_CBN_SELCHANGE(IDC_MESSAGE_TEAM, OnSelchangeMessageTeam)
ON_LBN_DBLCLK(IDC_MESSAGE_LIST, OnDblclkMessageList)
ON_BN_CLICKED(IDC_EVENT_MOVE_TO_TOP, OnEventMoveToTop)
ON_BN_CLICKED(IDC_EVENT_MOVE_UP, OnEventMoveUp)
ON_BN_CLICKED(IDC_EVENT_MOVE_DOWN, OnEventMoveDown)
ON_BN_CLICKED(IDC_EVENT_MOVE_TO_BOTTOM, OnEventMoveToBottom)
ON_BN_CLICKED(IDC_MESSAGE_MOVE_TO_TOP, OnMessageMoveToTop)
ON_BN_CLICKED(IDC_MESSAGE_MOVE_UP, OnMessageMoveUp)
ON_BN_CLICKED(IDC_MESSAGE_MOVE_DOWN, OnMessageMoveDown)
ON_BN_CLICKED(IDC_MESSAGE_MOVE_TO_BOTTOM, OnMessageMoveToBottom)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// event_editor message handlers
void maybe_add_head(CComboBox *box, const char* name)
{
if (box->FindStringExact(-1, name) == CB_ERR) {
box->AddString(name);
}
}
BOOL event_editor::OnInitDialog()
{
int i;
BOOL r = TRUE;
CListBox *list;
CComboBox *box;
MMessage msg;
CDialog::OnInitDialog(); // let the base class do the default work
m_play_icon = load_button_icon(IDB_PLAY, RGB(192, 192, 192));
((CButton *) GetDlgItem(IDC_PLAY)) -> SetIcon(m_play_icon);
// reorder arrows; one icon is shared between the event and message buttons.
// Icons carry a transparency mask, so they blend with the button face and
// gray out correctly when disabled (see load_button_icon).
m_move_to_top_icon = load_button_icon(IDB_MOVE_TO_TOP, RGB(255, 0, 255));
m_move_up_icon = load_button_icon(IDB_MOVE_UP, RGB(255, 0, 255));
m_move_down_icon = load_button_icon(IDB_MOVE_DOWN, RGB(255, 0, 255));
m_move_to_bottom_icon = load_button_icon(IDB_MOVE_TO_BOTTOM, RGB(255, 0, 255));
((CButton *) GetDlgItem(IDC_EVENT_MOVE_TO_TOP)) -> SetIcon(m_move_to_top_icon);
((CButton *) GetDlgItem(IDC_EVENT_MOVE_UP)) -> SetIcon(m_move_up_icon);
((CButton *) GetDlgItem(IDC_EVENT_MOVE_DOWN)) -> SetIcon(m_move_down_icon);
((CButton *) GetDlgItem(IDC_EVENT_MOVE_TO_BOTTOM)) -> SetIcon(m_move_to_bottom_icon);
((CButton *) GetDlgItem(IDC_MESSAGE_MOVE_TO_TOP)) -> SetIcon(m_move_to_top_icon);
((CButton *) GetDlgItem(IDC_MESSAGE_MOVE_UP)) -> SetIcon(m_move_up_icon);
((CButton *) GetDlgItem(IDC_MESSAGE_MOVE_DOWN)) -> SetIcon(m_move_down_icon);
((CButton *) GetDlgItem(IDC_MESSAGE_MOVE_TO_BOTTOM)) -> SetIcon(m_move_to_bottom_icon);
theApp.init_window(&Events_wnd_data, this, 0);
m_event_tree.setup((CEdit *) GetDlgItem(IDC_HELP_BOX));
load_tree();
create_tree();
update_cur_event();
i = m_event_tree.select_sexp_node;
if (i != -1) {
GetDlgItem(IDC_EVENT_TREE) -> SetFocus();
m_event_tree.hilite_item(i);
r = FALSE;
}
// load event annotations into local model and apply comment icons
m_annotation_model.loadFromGlobal(m_event_tree._model.tree_nodes, m_events, m_sig);
m_event_tree.m_annotations = &m_annotation_model;
m_event_tree._model.annotation_model = &m_annotation_model;
for (auto &ea : m_annotation_model.annotations())
{
if (ea.node_index >= 0 && !ea.comment.empty())
{
HTREEITEM h = m_event_tree.handle(ea.node_index);
if (h)
event_annotation_swap_image(&m_event_tree, h, ea);
}
}
// ---------- end of event initialization ----------
int num_messages = Num_messages - Num_builtin_messages;
m_messages.clear();
m_messages.reserve(num_messages);
for (i=0; i<num_messages; i++) {
msg = Messages[i + Num_builtin_messages];
m_messages.push_back(msg);
if (m_messages[i].avi_info.name){
m_messages[i].avi_info.name = strdup(m_messages[i].avi_info.name);
}
if (m_messages[i].wave_info.name){
m_messages[i].wave_info.name = strdup(m_messages[i].wave_info.name);
}
}
((CEdit *) GetDlgItem(IDC_MESSAGE_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 (const auto &message: m_messages) {
list->AddString(message.name);
}
box = (CComboBox *) GetDlgItem(IDC_AVI_FILENAME);
box->ResetContent();
box->AddString("<None>");
for (i=0; i<Num_messages; i++) {
if (Messages[i].avi_info.name) {
maybe_add_head(box, Messages[i].avi_info.name);
}
}
if (!Disable_hc_message_ani) {
maybe_add_head(box, "Head-TP2");
maybe_add_head(box, "Head-VC2");
maybe_add_head(box, "Head-TP4");
maybe_add_head(box, "Head-TP5");
maybe_add_head(box, "Head-TP6");
maybe_add_head(box, "Head-TP7");
maybe_add_head(box, "Head-TP8");
maybe_add_head(box, "Head-VP2");
maybe_add_head(box, "Head-VP2");
maybe_add_head(box, "Head-CM2");
maybe_add_head(box, "Head-CM3");
maybe_add_head(box, "Head-CM4");
maybe_add_head(box, "Head-CM5");
maybe_add_head(box, "Head-BSH");
}
for (auto &thisHead : Custom_head_anis) {
maybe_add_head(box, thisHead.c_str());
}
/*
box->AddString("Head-VC"); // force it in, since Sandeep wants it and it's not used in built-in messages
box->AddString("Head-VC2");
// add terran pilot heads
box->AddString("Head-TP4");
box->AddString("Head-TP5");
box->AddString("Head-TP6");
box->AddString("Head-TP7");
box->AddString("Head-TP8");
// add vasudan pilot heads
box->AddString("Head-VP2");
// BSH and CM2
box->AddString("Head-CM2");
box->AddString("Head-BSH");
*/
box = (CComboBox *) GetDlgItem(IDC_WAVE_FILENAME);
box->ResetContent();
box->AddString("<None>");
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);
}
// set the first message to be the first non-builtin message (if it exists)
if ( Num_messages > Num_builtin_messages ){
m_cur_msg = 0;
} else {
m_cur_msg = -1;
}
update_cur_message();
return r;
}
void event_editor::load_tree()
{
int i;
m_event_tree.select_sexp_node = select_sexp_node;
select_sexp_node = -1;
m_event_tree.clear_tree();
m_events.clear();
m_sig.clear();
for (i=0; i<(int)Mission_events.size(); i++) {
m_events.push_back(Mission_events[i]);
m_sig.push_back(i);
if (m_events[i].name.empty())
m_events[i].name = "<Unnamed>";
m_events[i].formula = m_event_tree._model.load_sub_tree(Mission_events[i].formula, false, "do-nothing");
// we must check for the case of the repeat count being 0. This would happen if the repeat
// count is not specified in a mission
if ( m_events[i].repeat_count == 0 ){
m_events[i].repeat_count = 1;
}
}
m_event_tree._model.post_load();
cur_event = -1;
}
void event_editor::create_tree()
{
int i;
HTREEITEM h;
m_event_tree.DeleteAllItems();
for (i=0; i<(int)m_events.size(); i++) {
// set the proper bitmap
int image;
if (m_events[i].chain_delay >= 0) {
image = BITMAP_CHAIN;
if (!m_events[i].objective_text.empty()) {
image = BITMAP_CHAIN_DIRECTIVE;
}
} else {
image = BITMAP_ROOT;
if (!m_events[i].objective_text.empty()) {
image = BITMAP_ROOT_DIRECTIVE;
}
}
h = m_event_tree.insert(m_events[i].name.c_str(), image, image);
m_event_tree.SetItemData(h, m_events[i].formula);
m_event_tree.add_sub_tree(m_events[i].formula, h);
}
cur_event = -1;
}
void event_editor::OnRclickEventTree(NMHDR* pNMHDR, LRESULT* pResult)
{
save();
m_event_tree.right_clicked();
*pResult = 0;
}
void event_editor::OnBeginlabeleditEventTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
CEdit *edit;
bool is_operator;
if (m_event_tree.edit_label(pTVDispInfo->item.hItem, &is_operator) == 1) {
// when editing operators, do not use the built-in CEdit control, but overlay the special CComboBox
if (is_operator) {
*pResult = 1;
modified = 1;
m_event_tree.start_operator_edit(pTVDispInfo->item.hItem);
}
else {
*pResult = 0;
modified = 1;
edit = m_event_tree.GetEditControl();
Assert(edit);
edit->SetLimitText(NAME_LENGTH - 1);
}
} else
*pResult = 1;
}
void event_editor::OnEndlabeleditEventTree(NMHDR* pNMHDR, LRESULT* pResult)
{
TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)pNMHDR;
*pResult = m_event_tree.end_label_edit(pTVDispInfo->item);
}
// This is needed as a HACK around default MFC standard
// It is not required, but overrides default MFC and links no errors without.
// (Specifically, this overrides the MFC behavior so that pressing Enter doesn't close the dialog)
void event_editor::OnOK()
{
HWND h;
CWnd *w;
save();
w = GetFocus();
if (w) {
h = w->m_hWnd;
GetDlgItem(IDC_EVENT_TREE)->SetFocus();
::SetFocus(h);
}
((CListBox *)GetDlgItem(IDC_MESSAGE_LIST))->SetCurSel(m_cur_msg);
}
int event_editor::query_modified()
{
UpdateData(TRUE);
if (modified)
return 1;
if (Mission_events.size() != m_events.size())
return 1;
for (size_t i = 0; i < m_events.size(); ++i) {
if (!lcase_equal(m_events[i].name, Mission_events[i].name))
return 1;
if (m_events[i].repeat_count != Mission_events[i].repeat_count)
return 1;
if (m_events[i].trigger_count != Mission_events[i].trigger_count)
return 1;
if (m_events[i].interval != Mission_events[i].interval)
return 1;
if (m_events[i].score != Mission_events[i].score)
return 1;
if (m_events[i].chain_delay != Mission_events[i].chain_delay)
return 1;
if (!lcase_equal(m_events[i].objective_text, Mission_events[i].objective_text))
return 1;
if (!lcase_equal(m_events[i].objective_key_text, Mission_events[i].objective_key_text))
return 1;
if (m_events[i].flags != Mission_events[i].flags)
return 1;
if (m_events[i].mission_log_flags != Mission_events[i].mission_log_flags)
return 1;
}
if (static_cast<int>(m_messages.size()) != Num_messages - Num_builtin_messages) {
return 1;
}
for (size_t i = 0; i < m_messages.size(); ++i) {
auto& local = m_messages[i];
auto& ref = Messages[Num_builtin_messages + i];
if (stricmp(local.name, ref.name) != 0)
return 1;
if (stricmp(local.message, ref.message) != 0)
return 1;
if (!lcase_equal(local.note, ref.note))
return 1;
if (local.persona_index != ref.persona_index)
return 1;
if (local.multi_team != ref.multi_team)
return 1;
if (safe_stricmp(local.avi_info.name, ref.avi_info.name) != 0)
return 1;
if (safe_stricmp(local.wave_info.name, ref.wave_info.name) != 0)
return 1;
}
return 0;
}
void event_editor::OnButtonOk()
{
SCP_vector<std::pair<SCP_string, SCP_string>> names;
int i;
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
save();
if (query_modified())
set_modified();
for (auto &event: Mission_events) {
free_sexp2(event.formula);
event.result = 0; // use this as a processed flag
}
// rename all sexp references to old events
for (i=0; i<(int)m_events.size(); i++) {
if (m_sig[i] >= 0) {
names.emplace_back(Mission_events[m_sig[i]].name, m_events[i].name);
Mission_events[m_sig[i]].result = 1;
}
}
// invalidate all sexp references to deleted events.
for (const auto &event: Mission_events) {
if (!event.result) {
SCP_string buf = "<" + event.name + ">";
// force it to not be too long
if (SCP_truncate(buf, NAME_LENGTH - 1))
buf.back() = '>';
names.emplace_back(event.name, buf);
}
}
// copy all dialog events to the mission
Mission_events.clear();
for (const auto &dialog_event: m_events) {
Mission_events.push_back(dialog_event);
Mission_events.back().formula = m_event_tree._model.save_tree(dialog_event.formula);
}
// 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_EVENT_NAME);
for (i=Num_builtin_messages; i<Num_messages; i++)
message_free_media_names(Messages[i]);
Num_messages = (int)m_messages.size() + Num_builtin_messages;
Messages.resize(Num_messages);
for (i=0; i<(int)m_messages.size(); i++)
Messages[i + Num_builtin_messages] = m_messages[i];
m_annotation_model.saveToGlobal(m_event_tree._model.tree_nodes, m_events, m_sig);
theApp.record_window_data(&Events_wnd_data, this);
delete Event_editor_dlg;
Event_editor_dlg = NULL;
FREDDoc_ptr->autosave("event editor");
}
// load controls with structure data
void event_editor::update_cur_message()
{
int enable = TRUE;
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
if (m_cur_msg < 0) {
enable = FALSE;
m_message_name = _T("");
m_message_note = _T("");
m_message_text = _T("");
m_avi_filename = _T("");
m_wave_filename = _T("");
m_persona = 0;
m_message_team = -1;
} else {
m_message_name = m_messages[m_cur_msg].name;
m_message_note = (CString)m_messages[m_cur_msg].note.c_str();
m_message_text = m_messages[m_cur_msg].message;
if (m_messages[m_cur_msg].avi_info.name){
m_avi_filename = _T(m_messages[m_cur_msg].avi_info.name);
} else {
m_avi_filename = _T("<None>");
}
if (m_messages[m_cur_msg].wave_info.name){
m_wave_filename = _T(m_messages[m_cur_msg].wave_info.name);
} else {
m_wave_filename = _T("<None>");
}
// add persona id
if ( m_messages[m_cur_msg].persona_index != -1 ){
m_persona = m_messages[m_cur_msg].persona_index + 1; // add one for the "none" at the beginning of the list
} else {
m_persona = 0;
}
if(m_messages[m_cur_msg].multi_team >= MAX_TVT_TEAMS){
m_message_team = -1;
m_messages[m_cur_msg].multi_team = -1;
} else {
m_message_team = m_messages[m_cur_msg].multi_team;
}
/*
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);
*/ }
GetDlgItem(IDC_MESSAGE_NAME)->EnableWindow(enable);
GetDlgItem(IDC_MESSAGE_TEXT)->EnableWindow(enable);
GetDlgItem(IDC_AVI_FILENAME)->EnableWindow(enable);
GetDlgItem(IDC_BROWSE_AVI)->EnableWindow(enable);
GetDlgItem(IDC_BROWSE_WAVE)->EnableWindow(enable);
GetDlgItem(IDC_WAVE_FILENAME)->EnableWindow(enable);
GetDlgItem(IDC_DELETE_MSG)->EnableWindow(enable);
GetDlgItem(IDC_PERSONA_NAME)->EnableWindow(enable);
GetDlgItem(IDC_MESSAGE_TEAM)->EnableWindow(enable);
update_move_buttons();
UpdateData(FALSE);
}
void event_editor::OnButtonNewEvent()
{
// before we do anything, we must check and save off any data from the current event (e.g
// the repeat count and interval count)
save();
m_events.emplace_back();
m_sig.push_back(-1);
reset_event((int)m_events.size() - 1, TVI_LAST);
}
void event_editor::OnInsert()
{
if(cur_event < 0 || m_events.empty())
{
//There are no events yet, so just create one
OnButtonNewEvent();
}
else
{
// before we do anything, we must check and save off any data from the current event (e.g
// the repeat count and interval count)
save();
m_events.insert(m_events.begin() + cur_event, mission_event());
m_sig.insert(m_sig.begin() + cur_event, -1);
if (cur_event){
reset_event(cur_event, get_event_handle(cur_event - 1));
} else {
reset_event(cur_event, TVI_FIRST);
}
}
}
HTREEITEM event_editor::get_event_handle(int num)
{
HTREEITEM h;
h = m_event_tree.GetRootItem();
while (h) {
if ((int) m_event_tree.GetItemData(h) == m_events[num].formula){
return h;
}
h = m_event_tree.GetNextSiblingItem(h);
}
return 0;
}
int event_editor::get_event_num(HTREEITEM handle)
{
int formula = (int)m_event_tree.GetItemData(handle);
for (int i = 0; i < (int)m_events.size(); ++i)
if (formula == m_events[i].formula)
return i;
return -1;
}
void event_editor::reset_event(int num, HTREEITEM after)
{
int index;
HTREEITEM h;
// this is always called for a freshly constructed event, so all we have to do is set the name
m_events[num].name = "Event name";
h = m_event_tree.insert(m_events[num].name.c_str(), BITMAP_ROOT, BITMAP_ROOT, TVI_ROOT, after);
m_event_tree.item_index = -1;
m_event_tree.add_operator("when", h);
index = m_events[num].formula = m_event_tree.item_index;
m_event_tree.SetItemData(h, index);
m_event_tree.add_operator("true");
m_event_tree.item_index = index;
m_event_tree.add_operator("do-nothing");
update_cur_event();
m_event_tree.SelectItem(h);
}
void event_editor::OnDelete()
{
HTREEITEM h;
// call update_cur_event to clean up local class variables so that we can correctly
// set up the newly selected item.
cur_event = -1;
update_cur_event();
h = m_event_tree.GetSelectedItem();
if (h) {
while (m_event_tree.GetParentItem(h))
h = m_event_tree.GetParentItem(h);
m_event_tree.setup_selected(h);
m_event_tree.OnCommand(ID_DELETE, 0);
}
}
// this is called when you hit the escape key..
void event_editor::OnCancel()
{
// override MFC default behavior and do nothing
// the Esc key is used for certain actions inside the events editor
// so pressing Esc shouldn't close the window
}
// this is called when you click the ID_CANCEL button
void event_editor::OnButtonCancel()
{
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
if (query_modified()) {
int 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(&Events_wnd_data, this);
delete Event_editor_dlg;
Event_editor_dlg = NULL;
}
void event_editor::OnClose()
{
OnButtonCancel();
}
void event_editor::insert_handler(int old, int node)
{
int i;
for (i=0; i<(int)m_events.size(); i++){
if (m_events[i].formula == old){
break;
}
}
Assert(i < (int)m_events.size());
m_events[i].formula = node;
return;
}
void event_editor::save()
{
int m;
if (m_cur_msg >= 0) {
m = m_cur_msg;
} else {
// the current message could be -1 because the message list
// lost focus, so remember the last focused message
// (but make sure it's valid)
if (m_cur_msg_old >= 0 && m_cur_msg_old < (int)m_messages.size()) {
m = m_cur_msg_old;
} else {
m = -1;
}
}
save_event(cur_event);
save_message(m);
}
void event_editor::save_event(int e)
{
if (e < 0) {
return;
}
UpdateData(TRUE);
// there is currently no usage of a number < -1, so cap it at that
if (m_repeat_count < -1)
m_repeat_count = -1;
if (m_trigger_count < -1)
m_trigger_count = -1;
m_events[e].repeat_count = m_repeat_count;
m_events[e].trigger_count = m_trigger_count;
m_events[e].interval = m_interval;
m_events[e].score = m_event_score;
// handle chain
if (m_chained) {
m_events[e].chain_delay = m_chain_delay;
} else {
m_events[e].chain_delay = -1;
}
// handle objective text
lcl_fred_replace_stuff(m_obj_text);
lcl_fred_replace_stuff(m_obj_key_text);
m_events[e].objective_text = (LPCTSTR)m_obj_text;
m_events[e].objective_key_text = (LPCTSTR)m_obj_key_text;
// update bitmap
int bitmap;
if (m_chained) {
if (m_obj_text.IsEmpty()) {
bitmap = BITMAP_CHAIN;
} else {
bitmap = BITMAP_CHAIN_DIRECTIVE;
}
} else {
// not chained
if (m_obj_text.IsEmpty()) {
bitmap = BITMAP_ROOT;
} else {
bitmap = BITMAP_ROOT_DIRECTIVE;
}
}
// handle event flags
m_events[e].flags = 0;
if (m_use_msecs)
m_events[e].flags |= MEF_USE_MSECS;
// handle event log flags
m_events[e].mission_log_flags = 0;
if (m_log_true)
m_events[e].mission_log_flags |= MLF_SEXP_TRUE;
if (m_log_false)
m_events[e].mission_log_flags |= MLF_SEXP_FALSE;
if (m_log_always_false)
m_events[e].mission_log_flags |= MLF_SEXP_KNOWN_FALSE;
if (m_log_1st_repeat)
m_events[e].mission_log_flags |= MLF_FIRST_REPEAT_ONLY;
if (m_log_last_repeat)
m_events[e].mission_log_flags |= MLF_LAST_REPEAT_ONLY;
if (m_log_1st_trigger)
m_events[e].mission_log_flags |= MLF_FIRST_TRIGGER_ONLY;
if (m_log_last_trigger)
m_events[e].mission_log_flags |= MLF_LAST_TRIGGER_ONLY;
if (m_log_state_change)
m_events[e].mission_log_flags |= MLF_STATE_CHANGE;
// Search for item to update
HTREEITEM h = m_event_tree.GetRootItem();
while (h) {
if ((int) m_event_tree.GetItemData(h) == m_events[e].formula) {
m_event_tree.SetItemImage(h, bitmap, bitmap);
return;
}
h = m_event_tree.GetNextSiblingItem(h);
}
}
// this function deals with the left click on an event in the tree view. We get into this
// function so that we may update the other data on the screen (i.e repeat count and interval
// count)
void event_editor::OnSelchangedEventTree(NMHDR* pNMHDR, LRESULT* pResult)
{
int i, z;
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
HTREEITEM h, h2;
// before we do anything, we must check and save off any data from the current event (i.e.
// the repeat count and interval count)
save();
h = pNMTreeView->itemNew.hItem;
if (!h){
return;
}
m_event_tree.update_help(h);
while ((h2 = m_event_tree.GetParentItem(h))>0){
h = h2;
}
z = (int)m_event_tree.GetItemData(h);
for (i=0; i<(int)m_events.size(); i++){
if (m_events[i].formula == z){
break;
}
}
Assert(i < (int)m_events.size());
cur_event = i;
update_cur_event();
*pResult = 0;
}
void event_editor::update_cur_event()
{
if (cur_event < 0) {
m_repeat_count = 1;
m_trigger_count = 1;
m_interval = 1;
m_chained = FALSE;
m_chain_delay = 0;
m_event_score = 0;
m_team = -1;
m_obj_text.Empty();
m_obj_key_text.Empty();
m_use_msecs = FALSE;
m_log_true = FALSE;
m_log_false = FALSE;
m_log_always_false = FALSE;
m_log_1st_repeat = FALSE;
m_log_last_repeat = FALSE;
m_log_1st_trigger = FALSE;
m_log_last_trigger = FALSE;
m_log_state_change = FALSE;
GetDlgItem(IDC_REPEAT_COUNT)->EnableWindow(FALSE);
GetDlgItem(IDC_TRIGGER_COUNT)->EnableWindow(FALSE);
GetDlgItem(IDC_INTERVAL_TIME)->EnableWindow(FALSE);
GetDlgItem(IDC_CHAINED)->EnableWindow(FALSE);
GetDlgItem(IDC_CHAIN_DELAY)->EnableWindow(FALSE);
GetDlgItem(IDC_EVENT_SCORE)->EnableWindow(FALSE);
GetDlgItem(IDC_EVENT_TEAM)->EnableWindow(FALSE);
GetDlgItem(IDC_OBJ_TEXT)->EnableWindow(FALSE);
GetDlgItem(IDC_OBJ_KEY_TEXT)->EnableWindow(FALSE);
GetDlgItem(IDC_USE_MSECS)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_TRUE)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_FALSE)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_ALWAYS_FALSE)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_1ST_REPEAT)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_LAST_REPEAT)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_1ST_TRIGGER)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_LAST_TRIGGER)->EnableWindow(FALSE);
GetDlgItem(IDC_MISSION_LOG_STATE_CHANGE)->EnableWindow(FALSE);
update_move_buttons();
UpdateData(FALSE);
return;
}
m_team = m_events[cur_event].team;
m_repeat_count = m_events[cur_event].repeat_count;
m_trigger_count = m_events[cur_event].trigger_count;
m_interval = m_events[cur_event].interval;
m_event_score = m_events[cur_event].score;
if (m_events[cur_event].chain_delay >= 0) {
m_chained = TRUE;
m_chain_delay = m_events[cur_event].chain_delay;
GetDlgItem(IDC_CHAIN_DELAY) -> EnableWindow(TRUE);
} else {
m_chained = FALSE;
m_chain_delay = 0;
GetDlgItem(IDC_CHAIN_DELAY) -> EnableWindow(FALSE);
}
m_obj_text = m_events[cur_event].objective_text.c_str();
m_obj_key_text = m_events[cur_event].objective_key_text.c_str();
GetDlgItem(IDC_REPEAT_COUNT)->EnableWindow(TRUE);
GetDlgItem(IDC_TRIGGER_COUNT)->EnableWindow(TRUE);
GetDlgItem(IDC_USE_MSECS)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_TRUE)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_FALSE)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_ALWAYS_FALSE)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_1ST_REPEAT)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_LAST_REPEAT)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_1ST_TRIGGER)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_LAST_TRIGGER)->EnableWindow(TRUE);
GetDlgItem(IDC_MISSION_LOG_STATE_CHANGE)->EnableWindow(TRUE);
if ((m_repeat_count < 0) || (m_repeat_count > 1) || (m_trigger_count < 0) || (m_trigger_count > 1)) {
GetDlgItem(IDC_INTERVAL_TIME) -> EnableWindow(TRUE);
} else {
m_interval = 1;
GetDlgItem(IDC_INTERVAL_TIME) -> EnableWindow(FALSE);
}
GetDlgItem(IDC_EVENT_SCORE) -> EnableWindow(TRUE);
GetDlgItem(IDC_CHAINED) -> EnableWindow(TRUE);
GetDlgItem(IDC_OBJ_TEXT) -> EnableWindow(TRUE);
GetDlgItem(IDC_OBJ_KEY_TEXT) -> EnableWindow(TRUE);
GetDlgItem(IDC_EVENT_TEAM)->EnableWindow(FALSE);
if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ){
GetDlgItem(IDC_EVENT_TEAM)->EnableWindow(TRUE);
}
// handle event flags
m_use_msecs = (m_events[cur_event].flags & MEF_USE_MSECS) ? TRUE : FALSE;
// handle event log flags
m_log_true = (m_events[cur_event].mission_log_flags & MLF_SEXP_TRUE) ? TRUE : FALSE;
m_log_false = (m_events[cur_event].mission_log_flags & MLF_SEXP_FALSE) ? TRUE : FALSE;
m_log_always_false = (m_events[cur_event].mission_log_flags & MLF_SEXP_KNOWN_FALSE) ? TRUE : FALSE;
m_log_1st_repeat = (m_events[cur_event].mission_log_flags & MLF_FIRST_REPEAT_ONLY) ? TRUE : FALSE;
m_log_last_repeat = (m_events[cur_event].mission_log_flags & MLF_LAST_REPEAT_ONLY) ? TRUE : FALSE;
m_log_1st_trigger = (m_events[cur_event].mission_log_flags & MLF_FIRST_TRIGGER_ONLY) ? TRUE : FALSE;
m_log_last_trigger = (m_events[cur_event].mission_log_flags & MLF_LAST_TRIGGER_ONLY) ? TRUE : FALSE;
m_log_state_change = (m_events[cur_event].mission_log_flags & MLF_STATE_CHANGE) ? TRUE : FALSE;
update_move_buttons();
UpdateData(FALSE);
}
void event_editor::OnUpdateRepeatCount()
{
char buf[128];
int count = 128;
GetDlgItem(IDC_REPEAT_COUNT)->GetWindowText(buf, count);
m_repeat_count = atoi(buf);
if ((m_repeat_count < 0) || (m_repeat_count > 1) || (m_trigger_count < 0) || (m_trigger_count > 1)) {
GetDlgItem(IDC_INTERVAL_TIME)->EnableWindow(TRUE);
} else {
GetDlgItem(IDC_INTERVAL_TIME)->EnableWindow(FALSE);
}
}
void event_editor::OnUpdateTriggerCount()
{
char buf[128];
int count = 128;
GetDlgItem(IDC_TRIGGER_COUNT)->GetWindowText(buf, count);
m_trigger_count = atoi(buf);
if ((m_repeat_count < 0) || (m_repeat_count > 1) || (m_trigger_count < 0) || (m_trigger_count > 1)) {
GetDlgItem(IDC_INTERVAL_TIME)->EnableWindow(TRUE);
} else {
GetDlgItem(IDC_INTERVAL_TIME)->EnableWindow(FALSE);
}
}
void event_editor::move_handler(int node1, int node2, bool insert_before)
{
int index1, index2, s;
mission_event e;
save();
for (index1=0; index1<(int)m_events.size(); index1++){
if (m_events[index1].formula == node1){
break;
}
}
Assert(index1 < (int)m_events.size());
for (index2=0; index2<(int)m_events.size(); index2++){
if (m_events[index2].formula == node2){
break;
}
}
Assert(index2 < (int)m_events.size());
e = m_events[index1];
s = m_sig[index1];
int offset = insert_before ? -1 : 0;
while (index1 < index2 + offset) {
m_events[index1] = m_events[index1 + 1];
m_sig[index1] = m_sig[index1 + 1];
index1++;
}
while (index1 > index2 + offset + 1) {
m_events[index1] = m_events[index1 - 1];
m_sig[index1] = m_sig[index1 - 1];
index1--;
}
m_events[index1] = e;
m_sig[index1] = s;
cur_event = index2;
update_cur_event();
}
// Enable/disable the event and message reorder arrows based on the current
// selection and its position in the list.
void event_editor::update_move_buttons()
{
int ecount = (int)m_events.size();
BOOL e_up = (cur_event > 0 && cur_event < ecount) ? TRUE : FALSE;
BOOL e_down = (cur_event >= 0 && cur_event < ecount - 1) ? TRUE : FALSE;
GetDlgItem(IDC_EVENT_MOVE_TO_TOP)->EnableWindow(e_up);
GetDlgItem(IDC_EVENT_MOVE_UP)->EnableWindow(e_up);
GetDlgItem(IDC_EVENT_MOVE_DOWN)->EnableWindow(e_down);
GetDlgItem(IDC_EVENT_MOVE_TO_BOTTOM)->EnableWindow(e_down);
int mcount = (int)m_messages.size();
BOOL m_up = (m_cur_msg > 0 && m_cur_msg < mcount) ? TRUE : FALSE;
BOOL m_down = (m_cur_msg >= 0 && m_cur_msg < mcount - 1) ? TRUE : FALSE;
GetDlgItem(IDC_MESSAGE_MOVE_TO_TOP)->EnableWindow(m_up);
GetDlgItem(IDC_MESSAGE_MOVE_UP)->EnableWindow(m_up);
GetDlgItem(IDC_MESSAGE_MOVE_DOWN)->EnableWindow(m_down);
GetDlgItem(IDC_MESSAGE_MOVE_TO_BOTTOM)->EnableWindow(m_down);
}
// Reorder the currently-selected event. 'up'/'all_the_way' are interpreted as:
// move up one, move to top, move down one, move to bottom.
void event_editor::move_event(bool up, bool all_the_way)
{
int count = (int)m_events.size();
if (cur_event < 0 || cur_event >= count)
return;
int from = cur_event;
int to;
if (up)
to = all_the_way ? 0 : from - 1;
else
to = all_the_way ? count - 1 : from + 1;
if (to < 0 || to >= count || to == from)
return;
// Perform the move exactly as a drag-and-drop would: move_root shifts the
// tree item in place (the on_node_handle_changed hook keeps annotations
// attached, and the expanded state is preserved), and move_handler reorders
// m_events/m_sig to match (it also calls save() and update_cur_event()).
HTREEITEM source = get_event_handle(from);
HTREEITEM dest = get_event_handle(to);
int node1 = m_events[from].formula;
int node2 = m_events[to].formula;
bool insert_before = (to < from);
m_event_tree.move_root(source, dest, insert_before);
move_handler(node1, node2, insert_before);
update_move_buttons();
}
// Reorder the currently-selected message. 'up'/'all_the_way' are interpreted
// as: move up one, move to top, move down one, move to bottom.
void event_editor::move_message(bool up, bool all_the_way)
{
// commit any pending edits first
save();
int count = (int)m_messages.size();
if (m_cur_msg < 0 || m_cur_msg >= count)
return;
int from = m_cur_msg;
int to;
if (up)
to = all_the_way ? 0 : from - 1;
else
to = all_the_way ? count - 1 : from + 1;
if (to < 0 || to >= count || to == from)
return;
// Rotate m_messages so the item at 'from' lands at 'to'. MMessage has move
// semantics, so this transfers the media-name pointers without copying.
MMessage m = std::move(m_messages[from]);
if (from < to)
{
for (int i = from; i < to; ++i)
m_messages[i] = std::move(m_messages[i + 1]);
}
else
{
for (int i = from; i > to; --i)
m_messages[i] = std::move(m_messages[i - 1]);
}
m_messages[to] = std::move(m);
// Rebuild the listbox to match the new order.
CListBox *list = (CListBox *) GetDlgItem(IDC_MESSAGE_LIST);
list->ResetContent();
for (const auto &msg : m_messages)
list->AddString(msg.name);
m_cur_msg = to;
list->SetCurSel(to);
update_cur_message();
update_move_buttons();
modified = 1;
}
void event_editor::OnEventMoveToTop() { move_event(true, true); }
void event_editor::OnEventMoveUp() { move_event(true, false); }
void event_editor::OnEventMoveDown() { move_event(false, false); }
void event_editor::OnEventMoveToBottom() { move_event(false, true); }
void event_editor::OnMessageMoveToTop() { move_message(true, true); }
void event_editor::OnMessageMoveUp() { move_message(true, false); }
void event_editor::OnMessageMoveDown() { move_message(false, false); }
void event_editor::OnMessageMoveToBottom() { move_message(false, true); }
void event_editor::OnChained()
{
int image;
HTREEITEM h;
UpdateData(TRUE);
if (m_chained) {
GetDlgItem(IDC_CHAIN_DELAY) -> EnableWindow(TRUE);
if (m_obj_text.IsEmpty()) {
image = BITMAP_CHAIN;
} else {
image = BITMAP_CHAIN_DIRECTIVE;
}
} else {
GetDlgItem(IDC_CHAIN_DELAY) -> EnableWindow(FALSE);
if (m_obj_text.IsEmpty()) {
image = BITMAP_ROOT;
} else {
image = BITMAP_ROOT_DIRECTIVE;
}
}
h = m_event_tree.GetRootItem();
while (h) {
if ((int) m_event_tree.GetItemData(h) == m_events[cur_event].formula) {
m_event_tree.SetItemImage(h, image, image);
return;
}
h = m_event_tree.GetNextSiblingItem(h);
}
}
void event_editor::OnSelchangeMessageList()
{
static int flag = 0;
if (flag)
return;
/*
if (save_message(m_cur_msg)) {
flag = 1;
((CListBox *) GetDlgItem(IDC_MESSAGE_LIST)) -> SetCurSel(old);
m_cur_msg = old;
flag = 0;
return;
}*/
save();
update_cur_message();
}
int event_editor::save_message(int num)
{
char *ptr;
int i, conflict = 0;
CListBox *list;
UpdateData(TRUE);
if (num >= 0) {
ptr = (char *) (LPCTSTR) m_message_name;
for (i=0; i<Num_builtin_messages; i++){
if (!stricmp(m_message_name, Messages[i].name)) {
conflict = 1;
break;
}
}
for (i=0; i<(int)m_messages.size(); i++){
if ((i != num) && (!stricmp(m_message_name, m_messages[i].name))) {
conflict = 1;
break;
}
}
if (!conflict) { // update name if no conflicts, otherwise keep old name
string_copy(m_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(m_messages[num].message, m_message_text, MESSAGE_LENGTH - 1);
m_messages[num].note = m_message_note;
lcl_fred_replace_stuff(m_messages[num].message, MESSAGE_LENGTH - 1);
if (m_messages[num].avi_info.name){
free(m_messages[num].avi_info.name);
}
ptr = (char *) (LPCTSTR) m_avi_filename;
if ( !ptr || !VALID_FNAME(ptr) ) {
m_messages[num].avi_info.name = NULL;
} else {
m_messages[num].avi_info.name = strdup(ptr);
}
if (m_messages[num].wave_info.name){
free(m_messages[num].wave_info.name);
}
ptr = (char *) (LPCTSTR) m_wave_filename;
if ( !ptr || !VALID_FNAME(ptr) ) {
m_messages[num].wave_info.name = NULL;
} else {
m_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.
m_messages[num].persona_index = m_persona - 1;
if(m_message_team >= MAX_TVT_TEAMS){
m_messages[num].multi_team = -1;
m_message_team = -1;
} else {
m_messages[num].multi_team = m_message_team;
}
// possible TODO: auto-update event tree references to this message if we renamed it.
}
return 0;
}
void event_editor::OnNewMsg()
{
MMessage msg;
save();
strcpy_s(msg.name, "<new message>");
((CListBox *) GetDlgItem(IDC_MESSAGE_LIST))->AddString("<new message>");
strcpy_s(msg.message, "<put description here>");
msg.avi_info.name = NULL;
msg.wave_info.name = NULL;
msg.persona_index = -1;
msg.multi_team = -1;
m_messages.push_back(msg);
m_cur_msg = (int)m_messages.size() - 1;
modified = 1;
update_cur_message();
}
void event_editor::OnInsertMsg()
{
// with no current message there's nothing to insert ahead of, so just append
if (m_cur_msg < 0 || m_messages.empty()) {
OnNewMsg();
return;
}
save();
MMessage msg;
strcpy_s(msg.name, "<new message>");
strcpy_s(msg.message, "<put description here>");
msg.avi_info.name = nullptr;
msg.wave_info.name = nullptr;
msg.persona_index = -1;
msg.multi_team = -1;
// insert ahead of the current message; the new one takes its slot
m_messages.insert(m_messages.begin() + m_cur_msg, std::move(msg));
// rebuild the listbox to match the new order
CListBox *list = (CListBox *) GetDlgItem(IDC_MESSAGE_LIST);
list->ResetContent();
for (const auto &m : m_messages)
list->AddString(m.name);
list->SetCurSel(m_cur_msg);
modified = 1;
update_cur_message();
}
void event_editor::OnDeleteMsg()
{
char buf[256];
// handle this case somewhat gracefully
Assertion((m_cur_msg >= -1) && (m_cur_msg < (int)m_messages.size()), "Unexpected m_cur_msg value (%d); expected either -1, or between 0-%d. Get a coder!\n", m_cur_msg, (int)m_messages.size() - 1);
if((m_cur_msg < 0) || (m_cur_msg >= (int)m_messages.size())){
return;
}
if (m_messages[m_cur_msg].avi_info.name){
free(m_messages[m_cur_msg].avi_info.name);
}
if (m_messages[m_cur_msg].wave_info.name){
free(m_messages[m_cur_msg].wave_info.name);
}
((CListBox *) GetDlgItem(IDC_MESSAGE_LIST))->DeleteString(m_cur_msg);
sprintf(buf, "<%s>", m_messages[m_cur_msg].name);
update_sexp_references(m_messages[m_cur_msg].name, buf, OPF_MESSAGE);
update_sexp_references(m_messages[m_cur_msg].name, buf, OPF_MESSAGE_OR_STRING);
m_messages.erase(m_messages.begin() + m_cur_msg);
if (m_cur_msg >= (int)m_messages.size()){
m_cur_msg = (int)m_messages.size() - 1;
}
GetDlgItem(IDC_NEW_MSG)->EnableWindow(TRUE);
modified = 1;
update_cur_message();
}
void event_editor::OnMsgNote()
{
// handle this case somewhat gracefully
Assertion((m_cur_msg >= -1) && (m_cur_msg < (int)m_messages.size()), "Unexpected m_cur_msg value (%d); expected either -1, or between 0-%d. Get a coder!\n", m_cur_msg, (int)m_messages.size() - 1);
if((m_cur_msg < 0) || (m_cur_msg >= (int)m_messages.size())){
return;
}
CString old_text = m_message_note;
CString new_text;
TextViewDlg dlg;
dlg.SetText(old_text);
dlg.SetCaption("Enter a note");
dlg.SetEditable(true);
dlg.DoModal();
dlg.GetText(new_text);
// comment is unchanged
if (new_text == old_text)
return;
m_message_note = new_text;
}
void event_editor::OnBrowseAvi()
{
int z;
CString name;
UpdateData(TRUE);
if (!stricmp(m_avi_filename, "<None>"))
m_avi_filename = _T("");
z = cfile_push_chdir(CF_TYPE_INTERFACE);
CFileDialog dlg(TRUE, "png", m_avi_filename, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR,
"APNG Files (*.png)|*.png|Ani Files (*.ani)|*.ani|Eff Files (*.eff)|*.eff|All Anims (*.ani, *.eff, *.png)|*.ani;*.eff;*.png|");
if (dlg.DoModal() == IDOK) {
m_avi_filename = dlg.GetFileName();
UpdateData(FALSE);
modified = 1;
}
if (!z)
cfile_pop_dir();
}
void event_editor::OnBrowseWave()
{
int z;
CString name;
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
UpdateData(TRUE);
if (!stricmp(m_wave_filename, "<None>"))
m_wave_filename = _T("");
if (The_mission.game_type & MISSION_TYPE_TRAINING)
z = cfile_push_chdir(CF_TYPE_VOICE_TRAINING);
else
z = cfile_push_chdir(CF_TYPE_VOICE_SPECIAL);
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();
update_persona();
}
if (!z){
cfile_pop_dir();
}
}
char *event_editor::current_message_name(int i)
{
if ( (i < 0) || (i >= (int)m_messages.size()) ){
return NULL;
}
return m_messages[i].name;
}
char *event_editor::get_message_list_item(int i)
{
return m_messages[i].name;
}
void event_editor::update_persona()
{
int i, mask;
if ((m_wave_filename[0] >= '1') && (m_wave_filename[0] <= '9') && (m_wave_filename[1] == '_') ) {
i = m_wave_filename[0] - '1';
if ((i < (int)Personas.size()) && (Personas[i].flags & PERSONA_FLAG_WINGMAN)) {
m_persona = i + 1;
if ((m_persona==1) || (m_persona==2))
m_avi_filename = "HEAD-TP1";
else if ((m_persona==3) || (m_persona==4))
m_avi_filename = "HEAD-TP2";
else if ((m_persona==5))
m_avi_filename = "HEAD-TP3";
else if ((m_persona==6))
m_avi_filename = "HEAD-VP1";
}
} else {
mask = 0;
if (!strnicmp(m_wave_filename, "S_", 2)) {
mask = PERSONA_FLAG_SUPPORT;
m_avi_filename = "HEAD-CM1";
}
else if (!strnicmp(m_wave_filename, "L_", 2)) {
mask = PERSONA_FLAG_LARGE;
m_avi_filename = "HEAD-CM1";
}
else if (!strnicmp(m_wave_filename, "TC_", 3)) {
mask = PERSONA_FLAG_COMMAND;
m_avi_filename = "HEAD-CM1";
}
for (i = 0; i < (int)Personas.size(); i++)
if (Personas[i].flags & mask)
m_persona = i + 1;
}
//GetDlgItem(IDC_ANI_FILENAME)->SetWindowText(m_avi_filename);
UpdateData(FALSE);
modified = 1;
}
void event_editor::OnSelchangeWaveFilename()
{
int z;
CComboBox *box;
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
box = (CComboBox *) GetDlgItem(IDC_WAVE_FILENAME);
z = box -> GetCurSel();
UpdateData(TRUE);
UpdateData(TRUE);
box -> GetLBText(z, m_wave_filename);
UpdateData(FALSE);
update_persona();
}
BOOL event_editor::DestroyWindow()
{
Event_editor_dlg = nullptr;
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
if (m_play_icon) DestroyIcon(m_play_icon);
if (m_move_to_top_icon) DestroyIcon(m_move_to_top_icon);
if (m_move_up_icon) DestroyIcon(m_move_up_icon);
if (m_move_down_icon) DestroyIcon(m_move_down_icon);
if (m_move_to_bottom_icon) DestroyIcon(m_move_to_bottom_icon);
return CDialog::DestroyWindow();
}
void event_editor::OnPlay()
{
GetDlgItem(IDC_WAVE_FILENAME)->GetWindowText(m_wave_filename);
if (m_wave_id >= 0) {
audiostream_close_file(m_wave_id, 0);
m_wave_id = -1;
return;
}
// we use ASF_EVENTMUSIC here so that it will keep the extension in place
m_wave_id = audiostream_open((char *)(LPCSTR) m_wave_filename, ASF_EVENTMUSIC);
if (m_wave_id >= 0) {
audiostream_play(m_wave_id, 1.0f, 0);
}
}
void event_editor::OnUpdateStuff()
{
int z = MessageBox("This will set the head ANIs according to the FS1 personas. Do you want to proceed?\n\n(Consider using the 'Sync Personas' buttons in the Voice Acting Manager instead.)", "Update Stuff", MB_ICONQUESTION | MB_YESNO);
if (z != IDYES) {
return;
}
// GetDlgItem(IDC_WAVE_FILENAME)->GetWindowText(m_wave_filename);
UpdateData(TRUE);
update_persona();
}
// code when the "team" selection in the combo box changes
void event_editor::OnSelchangeTeam()
{
if ( cur_event < 0 ){
return;
}
UpdateData(TRUE);
// If the team isn't valid mark it as such.
if((m_team >= MAX_TVT_TEAMS) || (m_team < -1) ){
m_team = -1;
}
m_events[cur_event].team = m_team;
}
// code when the "team" selection in the combo box changes
void event_editor::OnSelchangeMessageTeam()
{
if ( m_cur_msg < 0 ){
return;
}
UpdateData(TRUE);
// If the team isn't valid mark it as such.
if((m_message_team>= MAX_TVT_TEAMS) || (m_message_team < -1) ) {
m_message_team = -1;
}
m_messages[m_cur_msg].multi_team = m_message_team;
}
// Cycles among sexp nodes with message text
void event_editor::OnDblclkMessageList()
{
CListBox *list = (CListBox*) GetDlgItem(IDC_MESSAGE_LIST);
int num_messages;
int message_nodes[MAX_SEARCH_MESSAGE_DEPTH];
// get current message index and message name
int cur_index = list->GetCurSel();
// check if message name is in event tree
char buffer[256];
list->GetText(cur_index, buffer);
num_messages = m_event_tree._model.find_text(buffer, message_nodes, MAX_SEARCH_MESSAGE_DEPTH);
if (num_messages == 0) {
char message[256];
sprintf(message, "No events using message '%s'", buffer);
MessageBox(message);
} else {
// find last message_node
if (m_last_message_node == -1) {
m_last_message_node = message_nodes[0];
} else {
if (num_messages == 1) {
// only 1 message
m_last_message_node = message_nodes[0];
} else {
// find which message and go to next message
int found_pos = -1;
for (int i=0; i<num_messages; i++) {
if (message_nodes[i] == m_last_message_node) {
found_pos = i;
break;
}
}
if (found_pos == -1) {
// no previous message
m_last_message_node = message_nodes[0];
} else if (found_pos == num_messages-1) {
// cycle back to start
m_last_message_node = message_nodes[0];
} else {
// go to next
m_last_message_node = message_nodes[found_pos+1];
}
}
}
// highlight next
m_event_tree.hilite_item(m_last_message_node);
}
}
// Returns a unique annotation key for the given tree item.
// For regular nodes: their tree_nodes[] index (>= 0).
// For root label nodes (event names): -(formula + 2), which is always <= -2,
// avoiding collision with -1 (the default/unresolved sentinel).
static int annotation_key_for_item(const event_sexp_tree *tree, HTREEITEM h)
{
int node = tree->get_node(h);
if (node >= 0)
return node;
// Root label: encode the formula stored in item data
if (!tree->GetParentItem(h)) {
int formula = (int)tree->GetItemData(h);
if (formula >= 0)
return -(formula + 2);
}
return -1;
}
void event_annotation_swap_image(event_sexp_tree *tree, HTREEITEM handle, event_annotation &ea)
{
// see if this node is a top-level event - if so, don't mess with the image
if (!tree->GetParentItem(handle))
return;
int nImage, nSelectedImage;
tree->GetItemImage(handle, nImage, nSelectedImage);
// if tree node already has the comment image, replace it with the old one
if (nImage == BITMAP_COMMENT)
{
nImage = ea.item_image;
}
// if tree has its normal image, store it and use the comment image
else
{
ea.item_image = nImage;
nImage = BITMAP_COMMENT;
}
// all tree nodes use the same image for both selected and unselected
tree->SetItemImage(handle, nImage, nImage);
}
// tooltip stuff is based on example at
// https://www.codeproject.com/articles/1761/ctreectrl-clistctrl-clistbox-with-tooltip-based-on
void event_sexp_tree::PreSubclassWindow()
{
CTreeCtrl::PreSubclassWindow();
EnableToolTips(TRUE);
}
INT_PTR event_sexp_tree::OnToolHitTest(CPoint point, TOOLINFO *pTI) const
{
RECT rect;
UINT nFlags;
HTREEITEM hitem = HitTest(point, &nFlags);
if (nFlags & TVHT_ONITEMLABEL)
{
GetItemRect(hitem, &rect, TRUE);
pTI->hwnd = m_hWnd;
pTI->uId = (UINT)hitem;
pTI->lpszText = LPSTR_TEXTCALLBACK;
pTI->rect = rect;
return pTI->uId;
}
return -1;
}
//here we supply the text for the item
BOOL event_sexp_tree::OnToolTipText(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
{
// need to handle both ANSI and UNICODE versions of the message
TOOLTIPTEXTA* pTTTA = (TOOLTIPTEXTA*)pNMHDR;
TOOLTIPTEXTW* pTTTW = (TOOLTIPTEXTW*)pNMHDR;
UINT_PTR nID = pNMHDR->idFrom;
// Do not process the message from built in tooltip
if (nID == (UINT_PTR)m_hWnd &&
((pNMHDR->code == TTN_NEEDTEXTA && pTTTA->uFlags & TTF_IDISHWND) ||
(pNMHDR->code == TTN_NEEDTEXTW && pTTTW->uFlags & TTF_IDISHWND)))
return FALSE;
// Get the mouse position
auto pMessage = GetCurrentMessage(); // get mouse pos
Assert(pMessage);
auto pt = pMessage->pt;
ScreenToClient(&pt);
UINT nFlags;
HTREEITEM h = HitTest(pt, &nFlags); //Get item pointed by mouse
if (h && m_annotations)
{
int key = annotation_key_for_item(this, h);
auto ea = m_annotations->getByKey(key);
if (ea && !ea->comment.empty())
{
m_tooltiptextA = ea->comment.c_str();
m_tooltiptextW = ea->comment.c_str();
if (pNMHDR->code == TTN_NEEDTEXTA)
{
pTTTA->lpszText = (LPSTR)(LPCSTR)m_tooltiptextA;
::SendMessage(pTTTA->hdr.hwndFrom, TTM_SETMAXTIPWIDTH, 0, 400);
}
else
{
pTTTW->lpszText = (LPWSTR)(LPCWSTR)m_tooltiptextW;
::SendMessage(pTTTW->hdr.hwndFrom, TTM_SETMAXTIPWIDTH, 0, 400);
}
}
}
*pResult = 0;
return TRUE; // message was handled
}
// color stuff is based on example at
// https://stackoverflow.com/questions/2119717/changing-the-color-of-a-selected-ctreectrl-item
void event_sexp_tree::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
static CRect rect;
static bool drawBorder = false;
NMTVCUSTOMDRAW *pcd = (NMTVCUSTOMDRAW *)pNMHDR;
switch (pcd->nmcd.dwDrawStage)
{
case CDDS_PREPAINT:
*pResult = CDRF_NOTIFYITEMDRAW;
break;
case CDDS_ITEMPREPAINT:
{
drawBorder = false;
*pResult = CDRF_DODEFAULT;
HTREEITEM hItem = (HTREEITEM)pcd->nmcd.dwItemSpec;
if (hItem && m_annotations)
{
int key = annotation_key_for_item(this, hItem);
auto ea = m_annotations->getByKey(key);
if (ea)
{
if (ea->r != 255 || ea->g != 255 || ea->b != 255)
{
// contrast color calculation taken from here:
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
if ((ea->r*0.299 + ea->g*0.587 + ea->b*0.114) > 149)
pcd->clrText = RGB(0, 0, 0);
else
pcd->clrText = RGB(255, 255, 255);
pcd->clrTextBk = RGB(ea->r, ea->g, ea->b);
// if this is selected, save some information for drawing a border later
if (pcd->nmcd.uItemState & CDIS_SELECTED)
{
drawBorder = true;
GetItemRect((HTREEITEM)pcd->nmcd.dwItemSpec, &rect, TRUE);
// we want to get the CDDS_ITEMPOSTPAINT notification
*pResult = CDRF_NOTIFYPOSTPAINT;
}
}
}
}
break;
}
case CDDS_ITEMPOSTPAINT:
{
// see Solution 2 here: https://www.codeproject.com/Questions/685172/CTreeCtrl-with-Item-Border
if (drawBorder)
{
SelectObject(pcd->nmcd.hdc, CreatePen(PS_INSIDEFRAME, 2, RGB(0, 0, 0)));
SelectObject(pcd->nmcd.hdc, GetStockObject(HOLLOW_BRUSH));
Rectangle(pcd->nmcd.hdc, rect.left, rect.top, rect.right, rect.bottom);
}
break;
}
}
}
void event_sexp_tree::edit_comment(HTREEITEM h)
{
if (!m_annotations)
return;
CString old_text = _T("");
CString new_text;
int key = annotation_key_for_item(this, h);
auto* ea = m_annotations->getByKey(key);
if (ea)
old_text = (CString)ea->comment.c_str();
TextViewDlg dlg;
dlg.SetText(old_text);
dlg.SetCaption("Enter a comment");
dlg.SetEditable(true);
dlg.DoModal();
dlg.GetText(new_text);
// comment is unchanged
if (new_text == old_text)
return;
auto& ann = m_annotations->ensureByKey(key);
ann.comment = (LPCSTR)new_text;
// see if we are either adding a new comment or removing an existing comment
// if so, change the icon
if (old_text.IsEmpty() || new_text.IsEmpty())
event_annotation_swap_image(this, h, ann);
}
void event_sexp_tree::edit_bg_color(HTREEITEM h)
{
if (!m_annotations)
return;
COLORREF old_color = RGB(255, 255, 255);
COLORREF new_color;
int key = annotation_key_for_item(this, h);
auto* ea = m_annotations->getByKey(key);
if (ea)
old_color = RGB(ea->r, ea->g, ea->b);
CColorDialog dlg(old_color);
if (dlg.DoModal() != IDOK)
return;
new_color = dlg.GetColor();
// color is unchanged
if (new_color == old_color)
return;
auto& ann = m_annotations->ensureByKey(key);
ann.r = GetRValue(new_color);
ann.g = GetGValue(new_color);
ann.b = GetBValue(new_color);
// This is needed otherwise the color won't change until the user clicks something
RedrawWindow();
}
SCP_string event_sexp_tree::get_node_comment(int node_index) const
{
if (!m_annotations)
return "";
auto* ea = m_annotations->getByKey(node_index);
if (ea && !ea->comment.empty())
return ea->comment;
return "";
}
SCP_string event_sexp_tree::get_item_comment(HTREEITEM h, int /*node_index*/) const
{
// resolves both regular nodes and root labels (which have no model node,
// so the base class's node_index would be -1 for them)
return get_node_comment(annotation_key_for_item(this, h));
}