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