Files
fs2open.github.com/fred2/OperatorComboBox.cpp
T

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);
}