mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
419 lines
12 KiB
C++
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);
|
|
}
|