From http://www.jwz.org/xscreensaver/xscreensaver-5.22.tar.gz
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
index 12980fa7cf2f6222c15a3dd8394e797ca46dda62..ebf33827ea9747ba397588fbe336cfe45a1bc529 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -48,6 +48,7 @@
 #endif // USE_IPHONE
 
 #undef LABEL_ABOVE_SLIDER
+#define USE_HTML_LABELS
 
 
 #pragma mark XML Parser
@@ -191,6 +192,222 @@ typedef enum { SimpleXMLCommentKind,
 # endif // !USE_PICKER_VIEW
 
 
+# pragma mark Implementing labels with clickable links
+
+#if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
+
+@interface HTMLLabel : UIView <UIWebViewDelegate>
+{
+  NSString *html;
+  UIFont *font;
+  UIWebView *webView;
+}
+
+@property(nonatomic, retain) NSString *html;
+@property(nonatomic, retain) UIWebView *webView;
+
+- (id) initWithHTML:(NSString *)h font:(UIFont *)f;
+- (id) initWithText:(NSString *)t font:(UIFont *)f;
+- (void) setHTML:(NSString *)h;
+- (void) setText:(NSString *)t;
+- (void) sizeToFit;
+
+@end
+
+@implementation HTMLLabel
+
+@synthesize html;
+@synthesize webView;
+
+- (id) initWithHTML:(NSString *)h font:(UIFont *)f
+{
+  self = [super init];
+  if (! self) return 0;
+  font = [f retain];
+  webView = [[UIWebView alloc] init];
+  webView.delegate = self;
+  webView.dataDetectorTypes = UIDataDetectorTypeNone;
+  self.   autoresizingMask = (UIViewAutoresizingFlexibleWidth |
+                              UIViewAutoresizingFlexibleHeight);
+  webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
+                              UIViewAutoresizingFlexibleHeight);
+  [self addSubview: webView];
+  [self setHTML: h];
+  return self;
+}
+
+- (id) initWithText:(NSString *)t font:(UIFont *)f
+{
+  self = [self initWithHTML:@"" font:f];
+  if (! self) return 0;
+  [self setText: t];
+  return self;
+}
+
+
+- (void) setHTML: (NSString *)h
+{
+  if (! h) return;
+  [h retain];
+  if (html) [html release];
+  html = h;
+  NSString *h2 =
+    [NSString stringWithFormat:
+                @"<!DOCTYPE HTML PUBLIC "
+                   "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
+                   " \"http://www.w3.org/TR/html4/loose.dtd\">"
+                 "<HTML>"
+                  "<HEAD>"
+//                   "<META NAME=\"viewport\" CONTENT=\""
+//                      "width=device-width"
+//                      "initial-scale=1.0;"
+//                      "maximum-scale=1.0;\">"
+                   "<STYLE>"
+                    "<!--\n"
+                      "body {"
+                      " margin: 0; padding: 0; border: 0;"
+                      " font-family: \"%@\";"
+                      " font-size: %.4fpx;"    // Must be "px", not "pt"!
+                      " line-height: %.4fpx;"   // And no spaces before it.
+                      "}"
+                    "\n//-->\n"
+                   "</STYLE>"
+                  "</HEAD>"
+                  "<BODY>"
+                   "%@"
+                  "</BODY>"
+                 "</HTML>",
+              [font fontName],
+              [font pointSize],
+              [font lineHeight],
+              h];
+  [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
+}
+
+
+static char *anchorize (const char *url);
+
+- (void) setText: (NSString *)t
+{
+  t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"];
+  t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
+  t = [t stringByReplacingOccurrencesOfString:@">" withString:@"&gt;"];
+  t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
+  t = [t stringByReplacingOccurrencesOfString:@"<P>  "
+         withString:@"<P> &nbsp; &nbsp; &nbsp; &nbsp; "];
+  t = [t stringByReplacingOccurrencesOfString:@"\n "
+         withString:@"<BR> &nbsp; &nbsp; &nbsp; &nbsp; "];
+
+  NSString *h = @"";
+  for (NSString *s in
+         [t componentsSeparatedByCharactersInSet:
+              [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
+    if ([s hasPrefix:@"http://"] ||
+        [s hasPrefix:@"https://"]) {
+      char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
+      NSString *a2 = [NSString stringWithCString: anchor
+                               encoding: NSUTF8StringEncoding];
+      s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
+      free (anchor);
+    }
+    h = [NSString stringWithFormat: @"%@ %@", h, s];
+  }
+  [self setHTML: h];
+}
+
+
+-(BOOL) webView:(UIWebView *)wv
+        shouldStartLoadWithRequest:(NSURLRequest *)req
+        navigationType:(UIWebViewNavigationType)type
+{
+  // Force clicked links to open in Safari, not in this window.
+  if (type == UIWebViewNavigationTypeLinkClicked) {
+    [[UIApplication sharedApplication] openURL:[req URL]];
+    return NO;
+  }
+  return YES;
+}
+
+
+- (void) setFrame: (CGRect)r
+{
+  [super setFrame: r];
+  r.origin.x = 0;
+  r.origin.y = 0;
+  [webView setFrame: r];
+  [self setHTML: html];
+  [webView reload];
+}
+
+
+- (NSString *) stripTags:(NSString *)str
+{
+  NSString *result = @"";
+
+  str = [str stringByReplacingOccurrencesOfString:@"<P>"
+             withString:@"<BR><BR>"
+             options:NSCaseInsensitiveSearch
+             range:NSMakeRange(0, [str length])];
+  str = [str stringByReplacingOccurrencesOfString:@"<BR>"
+             withString:@"\n"
+             options:NSCaseInsensitiveSearch
+             range:NSMakeRange(0, [str length])];
+
+  for (NSString *s in [str componentsSeparatedByString: @"<"]) {
+    NSRange r = [s rangeOfString:@">"];
+    if (r.length > 0)
+      s = [s substringFromIndex: r.location + r.length];
+    result = [result stringByAppendingString: s];
+  }
+  return result;
+}
+
+
+- (void) sizeToFit
+{
+  CGRect r = [self frame];
+
+  /* It would be sensible to just ask the UIWebView how tall the page is,
+     instead of hoping that NSString and UIWebView measure fonts and do
+     wrapping in exactly the same way, but I can't make that work.
+     Maybe because it loads async?
+   */
+# if 0
+  r.size.height = [[webView
+                     stringByEvaluatingJavaScriptFromString:
+                       @"document.body.offsetHeight"]
+                    doubleValue];
+# else
+  NSString *text = [self stripTags: html];
+  CGSize s = r.size;
+  s.height = 999999;
+  s = [text sizeWithFont: font
+            constrainedToSize: s
+            lineBreakMode:NSLineBreakByWordWrapping];
+
+  // GAAAH. Add one more line, or the UIWebView is still scrollable!
+  // The text is sized right, but it lets you scroll it up anyway.
+  s.height += [font pointSize];
+
+  r.size.height = s.height;
+# endif
+
+  [self setFrame: r];
+}
+
+
+- (void) dealloc
+{
+  [html release];
+  [font release];
+  [webView release];
+  [super dealloc];
+}
+
+@end
+
+#endif // USE_IPHONE && USE_HTML_LABELS
+
 
 @interface XScreenSaverConfigSheet (Private)
 
@@ -219,6 +436,7 @@ static void layout_group (NSView *group, BOOL horiz_p);
 # define LEFT_LABEL_WIDTH 70   // width of all left labels
 # define LINE_SPACING     10   // leading between each line
 
+# define FONT_SIZE       17   // Magic hardcoded UITableView font size.
 
 #pragma mark Talking to the resource database
 
@@ -315,7 +533,12 @@ static void layout_group (NSView *group, BOOL horiz_p);
   if ([active_text_field canResignFirstResponder])
     [active_text_field resignFirstResponder];
   NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
-  double v = [sender value];
+
+  // Hacky API. See comment in InvertedSlider.m.
+  double v = ([sender isKindOfClass: [InvertedSlider class]]
+              ? [(InvertedSlider *) sender transformedValue]
+              : [sender value]);
+
   if (v == (int) v)
     [userDefaultsController setInteger:v forKey:pref_key];
   else
@@ -471,7 +694,11 @@ static void layout_group (NSView *group, BOOL horiz_p);
 
   if ([control isKindOfClass:[UISlider class]]) {
     sel = @selector(sliderAction:);
-    [(UISlider *) control setValue: dval];
+    // Hacky API. See comment in InvertedSlider.m.
+    if ([control isKindOfClass:[InvertedSlider class]])
+      [(InvertedSlider *) control setTransformedValue: dval];
+    else
+      [(UISlider *) control setValue: dval];
   } else if ([control isKindOfClass:[UISwitch class]]) {
     sel = @selector(switchAction:);
     [(UISwitch *) control setOn: ((int) dval != 0)];
@@ -681,6 +908,8 @@ anchorize (const char *url)
 }
 
 
+#if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
+
 /* Converts any http: URLs in the given text field to clickable links.
  */
 static void
@@ -765,6 +994,9 @@ hreffify (NSText *nstext)
 # endif
 }
 
+#endif /* !USE_IPHONE || !USE_HTML_LABELS */
+
+
 
 #pragma mark Creating controls from XML
 
@@ -812,6 +1044,26 @@ hreffify (NSText *nstext)
     if ([val length] == 0)
       [dict removeObjectForKey:key];
   }
+
+# ifdef USE_IPHONE
+  // Kludge for starwars.xml:
+  // If there is a "_low-label" and no "_label", but "_low-label" contains
+  // spaces, divide them.
+  NSString *lab = [dict objectForKey:@"_label"];
+  NSString *low = [dict objectForKey:@"_low-label"];
+  if (low && !lab) {
+    NSArray *split =
+      [[[low stringByTrimmingCharactersInSet:
+               [NSCharacterSet whitespaceAndNewlineCharacterSet]]
+         componentsSeparatedByString: @"  "]
+        filteredArrayUsingPredicate:
+          [NSPredicate predicateWithFormat:@"length > 0"]];
+    if (split && [split count] == 2) {
+      [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
+      [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
+    }
+  }
+# endif // USE_IPHONE
 }
 
 
@@ -856,7 +1108,8 @@ hreffify (NSText *nstext)
                  [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
   [lab setBackgroundColor:[UIColor clearColor]];
   [lab setNumberOfLines:0]; // unlimited
-  [lab setLineBreakMode:UILineBreakModeWordWrap];
+  // [lab setLineBreakMode:UILineBreakModeWordWrap];
+  [lab setLineBreakMode:NSLineBreakByTruncatingHead];
   [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
                              UIViewAutoresizingFlexibleHeight)];
 # endif // USE_IPHONE
@@ -1059,7 +1312,7 @@ hreffify (NSText *nstext)
       [lab setFrame:rect];
       [self placeChild:lab on:parent];
 # else  // USE_IPHONE
-      [lab setTextAlignment: UITextAlignmentRight];
+      [lab setTextAlignment: NSTextAlignmentRight];
       [self placeChild:lab on:parent right:(label ? YES : NO)];
 # endif // USE_IPHONE
 
@@ -1429,10 +1682,8 @@ set_menu_item_object (NSMenuItem *item, NSObject *obj)
   for (NSArray *item in items) {
     RadioButton *b = [[RadioButton alloc] initWithIndex:i 
                                           items:items];
-    [b setFont:[NSFont boldSystemFontOfSize:
-                         // #### Fucking hardcoded UITableView font size BS!
-                         17 // [NSFont systemFontSize]
-                ]];
+    [b setLineBreakMode:NSLineBreakByTruncatingHead];
+    [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
     [self placeChild:b on:parent];
     i++;
   }
@@ -1479,11 +1730,25 @@ set_menu_item_object (NSMenuItem *item, NSObject *obj)
   hreffify (lab);
   boldify (lab);
   [lab sizeToFit];
+
 # else  // USE_IPHONE
+
+#  ifndef USE_HTML_LABELS
+
   UILabel *lab = [self makeLabel:text];
   [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
   hreffify (lab);
+
+#  else  // USE_HTML_LABELS
+  HTMLLabel *lab = [[HTMLLabel alloc] 
+                     initWithText:text
+                     font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
+  [lab setFrame:rect];
+  [lab sizeToFit];
+#  endif // USE_HTML_LABELS
+
   [self placeSeparator];
+
 # endif // USE_IPHONE
 
   [self placeChild:lab on:parent];
@@ -1536,11 +1801,10 @@ set_menu_item_object (NSMenuItem *item, NSObject *obj)
 
   txt.adjustsFontSizeToFitWidth = YES;
   txt.textColor = [UIColor blackColor];
-  // #### Fucking hardcoded UITableView font size BS!
-  txt.font = [UIFont systemFontOfSize: 17];
+  txt.font = [UIFont systemFontOfSize: FONT_SIZE];
   txt.placeholder = @"";
   txt.borderStyle = UITextBorderStyleRoundedRect;
-  txt.textAlignment = UITextAlignmentRight;
+  txt.textAlignment = NSTextAlignmentRight;
   txt.keyboardType = UIKeyboardTypeDefault;  // Full kbd
   txt.autocorrectionType = UITextAutocorrectionTypeNo;
   txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
@@ -2011,7 +2275,6 @@ find_text_field_of_button (NSButton *button)
 
 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
 {
-# ifndef USE_IPHONE
   /*
     [x]  Grab desktop images
     [ ]  Choose random image:
@@ -2026,11 +2289,19 @@ find_text_field_of_button (NSButton *button)
 
   NSXMLElement *node2;
 
+# ifndef USE_IPHONE
+#  define SCREENS "Grab desktop images"
+#  define PHOTOS  "Choose random images"
+# else
+#  define SCREENS "Grab screenshots"
+#  define PHOTOS  "Use photo library"
+# endif
+
   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
   [node2 setAttributesAsDictionary:
           [NSDictionary dictionaryWithObjectsAndKeys:
                         @"grabDesktopImages",   @"id",
-                        @"Grab desktop images", @"_label",
+                        @ SCREENS,              @"_label",
                         @"-no-grab-desktop",    @"arg-unset",
                         nil]];
   [self makeCheckbox:node2 on:parent];
@@ -2039,7 +2310,7 @@ find_text_field_of_button (NSButton *button)
   [node2 setAttributesAsDictionary:
           [NSDictionary dictionaryWithObjectsAndKeys:
                         @"chooseRandomImages",    @"id",
-                        @"Choose random images",  @"_label",
+                        @ PHOTOS,                 @"_label",
                         @"-choose-random-images", @"arg-set",
                         nil]];
   [self makeCheckbox:node2 on:parent];
@@ -2054,6 +2325,10 @@ find_text_field_of_button (NSButton *button)
   [self makeFileSelector:node2 on:parent
         dirsOnly:YES withLabel:YES editable:YES];
 
+# undef SCREENS
+# undef PHOTOS
+
+# ifndef USE_IPHONE
   // Add a second, explanatory label below the file/URL selector.
 
   LABEL *lab2 = 0;
@@ -2066,7 +2341,7 @@ find_text_field_of_button (NSButton *button)
   r2.origin.y += 14;
   [lab2 setFrameOrigin:r2.origin];
   [lab2 release];
-# endif // !USE_IPHONE
+# endif // USE_IPHONE
 }
 
 
@@ -2599,6 +2874,8 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
   }
 
   saver_name = [self parseXScreenSaverTag: node];
+  saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
+                           withString:@""];
   [saver_name retain];
   
 # ifndef USE_IPHONE
@@ -2698,7 +2975,7 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
 - (CGFloat)pickerView:(UIPickerView *)pv
            rowHeightForComponent:(NSInteger)column
 {
-  return [NSFont systemFontSize] * 1.5; // #### WHAT
+  return FONT_SIZE;
 }
 
 - (CGFloat)pickerView:(UIPickerView *)pv
@@ -2753,6 +3030,10 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
                              style: UIBarButtonItemStyleBordered
                              target:self
                              action:@selector(resetAction:)]];
+  NSString *s = saver_name;
+  if ([self view].frame.size.width > 320)
+    s = [s stringByAppendingString: @" Settings"];
+  [self navigationItem].title = s;
 }
 
 
@@ -2778,8 +3059,8 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
               titleForHeaderInSection:(NSInteger)section
 {
   // Titles above each vertically-stacked white box.
-  if (section == 0)
-    return [saver_name stringByAppendingString:@" Settings"];
+//  if (section == 0)
+//    return [saver_name stringByAppendingString:@" Settings"];
   return nil;
 }
 
@@ -2814,7 +3095,20 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
     [t sizeToFit];
     r = t.frame;
     h = r.size.height + LINE_SPACING * 3;
+# ifdef USE_HTML_LABELS
+
+  } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
+    
+    HTMLLabel *t = (HTMLLabel *) ctl;
+    CGRect r = t.frame;
+    r.size.width = [tv frame].size.width;
+    r.size.width -= LEFT_MARGIN * 2;
+    [t setFrame:r];
+    [t sizeToFit];
+    r = t.frame;
+    h = r.size.height + LINE_SPACING * 3;
 
+# endif // USE_HTML_LABELS
   } else {
     CGFloat h2 = [ctl frame].size.height;
     h2 += LINE_SPACING * 2;
@@ -2955,9 +3249,8 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
     case 2:
       {
         // With 2 elements, the first of the pair must be a label.
-        ctl = [set objectAtIndex: 0];
-        NSAssert ([ctl isKindOfClass:[UILabel class]], @"unhandled type");
-        cell.textLabel.text = [(UILabel *) ctl text];
+        UILabel *label = (UILabel *) [set objectAtIndex: 0];
+        NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
         ctl = [set objectAtIndex: 1];
 
         r = [ctl frame];
@@ -2974,6 +3267,24 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
         }
         r.origin.y = (p.size.height - r.size.height) / 2;
         [ctl setFrame:r];
+
+        // Make a box.
+        NSView *box = [[UIView alloc] initWithFrame:p];
+        [box addSubview: ctl];
+
+        // cell.textLabel.text = [(UILabel *) ctl text];
+        r = [label frame];
+        r.origin.x = LEFT_MARGIN;
+        r.origin.y = 0;
+        r.size.width  = [ctl frame].origin.x - r.origin.x;
+        r.size.height = p.size.height;
+        [label setFrame:r];
+        [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
+        label.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
+        box.  autoresizingMask = UIViewAutoresizingFlexibleWidth;
+        [box addSubview: label];
+
+        ctl = box;
       }
       break;
     case 3:
@@ -3008,7 +3319,6 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
 
         // Top label goes above, flush center/top.
         if (top) {
-          // [top setFont:[[cell textLabel] font]];  // 0 point?
           r.size = [[top text] sizeWithFont:[top font]
                                constrainedToSize:
                                  CGSizeMake (p.size.width - LEFT_MARGIN*2,
@@ -3049,7 +3359,14 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
           top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin|
                                   UIViewAutoresizingFlexibleRightMargin);
 # else
-          cell.textLabel.text = [top text];
+          r = [top frame];
+          r.origin.x = LEFT_MARGIN;
+          r.origin.y = 0;
+          r.size.width  = [mid frame].origin.x - r.origin.x;
+          r.size.height = p.size.height;
+          [top setFrame:r];
+          top.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
+          [ctl addSubview: top];
 # endif
         }
         [ctl addSubview: left];
@@ -3082,6 +3399,14 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
 # endif // USE_PICKER_VIEW
   }
 
+  if ([ctl isKindOfClass:[UILabel class]]) {
+    // Make label full height to allow text to line-wrap if necessary.
+    r = [ctl frame];
+    r.origin.y = p.origin.y;
+    r.size.height = p.size.height;
+    [ctl setFrame:r];
+  }
+
   [cell.contentView addSubview: ctl];
 
   return cell;