diff options
Diffstat (limited to 'prev/wintable')
| -rw-r--r-- | prev/wintable/accessibility.h | 1081 | ||||
| -rw-r--r-- | prev/wintable/api.h | 110 | ||||
| -rw-r--r-- | prev/wintable/checkboxes.h | 247 | ||||
| -rw-r--r-- | prev/wintable/children.h | 19 | ||||
| -rw-r--r-- | prev/wintable/coord.h | 164 | ||||
| -rw-r--r-- | prev/wintable/draw.h | 215 | ||||
| -rw-r--r-- | prev/wintable/events.h | 62 | ||||
| -rw-r--r-- | prev/wintable/header.h | 80 | ||||
| -rw-r--r-- | prev/wintable/hscroll.h | 52 | ||||
| -rw-r--r-- | prev/wintable/includethis.h | 73 | ||||
| -rw-r--r-- | prev/wintable/links | 3 | ||||
| -rw-r--r-- | prev/wintable/main.h | 165 | ||||
| -rw-r--r-- | prev/wintable/resize.h | 21 | ||||
| -rw-r--r-- | prev/wintable/scroll.h | 137 | ||||
| -rw-r--r-- | prev/wintable/scrollbarseries | 24 | ||||
| -rw-r--r-- | prev/wintable/select.h | 269 | ||||
| -rw-r--r-- | prev/wintable/test.c | 198 | ||||
| -rw-r--r-- | prev/wintable/update.h | 53 | ||||
| -rw-r--r-- | prev/wintable/util.h | 119 | ||||
| -rw-r--r-- | prev/wintable/vscroll.h | 65 |
20 files changed, 3157 insertions, 0 deletions
diff --git a/prev/wintable/accessibility.h b/prev/wintable/accessibility.h new file mode 100644 index 0000000..ff7364b --- /dev/null +++ b/prev/wintable/accessibility.h @@ -0,0 +1,1081 @@ +// 24 december 2014 + +// implement MSAA-conformant accessibility +// we need to use MSAA because UI Automation is too new for us +// unfortunately, MSAA's documentation is... very poor. very ambiguous, very inconsistent (just run through this file's commit history and watch the TODO progression to see) +// resources: +// http://msdn.microsoft.com/en-us/library/ms971338.aspx +// http://msdn.microsoft.com/en-us/library/windows/desktop/cc307844.aspx +// http://msdn.microsoft.com/en-us/library/windows/desktop/cc307847.aspx +// http://blogs.msdn.com/b/saraford/archive/2004/08/20/which-controls-support-which-msaa-properties-and-how-these-controls-implement-msaa-properties.aspx +// http://msdn.microsoft.com/en-us/library/ms971325 +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd318017%28v=vs.85%29.aspx +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd373624%28v=vs.85%29.aspx + +// notes: +// - TODO figure out what to do about header +// - a row extends as far right as the right edge of the last cell in the row; anything to the right of that is treated as table space (just like with mouse selection) +// - this has the added effect that hit-testing can only ever return either the table or a cell, never a row +// - cells have no children; checkbox cells are themselves the accessible object +// - TODO if we ever add combobox columns, this will need to change somehow +// - only Table and Cell can have focus; only Row can have selection +// - TODO allow selecting a cell? + +// TODOs: +// - make sure E_POINTER and RPC_E_DISCONNECTED are correct returns for IAccessible +// - return last error on newTableAcc() in all accessible functions +// - figure out what should be names and what should be values +// - figure out what to do about that header row +// - http://acccheck.codeplex.com/ + +// uncomment this to debug table linked list management +//#define TABLE_DEBUG_LINKEDLIST + +// TODO get rid of this +typedef struct tableAccWhat tableAccWhat; + +struct tableAccWhat { + LONG role; + intptr_t row; + intptr_t column; +}; + +struct tableAcc { + const IAccessibleVtbl *vtbl; + ULONG refcount; + struct table *t; + IAccessible *std; + tableAccWhat what; + + // the list of currently active accessibility objects is a doubly linked list + struct tableAcc *prev; + struct tableAcc *next; +}; + +#ifdef TABLE_DEBUG_LINKEDLIST +void list(struct table *t) +{ + struct tableAcc *ta; + + printf("\n"); + if (t->firstAcc == NULL) { + printf("\tempty\n"); + return; + } + printf("\t-> "); + for (ta = t->firstAcc; ta != NULL; ta = ta->next) + printf("%p ", ta); + printf("\n\t<- "); + for (ta = t->firstAcc; ta->next != NULL; ta = ta->next) + ; + for (; ta != NULL; ta = ta->prev) + printf("%p ", ta); + printf("\n"); +} +#endif + +// called after each allocation +static struct tableAcc *newTableAcc(struct table *t, LONG role, intptr_t row, intptr_t column); + +// common validation for accessibility functions that take varChild +// also normalizes what as if varChild == CHILDID_SELF +static HRESULT normalizeWhat(struct tableAcc *ta, VARIANT varChild, tableAccWhat *what) +{ + LONG cid; + + if (varChild.vt != VT_I4) + return E_INVALIDARG; + cid = varChild.lVal; + if (cid == CHILDID_SELF) + return S_OK; + cid--; + if (cid < 0) + return E_INVALIDARG; + switch (what->role) { + case ROLE_SYSTEM_TABLE: + // TODO +1? + if (cid >= ta->t->count) + return E_INVALIDARG; + what->role = ROLE_SYSTEM_ROW; + what->row = (intptr_t) cid; + what->column = -1; + break; + case ROLE_SYSTEM_ROW: + case ROLE_SYSTEM_CELL: + // TODO + // TODO also figure out what to do if the current row/cell become invalid (rows being removed, etc.) + break; + } + return S_OK; +} + +#define TA ((struct tableAcc *) this) + +static HRESULT STDMETHODCALLTYPE tableAccQueryInterface(IAccessible *this, REFIID riid, void **ppvObject) +{ + if (ppvObject == NULL) + return E_POINTER; + if (IsEqualIID(riid, &IID_IUnknown) || + IsEqualIID(riid, &IID_IDispatch) || + IsEqualIID(riid, &IID_IAccessible)) { + IAccessible_AddRef(this); + *ppvObject = (void *) this; + return S_OK; + } + *ppvObject = NULL; + return E_NOINTERFACE; +} + +// TODO use InterlockedIncrement()/InterlockedDecrement() for these? + +static ULONG STDMETHODCALLTYPE tableAccAddRef(IAccessible *this) +{ + TA->refcount++; + // TODO correct? + return TA->refcount; +} + +static ULONG STDMETHODCALLTYPE tableAccRelease(IAccessible *this) +{ + TA->refcount--; + if (TA->refcount == 0) { + struct tableAcc *prev, *next; + +#ifdef TABLE_DEBUG_LINKEDLIST +if (TA->t != NULL) { printf("before delete:"); list(TA->t); } +#endif + if (TA->t != NULL && TA->t->firstAcc == TA) + TA->t->firstAcc = TA->next; + prev = TA->prev; + next = TA->next; + if (prev != NULL) + prev->next = next; + if (next != NULL) + next->prev = prev; +#ifdef TABLE_DEBUG_LINKEDLIST +if (TA->t != NULL) { printf("after delete:"); list(TA->t); } +#endif + if (TA->std != NULL) + IAccessible_Release(TA->std); + tableFree(TA, "error freeing Table accessibility object"); + return 0; + } + return TA->refcount; +} + +// IDispatch +// TODO make sure relegating these to the standard accessibility object is harmless + +static HRESULT STDMETHODCALLTYPE tableAccGetTypeInfoCount(IAccessible *this, UINT *pctinfo) +{ + if (TA->t == NULL || TA->std == NULL) { + // TODO set values on error + return RPC_E_DISCONNECTED; + } + return IAccessible_GetTypeInfoCount(TA->std, pctinfo); +} + +static HRESULT STDMETHODCALLTYPE tableAccGetTypeInfo(IAccessible *this, UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) +{ + if (TA->t == NULL || TA->std == NULL) { + // TODO set values on error + return RPC_E_DISCONNECTED; + } + return IAccessible_GetTypeInfo(TA->std, iTInfo, lcid, ppTInfo); +} + +static HRESULT STDMETHODCALLTYPE tableAccGetIDsOfNames(IAccessible *this, REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) +{ + if (TA->t == NULL || TA->std == NULL) { + // TODO set values on error + return RPC_E_DISCONNECTED; + } + return IAccessible_GetIDsOfNames(TA->std, riid, rgszNames, cNames, lcid, rgDispId); +} + +static HRESULT STDMETHODCALLTYPE tableAccInvoke(IAccessible *this, DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) +{ + if (TA->t == NULL || TA->std == NULL) { + // TODO set values on error + return RPC_E_DISCONNECTED; + } + return IAccessible_Invoke(TA->std, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr); +} + +// IAccessible + +static HRESULT STDMETHODCALLTYPE tableAccget_accParent(IAccessible *this, IDispatch **ppdispParent) +{ + if (ppdispParent == NULL) + return E_POINTER; + // TODO set ppdispParent to zero? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + // TODO check if row/column is still valid + switch (TA->what.role) { + case ROLE_SYSTEM_TABLE: + // defer to standard accessible object + // TODO [EDGE CASE/POOR DOCUMENTATION?] https://msdn.microsoft.com/en-us/library/ms971325 says "Returns the IDispatch interface of the Table object."; isn't that just returning self? + return IAccessible_get_accParent(TA->std, ppdispParent); + case ROLE_SYSTEM_ROW: + *ppdispParent = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_TABLE, -1, -1); + return S_OK; + case ROLE_SYSTEM_CELL: + *ppdispParent = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, TA->what.row, -1); + return S_OK; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accChildCount(IAccessible *this, long *pcountChildren) +{ + if (pcountChildren == NULL) + return E_POINTER; + // TODO set pcountChildren to zero? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + switch (TA->what.role) { + case ROLE_SYSTEM_TABLE: + // TODO header row + *pcountChildren = (long) (TA->t->count); + return S_OK; + case ROLE_SYSTEM_ROW: + *pcountChildren = (long) (TA->t->nColumns); + return S_OK; + case ROLE_SYSTEM_CELL: + *pcountChildren = 0; + return S_OK; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; +} + +// TODO [EDGE CASE/NOT DOCUMENTED/CHECK SAMPLE] what happens if CHILDID_SELF is passed? +// TODO [EDGE CASE/NOT DOCUMENTED/CHECK SAMPLE] what SHOULD happen if an out of bounds ID is passed? +static HRESULT STDMETHODCALLTYPE tableAccget_accChild(IAccessible *this, VARIANT varChild, IDispatch **ppdispChild) +{ + LONG cid; + + if (ppdispChild == NULL) + return E_POINTER; + *ppdispChild = NULL; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + if (varChild.vt != VT_I4) + return E_INVALIDARG; + cid = varChild.lVal; + if (cid < 0) + // TODO really? + return E_INVALIDARG; + if (cid == CHILDID_SELF) + return E_FAIL; // TODO + cid--; + switch (TA->what.role) { + case ROLE_SYSTEM_TABLE: + // TODO table header + if (TA->t->count == 0) + return S_FALSE; + if (cid > TA->t->count - 1) + // TODO really? + return E_INVALIDARG; + *ppdispChild = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, cid, -1); + return S_OK; + case ROLE_SYSTEM_ROW: + // TODO verify that row is still valid + if (TA->t->nColumns == 0) + return S_FALSE; + if (cid > TA->t->nColumns - 1) + // TODO really? + return E_INVALIDARG; + *ppdispChild = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, TA->what.row, cid); + case ROLE_SYSTEM_CELL: + // TODO verify that row/column are still valid? + return S_FALSE; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accName(IAccessible *this, VARIANT varChild, BSTR *pszName) +{ + HRESULT hr; + tableAccWhat what; + + if (pszName == NULL) + return E_POINTER; + // TODO double-check that this must be set to zero + *pszName = NULL; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + switch (what.role) { + case ROLE_SYSTEM_TABLE: + // defer to standard accessible object + return IAccessible_get_accName(TA->std, varChild, pszName); + case ROLE_SYSTEM_ROW: + // TODO + return S_FALSE; + case ROLE_SYSTEM_CELL: + // TODO + return S_FALSE; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; +} + +// this differs quite much from what is described at https://msdn.microsoft.com/en-us/library/ms971325 +static HRESULT STDMETHODCALLTYPE tableAccget_accValue(IAccessible *this, VARIANT varChild, BSTR *pszValue) +{ + HRESULT hr; + tableAccWhat what; + WCHAR *text; + + if (pszValue == NULL) + return E_POINTER; + // TODO set pszValue to zero? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + switch (what.role) { + case ROLE_SYSTEM_TABLE: + // TODO really? + return IAccessible_get_accValue(TA->std, varChild, pszValue); + case ROLE_SYSTEM_ROW: + // TODO + return DISP_E_MEMBERNOTFOUND; + case ROLE_SYSTEM_CELL: + switch (TA->t->columnTypes[what.column]) { + case tableColumnText: + text = getCellText(TA->t, what.row, what.column); + // TODO check for error + *pszValue = SysAllocString(text); + returnCellData(TA->t, what.row, what.column, text); + return S_OK; + case tableColumnImage: + // TODO + return DISP_E_MEMBERNOTFOUND; + case tableColumnCheckbox: + // TODO!!!!!! + return DISP_E_MEMBERNOTFOUND; + } + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accDescription(IAccessible *this, VARIANT varChild, BSTR *pszDescription) +{ + HRESULT hr; + tableAccWhat what; + + if (pszDescription == NULL) + return E_POINTER; + *pszDescription = NULL; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + // don't support descriptions anyway; do return the above errors just to be safe + return DISP_E_MEMBERNOTFOUND; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accRole(IAccessible *this, VARIANT varChild, VARIANT *pvarRole) +{ + HRESULT hr; + tableAccWhat what; + + if (pvarRole == NULL) + return E_POINTER; + pvarRole->vt = VT_EMPTY; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + pvarRole->vt = VT_I4; + pvarRole->lVal = what.role; + return S_OK; +} + +// TODO reason about STATE_SYSTEM_INVISIBLE and STATE_SYSTEM_OFFSCREEN +static HRESULT STDMETHODCALLTYPE tableAccget_accState(IAccessible *this, VARIANT varChild, VARIANT *pvarState) +{ + HRESULT hr; + tableAccWhat what; + LONG state; + + if (pvarState == NULL) + return E_POINTER; + pvarState->vt = VT_EMPTY; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + + state = 0; + switch (what.role) { + case ROLE_SYSTEM_TABLE: + hr = IAccessible_get_accState(TA->std, varChild, pvarState); + if (hr != S_OK) + return hr; + // TODO make sure pvarState->vt == VT_I4 (what to return otherwise?) + state |= pvarState->lVal; + break; + case ROLE_SYSTEM_ROW: + state |= STATE_SYSTEM_SELECTABLE; + if (TA->t->selectedRow == what.row) + state |= STATE_SYSTEM_SELECTED; + break; + case ROLE_SYSTEM_CELL: + if (TA->t->columnTypes[what.column] == tableColumnCheckbox) { + // TODO is there no STATE_SYSTEM_CHECKABLE? + if (isCheckboxChecked(TA->t, what.row, what.column)) + state |= STATE_SYSTEM_CHECKED; + } + state |= STATE_SYSTEM_FOCUSABLE; + if (TA->t->selectedRow == what.row && TA->t->selectedColumn == what.column) + state |= STATE_SYSTEM_FOCUSED; + if (TA->t->columnTypes[what.column] != tableColumnCheckbox) + state |= STATE_SYSTEM_READONLY; + break; + } + pvarState->vt = VT_I4; + pvarState->lVal = state; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accHelp(IAccessible *this, VARIANT varChild, BSTR *pszHelp) +{ + HRESULT hr; + tableAccWhat what; + + if (pszHelp == NULL) + return E_POINTER; + *pszHelp = NULL; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + // don't support help anyway; do return the above errors just to be safe + return DISP_E_MEMBERNOTFOUND; +} + +// TODO Inspect.exe seems to ignore the DISP_E_MEMBERNOTFOUND and just tells us the help topic is the empty string; make sure this works right +static HRESULT STDMETHODCALLTYPE tableAccget_accHelpTopic(IAccessible *this, BSTR *pszHelpFile, VARIANT varChild, long *pidTopic) +{ + HRESULT hr; + tableAccWhat what; + + if (pszHelpFile == NULL || pidTopic == NULL) + return E_POINTER; + // TODO set pszHelpFile and pidTopic to zero? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + // don't support Windows Help (the super-old .hlp help files) topics anyway; do return the above errors just to be safe + // TODO [EDGE CASE??] or should we defer back to the standard accessible object? get_accHelp() was explicitly documented as not being supported by the standard/common controls, but this one isn't... + return DISP_E_MEMBERNOTFOUND; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accKeyboardShortcut(IAccessible *this, VARIANT varChild, BSTR *pszKeyboardShortcut) +{ + HRESULT hr; + tableAccWhat what; + + if (pszKeyboardShortcut == NULL) + return E_POINTER; + // TODO set pszKeyboardShortcut to zero? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + // defer to the standard accessible object for the table itself in case a program assigns an access key somehow (adjacent label?); MSDN says to, anyway + if (what.role == ROLE_SYSTEM_TABLE) + return IAccessible_get_accKeyboardShortcut(TA->std, varChild, pszKeyboardShortcut); + if (what.role == ROLE_SYSTEM_CELL) + ; // TODO implement this for checkbox cells? + return DISP_E_MEMBERNOTFOUND; +} + +// TODO TEST THIS +// TODO [EDGE CASE??] no parents? +static HRESULT STDMETHODCALLTYPE tableAccget_accFocus(IAccessible *this, VARIANT *pvarChild) +{ + HRESULT hr; + + if (pvarChild == NULL) + return E_POINTER; + // TODO set pvarChild to empty? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + // TODO verify that TA is still pointing to a valid row/column + + // first see if the control has the focus + // this is why a standard accessible object is needed on all accessible objects + hr = IAccessible_get_accFocus(TA->std, pvarChild); + // check the pvarChild type instead of hr + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd318479%28v=vs.85%29.aspx does this + // TODO [EDGE CASE] figure out why + if (pvarChild->vt != VT_I4) + return hr; + + switch (TA->what.role) { + case ROLE_SYSTEM_TABLE: + if (TA->t->selectedRow != -1 && TA->t->selectedColumn != -1) + goto selectedCell; + goto self; + case ROLE_SYSTEM_ROW: + if (TA->t->selectedRow != TA->what.row) + goto nothing; + goto selectedCell; + case ROLE_SYSTEM_CELL: + if (TA->t->selectedRow != TA->what.row) + goto nothing; + if (TA->t->selectedColumn != TA->what.column) + goto nothing; + goto self; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; + +nothing: + pvarChild->vt = VT_EMPTY; + // TODO really this one? + return S_FALSE; + +self: + pvarChild->vt = VT_I4; + pvarChild->lVal = CHILDID_SELF; + return S_OK; + +selectedCell: + pvarChild->vt = VT_I4; + pvarChild->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, TA->what.row, TA->what.column); + return S_OK; +} + +// note: https://msdn.microsoft.com/en-us/library/ms971325 is geared toward cell-based selection +// we have row-based selection, so only Tables implement this method, and they return a row +static HRESULT STDMETHODCALLTYPE tableAccget_accSelection(IAccessible *this, VARIANT *pvarChildren) +{ + if (pvarChildren == NULL) + return E_POINTER; + // TOOD set pvarChildren to VT_EMPTY? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + if (TA->what.role != ROLE_SYSTEM_TABLE) + // TODO [EDGE CASE] implement this for row anyway? how? + return DISP_E_MEMBERNOTFOUND; + if (TA->t->selectedRow == -1) { + pvarChildren->vt = VT_EMPTY; + return S_OK; + } + pvarChildren->vt = VT_DISPATCH; + pvarChildren->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, TA->t->selectedRow, -1); + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE tableAccget_accDefaultAction(IAccessible *this, VARIANT varChild, BSTR *pszDefaultAction) +{ + HRESULT hr; + tableAccWhat what; + + if (pszDefaultAction == NULL) + return E_POINTER; + *pszDefaultAction = NULL; + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + if (what.role == ROLE_SYSTEM_CELL) + ; // TODO implement this for checkbox cells? + return DISP_E_MEMBERNOTFOUND; +} + +// TODO should this method result in an event? +// TODO [EDGE CASE] how do we deselect? in the table or in the row? wouldn't this go against multiple selection? +// TODO require cell rows to be selected before focusing? +static HRESULT STDMETHODCALLTYPE tableAccaccSelect(IAccessible *this, long flagsSelect, VARIANT varChild) +{ + HRESULT hr; + tableAccWhat what; + + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + if (what.role == ROLE_SYSTEM_TABLE) // defer to the standard accessible object + return IAccessible_accSelect(TA->std, flagsSelect, varChild); + // reject flags that are only applicable to multiple selection + if ((flagsSelect & (SELFLAG_EXTENDSELECTION | SELFLAG_ADDSELECTION | SELFLAG_REMOVESELECTION)) != 0) + return E_INVALIDARG; + // and do nothing if a no-op + if (flagsSelect == SELFLAG_NONE) + return S_FALSE; + // TODO cast ~ expressions to the correct type + switch (what.role) { + case ROLE_SYSTEM_ROW: + // reject any other flag + if ((flagsSelect & (~SELFLAG_TAKESELECTION)) != 0) + return E_INVALIDARG; + if ((flagsSelect & SELFLAG_TAKESELECTION) != 0) { + if (TA->t->nColumns == 0) // can't select + return S_FALSE; + // if no column selected, select first (congruent to behavior of certain keyboard events) + // TODO handle GetLastError() + if (TA->t->selectedColumn == -1) + doselect(TA->t, what.row, TA->t->selectedColumn); + else + doselect(TA->t, what.row, 0); + return S_OK; + } + return S_FALSE; + case ROLE_SYSTEM_CELL: + // reject any other flag + if ((flagsSelect & (~SELFLAG_TAKEFOCUS)) != 0) + return E_INVALIDARG; + if ((flagsSelect & SELFLAG_TAKEFOCUS) != 0) { + doselect(TA->t, what.row, what.column); + return S_OK; + } + return S_FALSE; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE tableAccaccLocation(IAccessible *this, long *pxLeft, long *pyTop, long *pcxWidth, long *pcyHeight, VARIANT varChild) +{ + HRESULT hr; + tableAccWhat what; + RECT r; + POINT pt; + struct rowcol rc; + + if (pxLeft == NULL || pyTop == NULL || pcxWidth == NULL || pcyHeight == NULL) + return E_POINTER; + // TODO set the out parameters to zero? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + switch (what.role) { + case ROLE_SYSTEM_TABLE: + return IAccessible_accLocation(TA->std, pxLeft, pyTop, pcxWidth, pcyHeight, varChild); + case ROLE_SYSTEM_ROW: + // TODO actually write this + return E_FAIL; + case ROLE_SYSTEM_CELL: + rc.row = what.row; + rc.column = what.column; + if (!rowColumnToClientRect(TA->t, rc, &r)) { + // TODO [EDGE CASE] what do we do here? + // TODO we have to return something indicating that the object is off-screen + } + // TODO [EDGE CASE] intersect with client rect? + break; + } + pt.x = r.left; + pt.y = r.top; + if (ClientToScreen(TA->t->hwnd, &pt) == 0) + return HRESULT_FROM_WIN32(GetLastError()); + *pxLeft = pt.x; + *pyTop = pt.y; + *pcxWidth = r.right - r.left; + *pcyHeight = r.bottom - r.top; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE tableAccaccNavigate(IAccessible *this, long navDir, VARIANT varStart, VARIANT *pvarEndUpAt) +{ + HRESULT hr; + tableAccWhat what; + intptr_t row = -1; + intptr_t column = -1; + + if (pvarEndUpAt == NULL) + return E_POINTER; + // TODO set pvarEndUpAt to an invalid value? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varStart, &what); + if (hr != S_OK) + return hr; + switch (what.role) { + case ROLE_SYSTEM_TABLE: + switch (navDir) { + case NAVDIR_FIRSTCHILD: + // TODO header row + if (TA->t->count == 0) + goto nowhere; + row = 0; + goto specificRow; + case NAVDIR_LASTCHILD: + // TODO header row + if (TA->t->count == 0) + goto nowhere; + row = TA->t->count - 1; + goto specificRow; + } + // otherwise, defer to the standard accessible object + return IAccessible_accNavigate(TA->std, navDir, varStart, pvarEndUpAt); + case ROLE_SYSTEM_ROW: + row = what.row; + switch (navDir) { + case NAVDIR_UP: + case NAVDIR_PREVIOUS: + if (row == 0) // can't go up + goto nowhere; + row--; + // row should still be valid because normalizeWhat() returns an error if the current row is no longer valid, and if that's valid, the row above it should also be valid + goto specificRow; + case NAVDIR_DOWN: + case NAVDIR_NEXT: + if (row == TA->t->count - 1) // can't go down + goto nowhere; + row++; + // row should still be valid by the above conjecture + goto specificRow; + case NAVDIR_LEFT: + case NAVDIR_RIGHT: + goto nowhere; +// TODO this doesn't actually exist yet https://msdn.microsoft.com/en-us/library/ms971325 talks about it +// case NAVDIR_PARENT: +// goto tableItself; + case NAVDIR_FIRSTCHILD: + if (TA->t->nColumns == 0) + goto nowhere; + column = 0; + goto specificCell; + case NAVDIR_LASTCHILD: + if (TA->t->nColumns == 0) + goto nowhere; + column = TA->t->nColumns - 1; + goto specificCell; + } + // TODO differentiate between unsupported navigation directions and invalid navigation directions + goto nowhere; + case ROLE_SYSTEM_CELL: + row = what.row; + column = what.column; + switch (navDir) { + case NAVDIR_UP: + if (row == 0) // can't go up + goto nowhere; + row--; + goto specificCell; + case NAVDIR_DOWN: + if (row == TA->t->count - 1) // can't go down + goto nowhere; + row++; + goto specificCell; + case NAVDIR_LEFT: + case NAVDIR_PREVIOUS: + if (column == 0) // can't go left + goto nowhere; + column--; + goto specificCell; + case NAVDIR_RIGHT: + case NAVDIR_NEXT: + if (column == TA->t->nColumns - 1) // can't go right + goto nowhere; + column++; + goto specificCell; +// TODO this doesn't actually exist yet https://msdn.microsoft.com/en-us/library/ms971325 talks about it +// case NAVDIR_PARENT: +// goto specificRow; + case NAVDIR_FIRSTCHILD: + case NAVDIR_LASTCHILD: + goto nowhere; + } + // TODO differentiate between unsupported navigation directions and invalid navigation directions + goto nowhere; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; + +nowhere: + pvarEndUpAt->vt = VT_EMPTY; + return S_FALSE; + +tableItself: + pvarEndUpAt->vt = VT_DISPATCH; + pvarEndUpAt->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_TABLE, -1, -1); + return S_OK; + +specificRow: + pvarEndUpAt->vt = VT_DISPATCH; + pvarEndUpAt->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, row, -1); + return S_OK; + +specificCell: + pvarEndUpAt->vt = VT_DISPATCH; + pvarEndUpAt->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, row, column); + return S_OK; +} + +// TODO [EDGE CASE??] should this ever return parents? +static HRESULT STDMETHODCALLTYPE tableAccaccHitTest(IAccessible *this, long xLeft, long yTop, VARIANT *pvarChild) +{ + POINT pt; + struct rowcol rc; + RECT r; + + if (pvarChild == NULL) + return E_POINTER; + // TODO set pvarChild to an invalid value? + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + + pt.x = xLeft; + pt.y = yTop; + if (ScreenToClient(TA->t->hwnd, &pt) == 0) + return HRESULT_FROM_WIN32(GetLastError()); + + // first see if the point is even IN the table + if (GetClientRect(TA->t->hwnd, &r) == 0) + return HRESULT_FROM_WIN32(GetLastError()); + r.top += TA->t->headerHeight; + if (PtInRect(&r, pt) == 0) + goto outside; + + // now see if we're in a cell or in the table + // TODO also handle GetLastError() here + rc = clientCoordToRowColumn(TA->t, pt); + switch (TA->what.role) { + case ROLE_SYSTEM_TABLE: + // either the table or the cell + if (rc.row == -1 || rc.column == -1) + goto self; + goto specificCell; + case ROLE_SYSTEM_ROW: + // a specific cell, but only if in the same row + // TODO actually do we really need these spurious rc.column ==/!= -1 checks? + if (rc.row == TA->what.row) { + if (rc.column == -1) + // TODO de-GetLastError() this + panic("impossible situation TODO write this"); + goto specificCell; + } + goto outside; + case ROLE_SYSTEM_CELL: + if (rc.row == TA->what.row && rc.column == TA->what.column) + goto self; + goto outside; + } + // TODO actually do this right + // TODO un-GetLastError() this + panic("impossible blah blah blah TODO write this"); + return E_FAIL; + +outside: + pvarChild->vt = VT_EMPTY; + return S_FALSE; + +self: + pvarChild->vt = VT_I4; + pvarChild->lVal = CHILDID_SELF; + return S_OK; + +specificCell: + pvarChild->vt = VT_DISPATCH; + // TODO GetLastError() here too + pvarChild->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, rc.row, rc.column); + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE tableAccaccDoDefaultAction(IAccessible *this, VARIANT varChild) +{ + HRESULT hr; + tableAccWhat what; + + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + if (what.role == ROLE_SYSTEM_CELL) + ; // TODO implement this for checkbox cells? + return DISP_E_MEMBERNOTFOUND; +} + +// inconsistencies, inconsistencies +// https://msdn.microsoft.com/en-us/library/windows/desktop/dd318491%28v=vs.85%29.aspx says to just return E_NOTIMPL and not even bother with an implementation; in fact it doesn't even *have* the documentation anymore +// http://blogs.msdn.com/b/saraford/archive/2004/08/20/which-controls-support-which-msaa-properties-and-how-these-controls-implement-msaa-properties.aspx says never to return E_NOTIMPL from an IAccessible method (but it also discounts RPC_E_DISCONNECTED (not explicitly), so I'm guessing this is a much older document) +// let's just do what our put_accValue() does and do full validation, then just return DISP_E_MEMBERNOTFOUND +// I really hope UI Automation isn't so ambiguous and inconsistent... too bad I'm still choosing to support Windows XP while its market share (compared to *every other OS ever*) is still as large as it is +static HRESULT STDMETHODCALLTYPE tableAccput_accName(IAccessible *this, VARIANT varChild, BSTR szName) +{ + HRESULT hr; + tableAccWhat what; + + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + // don't support setting values anyway; do return the above errors just to be safe + return DISP_E_MEMBERNOTFOUND; +} + +static HRESULT STDMETHODCALLTYPE tableAccput_accValue(IAccessible *this, VARIANT varChild, BSTR szValue) +{ + HRESULT hr; + tableAccWhat what; + + if (TA->t == NULL || TA->std == NULL) + return RPC_E_DISCONNECTED; + what = TA->what; + hr = normalizeWhat(TA, varChild, &what); + if (hr != S_OK) + return hr; + // don't support setting values anyway; do return the above errors just to be safe + // TODO defer ROW_SYSTEM_TABLE to the standard accessible object? + // TODO implement for checkboxes? + return DISP_E_MEMBERNOTFOUND; +} + +static const IAccessibleVtbl tableAccVtbl = { + .QueryInterface = tableAccQueryInterface, + .AddRef = tableAccAddRef, + .Release = tableAccRelease, + .GetTypeInfoCount = tableAccGetTypeInfoCount, + .GetTypeInfo = tableAccGetTypeInfo, + .GetIDsOfNames = tableAccGetIDsOfNames, + .Invoke = tableAccInvoke, + .get_accParent = tableAccget_accParent, + .get_accChildCount = tableAccget_accChildCount, + .get_accChild = tableAccget_accChild, + .get_accName = tableAccget_accName, + .get_accValue = tableAccget_accValue, + .get_accDescription = tableAccget_accDescription, + .get_accRole = tableAccget_accRole, + .get_accState = tableAccget_accState, + .get_accHelp = tableAccget_accHelp, + .get_accHelpTopic = tableAccget_accHelpTopic, + .get_accKeyboardShortcut = tableAccget_accKeyboardShortcut, + .get_accFocus = tableAccget_accFocus, + .get_accSelection = tableAccget_accSelection, + .get_accDefaultAction = tableAccget_accDefaultAction, + .accSelect = tableAccaccSelect, + .accLocation = tableAccaccLocation, + .accNavigate = tableAccaccNavigate, + .accHitTest = tableAccaccHitTest, + .accDoDefaultAction = tableAccaccDoDefaultAction, + .put_accName = tableAccput_accName, + .put_accValue = tableAccput_accValue, +}; + +static struct tableAcc *newTableAcc(struct table *t, LONG role, intptr_t row, intptr_t column) +{ + struct tableAcc *ta; + HRESULT hr; + IAccessible *std; + + ta = (struct tableAcc *) tableAlloc(sizeof (struct tableAcc), "error creating Table accessibility object"); + ta->vtbl = &tableAccVtbl; + ta->refcount = 1; + ta->t = t; + // TODO adjust last argument + hr = CreateStdAccessibleObject(t->hwnd, OBJID_CLIENT, &IID_IAccessible, (void **) (&std)); + if (hr != S_OK) + // TODO panichresult + panic("error creating standard accessible object for Table"); + ta->std = std; + ta->what.role = role; + ta->what.row = row; + ta->what.column = column; + +#ifdef TABLE_DEBUG_LINKEDLIST +printf("before add:"); list(t); +#endif + ta->next = t->firstAcc; + if (t->firstAcc != NULL) + t->firstAcc->prev = ta; + t->firstAcc = ta; +#ifdef TABLE_DEBUG_LINKEDLIST +printf("after add:"); list(t); +#endif + + return ta; +} + +static void invalidateTableAccs(struct table *t) +{ + struct tableAcc *ta; + + for (ta = t->firstAcc; ta != NULL; ta = ta->next) { + ta->t = NULL; + IAccessible_Release(ta->std); + ta->std = NULL; + } + t->firstAcc = NULL; + NotifyWinEvent(EVENT_OBJECT_DESTROY, t->hwnd, OBJID_CLIENT, CHILDID_SELF); +} + +HANDLER(accessibilityHandler) +{ + struct tableAcc *ta; + + if (uMsg != WM_GETOBJECT) + return FALSE; + // OBJID_CLIENT evaluates to an expression of type LONG + // the documentation for WM_GETOBJECT says to cast "it" to a DWORD before comparing + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd373624%28v=vs.85%29.aspx casts them both to DWORDs; let's do that + // its two siblings only cast lParam, resulting in an erroneous DWORD to LONG comparison + // The Old New Thing book does not cast anything + // Microsoft's MSAA sample casts lParam to LONG instead! + // (As you can probably tell, the biggest problem with MSAA is that its documentation is ambiguous and/or self-contradictory...) + if (((DWORD) lParam) != ((DWORD) OBJID_CLIENT)) + return FALSE; + ta = newTableAcc(t, ROLE_SYSTEM_TABLE, -1, -1); + *lResult = LresultFromObject(&IID_IAccessible, wParam, (LPUNKNOWN) (ta)); + // TODO check *lResult + // TODO adjust pointer + IAccessible_Release((IAccessible *) ta); + return TRUE; +} diff --git a/prev/wintable/api.h b/prev/wintable/api.h new file mode 100644 index 0000000..09552e5 --- /dev/null +++ b/prev/wintable/api.h @@ -0,0 +1,110 @@ +// 8 december 2014 + +static void addColumn(struct table *t, WPARAM wParam, LPARAM lParam) +{ + t->nColumns++; + t->columnTypes = (int *) tableRealloc(t->columnTypes, t->nColumns * sizeof (int), "adding the new column type to the current Table's list of column types"); + t->columnTypes[t->nColumns - 1] = (int) wParam; + // TODO make a panicNoErrCode() or panicArg() for this + if (t->columnTypes[t->nColumns - 1] >= nTableColumnTypes) + panic("invalid column type passed to tableAddColumn"); + headerAddColumn(t, (WCHAR *) lParam); + update(t, TRUE); + // TODO only redraw the part of the client area where the new client went, if any + // (TODO when — if — adding autoresize, figure this one out) + + // TODO send a notification for all rows? +} + +// TODO what happens if the currently selected row is lost? +static void setRowCount(struct table *t, intptr_t rc) +{ + intptr_t old, i; + + old = t->count; + t->count = rc; + // we DO redraw everything because we don't want any rows that should no longer be there to remain on screen! + updateAll(t); // DONE + // TODO reset checkbox and selection logic if the current row for both no longer exists + // TODO really send all these notifications? + if (old < t->count) // rows added + for (i = old; i < t->count; i++) + NotifyWinEvent(EVENT_OBJECT_CREATE, t->hwnd, OBJID_CLIENT, i); + else if (old > t->count) // rows removed + for (i = old; i > t->count; i++) + NotifyWinEvent(EVENT_OBJECT_DESTROY, t->hwnd, OBJID_CLIENT, i); + // TODO update existing rows? +} + +HANDLER(apiHandlers) +{ + intptr_t *rcp; + intptr_t row; + + switch (uMsg) { + case WM_SETFONT: + // don't free the old font; see http://blogs.msdn.com/b/oldnewthing/archive/2008/09/12/8945692.aspx + t->font = (HFONT) wParam; + SendMessageW(t->header, WM_SETFONT, wParam, lParam); + // if we redraw, we have to redraw ALL of it; after all, the font changed! + if (LOWORD(lParam) != FALSE) + updateAll(t); // DONE + else + update(t, FALSE); // DONE + *lResult = 0; + return TRUE; + case WM_GETFONT: + *lResult = (LRESULT) (t->font); + return TRUE; + case tableAddColumn: + addColumn(t, wParam, lParam); + *lResult = 0; + return TRUE; + case tableSetRowCount: + rcp = (intptr_t *) lParam; + setRowCount(t, *rcp); + *lResult = 0; + return TRUE; + case tableGetSelection: + rcp = (intptr_t *) wParam; + if (rcp != NULL) + *rcp = t->selectedRow; + rcp = (intptr_t *) lParam; + if (rcp != NULL) + *rcp = t->selectedColumn; + *lResult = 0; + return TRUE; + case tableSetSelection: + // TODO does doselect() do validation? + rcp = (intptr_t *) wParam; + row = *rcp; + rcp = (intptr_t *) lParam; + if (rcp == NULL) + if (row == -1) + doselect(t, -1, -1); + else // select column 0, just like keyboard selections; TODO what if there aren't any columns? + doselect(t, row, 0); + else + doselect(t, row, *rcp); + *lResult = 0; + return TRUE; + } + return FALSE; +} + +static LRESULT notify(struct table *t, UINT code, intptr_t row, intptr_t column, uintptr_t data) +{ + tableNM nm; + + ZeroMemory(&nm, sizeof (tableNM)); + nm.nmhdr.hwndFrom = t->hwnd; + // TODO check for error from here? 0 is a valid ID (IDCANCEL) + nm.nmhdr.idFrom = GetDlgCtrlID(t->hwnd); + nm.nmhdr.code = code; + nm.row = row; + nm.column = column; + nm.columnType = t->columnTypes[nm.column]; + nm.data = data; + // TODO check for error from GetParent()? + return SendMessageW(GetParent(t->hwnd), WM_NOTIFY, (WPARAM) (nm.nmhdr.idFrom), (LPARAM) (&nm)); +} diff --git a/prev/wintable/checkboxes.h b/prev/wintable/checkboxes.h new file mode 100644 index 0000000..b44a0d0 --- /dev/null +++ b/prev/wintable/checkboxes.h @@ -0,0 +1,247 @@ +// 16 august 2014 + +enum { + checkboxStateChecked = 1 << 0, + checkboxStateHot = 1 << 1, + checkboxStatePushed = 1 << 2, + checkboxnStates = 1 << 3, +}; + +// TODO actually make this +#define panichresult(a, b) panic(a) + +static UINT dfcState(int cbstate) +{ + UINT ret; + + ret = DFCS_BUTTONCHECK; + if ((cbstate & checkboxStateChecked) != 0) + ret |= DFCS_CHECKED; + if ((cbstate & checkboxStateHot) != 0) + ret |= DFCS_HOT; + if ((cbstate & checkboxStatePushed) != 0) + ret |= DFCS_PUSHED; + return ret; +} + +static void drawFrameControlCheckbox(HDC dc, RECT *r, int cbState) +{ + if (DrawFrameControl(dc, r, DFC_BUTTON, dfcState(cbState)) == 0) + panic("error drawing Table checkbox image with DrawFrameControl()"); +} + +static void getFrameControlCheckboxSize(HDC dc, int *width, int *height) +{ + // there's no real metric around + // let's use SM_CX/YSMICON and hope for the best + *width = GetSystemMetrics(SM_CXSMICON); + *height = GetSystemMetrics(SM_CYSMICON); +} + +static int themestates[checkboxnStates] = { + CBS_UNCHECKEDNORMAL, // 0 + CBS_CHECKEDNORMAL, // checked + CBS_UNCHECKEDHOT, // hot + CBS_CHECKEDHOT, // checked | hot + CBS_UNCHECKEDPRESSED, // pushed + CBS_CHECKEDPRESSED, // checked | pushed + CBS_UNCHECKEDPRESSED, // hot | pushed + CBS_CHECKEDPRESSED, // checked | hot | pushed +}; + +static SIZE getStateSize(HDC dc, int cbState, HTHEME theme) +{ + SIZE s; + HRESULT res; + + res = GetThemePartSize(theme, dc, BP_CHECKBOX, themestates[cbState], NULL, TS_DRAW, &s); + if (res != S_OK) + panichresult("error getting theme part size for Table checkboxes", res); + return s; +} + +static void drawThemeCheckbox(HDC dc, RECT *r, int cbState, HTHEME theme) +{ + HRESULT res; + + res = DrawThemeBackground(theme, dc, BP_CHECKBOX, themestates[cbState], r, NULL); + if (res != S_OK) + panichresult("error drawing Table checkbox image from theme", res); +} + +static void getThemeCheckboxSize(HDC dc, int *width, int *height, HTHEME theme) +{ + SIZE size; + int cbState; + + size = getStateSize(dc, 0, theme); + for (cbState = 1; cbState < checkboxnStates; cbState++) { + SIZE against; + + against = getStateSize(dc, cbState, theme); + if (size.cx != against.cx || size.cy != against.cy) + // TODO make this use a no-information (or two ints) panic() + panic("size mismatch in Table checkbox states"); + } + *width = (int) size.cx; + *height = (int) size.cy; +} + +static void drawCheckbox(struct table *t, HDC dc, RECT *r, int cbState) +{ + if (t->theme != NULL) { + drawThemeCheckbox(dc, r, cbState, t->theme); + return; + } + drawFrameControlCheckbox(dc, r, cbState); +} + +static void freeCheckboxThemeData(struct table *t) +{ + if (t->theme != NULL) { + HRESULT res; + + res = CloseThemeData(t->theme); + if (res != S_OK) + panichresult("error closing Table checkbox theme", res); + t->theme = NULL; + } +} + +static void loadCheckboxThemeData(struct table *t) +{ + HDC dc; + + freeCheckboxThemeData(t); + dc = GetDC(t->hwnd); + if (dc == NULL) + panic("error getting Table DC for loading checkbox theme data"); + // ignore error; if it can't be done, we can fall back to DrawFrameControl() + if (t->theme == NULL) // try to open the theme + t->theme = OpenThemeData(t->hwnd, L"button"); + if (t->theme != NULL) // use the theme + getThemeCheckboxSize(dc, &(t->checkboxWidth), &(t->checkboxHeight), t->theme); + else // couldn't open; fall back + getFrameControlCheckboxSize(dc, &(t->checkboxWidth), &(t->checkboxHeight)); + if (ReleaseDC(t->hwnd, dc) == 0) + panic("error releasing Table DC for loading checkbox theme data"); +} + +static void redrawCheckboxRect(struct table *t, LPARAM lParam) +{ + struct rowcol rc; + RECT r; + + rc = lParamToRowColumn(t, lParam); + if (rc.row == -1 && rc.column == -1) + return; + if (t->columnTypes[rc.column] != tableColumnCheckbox) + return; + if (!rowColumnToClientRect(t, rc, &r)) + return; + // TODO only the checkbox rect? + if (InvalidateRect(t->hwnd, &r, TRUE) == 0) + panic("error redrawing Table checkbox rect for mouse events"); +} + +HANDLER(checkboxMouseMoveHandler) +{ + // don't actually check to see if the mouse is in the checkbox rect + // if there's scrolling without mouse movement, that will change + // instead, drawCell() will handle it + if (!t->checkboxMouseOverLast) { + t->checkboxMouseOverLast = TRUE; + retrack(t); + } else + redrawCheckboxRect(t, t->checkboxMouseOverLastPoint); + t->checkboxMouseOverLastPoint = lParam; + redrawCheckboxRect(t, t->checkboxMouseOverLastPoint); + *lResult = 0; + return TRUE; +} + +HANDLER(checkboxMouseLeaveHandler) +{ + if (t->checkboxMouseOverLast) + redrawCheckboxRect(t, t->checkboxMouseOverLastPoint); + // TODO remember what I wanted to do here in the case of a held mouse button + t->checkboxMouseOverLast = FALSE; + *lResult = 0; + return TRUE; +} + +HANDLER(checkboxMouseDownHandler) +{ + struct rowcol rc; + RECT r; + POINT pt; + + rc = lParamToRowColumn(t, lParam); + if (rc.row == -1 || rc.column == -1) + return FALSE; + if (t->columnTypes[rc.column] != tableColumnCheckbox) + return FALSE; + if (!rowColumnToClientRect(t, rc, &r)) + return FALSE; + toCheckboxRect(t, &r, 0); + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + if (PtInRect(&r, pt) == 0) + return FALSE; + t->checkboxMouseDown = TRUE; + t->checkboxMouseDownRow = rc.row; + t->checkboxMouseDownColumn = rc.column; + // TODO redraw the whole cell? + if (InvalidateRect(t->hwnd, &r, TRUE) == 0) + panic("error redrawing Table checkbox after mouse down"); + *lResult = 0; + return TRUE; +} + +HANDLER(checkboxMouseUpHandler) +{ + struct rowcol rc; + RECT r; + POINT pt; + + if (!t->checkboxMouseDown) + return FALSE; + // the logic behind goto wrongUp is that the mouse must be released on the same checkbox + rc = lParamToRowColumn(t, lParam); + if (rc.row == -1 || rc.column == -1) + goto wrongUp; + if (rc.row != t->checkboxMouseDownRow || rc.column != t->checkboxMouseDownColumn) + goto wrongUp; + if (t->columnTypes[rc.column] != tableColumnCheckbox) + goto wrongUp; + if (!rowColumnToClientRect(t, rc, &r)) + goto wrongUp; + toCheckboxRect(t, &r, 0); + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + if (PtInRect(&r, pt) == 0) + goto wrongUp; + notify(t, tableNotificationCellCheckboxToggled, rc.row, rc.column, 0); + t->checkboxMouseDown = FALSE; + // TODO redraw the whole cell? + if (InvalidateRect(t->hwnd, &r, TRUE) == 0) + panic("error redrawing Table checkbox after mouse up"); + // TODO really only the row? no way to specify column too? + NotifyWinEvent(EVENT_OBJECT_STATECHANGE, t->hwnd, OBJID_CLIENT, rc.row); + *lResult = 0; + return TRUE; +wrongUp: + if (t->checkboxMouseDown) { + rc.row = t->checkboxMouseDownRow; + rc.column = t->checkboxMouseDownColumn; + if (rowColumnToClientRect(t, rc, &r)) + // TODO only the checkbox rect? + if (InvalidateRect(t->hwnd, &r, TRUE) == 0) + panic("error redrawing Table checkbox rect for aborted mouse up event"); + } + // if we landed on another checkbox, be sure to draw that one too + if (t->checkboxMouseOverLast) + redrawCheckboxRect(t, t->checkboxMouseOverLastPoint); + t->checkboxMouseDown = FALSE; + return FALSE; // TODO really? +} diff --git a/prev/wintable/children.h b/prev/wintable/children.h new file mode 100644 index 0000000..6a3aff3 --- /dev/null +++ b/prev/wintable/children.h @@ -0,0 +1,19 @@ +// 7 december 2014 + +static const handlerfunc commandHandlers[] = { + NULL, +}; + +static const handlerfunc notifyHandlers[] = { + headerNotifyHandler, + NULL, +}; + +HANDLER(childrenHandlers) +{ + if (uMsg == WM_COMMAND) + return runHandlers(commandHandlers, t, uMsg, wParam, lParam, lResult); + if (uMsg == WM_NOTIFY) + return runHandlers(notifyHandlers, t, uMsg, wParam, lParam, lResult); + return FALSE; +} diff --git a/prev/wintable/coord.h b/prev/wintable/coord.h new file mode 100644 index 0000000..52a70c9 --- /dev/null +++ b/prev/wintable/coord.h @@ -0,0 +1,164 @@ +// 4 december 2014 + +// TODO find a better place for these (metrics.h?) +static LONG textHeight(struct table *t, HDC dc, BOOL select) +{ + BOOL release; + HFONT prevfont, newfont; + TEXTMETRICW tm; + + release = FALSE; + if (dc == NULL) { + dc = GetDC(t->hwnd); + if (dc == NULL) + panic("error getting Table DC for rowHeight()"); + release = TRUE; + } + if (select) + prevfont = selectFont(t, dc, &newfont); + if (GetTextMetricsW(dc, &tm) == 0) + panic("error getting text metrics for rowHeight()"); + if (select) + deselectFont(dc, prevfont, newfont); + if (release) + if (ReleaseDC(t->hwnd, dc) == 0) + panic("error releasing Table DC for rowHeight()"); + return tm.tmHeight; +} + +#define tableImageWidth() GetSystemMetrics(SM_CXSMICON) +#define tableImageHeight() GetSystemMetrics(SM_CYSMICON) + +// TODO omit column types that are not present? +static LONG rowHeight(struct table *t, HDC dc, BOOL select) +{ + LONG height; + LONG tmHeight; + + height = tableImageHeight(); // start with this to avoid two function calls + tmHeight = textHeight(t, dc, select); + if (height < tmHeight) + height = tmHeight; + if (height < t->checkboxHeight) + height = t->checkboxHeight; + return height; +} + +#define rowht(t) rowHeight(t, NULL, TRUE) + +struct rowcol { + intptr_t row; + intptr_t column; +}; + +static struct rowcol clientCoordToRowColumn(struct table *t, POINT pt) +{ + RECT r; + struct rowcol rc; + intptr_t i; + + if (GetClientRect(t->hwnd, &r) == 0) + panic("error getting Table client rect in clientCoordToRowColumn()"); + r.top += t->headerHeight; + if (PtInRect(&r, pt) == 0) + goto outside; + + // the row is easy + pt.y -= t->headerHeight; + rc.row = (pt.y / rowht(t)) + t->vscrollpos; + if (rc.row >= t->count) + goto outside; + + // the column... not so much + // we scroll p.x, then subtract column widths until we cross the left edge of the control + pt.x += t->hscrollpos; + rc.column = 0; + for (i = 0; i < t->nColumns; i++) { + pt.x -= columnWidth(t, i); + // use <, not <=, here: + // assume r.left and t->hscrollpos == 0; + // given the first column is 100 wide, + // pt.x == 0 (first pixel of col 0) -> p.x - 100 == -100 < 0 -> break + // pt.x == 99 (last pixel of col 0) -> p.x - 100 == -1 < 0 -> break + // pt.x == 100 (first pixel of col 1) -> p.x - 100 == 0 >= 0 -> next column + if (pt.x < r.left) + break; + rc.column++; + } + if (rc.column >= t->nColumns) + goto outside; + + return rc; + +outside: + rc.row = -1; + rc.column = -1; + return rc; +} + +// same as client coordinates, but stored in a lParam (like the various mouse messages provide) +static struct rowcol lParamToRowColumn(struct table *t, LPARAM lParam) +{ + POINT pt; + + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + return clientCoordToRowColumn(t, pt); +} + +// returns TRUE if the row is visible (even partially visible) and thus has a rectangle in the client area; FALSE otherwise +static BOOL rowColumnToClientRect(struct table *t, struct rowcol rc, RECT *r) +{ + RECT client; + RECT out; // don't change r if we return FALSE + LONG height; + intptr_t xpos; + intptr_t i; + + if (rc.row < t->vscrollpos) + return FALSE; + rc.row -= t->vscrollpos; // align with client.top + + if (GetClientRect(t->hwnd, &client) == 0) + panic("error getting Table client rect in rowColumnToClientRect()"); + client.top += t->headerHeight; + + height = rowht(t); + out.top = client.top + (rc.row * height); + if (out.top >= client.bottom) // >= because RECT.bottom is the first pixel outside the rectangle + return FALSE; + out.bottom = out.top + height; + + // and again the columns are the hard part + // so we start with client.left - t->hscrollpos, then keep adding widths until we get to the column we want + xpos = client.left - t->hscrollpos; + for (i = 0; i < rc.column; i++) + xpos += columnWidth(t, i); + // did we stray too far to the right? if so it's not visible + if (xpos >= client.right) // >= because RECT.right is the first pixel outside the rectangle + return FALSE; + out.left = xpos; + out.right = xpos + columnWidth(t, rc.column); + // and is this too far to the left? + if (out.right < client.left) // < because RECT.left is the first pixel inside the rect + return FALSE; + + *r = out; + return TRUE; +} + +// TODO idealCoordToRowColumn/rowColumnToIdealCoord? + +static void toCellContentRect(struct table *t, RECT *r, LRESULT xoff, intptr_t width, intptr_t height) +{ + if (xoff == 0) + xoff = SendMessageW(t->header, HDM_GETBITMAPMARGIN, 0, 0); + r->left += xoff; + if (width != 0) + r->right = r->left + width; + if (height != 0) + // TODO vertical center + r->bottom = r->top + height; +} + +#define toCheckboxRect(t, r, xoff) toCellContentRect(t, r, xoff, t->checkboxWidth, t->checkboxHeight) diff --git a/prev/wintable/draw.h b/prev/wintable/draw.h new file mode 100644 index 0000000..3aeb300 --- /dev/null +++ b/prev/wintable/draw.h @@ -0,0 +1,215 @@ +// 8 december 2014 + +// TODO move to api.h? definitely move somewhere +static WCHAR *getCellText(struct table *t, intptr_t row, intptr_t column) +{ + return (WCHAR *) notify(t, tableNotificationGetCellData, row, column, 0); +} +static void returnCellData(struct table *t, intptr_t row, intptr_t column, void *what) +{ + notify(t, tableNotificationFinishedWithCellData, row, column, (uintptr_t) what); +} +static int isCheckboxChecked(struct table *t, intptr_t row, intptr_t column) +{ + return notify(t, tableNotificationGetCellData, row, column, 0) != 0; +} + +struct drawCellParams { + intptr_t row; + intptr_t column; + LONG x; + LONG y; + LONG width; // of column + LONG height; // rowHeight() + LRESULT xoff; // result of HDM_GETBITMAPMARGIN +}; + +static void drawTextCell(struct table *t, HDC dc, struct drawCellParams *p, RECT *r, int textColor) +{ + WCHAR *text; + + toCellContentRect(t, r, p->xoff, 0, 0); // TODO get the text height + if (SetTextColor(dc, GetSysColor(textColor)) == CLR_INVALID) + panic("error setting Table cell text color"); + if (SetBkMode(dc, TRANSPARENT) == 0) + panic("error setting transparent text drawing mode for Table cell"); + text = getCellText(t, p->row, p->column); + if (DrawTextExW(dc, text, -1, r, DT_END_ELLIPSIS | DT_LEFT | DT_NOPREFIX | DT_SINGLELINE, NULL) == 0) + panic("error drawing Table cell text"); + returnCellData(t, p->row, p->column, text); +} + +static void drawImageCell(struct table *t, HDC dc, struct drawCellParams *p, RECT *r) +{ + HBITMAP bitmap; + BITMAP bi; + HDC idc; + HBITMAP previbitmap; + BLENDFUNCTION bf; + + // only call tableImageWidth() and tableImageHeight() here in case it changes partway through + // we can get the values back out with basic subtraction (r->right - r->left/r->bottom - r->top) + toCellContentRect(t, r, p->xoff, tableImageWidth(), tableImageHeight()); + + bitmap = (HBITMAP) notify(t, tableNotificationGetCellData, p->row, p->column, 0); + ZeroMemory(&bi, sizeof (BITMAP)); + if (GetObject(bitmap, sizeof (BITMAP), &bi) == 0) + panic("error getting Table cell image dimensions for drawing"); + // is it even possible to enforce the type of bitmap we need here based on the contents of the BITMAP (or even the DIBSECTION) structs? + + idc = CreateCompatibleDC(dc); + if (idc == NULL) + panic("error creating compatible DC for Table image cell drawing"); + previbitmap = SelectObject(idc, bitmap); + if (previbitmap == NULL) + panic("error selecting Table cell image into compatible DC for image drawing"); + + ZeroMemory(&bf, sizeof (BLENDFUNCTION)); + bf.BlendOp = AC_SRC_OVER; + bf.BlendFlags = 0; + bf.SourceConstantAlpha = 255; // per-pixel alpha values + bf.AlphaFormat = AC_SRC_ALPHA; + if (AlphaBlend(dc, r->left, r->top, r->right - r->left, r->bottom - r->top, + idc, 0, 0, bi.bmWidth, bi.bmHeight, bf) == FALSE) + panic("error drawing image into Table cell"); + + if (SelectObject(idc, previbitmap) != bitmap) + panic("error deselecting Table cell image for drawing image"); + if (DeleteDC(idc) == 0) + panic("error deleting Table compatible DC for image cell drawing"); + + returnCellData(t, p->row, p->column, bitmap); +} + +static void drawCheckboxCell(struct table *t, HDC dc, struct drawCellParams *p, RECT *r) +{ + POINT pt; + int cbState; + + toCheckboxRect(t, r, p->xoff); + cbState = 0; + if (isCheckboxChecked(t, p->row, p->column)) + cbState |= checkboxStateChecked; + if (t->checkboxMouseDown) + if (p->row == t->checkboxMouseDownRow && p->column == t->checkboxMouseDownColumn) + cbState |= checkboxStatePushed; + if (t->checkboxMouseOverLast) { + pt.x = GET_X_LPARAM(t->checkboxMouseOverLastPoint); + pt.y = GET_Y_LPARAM(t->checkboxMouseOverLastPoint); + if (PtInRect(r, pt) != 0) + cbState |= checkboxStateHot; + } + drawCheckbox(t, dc, r, cbState); +} + +static void drawCell(struct table *t, HDC dc, struct drawCellParams *p) +{ + RECT r; + HBRUSH background; + int textColor; + RECT cellrect; + + // TODO verify these two + background = (HBRUSH) (COLOR_WINDOW + 1); + textColor = COLOR_WINDOWTEXT; + if (t->selectedRow == p->row) { + // these are the colors wine uses (http://source.winehq.org/source/dlls/comctl32/listview.c) + // the two for unfocused are also suggested by http://stackoverflow.com/questions/10428710/windows-forms-inactive-highlight-color + background = (HBRUSH) (COLOR_HIGHLIGHT + 1); + textColor = COLOR_HIGHLIGHTTEXT; + if (GetFocus() != t->hwnd) { + background = (HBRUSH) (COLOR_BTNFACE + 1); + textColor = COLOR_BTNTEXT; + } + // TODO disabled + } + + r.left = p->x; + r.right = p->x + p->width; + r.top = p->y; + r.bottom = p->y + p->height; + if (FillRect(dc, &r, background) == 0) + panic("error filling Table cell background"); + cellrect = r; // save for drawing the focus rect + + switch (t->columnTypes[p->column]) { + case tableColumnText: + drawTextCell(t, dc, p, &r, textColor); + break; + case tableColumnImage: + drawImageCell(t, dc, p, &r); + break; + case tableColumnCheckbox: + drawCheckboxCell(t, dc, p, &r); + break; + } + + // TODO in front of or behind the cell contents? + if (t->selectedRow == p->row && t->selectedColumn == p->column) + if (DrawFocusRect(dc, &cellrect) == 0) + panic("error drawing focus rect on current Table cell"); +} + +// TODO use cliprect +static void draw(struct table *t, HDC dc, RECT cliprect, RECT client) +{ + intptr_t i, j; + HFONT prevfont, newfont; + struct drawCellParams p; + + prevfont = selectFont(t, dc, &newfont); + + client.top += t->headerHeight; + + ZeroMemory(&p, sizeof (struct drawCellParams)); + p.height = rowHeight(t, dc, FALSE); + p.xoff = SendMessageW(t->header, HDM_GETBITMAPMARGIN, 0, 0); + + p.y = client.top; + for (i = t->vscrollpos; i < t->count; i++) { + p.row = i; + p.x = client.left - t->hscrollpos; + for (j = 0; j < t->nColumns; j++) { + p.column = j; + p.width = columnWidth(t, p.column); + drawCell(t, dc, &p); + p.x += p.width; + } + p.y += p.height; + if (p.y >= client.bottom) // >= because RECT.bottom is the first pixel outside the rect + break; + } + + deselectFont(dc, prevfont, newfont); +} + +HANDLER(drawHandlers) +{ + HDC dc; + PAINTSTRUCT ps; + RECT client; + RECT r; + + if (uMsg != WM_PAINT && uMsg != WM_PRINTCLIENT) + return FALSE; + if (GetClientRect(t->hwnd, &client) == 0) + panic("error getting client rect for Table painting"); + if (uMsg == WM_PAINT) { + dc = BeginPaint(t->hwnd, &ps); + if (dc == NULL) + panic("error beginning Table painting"); + r = ps.rcPaint; + } else { + dc = (HDC) wParam; + r = client; + } + draw(t, dc, r, client); + if (uMsg == WM_PAINT) + EndPaint(t->hwnd, &ps); + // this is correct for WM_PRINTCLIENT; see http://stackoverflow.com/a/27362258/3408572 + *lResult = 0; + return TRUE; +} + +// TODO redraw selected row on focus change +// TODO here or in select.h? diff --git a/prev/wintable/events.h b/prev/wintable/events.h new file mode 100644 index 0000000..fbd1522 --- /dev/null +++ b/prev/wintable/events.h @@ -0,0 +1,62 @@ +// 5 december 2014 + +// TODO handler functions don't work here because you can't have more than one for the mouse ones... + +static const handlerfunc keyDownHandlers[] = { + keyDownSelectHandler, + NULL, +}; + +static const handlerfunc keyUpHandlers[] = { + NULL, +}; + +static const handlerfunc charHandlers[] = { + NULL, +}; + +static const handlerfunc mouseMoveHandlers[] = { + checkboxMouseMoveHandler, + NULL, +}; + +static const handlerfunc mouseLeaveHandlers[] = { + checkboxMouseLeaveHandler, + NULL, +}; + +static const handlerfunc lbuttonDownHandlers[] = { + mouseDownSelectHandler, + NULL, +}; + +static const handlerfunc lbuttonUpHandlers[] = { + checkboxMouseUpHandler, + NULL, +}; + +// TODO remove or something? depends on if we implement combobox and how +static const handlerfunc mouseWheelHandlers[] = { + NULL, +}; + +// TODO WM_MOUSEHOVER, other mouse buttons + +HANDLER(eventHandlers) +{ + switch (uMsg) { +#define eventHandler(msg, array) \ + case msg: \ + return runHandlers(array, t, uMsg, wParam, lParam, lResult); + eventHandler(WM_KEYDOWN, keyDownHandlers) + eventHandler(WM_KEYUP, keyUpHandlers) + eventHandler(WM_CHAR, charHandlers) + eventHandler(WM_MOUSEMOVE, mouseMoveHandlers) + eventHandler(WM_MOUSELEAVE, mouseLeaveHandlers) + eventHandler(WM_LBUTTONDOWN, lbuttonDownHandlers) + eventHandler(WM_LBUTTONUP, lbuttonUpHandlers) + eventHandler(WM_MOUSEWHEEL, mouseWheelHandlers) +#undef eventHandler + } + return FALSE; +} diff --git a/prev/wintable/header.h b/prev/wintable/header.h new file mode 100644 index 0000000..b87f7c6 --- /dev/null +++ b/prev/wintable/header.h @@ -0,0 +1,80 @@ +// 7 december 2014 + +// TODO verify header events (double-clicking on a divider, for example) + +static void makeHeader(struct table *t, HINSTANCE hInstance) +{ + t->header = CreateWindowExW(0, + WC_HEADERW, L"", + // don't set WS_VISIBLE; according to MSDN we create the header hidden as part of setting the initial position (http://msdn.microsoft.com/en-us/library/windows/desktop/ff485935%28v=vs.85%29.aspx) + // TODO WS_BORDER? + // TODO is HDS_HOTTRACK needed? + WS_CHILD | HDS_FULLDRAG | HDS_HORZ | HDS_HOTTRACK, + 0, 0, 0, 0, // no initial size + t->hwnd, (HMENU) 100, hInstance, NULL); + if (t->header == NULL) + panic("error creating Table header"); +} + +static void destroyHeader(struct table *t) +{ + if (DestroyWindow(t->header) == 0) + panic("error destroying Table header"); +} + +// to avoid weird bugs, the only functions allowed to call this one are the horizontal scroll functions +// when we need to reposition the header in a situation other than a user-initiated scroll, we use a dummy scroll (hscrollby(t, 0)) +// see update() in update.h +static void repositionHeader(struct table *t) +{ + RECT r; + WINDOWPOS wp; + HDLAYOUT l; + + if (GetClientRect(t->hwnd, &r) == 0) + panic("error getting client rect for Table header repositioning"); + // we fake horizontal scrolling here by extending the client rect to the left by the scroll position + r.left -= t->hscrollpos; + l.prc = &r; + l.pwpos = ℘ + if (SendMessageW(t->header, HDM_LAYOUT, 0, (LPARAM) (&l)) == FALSE) + panic("error getting new Table header position"); + if (SetWindowPos(t->header, wp.hwndInsertAfter, + wp.x, wp.y, wp.cx, wp.cy, + // see above on showing the header here instead of in the CreateWindowExW() call + wp.flags | SWP_SHOWWINDOW) == 0) + panic("error repositioning Table header"); + t->headerHeight = wp.cy; +} + +static void headerAddColumn(struct table *t, WCHAR *name) +{ + HDITEMW item; + + ZeroMemory(&item, sizeof (HDITEMW)); + item.mask = HDI_WIDTH | HDI_TEXT | HDI_FORMAT; + item.cxy = 200; // TODO + item.pszText = name; + item.fmt = HDF_LEFT | HDF_STRING; + // TODO replace 100 with (t->nColumns - 1) + if (SendMessage(t->header, HDM_INSERTITEM, (WPARAM) (100), (LPARAM) (&item)) == (LRESULT) (-1)) + panic("error adding column to Table header"); +} + +// TODO is this triggered if we programmatically move headers (for autosizing)? +HANDLER(headerNotifyHandler) +{ + NMHDR *nmhdr = (NMHDR *) lParam; + + if (nmhdr->hwndFrom != t->header) + return FALSE; + if (nmhdr->code != HDN_ITEMCHANGED) + return FALSE; + update(t, TRUE); + // TODO make more intelligent + // to do this, we have to redraw the column to the left of the divider that was dragged and scroll everything to the right normally (leaving the hole that was scrolled invalidated as well) + // of course, this implies that dragging a divider only resizes the column of which it is the right side of and moves all others + InvalidateRect(t->hwnd, NULL, TRUE); + *lResult = 0; + return TRUE; +} diff --git a/prev/wintable/hscroll.h b/prev/wintable/hscroll.h new file mode 100644 index 0000000..1560490 --- /dev/null +++ b/prev/wintable/hscroll.h @@ -0,0 +1,52 @@ +// 9 december 2014 + +// forward declaration needed here +static void repositionHeader(struct table *); + +static struct scrollParams hscrollParams(struct table *t) +{ + struct scrollParams p; + + ZeroMemory(&p, sizeof (struct scrollParams)); + p.pos = &(t->hscrollpos); + p.pagesize = t->hpagesize; + p.length = t->width; + p.scale = 1; + p.post = repositionHeader; + p.wheelCarry = &(t->hwheelCarry); + return p; +} + +static void hscrollto(struct table *t, intptr_t pos) +{ + struct scrollParams p; + + p = hscrollParams(t); + scrollto(t, SB_HORZ, &p, pos); +} + +static void hscrollby(struct table *t, intptr_t delta) +{ + struct scrollParams p; + + p = hscrollParams(t); + scrollby(t, SB_HORZ, &p, delta); +} + +static void hscroll(struct table *t, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + p = hscrollParams(t); + scroll(t, SB_HORZ, &p, wParam, lParam); +} + +// TODO find out if we can indicriminately check for WM_WHEELHSCROLL +HANDLER(hscrollHandler) +{ + if (uMsg != WM_HSCROLL) + return FALSE; + hscroll(t, wParam, lParam); + *lResult = 0; + return TRUE; +} diff --git a/prev/wintable/includethis.h b/prev/wintable/includethis.h new file mode 100644 index 0000000..422b101 --- /dev/null +++ b/prev/wintable/includethis.h @@ -0,0 +1,73 @@ +// 17 february 2015 + +#ifndef __GO_UI_WINTABLE_INCLUDETHIS_H__ +#define __GO_UI_WINTABLE_INCLUDETHIS_H__ + +#define tableWindowClass L"gouitable" + +// start at WM_USER + 20 just in case for whatever reason we ever get the various dialog manager messages (see also http://blogs.msdn.com/b/oldnewthing/archive/2003/10/21/55384.aspx) +// each of these returns nothing unless otherwise indicated +enum { + // wParam - one of the type constants + // lParam - column name as a Unicode string + tableAddColumn = WM_USER + 20, + // wParam - 0 + // lParam - pointer to intptr_t containing new count + tableSetRowCount, + // wParam - pointer to intptr_t where selected row will be stored + // lParam - pointer to intptr_t where selected column will be stored + // both will be -1 for no selection + // if either is NULL, that value is not written + tableGetSelection, + // wParam - pointer to intptr_t containing selected row + // lParam - pointer to intptr_t containing selected column + // if lParam is NULL, do not change selected column (selects column 0 if nothing previously selected; TODO explicitly document this?) + // TODO allow wParam to be NULL too; should both being NULL select nothing or keep the current selection? + // this WILL result in a selection changed notification (TODO work into the package ui Table) + tableSetSelection, +}; + +enum { + tableColumnText, + tableColumnImage, + tableColumnCheckbox, + nTableColumnTypes, +}; + +// notification codes +// note that these are positive; see http://blogs.msdn.com/b/oldnewthing/archive/2009/08/21/9877791.aspx +// each of these is of type tableNM +// all fields except data will always be set +enum { + // data parameter is always 0 + // for tableColumnText return should be WCHAR * + // for tableColumnImage return should be HBITMAP + // for tableColumnCheckbox return is nonzero for checked, zero for unchecked + tableNotificationGetCellData, + // data parameter is pointer, same as tableNotificationGetCellData + // not sent for checkboxes + // no return + tableNotificationFinishedWithCellData, + // data is zero + // no return + tableNotificationCellCheckboxToggled, + // sent even on deselection (in that case, row == -1 and column == -1) + // data is zero + // no return + tableNotificationSelectionChanged, +}; + +typedef struct tableNM tableNM; + +struct tableNM { + NMHDR nmhdr; + intptr_t row; + intptr_t column; + int columnType; + uintptr_t data; +}; + +// TODO have hInstance passed in +extern void initTable(void (*panicfunc)(const char *msg, DWORD lastError), BOOL (*WINAPI tme)(LPTRACKMOUSEEVENT)); + +#endif diff --git a/prev/wintable/links b/prev/wintable/links new file mode 100644 index 0000000..f0c9523 --- /dev/null +++ b/prev/wintable/links @@ -0,0 +1,3 @@ +default list view message processing + http://msdn.microsoft.com/en-us/library/windows/desktop/bb774734%28v=vs.85%29.aspx + includes information on handling the edit control and event messages diff --git a/prev/wintable/main.h b/prev/wintable/main.h new file mode 100644 index 0000000..fe49d77 --- /dev/null +++ b/prev/wintable/main.h @@ -0,0 +1,165 @@ +// 7 january 2015 + +// TODO remove +#include <stdio.h> + +// TODO +// - should tablePanic be CALLBACK or some other equivalent macro? and definitely export initTable somehow, but which alias macro to use? +// - make panic messages grammatically correct ("Table error: adding...") +// - make access to column widths consistent; see whether HDITEMW.cxy == (ITEMRECT.right - ITEMRECT.left) +// - make sure all uses of t->headerHeight are ADDED to RECT.top +// - WM_THEMECHANGED, etc. +// - see if vertical centering is really what we want or if we just want to offset by a few pixels or so +// - going right from column 0 to column 2 with the right arrow key deselects +// - make sure all error messages involving InvalidateRect() are consistent with regards to "redrawing" and "queueing for redraw" +// - collect all resize-related tasks in a single function (so things like adding columns will refresh everything, not just horizontal scrolls; also would fix initial coordinates) +// - checkbox columns don't clip to the column width +// - send standard notification codes +// - seriously figure out how we're going to update everything about the table in one place +// - redraw on focus change +// - draw themed WS_EX_CLIENTEDGE (and WS_EX_WINDOWEDGE?) +// - in-place tooltips for text columns +// - checkboxes: use capture instead of manual tracking logic? http://blogs.msdn.com/b/oldnewthing/archive/2015/02/25/10595729.aspx + +#include "includethis.h" + +static void (*tablePanic)(const char *, DWORD) = NULL; +#define panic(...) (*tablePanic)(__VA_ARGS__, GetLastError()) +#define abort $$$$ // prevent accidental use of abort() + +static BOOL (*WINAPI tableTrackMouseEvent)(LPTRACKMOUSEEVENT); + +// forward declaration +struct tableAcc; + +struct table { + HWND hwnd; + HWND header; + HFONT font; + intptr_t nColumns; + int *columnTypes; + intptr_t width; + intptr_t headerHeight; + intptr_t hscrollpos; // in logical units + intptr_t hpagesize; // in logical units + intptr_t count; + intptr_t vscrollpos; // in rows + intptr_t vpagesize; // in rows + int hwheelCarry; + int vwheelCarry; + intptr_t selectedRow; + intptr_t selectedColumn; + HTHEME theme; + int checkboxWidth; + int checkboxHeight; + BOOL checkboxMouseOverLast; + LPARAM checkboxMouseOverLastPoint; + BOOL checkboxMouseDown; + intptr_t checkboxMouseDownRow; + intptr_t checkboxMouseDownColumn; + struct tableAcc *firstAcc; +}; + +// forward declaration (TODO needed?) +static LRESULT notify(struct table *, UINT, intptr_t, intptr_t, uintptr_t); + +// necessary forward declarations +static void update(struct table *, BOOL); +static void updateAll(struct table *); + +#include "util.h" +#include "coord.h" +#include "scroll.h" +#include "hscroll.h" +#include "vscroll.h" +#include "select.h" +#include "checkboxes.h" +#include "events.h" +#include "header.h" +#include "children.h" +#include "resize.h" +#include "draw.h" +#include "api.h" +#include "accessibility.h" +#include "update.h" + +static const handlerfunc handlers[] = { + eventHandlers, + childrenHandlers, + resizeHandler, + drawHandlers, + apiHandlers, + hscrollHandler, + vscrollHandler, + accessibilityHandler, + NULL, +}; + +static LRESULT CALLBACK tableWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + struct table *t; + LRESULT lResult; + + t = (struct table *) GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (t == NULL) { + // we have to do things this way because creating the header control will fail mysteriously if we create it first thing + // (which is fine; we can get the parent hInstance this way too) + // we use WM_CREATE because we have to use WM_DESTROY to destroy the header; we can't do it in WM_NCDESTROY because Windows will have destroyed it for us by then, and let's match message pairs to be safe + if (uMsg == WM_CREATE) { + CREATESTRUCTW *cs = (CREATESTRUCTW *) lParam; + + t = (struct table *) tableAlloc(sizeof (struct table), "error allocating internal Table data structure"); + t->hwnd = hwnd; + makeHeader(t, cs->hInstance); + t->selectedRow = -1; + t->selectedColumn = -1; + loadCheckboxThemeData(t); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR) t); + } + // even if we did the above, fall through + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + if (uMsg == WM_DESTROY) { +printf("destroy\n"); + // TODO free appropriate (after figuring this part out) components of t + invalidateTableAccs(t); + freeCheckboxThemeData(t); + destroyHeader(t); + tableFree(t, "error allocating internal Table data structure"); + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + if (runHandlers(handlers, t, uMsg, wParam, lParam, &lResult)) + return lResult; + return DefWindowProcW(hwnd, uMsg, wParam, lParam); +} + +static void deftablePanic(const char *msg, DWORD lastError) +{ + fprintf(stderr, "Table error: %s (last error %I32u)\n", msg, lastError); + fprintf(stderr, "This is the default Table error handler function; programs that use Table should provide their own instead.\nThe program will now break into the debugger.\n"); + DebugBreak(); +} + +// TODO have hInstance passed in +void initTable(void (*panicfunc)(const char *msg, DWORD lastError), BOOL (*WINAPI tme)(LPTRACKMOUSEEVENT)) +{ + WNDCLASSW wc; + + tablePanic = panicfunc; + if (tablePanic == NULL) + tablePanic = deftablePanic; + if (tme == NULL) + // TODO errorless version + panic("must provide a TrackMouseEvent() to initTable()"); + tableTrackMouseEvent = tme; + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = tableWindowClass; + wc.lpfnWndProc = tableWndProc; + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.hIcon = LoadIconW(NULL, IDI_APPLICATION); + wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); // TODO correct? + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.hInstance = GetModuleHandle(NULL); + if (RegisterClassW(&wc) == 0) + panic("error registering Table window class"); +} diff --git a/prev/wintable/resize.h b/prev/wintable/resize.h new file mode 100644 index 0000000..7805bd9 --- /dev/null +++ b/prev/wintable/resize.h @@ -0,0 +1,21 @@ +// 7 december 2014 + +// TODO why doesn't this trigger on first show? +// TODO see if there's anything not metaphor related in the last bits of the scrollbar series +// TODO rename this to boot +// TODO merge with update.h? + +HANDLER(resizeHandler) +{ + WINDOWPOS *wp; + + if (uMsg != WM_WINDOWPOSCHANGED) + return FALSE; + wp = (WINDOWPOS *) lParam; + if ((wp->flags & SWP_NOSIZE) != 0) + return FALSE; + // TODO redraw everything? + update(t, TRUE); + *lResult = 0; + return TRUE; +} diff --git a/prev/wintable/scroll.h b/prev/wintable/scroll.h new file mode 100644 index 0000000..ba5a9f8 --- /dev/null +++ b/prev/wintable/scroll.h @@ -0,0 +1,137 @@ +// 9 december 2014 + +struct scrollParams { + intptr_t *pos; + intptr_t pagesize; + intptr_t length; + intptr_t scale; + void (*post)(struct table *); + int *wheelCarry; +}; + +static void scrollto(struct table *t, int which, struct scrollParams *p, intptr_t pos) +{ + RECT scrollArea; + SCROLLINFO si; + intptr_t xamount, yamount; + + if (pos < 0) + pos = 0; + if (pos > p->length - p->pagesize) + pos = p->length - p->pagesize; + // TODO this shouldn't have been necessary but alas + // TODO the logic is really intended for the whole y origin thing in the scrollbar series; fix that + if (pos < 0) + pos = 0; + + // we don't want to scroll the header + if (GetClientRect(t->hwnd, &scrollArea) == 0) + panic("error getting Table client rect for scrollto()"); + scrollArea.top += t->headerHeight; + + // negative because ScrollWindowEx() is "backwards" + xamount = -(pos - *(p->pos)) * p->scale; + yamount = 0; + if (which == SB_VERT) { + yamount = xamount; + xamount = 0; + } + + if (ScrollWindowEx(t->hwnd, xamount, yamount, + &scrollArea, &scrollArea, NULL, NULL, + SW_ERASE | SW_INVALIDATE) == ERROR) +;// TODO failure case ignored for now; see https://bugs.winehq.org/show_bug.cgi?id=37706 +// panic("error scrolling Table"); + // TODO call UpdateWindow()? + + *(p->pos) = pos; + + // now commit our new scrollbar setup... + ZeroMemory(&si, sizeof (SCROLLINFO)); + si.cbSize = sizeof (SCROLLINFO); + si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE; + si.nPage = p->pagesize; + si.nMin = 0; + si.nMax = p->length - 1; // endpoint inclusive + si.nPos = *(p->pos); + SetScrollInfo(t->hwnd, which, &si, TRUE); + + if (p->post != NULL) + (*(p->post))(t); + + // EVENT_OBJECT_CONTENTSCROLLED is Vista and up only + // TODO send state changes for all affected rows/cells? +} + +static void scrollby(struct table *t, int which, struct scrollParams *p, intptr_t delta) +{ + scrollto(t, which, p, *(p->pos) + delta); +} + +static void scroll(struct table *t, int which, struct scrollParams *p, WPARAM wParam, LPARAM lParam) +{ + intptr_t pos; + SCROLLINFO si; + + pos = *(p->pos); + switch (LOWORD(wParam)) { + case SB_LEFT: // also SB_TOP + pos = 0; + break; + case SB_RIGHT: // also SB_BOTTOM + pos = p->length - p->pagesize; + break; + case SB_LINELEFT: // also SB_LINEUP + pos--; + break; + case SB_LINERIGHT: // also SB_LINEDOWN + pos++; + break; + case SB_PAGELEFT: // also SB_PAGEUP + pos -= p->pagesize; + break; + case SB_PAGERIGHT: // also SB_PAGEDOWN + pos += p->pagesize; + break; + case SB_THUMBPOSITION: + ZeroMemory(&si, sizeof (SCROLLINFO)); + si.cbSize = sizeof (SCROLLINFO); + si.fMask = SIF_POS; + if (GetScrollInfo(t->hwnd, which, &si) == 0) + panic("error getting thumb position for scroll() in Table"); + pos = si.nPos; + break; + case SB_THUMBTRACK: + ZeroMemory(&si, sizeof (SCROLLINFO)); + si.cbSize = sizeof (SCROLLINFO); + si.fMask = SIF_TRACKPOS; + if (GetScrollInfo(t->hwnd, which, &si) == 0) + panic("error getting thumb track position for scroll() in Table"); + pos = si.nTrackPos; + break; + } + scrollto(t, which, p, pos); +} + +static void wheelscroll(struct table *t, int which, struct scrollParams *p, WPARAM wParam, LPARAM lParam) +{ + int delta; + int lines; + UINT scrollAmount; + + delta = GET_WHEEL_DELTA_WPARAM(wParam); + // TODO make a note of what the appropriate hscroll constant is + if (SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &scrollAmount, 0) == 0) + // TODO use scrollAmount == 3 instead? + panic("error getting wheel scroll amount in wheelscroll()"); + if (scrollAmount == WHEEL_PAGESCROLL) + scrollAmount = p->pagesize; + if (scrollAmount == 0) // no mouse wheel scrolling (or t->pagesize == 0) + return; + // the rest of this is basically http://blogs.msdn.com/b/oldnewthing/archive/2003/08/07/54615.aspx and http://blogs.msdn.com/b/oldnewthing/archive/2003/08/11/54624.aspx + // see those pages for information on subtleties + delta += *(p->wheelCarry); + lines = delta * ((int) scrollAmount) / WHEEL_DELTA; + *(p->wheelCarry) = delta - lines * WHEEL_DELTA / ((int) scrollAmount); + scrollby(t, which, p, -lines); +} diff --git a/prev/wintable/scrollbarseries b/prev/wintable/scrollbarseries new file mode 100644 index 0000000..818b6a8 --- /dev/null +++ b/prev/wintable/scrollbarseries @@ -0,0 +1,24 @@ + + http://blogs.msdn.com/b/oldnewthing/archive/2003/07/23/54576.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/07/25/54582.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/07/29/54591.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/07/30/54600.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/07/31/54601.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/05/54602.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/05/54610.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/07/54615.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/07/54617.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/11/54624.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/11/54629.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/13/54639.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/15/54647.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/08/18/54668.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/09/54826.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/11/54885.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/13/54917.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/15/54925.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/17/54944.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/17/54945.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/09/17/54946.aspx + http://blogs.msdn.com/b/oldnewthing/archive/2003/10/16/55344.aspx + not really part of, so to speak, but still http://blogs.msdn.com/b/oldnewthing/archive/2004/05/10/129068.aspx diff --git a/prev/wintable/select.h b/prev/wintable/select.h new file mode 100644 index 0000000..6ece240 --- /dev/null +++ b/prev/wintable/select.h @@ -0,0 +1,269 @@ +// 13 december 2014 + +// damn winsock +static void doselect(struct table *t, intptr_t row, intptr_t column) +{ + RECT r, client; + intptr_t oldrow; + LONG width, height; + struct rowcol rc; + BOOL dovscroll; + intptr_t i; + intptr_t xpos; + LONG clientWidth; + + // check existing selection to see if it's valid + if (t->selectedRow == -1 && t->selectedColumn != -1) + panic("sanity check failure: old Table selection invalid (row == -1, column != -1)"); + if (t->selectedRow != -1 && t->selectedColumn == -1) + panic("sanity check failure: old Table selection invalid (row != -1, column == -1)"); + if (t->selectedRow >= t->count) + panic("sanity check failure: old Table selection invalid (row out of range)"); + if (t->selectedColumn >= t->nColumns) + panic("sanity check failure: old Table selection invalid (column out of range)"); + + oldrow = t->selectedRow; + t->selectedRow = row; + t->selectedColumn = column; + + // check new selection to see if it's valid + if (t->selectedRow == -1 && t->selectedColumn != -1) + panic("sanity check failure: new Table selection invalid (row == -1, column != -1)"); + if (t->selectedRow != -1 && t->selectedColumn == -1) + panic("sanity check failure: new Table selection invalid (row != -1, column == -1)"); + if (t->selectedRow >= t->count) + panic("sanity check failure: new Table selection invalid (row out of range)"); + if (t->selectedColumn >= t->nColumns) + panic("sanity check failure: new Table selection invalid (column out of range)"); + + // do this even if we don't scroll before; noScroll depends on it + if (GetClientRect(t->hwnd, &client) == 0) + panic("error getting Table client rect in doselect()"); + client.top += t->headerHeight; + height = rowht(t); + + // only scroll if we selected something + if (t->selectedRow == -1 || t->selectedColumn == -1) + goto noScroll; + + // first vertically scroll to the new row to make it fully visible (or as visible as possible) + if (t->selectedRow < t->vscrollpos) + vscrollto(t, t->selectedRow); + else { + rc.row = t->selectedRow; + rc.column = t->selectedColumn; + // first assume entirely outside the client area + dovscroll = TRUE; + if (rowColumnToClientRect(t, rc, &r)) + // partially outside the client area? + if (r.bottom <= client.bottom) // <= here since we are comparing bottoms (which are the first pixels outside the rectangle) + dovscroll = FALSE; + if (dovscroll) + vscrollto(t, t->selectedRow - t->vpagesize + 1); // + 1 because apparently just t->selectedRow - t->vpagesize results in no scrolling (t->selectedRow - t->vpagesize == t->vscrollpos)... + } + + // now see if the cell we want is to the left of offscreen, in which case scroll to its x-position + xpos = 0; + for (i = 0; i < t->selectedColumn; i++) + xpos += columnWidth(t, i); + if (xpos < t->hscrollpos) + hscrollto(t, xpos); + else { + // if the full cell is not visible, scroll to the right just enough to make it fully visible (or as visible as possible) + width = columnWidth(t, t->selectedColumn); + clientWidth = client.right - client.left; + if (xpos + width > t->hscrollpos + clientWidth) // > because both sides deal with the first pixel outside + // if the column is too wide, then just make it occupy the whole visible area (left-aligned) + if (width > clientWidth) // TODO >= ? + hscrollto(t, xpos); + else + // TODO don't use t->hpagesize here? depends if other code uses it + hscrollto(t, (xpos + width) - t->hpagesize); + } + +noScroll: + // now redraw the old and new /rows/ + // we do this after scrolling so the rectangles to be invalidated make sense + r.left = client.left; + r.right = client.right; + if (oldrow != -1 && oldrow >= t->vscrollpos) { + r.top = client.top + ((oldrow - t->vscrollpos) * height); + r.bottom = r.top + height; + if (InvalidateRect(t->hwnd, &r, TRUE) == 0) + panic("error queueing previously selected row for redraw in doselect()"); + } + // t->selectedRow must be visible by this point; we scrolled to it + if (t->selectedRow != -1 && t->selectedRow != oldrow) { + r.top = client.top + ((t->selectedRow - t->vscrollpos) * height); + r.bottom = r.top + height; + if (InvalidateRect(t->hwnd, &r, TRUE) == 0) + panic("error queueing newly selected row for redraw in doselect()"); + } + + // TODO what about deselect/defocus? + // TODO notify on the old row too? + NotifyWinEvent(EVENT_OBJECT_SELECTION, t->hwnd, OBJID_CLIENT, t->selectedRow); + // TODO send EVENT_OBJECT_STATECHANGED too? + // TODO send EVENT_OBJECT_FOCUS + + // TODO before or after NotifyWinEvent()? (see what other things I'm doing) + notify(t, tableNotificationSelectionChanged, t->selectedRow, t->selectedColumn, 0); +} + +// TODO make this needless +HANDLER(checkboxMouseDownHandler); + +// TODO which WM_xBUTTONDOWNs? +HANDLER(mouseDownSelectHandler) +{ + struct rowcol rc; + + // TODO separate this from here + // TODO other mouse buttons? + // don't check SetFocus()'s error (http://stackoverflow.com/questions/24073695/winapi-can-setfocus-return-null-without-an-error-because-thats-what-im-see) + // TODO NotifyWinEvent() here? + SetFocus(t->hwnd); + rc = lParamToRowColumn(t, lParam); + // don't check if lParamToRowColumn() returned row -1 or column -1; we want deselection behavior + doselect(t, rc.row, rc.column); + // TODO separate this from here + checkboxMouseDownHandler(t, uMsg, wParam, lParam, lResult); + *lResult = 0; + return TRUE; +} + +/* +the routine below is intended to simulate the comctl32.dll listview keyboard navigation rules, at least as far as vertical navigation is concerned. +horizontal scrolling is different because unlike the comctl32 listview, we say that a single column in each row has the keyboard focus, so left and right navigate between columns here, instead of scrolling left/right by pixels. + TODO provide an override for scrolling by pixels? + TODO any other keyboard shortcuts? + TODO browser keys + TODO media navigation keys + TODO XBUTTON1/2? + TODO clear keys? + +keyboard selection behaviors of the windows 7 listview: +with 100 items (0-99), the window currently shows items 30 through 47 as well as having item 48 partially visible +- item 30: + - page up -> item 13 + - page down -> item 47 +- item 31: + - page up -> item 30 + - page down -> item 47 +- item 42: + - page up -> item 30 + - page down -> item 47 +- item 46: + - page up -> item 30 + - page down -> item 47 +- item 47: + - page up: -> item 30 + - page down: -> item 64 + +when nothing is selected: +- down selects item 0 regardless of scroll +- up selects nothing regardless of scroll +- page down selects the last fully visible item depending on scroll + - so with the above configuration: + - item 0 -> item 17 + - item 30 -> item 47 + - item 80 -> item 97 +- page up selects item 0 regardless of scroll +- home selects item 0 regardless of scroll +- end selects the last item regardless of scroll + +for left and right we will simulate up and down, respectively (so right selects row 0 column 0); remember that you can't have either row or column be -1 but not both + +TODO what happens if page up and page down are pressed with an item selected and the scroll in a different position? +*/ + +HANDLER(keyDownSelectHandler) +{ + intptr_t row; + intptr_t column; + + if (t->count == 0 || t->nColumns == 0) // no items to select + return FALSE; + row = t->selectedRow; + column = t->selectedColumn; + switch (wParam) { + case VK_UP: + if (row == -1) + return FALSE; + row--; + if (row < 0) + row = 0; + break; + case VK_DOWN: + if (row == -1) { + row = 0; + column = 0; + } else { + row++; + if (row >= t->count) + row = t->count - 1; + } + break; + case VK_LEFT: + if (column == -1) + return FALSE; + column--; + if (column < 0) + column = 0; + break; + case VK_RIGHT: + if (column == -1) { + row = 0; + column = 0; + } else { + column++; + if (column >= t->nColumns) + column = t->nColumns - 1; + } + break; + case VK_HOME: + row = 0; + if (column == -1) + column = 0; + break; + case VK_END: + row = t->count - 1; + if (column == -1) + column = 0; + break; + case VK_PRIOR: + if (row == -1) { + row = 0; + column = 0; + } else { + row = t->vscrollpos; + if (row == t->selectedRow) + // TODO investigate why the - 1 is needed here and below + // TODO if this is a misunderstanding of how t->vpagesize works, figure out what happens if there is no partially visible row, and what is supposed to happen + row -= t->vpagesize - 1; + if (row < 0) + row = 0; + } + break; + case VK_NEXT: + if (row == -1) { + row = t->vscrollpos + t->vpagesize - 1; + // TODO ensusre this is the case with the real list view + if (row >= t->count) + row = t->count - 1; + column = 0; + } else { + row = t->vscrollpos + t->vpagesize - 1; + if (row == t->selectedRow) + row += t->vpagesize - 1; + if (row >= t->count) + row = t->count - 1; + } + break; + default: + return FALSE; + } + doselect(t, row, column); + *lResult = 0; + return TRUE; +} diff --git a/prev/wintable/test.c b/prev/wintable/test.c new file mode 100644 index 0000000..942860c --- /dev/null +++ b/prev/wintable/test.c @@ -0,0 +1,198 @@ +// 19 october 2014 +#include "../wininclude_windows.h" + +// #qo LIBS: user32 kernel32 gdi32 comctl32 uxtheme ole32 oleaut32 oleacc uuid msimg32 + +#include "main.h" + +HWND tablehwnd = NULL; +BOOL msgfont = FALSE; + +HBITMAP mkbitmap(void); + +BOOL mainwinCreate(HWND hwnd, LPCREATESTRUCT lpcs) +{ + intptr_t c; + intptr_t row, col; + + tablehwnd = CreateWindowExW(0, + tableWindowClass, L"Main Window", + WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL, + CW_USEDEFAULT, CW_USEDEFAULT, + 400, 400, + hwnd, NULL, lpcs->hInstance, NULL); + if (tablehwnd == NULL) + panic("(test program) error creating Table"); + SendMessageW(tablehwnd, tableAddColumn, tableColumnText, (LPARAM) L"Column"); + SendMessageW(tablehwnd, tableAddColumn, tableColumnImage, (LPARAM) L"Column 2"); + SendMessageW(tablehwnd, tableAddColumn, tableColumnCheckbox, (LPARAM) L"Column 3"); + if (msgfont) { + NONCLIENTMETRICSW ncm; + HFONT font; + + ZeroMemory(&ncm, sizeof (NONCLIENTMETRICSW)); + ncm.cbSize = sizeof (NONCLIENTMETRICSW); + if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof (NONCLIENTMETRICSW), &ncm, sizeof (NONCLIENTMETRICSW)) == 0) + panic("(test program) error getting non-client metrics"); + font = CreateFontIndirectW(&ncm.lfMessageFont); + if (font == NULL) + panic("(test program) error creating lfMessageFont HFONT"); + SendMessageW(tablehwnd, WM_SETFONT, (WPARAM) font, TRUE); + } + c = 100; + SendMessageW(tablehwnd, tableSetRowCount, 0, (LPARAM) (&c)); + row = 2; + col = 1; + SendMessageW(tablehwnd, tableSetSelection, (WPARAM) (&row), (LPARAM) (&col)); + SetFocus(tablehwnd); + return TRUE; +} + +void mainwinDestroy(HWND hwnd) +{ + DestroyWindow(tablehwnd); + PostQuitMessage(0); +} + +void mainwinResize(HWND hwnd, UINT state, int cx, int cy) +{ + if (tablehwnd != NULL) + MoveWindow(tablehwnd, 0, 0, cx, cy, TRUE); +} + +BOOL checkboxstates[100]; + +LRESULT CALLBACK mainwndproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + NMHDR *nmhdr = (NMHDR *) lParam; + tableNM *nm = (tableNM *) lParam; + WCHAR *text; + int n; + + if (uMsg == WM_CREATE) + ZeroMemory(checkboxstates, 100 * sizeof (BOOL)); + switch (uMsg) { + HANDLE_MSG(hwnd, WM_CREATE, mainwinCreate); + HANDLE_MSG(hwnd, WM_SIZE, mainwinResize); + HANDLE_MSG(hwnd, WM_DESTROY, mainwinDestroy); + case WM_NOTIFY: + if (nmhdr->hwndFrom != tablehwnd) + break; + switch (nmhdr->code) { + case tableNotificationGetCellData: + switch (nm->columnType) { + case tableColumnText: + n = _scwprintf(L"mainwin (%d,%d)", nm->row, nm->column); + text = (WCHAR *) malloc((n + 1) * sizeof (WCHAR)); + if (text == NULL) + panic("(table program) error allocating string"); + _swprintf(text, L"mainwin (%d,%d)", nm->row, nm->column); + return (LRESULT) text; + case tableColumnImage: + return (LRESULT) mkbitmap(); + case tableColumnCheckbox: + return (LRESULT) (checkboxstates[nm->row]); + } + panic("(test program) unreachable"); + case tableNotificationFinishedWithCellData: + switch (nm->columnType) { + case tableColumnText: + free((void *) (nm->data)); + break; + case tableColumnImage: + if (DeleteObject((HBITMAP) (nm->data)) == 0) + panic("(test program) error deleting cell image"); + break; + } + return 0; + case tableNotificationCellCheckboxToggled: + checkboxstates[nm->row] = !checkboxstates[nm->row]; + return 0; + } + break; + } + return DefWindowProcW(hwnd, uMsg, wParam, lParam); +} + +int main(int argc, char *argv[]) +{ + HWND mainwin; + MSG msg; + INITCOMMONCONTROLSEX icc; + WNDCLASSW wc; + + if (argc != 1) + msgfont = TRUE; + ZeroMemory(&icc, sizeof (INITCOMMONCONTROLSEX)); + icc.dwSize = sizeof (INITCOMMONCONTROLSEX); + icc.dwICC = ICC_LISTVIEW_CLASSES; + if (InitCommonControlsEx(&icc) == 0) + panic("(test program) error initializing comctl32.dll"); + initTable(NULL, _TrackMouseEvent); + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = L"mainwin"; + wc.lpfnWndProc = mainwndproc; + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); + wc.hInstance = GetModuleHandle(NULL); + if (RegisterClassW(&wc) == 0) + panic("(test program) error registering main window class"); + mainwin = CreateWindowExW(0, + L"mainwin", L"Main Window", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, + 400, 400, + NULL, NULL, GetModuleHandle(NULL), NULL); + if (mainwin == NULL) + panic("(test program) error creating main window"); + ShowWindow(mainwin, SW_SHOWDEFAULT); + if (UpdateWindow(mainwin) == 0) + panic("(test program) error updating window"); + while (GetMessageW(&msg, NULL, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + return 0; +} + +// from tango-icon-theme-0.8.90/16x16/status/audio-volume-high.png (public domain) +COLORREF iconpix[] = { + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1003060A, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x34101210, 0x5020202, 0x0, 0x0, 0x0, 0x0, 0x9E203E66, 0x78182F4D, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x32111110, 0xF051534F, 0x8030303, 0x0, 0x0, 0x0, 0x0, 0x1C050B12, 0xE92F5C96, 0x2B08111B, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x32111110, 0xF1575855, 0xFE545652, 0x8030303, 0x0, 0x0, 0x67152942, 0x440E1B2C, 0x0, 0x6214263F, 0xCB295083, 0x4000102, + 0xF3525450, 0xFB545653, 0xF0555653, 0x8830302E, 0xF1575855, 0xFFBEBFBC, 0xFC565854, 0x8030303, 0x0, 0x0, 0x420C192A, 0xE12E5991, 0xF03060A, 0x0, 0xC6284E7F, 0x52102034, + 0xFB555653, 0xFFE5E5E3, 0xFFD1D2CF, 0xFE585A56, 0xFFC7C8C5, 0xFFF7F7F6, 0xFC5A5C58, 0x8030303, 0x68142943, 0x1C060B12, 0x0, 0x9A1E3D63, 0x81193353, 0x0, 0x831A3454, 0x891C3658, + 0xFB545653, 0xFFFAFAF9, 0xFFFAFAFA, 0xFF585A56, 0xFFF8F8F8, 0xFFFFFFFF, 0xFC5B5D59, 0x8030303, 0x7B19314F, 0xAB23436E, 0x0, 0x4A0F1D30, 0xBA264978, 0x0, 0x4F101F33, 0xBD264B7A, + 0xFB545653, 0xFFCFD0CD, 0xFFD3D4D1, 0xFF585A56, 0xFFD2D2D0, 0xFFD7D7D5, 0xFC565854, 0x8030303, 0x1C060B12, 0xEF305F9A, 0x1000001, 0x19050A10, 0xEF305F9A, 0x1000001, 0x1B050A11, 0xF0315F9A, + 0xFB545653, 0xFFC2C3C0, 0xFFC6C7C4, 0xFF585A56, 0xFFCDCECB, 0xFFD1D2CF, 0xFC565854, 0x8030303, 0x19050A10, 0xF231609C, 0x1000001, 0x1603090E, 0xF1315F9B, 0x1000001, 0x1804090F, 0xF231609C, + 0xFB545653, 0xFFB5B7B3, 0xFFB9BBB7, 0xFF575955, 0xFFC8C8C6, 0xFFCCCCCA, 0xFC565854, 0x8030303, 0x71172D49, 0xB2244772, 0x0, 0x470D1C2E, 0xBE264B7A, 0x0, 0x4C0F1E31, 0xC0274C7B, + 0xFB545653, 0xFFA9ABA7, 0xFFA0A29E, 0xFF575955, 0xFFA9ABA8, 0xFFC6C7C4, 0xFC565854, 0x8030303, 0x75172E4B, 0x23070E17, 0x0, 0x921D3A5E, 0x861A3556, 0x0, 0x801A3252, 0x8C1C375A, + 0xFA535551, 0xFC555753, 0xF7545652, 0x98353534, 0xF3565854, 0xFFA8A8A6, 0xFC565854, 0x8030303, 0x0, 0x0, 0x390C1725, 0xE62F5B94, 0x1404080D, 0x0, 0xC0274C7B, 0x57112238, + 0x7020202, 0x8020202, 0x3010100, 0x0, 0x3C141414, 0xF3565854, 0xFE545652, 0x8030303, 0x0, 0x0, 0x70162C48, 0x4E0F1F32, 0x0, 0x57112238, 0xD32B5388, 0x6010203, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x3C141414, 0xF5515350, 0x8030303, 0x0, 0x0, 0x0, 0x0, 0x1604080E, 0xE72F5B95, 0x330A1420, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3F151515, 0x5020202, 0x0, 0x0, 0x0, 0x0, 0x991F3C62, 0x831A3454, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1B050A11, 0x4000102, 0x0, 0x0, +}; + +HBITMAP mkbitmap(void) +{ + BITMAPINFO bi; + void *ppvBits; + HBITMAP b; + + ZeroMemory(&bi, sizeof (BITMAPINFO)); + bi.bmiHeader.biSize = sizeof (BITMAPINFOHEADER); + bi.bmiHeader.biWidth = 16; + bi.bmiHeader.biHeight = -16; // negative height to force top-down drawing + bi.bmiHeader.biPlanes = 1; + bi.bmiHeader.biBitCount = 32; + bi.bmiHeader.biCompression = BI_RGB; + bi.bmiHeader.biSizeImage = 16 * 16 * 4; + b = CreateDIBSection(NULL, &bi, DIB_RGB_COLORS, &ppvBits, 0, 0); + if (b == 0) + panic("test bitmap creation failed"); + memcpy(ppvBits, iconpix, bi.bmiHeader.biSizeImage); + return b; +} diff --git a/prev/wintable/update.h b/prev/wintable/update.h new file mode 100644 index 0000000..dae4811 --- /dev/null +++ b/prev/wintable/update.h @@ -0,0 +1,53 @@ +// 8 january 2015 + +// Whenever a number of things in the Table changes, the update() function needs to be called to update any metrics and scrolling positions. +// The control font changing is the big one, as that comes with a flag that decides whether or not to redraw everything. We'll need to respect that here. + +// For my personal convenience, each invocation of update() and updateAll() will be suffixed with a DONE comment once I have reasoned that the chosen function is correct and that no additional redrawing is necessary. + +// TODO actually use redraw here +static void update(struct table *t, BOOL redraw) +{ + RECT client; + intptr_t i; + intptr_t height; + + // before we do anything we need the client rect + if (GetClientRect(t->hwnd, &client) == 0) + panic("error getting Table client rect in update()"); + + // the first step is to figure out how wide the whole table is + // TODO count dividers? + t->width = 0; + for (i = 0; i < t->nColumns; i++) + t->width += columnWidth(t, i); + + // now we need to figure out how much of the width of the table can be seen at once + t->hpagesize = client.right - client.left; + // this part is critical: if we resize the columns to less than the client area width, then the following hscrollby() will make t->hscrollpos negative, which does very bad things + // we do this regardless of which of the two has changed, just to be safe + if (t->hpagesize > t->width) + t->hpagesize = t->width; + + // now we do a dummy horizontal scroll to apply the new width and horizontal page size + // this will also reposition and resize the header (the latter in case the font changed), which will be important for the next step + hscrollby(t, 0); + + // now that we have the new height of the header, we can fix up vertical scrolling + // so let's take the header height away from the client area + client.top += t->headerHeight; + // and update our page size appropriately + height = client.bottom - client.top; + t->vpagesize = height / rowht(t); + // and do a dummy vertical scroll to apply that + vscrollby(t, 0); +} + +// this is the same as update(), but also redraws /everything/ +// as such, redraw is TRUE +static void updateAll(struct table *t) +{ + update(t, TRUE); + if (InvalidateRect(t->hwnd, NULL, TRUE) == 0) + panic("error queueing all of Table for redraw in updateAll()"); +} diff --git a/prev/wintable/util.h b/prev/wintable/util.h new file mode 100644 index 0000000..164ca72 --- /dev/null +++ b/prev/wintable/util.h @@ -0,0 +1,119 @@ +// 4 december 2014 + +typedef BOOL (*handlerfunc)(struct table *, UINT, WPARAM, LPARAM, LRESULT *); +#define HANDLER(name) static BOOL name(struct table *t, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult) + +static BOOL runHandlers(const handlerfunc list[], struct table *t, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + const handlerfunc *p; + + for (p = list; *p != NULL; p++) + if ((*(*p))(t, uMsg, wParam, lParam, lResult)) + return TRUE; + return FALSE; +} + +// memory allocation stuff +// each of these functions do an implicit ZeroMemory() +// these also make tableRealloc(NULL, ...)/tableFree(NULL) act like realloc(NULL, ...)/free(NULL) (that is, same as tableAlloc(...)/malloc(...) and a no-op, respectively) +// we /would/ use LocalAlloc() here because +// - whether the malloc() family supports the last-error code is undefined +// - the HeapAlloc() family DOES NOT support the last-error code; you're supposed to use Windows exceptions, and I can't find a clean way to do this with MinGW-w64 that doesn't rely on inline assembly or external libraries (unless they added __try/__except blocks) +// - there's no VirtualReAlloc() to complement VirtualAlloc() and I'm not sure if we can even get the original allocated size back out reliably to write it ourselves (http://blogs.msdn.com/b/oldnewthing/archive/2012/03/16/10283988.aspx) +// alas, LocalAlloc() doesn't want to work on real Windows 7 after a few times, throwing up ERROR_NOT_ENOUGH_MEMORY after three (3) ints or so :| +// we'll use malloc() until then +// needless to say, TODO + +static void *tableAlloc(size_t size, const char *panicMessage) +{ +// HLOCAL out; + void *out; + +// out = LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, size); + out = malloc(size); + if (out == NULL) + panic(panicMessage); + ZeroMemory(out, size); + return (void *) out; +} + +static void *tableRealloc(void *p, size_t size, const char *panicMessage) +{ +// HLOCAL out; + void *out; + + if (p == NULL) + return tableAlloc(size, panicMessage); +// out = LocalReAlloc((HLOCAL) p, size, LMEM_ZEROINIT); + out = realloc(p, size); + if (out == NULL) + panic(panicMessage); + // TODO zero the extra memory + return (void *) out; +} + +static void tableFree(void *p, const char *panicMessage) +{ + if (p == NULL) + return; +// if (LocalFree((HLOCAL) p) != NULL) +// panic(panicMessage); + free(p); +} + +// font selection + +static HFONT selectFont(struct table *t, HDC dc, HFONT *newfont) +{ + HFONT prevfont; + + // copy it in casse we get a WM_SETFONT before this call's respective deselectFont() call + *newfont = t->font; + if (*newfont == NULL) { + // get it on demand in the (unlikely) event it changes while this Table is alive + *newfont = GetStockObject(SYSTEM_FONT); + if (*newfont == NULL) + panic("error getting default font for selecting into Table DC"); + } + prevfont = (HFONT) SelectObject(dc, *newfont); + if (prevfont == NULL) + panic("error selecting Table font into Table DC"); + return prevfont; +} + +static void deselectFont(HDC dc, HFONT prevfont, HFONT newfont) +{ + if (SelectObject(dc, prevfont) != newfont) + panic("error deselecting Table font from Table DC"); + // doin't delete newfont here, even if it is the system font (see http://msdn.microsoft.com/en-us/library/windows/desktop/dd144925%28v=vs.85%29.aspx) +} + +// and back to other functions + +static LONG columnWidth(struct table *t, intptr_t n) +{ + RECT r; + + if (SendMessageW(t->header, HDM_GETITEMRECT, (WPARAM) n, (LPARAM) (&r)) == 0) + panic("error getting Table column width"); + return r.right - r.left; +} + +/* TODO: +http://blogs.msdn.com/b/oldnewthing/archive/2003/10/13/55279.aspx +http://blogs.msdn.com/b/oldnewthing/archive/2003/10/14/55286.aspx +we'll need to make sure that initial edge case works properly +(TODO get the linked article in the latter) +also implement retrack() as so, in the WM_MOUSEMOVE handler +*/ +static void retrack(struct table *t) +{ + TRACKMOUSEEVENT tm; + + ZeroMemory(&tm, sizeof (TRACKMOUSEEVENT)); + tm.cbSize = sizeof (TRACKMOUSEEVENT); + tm.dwFlags = TME_LEAVE; // TODO TME_NONCLIENT as well? + tm.hwndTrack = t->hwnd; + if ((*tableTrackMouseEvent)(&tm) == 0) + panic("error retracking Table mouse events"); +} diff --git a/prev/wintable/vscroll.h b/prev/wintable/vscroll.h new file mode 100644 index 0000000..ca34957 --- /dev/null +++ b/prev/wintable/vscroll.h @@ -0,0 +1,65 @@ +// 9 december 2014 + +// forward declaration needed here +static void repositionHeader(struct table *); + +static struct scrollParams vscrollParams(struct table *t) +{ + struct scrollParams p; + + ZeroMemory(&p, sizeof (struct scrollParams)); + p.pos = &(t->vscrollpos); + p.pagesize = t->vpagesize; + p.length = t->count; + p.scale = rowht(t); + p.post = NULL; + p.wheelCarry = &(t->vwheelCarry); + return p; +} + +static void vscrollto(struct table *t, intptr_t pos) +{ + struct scrollParams p; + + p = vscrollParams(t); + scrollto(t, SB_VERT, &p, pos); +} + +static void vscrollby(struct table *t, intptr_t delta) +{ + struct scrollParams p; + + p = vscrollParams(t); + scrollby(t, SB_VERT, &p, delta); +} + +static void vscroll(struct table *t, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + p = vscrollParams(t); + scroll(t, SB_VERT, &p, wParam, lParam); +} + +static void vwheelscroll(struct table *t, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + p = vscrollParams(t); + wheelscroll(t, SB_VERT, &p, wParam, lParam); +} + +HANDLER(vscrollHandler) +{ + switch (uMsg) { + case WM_VSCROLL: + vscroll(t, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_MOUSEWHEEL: + vwheelscroll(t, wParam, lParam); + *lResult = 0; + return TRUE; + } + return FALSE; +} |
