From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[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:@"https://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   UIBarButtonItem *bi = [[UIBarButtonItem alloc] initWithCustomView: button];
69   self.navigationItem.rightBarButtonItem = bi;
70   [bi release];
71   [button release];
72
73   // The title bar
74
75   UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
76   UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
77   [label1 setText: line1];
78   [label2 setText: line2];
79   [label1 setBackgroundColor:[UIColor clearColor]];
80   [label2 setBackgroundColor:[UIColor clearColor]];
81
82   [label1 setFont: [UIFont boldSystemFontOfSize: 17]];
83   [label2 setFont: [UIFont systemFontOfSize: 12]];
84   [label1 sizeToFit];
85   [label2 sizeToFit];
86
87   CGRect r1 = [label1 frame];
88   CGRect r2 = [label2 frame];
89   CGRect r3 = r2;
90
91   CGRect win = [self view].frame;
92   if (win.size.width > 414 && win.size.height > 414) {          // iPad
93     [label1 setTextAlignment: NSTextAlignmentLeft];
94     [label2 setTextAlignment: NSTextAlignmentRight];
95     label2.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
96     r3.size.width = win.size.width;
97     r1 = r3;
98     r1.origin.x   += 6;
99     r1.size.width -= 12;
100     r2 = r1;
101
102   } else {                                                      // iPhone
103     r3.size.width = win.size.width; // force it to be flush-left
104     [label1 setTextAlignment: NSTextAlignmentLeft];
105     [label2 setTextAlignment: NSTextAlignmentLeft];
106     r1.origin.y = -1;    // make it fit in landscape
107     r2.origin.y = r1.origin.y + r1.size.height - 2;
108     r3.size.height = r1.size.height + r2.size.height;
109   }
110   v.autoresizingMask = UIViewAutoresizingFlexibleWidth;
111   [label1 setFrame:r1];
112   [label2 setFrame:r2];
113   [v setFrame:r3];
114
115   [v addSubview:label1];
116   [v addSubview:label2];
117
118   // Default opacity looks bad.
119   [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
120
121   self.navigationItem.titleView = v;
122
123   win.origin.x = 0;
124   win.origin.y = 0;
125   win.size.height = 44; // #### This cannot possibly be right.
126   UISearchBar *search = [[UISearchBar alloc] initWithFrame:win];
127   search.delegate = self;
128   search.placeholder = @"Search...";
129   self.tableView.tableHeaderView = search;
130
131   // Dismiss the search field's keyboard as soon as we scroll.
132 # ifdef __IPHONE_7_0
133   if ([self.tableView respondsToSelector:@selector(keyboardDismissMode)])
134     [self.tableView setKeyboardDismissMode:
135            UIScrollViewKeyboardDismissModeOnDrag];
136 # endif
137 }
138
139
140 - (id)initWithNames:(NSArray *)_names descriptions:(NSDictionary *)_descs;
141 {
142   self = [self init];
143   if (! self) return 0;
144   [self reload:_names descriptions:_descs search:nil];
145   [self makeTitleBar];
146   return self;
147 }
148
149
150 - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tv
151 {
152   int n = countof(list_by_letter);
153   NSMutableArray *a = [NSMutableArray arrayWithCapacity: n];
154   for (int i = 0; i < n; i++) {
155     if ([list_by_letter[i] count] == 0)  // Omit empty letter sections.
156       continue;
157     char s[2];
158     s[0] = (i == 'Z'-'A'+1 ? '#' : i+'A');
159     s[1] = 0;
160     [a addObject: [NSString stringWithCString:s
161                             encoding:NSASCIIStringEncoding]];
162   }
163   return a;
164 }
165
166
167 /* Called when text is typed into the top search bar.
168  */
169 - (void)searchBar:(UISearchBar *)bar textDidChange:(NSString *)txt
170 {
171   [self reload:names descriptions:descriptions search:txt];
172 }
173
174
175 - (void) reload:(NSArray *)_names descriptions:(NSDictionary *)_descs
176          search:search
177 {
178   if (names != _names) {
179     if (names) [names release];
180     names = [_names retain];
181   }
182   if (_descs != descriptions) {
183     if (descriptions) [descriptions release];
184     descriptions = [_descs retain];
185   }
186
187   int n = countof(list_by_letter);
188   for (int i = 0; i < n; i++) {
189     list_by_letter[i] = [[NSMutableArray alloc] init];
190   }
191
192   for (NSString *name in names) {
193
194     // If we're searching, omit any items that don't have a match in the
195     // title or description.
196     //
197     BOOL matchp = (!search || [search length] == 0);
198     if (! matchp) {
199       matchp = ([name rangeOfString:search
200                             options:NSCaseInsensitiveSearch].location
201                 != NSNotFound);
202     }
203     if (! matchp) {
204       NSString *desc = [descriptions objectForKey:name];
205       matchp = ([desc rangeOfString:search
206                             options:NSCaseInsensitiveSearch].location
207                 != NSNotFound);
208     }
209     if (! matchp)
210       continue;
211
212     int index = ([name cStringUsingEncoding: NSASCIIStringEncoding])[0];
213     if (index >= 'a' && index <= 'z')
214       index -= 'a'-'A';
215     if (index >= 'A' && index <= 'Z')
216       index -= 'A';
217     else
218       index = n-1;
219     [list_by_letter[index] addObject: name];
220   }
221
222   active_section_count = 0;
223   letter_sections = [[[NSMutableArray alloc] init] retain];
224   section_titles = [[[NSMutableArray alloc] init] retain];
225   for (int i = 0; i < n; i++) {
226     if ([list_by_letter[i] count] > 0) {
227       active_section_count++;
228       [letter_sections addObject: list_by_letter[i]];
229       if (i <= 'Z'-'A')
230         [section_titles addObject: [NSString stringWithFormat: @"%c", i+'A']];
231       else
232         [section_titles addObject: @"#"];
233     }
234   }
235   [self.tableView reloadData];
236 }
237
238
239 - (NSString *)tableView:(UITableView *)tv
240               titleForHeaderInSection:(NSInteger)section
241 {
242   return [section_titles objectAtIndex: section];
243 }
244
245 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv
246 {
247   return active_section_count;
248 }
249
250
251 - (NSInteger)tableView:(UITableView *)tv
252                        numberOfRowsInSection:(NSInteger)section
253 {
254   return [[letter_sections objectAtIndex: section] count];
255 }
256
257 - (NSInteger)tableView:(UITableView *)tv
258              sectionForSectionIndexTitle:(NSString *)title
259                atIndex:(NSInteger) index
260 {
261   int i = 0;
262   for (NSString *sectionTitle in section_titles) {
263     if ([sectionTitle isEqualToString: title])
264       return i;
265     i++;
266   }
267   return -1;
268 }
269
270
271 - (UITableViewCell *)tableView:(UITableView *)tv
272                      cellForRowAtIndexPath:(NSIndexPath *)ip
273 {
274   NSString *title = 
275     [[letter_sections objectAtIndex: [ip indexAtPosition: 0]]
276       objectAtIndex: [ip indexAtPosition: 1]];
277   NSString *desc = [descriptions objectForKey:title];
278
279   NSString *id = @"Cell";
280   UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
281   if (!cell)
282     cell = [[[UITableViewCell alloc]
283                 initWithStyle: UITableViewCellStyleSubtitle
284               reuseIdentifier: id]
285              autorelease];
286
287   cell.textLabel.text = title;
288   cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
289   cell.detailTextLabel.text = desc;
290
291   return cell;
292 }
293
294
295 /* Selecting a row launches the saver.
296  */
297 - (void)tableView:(UITableView *)tv
298         didSelectRowAtIndexPath:(NSIndexPath *)ip
299 {
300   UITableViewCell *cell = [tv cellForRowAtIndexPath: ip];
301   SaverRunner *s = 
302     (SaverRunner *) [[UIApplication sharedApplication] delegate];
303   if (! s) return;
304
305   // Dismiss the search field's keyboard before launching a saver.
306   [self.tableView.tableHeaderView resignFirstResponder];
307
308   NSAssert ([s isKindOfClass:[SaverRunner class]], @"not a SaverRunner");
309   [s loadSaver: cell.textLabel.text];
310 }
311
312 /* Selecting a row's Disclosure Button opens the preferences.
313  */
314 - (void)tableView:(UITableView *)tv
315         accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip
316 {
317   UITableViewCell *cell = [tv cellForRowAtIndexPath: ip];
318   SaverRunner *s = 
319     (SaverRunner *) [[UIApplication sharedApplication] delegate];
320   if (! s) return;
321   NSAssert ([s isKindOfClass:[SaverRunner class]], @"not a SaverRunner");
322   [s openPreferences: cell.textLabel.text];
323 }
324
325
326 - (void) scrollTo: (NSString *) name
327 {
328   int i = 0;
329   int j = 0;
330   Bool ok = NO;
331   for (NSArray *a in letter_sections) {
332     j = 0;
333     for (NSString *n in a) {
334       ok = [n isEqualToString: name];
335       if (ok) goto DONE;
336       j++;
337     }
338     i++;
339   }
340  DONE:
341   if (ok) {
342     NSIndexPath *ip = [NSIndexPath indexPathForRow: j inSection: i];
343     [self.tableView selectRowAtIndexPath:ip
344                     animated:NO
345                     scrollPosition: UITableViewScrollPositionMiddle];
346   }
347 }
348
349
350 /* We need this to respond to "shake" gestures
351  */
352 - (BOOL)canBecomeFirstResponder
353 {
354   return YES;
355 }
356
357 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
358 {
359 }
360
361 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
362 {
363 }
364
365
366 /* Shake means load a random screen saver.
367  */
368 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
369 {
370   if (motion != UIEventSubtypeMotionShake)
371     return;
372   NSMutableArray *a = [NSMutableArray arrayWithCapacity: 200];
373   for (NSArray *sec in letter_sections)
374     for (NSString *s in sec)
375       [a addObject: s];
376   int n = [a count];
377   if (! n) return;
378   NSString *which = [a objectAtIndex: (random() % n)];
379
380   SaverRunner *s = 
381     (SaverRunner *) [[UIApplication sharedApplication] delegate];
382   if (! s) return;
383   NSAssert ([s isKindOfClass:[SaverRunner class]], @"not a SaverRunner");
384   [self scrollTo: which];
385   [s loadSaver: which];
386 }
387
388
389 - (void)dealloc
390 {
391   for (int i = 0; i < countof(list_by_letter); i++)
392     [list_by_letter[i] release];
393   [letter_sections release];
394   [section_titles release];
395   [descriptions release];
396   [super dealloc];
397 }
398
399 @end
400
401
402 #endif // USE_IPHONE -- whole file