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

419 lines
12 KiB
C++

// OperatorComboBox.cpp : implementation file
//
#include "OperatorComboBox.h"
#include "resource.h"
#include "globalincs/utility.h"
#include "parse/parselo.h"
#include "parse/sexp.h"
BEGIN_MESSAGE_MAP(OperatorComboBox, CComboBox)
//{{AFX_MSG_MAP(OperatorComboBox)
ON_WM_CTLCOLOR()
ON_WM_DESTROY()
ON_CONTROL_REFLECT_EX(CBN_EDITCHANGE, OnEditChange)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(OperatorComboBoxList, CListBox)
//{{AFX_MSG_MAP(OperatorComboBoxList)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipText)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipText)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
OperatorComboBox::OperatorComboBox(const char* (*help_callback)(int))
: m_listbox(help_callback, OPF_NONE), m_help_callback(help_callback), m_pressed_enter(false)
{
}
OperatorComboBox::~OperatorComboBox()
{
}
// ----------------------------------------
// properly subclassing the edit and list components
// see https://jeffpar.github.io/kbarchive/kb/174/Q174667/
HBRUSH OperatorComboBox::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
if (nCtlColor == CTLCOLOR_EDIT)
{
//[ASCII 160][ASCII 160][ASCII 160]Edit control
if (m_edit.GetSafeHwnd() == NULL)
m_edit.SubclassWindow(pWnd->GetSafeHwnd());
}
else if (nCtlColor == CTLCOLOR_LISTBOX)
{
//ListBox control
if (m_listbox.GetSafeHwnd() == NULL)
m_listbox.SubclassWindow(pWnd->GetSafeHwnd());
}
HBRUSH hbr = CComboBox::OnCtlColor(pDC, pWnd, nCtlColor);
return hbr;
}
void OperatorComboBox::OnDestroy()
{
if (m_edit.GetSafeHwnd() != NULL)
m_edit.UnsubclassWindow();
if (m_listbox.GetSafeHwnd() != NULL)
m_listbox.UnsubclassWindow();
CComboBox::OnDestroy();
}
// ----------------------------------------
// handling edit changes
// see https://www.codeproject.com/Articles/187753/Extended-CComboBox
BOOL OperatorComboBox::PreTranslateMessage(MSG* pMsg)
{
if (pMsg->message == WM_KEYDOWN)
{
// keep track of whether Enter was the most recent key pressed
m_pressed_enter = (pMsg->wParam == VK_RETURN);
// when we press Escape, kill the focus so that the CBN_KILLFOCUS handler in sexp_tree_view.cpp will close the popup
if (pMsg->wParam == VK_ESCAPE)
GetOwner()->SetFocus();
// we don't need to check for DELETE and BACKSPACE here because the edit box isn't cleared because we don't use ResetContent
}
return CComboBox::PreTranslateMessage(pMsg);
}
BOOL OperatorComboBox::OnEditChange()
{
CString typed_text;
GetWindowText(typed_text);
filter_popup_operators((LPCSTR)typed_text);
return FALSE;
}
void OperatorComboBox::refresh_popup_operators(sexp_opf_t opf_type, const SCP_string &filter_string)
{
// operator type might have changed
m_listbox.SetOpfType(opf_type);
// reset filter
filter_popup_operators(filter_string);
// set the text and select all of it
SetWindowText(filter_string.c_str());
if (!filter_string.empty())
SetEditSel(0, (int)filter_string.length());
}
void OperatorComboBox::filter_popup_operators(const SCP_string &filter_string)
{
SCP_vector<int> filtered_operator_indexes;
// quick check to see if everything is already there
if (filter_string.empty() && GetCount() == (int)Operators.size())
return;
// if we're not filtering, just add everything
if (filter_string.empty())
{
set_popup_operators(Sorted_operator_indexes);
return;
}
// if the filter string is just one character, add operators that start with that character
if (filter_string.length() == 1)
{
auto first_ch = SCP_tolower(filter_string[0]);
std::copy_if(Sorted_operator_indexes.begin(), Sorted_operator_indexes.end(), std::back_inserter(filtered_operator_indexes), [first_ch](int op_index)
{
return first_ch == SCP_tolower(Operators[op_index].text[0]);
});
set_popup_operators(filtered_operator_indexes);
return;
}
// find all the operators below a threshold stringcost
// "an input that has n unmatched chars will have at least MAX_LENGTH * MAX_LENGTH * n, so this sets it as max 2 unaccounted chars"
size_t threshold = Max_operator_length * Max_operator_length * 3;
SCP_vector<std::pair<int, size_t>> filtered_operators;
for (int op_index : Sorted_operator_indexes)
{
const auto &op_text = Operators[op_index].text;
size_t cost = stringcost(op_text, filter_string, Max_operator_length, stringcost_tolower_equal);
if (cost < threshold)
filtered_operators.emplace_back(op_index, cost);
}
// sort operators by cost
std::stable_sort(filtered_operators.begin(), filtered_operators.end(), [](const std::pair<int, size_t> &a, const std::pair<int, size_t> &b)
{
// compare the size_t parts of both pairs
return std::get<1>(a) < std::get<1>(b);
});
std::transform(filtered_operators.begin(), filtered_operators.end(), std::back_inserter(filtered_operator_indexes), [](const std::pair<int, size_t>& a) {return a.first; });
// put them in the combo box
set_popup_operators(filtered_operator_indexes);
}
void OperatorComboBox::set_popup_operators(const SCP_vector<int> &operator_indexes)
{
// Turn off redraw while we modify the combo box.
SetRedraw(FALSE);
// Remove all items in the combo box. Don't use ResetContent() which also clears the edit control.
for (int i = GetCount() - 1; i >= 0; i--)
DeleteString(i);
if (!operator_indexes.empty())
{
int nIndex = 0;
InitStorage(sz2i(operator_indexes.size()), static_cast<UINT>(operator_indexes.size() * TOKEN_LENGTH));
// Add all the specified operators to the combo box.
for (int op_index : operator_indexes)
{
AddString(_T(Operators[op_index].text.c_str()));
SetItemData(nIndex++, op_index);
}
}
// Now we can draw again.
SetRedraw(TRUE);
InvalidateRect(NULL, TRUE);
}
void OperatorComboBox::cleanup(bool confirm)
{
SCP_UNUSED(confirm);
// for now, all we do is clear what was in the edit box
SetWindowText("");
}
// ----------------------------------------
bool OperatorComboBox::PressedEnter() const
{
return m_pressed_enter;
}
int OperatorComboBox::GetOpIndex(int index) const
{
if (index < 0)
{
// choose the operator that is the best match
CString typed_text;
GetWindowText(typed_text);
return sexp_match_closest_operator((LPCSTR)typed_text, m_listbox.GetOpfType());
}
return (int)m_listbox.GetItemData(index);
}
bool OperatorComboBox::IsItemEnabled(int index) const
{
if (index < 0)
return false;
return m_listbox.IsItemEnabled(index);
}
// ----------------------------------------
// tooltip stuff is based on example at
// https://www.codeproject.com/articles/1761/ctreectrl-clistctrl-clistbox-with-tooltip-based-on
void OperatorComboBox::PreSubclassWindow()
{
CComboBox::PreSubclassWindow();
EnableToolTips(TRUE);
}
INT_PTR OperatorComboBox::OnToolHitTest(CPoint point, TOOLINFO *pTI) const
{
// not linked yet
if (m_listbox.GetSafeHwnd() == NULL)
return -1;
CPoint screenpoint = point;
ClientToScreen(&screenpoint);
int item = LBItemFromPt(m_listbox.GetSafeHwnd(), screenpoint, FALSE);
if (item >= 0 && item < GetCount())
{
int op_index = (int)GetItemData(item);
int op_const = Operators[op_index].value;
auto helptext = m_help_callback(op_const);
if (helptext == nullptr && m_listbox.IsItemEnabled(item)) // a disabled item will show a tooltip regardless
return -1;
CRect rect;
m_listbox.GetItemRect(item, &rect);
pTI->hwnd = m_listbox.GetSafeHwnd();
pTI->uId = item;
pTI->lpszText = LPSTR_TEXTCALLBACK;
pTI->rect = rect;
return pTI->uId;
}
return -1;
}
OperatorComboBoxList::OperatorComboBoxList(const char* (*help_callback)(int), sexp_opf_t opf_type)
{
m_help_callback = help_callback;
SetOpfType(opf_type);
}
sexp_opf_t OperatorComboBoxList::GetOpfType() const
{
return m_opf_type;
}
void OperatorComboBoxList::SetOpfType(sexp_opf_t opf_type)
{
m_opf_type = opf_type;
}
//here we supply the text for the item
BOOL OperatorComboBoxList::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;
CString strTipText;
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;
int item = LBItemFromPt(GetSafeHwnd(), pt, FALSE);
if (item >= 0 && item < GetCount())
{
int op_index = (int)GetItemData(item);
int op_const = Operators[op_index].value;
auto helptext = m_help_callback(op_const);
if ((helptext != nullptr && *helptext != '\0') || !IsItemEnabled(item))
{
SCP_string buffer;
if (!IsItemEnabled(item))
{
sexp_opr_t opr_type;
map_opf_to_opr(m_opf_type, opr_type);
buffer = "The operator \"";
buffer += op_index >= 0 ? Operators[op_index].text : "<invalid operator>";
buffer += "\" cannot be selected because it has an incompatible return type.\r\n\tReturns: ";
buffer += opr_type_name((sexp_opr_t)query_operator_return_type(op_const));
buffer += "\r\n\tExpected: ";
buffer += opr_type_name(opr_type);
}
else
{
buffer = helptext; // never mind the MSVC warning; helptext will be non-null and non-empty here
}
replace_all(buffer, "\t", " ");
m_tooltiptextA = buffer.c_str();
m_tooltiptextW = buffer.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
}
// visual part of disabled items is based on example at:
// https://www.codeproject.com/Articles/451/CListBox-with-disabled-items
void OperatorComboBoxList::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
Assert(lpDrawItemStruct->CtlType == ODT_LISTBOX);
Assert((GetStyle() & (LBS_OWNERDRAWFIXED | LBS_HASSTRINGS)) == (LBS_OWNERDRAWFIXED | LBS_HASSTRINGS));
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
if (((LONG)(lpDrawItemStruct->itemID) >= 0) &&
(lpDrawItemStruct->itemAction & (ODA_DRAWENTIRE | ODA_SELECT)))
{
BOOL fDisabled = !IsWindowEnabled() || !IsItemEnabled(lpDrawItemStruct->itemID);
COLORREF newTextColor = fDisabled ?
RGB(0x80, 0x80, 0x80) : GetSysColor(COLOR_WINDOWTEXT); // light gray
COLORREF oldTextColor = pDC->SetTextColor(newTextColor);
COLORREF newBkColor = GetSysColor(COLOR_WINDOW);
COLORREF oldBkColor = pDC->SetBkColor(newBkColor);
if (newTextColor == newBkColor)
newTextColor = RGB(0xC0, 0xC0, 0xC0); // dark gray
if (!fDisabled && ((lpDrawItemStruct->itemState & ODS_SELECTED) != 0))
{
pDC->SetTextColor(GetSysColor(COLOR_HIGHLIGHTTEXT));
pDC->SetBkColor(GetSysColor(COLOR_HIGHLIGHT));
}
CString strText;
GetText(lpDrawItemStruct->itemID, strText);
const RECT& rc = lpDrawItemStruct->rcItem;
pDC->ExtTextOut(rc.left + 2,
rc.top + 2,// + max(0, (cyItem - m_cyText) / 2),
ETO_OPAQUE, &rc,
strText, strText.GetLength(), NULL);
pDC->SetTextColor(oldTextColor);
pDC->SetBkColor(oldBkColor);
}
if ((lpDrawItemStruct->itemAction & ODA_FOCUS) != 0)
pDC->DrawFocusRect(&lpDrawItemStruct->rcItem);
}
// we need to override this method when we create an owner-drawn combo box
void OperatorComboBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
// do nothing; the supplied width and height are fine
}
BOOL OperatorComboBoxList::IsItemEnabled(UINT nIndex) const
{
int op_index = (int)GetItemData(nIndex);
int op_const = Operators[op_index].value;
auto opr_type = query_operator_return_type(op_const);
return sexp_query_type_match(m_opf_type, opr_type);
}