1 /** DGui project file.
2 
3 Copyright: Trogu Antonio Davide 2011-2013
4 
5 License: $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0).
6 
7 Authors: Trogu Antonio Davide
8 */
9 module dgui.listview;
10 
11 import std.utf: toUTF8;
12 public import dgui.core.controls.ownerdrawcontrol;
13 import dgui.core.utils;
14 import dgui.imagelist;
15 
16 enum ColumnTextAlign: int
17 {
18 	left = LVCFMT_LEFT,
19 	center = LVCFMT_CENTER,
20 	right = LVCFMT_RIGHT,
21 }
22 
23 enum ViewStyle: uint
24 {
25 	list = LVS_LIST,
26 	report = LVS_REPORT,
27 	largeIcon = LVS_ICON,
28 	smallIcon = LVS_SMALLICON,
29 }
30 
31 enum ListViewBits: ubyte
32 {
33 	none 			   = 0,
34 	gridLines  	   = 1,
35 	fullRowSelect    = 2,
36 	checkBoxes 	   = 4,
37 }
38 
39 class ListViewItem
40 {
41 	private Collection!(ListViewItem) _subItems;
42 	private bool _checked = false;
43 	private ListViewItem _parentItem;
44 	private ListView _owner;
45 	private string _text;
46 	private int _imgIdx;
47 
48 	mixin tagProperty;
49 
50 	package this(ListView owner, string txt, int imgIdx, bool check)
51 	{
52 		this._checked = check;
53 		this._imgIdx = imgIdx;
54 		this._owner = owner;
55 		this._text = txt;
56 	}
57 
58 	package this(ListView owner, ListViewItem parentItem, string txt, int imgIdx, bool check)
59 	{
60 		this._parentItem = parentItem;
61 		this(owner, txt, imgIdx, check);
62 	}
63 
64 	@property public final int index()
65 	{
66 		if(this._owner)
67 		{
68 			foreach(int i, ListViewItem lvi; this._owner.items)
69 			{
70 				if(lvi is (this._parentItem ? this._parentItem : this))
71 				{
72 					return i;
73 				}
74 			}
75 		}
76 
77 		return -1;
78 	}
79 
80 	@property public final int imageIndex()
81 	{
82 		return this._imgIdx;
83 	}
84 
85 	@property public final void imageIndex(int imgIdx)
86 	{
87 		if(this._parentItem)
88 		{
89 			return;
90 		}
91 
92 		this._imgIdx = imgIdx;
93 
94 		if(this._owner && this._owner.created)
95 		{
96 			LVITEMW lvi;
97 
98 			lvi.mask = LVIF_IMAGE;
99 			lvi.iItem = this.index;
100 			lvi.iSubItem = 0;
101 			lvi.iImage = imgIdx;
102 
103 			this._owner.sendMessage(LVM_SETITEMW, 0, cast(LPARAM)&lvi);
104 		}
105 	}
106 
107 	@property public final string text()
108 	{
109 		return this._text;
110 	}
111 
112 	@property public final void text(string s)
113 	{
114 		this._text = s;
115 
116 		if(this._owner && this._owner.created)
117 		{
118 			LVITEMW lvi;
119 
120 			lvi.mask = LVIF_TEXT;
121 			lvi.iItem = this.index;
122 			lvi.iSubItem = !this._parentItem ? 0 : this.subitemIndex;
123 			lvi.pszText = toUTFz!(wchar*)(s);
124 
125 			this._owner.sendMessage(LVM_SETITEMW, 0, cast(LPARAM)&lvi);
126 		}
127 	}
128 
129 	@property package bool internalChecked()
130 	{
131 		return this._checked;
132 	}
133 
134 	@property public final bool checked()
135 	{
136 		if(this._owner && this._owner.created)
137 		{
138 			return cast(bool)((this._owner.sendMessage(LVM_GETITEMSTATE, this.index, LVIS_STATEIMAGEMASK) >> 12) - 1);
139 		}
140 
141 		return this._checked;
142 	}
143 
144 	@property public final void checked(bool b)
145 	{
146 		if(this._parentItem)
147 		{
148 			return;
149 		}
150 
151 		this._checked = b;
152 
153 		if(this._owner && this._owner.created)
154 		{
155 			LVITEMW lvi;
156 
157 			lvi.mask = LVIF_STATE;
158 			lvi.stateMask = LVIS_STATEIMAGEMASK;
159 			lvi.state = cast(LPARAM)(b ? 2 : 1) << 12; //Checked State
160 
161 			this._owner.sendMessage(LVM_SETITEMSTATE, this.index, cast(LPARAM)&lvi);
162 		}
163 	}
164 
165 	public final void addSubItem(string txt)
166 	{
167 		if(this._parentItem) //E' un subitem, non fare niente.
168 		{
169 			return;
170 		}
171 
172 		if(!this._subItems)
173 		{
174 			this._subItems = new Collection!(ListViewItem)();
175 		}
176 
177 		ListViewItem lvi = new ListViewItem(this._owner, this, txt, -1, false);
178 		this._subItems.add(lvi);
179 
180 		if(this._owner && this._owner.created)
181 		{
182 			ListView.insertItem(lvi, true);
183 		}
184 	}
185 
186 	@property public final ListViewItem[] subItems()
187 	{
188 		if(this._subItems)
189 		{
190 			return this._subItems.get();
191 		}
192 
193 		return null;
194 	}
195 
196 	@property public final ListView listView()
197 	{
198 		return this._owner;
199 	}
200 
201 	@property package ListViewItem parentItem()
202 	{
203 		return this._parentItem;
204 	}
205 
206 	package void removeSubItem(int idx)
207 	{
208 		this._subItems.removeAt(idx);
209 	}
210 
211 	@property package int subitemIndex()
212 	{
213 		if(this._parentItem is this)
214 		{
215 			return 0; //Se è l'item principale ritorna 0.
216 		}
217 		else if(!this._parentItem.subItems)
218 		{
219 			return 1; //E' il primo subitem
220 		}
221 		else if(this._owner && this._parentItem)
222 		{
223 			int i = 0;
224 
225 			foreach(ListViewItem lvi; this._parentItem.subItems)
226 			{
227 				if(lvi is this)
228 				{
229 					return i + 1;
230 				}
231 
232 				i++;
233 			}
234 		}
235 
236 		return -1; //Non dovrebbe mai restituire -1
237 	}
238 }
239 
240 class ListViewColumn
241 {
242 	private ColumnTextAlign _cta;
243 	private ListView _owner;
244 	private string _text;
245 	private int _width;
246 
247 	package this(ListView owner, string txt, int w, ColumnTextAlign cta)
248 	{
249 		this._owner = owner;
250 		this._text = txt;
251 		this._width = w;
252 		this._cta = cta;
253 	}
254 
255 	@property public int index()
256 	{
257 		if(this._owner)
258 		{
259 			int i = 0;
260 
261 			foreach(ListViewColumn lvc; this._owner.columns)
262 			{
263 				if(lvc is this)
264 				{
265 					return i;
266 				}
267 
268 				i++;
269 			}
270 		}
271 
272 		return -1;
273 	}
274 
275 	@property public string text()
276 	{
277 		return this._text;
278 	}
279 
280 	@property public int width()
281 	{
282 		return this._width;
283 	}
284 
285 	@property public ColumnTextAlign textAlign()
286 	{
287 		return this._cta;
288 	}
289 
290 	@property public ListView listView()
291 	{
292 		return this._owner;
293 	}
294 }
295 
296 public alias ItemEventArgs!(ListViewItem) ListViewItemCheckedEventArgs;
297 
298 class ListView: OwnerDrawControl
299 {
300 	public Event!(Control, EventArgs) itemChanged;
301 	public Event!(Control, ListViewItemCheckedEventArgs) itemChecked;
302 
303 	private Collection!(ListViewColumn) _columns;
304 	private Collection!(ListViewItem) _items;
305 	private ListViewBits _lBits = ListViewBits.none;
306 	private ListViewItem _selectedItem;
307 	private ImageList _imgList;
308 
309 	@property public final ImageList imageList()
310 	{
311 		return this._imgList;
312 	}
313 
314 	@property public final void imageList(ImageList imgList)
315 	{
316 		 this._imgList = imgList;
317 
318 		if(this.created)
319 		{
320 			this.sendMessage(LVM_SETIMAGELIST, LVSIL_NORMAL, cast(LPARAM)imgList.handle);
321 			this.sendMessage(LVM_SETIMAGELIST, LVSIL_SMALL, cast(LPARAM)imgList.handle);
322 		}
323 	}
324 
325 	@property public final ViewStyle viewStyle()
326 	{
327 		if(this.getStyle() & ViewStyle.largeIcon)
328 		{
329 			return ViewStyle.largeIcon;
330 		}
331 		else if(this.getStyle() & ViewStyle.smallIcon)
332 		{
333 			return ViewStyle.smallIcon;
334 		}
335 		else if(this.getStyle() & ViewStyle.list)
336 		{
337 			return ViewStyle.list;
338 		}
339 		else if(this.getStyle() & ViewStyle.report)
340 		{
341 			return ViewStyle.report;
342 		}
343 
344 		assert(false, "Unknwown ListView Style");
345 	}
346 
347 	@property public final void viewStyle(ViewStyle vs)
348 	{
349 		/* Remove flickering in Report Mode */
350 		ListView.setBit(this._cBits, ControlBits.doubleBuffered, vs is ViewStyle.report);
351 
352 		this.setStyle(vs, true);
353 	}
354 
355 	@property public final bool fullRowSelect()
356 	{
357 		return cast(bool)(this._lBits & ListViewBits.fullRowSelect);
358 	}
359 
360 	@property public final void fullRowSelect(bool b)
361 	{
362 		this._lBits |= ListViewBits.fullRowSelect;
363 
364 		if(this.created)
365 		{
366 			this.sendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_FULLROWSELECT, b ? LVS_EX_FULLROWSELECT : 0);
367 		}
368 	}
369 
370 	@property public final bool gridLines()
371 	{
372 		return cast(bool)(this._lBits & ListViewBits.gridLines);
373 	}
374 
375 	@property public final void gridLines(bool b)
376 	{
377 		this._lBits |= ListViewBits.gridLines;
378 
379 		if(this.created)
380 		{
381 			this.sendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_GRIDLINES, b ? LVS_EX_GRIDLINES : 0);
382 		}
383 	}
384 
385 	@property public final bool checkBoxes()
386 	{
387 		return cast(bool)(this._lBits & ListViewBits.checkBoxes);
388 	}
389 
390 	@property public final void checkBoxes(bool b)
391 	{
392 		this._lBits |= ListViewBits.checkBoxes;
393 
394 		if(this.created)
395 		{
396 			this.sendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_CHECKBOXES, b ? LVS_EX_CHECKBOXES : 0);
397 		}
398 	}
399 
400 	@property public final ListViewItem selectedItem()
401 	{
402 		return this._selectedItem;
403 	}
404 
405 	public final ListViewColumn addColumn(string txt, int w, ColumnTextAlign cta = ColumnTextAlign.left)
406 	{
407 		if(!this._columns)
408 		{
409 			this._columns = new Collection!(ListViewColumn)();
410 		}
411 
412 		ListViewColumn lvc = new ListViewColumn(this, txt, w, cta);
413 		this._columns.add(lvc);
414 
415 		if(this.created)
416 		{
417 			ListView.insertColumn(lvc);
418 		}
419 
420 		return lvc;
421 	}
422 
423 	public final void removeColumn(int idx)
424 	{
425 		this._columns.removeAt(idx);
426 
427 		/*
428 		 * Rimuovo tutti gli items nella colonna rimossa
429 		 */
430 
431 		if(this._items)
432 		{
433 			if(idx)
434 			{
435 				foreach(ListViewItem lvi; this._items)
436 				{
437 					lvi.removeSubItem(idx - 1); //Subitems iniziano da 0 nelle DGui e da 1 su Windows.
438 				}
439 			}
440 			else
441 			{
442 				//TODO: Gestire caso "Rimozione colonna 0".
443 			}
444 		}
445 
446 		if(this.created)
447 		{
448 			this.sendMessage(LVM_DELETECOLUMN, idx, 0);
449 		}
450 	}
451 
452 	public final ListViewItem addItem(string txt, int imgIdx = -1, bool checked = false)
453 	{
454 		if(!this._items)
455 		{
456 			this._items = new Collection!(ListViewItem)();
457 		}
458 
459 		ListViewItem lvi = new ListViewItem(this, txt, imgIdx, checked);
460 		this._items.add(lvi);
461 
462 		if(this.created)
463 		{
464 			ListView.insertItem(lvi);
465 		}
466 
467 		return lvi;
468 	}
469 
470 	public final void removeItem(int idx)
471 	{
472 		if(this._items)
473 		{
474 			this._items.removeAt(idx);
475 		}
476 
477 		if(this.created)
478 		{
479 			this.sendMessage(LVM_DELETEITEM, idx, 0);
480 		}
481 	}
482 
483 	public final void clear()
484 	{
485 		if(this._items)
486 		{
487 			this._items.clear();
488 		}
489 
490 		if(this.created)
491 		{
492 			this.sendMessage(LVM_DELETEALLITEMS, 0, 0);
493 		}
494 	}
495 
496 	@property public final Collection!(ListViewItem) items()
497 	{
498 		return this._items;
499 	}
500 
501 	@property public final Collection!(ListViewColumn) columns()
502 	{
503 		return this._columns;
504 	}
505 
506 	package static void insertItem(ListViewItem item, bool subitem = false)
507 	{
508 		/*
509 		 * Item: Item (or SubItem) to insert.
510 		 * Subitem = Is a SubItem?
511 		 */
512 
513 		int idx = item.index;
514 		LVITEMW lvi;
515 
516 		lvi.mask = LVIF_TEXT | (!subitem ? (LVIF_IMAGE | LVIF_STATE | LVIF_PARAM) : 0);
517 		lvi.iImage = !subitem ? item.imageIndex : -1;
518 		lvi.iItem = !subitem ? idx : item.parentItem.index;
519 		lvi.iSubItem = !subitem ? 0 : item.subitemIndex; //ListView's subitem starts from 1 (0 is the main item).
520 		lvi.pszText = toUTFz!(wchar*)(item.text);
521 		lvi.lParam = winCast!(LPARAM)(item);
522 
523 		item.listView.sendMessage(!subitem ? LVM_INSERTITEMW : LVM_SETITEMW, 0, cast(LPARAM)&lvi);
524 
525 		if(!subitem)
526 		{
527 			if(item.listView.checkBoxes) //LVM_INSERTITEM doesn't handle CheckBoxes, use LVM_SETITEMSTATE
528 			{
529 				//Recycle the variable 'lvi'
530 
531 				lvi.mask = LVIF_STATE;
532 				lvi.stateMask = LVIS_STATEIMAGEMASK;
533 				lvi.state = cast(LPARAM)(item.internalChecked ? 2 : 1) << 12; //Checked State
534 				item.listView.sendMessage(LVM_SETITEMSTATE, idx, cast(LPARAM)&lvi);
535 			}
536 
537 			ListViewItem[] subItems = item.subItems;
538 
539 			if(subItems)
540 			{
541 				foreach(ListViewItem slvi; subItems)
542 				{
543 					ListView.insertItem(slvi, true);
544 				}
545 			}
546 		}
547 	}
548 
549 	private static void insertColumn(ListViewColumn col)
550 	{
551 		LVCOLUMNW lvc;
552 
553 		lvc.mask =  LVCF_TEXT | LVCF_WIDTH | LVCF_FMT;
554 		lvc.cx = col.width;
555 		lvc.fmt = col.textAlign;
556 		lvc.pszText = toUTFz!(wchar*)(col.text);
557 
558 		col.listView.sendMessage(LVM_INSERTCOLUMNW, col.listView._columns.length, cast(LPARAM)&lvc);
559 	}
560 
561 	protected override void createControlParams(ref CreateControlParams ccp)
562 	{
563 		this.setStyle(LVS_ALIGNLEFT | LVS_ALIGNTOP | LVS_AUTOARRANGE | LVS_SHAREIMAGELISTS, true);
564 
565 		/* WS_CLIPSIBLINGS | WS_CLIPCHILDREN: There is a SysHeader Component inside a list view in Report Mode */
566 		if(this.getStyle() & ViewStyle.report)
567 		{
568 			this.setStyle(WS_CLIPSIBLINGS | WS_CLIPCHILDREN, true);
569 		}
570 
571 		ccp.superclassName = WC_LISTVIEW;
572 		ccp.className = WC_DLISTVIEW;
573 		ccp.defaultBackColor = SystemColors.colorWindow;
574 
575 		switch(this._drawMode)
576 		{
577 			case OwnerDrawMode.fixed:
578 				this.setStyle(LVS_OWNERDRAWFIXED, true);
579 				break;
580 
581 			case OwnerDrawMode.variable:
582 				assert(false, "ListView: Owner Draw Variable Style not allowed");
583 
584 			default:
585 				break;
586 		}
587 
588 		//ListView.setBit(this._cBits, ControlBits.ORIGINAL_PAINT, true);
589 		super.createControlParams(ccp);
590 	}
591 
592 	protected override void onReflectedMessage(ref Message m)
593 	{
594 		switch(m.msg)
595 		{
596 			case WM_NOTIFY:
597 			{
598 				NMLISTVIEW* pNotify = cast(NMLISTVIEW*)m.lParam;
599 
600 				if(pNotify && pNotify.iItem != -1)
601 				{
602 					switch(pNotify.hdr.code)
603 					{
604 						case LVN_ITEMCHANGED:
605 						{
606 							if(pNotify.uChanged & LVIF_STATE)
607 							{
608 								uint changedState = pNotify.uNewState ^ pNotify.uOldState;
609 
610 								if(pNotify.uNewState & LVIS_SELECTED)
611 								{
612 									this._selectedItem = this._items[pNotify.iItem];
613 									this.onSelectedItemChanged(EventArgs.empty);
614 								}
615 
616 								if((changedState & 0x2000) || (changedState & 0x1000)) /* IF Checked || Unchecked THEN */
617 								{
618 									scope ListViewItemCheckedEventArgs e = new ListViewItemCheckedEventArgs(this._items[pNotify.iItem]);
619 									this.onItemChecked(e);
620 								}
621 							}
622 						}
623 						break;
624 
625 						default:
626 							break;
627 					}
628 				}
629 			}
630 			break;
631 
632 			default:
633 				break;
634 		}
635 
636 		super.onReflectedMessage(m);
637 	}
638 
639 	protected override void onHandleCreated(EventArgs e)
640 	{
641 		if(this._lBits & ListViewBits.gridLines)
642 		{
643 			this.sendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_GRIDLINES, LVS_EX_GRIDLINES);
644 		}
645 
646 		if(this._lBits & ListViewBits.fullRowSelect)
647 		{
648 			this.sendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);
649 		}
650 
651 		if(this._lBits & ListViewBits.checkBoxes)
652 		{
653 			this.sendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_CHECKBOXES, LVS_EX_CHECKBOXES);
654 		}
655 
656 		if(this._imgList)
657 		{
658 			this.sendMessage(LVM_SETIMAGELIST, LVSIL_NORMAL, cast(LPARAM)this._imgList.handle);
659 			this.sendMessage(LVM_SETIMAGELIST, LVSIL_SMALL, cast(LPARAM)this._imgList.handle);
660 		}
661 
662 		if(this.getStyle() & ViewStyle.report)
663 		{
664 			if(this._columns)
665 			{
666 				foreach(ListViewColumn lvc; this._columns)
667 				{
668 					ListView.insertColumn(lvc);
669 				}
670 			}
671 
672 			/* Remove flickering in Report Mode */
673 			ListView.setBit(this._cBits, ControlBits.doubleBuffered, true);
674 		}
675 
676 		if(this._items)
677 		{
678 			foreach(ListViewItem lvi; this._items)
679 			{
680 				ListView.insertItem(lvi);
681 			}
682 		}
683 
684 		super.onHandleCreated(e);
685 	}
686 
687 	protected void onSelectedItemChanged(EventArgs e)
688 	{
689 		this.itemChanged(this, e);
690 	}
691 
692 	protected void onItemChecked(ListViewItemCheckedEventArgs e)
693 	{
694 		this.itemChecked(this, e);
695 	}
696 }