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