]> cygwin.com Git - cygwin-apps/setup.git/blob - ListView.cc
Add double-click for a 'default action' to ListView
[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 GetCursorPos(&p);
349
350 RECT r;
351 ListView_GetSubItemRect(hWndListView, iRow, iCol, LVIR_BOUNDS, &r);
352 POINT cp = p;
353 ::ScreenToClient(hWndListView, &cp);
354
355 // if the click isn't over the pop-up button, do nothing yet (but this
356 // might be followed by a NM_DBLCLK)
357 if (cp.x < r.right - GetSystemMetrics(SM_CXVSCROLL))
358 return true;
359
360 // position pop-up menu at the location of the click
361 update = popup_menu(iRow, iCol, p);
362 }
363 else
364 {
365 // Inform the item of the click
366 update = (*contents)[iRow]->do_action(iCol, 0);
367 }
368
369 // Update items, if needed
370 if (update > 0)
371 {
372 ListView_RedrawItems(hWndListView, iRow, iRow + update -1);
373 }
374
375 return true;
376 }
377 break;
378
379 case NM_DBLCLK:
380 {
381 NMITEMACTIVATE *pNmItemAct = (NMITEMACTIVATE *) pNmHdr;
382 #if DEBUG
383 Log (LOG_BABBLE) << "NM_DBLCLICK: pnmitem->iItem " << pNmItemAct->iItem << " pNmItemAct->iSubItem " << pNmItemAct->iSubItem << endLog;
384 #endif
385 int iRow = pNmItemAct->iItem;
386 int iCol = pNmItemAct->iSubItem;
387 if (iRow < 0)
388 return false;
389
390 int update = 0;
391
392 // Inform the item of the double-click
393 update = (*contents)[iRow]->do_default_action(iCol );
394
395 // Update items, if needed
396 if (update > 0)
397 {
398 ListView_RedrawItems(hWndListView, iRow, iRow + update -1);
399 }
400
401 return true;
402 }
403 break;
404
405 case NM_CUSTOMDRAW:
406 {
407 NMLVCUSTOMDRAW *pNmLvCustomDraw = (NMLVCUSTOMDRAW *)pNmHdr;
408
409 switch(pNmLvCustomDraw->nmcd.dwDrawStage)
410 {
411 case CDDS_PREPAINT:
412 *pResult = CDRF_NOTIFYITEMDRAW;
413 return true;
414 case CDDS_ITEMPREPAINT:
415 *pResult = CDRF_NOTIFYSUBITEMDRAW;
416 return true;
417 case CDDS_SUBITEM | CDDS_ITEMPREPAINT:
418 {
419 LRESULT result = CDRF_DODEFAULT;
420 int iCol = pNmLvCustomDraw->iSubItem;
421 int iRow = pNmLvCustomDraw->nmcd.dwItemSpec;
422
423 switch (headers[iCol].type)
424 {
425 default:
426 case ListView::ControlType::text:
427 result = CDRF_DODEFAULT;
428 break;
429
430 case ListView::ControlType::checkbox:
431 {
432 // get the subitem text
433 char buf[3];
434 ListView_GetItemText(hWndListView, iRow, iCol, buf, _countof(buf));
435
436 // map the subitem text to a checkbox state
437 UINT state = DFCS_BUTTONCHECK | DFCS_FLAT;
438 if (buf[0] == '\0') // empty
439 {
440 result = CDRF_DODEFAULT;
441 break;
442 }
443 else if (buf[0] == 'y') // yes
444 state |= DFCS_CHECKED;
445 else if ((buf[0] == 'n') && (buf[1] == 'o')) // no
446 state |= 0;
447 else // n/a
448 state |= DFCS_INACTIVE;
449
450 // erase and draw a checkbox
451 RECT r;
452 ListView_GetSubItemRect(hWndListView, iRow, iCol, LVIR_BOUNDS, &r);
453 DWORD bkg_color;
454 if (pNmLvCustomDraw->nmcd.uItemState & CDIS_SELECTED)
455 bkg_color = GetSysColor(COLOR_HIGHLIGHT);
456 else
457 bkg_color = ListView_GetBkColor(hWndListView);
458 HBRUSH hBrush = CreateSolidBrush(bkg_color);
459 FillRect(pNmLvCustomDraw->nmcd.hdc, &r, hBrush);
460 DeleteObject(hBrush);
461 DrawFrameControl(pNmLvCustomDraw->nmcd.hdc, &r, DFC_BUTTON, state);
462
463 result = CDRF_SKIPDEFAULT;
464 }
465 break;
466
467 case ListView::ControlType::popup:
468 {
469 // let the control draw the text, but notify us afterwards
470 result = CDRF_NOTIFYPOSTPAINT;
471 }
472 break;
473 }
474
475 *pResult = result;
476 return true;
477 }
478 case CDDS_SUBITEM | CDDS_ITEMPOSTPAINT:
479 {
480 LRESULT result = CDRF_DODEFAULT;
481 int iCol = pNmLvCustomDraw->iSubItem;
482 int iRow = pNmLvCustomDraw->nmcd.dwItemSpec;
483
484 switch (headers[iCol].type)
485 {
486 default:
487 result = CDRF_DODEFAULT;
488 break;
489
490 case ListView::ControlType::popup:
491 {
492 // draw the control at the RHS of the cell
493 RECT r;
494 ListView_GetSubItemRect(hWndListView, iRow, iCol, LVIR_BOUNDS, &r);
495 r.left = r.right - GetSystemMetrics(SM_CXVSCROLL);
496 DrawFrameControl(pNmLvCustomDraw->nmcd.hdc, &r, DFC_SCROLL,DFCS_SCROLLCOMBOBOX);
497
498 result = CDRF_DODEFAULT;
499 }
500 break;
501 }
502 *pResult = result;
503 return true;
504 }
505 }
506 }
507 break;
508
509 case LVN_HOTTRACK:
510 {
511 NMLISTVIEW *pNmListView = (NMLISTVIEW *)pNmHdr;
512 int iRow = pNmListView->iItem;
513 int iCol = pNmListView->iSubItem;
514 #if DEBUG
515 Log (LOG_BABBLE) << "LVN_HOTTRACK " << iRow << " " << iCol << endLog;
516 #endif
517 if (iRow < 0)
518 return true;
519
520 // if we've tracked off to a different cell
521 if ((iRow != iRow_track) || (iCol != iCol_track))
522 {
523 #if DEBUG
524 Log (LOG_BABBLE) << "LVN_HOTTRACK changed cell" << endLog;
525 #endif
526
527 // if the tooltip for previous cell is displayed, remove it
528 // restart the tooltip AUTOPOP timer for this cell
529 SendMessage(hWndTip, TTM_ACTIVATE, FALSE, 0);
530 SendMessage(hWndTip, TTM_ACTIVATE, TRUE, 0);
531
532 iRow_track = iRow;
533 iCol_track = iCol;
534 }
535
536 return true;
537 }
538 break;
539
540 case LVN_KEYDOWN:
541 {
542 NMLVKEYDOWN *pNmLvKeyDown = (NMLVKEYDOWN *)pNmHdr;
543 int iRow = ListView_GetSelectionMark(hWndListView);
544 #if DEBUG
545 Log (LOG_PLAIN) << "LVN_KEYDOWN vkey " << pNmLvKeyDown->wVKey << " on row " << iRow << endLog;
546 #endif
547
548 if (contents && iRow >= 0)
549 {
550 int col_num;
551 int action_id;
552 if ((*contents)[iRow]->map_key_to_action(pNmLvKeyDown->wVKey, &col_num, &action_id))
553 {
554 int update;
555 if (action_id >= 0)
556 update = (*contents)[iRow]->do_action(col_num, action_id);
557 else
558 {
559 POINT p;
560 RECT r;
561 ListView_GetSubItemRect(hWndListView, iRow, col_num, LVIR_BOUNDS, &r);
562 p.x = r.left;
563 p.y = r.top;
564 ClientToScreen(hWndListView, &p);
565
566 update = popup_menu(iRow, col_num, p);
567 }
568
569 if (update > 0)
570 ListView_RedrawItems(hWndListView, iRow, iRow + update -1);
571 }
572 }
573 }
574 break;
575
576 case TTN_GETDISPINFO:
577 {
578 // convert mouse position to item/subitem
579 LVHITTESTINFO lvHitTestInfo;
580 lvHitTestInfo.flags = LVHT_ONITEM;
581 GetCursorPos(&lvHitTestInfo.pt);
582 ::ScreenToClient(hWndListView, &lvHitTestInfo.pt);
583 ListView_SubItemHitTest(hWndListView, &lvHitTestInfo);
584
585 int iRow = lvHitTestInfo.iItem;
586 int iCol = lvHitTestInfo.iSubItem;
587 if (iRow < 0)
588 return false;
589
590 #if DEBUG
591 Log (LOG_BABBLE) << "TTN_GETDISPINFO " << iRow << " " << iCol << endLog;
592 #endif
593
594 // get the tooltip text for that item/subitem
595 static StringCache tooltip;
596 tooltip = "";
597 if (contents)
598 tooltip = (*contents)[iRow]->get_tooltip(iCol);
599
600 // set the tooltip text
601 NMTTDISPINFO *pNmTTDispInfo = (NMTTDISPINFO *)pNmHdr;
602 pNmTTDispInfo->lpszText = tooltip;
603 pNmTTDispInfo->hinst = NULL;
604 pNmTTDispInfo->uFlags = 0;
605
606 return true;
607 }
608 break;
609 }
610
611 // We don't care.
612 return false;
613 }
614
615 void
616 ListView::empty(void)
617 {
618 ListView_DeleteAllItems(hWndListView);
619 }
620
621 void
622 ListView::setEmptyText(const char *text)
623 {
624 empty_list_text = text;
625 }
626
627 int
628 ListView::popup_menu(int iRow, int iCol, POINT p)
629 {
630 int update = 0;
631 // construct menu
632 HMENU hMenu = CreatePopupMenu();
633
634 MENUITEMINFO mii;
635 memset(&mii, 0, sizeof(mii));
636 mii.cbSize = sizeof(mii);
637 mii.fMask = MIIM_FTYPE | MIIM_STATE | MIIM_STRING | MIIM_ID;
638 mii.fType = MFT_STRING;
639
640 ActionList *al = (*contents)[iRow]->get_actions(iCol);
641
642 Actions::iterator i;
643 int j = 1;
644 for (i = al->list.begin (); i != al->list.end (); ++i, ++j)
645 {
646 BOOL res;
647 mii.dwTypeData = (char *)i->name.c_str();
648 mii.fState = (i->selected ? MFS_CHECKED : MFS_UNCHECKED |
649 i->enabled ? MFS_ENABLED : MFS_DISABLED);
650 mii.wID = j;
651
652 res = InsertMenuItem(hMenu, -1, TRUE, &mii);
653 if (!res) Log (LOG_BABBLE) << "InsertMenuItem failed " << endLog;
654 }
655
656 int id = TrackPopupMenu(hMenu,
657 TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_LEFTBUTTON | TPM_NOANIMATION,
658 p.x, p.y, 0, hWndListView, NULL);
659
660 // Inform the item of the menu choice
661 if (id)
662 update = (*contents)[iRow]->do_action(iCol, al->list[id-1].id);
663
664 DestroyMenu(hMenu);
665 delete al;
666
667 return update;
668 }
This page took 0.069499 seconds and 6 git commands to generate.