05d2c436aaa8b106d6f0ba569d0416815eeaa48e
[xscreensaver] / OSX / SaverListController.m
1 /* xscreensaver, Copyright (c) 2012-2014 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  *
11  * This implements the top-level screen-saver selection list in the iOS app.
12  */
13
14 #ifdef USE_IPHONE  // whole file
15
16
17 #import "SaverListController.h"
18 #import "SaverRunner.h"
19 #import "yarandom.h"
20 #import "version.h"
21
22 #undef countof
23 #define countof(x) (sizeof((x))/sizeof((*x)))
24
25
26 @implementation SaverListController
27
28 - (void) titleTapped:(id) sender
29 {
30   [[UIApplication sharedApplication]
31     openURL:[NSURL URLWithString:@"http://www.jwz.org/xscreensaver/"]];
32 }
33
34
35 - (void)makeTitleBar
36 {
37   // Extract the version number and release date from the version string.
38   // Here's an area where I kind of wish I had "Two Problems".
39   // I guess I could add custom key to the Info.plist for this.
40
41   NSArray *a = [[NSString stringWithCString: screensaver_id
42                           encoding:NSASCIIStringEncoding]
43                  componentsSeparatedByCharactersInSet:
44                    [NSCharacterSet
45                      characterSetWithCharactersInString:@" ()-"]];
46   NSString *vers = [a objectAtIndex: 3];
47   NSString *year = [a objectAtIndex: 7];
48
49   NSString *line1 = [@"XScreenSaver " stringByAppendingString: vers];
50   NSString *line2 = [@"\u00A9 " stringByAppendingString:
51                         [year stringByAppendingString:
52                                 @" Jamie Zawinski <jwz@jwz.org>"]];
53
54   UIView *v = [[UIView alloc] initWithFrame:CGRectZero];
55
56   // The "go to web page" button on the right
57
58   UIImage *img = [UIImage imageWithContentsOfFile:
59                             [[[NSBundle mainBundle] bundlePath]
60                               stringByAppendingPathComponent:
61                                 @"iSaverRunner57t.png"]];
62   UIButton *button = [[UIButton alloc] init];
63   [button setFrame: CGRectMake(0, 0, img.size.width/2, img.size.height/2)];
64   [button setBackgroundImage:img forState:UIControlStateNormal];
65   [button addTarget:self
66           action:@selector(titleTapped:)
67           forControlEvents:UIControlEventTouchUpInside];
68   self.navigationItem.rightBarButtonItem = 
69     [[UIBarButtonItem alloc] initWithCustomView: button];
70   [button release];
71
72   // The title bar
73
74   UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
75   UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
76   [label1 setText: line1];
77   [label2 setText: line2];
78   [label1 setBackgroundColor:[UIColor clearColor]];
79   [label2 setBackgroundColor:[UIColor clearColor]];
80
81   [label1 setFont: [UIFont boldSystemFontOfSize: 17]];
82   [label2 setFont: [UIFont systemFontOfSize: 12]];
83   [label1 sizeToFit];
84   [label2 sizeToFit];
85
86   CGRect r1 = [label1 frame];
87   CGRect r2 = [label2 frame];
88   CGRect r3 = r2;
89
90   CGRect win = [self view].frame;
91   if (win.size.width > 414 && win.size.height > 414) {          // iPad
92     [label1 setTextAlignment: NSTextAlignmentLeft];
93     [label2 setTextAlignment: NSTextAlignmentRight];
94     label2.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
95     r3.size.width = win.size.width;
96     r1 = r3;
97     r1.origin.x   += 6;
98     r1.size.width -= 12;
99     r2 = r1;
100
101   } else {                                                      // iPhone
102     r3.size.width = win.size.width; // force it to be flush-left
103     [label1 setTextAlignment: NSTextAlignmentLeft];
104     [label2 setTextAlignment: NSTextAlignmentLeft];
105     r1.origin.y = -1;    // make it fit in landscape
106     r2.origin.y = r1.origin.y + r1.size.height - 2;
107     r3.size.height = r1.size.height + r2.size.height;
108   }
109   v.autoresizingMask = UIViewAutoresizingFlexibleWidth;
110   [label1 setFrame:r1];
111   [label2 setFrame:r2];
112   [v setFrame:r3];
113
114   [v addSubview:label1];
115   [v addSubview:label2];
116
117   // Default opacity looks bad.
118   [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
119
120   self.navigationItem.titleView = v;
121
122   win.origin.x = 0;
123   win.origin.y = 0;
124   win.size.height = 44; // #### This cannot possibly be right.
125   UISearchBar *search = [[UISearchBar alloc] initWithFrame:win];
126   search.delegate = self;
127   search.placeholder = @"Search...";
128   self.tableView.tableHeaderView = search;
129
130   // Dismiss the search field's keyboard as soon as we scroll.
131 # ifdef __IPHONE_7_0
132   if ([self.tableView respondsToSelector:@selector(keyboardDismissMode)])
133     [self.tableView setKeyboardDismissMode:
134            UIScrollViewKeyboardDismissModeOnDrag];
135 # endif
136 }
137
138
139 - (id)initWithNames:(NSArray *)_names descriptions:(NSDictionary *)_descs;
140 {
141   self = [self init];
142   if (! self) return 0;
143   [self reload:_names descriptions:_descs search:nil];
144   [self makeTitleBar];
145   return self;
146 }
147
148
149 - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tv
150 {
151   int n = countof(list_by_letter);
152   NSMutableArray *a = [NSMutableArray arrayWithCapacity: n];
153   for (int i = 0; i < n; i++) {
154     if ([list_by_letter[i] count] == 0)  // Omit empty letter sections.
155       continue;
156     char s[2];
157     s[0] = (i == 'Z'-'A'+1 ? '#' : i+'A');
158     s[1] = 0;
159     [a addObject: [NSString stringWithCString:s
160                             encoding:NSASCIIStringEncoding]];
161   }
162   return a;
163 }
164
165
166 /* Called when text is typed into the top search bar.
167  */
168 - (void)searchBar:(UISearchBar *)bar textDidChange:(NSString *)txt
169 {
170   [self reload:names descriptions:descriptions search:txt];
171 }
172
173
174 - (void) reload:(NSArray *)_names descriptions:(NSDictionary *)_descs
175          search:search
176 {
177   if (names != _names) {
178     if (names) [names release];
179     names = [_names retain];
180   }
181   if (_descs != descriptions) {
182     if (descriptions) [descriptions release];
183     descriptions = [_descs retain];
184   }
185
186   int n = countof(list_by_letter);
187   for (int i = 0; i < n; i++) {
188     list_by_letter[i] = [[NSMutableArray alloc] init];
189   }
190
191   for (NSString *name in names) {
192
193     // If we're searching, omit any items that don't have a match in the
194     // title or description.
195     //
196     BOOL matchp = (!search || [search length] == 0);
197     if (! matchp) {
198       matchp = ([name rangeOfString:search
199                             options:NSCaseInsensitiveSearch].location
200                 != NSNotFound);
201     }
202     if (! matchp) {
203       NSString *desc = [descriptions objectForKey:name];
204       matchp = ([desc rangeOfString:search
205                             options:NSCaseInsensitiveSearch].location
206                 != NSNotFound);
207     }
208     if (! matchp)
209       continue;
210
211     int index = ([name cStringUsingEncoding: NSASCIIStringEncoding])[0];
212     if (index >= 'a' && index <= 'z')
213       index -= 'a'-'A';
214     if (index >= 'A' && index <= 'Z')
215       index -= 'A';
216     else
217       index = n-1;
218     [list_by_letter[index] addObject: name];
219   }
220
221   active_section_count = 0;
222   letter_sections = [[[NSMutableArray alloc] init] retain];
223   section_titles = [[[NSMutableArray alloc] init] retain];
224   for (int i = 0; i < n; i++) {
225     if ([list_by_letter[i] count] > 0) {
226       active_section_count++;
227       [letter_sections addObject: list_by_letter[i]];
228       if (i <= 'Z'-'A')
229         [section_titles addObject: [NSString stringWithFormat: @"%c", i+'A']];
230       else
231         [section_titles addObject: @"#"];
232     }
233   }
234   [self.tableView reloadData];
235 }
236
237
238 - (NSString *)tableView:(UITableView *)tv
239               titleForHeaderInSection:(NSInteger)section
240 {
241   return [section_titles objectAtIndex: section];
242 }
243
244 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv
245 {
246   return active_section_count;
247 }
248
249
250 - (NSInteger)tableView:(UITableView *)tv
251                        numberOfRowsInSection:(NSInteger)section
252 {
253   return [[letter_sections objectAtIndex: section] count];
254 }
255
256 - (NSInteger)tableView:(UITableView *)tv
257              sectionForSectionIndexTitle:(NSString *)title
258                atIndex:(NSInteger) index
259 {
260   int i = 0;
261   for (NSString *sectionTitle in section_titles) {
262     if ([sectionTitle isEqualToString: title])
263       return i;
264     i++;
265   }
266   return -1;
267 }
268
269
270 - (UITableViewCell *)tableView:(UITableView *)tv
271                      cellForRowAtIndexPath:(NSIndexPath *)ip
272 {
273   NSString *title = 
274     [[letter_sections objectAtIndex: [ip indexAtPosition: 0]]
275       objectAtIndex: [ip indexAtPosition: 1]];
276   NSString *desc = [descriptions objectForKey:title];
277
278   NSString *id = @"Cell";
279   UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
280   if (!cell)
281     cell = [[[UITableViewCell alloc]
282                 initWithStyle: UITableViewCellStyleSubtitle
283               reuseIdentifier: id]
284              autorelease];
285
286   cell.textLabel.text = title;
287   cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
288   cell.detailTextLabel.text = desc;
289
290   return cell;
291 }
292
293
294 /* Selecting a row launches the saver.
295  */
296 - (void)tableView:(UITableView *)tv
297         didSelectRowAtIndexPath:(NSIndexPath *)ip
298 {
299   UITableViewCell *cell = [tv cellForRowAtIndexPath: ip];
300   SaverRunner *s = 
301     (SaverRunner *) [[UIApplication sharedApplication] delegate];
302   if (! s) return;
303
304   // Dismiss the search field's keyboard before launching a saver.
305   [self.tableView.tableHeaderView resignFirstResponder];
306
307   NSAssert ([s isKindOfClass:[SaverRunner class]], @"not a SaverRunner");
308   [s loadSaver: cell.textLabel.text];
309 }
310
311 /* Selecting a row's Disclosure Button opens the preferences.
312  */
313 - (void)tableView:(UITableView *)tv
314         accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip
315 {
316   UITableViewCell *cell = [tv cellForRowAtIndexPath: ip];
317   SaverRunner *s = 
318     (SaverRunner *) [[UIApplication sharedApplication] delegate];
319   if (! s) return;
320   NSAssert ([s isKindOfClass:[SaverRunner class]], @"not a SaverRunner");
321   [s openPreferences: cell.textLabel.text];
322 }
323
324
325 - (void) scrollTo: (NSString *) name
326 {
327   int i = 0;
328   int j = 0;
329   Bool ok = NO;
330   for (NSArray *a in letter_sections) {
331     j = 0;
332     for (NSString *n in a) {
333       ok = [n isEqualToString: name];
334       if (ok) goto DONE;
335       j++;
336     }
337     i++;
338   }
339  DONE:
340   if (ok) {
341     NSIndexPath *ip = [NSIndexPath indexPathForRow: j inSection: i];
342     [self.tableView selectRowAtIndexPath:ip
343                     animated:NO
344                     scrollPosition: UITableViewScrollPositionMiddle];
345   }
346 }
347
348
349 /* We need this to respond to "shake" gestures
350  */
351 - (BOOL)canBecomeFirstResponder
352 {
353   return YES;
354 }
355
356 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
357 {
358 }
359
360 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
361 {
362 }
363
364
365 /* Shake means load a random screen saver.
366  */
367 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
368 {
369   if (motion != UIEventSubtypeMotionShake)
370     return;
371   NSMutableArray *a = [NSMutableArray arrayWithCapacity: 200];
372   for (NSArray *sec in letter_sections)
373     for (NSString *s in sec)
374       [a addObject: s];
375   int n = [a count];
376   if (! n) return;
377   NSString *which = [a objectAtIndex: (random() % n)];
378
379   SaverRunner *s = 
380     (SaverRunner *) [[UIApplication sharedApplication] delegate];
381   if (! s) return;
382   NSAssert ([s isKindOfClass:[SaverRunner class]], @"not a SaverRunner");
383   [self scrollTo: which];
384   [s loadSaver: which];
385 }
386
387
388 - (void)dealloc
389 {
390   for (int i = 0; i < countof(list_by_letter); i++)
391     [list_by_letter[i] release];
392   [letter_sections release];
393   [section_titles release];
394   [descriptions release];
395   [super dealloc];
396 }
397
398 @end
399
400
401 #endif // USE_IPHONE -- whole file