273 lines
7.7 KiB
C++
273 lines
7.7 KiB
C++
// 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)
|
|
{
|
|
uiTableValue *value;
|
|
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
|
|
value = (*(t->model->mh->CellValue))(t->model->mh, t->model, iItem, p->textModelColumn);
|
|
wstr = toUTF16(uiTableValueString(value));
|
|
uiFreeTableValue(value);
|
|
// 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;
|
|
uiTableValue *value;
|
|
char *text;
|
|
|
|
if (t->edit == NULL)
|
|
return S_OK;
|
|
text = uiWindowsWindowText(t->edit);
|
|
value = uiNewTableValueString(text);
|
|
uiFreeText(text);
|
|
p = (*(t->columns))[t->editedSubitem];
|
|
(*(t->model->mh->SetCellValue))(t->model->mh, t->model, t->editedItem, p->textModelColumn, value);
|
|
uiFreeTableValue(value);
|
|
// 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;
|
|
uiTableValue *value;
|
|
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:
|
|
value = (*(t->model->mh->CellValue))(t->model->mh, t->model, ht.iItem, editableColumn);
|
|
editable = uiTableValueInt(value);
|
|
uiFreeTableValue(value);
|
|
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;
|
|
value = (*(t->model->mh->CellValue))(t->model->mh, t->model, ht.iItem, modelColumn);
|
|
checked = uiTableValueInt(value);
|
|
uiFreeTableValue(value);
|
|
value = uiNewTableValueInt(!checked);
|
|
(*(t->model->mh->SetCellValue))(t->model->mh, t->model, ht.iItem, modelColumn, value);
|
|
uiFreeTableValue(value);
|
|
} 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;
|
|
}
|