using common notification mechanism for selection changes (key or mouse), see #10406
[wxWidgets.git] / src / osx / cocoa / listbox.mm
1 ///////////////////////////////////////////////////////////////////////////////
2 // Name:        src/osx/cocoa/listbox.mm
3 // Purpose:     wxListBox
4 // Author:      Stefan Csomor
5 // Modified by:
6 // Created:     1998-01-01
7 // RCS-ID:      $Id: listbox.cpp 54820 2008-07-29 20:04:11Z SC $
8 // Copyright:   (c) Stefan Csomor
9 // Licence:     wxWindows licence
10 ///////////////////////////////////////////////////////////////////////////////
11
12 #include "wx/wxprec.h"
13
14 #if wxUSE_LISTBOX
15
16 #include "wx/listbox.h"
17 #include "wx/dnd.h"
18
19 #ifndef WX_PRECOMP
20     #include "wx/log.h"
21     #include "wx/intl.h"
22     #include "wx/utils.h"
23     #include "wx/settings.h"
24     #include "wx/arrstr.h"
25     #include "wx/dcclient.h"
26 #endif
27
28 #include "wx/osx/private.h"
29
30 #include <vector>
31
32 // forward decls
33
34 class wxListWidgetCocoaImpl;
35
36 @interface wxNSTableDataSource : NSObject
37 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
38     <NSTableViewDataSource>
39 #endif
40 {
41     wxListWidgetCocoaImpl* impl;
42 }
43
44 - (id)tableView:(NSTableView *)aTableView
45         objectValueForTableColumn:(NSTableColumn *)aTableColumn
46         row:(NSInteger)rowIndex;
47
48 - (void)tableView:(NSTableView *)aTableView
49         setObjectValue:(id)value forTableColumn:(NSTableColumn *)aTableColumn
50         row:(NSInteger)rowIndex;
51
52 - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView;
53
54 - (void)setImplementation: (wxListWidgetCocoaImpl *) theImplementation;
55 - (wxListWidgetCocoaImpl*) implementation;
56
57 @end
58
59 @interface wxNSTableView : NSTableView
60 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
61     <NSTableViewDelegate>
62 #endif
63 {
64 }
65
66 @end
67
68 //
69 // table column
70 //
71
72 class wxCocoaTableColumn;
73
74 @interface wxNSTableColumn : NSTableColumn
75 {
76     wxCocoaTableColumn* column;
77 }
78
79 - (void) setColumn: (wxCocoaTableColumn*) col;
80
81 - (wxCocoaTableColumn*) column;
82
83 @end
84
85 class WXDLLIMPEXP_CORE wxCocoaTableColumn : public wxListWidgetColumn
86 {
87 public :
88     wxCocoaTableColumn( wxNSTableColumn* column, bool editable )
89         : m_column( column ), m_editable(editable)
90     {
91     }
92
93     ~wxCocoaTableColumn()
94     {
95     }
96
97     wxNSTableColumn* GetNSTableColumn() const { return m_column ; }
98
99     bool IsEditable() const { return m_editable; }
100
101 protected :
102     wxNSTableColumn* m_column;
103     bool m_editable;
104 } ;
105
106 NSString* column1 = @"1";
107
108 class wxListWidgetCocoaImpl : public wxWidgetCocoaImpl, public wxListWidgetImpl
109 {
110 public :
111     wxListWidgetCocoaImpl( wxWindowMac* peer, NSScrollView* view, wxNSTableView* tableview, wxNSTableDataSource* data );
112
113     ~wxListWidgetCocoaImpl();
114
115     virtual wxListWidgetColumn*     InsertTextColumn( unsigned pos, const wxString& title, bool editable = false,
116                                 wxAlignment just = wxALIGN_LEFT , int defaultWidth = -1)  ;
117     virtual wxListWidgetColumn*     InsertCheckColumn( unsigned pos , const wxString& title, bool editable = false,
118                                 wxAlignment just = wxALIGN_LEFT , int defaultWidth =  -1)  ;
119
120     // add and remove
121
122     virtual void            ListDelete( unsigned int n ) ;
123     virtual void            ListInsert( unsigned int n ) ;
124     virtual void            ListClear() ;
125
126     // selecting
127
128     virtual void            ListDeselectAll();
129
130     virtual void            ListSetSelection( unsigned int n, bool select, bool multi ) ;
131     virtual int             ListGetSelection() const ;
132
133     virtual int             ListGetSelections( wxArrayInt& aSelections ) const ;
134
135     virtual bool            ListIsSelected( unsigned int n ) const ;
136
137     // display
138
139     virtual void            ListScrollTo( unsigned int n ) ;
140
141     // accessing content
142
143     virtual unsigned int    ListGetCount() const ;
144
145     int                     ListGetColumnType( int col )
146     {
147         return col;
148     }
149     virtual void            UpdateLine( unsigned int n, wxListWidgetColumn* col = NULL ) ;
150     virtual void            UpdateLineToEnd( unsigned int n);
151
152     virtual void            controlDoubleAction(WXWidget slf, void* _cmd, void *sender);
153 protected :
154     wxNSTableView*          m_tableView ;
155
156     wxNSTableDataSource*    m_dataSource;
157 } ;
158
159 //
160 // implementations
161 //
162
163 @implementation wxNSTableColumn
164
165 - (id) init
166 {
167     [super init];
168     column = nil;
169     return self;
170 }
171
172 - (void) setColumn: (wxCocoaTableColumn*) col
173 {
174     column = col;
175 }
176
177 - (wxCocoaTableColumn*) column
178 {
179     return column;
180 }
181
182 @end
183
184 class wxNSTableViewCellValue : public wxListWidgetCellValue
185 {
186 public :
187     wxNSTableViewCellValue( id &v ) : value(v)
188     {
189     }
190
191     virtual ~wxNSTableViewCellValue() {}
192
193     virtual void Set( CFStringRef v )
194     {
195         value = [[(NSString*)v retain] autorelease];
196     }
197     virtual void Set( const wxString& value )
198     {
199         Set( (CFStringRef) wxCFStringRef( value ) );
200     }
201     virtual void Set( int v )
202     {
203         value = [NSNumber numberWithInt:v];
204     }
205
206     virtual int GetIntValue() const
207     {
208         if ( [value isKindOfClass:[NSNumber class]] )
209             return [ (NSNumber*) value intValue ];
210
211         return 0;
212     }
213
214     virtual wxString GetStringValue() const
215     {
216         if ( [value isKindOfClass:[NSString class]] )
217             return wxCFStringRef::AsString( (NSString*) value );
218
219         return wxEmptyString;
220     }
221
222 protected:
223     id& value;
224 } ;
225
226 @implementation wxNSTableDataSource
227
228 - (id) init
229 {
230     [super init];
231     impl = nil;
232     return self;
233 }
234
235 - (void)setImplementation: (wxListWidgetCocoaImpl *) theImplementation
236 {
237     impl = theImplementation;
238 }
239
240 - (wxListWidgetCocoaImpl*) implementation
241 {
242     return impl;
243 }
244
245 - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
246 {
247     wxUnusedVar(aTableView);
248     if ( impl )
249         return impl->ListGetCount();
250     return 0;
251 }
252
253 - (id)tableView:(NSTableView *)aTableView
254         objectValueForTableColumn:(NSTableColumn *)aTableColumn
255         row:(NSInteger)rowIndex
256 {
257     wxUnusedVar(aTableView);
258     wxNSTableColumn* tablecol = (wxNSTableColumn *)aTableColumn;
259     wxListBox* lb = dynamic_cast<wxListBox*>(impl->GetWXPeer());
260     wxCocoaTableColumn* col = [tablecol column];
261     id value = nil;
262     wxNSTableViewCellValue cellvalue(value);
263     lb->GetValueCallback(rowIndex, col, cellvalue);
264     return value;
265 }
266
267 - (void)tableView:(NSTableView *)aTableView
268         setObjectValue:(id)value forTableColumn:(NSTableColumn *)aTableColumn
269         row:(NSInteger)rowIndex
270 {
271     wxUnusedVar(aTableView);
272     wxNSTableColumn* tablecol = (wxNSTableColumn *)aTableColumn;
273     wxListBox* lb = dynamic_cast<wxListBox*>(impl->GetWXPeer());
274     wxCocoaTableColumn* col = [tablecol column];
275     wxNSTableViewCellValue cellvalue(value);
276     lb->SetValueCallback(rowIndex, col, cellvalue);
277 }
278
279 @end
280
281 @implementation wxNSTableView
282
283 + (void)initialize
284 {
285     static BOOL initialized = NO;
286     if (!initialized)
287     {
288         initialized = YES;
289         wxOSXCocoaClassAddWXMethods( self );
290     }
291 }
292
293 - (void) tableViewSelectionDidChange: (NSNotification *) notification
294 {
295     wxUnusedVar(notification);
296     
297     int row = [self selectedRow];
298     
299     if (row == -1) 
300     {
301         // no row selected
302     } 
303     else 
304     {
305         wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
306         wxListBox *list = static_cast<wxListBox*> ( impl->GetWXPeer());
307         wxCHECK_RET( list != NULL , wxT("Listbox expected"));
308         
309         wxCommandEvent event( wxEVT_COMMAND_LISTBOX_SELECTED, list->GetId() );
310         
311         if ((row < 0) || (row > (int) list->GetCount()))  // OS X can select an item below the last item
312             return;
313         
314         if ( !list->MacGetBlockEvents() )
315             list->HandleLineEvent( row, false );
316     }
317     
318
319
320 @end
321
322 //
323 //
324 //
325
326 wxListWidgetCocoaImpl::wxListWidgetCocoaImpl( wxWindowMac* peer, NSScrollView* view, wxNSTableView* tableview, wxNSTableDataSource* data ) :
327     wxWidgetCocoaImpl( peer, view ), m_tableView(tableview), m_dataSource(data)
328 {
329     InstallEventHandler( tableview );
330 }
331
332 wxListWidgetCocoaImpl::~wxListWidgetCocoaImpl()
333 {
334     [m_dataSource release];
335 }
336
337 unsigned int wxListWidgetCocoaImpl::ListGetCount() const
338 {
339     wxListBox* lb = dynamic_cast<wxListBox*> ( GetWXPeer() );
340     return lb->GetCount();
341 }
342
343 //
344 // columns
345 //
346
347 wxListWidgetColumn* wxListWidgetCocoaImpl::InsertTextColumn( unsigned pos, const wxString& WXUNUSED(title), bool editable,
348                                 wxAlignment WXUNUSED(just), int defaultWidth)
349 {
350     wxNSTableColumn* col1 = [[wxNSTableColumn alloc] init];
351     [col1 setEditable:editable];
352
353     unsigned formerColCount = [m_tableView numberOfColumns];
354
355     // there's apparently no way to insert at a specific position
356     [m_tableView addTableColumn:col1 ];
357     if ( pos < formerColCount )
358         [m_tableView moveColumn:formerColCount toColumn:pos];
359
360     if ( defaultWidth >= 0 )
361     {
362         [col1 setMaxWidth:defaultWidth];
363         [col1 setMinWidth:defaultWidth];
364         [col1 setWidth:defaultWidth];
365     }
366     else
367     {
368         [col1 setMaxWidth:1000];
369         [col1 setMinWidth:10];
370         // temporary hack, because I cannot get the automatic column resizing
371         // to work properly
372         [col1 setWidth:1000];
373     }
374     [col1 setResizingMask: NSTableColumnAutoresizingMask];
375     wxCocoaTableColumn* wxcol = new wxCocoaTableColumn( col1, editable );
376     [col1 setColumn:wxcol];
377
378     // owned by the tableview
379     [col1 release];
380     return wxcol;
381 }
382
383 wxListWidgetColumn* wxListWidgetCocoaImpl::InsertCheckColumn( unsigned pos , const wxString& WXUNUSED(title), bool editable,
384                                 wxAlignment WXUNUSED(just), int defaultWidth )
385 {
386    wxNSTableColumn* col1 = [[wxNSTableColumn alloc] init];
387     [col1 setEditable:editable];
388
389     // set your custom cell & set it up
390     NSButtonCell* checkbox = [[NSButtonCell alloc] init];
391     [checkbox setTitle:@""];
392     [checkbox setButtonType:NSSwitchButton];
393     [col1 setDataCell:checkbox] ;
394     [checkbox release];
395
396     unsigned formerColCount = [m_tableView numberOfColumns];
397
398     // there's apparently no way to insert at a specific position
399     [m_tableView addTableColumn:col1 ];
400     if ( pos < formerColCount )
401         [m_tableView moveColumn:formerColCount toColumn:pos];
402
403     if ( defaultWidth >= 0 )
404     {
405         [col1 setMaxWidth:defaultWidth];
406         [col1 setMinWidth:defaultWidth];
407         [col1 setWidth:defaultWidth];
408     }
409
410     [col1 setResizingMask: NSTableColumnNoResizing];
411     wxCocoaTableColumn* wxcol = new wxCocoaTableColumn( col1, editable );
412     [col1 setColumn:wxcol];
413
414     // owned by the tableview
415     [col1 release];
416     return wxcol;
417 }
418
419
420 //
421 // inserting / removing lines
422 //
423
424 void wxListWidgetCocoaImpl::ListInsert( unsigned int WXUNUSED(n) )
425 {
426     [m_tableView reloadData];
427 }
428
429 void wxListWidgetCocoaImpl::ListDelete( unsigned int WXUNUSED(n) )
430 {
431     [m_tableView reloadData];
432 }
433
434 void wxListWidgetCocoaImpl::ListClear()
435 {
436     [m_tableView reloadData];
437 }
438
439 // selecting
440
441 void wxListWidgetCocoaImpl::ListDeselectAll()
442 {
443     [m_tableView deselectAll:nil];
444 }
445
446 void wxListWidgetCocoaImpl::ListSetSelection( unsigned int n, bool select, bool multi )
447 {
448     // TODO
449     if ( select )
450         [m_tableView selectRow: n byExtendingSelection:multi];
451     else
452         [m_tableView deselectRow: n];
453
454 }
455
456 int wxListWidgetCocoaImpl::ListGetSelection() const
457 {
458     return [m_tableView selectedRow];
459 }
460
461 int wxListWidgetCocoaImpl::ListGetSelections( wxArrayInt& aSelections ) const
462 {
463     aSelections.Empty();
464
465     int count = ListGetCount();
466
467     for ( int i = 0; i < count; ++i)
468     {
469         if ([m_tableView isRowSelected:count])
470         aSelections.Add(i);
471     }
472
473     return aSelections.Count();
474 }
475
476 bool wxListWidgetCocoaImpl::ListIsSelected( unsigned int n ) const
477 {
478     return [m_tableView isRowSelected:n];
479 }
480
481 // display
482
483 void wxListWidgetCocoaImpl::ListScrollTo( unsigned int n )
484 {
485     [m_tableView scrollRowToVisible:n];
486 }
487
488
489 void wxListWidgetCocoaImpl::UpdateLine( unsigned int WXUNUSED(n), wxListWidgetColumn* WXUNUSED(col) )
490 {
491     // TODO optimize
492     [m_tableView reloadData];
493 }
494
495 void wxListWidgetCocoaImpl::UpdateLineToEnd( unsigned int WXUNUSED(n))
496 {
497     // TODO optimize
498     [m_tableView reloadData];
499 }
500
501 void wxListWidgetCocoaImpl::controlDoubleAction(WXWidget WXUNUSED(slf),void* WXUNUSED(_cmd), void *WXUNUSED(sender))
502 {
503     wxListBox *list = static_cast<wxListBox*> ( GetWXPeer());
504     wxCHECK_RET( list != NULL , wxT("Listbox expected"));
505
506     int sel = [m_tableView clickedRow];
507     if ((sel < 0) || (sel > (int) list->GetCount()))  // OS X can select an item below the last item (why?)
508        return;
509
510     list->HandleLineEvent( sel, true );
511 }
512
513 // accessing content
514
515
516 wxWidgetImplType* wxWidgetImpl::CreateListBox( wxWindowMac* wxpeer,
517                                     wxWindowMac* WXUNUSED(parent),
518                                     wxWindowID WXUNUSED(id),
519                                     const wxPoint& pos,
520                                     const wxSize& size,
521                                     long style,
522                                     long WXUNUSED(extraStyle))
523 {
524     NSRect r = wxOSXGetFrameForControl( wxpeer, pos , size ) ;
525     NSScrollView* scrollview = [[NSScrollView alloc] initWithFrame:r];
526
527     // use same scroll flags logic as msw
528
529     [scrollview setHasVerticalScroller:YES];
530
531     if ( style & wxLB_HSCROLL )
532         [scrollview setHasHorizontalScroller:YES];
533
534     [scrollview setAutohidesScrollers: ((style & wxLB_ALWAYS_SB) ? NO : YES)];
535
536     // setting up the true table
537
538     wxNSTableView* tableview = [[wxNSTableView alloc] init];
539     [tableview setDelegate:tableview];
540     // only one multi-select mode available
541     if ( (style & wxLB_EXTENDED) || (style & wxLB_MULTIPLE) )
542         [tableview setAllowsMultipleSelection:YES];
543
544     // simple listboxes have no header row
545     [tableview setHeaderView:nil];
546
547     if ( style & wxLB_HSCROLL )
548         [tableview setColumnAutoresizingStyle:NSTableViewNoColumnAutoresizing];
549     else
550         [tableview setColumnAutoresizingStyle:NSTableViewLastColumnOnlyAutoresizingStyle];
551
552     wxNSTableDataSource* ds = [[ wxNSTableDataSource alloc] init];
553     [tableview setDataSource:ds];
554     [scrollview setDocumentView:tableview];
555     [tableview release];
556
557     wxListWidgetCocoaImpl* c = new wxListWidgetCocoaImpl( wxpeer, scrollview, tableview, ds );
558
559     // temporary hook for dnd
560     [tableview registerForDraggedTypes:[NSArray arrayWithObjects:
561         NSStringPboardType, NSFilenamesPboardType, NSTIFFPboardType, NSPICTPboardType, NSPDFPboardType, nil]];
562
563     [ds setImplementation:c];
564     return c;
565 }
566
567 int wxListBox::DoListHitTest(const wxPoint& WXUNUSED(inpoint)) const
568 {
569 #if wxOSX_USE_CARBON
570     OSStatus err;
571
572     // There are few reasons why this is complicated:
573     // 1) There is no native HitTest function for Mac
574     // 2) GetDataBrowserItemPartBounds only works on visible items
575     // 3) We can't do it through GetDataBrowserTableView[Item]RowHeight
576     //    because what it returns is basically inaccurate in the context
577     //    of the coordinates we want here, but we use this as a guess
578     //    for where the first visible item lies
579
580     wxPoint point = inpoint;
581
582     // get column property ID (req. for call to itempartbounds)
583     DataBrowserTableViewColumnID colId = 0;
584     err = GetDataBrowserTableViewColumnProperty(m_peer->GetControlRef(), 0, &colId);
585     wxCHECK_MSG(err == noErr, wxNOT_FOUND, wxT("Unexpected error from GetDataBrowserTableViewColumnProperty"));
586
587     // OK, first we need to find the first visible item we have -
588     // this will be the "low" for our binary search. There is no real
589     // easy way around this, as we will need to do a SLOW linear search
590     // until we find a visible item, but we can do a cheap calculation
591     // via the row height to speed things up a bit
592     UInt32 scrollx, scrolly;
593     err = GetDataBrowserScrollPosition(m_peer->GetControlRef(), &scrollx, &scrolly);
594     wxCHECK_MSG(err == noErr, wxNOT_FOUND, wxT("Unexpected error from GetDataBrowserScrollPosition"));
595
596     UInt16 height;
597     err = GetDataBrowserTableViewRowHeight(m_peer->GetControlRef(), &height);
598     wxCHECK_MSG(err == noErr, wxNOT_FOUND, wxT("Unexpected error from GetDataBrowserTableViewRowHeight"));
599
600     // these indices are 0-based, as usual, so we need to add 1 to them when
601     // passing them to data browser functions which use 1-based indices
602     int low = scrolly / height,
603         high = GetCount() - 1;
604
605     // search for the first visible item (note that the scroll guess above
606     // is the low bounds of where the item might lie so we only use that as a
607     // starting point - we should reach it within 1 or 2 iterations of the loop)
608     while ( low <= high )
609     {
610         Rect bounds;
611         err = GetDataBrowserItemPartBounds(
612             m_peer->GetControlRef(), low + 1, colId,
613             kDataBrowserPropertyEnclosingPart,
614             &bounds); // note +1 to translate to Mac ID
615         if ( err == noErr )
616             break;
617
618         // errDataBrowserItemNotFound is expected as it simply means that the
619         // item is not currently visible -- but other errors are not
620         wxCHECK_MSG( err == errDataBrowserItemNotFound, wxNOT_FOUND,
621                      wxT("Unexpected error from GetDataBrowserItemPartBounds") );
622
623         low++;
624     }
625
626     // NOW do a binary search for where the item lies, searching low again if
627     // we hit an item that isn't visible
628     while ( low <= high )
629     {
630         int mid = (low + high) / 2;
631
632         Rect bounds;
633         err = GetDataBrowserItemPartBounds(
634             m_peer->GetControlRef(), mid + 1, colId,
635             kDataBrowserPropertyEnclosingPart,
636             &bounds); //note +1 to trans to mac id
637         wxCHECK_MSG( err == noErr || err == errDataBrowserItemNotFound,
638                      wxNOT_FOUND,
639                      wxT("Unexpected error from GetDataBrowserItemPartBounds") );
640
641         if ( err == errDataBrowserItemNotFound )
642         {
643             // item not visible, attempt to find a visible one
644             // search lower
645             high = mid - 1;
646         }
647         else // visible item, do actual hitttest
648         {
649             // if point is within the bounds, return this item (since we assume
650             // all x coords of items are equal we only test the x coord in
651             // equality)
652             if ((point.x >= bounds.left && point.x <= bounds.right) &&
653                 (point.y >= bounds.top && point.y <= bounds.bottom) )
654             {
655                 // found!
656                 return mid;
657             }
658
659             if ( point.y < bounds.top )
660                 // index(bounds) greater then key(point)
661                 high = mid - 1;
662             else
663                 // index(bounds) less then key(point)
664                 low = mid + 1;
665         }
666     }
667 #endif
668     return wxNOT_FOUND;
669 }
670
671 #endif // wxUSE_LISTBOX