made multiple selection behave more consistently with the usual (Windows) way
[wxWidgets.git] / src / generic / vlbox.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // Name: 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 #ifndef WX_PRECOMP
28 #include "wx/settings.h"
29 #include "wx/dcclient.h"
30 #endif //WX_PRECOMP
31
32 #include "wx/vlbox.h"
33 #include "wx/selstore.h"
34
35 // ----------------------------------------------------------------------------
36 // event tables
37 // ----------------------------------------------------------------------------
38
39 BEGIN_EVENT_TABLE(wxVListBox, wxVScrolledWindow)
40 EVT_PAINT(wxVListBox::OnPaint)
41
42 EVT_KEY_DOWN(wxVListBox::OnKeyDown)
43 EVT_LEFT_DOWN(wxVListBox::OnLeftDown)
44 EVT_LEFT_DCLICK(wxVListBox::OnLeftDClick)
45 END_EVENT_TABLE()
46
47 // ============================================================================
48 // implementation
49 // ============================================================================
50
51 // ----------------------------------------------------------------------------
52 // wxVListBox creation
53 // ----------------------------------------------------------------------------
54
55 void wxVListBox::Init()
56 {
57 m_current =
58 m_anchor = wxNOT_FOUND;
59 m_selStore = NULL;
60 }
61
62 bool wxVListBox::Create(wxWindow *parent,
63 wxWindowID id,
64 const wxPoint& pos,
65 const wxSize& size,
66 long style,
67 const wxString& name)
68 {
69 if ( !wxVScrolledWindow::Create(parent, id, pos, size, style, name) )
70 return false;
71
72 SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX));
73
74 if ( style & wxLB_MULTIPLE )
75 m_selStore = new wxSelectionStore;
76
77 return true;
78 }
79
80 wxVListBox::~wxVListBox()
81 {
82 delete m_selStore;
83 }
84
85 void wxVListBox::SetItemCount(size_t count)
86 {
87 if ( m_selStore )
88 {
89 // tell the selection store that our number of items has changed
90 m_selStore->SetItemCount(count);
91 }
92
93 SetLineCount(count);
94 }
95
96 // ----------------------------------------------------------------------------
97 // selection handling
98 // ----------------------------------------------------------------------------
99
100 bool wxVListBox::IsSelected(size_t line) const
101 {
102 return m_selStore ? m_selStore->IsSelected(line) : (int)line == m_current;
103 }
104
105 bool wxVListBox::Select(size_t item, bool select)
106 {
107 wxCHECK_MSG( m_selStore, false,
108 _T("Select() may only be used with multiselection listbox") );
109
110 wxCHECK_MSG( item < GetItemCount(), false,
111 _T("Select(): invalid item index") );
112
113 bool changed = m_selStore->SelectItem(item, select);
114 if ( changed )
115 {
116 // selection really changed
117 RefreshLine(item);
118 }
119
120 DoSetCurrent(item);
121
122 return changed;
123 }
124
125 bool wxVListBox::SelectRange(size_t from, size_t to)
126 {
127 wxCHECK_MSG( m_selStore, false,
128 _T("SelectRange() may only be used with multiselection listbox") );
129
130 // make sure items are in correct order
131 if ( from > to )
132 {
133 size_t tmp = from;
134 from = to;
135 to = tmp;
136 }
137
138 wxCHECK_MSG( to < GetItemCount(), false,
139 _T("SelectRange(): invalid item index") );
140
141 wxArrayInt changed;
142 if ( !m_selStore->SelectRange(from, to, true, &changed) )
143 {
144 // too many items have changed, we didn't record them in changed array
145 // so we have no choice but to refresh everything between from and to
146 RefreshLines(from, to);
147 }
148 else // we've got the indices of the changed items
149 {
150 const size_t count = changed.GetCount();
151 if ( !count )
152 {
153 // nothing changed
154 return false;
155 }
156
157 // refresh just the lines which have really changed
158 for ( size_t n = 0; n < count; n++ )
159 {
160 RefreshLine(changed[n]);
161 }
162 }
163
164 // something changed
165 return true;
166 }
167
168 bool wxVListBox::DoSelectAll(bool select)
169 {
170 wxCHECK_MSG( m_selStore, false,
171 _T("SelectAll may only be used with multiselection listbox") );
172
173 size_t count = GetItemCount();
174 if ( count )
175 {
176 wxArrayInt changed;
177 if ( !m_selStore->SelectRange(0, count - 1, select) ||
178 !changed.IsEmpty() )
179 {
180 Refresh();
181
182 // something changed
183 return true;
184 }
185 }
186
187 return false;
188 }
189
190 bool wxVListBox::DoSetCurrent(int current)
191 {
192 wxASSERT_MSG( current == wxNOT_FOUND ||
193 (current >= 0 && (size_t)current < GetItemCount()),
194 _T("wxVListBox::DoSetCurrent(): invalid item index") );
195
196 if ( current == m_current )
197 {
198 // nothing to do
199 return false;
200 }
201
202 if ( m_current != wxNOT_FOUND )
203 RefreshLine(m_current);
204
205 m_current = current;
206
207 if ( m_current != wxNOT_FOUND )
208 {
209 // if the line is not visible at all, we scroll it into view but we
210 // don't need to refresh it -- it will be redrawn anyhow
211 if ( !IsVisible(m_current) )
212 {
213 ScrollToLine(m_current);
214 }
215 else // line is at least partly visible
216 {
217 // it is, indeed, only partly visible, so scroll it into view to
218 // make it entirely visible
219 if ( (size_t)m_current == GetLastVisibleLine() )
220 {
221 ScrollToLine(m_current);
222 }
223
224 // but in any case refresh it as even if it was only partly visible
225 // before we need to redraw it entirely as its background changed
226 RefreshLine(m_current);
227 }
228 }
229
230 return true;
231 }
232
233 void wxVListBox::SendSelectedEvent()
234 {
235 wxASSERT_MSG( m_current != wxNOT_FOUND,
236 _T("SendSelectedEvent() shouldn't be called") );
237
238 wxCommandEvent event(wxEVT_COMMAND_LISTBOX_SELECTED, GetId());
239 event.SetEventObject(this);
240 event.m_commandInt = m_current;
241
242 (void)GetEventHandler()->ProcessEvent(event);
243 }
244
245 void wxVListBox::SetSelection(int selection)
246 {
247 wxCHECK_RET( selection == wxNOT_FOUND ||
248 (selection >= 0 && (size_t)selection < GetItemCount()),
249 _T("wxVListBox::SetSelection(): invalid item index") );
250
251 wxASSERT_MSG( !HasMultipleSelection(),
252 _T("SetSelection() is invalid with multiselection listbox") );
253
254 DoSetCurrent(selection);
255 }
256
257 size_t wxVListBox::GetSelectedCount() const
258 {
259 return m_selStore ? m_selStore->GetSelectedCount()
260 : m_current == wxNOT_FOUND ? 0 : 1;
261 }
262
263 int wxVListBox::GetFirstSelected(unsigned long& cookie) const
264 {
265 cookie = 0;
266
267 return GetNextSelected(cookie);
268 }
269
270 int wxVListBox::GetNextSelected(unsigned long& cookie) const
271 {
272 wxCHECK_MSG( m_selStore, wxNOT_FOUND,
273 _T("GetFirst/NextSelected() may only be used with multiselection listboxes") );
274
275 while ( cookie < GetItemCount() )
276 {
277 if ( IsSelected(cookie++) )
278 return cookie - 1;
279 }
280
281 return wxNOT_FOUND;
282 }
283
284 // ----------------------------------------------------------------------------
285 // wxVListBox painting
286 // ----------------------------------------------------------------------------
287
288 void wxVListBox::SetMargins(const wxPoint& pt)
289 {
290 if ( pt != m_ptMargins )
291 {
292 m_ptMargins = pt;
293
294 Refresh();
295 }
296 }
297
298 wxCoord wxVListBox::OnGetLineHeight(size_t line) const
299 {
300 return OnMeasureItem(line) + 2*m_ptMargins.y;
301 }
302
303 void wxVListBox::OnDrawSeparator(wxDC& WXUNUSED(dc),
304 wxRect& WXUNUSED(rect),
305 size_t WXUNUSED(n)) const
306 {
307 }
308
309 void wxVListBox::OnPaint(wxPaintEvent& event)
310 {
311 wxPaintDC dc(this);
312
313 // the update rectangle
314 wxRect rectUpdate = GetUpdateClientRect();
315
316 // the bounding rectangle of the current line
317 wxRect rectLine;
318 rectLine.width = GetClientSize().x;
319
320 // iterate over all visible lines
321 const size_t lineMax = GetLastVisibleLine();
322 for ( size_t line = GetFirstVisibleLine(); line <= lineMax; line++ )
323 {
324 const wxCoord hLine = OnGetLineHeight(line);
325
326 rectLine.height = hLine;
327
328 // and draw the ones which intersect the update rect
329 if ( rectLine.Intersects(rectUpdate) )
330 {
331 // don't allow drawing outside of the lines rectangle
332 wxDCClipper clip(dc, rectLine);
333
334 // we need to render selected and current items differently
335 const bool isSelected = IsSelected(line);
336 if ( isSelected || IsCurrent(line) )
337 {
338 if ( isSelected )
339 {
340 wxBrush brush(wxSystemSettings::
341 GetColour(wxSYS_COLOUR_HIGHLIGHT),
342 wxSOLID);
343 dc.SetBrush(brush);
344 }
345 else // !selected
346 {
347 dc.SetBrush(*wxTRANSPARENT_BRUSH);
348 }
349
350 dc.SetPen(*(IsCurrent(line) ? wxBLACK_PEN : wxTRANSPARENT_PEN));
351
352 dc.DrawRectangle(rectLine);
353 }
354
355 wxRect rect = rectLine;
356 OnDrawSeparator(dc, rect, line);
357
358 rect.Deflate(m_ptMargins.x, m_ptMargins.y);
359 OnDrawItem(dc, rect, line);
360 }
361 else // no intersection
362 {
363 if ( rectLine.GetTop() > rectUpdate.GetBottom() )
364 {
365 // we are already below the update rect, no need to continue
366 // further
367 break;
368 }
369 //else: the next line may intersect the update rect
370 }
371
372 rectLine.y += hLine;
373 }
374 }
375
376 // ============================================================================
377 // wxVListBox keyboard/mouse handling
378 // ============================================================================
379
380 void wxVListBox::DoHandleItemClick(int item, int flags)
381 {
382 // has anything worth telling the client code about happened?
383 bool notify = false;
384
385 if ( HasMultipleSelection() )
386 {
387 // select the iteem clicked?
388 bool select = true;
389
390 // NB: the keyboard interface we implement here corresponds to
391 // wxLB_EXTENDED rather than wxLB_MULTIPLE but this one makes more
392 // sense IMHO
393 if ( flags & ItemClick_Shift )
394 {
395 if ( m_current != wxNOT_FOUND )
396 {
397 if ( m_anchor == wxNOT_FOUND )
398 m_anchor = m_current;
399
400 select = false;
401
402 // only the range from the selection anchor to new m_current
403 // must be selected
404 if ( DeselectAll() )
405 notify = true;
406
407 if ( SelectRange(m_anchor, item) )
408 notify = true;
409 }
410 //else: treat it as ordinary click/keypress
411 }
412 else // Shift not pressed
413 {
414 m_anchor = item;
415
416 if ( flags & ItemClick_Ctrl )
417 {
418 select = false;
419
420 if ( !(flags & ItemClick_Kbd) )
421 {
422 Toggle(item);
423
424 // the status of the item has definitely changed
425 notify = true;
426 }
427 //else: Ctrl-arrow pressed, don't change selection
428 }
429 //else: behave as in single selection case
430 }
431
432 if ( select )
433 {
434 // make the clicked item the only selection
435 if ( DeselectAll() )
436 notify = true;
437
438 if ( Select(item) )
439 notify = true;
440 }
441 }
442
443 // in any case the item should become the current one
444 if ( DoSetCurrent(item) )
445 {
446 if ( !HasMultipleSelection() )
447 {
448 // this has also changed the selection for single selection case
449 notify = true;
450 }
451 }
452
453 if ( notify )
454 {
455 // notify the user about the selection change
456 SendSelectedEvent();
457 }
458 //else: nothing changed at all
459 }
460
461 // ----------------------------------------------------------------------------
462 // keyboard handling
463 // ----------------------------------------------------------------------------
464
465 void wxVListBox::OnKeyDown(wxKeyEvent& event)
466 {
467 // flags for DoHandleItemClick()
468 int flags = ItemClick_Kbd;
469
470 int current = 0; // just to silent the stupid compiler warnings
471 switch ( event.GetKeyCode() )
472 {
473 case WXK_HOME:
474 current = 0;
475 break;
476
477 case WXK_END:
478 current = GetLineCount() - 1;
479 break;
480
481 case WXK_DOWN:
482 if ( m_current == (int)GetLineCount() - 1 )
483 return;
484
485 current = m_current + 1;
486 break;
487
488 case WXK_UP:
489 if ( m_current == wxNOT_FOUND )
490 current = GetLineCount() - 1;
491 else if ( m_current != 0 )
492 current = m_current - 1;
493 else // m_current == 0
494 return;
495 break;
496
497 case WXK_PAGEDOWN:
498 case WXK_NEXT:
499 PageDown();
500 current = GetFirstVisibleLine();
501 break;
502
503 case WXK_PAGEUP:
504 case WXK_PRIOR:
505 if ( m_current == (int)GetFirstVisibleLine() )
506 {
507 PageUp();
508 }
509
510 current = GetFirstVisibleLine();
511 break;
512
513 case WXK_SPACE:
514 // hack: pressing space should work like a mouse click rather than
515 // like a keyboard arrow press, so trick DoHandleItemClick() in
516 // thinking we were clicked
517 flags &= ~ItemClick_Kbd;
518 current = m_current;
519 break;
520
521 default:
522 event.Skip();
523 return;
524 }
525
526 if ( event.ShiftDown() )
527 flags |= ItemClick_Shift;
528 if ( event.ControlDown() )
529 flags |= ItemClick_Ctrl;
530
531 DoHandleItemClick(current, flags);
532 }
533
534 // ----------------------------------------------------------------------------
535 // wxVListBox mouse handling
536 // ----------------------------------------------------------------------------
537
538 void wxVListBox::OnLeftDown(wxMouseEvent& event)
539 {
540 int item = HitTest(event.GetPosition());
541
542 if ( item != wxNOT_FOUND )
543 {
544 int flags = 0;
545 if ( event.ShiftDown() )
546 flags |= ItemClick_Shift;
547
548 // under Mac Apple-click is used in the same way as Ctrl-click
549 // elsewhere
550 #ifdef __WXMAC__
551 if ( event.MetaDown() )
552 #else
553 if ( event.ControlDown() )
554 #endif
555 flags |= ItemClick_Ctrl;
556
557 DoHandleItemClick(item, flags);
558 }
559 }
560
561 void wxVListBox::OnLeftDClick(wxMouseEvent& event)
562 {
563 int item = HitTest(event.GetPosition());
564 if ( item != wxNOT_FOUND )
565 {
566 wxCommandEvent event(wxEVT_COMMAND_LISTBOX_DOUBLECLICKED, GetId());
567 event.SetEventObject(this);
568 event.m_commandInt = item;
569
570 (void)GetEventHandler()->ProcessEvent(event);
571 }
572 }
573