2 * Copyright (c) 2016 Jon Turney
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.
9 * A copy of the GNU General Public License can be found at
15 #include "LogSingleton.h"
21 // ---------------------------------------------------------------------------
22 // implements class ListView
24 // ListView Common Control
25 // ---------------------------------------------------------------------------
27 int ModifierKeys::get()
30 if (GetKeyState(VK_SHIFT
) & 0x8000)
32 if (GetKeyState(VK_CONTROL
) & 0x8000)
34 if (GetKeyState(VK_MENU
) & 0x8000)
40 ListView::init(HWND parent
, int id
, HeaderList headers
)
44 // locate the listview control
45 hWndListView
= ::GetDlgItem(parent
, id
);
47 // configure the listview control
48 SendMessage(hWndListView
, CCM_SETVERSION
, 6, 0);
50 ListView_SetExtendedListViewStyle(hWndListView
,
51 LVS_EX_COLUMNSNAPPOINTS
| // use cxMin
52 LVS_EX_FULLROWSELECT
|
54 LVS_EX_HEADERDRAGDROP
); // headers can be re-ordered
56 // give the header control a border
57 HWND hWndHeader
= ListView_GetHeader(hWndListView
);
58 SetWindowLongPtr(hWndHeader
, GWL_STYLE
,
59 GetWindowLongPtr(hWndHeader
, GWL_STYLE
) | WS_BORDER
);
61 // ensure an initial item exists for width calculations...
66 lvi
.pszText
= const_cast <char *> ("Working...");
67 ListView_InsertItem(hWndListView
, &lvi
);
69 // populate with columns
72 // create a small icon imagelist
73 // (the order of images matches ListViewLine::State enum)
74 hImgList
= ImageList_Create(GetSystemMetrics(SM_CXSMICON
),
75 GetSystemMetrics(SM_CYSMICON
),
77 ImageList_AddIcon(hImgList
, LoadIcon(GetModuleHandle(NULL
), MAKEINTRESOURCE(IDI_TREE_PLUS
)));
78 ImageList_AddIcon(hImgList
, LoadIcon(GetModuleHandle(NULL
), MAKEINTRESOURCE(IDI_TREE_MINUS
)));
80 // create an empty imagelist, used to reset the indent
81 hEmptyImgList
= ImageList_Create(1, 1,
84 // LVS_EX_INFOTIP/LVN_GETINFOTIP doesn't work for subitems, so we have to do
85 // our own tooltip handling
86 hWndTip
= CreateWindowEx (0,
87 (LPCTSTR
) TOOLTIPS_CLASS
,
89 WS_POPUP
| TTS_NOPREFIX
| TTS_ALWAYSTIP
,
90 CW_USEDEFAULT
, CW_USEDEFAULT
, CW_USEDEFAULT
, CW_USEDEFAULT
,
93 GetModuleHandle(NULL
),
95 // must be topmost so that tooltips will display on top
96 SetWindowPos(hWndTip
, HWND_TOPMOST
,0, 0, 0, 0, SWP_NOMOVE
| SWP_NOSIZE
| SWP_NOACTIVATE
);
99 memset ((void *)&ti
, 0, sizeof(ti
));
100 ti
.cbSize
= sizeof(ti
);
101 ti
.uFlags
= TTF_IDISHWND
| TTF_SUBCLASS
;
102 ti
.hwnd
= hWndParent
;
103 ti
.uId
= (UINT_PTR
)hWndListView
;
104 ti
.lpszText
= LPSTR_TEXTCALLBACK
; // use TTN_GETDISPINFO
105 SendMessage(hWndTip
, TTM_ADDTOOL
, 0, (LPARAM
)&ti
);
107 // match long delay for tooltip to disappear used elsewhere (30s)
108 SendMessage(hWndTip
, TTM_SETDELAYTIME
, TTDT_AUTOPOP
, (LPARAM
) MAKELONG (30000, 0));
109 // match tip width used elsewhere
110 SendMessage(hWndTip
, TTM_SETMAXTIPWIDTH
, 0, 450);
112 // switch to using wide-char WM_NOTIFY messages
113 ListView_SetUnicodeFormat(hWndListView
, TRUE
);
117 ListView::initColumns(HeaderList headers_
)
119 // store HeaderList for later use
122 // create the columns
124 lvc
.mask
= LVCF_FMT
| LVCF_WIDTH
| LVCF_TEXT
| LVCF_SUBITEM
;
127 for (i
= 0; headers
[i
].text
!= 0; i
++)
129 std::wstring h
= LoadStringW(headers
[i
].text
);
132 lvc
.pszText
= const_cast <wchar_t *> (h
.c_str());
134 lvc
.fmt
= headers
[i
].fmt
;
136 SendMessage(hWndListView
, LVM_INSERTCOLUMNW
, i
, (LPARAM
)&lvc
);
139 // now do some width calculations
140 for (i
= 0; headers
[i
].text
!= 0; i
++)
142 headers
[i
].width
= 0;
144 ListView_SetColumnWidth(hWndListView
, i
, LVSCW_AUTOSIZE_USEHEADER
);
145 headers
[i
].hdr_width
= ListView_GetColumnWidth(hWndListView
, i
);
150 ListView::noteColumnWidthStart()
152 dc
= GetDC (hWndListView
);
154 // we must set the font of the DC here, otherwise the width calculations
155 // will be off because the system will use the wrong font metrics
156 HANDLE sysfont
= GetStockObject (DEFAULT_GUI_FONT
);
157 SelectObject (dc
, sysfont
);
160 for (i
= 0; headers
[i
].text
!= 0; i
++)
162 headers
[i
].width
= 0;
166 // wrappers to help instantiations of the noteColumnWidth() template call the
167 // right version of GetTextExtentPoint32
168 #undef GetTextExtentPoint32
170 static BOOL
GetTextExtentPoint32(HDC hdc
, LPCSTR lpString
, int c
, LPSIZE psizl
)
172 return GetTextExtentPoint32A(hdc
, lpString
, c
, psizl
);
175 static BOOL
GetTextExtentPoint32(HDC hdc
, LPCWSTR lpString
, int c
, LPSIZE psizl
)
177 return GetTextExtentPoint32W(hdc
, lpString
, c
, psizl
);
180 template <typename T
>
182 ListView::noteColumnWidth(int col_num
, const T
& string
)
186 // A margin of 3*GetSystemMetrics(SM_CXEDGE) is used at each side of the
188 int addend
= 2*3*GetSystemMetrics(SM_CXEDGE
);
191 GetTextExtentPoint32 (dc
, string
.c_str(), string
.size(), &s
);
193 int width
= addend
+ s
.cx
;
195 // allow for width of dropdown button in popup columns
196 if (headers
[col_num
].type
== ListView::ControlType::popup
)
198 width
+= GetSystemMetrics(SM_CXVSCROLL
);
201 if (width
> headers
[col_num
].width
)
202 headers
[col_num
].width
= width
;
205 // explicit instantiation
206 template void ListView::noteColumnWidth(int col_num
, const std::string
& string
);
207 template void ListView::noteColumnWidth(int col_num
, const std::wstring
& wstring
);
210 ListView::noteColumnWidthEnd()
212 ReleaseDC(hWndListView
, dc
);
216 ListView::resizeColumns(void)
218 // ensure the last column stretches all the way to the right-hand side of the
222 for (i
= 0; headers
[i
].text
!= 0; i
++)
223 total
= total
+ headers
[i
].width
;
226 GetClientRect(hWndListView
, &r
);
227 int width
= r
.right
- r
.left
;
230 headers
[i
-1].width
+= width
- total
;
234 lvc
.mask
= LVCF_WIDTH
| LVCF_MINWIDTH
;
235 for (i
= 0; headers
[i
].text
!= 0; i
++)
238 lvc
.cx
= (headers
[i
].width
< headers
[i
].hdr_width
) ? headers
[i
].hdr_width
: headers
[i
].width
;
239 lvc
.cxMin
= headers
[i
].hdr_width
;
241 Log (LOG_BABBLE
) << "resizeColumns: " << i
<< " cx " << lvc
.cx
<< " cxMin " << lvc
.cxMin
<<endLog
;
244 ListView_SetColumn(hWndListView
, i
, &lvc
);
249 ListView::setContents(ListViewContents
*_contents
, bool tree
)
251 contents
= _contents
;
253 // disable redrawing of ListView
254 // (otherwise it will redraw every time a row is added, which makes this very slow)
255 SendMessage(hWndListView
, WM_SETREDRAW
, FALSE
, 0);
257 // preserve focus/selection
258 int iRow
= ListView_GetSelectionMark(hWndListView
);
262 // assign imagelist to listview control (this also sets the size for indents)
264 ListView_SetImageList(hWndListView
, hImgList
, LVSIL_SMALL
);
266 ListView_SetImageList(hWndListView
, hEmptyImgList
, LVSIL_SMALL
);
269 for (i
= 0; i
< contents
->size(); i
++)
272 lvi
.mask
= LVIF_TEXT
| (tree
? LVIF_IMAGE
| LVIF_INDENT
: 0);
275 lvi
.pszText
= LPSTR_TEXTCALLBACK
;
278 lvi
.iImage
= I_IMAGECALLBACK
;
279 lvi
.iIndent
= (*contents
)[i
]->get_indent();
282 ListView_InsertItem(hWndListView
, &lvi
);
287 ListView_SetItemState(hWndListView
, iRow
, LVNI_SELECTED
| LVNI_FOCUSED
, LVNI_SELECTED
| LVNI_FOCUSED
);
288 ListView_EnsureVisible(hWndListView
, iRow
, false);
291 // enable redrawing of ListView and redraw
292 SendMessage(hWndListView
, WM_SETREDRAW
, TRUE
, 0);
293 RedrawWindow(hWndListView
, NULL
, NULL
, RDW_ERASE
| RDW_FRAME
| RDW_INVALIDATE
| RDW_ALLCHILDREN
);
296 // Helper class: The pointer we hand back needs to remain valid for some time
297 // after OnNotify returns, when the string object we have retrieved has gone out
298 // of scope, so a static instance of this class maintains a local cache.
299 template <class T
> class StringCache
301 typedef typename
T::traits_type::char_type char_type
;
303 StringCache() : cache(NULL
), cache_size(0) { }
304 StringCache
& operator = (const T
& s
)
306 if ((s
.length() + 1) > cache_size
)
308 cache_size
= s
.length() + 1;
309 cache
= (char_type
*)realloc(cache
, cache_size
* sizeof(char_type
));
311 memcpy(cache
, s
.c_str(), (s
.length() + 1) * sizeof(char_type
));
314 operator char_type
*() const
324 ListView::OnNotify (NMHDR
*pNmHdr
, LRESULT
*pResult
)
327 Log (LOG_BABBLE
) << "ListView::OnNotify id:" << pNmHdr
->idFrom
<< " hwnd:" << pNmHdr
->hwndFrom
<< " code:" << (int)pNmHdr
->code
<< endLog
;
330 switch (pNmHdr
->code
)
332 case LVN_GETDISPINFOW
:
334 NMLVDISPINFOW
*pNmLvDispInfo
= (NMLVDISPINFOW
*)pNmHdr
;
336 Log (LOG_BABBLE
) << "LVN_GETDISPINFO " << pNmLvDispInfo
->item
.iItem
<< endLog
;
340 int iRow
= pNmLvDispInfo
->item
.iItem
;
341 int iCol
= pNmLvDispInfo
->item
.iSubItem
;
343 static StringCache
<std::wstring
> s
;
344 s
= (*contents
)[iRow
]->get_text(iCol
);
345 pNmLvDispInfo
->item
.pszText
= s
;
347 if (pNmLvDispInfo
->item
.iSubItem
== 0)
349 pNmLvDispInfo
->item
.iImage
= (int)((*contents
)[pNmLvDispInfo
->item
.iItem
]->get_state());
357 case LVN_GETEMPTYMARKUP
:
359 NMLVEMPTYMARKUP
*pNmMarkup
= (NMLVEMPTYMARKUP
*) pNmHdr
;
360 wcsncpy(pNmMarkup
->szMarkup
, empty_list_text
.c_str(), L_MAX_URL_LENGTH
);
368 NMITEMACTIVATE
*pNmItemAct
= (NMITEMACTIVATE
*) pNmHdr
;
370 Log (LOG_BABBLE
) << "NM_CLICK: pnmitem->iItem " << pNmItemAct
->iItem
<< " pNmItemAct->iSubItem " << pNmItemAct
->iSubItem
<< endLog
;
372 int iRow
= pNmItemAct
->iItem
;
373 int iCol
= pNmItemAct
->iSubItem
;
379 if (headers
[iCol
].type
== ListView::ControlType::popup
)
385 ListView_GetSubItemRect(hWndListView
, iRow
, iCol
, LVIR_BOUNDS
, &r
);
387 ::ScreenToClient(hWndListView
, &cp
);
389 // if the click isn't over the pop-up button, do nothing yet (but this
390 // might be followed by a NM_DBLCLK)
391 if (cp
.x
< r
.right
- GetSystemMetrics(SM_CXVSCROLL
))
394 // position pop-up menu at the location of the click
395 update
= popup_menu(iRow
, iCol
, p
);
399 // Inform the item of the click
400 update
= (*contents
)[iRow
]->do_action(iCol
, 0);
403 // Update items, if needed
406 ListView_RedrawItems(hWndListView
, iRow
, iRow
+ update
-1);
415 NMITEMACTIVATE
*pNmItemAct
= (NMITEMACTIVATE
*) pNmHdr
;
417 Log (LOG_BABBLE
) << "NM_DBLCLICK: pnmitem->iItem " << pNmItemAct
->iItem
<< " pNmItemAct->iSubItem " << pNmItemAct
->iSubItem
<< endLog
;
419 int iRow
= pNmItemAct
->iItem
;
420 int iCol
= pNmItemAct
->iSubItem
;
426 // Inform the item of the double-click
427 update
= (*contents
)[iRow
]->do_default_action(iCol
);
429 // Update items, if needed
432 ListView_RedrawItems(hWndListView
, iRow
, iRow
+ update
-1);
441 NMLVCUSTOMDRAW
*pNmLvCustomDraw
= (NMLVCUSTOMDRAW
*)pNmHdr
;
443 switch(pNmLvCustomDraw
->nmcd
.dwDrawStage
)
446 *pResult
= CDRF_NOTIFYITEMDRAW
;
448 case CDDS_ITEMPREPAINT
:
449 *pResult
= CDRF_NOTIFYSUBITEMDRAW
;
451 case CDDS_SUBITEM
| CDDS_ITEMPREPAINT
:
453 LRESULT result
= CDRF_DODEFAULT
;
454 int iCol
= pNmLvCustomDraw
->iSubItem
;
455 int iRow
= pNmLvCustomDraw
->nmcd
.dwItemSpec
;
457 switch (headers
[iCol
].type
)
460 case ListView::ControlType::text
:
461 result
= CDRF_DODEFAULT
;
464 case ListView::ControlType::checkbox
:
466 // get the subitem text (as ASCII)
468 ListView_GetItemText(hWndListView
, iRow
, iCol
, buf
, _countof(buf
));
470 // map the subitem text to a checkbox state
471 UINT state
= DFCS_BUTTONCHECK
| DFCS_FLAT
;
472 if (buf
[0] == '\0') // empty
474 result
= CDRF_DODEFAULT
;
477 else if (buf
[0] == 'y') // yes
478 state
|= DFCS_CHECKED
;
479 else if ((buf
[0] == 'n') && (buf
[1] == 'o')) // no
482 state
|= DFCS_INACTIVE
;
484 // erase and draw a checkbox
486 ListView_GetSubItemRect(hWndListView
, iRow
, iCol
, LVIR_BOUNDS
, &r
);
488 if (pNmLvCustomDraw
->nmcd
.uItemState
& CDIS_SELECTED
)
489 bkg_color
= GetSysColor(COLOR_HIGHLIGHT
);
491 bkg_color
= ListView_GetBkColor(hWndListView
);
492 HBRUSH hBrush
= CreateSolidBrush(bkg_color
);
493 FillRect(pNmLvCustomDraw
->nmcd
.hdc
, &r
, hBrush
);
494 DeleteObject(hBrush
);
495 DrawFrameControl(pNmLvCustomDraw
->nmcd
.hdc
, &r
, DFC_BUTTON
, state
);
497 result
= CDRF_SKIPDEFAULT
;
501 case ListView::ControlType::popup
:
503 // let the control draw the text, but notify us afterwards
504 result
= CDRF_NOTIFYPOSTPAINT
;
512 case CDDS_SUBITEM
| CDDS_ITEMPOSTPAINT
:
514 LRESULT result
= CDRF_DODEFAULT
;
515 int iCol
= pNmLvCustomDraw
->iSubItem
;
516 int iRow
= pNmLvCustomDraw
->nmcd
.dwItemSpec
;
518 switch (headers
[iCol
].type
)
521 result
= CDRF_DODEFAULT
;
524 case ListView::ControlType::popup
:
526 // draw the control at the RHS of the cell
528 ListView_GetSubItemRect(hWndListView
, iRow
, iCol
, LVIR_BOUNDS
, &r
);
529 r
.left
= r
.right
- GetSystemMetrics(SM_CXVSCROLL
);
530 DrawFrameControl(pNmLvCustomDraw
->nmcd
.hdc
, &r
, DFC_SCROLL
,DFCS_SCROLLCOMBOBOX
);
532 result
= CDRF_DODEFAULT
;
545 NMLISTVIEW
*pNmListView
= (NMLISTVIEW
*)pNmHdr
;
546 int iRow
= pNmListView
->iItem
;
547 int iCol
= pNmListView
->iSubItem
;
549 Log (LOG_BABBLE
) << "LVN_HOTTRACK " << iRow
<< " " << iCol
<< endLog
;
554 // if we've tracked off to a different cell
555 if ((iRow
!= iRow_track
) || (iCol
!= iCol_track
))
558 Log (LOG_BABBLE
) << "LVN_HOTTRACK changed cell" << endLog
;
561 // if the tooltip for previous cell is displayed, remove it
562 // restart the tooltip AUTOPOP timer for this cell
563 SendMessage(hWndTip
, TTM_ACTIVATE
, FALSE
, 0);
564 SendMessage(hWndTip
, TTM_ACTIVATE
, TRUE
, 0);
576 NMLVKEYDOWN
*pNmLvKeyDown
= (NMLVKEYDOWN
*)pNmHdr
;
577 int iRow
= ListView_GetSelectionMark(hWndListView
);
578 int modkeys
= ModifierKeys::get();
580 Log (LOG_PLAIN
) << "LVN_KEYDOWN vkey " << pNmLvKeyDown
->wVKey
<< " on row " << iRow
581 << " Shift:" << !!(modkeys
& ModifierKeys::Shift
)
582 << " Ctrl:" << !!(modkeys
& ModifierKeys::Control
)
583 << " Alt:" << !!(modkeys
& ModifierKeys::Alt
) << endLog
;
586 if (contents
&& iRow
>= 0)
590 int todo
= (*contents
)[iRow
]->map_key_to_action(pNmLvKeyDown
->wVKey
, modkeys
,
593 if (todo
& ListViewLine::Action::Direct
)
594 update
= (*contents
)[iRow
]->do_action(col_num
, action_id
);
595 else if (todo
& ListViewLine::Action::PopUp
)
599 ListView_GetSubItemRect(hWndListView
, iRow
, col_num
, LVIR_BOUNDS
, &r
);
602 ClientToScreen(hWndListView
, &p
);
604 update
= popup_menu(iRow
, col_num
, p
);
608 ListView_RedrawItems(hWndListView
, iRow
, iRow
+ update
-1);
610 if ((todo
& ListViewLine::Action::NextRow
)
611 && iRow
+ 1 < ListView_GetItemCount(hWndListView
))
613 // move selection to next row
614 ListView_SetItemState(hWndListView
, -1, 0, LVIS_SELECTED
| LVIS_FOCUSED
);
615 ListView_SetItemState(hWndListView
, iRow
+ 1, LVIS_SELECTED
| LVIS_FOCUSED
,
616 LVIS_SELECTED
| LVIS_FOCUSED
);
617 ListView_SetSelectionMark(hWndListView
, iRow
+ 1);
618 ListView_EnsureVisible(hWndListView
, iRow
+ 1, false);
624 case TTN_GETDISPINFOW
:
626 // convert mouse position to item/subitem
627 LVHITTESTINFO lvHitTestInfo
;
628 lvHitTestInfo
.flags
= LVHT_ONITEM
;
629 GetCursorPos(&lvHitTestInfo
.pt
);
630 ::ScreenToClient(hWndListView
, &lvHitTestInfo
.pt
);
631 ListView_SubItemHitTest(hWndListView
, &lvHitTestInfo
);
633 int iRow
= lvHitTestInfo
.iItem
;
634 int iCol
= lvHitTestInfo
.iSubItem
;
639 Log (LOG_BABBLE
) << "TTN_GETDISPINFO " << iRow
<< " " << iCol
<< endLog
;
642 // get the tooltip text for that item/subitem
643 static StringCache
<std::wstring
> wtooltip
;
644 std::string tooltip
= "";
646 tooltip
= (*contents
)[iRow
]->get_tooltip(iCol
);
648 wtooltip
= string_to_wstring(tooltip
);
650 // set the tooltip text
651 NMTTDISPINFOW
*pNmTTDispInfo
= (NMTTDISPINFOW
*)pNmHdr
;
652 pNmTTDispInfo
->lpszText
= wtooltip
;
653 pNmTTDispInfo
->hinst
= NULL
;
654 pNmTTDispInfo
->uFlags
= 0;
666 ListView::empty(void)
668 ListView_DeleteAllItems(hWndListView
);
672 ListView::setEmptyText(unsigned int text
)
674 empty_list_text
= LoadStringW(text
);
678 ListView::popup_menu(int iRow
, int iCol
, POINT p
)
682 HMENU hMenu
= CreatePopupMenu();
685 memset(&mii
, 0, sizeof(mii
));
686 mii
.cbSize
= sizeof(mii
);
687 mii
.fMask
= MIIM_FTYPE
| MIIM_STATE
| MIIM_STRING
| MIIM_ID
;
688 mii
.fType
= MFT_STRING
;
690 ActionList
*al
= (*contents
)[iRow
]->get_actions(iCol
);
694 for (i
= al
->list
.begin (); i
!= al
->list
.end (); ++i
, ++j
)
697 mii
.dwTypeData
= const_cast <wchar_t *> (i
->name
.c_str());
698 mii
.fState
= (i
->selected
? MFS_CHECKED
: MFS_UNCHECKED
|
699 i
->enabled
? MFS_ENABLED
: MFS_DISABLED
);
702 res
= InsertMenuItemW(hMenu
, -1, TRUE
, &mii
);
703 if (!res
) Log (LOG_BABBLE
) << "InsertMenuItem failed " << endLog
;
706 int id
= TrackPopupMenu(hMenu
,
707 TPM_LEFTALIGN
| TPM_TOPALIGN
| TPM_RETURNCMD
| TPM_LEFTBUTTON
| TPM_NOANIMATION
,
708 p
.x
, p
.y
, 0, hWndListView
, NULL
);
710 // Inform the item of the menu choice
712 update
= (*contents
)[iRow
]->do_action(iCol
, al
->list
[id
-1].id
);