]> cygwin.com Git - cygwin-apps/setup.git/blob - ListView.cc
Use selected row background colour for a checkbox column
[cygwin-apps/setup.git] / ListView.cc
1 /*
2 * Copyright (c) 2016 Jon Turney
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * A copy of the GNU General Public License can be found at
10 * http://www.gnu.org/
11 *
12 */
13
14 #include "ListView.h"
15 #include "LogSingleton.h"
16 #include "resource.h"
17
18 #include <commctrl.h>
19
20 // ---------------------------------------------------------------------------
21 // implements class ListView
22 //
23 // ListView Common Control
24 // ---------------------------------------------------------------------------
25
26 void
27 ListView::init(HWND parent, int id, HeaderList headers)
28 {
29 hWndParent = parent;
30
31 // locate the listview control
32 hWndListView = ::GetDlgItem(parent, id);
33
34 // configure the listview control
35 SendMessage(hWndListView, CCM_SETVERSION, 6, 0);
36
37 ListView_SetExtendedListViewStyle(hWndListView,
38 LVS_EX_COLUMNSNAPPOINTS | // use cxMin
39 LVS_EX_FULLROWSELECT |
40 LVS_EX_GRIDLINES |
41 LVS_EX_HEADERDRAGDROP); // headers can be re-ordered
42
43 // give the header control a border
44 HWND hWndHeader = ListView_GetHeader(hWndListView);
45 SetWindowLongPtr(hWndHeader, GWL_STYLE,
46 GetWindowLongPtr(hWndHeader, GWL_STYLE) | WS_BORDER);
47
48 // ensure an initial item exists for width calculations...
49 LVITEM lvi;
50 lvi.mask = LVIF_TEXT;
51 lvi.iItem = 0;
52 lvi.iSubItem = 0;
53 lvi.pszText = const_cast <char *> ("Working...");
54 ListView_InsertItem(hWndListView, &lvi);
55
56 // populate with columns
57 initColumns(headers);
58
59 // create a small icon imagelist
60 // (the order of images matches ListViewLine::State enum)
61 hImgList = ImageList_Create(GetSystemMetrics(SM_CXSMICON),
62 GetSystemMetrics(SM_CYSMICON),
63 ILC_COLOR32, 2, 0);
64 ImageList_AddIcon(hImgList, LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_TREE_PLUS)));
65 ImageList_AddIcon(hImgList, LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_TREE_MINUS)));
66
67 // create an empty imagelist, used to reset the indent
68 hEmptyImgList = ImageList_Create(1, 1,
69 ILC_COLOR32, 2, 0);
70
71 // LVS_EX_INFOTIP/LVN_GETINFOTIP doesn't work for subitems, so we have to do
72 // our own tooltip handling
73 hWndTip = CreateWindowEx (0,
74 (LPCTSTR) TOOLTIPS_CLASS,
75 NULL,
76 WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
77 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
78 hWndParent,
79 (HMENU) 0,
80 GetModuleHandle(NULL),
81 NULL);
82 // must be topmost so that tooltips will display on top
83 SetWindowPos(hWndTip, HWND_TOPMOST,0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
84
85 TOOLINFO ti;
86 memset ((void *)&ti, 0, sizeof(ti));
87 ti.cbSize = sizeof(ti);
88 ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
89 ti.hwnd = hWndParent;
90 ti.uId = (UINT_PTR)hWndListView;
91 ti.lpszText = LPSTR_TEXTCALLBACK; // use TTN_GETDISPINFO
92 SendMessage(hWndTip, TTM_ADDTOOL, 0, (LPARAM)&ti);
93
94 // match long delay for tooltip to disappear used elsewhere (30s)
95 SendMessage(hWndTip, TTM_SETDELAYTIME, TTDT_AUTOPOP, (LPARAM) MAKELONG (30000, 0));
96 // match tip width used elsewhere
97 SendMessage(hWndTip, TTM_SETMAXTIPWIDTH, 0, 450);
98 }
99
100 void
101 ListView::initColumns(HeaderList headers_)
102 {
103 // store HeaderList for later use
104 headers = headers_;
105
106 // create the columns
107 LVCOLUMN lvc;
108 lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
109
110 int i;
111 for (i = 0; headers[i].text != 0; i++)
112 {
113 lvc.iSubItem = i;
114 lvc.pszText = const_cast <char *> (headers[i].text);
115 lvc.cx = 100;
116 lvc.fmt = headers[i].fmt;
117
118 ListView_InsertColumn(hWndListView, i, &lvc);
119 }
120
121 // now do some width calculations
122 for (i = 0; headers[i].text != 0; i++)
123 {
124 headers[i].width = 0;
125
126 ListView_SetColumnWidth(hWndListView, i, LVSCW_AUTOSIZE_USEHEADER);
127 headers[i].hdr_width = ListView_GetColumnWidth(hWndListView, i);
128 }
129 }
130
131 void
132 ListView::noteColumnWidthStart()
133 {
134 dc = GetDC (hWndListView);
135
136 // we must set the font of the DC here, otherwise the width calculations
137 // will be off because the system will use the wrong font metrics
138 HANDLE sysfont = GetStockObject (DEFAULT_GUI_FONT);
139 SelectObject (dc, sysfont);
140
141 int i;
142 for (i = 0; headers[i].text != 0; i++)
143 {
144 headers[i].width = 0;
145 }
146 }
147
148 void
149 ListView::noteColumnWidth(int col_num, const std::string& string)
150 {
151 SIZE s = { 0, 0 };
152
153 // A margin of 3*GetSystemMetrics(SM_CXEDGE) is used at each side of the
154 // header text.
155 int addend = 2*3*GetSystemMetrics(SM_CXEDGE);
156
157 if (string.size())
158 GetTextExtentPoint32 (dc, string.c_str(), string.size(), &s);
159
160 int width = addend + s.cx;
161
162 // allow for width of dropdown button in popup columns
163 if (headers[col_num].type == ListView::ControlType::popup)
164 {
165 width += GetSystemMetrics(SM_CXVSCROLL);
166 }
167
168 if (width > headers[col_num].width)
169 headers[col_num].width = width;
170 }
171
172 void
173 ListView::noteColumnWidthEnd()
174 {
175 ReleaseDC(hWndListView, dc);
176 }
177
178 void
179 ListView::resizeColumns(void)
180 {
181 // ensure the last column stretches all the way to the right-hand side of the
182 // listview control
183 int i;
184 int total = 0;
185 for (i = 0; headers[i].text != 0; i++)
186 total = total + headers[i].width;
187
188 RECT r;
189 GetClientRect(hWndListView, &r);
190 int width = r.right - r.left;
191
192 if (total < width)
193 headers[i-1].width += width - total;
194
195 // size each column
196 LVCOLUMN lvc;
197 lvc.mask = LVCF_WIDTH | LVCF_MINWIDTH;
198 for (i = 0; headers[i].text != 0; i++)
199 {
200 lvc.iSubItem = i;
201 lvc.cx = (headers[i].width < headers[i].hdr_width) ? headers[i].hdr_width : headers[i].width;
202 lvc.cxMin = headers[i].hdr_width;
203 #if DEBUG
204 Log (LOG_BABBLE) << "resizeColumns: " << i << " cx " << lvc.cx << " cxMin " << lvc.cxMin <<endLog;
205 #endif
206
207 ListView_SetColumn(hWndListView, i, &lvc);
208 }
209 }
210
211 void
212 ListView::setContents(ListViewContents *_contents, bool tree)
213 {
214 contents = _contents;
215
216 // disable redrawing of ListView
217 // (otherwise it will redraw every time a row is added, which makes this very slow)
218 SendMessage(hWndListView, WM_SETREDRAW, FALSE, 0);
219
220 // preserve focus/selection
221 int iRow = ListView_GetSelectionMark(hWndListView);
222
223 empty();
224
225 // assign imagelist to listview control (this also sets the size for indents)
226 if (tree)
227 ListView_SetImageList(hWndListView, hImgList, LVSIL_SMALL);
228 else
229 ListView_SetImageList(hWndListView, hEmptyImgList, LVSIL_SMALL);
230
231 size_t i;
232 for (i = 0; i < contents->size(); i++)
233 {
234 LVITEM lvi;
235 lvi.mask = LVIF_TEXT | (tree ? LVIF_IMAGE | LVIF_INDENT : 0);
236 lvi.iItem = i;
237 lvi.iSubItem = 0;
238 lvi.pszText = LPSTR_TEXTCALLBACK;
239 if (tree)
240 {
241 lvi.iImage = I_IMAGECALLBACK;
242 lvi.iIndent = (*contents)[i]->get_indent();
243 }
244
245 ListView_InsertItem(hWndListView, &lvi);
246 }
247
248 if (iRow >= 0)
249 {
250 ListView_SetItemState(hWndListView, iRow, LVNI_SELECTED | LVNI_FOCUSED, LVNI_SELECTED | LVNI_FOCUSED);
251 ListView_EnsureVisible(hWndListView, iRow, false);
252 }
253
254 // enable redrawing of ListView and redraw
255 SendMessage(hWndListView, WM_SETREDRAW, TRUE, 0);
256 RedrawWindow(hWndListView, NULL, NULL, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);
257 }
258
259 // Helper class: The char * pointer we hand back needs to remain valid for some
260 // time after OnNotify returns, when the std::string we have retrieved has gone
261 // out of scope, so a static instance of this class maintains a local cache.
262 class StringCache
263 {
264 public:
265 StringCache() : cache(NULL), cache_size(0) { }
266 StringCache & operator = (const std::string & s)
267 {
268 if ((s.length() + 1) > cache_size)
269 {
270 cache_size = s.length() + 1;
271 cache = (char *)realloc(cache, cache_size);
272 }
273 strcpy(cache, s.c_str());
274 return *this;
275 }
276 operator char *() const
277 {
278 return cache;
279 }
280 private:
281 char *cache;
282 size_t cache_size;
283 };
284
285 bool
286 ListView::OnNotify (NMHDR *pNmHdr, LRESULT *pResult)
287 {
288 #if DEBUG
289 Log (LOG_BABBLE) << "ListView::OnNotify id:" << pNmHdr->idFrom << " hwnd:" << pNmHdr->hwndFrom << " code:" << (int)pNmHdr->code << endLog;
290 #endif
291
292 switch (pNmHdr->code)
293 {
294 case LVN_GETDISPINFO:
295 {
296 NMLVDISPINFO *pNmLvDispInfo = (NMLVDISPINFO *)pNmHdr;
297 #if DEBUG
298 Log (LOG_BABBLE) << "LVN_GETDISPINFO " << pNmLvDispInfo->item.iItem << endLog;
299 #endif
300 if (contents)
301 {
302 int iRow = pNmLvDispInfo->item.iItem;
303 int iCol = pNmLvDispInfo->item.iSubItem;
304
305 static StringCache s;
306 s = (*contents)[iRow]->get_text(iCol);
307 pNmLvDispInfo->item.pszText = s;
308
309 if (pNmLvDispInfo->item.iSubItem == 0)
310 {
311 pNmLvDispInfo->item.iImage = (int)((*contents)[pNmLvDispInfo->item.iItem]->get_state());
312 }
313 }
314
315 return true;
316 }
317 break;
318
319 case LVN_GETEMPTYMARKUP:
320 {
321 NMLVEMPTYMARKUP *pNmMarkup = (NMLVEMPTYMARKUP*) pNmHdr;
322
323 MultiByteToWideChar(CP_UTF8, 0,
324 empty_list_text, -1,
325 pNmMarkup->szMarkup, L_MAX_URL_LENGTH);
326
327 *pResult = true;
328 return true;
329 }
330 break;
331
332 case NM_CLICK:
333 {
334 NMITEMACTIVATE *pNmItemAct = (NMITEMACTIVATE *) pNmHdr;
335 #if DEBUG
336 Log (LOG_BABBLE) << "NM_CLICK: pnmitem->iItem " << pNmItemAct->iItem << " pNmItemAct->iSubItem " << pNmItemAct->iSubItem << endLog;
337 #endif
338 int iRow = pNmItemAct->iItem;
339 int iCol = pNmItemAct->iSubItem;
340 if (iRow < 0)
341 return false;
342
343 int update = 0;
344
345 if (headers[iCol].type == ListView::ControlType::popup)
346 {
347 POINT p;
348 // position pop-up menu at the location of the click
349 GetCursorPos(&p);
350
351 update = popup_menu(iRow, iCol, p);
352 }
353 else
354 {
355 // Inform the item of the click
356 update = (*contents)[iRow]->do_action(iCol, 0);
357 }
358
359 // Update items, if needed
360 if (update > 0)
361 {
362 ListView_RedrawItems(hWndListView, iRow, iRow + update -1);
363 }
364
365 return true;
366 }
367 break;
368
369 case NM_CUSTOMDRAW:
370 {
371 NMLVCUSTOMDRAW *pNmLvCustomDraw = (NMLVCUSTOMDRAW *)pNmHdr;
372
373 switch(pNmLvCustomDraw->nmcd.dwDrawStage)
374 {
375 case CDDS_PREPAINT:
376 *pResult = CDRF_NOTIFYITEMDRAW;
377 return true;
378 case CDDS_ITEMPREPAINT:
379 *pResult = CDRF_NOTIFYSUBITEMDRAW;
380 return true;
381 case CDDS_SUBITEM | CDDS_ITEMPREPAINT:
382 {
383 LRESULT result = CDRF_DODEFAULT;
384 int iCol = pNmLvCustomDraw->iSubItem;
385 int iRow = pNmLvCustomDraw->nmcd.dwItemSpec;
386
387 switch (headers[iCol].type)
388 {
389 default:
390 case ListView::ControlType::text:
391 result = CDRF_DODEFAULT;
392 break;
393
394 case ListView::ControlType::checkbox:
395 {
396 // get the subitem text
397 char buf[3];
398 ListView_GetItemText(hWndListView, iRow, iCol, buf, _countof(buf));
399
400 // map the subitem text to a checkbox state
401 UINT state = DFCS_BUTTONCHECK | DFCS_FLAT;
402 if (buf[0] == '\0') // empty
403 {
404 result = CDRF_DODEFAULT;
405 break;
406 }
407 else if (buf[0] == 'y') // yes
408 state |= DFCS_CHECKED;
409 else if ((buf[0] == 'n') && (buf[1] == 'o')) // no
410 state |= 0;
411 else // n/a
412 state |= DFCS_INACTIVE;
413
414 // erase and draw a checkbox
415 RECT r;
416 ListView_GetSubItemRect(hWndListView, iRow, iCol, LVIR_BOUNDS, &r);
417 DWORD bkg_color;
418 if (pNmLvCustomDraw->nmcd.uItemState & CDIS_SELECTED)
419 bkg_color = GetSysColor(COLOR_HIGHLIGHT);
420 else
421 bkg_color = ListView_GetBkColor(hWndListView);
422 HBRUSH hBrush = CreateSolidBrush(bkg_color);
423 FillRect(pNmLvCustomDraw->nmcd.hdc, &r, hBrush);
424 DeleteObject(hBrush);
425 DrawFrameControl(pNmLvCustomDraw->nmcd.hdc, &r, DFC_BUTTON, state);
426
427 result = CDRF_SKIPDEFAULT;
428 }
429 break;
430
431 case ListView::ControlType::popup:
432 {
433 // let the control draw the text, but notify us afterwards
434 result = CDRF_NOTIFYPOSTPAINT;
435 }
436 break;
437 }
438
439 *pResult = result;
440 return true;
441 }
442 case CDDS_SUBITEM | CDDS_ITEMPOSTPAINT:
443 {
444 LRESULT result = CDRF_DODEFAULT;
445 int iCol = pNmLvCustomDraw->iSubItem;
446 int iRow = pNmLvCustomDraw->nmcd.dwItemSpec;
447
448 switch (headers[iCol].type)
449 {
450 default:
451 result = CDRF_DODEFAULT;
452 break;
453
454 case ListView::ControlType::popup:
455 {
456 // draw the control at the RHS of the cell
457 RECT r;
458 ListView_GetSubItemRect(hWndListView, iRow, iCol, LVIR_BOUNDS, &r);
459 r.left = r.right - GetSystemMetrics(SM_CXVSCROLL);
460 DrawFrameControl(pNmLvCustomDraw->nmcd.hdc, &r, DFC_SCROLL,DFCS_SCROLLCOMBOBOX);
461
462 result = CDRF_DODEFAULT;
463 }
464 break;
465 }
466 *pResult = result;
467 return true;
468 }
469 }
470 }
471 break;
472
473 case LVN_HOTTRACK:
474 {
475 NMLISTVIEW *pNmListView = (NMLISTVIEW *)pNmHdr;
476 int iRow = pNmListView->iItem;
477 int iCol = pNmListView->iSubItem;
478 #if DEBUG
479 Log (LOG_BABBLE) << "LVN_HOTTRACK " << iRow << " " << iCol << endLog;
480 #endif
481 if (iRow < 0)
482 return true;
483
484 // if we've tracked off to a different cell
485 if ((iRow != iRow_track) || (iCol != iCol_track))
486 {
487 #if DEBUG
488 Log (LOG_BABBLE) << "LVN_HOTTRACK changed cell" << endLog;
489 #endif
490
491 // if the tooltip for previous cell is displayed, remove it
492 // restart the tooltip AUTOPOP timer for this cell
493 SendMessage(hWndTip, TTM_ACTIVATE, FALSE, 0);
494 SendMessage(hWndTip, TTM_ACTIVATE, TRUE, 0);
495
496 iRow_track = iRow;
497 iCol_track = iCol;
498 }
499
500 return true;
501 }
502 break;
503
504 case TTN_GETDISPINFO:
505 {
506 // convert mouse position to item/subitem
507 LVHITTESTINFO lvHitTestInfo;
508 lvHitTestInfo.flags = LVHT_ONITEM;
509 GetCursorPos(&lvHitTestInfo.pt);
510 ::ScreenToClient(hWndListView, &lvHitTestInfo.pt);
511 ListView_SubItemHitTest(hWndListView, &lvHitTestInfo);
512
513 int iRow = lvHitTestInfo.iItem;
514 int iCol = lvHitTestInfo.iSubItem;
515 if (iRow < 0)
516 return false;
517
518 #if DEBUG
519 Log (LOG_BABBLE) << "TTN_GETDISPINFO " << iRow << " " << iCol << endLog;
520 #endif
521
522 // get the tooltip text for that item/subitem
523 static StringCache tooltip;
524 tooltip = "";
525 if (contents)
526 tooltip = (*contents)[iRow]->get_tooltip(iCol);
527
528 // set the tooltip text
529 NMTTDISPINFO *pNmTTDispInfo = (NMTTDISPINFO *)pNmHdr;
530 pNmTTDispInfo->lpszText = tooltip;
531 pNmTTDispInfo->hinst = NULL;
532 pNmTTDispInfo->uFlags = 0;
533
534 return true;
535 }
536 break;
537 }
538
539 // We don't care.
540 return false;
541 }
542
543 void
544 ListView::empty(void)
545 {
546 ListView_DeleteAllItems(hWndListView);
547 }
548
549 void
550 ListView::setEmptyText(const char *text)
551 {
552 empty_list_text = text;
553 }
554
555 int
556 ListView::popup_menu(int iRow, int iCol, POINT p)
557 {
558 int update = 0;
559 // construct menu
560 HMENU hMenu = CreatePopupMenu();
561
562 MENUITEMINFO mii;
563 memset(&mii, 0, sizeof(mii));
564 mii.cbSize = sizeof(mii);
565 mii.fMask = MIIM_FTYPE | MIIM_STATE | MIIM_STRING | MIIM_ID;
566 mii.fType = MFT_STRING;
567
568 ActionList *al = (*contents)[iRow]->get_actions(iCol);
569
570 Actions::iterator i;
571 int j = 1;
572 for (i = al->list.begin (); i != al->list.end (); ++i, ++j)
573 {
574 BOOL res;
575 mii.dwTypeData = (char *)i->name.c_str();
576 mii.fState = (i->selected ? MFS_CHECKED : MFS_UNCHECKED |
577 i->enabled ? MFS_ENABLED : MFS_DISABLED);
578 mii.wID = j;
579
580 res = InsertMenuItem(hMenu, -1, TRUE, &mii);
581 if (!res) Log (LOG_BABBLE) << "InsertMenuItem failed " << endLog;
582 }
583
584 int id = TrackPopupMenu(hMenu,
585 TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_LEFTBUTTON | TPM_NOANIMATION,
586 p.x, p.y, 0, hWndListView, NULL);
587
588 // Inform the item of the menu choice
589 if (id)
590 update = (*contents)[iRow]->do_action(iCol, al->list[id-1].id);
591
592 DestroyMenu(hMenu);
593 delete al;
594
595 return update;
596 }
This page took 0.065339 seconds and 6 git commands to generate.