Logo Search packages:      
Sourcecode: xdaliclock version File versions  Download package

DaliClockView.m

/* xdaliclock - a melting digital clock
 * Copyright (c) 1991-2006 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
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or
 * implied warranty.
 */

#import "DaliClockView.h"
#import "xdaliclock.h"
#import <sys/time.h>

/* Currently there are two redisplay methods implemented here.
   The Aqua way is straightforward, but maybe slow.
   The Quartz way is complicated, and a little flaky, and maybe faster
   but I can't tell.  Define this to do it the Quartz way.
 */
//#define BE_QUARTZY

@interface DaliClockView (ForwardDeclarations)
- (void)setForeground:(NSColor *)fg background:(NSColor *)bg;
- (void)clockTick;
- (void)colorTick;
@end

@implementation DaliClockView

/* This is called when the View is created (but pretty early: there are
   some things you can't do from here...)
 */
- (id)initWithFrame:(NSRect)frameRect
{
  if ((self = [super initWithFrame:frameRect]) != nil) {
    
    memset (&config, 0, sizeof(config));

    img = [[NSImage alloc] init];
    if (! img) abort();
    
    config.max_fps = 12;
    
    // Tell the color selector widget to show the "opacity" slider.
    [[NSColorPanel sharedColorPanel] setShowsAlpha:YES];
    
    // initialize the fonts and bitmaps
    [self setFrameSize:[self frame].size];
    
    [self clockTick];
    [self colorTick];
  }
  return self;
}

-(oneway void)dealloc
{
  [img release];
  [super dealloc];
}


/* Called when the user starts interactively resizing the window.
 */
- (void)viewWillStartLiveResize
{
}

/* Called when the user is done interactively sizing the window.
 */
- (void)viewDidEndLiveResize
{
  // Resize the frame one last time, now that we're finished dragging.
  [self setFrameSize:[self frame].size];
}


/* Called when the View is resized.
 */
- (void)setFrameSize:(NSSize)newSize
{
  [super setFrameSize:newSize];

  // If the user is interactively resizing the window, don't regenerate
  // the pixmap until we're done.  (We will just scale whatever image is
  // already on the window instead, which reduces flicker when the
  // target bitmap size shifts over a boundary).
  if ([self inLiveResize])  return;
  
  int ow = config.width;
  int oh = config.height;
  
   config.width  = newSize.width * 2;   // use the next-larger bitmap
   config.height = newSize.height * 2;
//  config.width = 1280;    // always use the biggest font image
//  config.height = 1024;
  
  render_bitmap_size (&config, &config.width, &config.height);
  
  if (config.render_state && (ow == config.width && oh == config.height))
    return;  // nothing to do
  
  // When the window is resized, re-create the bitmaps for the largest
  // font that will now fit in the window.
  //  
  if (config.bitmap) free (config.bitmap);
  config.bitmap = calloc (1, config.height * (config.width << 3));
  if (! config.bitmap) abort();
  
  if (pixmap) free (pixmap);
  pixmap = calloc (1, config.height * config.width * 4);
  if (! pixmap) abort();
  
  if (config.render_state)
    render_free (&config);
  render_init (&config);
}


/* Announce our willingness to accept keyboard input.
 */
- (BOOL)acceptsFirstResponder
{
  return YES;
}


/* Display date when mouse clicked in window.
 */
- (void)mouseDown:(NSEvent *)ev
{
  config.display_date_p = 1;
}

/* Back to time display when mouse released.
 */
- (void)mouseUp:(NSEvent *)ev
{
  config.display_date_p = 0;
}


/* Typing Ctrl-0 through Ctrl-9 and Ctrl-hyphen are a debugging hack.
 */
- (void)keyDown:(NSEvent *)ev
{
  NSString *ns = [ev charactersIgnoringModifiers];
  if (! [ns canBeConvertedToEncoding:NSASCIIStringEncoding])
    goto FAIL;
  const char *s = [ns cStringUsingEncoding:NSASCIIStringEncoding];
  if (! s) goto FAIL;
  if (strlen(s) != 1) goto FAIL;
  if (! ([ev modifierFlags] & NSControlKeyMask)) goto FAIL;
  if (*s == '-' || (*s >= '0' && *s <= '9'))
    config.test_hack = *s;
  else goto FAIL;
  return;
FAIL:
  [super keyDown:ev];
}


/* This is called from the timer (and other places) to change the colors.
 */
- (void)setForeground:(NSColor *)new_fg background:(NSColor *)new_bg
{
  if (fg != new_fg)
  {
    if (fg) [fg release];
    fg = [[new_fg colorUsingColorSpaceName:NSCalibratedRGBColorSpace] retain];
  }
  if (bg != new_bg)
  {
    if (bg) [bg release];
    bg = [[new_bg colorUsingColorSpaceName:NSCalibratedRGBColorSpace] retain];
  }
  [self setNeedsDisplay:TRUE];
}


/* The big kahuna refresh method.  Draw the current contents of the bitmap
   onto the window.  Sounds easy, doesn't it?
 */
- (void)drawRect:(NSRect)rect
{
  NSRect framerect, fromrect, torect;
  framerect.size = [self frame].size;
  framerect.origin.x = framerect.origin.y = 0;
  fromrect.origin.x = fromrect.origin.y = 0;
  fromrect.size.width = config.width;
  fromrect.size.height = config.height;
  
  // Scale the image to fill the window without changing its aspect ratio.
  //
  if (((float) framerect.size.width / (float) framerect.size.height) >
      ((float) config.width / (float) config.height)) {
    torect.size.height = framerect.size.height;
    torect.size.width = (framerect.size.height *
                          ((float) config.width / (float) config.height));
  } else {
    torect.size.width = framerect.size.width;
    torect.size.height = (framerect.size.width /
                          ((float) config.width /(float) config.height));
  }

  // put a margin between the numbers and the edge of the window.
  torect.size.width  *= 0.95;  // 5% horizontally
  torect.size.height *= 0.80;  // 20% vertically
  
//  torect.size = fromrect.size; // #### debugging: don't scale

  // center it in the window
  //
  torect.origin.x = (framerect.size.width  - torect.size.width ) / 2;
  torect.origin.y = (framerect.size.height - torect.size.height) / 2;

  
# ifndef BE_QUARTZY
  
  /* This is the straightforward Aqua way of doing things.
     It's simple, but I think it might be too slow.
     (It relies on the 32bpp pixmap created in colorizePixmap
     instead of using the 1bpp bitmap directly)
   */

  NSBitmapImageRep *imgrep = [NSBitmapImageRep alloc];
#if 1
  [imgrep
    initWithBitmapDataPlanes:&pixmap
                  pixelsWide:config.width
                  pixelsHigh:config.height
               bitsPerSample:8
             samplesPerPixel:4
                    hasAlpha:YES
                    isPlanar:NO
              colorSpaceName:NSCalibratedRGBColorSpace
                 bytesPerRow:config.width * 4
                bitsPerPixel:32
    ];
#else
    [imgrep
    initWithBitmapDataPlanes:&config.bitmap
                  pixelsWide:config.width
                  pixelsHigh:config.height
               bitsPerSample:1
             samplesPerPixel:1
                    hasAlpha:NO
                    isPlanar:YES
              colorSpaceName:NSCalibratedWhiteColorSpace
                 bytesPerRow:config.width >> 3
                bitsPerPixel:1
    ];
/*
 [imgrep colorizeByMappingGray:0.0
                      toColor:bg
                 blackMapping:bg
                 whiteMapping:fg];
 */
#endif
  if (! imgrep) abort();
  
  [img setSize:[imgrep size]];
  [img setScalesWhenResized:NO];

//  [[self window] setOpaque:NO];
  [[self window] setBackgroundColor:bg];

  [img addRepresentation:imgrep];
  [img drawInRect:torect
         fromRect:fromrect
        operation:NSCompositeSourceOver
         fraction:1.0];
  [img removeRepresentation:imgrep];
  [imgrep release];
  
# else /* BE_QUARTZY */
  
  /* This uses lower level Quartz/CoreGraphics calls to do the drawing.
     It might be more efficient, but it's way more complicated...
   */
  
  float fgr, fgg, fgb, fga;
  float bgr, bgg, bgb, bga;
  [fg getRed:&fgr green:&fgg blue:&fgb alpha:&fga];
  [bg getRed:&bgr green:&bgg blue:&bgb alpha:&bga];

  CGDataProviderRef provider = 
    CGDataProviderCreateWithData (NULL /* info */, config.bitmap,
                                  config.height * (config.width>>3),
                                  NULL);
  float decode = { 1.0, 0.0 };  /* invert pixels */
  CGImageRef fgmask=0, bgmask=0;
  fgmask = CGImageMaskCreate (config.width, config.height,
                              1 /* bitsPerComponent */,
                              1 /* bitsPerPixel */,
                              config.width>>3 /* bytesPerRow */,
                              provider, decode,
                              NO /* shouldInterpolate */
                              );
  if (fga < 1.0)
    bgmask = CGImageMaskCreate (config.width, config.height,
                                1 /* bitsPerComponent */,
                                1 /* bitsPerPixel */,
                                config.width>>3 /* bytesPerRow */,
                                provider, NULL /* decode */,
                                NO /* shouldInterpolate */
                                );
  CGDataProviderRelease (provider);
  
  CGContextRef cgc = [[NSGraphicsContext currentContext] graphicsPort];
  CGRect bgrect, fgrect;
  bgrect.origin.x    = framerect.origin.x;
  bgrect.origin.y    = framerect.origin.y;
  bgrect.size.width  = framerect.size.width;
  bgrect.size.height = framerect.size.height;
  fgrect.origin.x    = torect.origin.x;
  fgrect.origin.y    = torect.origin.y;
  fgrect.size.width  = torect.size.width;
  fgrect.size.height = torect.size.height;

  if (! bgmask) {
    /* foreground is solid; background is solid or transparent.
       we can just blast the background down as a rectangle.
     */
    CGContextSetRGBFillColor (cgc, bgr,bgg,bgb,bga);
    if (bga < 1.0) 
      CGContextClearRect (cgc, bgrect);
    CGContextFillRect (cgc, bgrect);
 
  } else {
    /* foreground is transparent; background is solid or transparent.
       we have to draw the background with a mask so that it doesn't
       interfere with the foreground.
     */
    CGContextSaveGState (cgc);
    CGContextSetRGBFillColor (cgc, bgr,bgg,bgb,bga);
    CGContextClearRect (cgc, bgrect);
    
    /* Ok, this is fucked up.  Since our "mask" bitmap is not as large as
       the window itself, and there's no way to specify a mask origin for
       the inverse mask (that is, say "draw everything *but* this mask)
       we have to fill in the rectangles outside the masked rectangle first.
       But, this leaves artifacts!  Even though the rectangles line up
       exactly, there's a transparent box getting left around the characters.
       Or, if the retangles overlap, then we get a too-dark box.  SHIT!
     */
    CGRect rects;
    rects = bgrect;
    rects.size.height = fgrect.origin.y - bgrect.origin.y /*+ 1*/;
    rects = rects;
    rects.origin.y = fgrect.origin.y + fgrect.size.height /*- 1*/;
    rects = rects;
    rects.origin.y = fgrect.origin.y;
    rects.size.width = fgrect.origin.x - bgrect.origin.x /*+ 1*/;
    rects.size.height = fgrect.size.height;
    rects = rects;
    rects.origin.x = bgrect.origin.x+fgrect.origin.x+fgrect.size.width/*-1*/;
    rects.size.width = bgrect.size.width-fgrect.size.width-fgrect.origin.x;
    rects.size.height = fgrect.size.height;
    CGContextFillRects (cgc, rects, 4);
    
    CGContextClipToMask (cgc, fgrect, bgmask);
    CGContextFillRect (cgc, fgrect);
    CGImageRelease (bgmask);
    bgmask = 0;
    CGContextRestoreGState (cgc);
  }
  
  CGContextSetRGBFillColor (cgc, fgr,fgg,fgb,fga);
  CGContextClipToMask (cgc, fgrect, fgmask);
  CGContextFillRect (cgc, fgrect);
  CGImageRelease (fgmask);
  fgmask = 0;

# endif /* BE_QUARTZY */
}


/* The code in digital.c gives us back a 1bpp bitmap (in config.bitmap)
   To actually render that in color with Aqua, we need to scale it up to
   an RGBA image (in self.pixmap) using the colors in "fg" and "bg".
  
   If we're doing the hairy Quartz stuff, this isn't needed.
*/
#ifndef BE_QUARTZY
- (void)colorizePixmap
{
  int x, y;
  unsigned char *scanin  = config.bitmap;
  unsigned char *scanout = pixmap;

  float rf, gf, bf, af;
  [fg getRed:&rf green:&gf blue:&bf alpha:&af];
  unsigned char fgr = rf * 255.0;
  unsigned char fgg = gf * 255.0;
  unsigned char fgb = bf * 255.0;
  unsigned char fga = af * 255.0;

  // in the bitmap, background is transparent (composited with window bg).
  unsigned char bgr = 0;
  unsigned char bgg = 0;
  unsigned char bgb = 0;
  unsigned char bga = 0;
  
  for (y = 0; y < config.height; y++)
  {
    for (x = 0; x < config.width; x++)
    {
      unsigned char bit = scanin & (1 << (7 - (x & 7)));
      unsigned char r, g, b, a;
      if (bit)
        r = fgr, g = fgg, b = fgb, a = fga;
      else
        r = bgr, g = bgg, b = bgb, a = bga;
      *scanout++ = r;
      *scanout++ = g;
      *scanout++ = b;
      *scanout++ = a;
    }
    scanin += (config.width + 7) >> 3;
  }
}

#endif /* !BE_QUARTZY */


/* When this timer goes off, we re-generate the bitmap/pixmap, 
   and mark the display as invalid.
*/
- (void)clockTick
{
  if (clockTimer && [clockTimer isValid])
  {
    [clockTimer invalidate];
    clockTimer = 0;
  }
  
  if (config.max_fps <= 0) abort();
  
  NSWindow *w = [self window];
  if (w && ![w isMiniaturized]) {   // noop if no window yet, or if iconified.
    struct timeval now;
    struct timezone tzp;
    gettimeofday (&now, &tzp);
    render_once (&config, now.tv_sec, now.tv_usec);
# ifndef BE_QUARTZY
    [self colorizePixmap];
# endif /* !BE_QUARTZY */
    // [[self window] invalidateShadow]; // windows with shadows flicker...
    [self setNeedsDisplay:TRUE];
  }
  
  // re-schedule the timer according to current fps.
  //
  float delay = 0.9 / config.max_fps;
  clockTimer = [NSTimer scheduledTimerWithTimeInterval:delay
                                                target:self
                                              selector:@selector(clockTick)
                                              userInfo:nil
                                               repeats:NO];  
}


/* When this timer goes off, we re-pick the foreground/background colors,
   and mark the display as invalid.
 */
- (void)colorTick
{
  if (colorTimer && [colorTimer isValid])
  {
    [colorTimer invalidate];
    colorTimer = 0;
  }
  
  if (config.max_cps <= 0) return;   // cycling is turned off, do nothing
  
  if ([self window]) {  // do nothing if no window yet

    float h, s, v, a;
    NSColor *fg2, *bg2;
    float tick = 1.0 / 360.0;   // cycle H by one degree per tick

    [fg getHue:&h saturation:&s brightness:&v alpha:&a];
    h += tick;
    while (h > 1.0) h -= 1.0;
    fg2 = [NSColor colorWithCalibratedHue:h saturation:s brightness:v alpha:a];

    [bg getHue:&h saturation:&s brightness:&v alpha:&a];
    h += tick * 0.91;   // cycle bg slightly slower than fg, for randomosity.
    while (h > 1.0) h -= 1.0;
    bg2 = [NSColor colorWithCalibratedHue:h saturation:s brightness:v alpha:a];

    [self setForeground:fg2 background:bg2];
  }
  
  /* re-schedule the timer according to current fps.
   */
  float delay = 1.0 / config.max_cps;
  colorTimer = [NSTimer scheduledTimerWithTimeInterval:delay
                                                target:self
                                              selector:@selector(colorTick)
                                              userInfo:nil
                                               repeats:NO];
}

- (void)updateCountdown {
  config.countdown = (usesCountdownTimer
                      ? [countdownDate timeIntervalSince1970]
                      : 0);
}

// hint for XCode popup menu
#pragma mark accessors

/* [ken] if you (1) only wanted these for bindings purposes (true) and
   (2) they were dumb methods that just manipulated ivars of the same
   name (not true, they mostly write into config), then we would not
   have to write them.  I use the freeware Accessorizer to give me
   accessor templates.
 */
- (int)hourStyle { return !config.twelve_hour_p; }
- (void)setHourStyle:(int)aHourStyle
{
  config.twelve_hour_p = !aHourStyle;
}

- (int)timeStyle { return config.time_mode; }
- (void)setTimeStyle:(int)aTimeStyle
{
  if (config.time_mode != aTimeStyle) {
    config.time_mode = aTimeStyle;
    config.width++;  // kludge: force regeneration of bitmap
    [self setFrameSize:[self frame].size];
  }
}

- (int)dateStyle { return config.date_mode; }
- (void)setDateStyle:(int)aDateStyle
{
  config.date_mode = aDateStyle;
}

- (float)cycleSpeed { return (float)config.max_cps; }
- (void)setCycleSpeed:(float)aCycleSpeed
{
  if (config.max_cps != aCycleSpeed) {
    config.max_cps = (int)aCycleSpeed;
    [self colorTick];
  }
}

- (int)usesCountdownTimer { return usesCountdownTimer; }
- (void)setUsesCountdownTimer:(int)flag
{
  usesCountdownTimer = flag;
  [self updateCountdown];
}

- (NSDate *)countdownDate { return [[countdownDate retain] autorelease]; }
- (void)setCountdownDate:(NSDate *)aCountdownDate
{
  if (countdownDate != aCountdownDate) {
    [countdownDate release];
    countdownDate = [aCountdownDate retain];
    [self updateCountdown];
  }
}

- (NSColor *)initialForegroundColor
{
  return [[initialForegroundColor retain] autorelease];
}

- (void)setInitialForegroundColor:(NSColor *)anInitialForegroundColor
{
  if (initialForegroundColor != anInitialForegroundColor) {
    [initialForegroundColor release];
    initialForegroundColor = [anInitialForegroundColor copy];
    [self setForeground:initialForegroundColor
             background:initialBackgroundColor];
  }
}

- (NSColor *)initialBackgroundColor
{
  return [[initialBackgroundColor retain] autorelease];
}

- (void)setInitialBackgroundColor:(NSColor *)anInitialBackgroundColor
{
  if (initialBackgroundColor != anInitialBackgroundColor) {
    [initialBackgroundColor release];
    initialBackgroundColor = [anInitialBackgroundColor copy];
    [self setForeground:initialForegroundColor
             background:initialBackgroundColor];
  }
}

@end

Generated by  Doxygen 1.6.0   Back to index