// 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 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> 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 &a, const std::pair &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& a) {return a.first; }); // put them in the combo box set_popup_operators(filtered_operator_indexes); } void OperatorComboBox::set_popup_operators(const SCP_vector &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(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 : ""; 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); }