// 17 june 2018 #include "uipriv_windows.hpp" #include "table.hpp" // TODOs // - clicking on the same item restarts editing instead of cancels it // this is not how the real list view positions and sizes the edit control, but this is a) close enough b) a lot easier to follow c) something I can actually get working d) something I'm slightly more comfortable including in libui static HRESULT resizeEdit(uiTable *t, WCHAR *wstr, int iItem, int iSubItem) { uiprivTableMetrics *m; RECT r; HDC dc; HFONT prevFont; TEXTMETRICW tm; SIZE textSize; RECT editRect, clientRect; HRESULT hr; hr = uiprivTableGetMetrics(t, iItem, iSubItem, &m); if (hr != S_OK) return hr; r = m->realTextRect; uiprivFree(m); // TODO check errors for all these dc = GetDC(t->hwnd); // use the list view DC since we're using its coordinate space prevFont = (HFONT) SelectObject(dc, hMessageFont); GetTextMetricsW(dc, &tm); GetTextExtentPoint32W(dc, wstr, wcslen(wstr), &textSize); SelectObject(dc, prevFont); ReleaseDC(t->hwnd, dc); SendMessageW(t->edit, EM_GETRECT, 0, (LPARAM) (&editRect)); r.left -= editRect.left; // find the top of the text r.top += ((r.bottom - r.top) - tm.tmHeight) / 2; // and move THAT by the right offset r.top -= editRect.top; r.right = r.left + textSize.cx; // the real listview does this to add some extra space at the end // TODO this still isn't enough space r.right += 4 * GetSystemMetrics(SM_CXEDGE) + GetSystemMetrics(SM_CYEDGE); // and make the bottom equally positioned to the top r.bottom = r.top + editRect.top + tm.tmHeight + editRect.top; // make sure the edit box doesn't stretch outside the listview // the list view just does this, which is dumb for when the list view wouldn't be visible at all, but given that it doesn't scroll the edit into view either... // TODO check errors GetClientRect(t->hwnd, &clientRect); IntersectRect(&r, &r, &clientRect); // TODO check error or use the right function SetWindowPos(t->edit, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER); return S_OK; } // the real list view intercepts these keys to control editing static LRESULT CALLBACK editSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIDSubclass, DWORD_PTR dwRefData) { uiTable *t = (uiTable *) dwRefData; HRESULT hr; switch (uMsg) { case WM_KEYDOWN: switch (wParam) { // TODO handle VK_TAB and VK_SHIFT+VK_TAB case VK_RETURN: hr = uiprivTableFinishEditingText(t); if (hr != S_OK) { // TODO } return 0; // yes, the real list view just returns here case VK_ESCAPE: hr = uiprivTableAbortEditingText(t); if (hr != S_OK) { // TODO } return 0; } break; // the real list view also forces these flags case WM_GETDLGCODE: return DLGC_HASSETSEL | DLGC_WANTALLKEYS; case WM_NCDESTROY: if (RemoveWindowSubclass(hwnd, editSubProc, uIDSubclass) == FALSE) logLastError(L"RemoveWindowSubclass()"); // fall through } return DefSubclassProc(hwnd, uMsg, wParam, lParam); } static HRESULT openEditControl(uiTable *t, int iItem, int iSubItem, uiprivTableColumnParams *p) { uiTableData *data; WCHAR *wstr; HRESULT hr; // the real list view accepts changes to the existing item when editing a new item hr = uiprivTableFinishEditingText(t); if (hr != S_OK) return hr; // the real list view creates the edit control with the string data = (*(t->model->mh->CellValue))(t->model->mh, t->model, iItem, p->textModelColumn); wstr = toUTF16(uiTableDataString(data)); uiFreeTableData(data); // TODO copy WS_EX_RTLREADING t->edit = CreateWindowExW(0, L"EDIT", wstr, // these styles are what the normal listview edit uses WS_CHILD | WS_CLIPSIBLINGS | WS_BORDER | ES_AUTOHSCROLL, // as is this size 0, 0, 16384, 16384, // and this control ID t->hwnd, (HMENU) 1, hInstance, NULL); if (t->edit == NULL) { logLastError(L"CreateWindowExW()"); uiprivFree(wstr); return E_FAIL; } SendMessageW(t->edit, WM_SETFONT, (WPARAM) hMessageFont, (LPARAM) TRUE); // TODO check errors SetWindowSubclass(t->edit, editSubProc, 0, (DWORD_PTR) t); hr = resizeEdit(t, wstr, iItem, iSubItem); if (hr != S_OK) // TODO proper cleanup return hr; // TODO check errors on these two, if any SetFocus(t->edit); ShowWindow(t->edit, SW_SHOW); SendMessageW(t->edit, EM_SETSEL, 0, (LPARAM) (-1)); uiprivFree(wstr); t->editedItem = iItem; t->editedSubitem = iSubItem; return S_OK; } HRESULT uiprivTableResizeWhileEditing(uiTable *t) { WCHAR *text; HRESULT hr; if (t->edit == NULL) return S_OK; text = windowText(t->edit); hr = resizeEdit(t, text, t->editedItem, t->editedSubitem); uiprivFree(text); return hr; } HRESULT uiprivTableFinishEditingText(uiTable *t) { uiprivTableColumnParams *p; uiTableData *data; char *text; if (t->edit == NULL) return S_OK; text = uiWindowsWindowText(t->edit); data = uiNewTableDataString(text); uiFreeText(text); p = (*(t->columns))[t->editedSubitem]; (*(t->model->mh->SetCellValue))(t->model->mh, t->model, t->editedItem, p->textModelColumn, data); uiFreeTableData(data); // always refresh the value in case the model rejected it if (SendMessageW(t->hwnd, LVM_UPDATE, (WPARAM) (t->editedItem), 0) == (LRESULT) (-1)) { logLastError(L"LVM_UPDATE"); return E_FAIL; } return uiprivTableAbortEditingText(t); } HRESULT uiprivTableAbortEditingText(uiTable *t) { HWND edit; if (t->edit == NULL) return S_OK; // set t->edit to NULL now so we don't trigger commits on focus killed edit = t->edit; t->edit = NULL; if (DestroyWindow(edit) == 0) { logLastError(L"DestroyWindow()"); return E_FAIL; } return S_OK; } HRESULT uiprivTableHandleNM_CLICK(uiTable *t, NMITEMACTIVATE *nm, LRESULT *lResult) { LVHITTESTINFO ht; uiprivTableColumnParams *p; int modelColumn, editableColumn; bool text, checkbox; uiTableData *data; int checked, editable; HRESULT hr; ZeroMemory(&ht, sizeof (LVHITTESTINFO)); ht.pt = nm->ptAction; if (SendMessageW(t->hwnd, LVM_SUBITEMHITTEST, 0, (LPARAM) (&ht)) == (LRESULT) (-1)) goto done; modelColumn = -1; editableColumn = -1; text = false; checkbox = false; p = (*(t->columns))[ht.iSubItem]; if (p->textModelColumn != -1) { modelColumn = p->textModelColumn; editableColumn = p->textEditableColumn; text = true; } else if (p->checkboxModelColumn != -1) { modelColumn = p->checkboxModelColumn; editableColumn = p->checkboxEditableColumn; checkbox = true; } else if (p->buttonModelColumn != -1) { modelColumn = p->buttonModelColumn; editableColumn = p->buttonClickableModelColumn; } if (modelColumn == -1) goto done; if (text && t->inDoubleClickTimer) // don't even ask for info if it's too soon to edit text goto done; switch (editableColumn) { case uiTableModelColumnNeverEditable: goto done; case uiTableModelColumnAlwaysEditable: break; default: data = (*(t->model->mh->CellValue))(t->model->mh, t->model, ht.iItem, editableColumn); editable = uiTableDataInt(data); uiFreeTableData(data); if (!editable) goto done; } if (text) { hr = openEditControl(t, ht.iItem, ht.iSubItem, p); if (hr != S_OK) return hr; } else if (checkbox) { if ((ht.flags & LVHT_ONITEMICON) == 0) goto done; data = (*(t->model->mh->CellValue))(t->model->mh, t->model, ht.iItem, modelColumn); checked = uiTableDataInt(data); uiFreeTableData(data); data = uiNewTableDataInt(!checked); (*(t->model->mh->SetCellValue))(t->model->mh, t->model, ht.iItem, modelColumn, data); uiFreeTableData(data); } else (*(t->model->mh->SetCellValue))(t->model->mh, t->model, ht.iItem, modelColumn, NULL); // always refresh the value in case the model rejected it if (SendMessageW(t->hwnd, LVM_UPDATE, (WPARAM) (ht.iItem), 0) == (LRESULT) (-1)) { logLastError(L"LVM_UPDATE"); return E_FAIL; } done: *lResult = 0; return S_OK; }