diff --git a/windows/table.cpp b/windows/table.cpp index b3b397e8..5704b189 100644 --- a/windows/table.cpp +++ b/windows/table.cpp @@ -3,6 +3,7 @@ // general TODOs: // - tooltips don't work properly on columns with icons (the listview always thinks there's enough room for a short label because it's not taking the icon into account); is this a bug in our LVN_GETDISPINFO handler or something else? +// - should clicking on some other column of the same row, even one that doesn't edit, cancel editing? static uiTableTextColumnOptionalParams defaultTextColumnOptionalParams = { /*TODO.ColorModelColumn = */-1, @@ -83,8 +84,14 @@ void uiTableModelRowDeleted(uiTableModel *m, int oldIndex) static LRESULT CALLBACK tableSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIDSubclass, DWORD_PTR dwRefData) { uiTable *t = (uiTable *) dwRefData; + NMHDR *nmhdr = (NMHDR *) lParam; + bool finishEdit, abortEdit; + HWND header; LRESULT lResult; + HRESULT hr; + finishEdit = false; + abortEdit = false; switch (uMsg) { case WM_TIMER: if (wParam == (WPARAM) (&(t->inDoubleClickTimer))) { @@ -107,11 +114,69 @@ static LRESULT CALLBACK tableSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lResult = DefSubclassProc(hwnd, uMsg, wParam, lParam); t->inLButtonDown = FALSE; return lResult; + case WM_COMMAND: + if (HIWORD(wParam) == EN_UPDATE) { + // the real list view resizes the edit control on this notification specifically + // TODO + } + // the real list view accepts changes in this case + if (HIWORD(wParam) == EN_KILLFOCUS) + finishEdit = true; + break; // don't override default handling + case WM_NOTIFY: + // list view accepts changes on column resize, but does not provide such notifications :/ + header = (HWND) SendMessageW(t->hwnd, LVM_GETHEADER, 0, 0); + if (nmhdr->hwndFrom == header) { + NMHEADERW *nm = (NMHEADERW *) nmhdr; + + switch (nmhdr->code) { + case HDN_ITEMCHANGED: + if ((nm->pitem->mask & HDI_WIDTH) == 0) + break; + // fall through + case HDN_DIVIDERDBLCLICK: + case HDN_TRACK: + case HDN_ENDTRACK: + finishEdit = true; + } + } + // I think this mirrors the WM_COMMAND one above... TODO + if (nmhdr->code == NM_KILLFOCUS) + finishEdit = true; + break; // don't override default handling + case LVM_CANCELEDITLABEL: + finishEdit = true; + // TODO properly imitate notifiactions + break; // don't override default handling + // TODO finish edit on WM_WINDOWPOSCHANGING and WM_SIZE? + // for the next three: this item is about to go away; don't bother keeping changes + case LVM_SETITEMCOUNT: + if (wParam <= t->editedItem) + abortEdit = true; + break; // don't override default handling + case LVM_DELETEITEM: + if (wParam == t->editedItem) + abortEdit = true; + break; // don't override default handling + case LVM_DELETEALLITEMS: + abortEdit = true; + break; // don't override default handling case WM_NCDESTROY: if (RemoveWindowSubclass(hwnd, tableSubProc, uIDSubclass) == FALSE) logLastError(L"RemoveWindowSubclass()"); // fall through } + if (finishEdit) { + hr = uiprivTableFinishEditingText(t); + if (hr != S_OK) { + // TODO + } + } else if (abortEdit) { + hr = uiprivTableAbortEditingText(t); + if (hr != S_OK) { + // TODO + } + } return DefSubclassProc(hwnd, uMsg, wParam, lParam); } @@ -157,6 +222,7 @@ int uiprivTableProgress(uiTable *t, int item, int subitem, int modelColumn, LONG return progress; } +// TODO properly integrate compound statements static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nmhdr, LRESULT *lResult) { uiTable *t = uiTable(c); @@ -208,12 +274,14 @@ static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nmhdr, LRESULT *lResult) { NMLISTVIEW *nm = (NMLISTVIEW *) nmhdr; UINT oldSelected, newSelected; + HRESULT hr; - if (!t->inLButtonDown) + // TODO clean up these if cases + if (!t->inLButtonDown && t->edit == NULL) return FALSE; oldSelected = nm->uOldState & LVIS_SELECTED; newSelected = nm->uNewState & LVIS_SELECTED; - if (oldSelected == 0 && newSelected != 0) { + if (t->inLButtonDown && oldSelected == 0 && newSelected != 0) { t->inDoubleClickTimer = TRUE; // TODO check error SetTimer(t->hwnd, (UINT_PTR) (&(t->inDoubleClickTimer)), @@ -221,8 +289,29 @@ static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nmhdr, LRESULT *lResult) *lResult = 0; return TRUE; } + // the nm->iItem == -1 case is because that is used if "the change has been applied to all items in the list view" + if (t->edit != NULL && oldSelected != 0 && newSelected == 0 && (t->editedItem == nm->iItem || nm->iItem == -1)) { + // TODO see if the real list view accepts or rejects changes here; Windows Explorer accepts + hr = uiprivTableFinishEditingText(t); + if (hr != S_OK) { + // TODO + return FALSE; + } + *lResult = 0; + return TRUE; + } return FALSE; } + // the real list view accepts changes when scrolling or clicking column headers + case LVN_BEGINSCROLL: + case LVN_COLUMNCLICK: + hr = uiprivTableFinishEditingText(t); + if (hr != S_OK) { + // TODO + return FALSE; + } + *lResult = 0; + return TRUE; } return FALSE; } @@ -232,7 +321,12 @@ static void uiTableDestroy(uiControl *c) uiTable *t = uiTable(c); uiTableModel *model = t->model; std::vector::iterator it; + HRESULT hr; + hr = uiprivTableAbortEditingText(t); + if (hr != S_OK) { + // TODO + } uiWindowsUnregisterWM_NOTIFYHandler(t->hwnd); uiWindowsEnsureDestroyWindow(t->hwnd); // detach table from model diff --git a/windows/table.hpp b/windows/table.hpp index 6ab77d57..67b166f8 100644 --- a/windows/table.hpp +++ b/windows/table.hpp @@ -36,6 +36,8 @@ struct uiTable { BOOL inLButtonDown; BOOL inDoubleClickTimer; HWND edit; + int editedItem; + int editedSubitem; }; extern int uiprivTableProgress(uiTable *t, int item, int subitem, int modelColumn, LONG *pos); @@ -48,3 +50,5 @@ extern HRESULT uiprivUpdateImageListSize(uiTable *t); // tableevents.cpp extern HRESULT uiprivTableHandleNM_CLICK(uiTable *t, NMITEMACTIVATE *nm, LRESULT *lResult); +extern HRESULT uiprivTableFinishEditingText(uiTable *t); +extern HRESULT uiprivTableAbortEditingText(uiTable *t); diff --git a/windows/tableevents.cpp b/windows/tableevents.cpp index 4441da2e..d77e1ab6 100644 --- a/windows/tableevents.cpp +++ b/windows/tableevents.cpp @@ -17,6 +17,41 @@ static HRESULT itemRect(HRESULT hr, uiTable *t, UINT uMsg, WPARAM wParam, LONG l 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) { RECT itemLabel; @@ -27,6 +62,11 @@ static HRESULT openEditControl(uiTable *t, int iItem, int iSubItem, uiprivTableC LONG xInflate, yInflate; 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; + // compute this in advance so we don't have to needlessly call DestroyWindow() later // TODO deduplicate this code with tabledraw.cpp // TODO check LRESULT bad parameters here @@ -132,6 +172,27 @@ static HRESULT openEditControl(uiTable *t, int iItem, int iSubItem, uiprivTableC ShowWindow(t->edit, SW_SHOWDEFAULT); uiprivFree(wstr); + t->editedItem = iItem; + t->editedSubitem = iSubItem; + return S_OK; +} + +HRESULT uiprivTableFinishEditingText(uiTable *t) +{ + if (t->edit == NULL) + return S_OK; + return uiprivTableAbortEditingText(t); +} + +HRESULT uiprivTableAbortEditingText(uiTable *t) +{ + if (t->edit == NULL) + return S_OK; + if (DestroyWindow(t->edit) == 0) { + logLastError(L"DestroyWindow()"); + return E_FAIL; + } + t->edit = NULL; return S_OK; }