]> git.saurik.com Git - wxWidgets.git/blob - src/generic/vlbox.cpp
Applied #9011: Native wxListCtrl::HitTest on OS X
[wxWidgets.git] / src / generic / vlbox.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // Name: src/generic/vlbox.cpp
3 // Purpose: implementation of wxVListBox
4 // Author: Vadim Zeitlin
5 // Modified by:
6 // Created: 31.05.03
7 // RCS-ID: $Id$
8 // Copyright: (c) 2003 Vadim Zeitlin <vadim@wxwindows.org>
9 // License: wxWindows license
10 ///////////////////////////////////////////////////////////////////////////////
11
12 // ============================================================================
13 // declarations
14 // ============================================================================
15
16 // ----------------------------------------------------------------------------
17 // headers
18 // ----------------------------------------------------------------------------
19
20 // For compilers that support precompilation, includes "wx.h".
21 #include "wx/wxprec.h"
22
23 #ifdef __BORLANDC__
24 #pragma hdrstop
25 #endif
26
27 #if wxUSE_LISTBOX
28
29 #include "wx/vlbox.h"
30
31 #ifndef WX_PRECOMP
32 #include "wx/settings.h"
33 #include "wx/dcclient.h"
34 #include "wx/listbox.h"
35 #endif //WX_PRECOMP
36
37 #include "wx/dcbuffer.h"
38 #include "wx/selstore.h"
39 #include "wx/renderer.h"
40
41 // ----------------------------------------------------------------------------
42 // event tables
43 // ----------------------------------------------------------------------------
44
45 BEGIN_EVENT_TABLE(wxVListBox, wxVScrolledWindow)
46 EVT_PAINT(wxVListBox::OnPaint)
47
48 EVT_KEY_DOWN(wxVListBox::OnKeyDown)
49 EVT_LEFT_DOWN(wxVListBox::OnLeftDown)
50 EVT_LEFT_DCLICK(wxVListBox::OnLeftDClick)
51
52 EVT_SET_FOCUS(wxVListBox::OnSetOrKillFocus)
53 EVT_KILL_FOCUS(wxVListBox::OnSetOrKillFocus)
54
55 EVT_SIZE(wxVListBox::OnSize)
56 END_EVENT_TABLE()
57
58 // ============================================================================
59 // implementation
60 // ============================================================================
61
62 IMPLEMENT_ABSTRACT_CLASS(wxVListBox, wxVScrolledWindow)
63 const char wxVListBoxNameStr[] = "wxVListBox";
64
65 // ----------------------------------------------------------------------------
66 // wxVListBox creation
67 // ----------------------------------------------------------------------------
68
69 void wxVListBox::Init()
70 {
71 m_current =
72 m_anchor = wxNOT_FOUND;
73 m_selStore = NULL;
74 }
75
76 bool wxVListBox::Create(wxWindow *parent,
77 wxWindowID id,
78 const wxPoint& pos,
79 const wxSize& size,
80 long style,
81 const wxString& name)
82 {
83 #ifdef __WXMSW__
84 if ( (style & wxBORDER_MASK) == wxDEFAULT )
85 style |= wxBORDER_THEME;
86 #endif
87
88 style |= wxWANTS_CHARS | wxFULL_REPAINT_ON_RESIZE;
89 if ( !wxVScrolledWindow::Create(parent, id, pos, size, style, name) )
90 return false;
91
92 if ( style & wxLB_MULTIPLE )
93 m_selStore = new wxSelectionStore;
94
95 // make sure the native widget has the right colour since we do
96 // transparent drawing by default
97 SetBackgroundColour(GetBackgroundColour());
98
99 // leave m_colBgSel in an invalid state: it means for OnDrawBackground()
100 // to use wxRendererNative instead of painting selection bg ourselves
101 m_colBgSel = wxNullColour;
102
103 // flicker-free drawing requires this
104 SetBackgroundStyle(wxBG_STYLE_CUSTOM);
105
106 return true;
107 }
108
109 wxVListBox::~wxVListBox()
110 {
111 delete m_selStore;
112 }
113
114 void wxVListBox::SetItemCount(size_t count)
115 {
116 // don't leave the current index invalid
117 if ( m_current != wxNOT_FOUND && (size_t)m_current >= count )
118 m_current = count - 1; // also ok when count == 0 as wxNOT_FOUND == -1
119
120 if ( m_selStore )
121 {
122 // tell the selection store that our number of items has changed
123 m_selStore->SetItemCount(count);
124 }
125
126 SetRowCount(count);
127 }
128
129 // ----------------------------------------------------------------------------
130 // selection handling
131 // ----------------------------------------------------------------------------
132
133 bool wxVListBox::IsSelected(size_t line) const
134 {
135 return m_selStore ? m_selStore->IsSelected(line) : (int)line == m_current;
136 }
137
138 bool wxVListBox::Select(size_t item, bool select)
139 {
140 wxCHECK_MSG( m_selStore, false,
141 wxT("Select() may only be used with multiselection listbox") );
142
143 wxCHECK_MSG( item < GetItemCount(), false,
144 wxT("Select(): invalid item index") );
145
146 bool changed = m_selStore->SelectItem(item, select);
147 if ( changed )
148 {
149 // selection really changed
150 RefreshRow(item);
151 }
152
153 DoSetCurrent(item);
154
155 return changed;
156 }
157
158 bool wxVListBox::SelectRange(size_t from, size_t to)
159 {
160 wxCHECK_MSG( m_selStore, false,
161 wxT("SelectRange() may only be used with multiselection listbox") );
162
163 // make sure items are in correct order
164 if ( from > to )
165 {
166 size_t tmp = from;
167 from = to;
168 to = tmp;
169 }
170
171 wxCHECK_MSG( to < GetItemCount(), false,
172 wxT("SelectRange(): invalid item index") );
173
174 wxArrayInt changed;
175 if ( !m_selStore->SelectRange(from, to, true, &changed) )
176 {
177 // too many items have changed, we didn't record them in changed array
178 // so we have no choice but to refresh everything between from and to
179 RefreshRows(from, to);
180 }
181 else // we've got the indices of the changed items
182 {
183 const size_t count = changed.GetCount();
184 if ( !count )
185 {
186 // nothing changed
187 return false;
188 }
189
190 // refresh just the lines which have really changed
191 for ( size_t n = 0; n < count; n++ )
192 {
193 RefreshRow(changed[n]);
194 }
195 }
196
197 // something changed
198 return true;
199 }
200
201 bool wxVListBox::DoSelectAll(bool select)
202 {
203 wxCHECK_MSG( m_selStore, false,
204 wxT("SelectAll may only be used with multiselection listbox") );
205
206 size_t count = GetItemCount();
207 if ( count )
208 {
209 wxArrayInt changed;
210 if ( !m_selStore->SelectRange(0, count - 1, select) ||
211 !changed.IsEmpty() )
212 {
213 Refresh();
214
215 // something changed
216 return true;
217 }
218 }
219
220 return false;
221 }
222
223 bool wxVListBox::DoSetCurrent(int current)
224 {
225 wxASSERT_MSG( current == wxNOT_FOUND ||
226 (current >= 0 && (size_t)current < GetItemCount()),
227 wxT("wxVListBox::DoSetCurrent(): invalid item index") );
228
229 if ( current == m_current )
230 {
231 // nothing to do
232 return false;
233 }
234
235 if ( m_current != wxNOT_FOUND )
236 RefreshRow(m_current);
237
238 m_current = current;
239
240 if ( m_current != wxNOT_FOUND )
241 {
242 // if the line is not visible at all, we scroll it into view but we
243 // don't need to refresh it -- it will be redrawn anyhow
244 if ( !IsVisible(m_current) )
245 {
246 ScrollToRow(m_current);
247 }
248 else // line is at least partly visible
249 {
250 // it is, indeed, only partly visible, so scroll it into view to
251 // make it entirely visible
252 while ( (size_t)m_current + 1 == GetVisibleRowsEnd() &&
253 ScrollToRow(GetVisibleBegin() + 1) ) ;
254
255 // but in any case refresh it as even if it was only partly visible
256 // before we need to redraw it entirely as its background changed
257 RefreshRow(m_current);
258 }
259 }
260
261 return true;
262 }
263
264 void wxVListBox::InitEvent(wxCommandEvent& event, int n)
265 {
266 event.SetEventObject(this);
267 event.SetInt(n);
268 }
269
270 void wxVListBox::SendSelectedEvent()
271 {
272 wxASSERT_MSG( m_current != wxNOT_FOUND,
273 wxT("SendSelectedEvent() shouldn't be called") );
274
275 wxCommandEvent event(wxEVT_COMMAND_LISTBOX_SELECTED, GetId());
276 InitEvent(event, m_current);
277 (void)GetEventHandler()->ProcessEvent(event);
278 }
279
280 void wxVListBox::SetSelection(int selection)
281 {
282 wxCHECK_RET( selection == wxNOT_FOUND ||
283 (selection >= 0 && (size_t)selection < GetItemCount()),
284 wxT("wxVListBox::SetSelection(): invalid item index") );
285
286 if ( HasMultipleSelection() )
287 {
288 if (selection != wxNOT_FOUND)
289 Select(selection);
290 else
291 DeselectAll();
292 m_anchor = selection;
293 }
294
295 DoSetCurrent(selection);
296 }
297
298 size_t wxVListBox::GetSelectedCount() const
299 {
300 return m_selStore ? m_selStore->GetSelectedCount()
301 : m_current == wxNOT_FOUND ? 0 : 1;
302 }
303
304 int wxVListBox::GetFirstSelected(unsigned long& cookie) const
305 {
306 cookie = 0;
307
308 return GetNextSelected(cookie);
309 }
310
311 int wxVListBox::GetNextSelected(unsigned long& cookie) const
312 {
313 wxCHECK_MSG( m_selStore, wxNOT_FOUND,
314 wxT("GetFirst/NextSelected() may only be used with multiselection listboxes") );
315
316 while ( cookie < GetItemCount() )
317 {
318 if ( IsSelected(cookie++) )
319 return cookie - 1;
320 }
321
322 return wxNOT_FOUND;
323 }
324
325 void wxVListBox::RefreshSelected()
326 {
327 // only refresh those items which are currently visible and selected:
328 for ( size_t n = GetVisibleBegin(), end = GetVisibleEnd(); n < end; n++ )
329 {
330 if ( IsSelected(n) )
331 RefreshRow(n);
332 }
333 }
334
335 wxRect wxVListBox::GetItemRect(size_t n) const
336 {
337 wxRect itemrect;
338
339 // check that this item is visible
340 const size_t lineMax = GetVisibleEnd();
341 if ( n >= lineMax )
342 return itemrect;
343 size_t line = GetVisibleBegin();
344 if ( n < line )
345 return itemrect;
346
347 while ( line <= n )
348 {
349 itemrect.y += itemrect.height;
350 itemrect.height = OnGetRowHeight(line);
351
352 line++;
353 }
354
355 itemrect.width = GetClientSize().x;
356
357 return itemrect;
358 }
359
360 // ----------------------------------------------------------------------------
361 // wxVListBox appearance parameters
362 // ----------------------------------------------------------------------------
363
364 void wxVListBox::SetMargins(const wxPoint& pt)
365 {
366 if ( pt != m_ptMargins )
367 {
368 m_ptMargins = pt;
369
370 Refresh();
371 }
372 }
373
374 void wxVListBox::SetSelectionBackground(const wxColour& col)
375 {
376 m_colBgSel = col;
377 }
378
379 // ----------------------------------------------------------------------------
380 // wxVListBox painting
381 // ----------------------------------------------------------------------------
382
383 wxCoord wxVListBox::OnGetRowHeight(size_t line) const
384 {
385 return OnMeasureItem(line) + 2*m_ptMargins.y;
386 }
387
388 void wxVListBox::OnDrawSeparator(wxDC& WXUNUSED(dc),
389 wxRect& WXUNUSED(rect),
390 size_t WXUNUSED(n)) const
391 {
392 }
393
394 bool
395 wxVListBox::DoDrawSolidBackground(const wxColour& col,
396 wxDC& dc,
397 const wxRect& rect,
398 size_t n) const
399 {
400 if ( !col.IsOk() )
401 return false;
402
403 // we need to render selected and current items differently
404 const bool isSelected = IsSelected(n),
405 isCurrent = IsCurrent(n);
406 if ( isSelected || isCurrent )
407 {
408 if ( isSelected )
409 {
410 dc.SetBrush(wxBrush(col, wxBRUSHSTYLE_SOLID));
411 }
412 else // !selected
413 {
414 dc.SetBrush(*wxTRANSPARENT_BRUSH);
415 }
416 dc.SetPen(*(isCurrent ? wxBLACK_PEN : wxTRANSPARENT_PEN));
417 dc.DrawRectangle(rect);
418 }
419 //else: do nothing for the normal items
420
421 return true;
422 }
423
424 void wxVListBox::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const
425 {
426 // use wxRendererNative for more native look unless we use custom bg colour
427 if ( !DoDrawSolidBackground(m_colBgSel, dc, rect, n) )
428 {
429 int flags = 0;
430 if ( IsSelected(n) )
431 flags |= wxCONTROL_SELECTED;
432 if ( IsCurrent(n) )
433 flags |= wxCONTROL_CURRENT;
434 if ( wxWindow::FindFocus() == const_cast<wxVListBox*>(this) )
435 flags |= wxCONTROL_FOCUSED;
436
437 wxRendererNative::Get().DrawItemSelectionRect(
438 const_cast<wxVListBox *>(this), dc, rect, flags);
439 }
440 }
441
442 void wxVListBox::OnPaint(wxPaintEvent& WXUNUSED(event))
443 {
444 wxSize clientSize = GetClientSize();
445
446 wxAutoBufferedPaintDC dc(this);
447
448 // the update rectangle
449 wxRect rectUpdate = GetUpdateClientRect();
450
451 // fill it with background colour
452 dc.SetBackground(GetBackgroundColour());
453 dc.Clear();
454
455 // the bounding rectangle of the current line
456 wxRect rectRow;
457 rectRow.width = clientSize.x;
458
459 // iterate over all visible lines
460 const size_t lineMax = GetVisibleEnd();
461 for ( size_t line = GetVisibleBegin(); line < lineMax; line++ )
462 {
463 const wxCoord hRow = OnGetRowHeight(line);
464
465 rectRow.height = hRow;
466
467 // and draw the ones which intersect the update rect
468 if ( rectRow.Intersects(rectUpdate) )
469 {
470 // don't allow drawing outside of the lines rectangle
471 wxDCClipper clip(dc, rectRow);
472
473 wxRect rect = rectRow;
474 OnDrawBackground(dc, rect, line);
475
476 OnDrawSeparator(dc, rect, line);
477
478 rect.Deflate(m_ptMargins.x, m_ptMargins.y);
479 OnDrawItem(dc, rect, line);
480 }
481 else // no intersection
482 {
483 if ( rectRow.GetTop() > rectUpdate.GetBottom() )
484 {
485 // we are already below the update rect, no need to continue
486 // further
487 break;
488 }
489 //else: the next line may intersect the update rect
490 }
491
492 rectRow.y += hRow;
493 }
494 }
495
496 void wxVListBox::OnSetOrKillFocus(wxFocusEvent& WXUNUSED(event))
497 {
498 // we need to repaint the selection when we get the focus since
499 // wxRendererNative in general draws the focused selection differently
500 // from the unfocused selection (see OnDrawItem):
501 RefreshSelected();
502 }
503
504 void wxVListBox::OnSize(wxSizeEvent& event)
505 {
506 UpdateScrollbar();
507 event.Skip();
508 }
509
510 // ============================================================================
511 // wxVListBox keyboard/mouse handling
512 // ============================================================================
513
514 void wxVListBox::DoHandleItemClick(int item, int flags)
515 {
516 // has anything worth telling the client code about happened?
517 bool notify = false;
518
519 if ( HasMultipleSelection() )
520 {
521 // select the iteem clicked?
522 bool select = true;
523
524 // NB: the keyboard interface we implement here corresponds to
525 // wxLB_EXTENDED rather than wxLB_MULTIPLE but this one makes more
526 // sense IMHO
527 if ( flags & ItemClick_Shift )
528 {
529 if ( m_current != wxNOT_FOUND )
530 {
531 if ( m_anchor == wxNOT_FOUND )
532 m_anchor = m_current;
533
534 select = false;
535
536 // only the range from the selection anchor to new m_current
537 // must be selected
538 if ( DeselectAll() )
539 notify = true;
540
541 if ( SelectRange(m_anchor, item) )
542 notify = true;
543 }
544 //else: treat it as ordinary click/keypress
545 }
546 else // Shift not pressed
547 {
548 m_anchor = item;
549
550 if ( flags & ItemClick_Ctrl )
551 {
552 select = false;
553
554 if ( !(flags & ItemClick_Kbd) )
555 {
556 Toggle(item);
557
558 // the status of the item has definitely changed
559 notify = true;
560 }
561 //else: Ctrl-arrow pressed, don't change selection
562 }
563 //else: behave as in single selection case
564 }
565
566 if ( select )
567 {
568 // make the clicked item the only selection
569 if ( DeselectAll() )
570 notify = true;
571
572 if ( Select(item) )
573 notify = true;
574 }
575 }
576
577 // in any case the item should become the current one
578 if ( DoSetCurrent(item) )
579 {
580 if ( !HasMultipleSelection() )
581 {
582 // this has also changed the selection for single selection case
583 notify = true;
584 }
585 }
586
587 if ( notify )
588 {
589 // notify the user about the selection change
590 SendSelectedEvent();
591 }
592 //else: nothing changed at all
593 }
594
595 // ----------------------------------------------------------------------------
596 // keyboard handling
597 // ----------------------------------------------------------------------------
598
599 void wxVListBox::OnKeyDown(wxKeyEvent& event)
600 {
601 // flags for DoHandleItemClick()
602 int flags = ItemClick_Kbd;
603
604 int current;
605 switch ( event.GetKeyCode() )
606 {
607 case WXK_HOME:
608 current = 0;
609 break;
610
611 case WXK_END:
612 current = GetRowCount() - 1;
613 break;
614
615 case WXK_DOWN:
616 if ( m_current == (int)GetRowCount() - 1 )
617 return;
618
619 current = m_current + 1;
620 break;
621
622 case WXK_UP:
623 if ( m_current == wxNOT_FOUND )
624 current = GetRowCount() - 1;
625 else if ( m_current != 0 )
626 current = m_current - 1;
627 else // m_current == 0
628 return;
629 break;
630
631 case WXK_PAGEDOWN:
632 PageDown();
633 current = GetVisibleBegin();
634 break;
635
636 case WXK_PAGEUP:
637 if ( m_current == (int)GetVisibleBegin() )
638 {
639 PageUp();
640 }
641
642 current = GetVisibleBegin();
643 break;
644
645 case WXK_SPACE:
646 // hack: pressing space should work like a mouse click rather than
647 // like a keyboard arrow press, so trick DoHandleItemClick() in
648 // thinking we were clicked
649 flags &= ~ItemClick_Kbd;
650 current = m_current;
651 break;
652
653 #ifdef __WXMSW__
654 case WXK_TAB:
655 // Since we are using wxWANTS_CHARS we need to send navigation
656 // events for the tabs on MSW
657 HandleAsNavigationKey(event);
658 // fall through to default
659 #endif
660 default:
661 event.Skip();
662 current = 0; // just to silent the stupid compiler warnings
663 wxUnusedVar(current);
664 return;
665 }
666
667 if ( event.ShiftDown() )
668 flags |= ItemClick_Shift;
669 if ( event.ControlDown() )
670 flags |= ItemClick_Ctrl;
671
672 DoHandleItemClick(current, flags);
673 }
674
675 // ----------------------------------------------------------------------------
676 // wxVListBox mouse handling
677 // ----------------------------------------------------------------------------
678
679 void wxVListBox::OnLeftDown(wxMouseEvent& event)
680 {
681 SetFocus();
682
683 int item = VirtualHitTest(event.GetPosition().y);
684
685 if ( item != wxNOT_FOUND )
686 {
687 int flags = 0;
688 if ( event.ShiftDown() )
689 flags |= ItemClick_Shift;
690
691 // under Mac Apple-click is used in the same way as Ctrl-click
692 // elsewhere
693 #ifdef __WXMAC__
694 if ( event.MetaDown() )
695 #else
696 if ( event.ControlDown() )
697 #endif
698 flags |= ItemClick_Ctrl;
699
700 DoHandleItemClick(item, flags);
701 }
702 }
703
704 void wxVListBox::OnLeftDClick(wxMouseEvent& eventMouse)
705 {
706 int item = VirtualHitTest(eventMouse.GetPosition().y);
707 if ( item != wxNOT_FOUND )
708 {
709
710 // if item double-clicked was not yet selected, then treat
711 // this event as a left-click instead
712 if ( item == m_current )
713 {
714 wxCommandEvent event(wxEVT_COMMAND_LISTBOX_DOUBLECLICKED, GetId());
715 InitEvent(event, item);
716 (void)GetEventHandler()->ProcessEvent(event);
717 }
718 else
719 {
720 OnLeftDown(eventMouse);
721 }
722
723 }
724 }
725
726
727 // ----------------------------------------------------------------------------
728 // use the same default attributes as wxListBox
729 // ----------------------------------------------------------------------------
730
731 //static
732 wxVisualAttributes
733 wxVListBox::GetClassDefaultAttributes(wxWindowVariant variant)
734 {
735 return wxListBox::GetClassDefaultAttributes(variant);
736 }
737
738 #endif