// This file is part of BOINC. // http://boinc.berkeley.edu // Copyright (C) 2008 University of California // // BOINC is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License // as published by the Free Software Foundation, // either version 3 of the License, or (at your option) any later version. // // BOINC is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with BOINC. If not, see . // // Mac_Saver_ModuleView.m // BOINC_Saver_Module // #import "Mac_Saver_ModuleView.h" #include #include #include // For NSInteger #include #include #include #include "mac_util.h" #ifndef NSInteger #if __LP64__ || NS_BUILD_32_LIKE_64 typedef long NSInteger; #else typedef int NSInteger; #endif #endif #ifndef CGFLOAT_DEFINED typedef float CGFloat; #endif // NSCompositeSourceOver is deprecated in OS 10.12 and is replaced by // NSCompositingOperationSourceOver, which is not defined before OS 10.12 #ifndef NSCompositingOperationSourceOver #define NSCompositingOperationSourceOver NSCompositeSourceOver #endif // NSCompositeCopy is deprecated in OS 10.12 and is replaced by // NSCompositingOperationCopy, which is not defined before OS 10.12 #ifndef NSCompositingOperationCopy #define NSCompositingOperationCopy NSCompositeCopy #endif // NSCriticalAlertStyle is deprecated in OS 10.12 and is replaced by // NSAlertStyleCritical, which is not defined before OS 10.12 #ifndef NSAlertStyleCritical #define NSAlertStyleCritical NSCriticalAlertStyle #endif void print_to_log_file(const char *format, ...); void strip_cr(char *buf); static double gSS_StartTime = 0.0; mach_port_t gEventHandle = 0; int gGoToBlank; // True if we are to blank the screen int gBlankingTime; // Delay in minutes before blanking the screen NSString *gPathToBundleResources = NULL; NSString *mBundleID = NULL; // our bundle ID NSImage *gBOINC_Logo = NULL; NSImage *gPreview_Image = NULL; int gTopWindowListIndex = -1; NSInteger myWindowNumber; NSRect gMovingRect; float gImageXIndent; float gTextBoxHeight; CGFloat gActualTextBoxHeight; NSPoint gCurrentPosition; NSPoint gCurrentDelta; CGContextRef myContext; bool isErased; #define TEXTBOXMINWIDTH 400.0 #define MINTEXTBOXHEIGHT 40.0 #define MAXTEXTBOXHEIGHT 300.0 #define TEXTBOXTOPBORDER 15 #define SAFETYBORDER 20.0 #define MINDELTA 8 #define MAXDELTA 16 int signof(float x) { return (x > 0.0 ? 1 : -1); } @implementation BOINC_Saver_ModuleView @synthesize NIBTopLevel; - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview { self = [ super initWithFrame:frame isPreview:isPreview ]; return self; } // If there are multiple displays, this may get called // multiple times (once for each display), so we need to guard // against any problems that may cause. - (void)startAnimation { NSBundle * myBundle; int newFrequency; int period; gEventHandle = NXOpenEventStatus(); initBOINCSaver(); if (gBOINC_Logo == NULL) { if (self) { myBundle = [ NSBundle bundleForClass:[self class]]; // grab the screensaver defaults if (mBundleID == NULL) { mBundleID = [ myBundle bundleIdentifier ]; } // Path to our copy of switcher utility application in this screensaver bundle if (gPathToBundleResources == NULL) { gPathToBundleResources = [ myBundle resourcePath ]; } ScreenSaverDefaults *defaults = [ ScreenSaverDefaults defaultsForModuleWithName:mBundleID ]; // try to load the version key, used to see if we have any saved settings mVersion = [defaults floatForKey:@"version"]; if (!mVersion) { // no previous settings so define our defaults gGoToBlank = NO; gBlankingTime = 1; // write out the defaults [ defaults setInteger:gGoToBlank forKey:@"GoToBlank" ]; [ defaults setInteger:gBlankingTime forKey:@"BlankingTime" ]; } if (mVersion < 2) { mVersion = 2; [ defaults setInteger:mVersion forKey:@"version" ]; period = getGFXDefaultPeriod() / 60; [ defaults setInteger:period forKey:@"DefaultPeriod" ]; period = getGFXSciencePeriod() / 60; [ defaults setInteger:period forKey:@"SciencePeriod" ]; period = getGGFXChangePeriod() / 60; [ defaults setInteger:period forKey:@"ChangePeriod" ]; // synchronize [defaults synchronize]; } // get defaults... gGoToBlank = [ defaults integerForKey:@"GoToBlank" ]; gBlankingTime = [ defaults integerForKey:@"BlankingTime" ]; period = [ defaults integerForKey:@"DefaultPeriod" ]; setGFXDefaultPeriod((double)(period * 60)); period = [ defaults integerForKey:@"SciencePeriod" ]; setGFXSciencePeriod((double)(period * 60)); period = [ defaults integerForKey:@"ChangePeriod" ]; setGGFXChangePeriod((double)(period * 60)); [ self setAutoresizesSubviews:YES ]; // make sure the subview resizes. NSString *fileName = [[ NSBundle bundleForClass:[ self class ]] pathForImageResource:@"boinc_ss_logo" ]; if (! fileName) { // What should we do in this case? return; } gBOINC_Logo = [[ NSImage alloc ] initWithContentsOfFile:fileName ]; gMovingRect.origin.x = 0.0; gMovingRect.origin.y = 0.0; gMovingRect.size = [gBOINC_Logo size]; if (gMovingRect.size.width < TEXTBOXMINWIDTH) { gImageXIndent = (TEXTBOXMINWIDTH - gMovingRect.size.width) / 2; gMovingRect.size.width = TEXTBOXMINWIDTH; } else { gImageXIndent = 0.0; } gTextBoxHeight = MINTEXTBOXHEIGHT; gMovingRect.size.height += gTextBoxHeight; gCurrentPosition.x = SAFETYBORDER + 1; gCurrentPosition.y = SAFETYBORDER + 1 + gTextBoxHeight; gCurrentDelta.x = 1.0; gCurrentDelta.y = 1.0; gActualTextBoxHeight = MINTEXTBOXHEIGHT; [ self setAnimationTimeInterval:1/8.0 ]; } } // Path to our copy of switcher utility application in this screensaver bundle if (gPathToBundleResources == NULL) { gPathToBundleResources = [ myBundle resourcePath ]; } [ super startAnimation ]; if ( [ self isPreview ] ) { [ self setAnimationTimeInterval:1.0/8.0 ]; return; } newFrequency = startBOINCSaver(); if (newFrequency) [ self setAnimationTimeInterval:1.0/newFrequency ]; gSS_StartTime = getDTime(); } // If there are multiple displays, this may get called // multiple times (once for each display), so we need to guard // against any problems that may cause. - (void)stopAnimation { [ super stopAnimation ]; if ( ! [ self isPreview ] ) { closeBOINCSaver(); } gTopWindowListIndex = -1; // if (gBOINC_Logo) { // [ gBOINC_Logo release ]; // } gBOINC_Logo = NULL; // gPathToBundleResources has been released by autorelease gPathToBundleResources = NULL; } // If there are multiple displays, this may get called // multiple times (once for each display), so we need to guard // against any problems that may cause. - (void)drawRect:(NSRect)rect { [ super drawRect:rect ]; // optionally draw here } // If there are multiple displays, this may get called // multiple times (once for each display), so we need to guard // against any problems that may cause. - (void)animateOneFrame { int newFrequency = 0; int coveredFreq = 0; NSRect theFrame = [ self frame ]; NSUInteger n; NSRect currentDrawingRect, eraseRect; NSPoint imagePosition; char *msg; CFStringRef cf_msg; double timeToBlock, frameStartTime = getDTime(); double idleTime = 0; HIThemeTextInfo textInfo; if ([ self isPreview ]) { #if 1 // Currently drawRect just draws our logo in the preview window if (gPreview_Image == NULL) { NSString *fileName = [[ NSBundle bundleForClass:[ self class ]] pathForImageResource:@"boinc" ]; if (fileName) { gPreview_Image = [[ NSImage alloc ] initWithContentsOfFile:fileName ]; } } if (gPreview_Image) { [ gPreview_Image setSize:theFrame.size ]; [ gPreview_Image drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 ]; } [ self setAnimationTimeInterval:1/1.0 ]; #else // Code for possible future use if we want to draw more in preview myContext = [[NSGraphicsContext currentContext] graphicsPort]; drawPreview(myContext); [ self setAnimationTimeInterval:1/30.0 ]; #endif return; } // For unkown reasons, OS 10.7 Lion screensaver and later delay several seconds // after user activity before calling stopAnimation, so we check user activity here if ((compareOSVersionTo(10, 7) >= 0) && ((getDTime() - gSS_StartTime) > 2.0)) { idleTime = CGEventSourceSecondsSinceLastEventType (kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType); if (idleTime < 1.5) { [ NSApp terminate:nil ]; } } myContext = [[NSGraphicsContext currentContext] graphicsPort]; // [myContext retain]; NSWindow *myWindow = [ self window ]; NSRect windowFrame = [ myWindow frame ]; if ( (windowFrame.origin.x != 0) || (windowFrame.origin.y != 0) ) { // Hide window on second display to aid in debugging #ifdef _DEBUG [ myWindow setLevel:kCGMinimumWindowLevel ]; NSInteger alpha = 0; [ myWindow setAlphaValue:alpha ]; // For OS 10.6 #endif return; // We draw only to main screen } NSRect viewBounds = [self bounds]; newFrequency = getSSMessage(&msg, &coveredFreq); // NOTE: My tests seem to confirm that the top window is always the first // window returned by [NSWindow windowNumbersWithOptions:] However, Apple's // documentation is unclear whether we can depend on this. So I have // added some safety by doing two things: // [1] Only use the windowNumbersWithOptions test when we have started // project graphics. // [2] Assume that our window is covered 45 seconds after starting project // graphics even if the windowNumbersWithOptions test did not indicate // that is so. // // getSSMessage() returns a non-zero value for coveredFreq only if we have started // project graphics. // // If we should use a different frequency when our window is covered by another // window, then check whether there is a window at a higher z-level than ours. // Assuming our window(s) are initially the top window(s), determine our position // in the window list when no graphics applications have covered us. if (gTopWindowListIndex < 0) { NSArray *theWindowList = [NSWindow windowNumbersWithOptions:NSWindowNumberListAllApplications]; myWindowNumber = [ myWindow windowNumber ]; gTopWindowListIndex = [theWindowList indexOfObjectIdenticalTo:[NSNumber numberWithInt:myWindowNumber]]; } if (coveredFreq) { if ( (msg != NULL) && (msg[0] != '\0') ) { NSArray *theWindowList = [NSWindow windowNumbersWithOptions:NSWindowNumberListAllApplications]; n = [theWindowList count]; if (gTopWindowListIndex < n) { if ([(NSNumber*)[theWindowList objectAtIndex:gTopWindowListIndex] integerValue] != myWindowNumber) { // Project graphics application has a window open above ours // Don't waste CPU cycles since our window is obscured by application graphics newFrequency = coveredFreq; msg = NULL; windowIsCovered(); } } } else { newFrequency = coveredFreq; } } // Clear the previous drawing area currentDrawingRect = gMovingRect; currentDrawingRect.origin.x = (float) ((int)gCurrentPosition.x); currentDrawingRect.origin.y += (float) ((int)gCurrentPosition.y - gTextBoxHeight); if ( (msg != NULL) && (msg[0] != '\0') ) { // Set direction of motion to "bounce" off edges of screen if (currentDrawingRect.origin.x <= SAFETYBORDER) { gCurrentDelta.x = (float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.; gCurrentDelta.y = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.y)) / 16.; } if ( (currentDrawingRect.origin.x + currentDrawingRect.size.width) >= (viewBounds.origin.x + viewBounds.size.width - SAFETYBORDER) ) { gCurrentDelta.x = -(float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.; gCurrentDelta.y = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.y)) / 16.; } if (currentDrawingRect.origin.y + gTextBoxHeight - gActualTextBoxHeight <= SAFETYBORDER) { gCurrentDelta.y = (float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.; gCurrentDelta.x = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.x)) / 16.; } if ( (currentDrawingRect.origin.y + currentDrawingRect.size.height) >= (viewBounds.origin.y + viewBounds.size.height - SAFETYBORDER) ) { gCurrentDelta.y = -(float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.; gCurrentDelta.x = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.x)) / 16.; } #if 0 // For testing gCurrentDelta.x = 0; gCurrentDelta.y = 0; #endif if (!isErased) { [[NSColor blackColor] set]; // Erasing only 2 small rectangles reduces screensaver's CPU usage by about 25% imagePosition.x = (float) ((int)gCurrentPosition.x + gImageXIndent); imagePosition.y = (float) (int)gCurrentPosition.y; eraseRect.origin.y = imagePosition.y; eraseRect.size.height = currentDrawingRect.size.height - gTextBoxHeight; if (gCurrentDelta.x > 0) { eraseRect.origin.x = imagePosition.x - 1; eraseRect.size.width = gCurrentDelta.x + 1; } else { eraseRect.origin.x = currentDrawingRect.origin.x + currentDrawingRect.size.width - gImageXIndent + gCurrentDelta.x - 1; eraseRect.size.width = -gCurrentDelta.x + 1; } eraseRect = NSInsetRect(eraseRect, -1, -1); NSRectFill(eraseRect); eraseRect.origin.x = imagePosition.x; eraseRect.size.width = currentDrawingRect.size.width - gImageXIndent - gImageXIndent; if (gCurrentDelta.y > 0) { eraseRect.origin.y = imagePosition.y; eraseRect.size.height = gCurrentDelta.y + 1; } else { eraseRect.origin.y = imagePosition.y + currentDrawingRect.size.height - gTextBoxHeight - 1; eraseRect.size.height = -gCurrentDelta.y + 1; } eraseRect = NSInsetRect(eraseRect, -1, -1); NSRectFill(eraseRect); eraseRect = currentDrawingRect; eraseRect.size.height = gTextBoxHeight; eraseRect = NSInsetRect(eraseRect, -1, -1); NSRectFill(eraseRect); isErased = true; } // Get the new drawing area gCurrentPosition.x += gCurrentDelta.x; gCurrentPosition.y += gCurrentDelta.y; imagePosition.x = (float) ((int)gCurrentPosition.x + gImageXIndent); imagePosition.y = (float) (int)gCurrentPosition.y; [ gBOINC_Logo drawAtPoint:imagePosition fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0 ]; if ( (msg != NULL) && (msg[0] != '\0') ) { cf_msg = CFStringCreateWithCString(NULL, msg, kCFStringEncodingMacRoman); CGRect bounds = CGRectMake((float) ((int)gCurrentPosition.x), viewBounds.size.height - imagePosition.y + TEXTBOXTOPBORDER, gMovingRect.size.width, MAXTEXTBOXHEIGHT ); CGContextSaveGState (myContext); CGContextTranslateCTM (myContext, 0, viewBounds.origin.y + viewBounds.size.height); CGContextScaleCTM (myContext, 1.0f, -1.0f); CTFontRef myFont = CTFontCreateWithName(CFSTR("Helvetica"), 20, NULL); HIThemeTextInfo theTextInfo = {kHIThemeTextInfoVersionOne, kThemeStateActive, kThemeSpecifiedFont, kHIThemeTextHorizontalFlushLeft, kHIThemeTextVerticalFlushTop, kHIThemeTextBoxOptionNone, kHIThemeTextTruncationNone, 0, false, 0, myFont }; textInfo = theTextInfo; HIThemeGetTextDimensions(cf_msg, (float)gMovingRect.size.width, &textInfo, NULL, &gActualTextBoxHeight, NULL); gActualTextBoxHeight += TEXTBOXTOPBORDER; // Use only APIs available in Mac OS 10.3.9 // HIThemeSetTextFill(kThemeTextColorWhite, NULL, myContext, kHIThemeOrientationNormal); // SetThemeTextColor(kThemeTextColorWhite, 32, true); CGFloat myWhiteComponents[] = {1.0, 1.0, 1.0, 1.0}; CGColorSpaceRef myColorSpace = CGColorSpaceCreateDeviceRGB (); CGColorRef myTextColor = CGColorCreate(myColorSpace, myWhiteComponents); CGContextSetFillColorWithColor(myContext, myTextColor); HIThemeDrawTextBox(cf_msg, &bounds, &textInfo, myContext, kHIThemeOrientationNormal); CGColorRelease(myTextColor); CGColorSpaceRelease(myColorSpace); CGContextRestoreGState (myContext); CFRelease(cf_msg); } gTextBoxHeight = MAXTEXTBOXHEIGHT + TEXTBOXTOPBORDER; gMovingRect.size.height = [gBOINC_Logo size].height + gTextBoxHeight; isErased = false; } else { // Empty or NULL message if (!isErased) { eraseRect = NSInsetRect(currentDrawingRect, -1, -1); [[NSColor blackColor] set]; isErased = true; NSRectFill(eraseRect); gTextBoxHeight = MAXTEXTBOXHEIGHT; gMovingRect.size.height = [gBOINC_Logo size].height + gTextBoxHeight; } } if (newFrequency) { [ self setAnimationTimeInterval:(1.0/newFrequency) ]; // setAnimationTimeInterval does not seem to be working, so we // throttle the screensaver directly here. timeToBlock = (1.0/newFrequency) - (getDTime() - frameStartTime); if (timeToBlock > 0.0) { doBoinc_Sleep(timeToBlock); } } } - (BOOL)hasConfigureSheet { return YES; } // Display the configuration sheet for the user to choose their settings - (NSWindow*)configureSheet { int period; // if we haven't loaded our configure sheet, load the nib named MyScreenSaver.nib if (!mConfigureSheet) { if ([[ NSBundle bundleForClass:[ self class ]] respondsToSelector: @selector(loadNibNamed: owner: topLevelObjects:)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-method-access" // [NSBundle loadNibNamed: owner: topLevelObjects:] is not available before OS 10.8 [ [ NSBundle bundleForClass:[ self class ]] loadNibNamed:@"BOINCSaver" owner:self topLevelObjects:&NIBTopLevel ]; #pragma clang diagnostic pop } #if __MAC_OS_X_VERSION_MIN_REQUIRED < 1080 else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // [NSBundle loadNibNamed: owner:] is deprecated in OS 10.8 [ NSBundle loadNibNamed:@"BOINCSaver" owner:self ]; #pragma clang diagnostic pop } #endif } // set the UI state [ mGoToBlankCheckbox setState:gGoToBlank ]; mBlankingTimeString = [[ NSString alloc ] initWithFormat:@"%d", gBlankingTime ]; [ mBlankingTimeTextField setStringValue:mBlankingTimeString ]; period = getGFXDefaultPeriod() / 60; mDefaultPeriodString = [[ NSString alloc ] initWithFormat:@"%d", period ]; [ mDefaultPeriodTextField setStringValue:mDefaultPeriodString ]; period = getGFXSciencePeriod() / 60; mSciencePeriodString = [[ NSString alloc ] initWithFormat:@"%d", period ]; [ mSciencePeriodTextField setStringValue:mSciencePeriodString ]; period = getGGFXChangePeriod() / 60; mChangePeriodString = [[ NSString alloc ] initWithFormat:@"%d", period ]; [ mChangePeriodTextField setStringValue:mChangePeriodString ]; return mConfigureSheet; } // Called when the user clicked the SAVE button - (IBAction) closeSheetSave:(id) sender { int period = 0; NSScanner *scanner, *scanner2; // get the defaults ScreenSaverDefaults *defaults = [ ScreenSaverDefaults defaultsForModuleWithName:mBundleID ]; // save the UI state gGoToBlank = [ mGoToBlankCheckbox state ]; mBlankingTimeString = [ mBlankingTimeTextField stringValue ]; gBlankingTime = [ mBlankingTimeString intValue ]; scanner = [ NSScanner scannerWithString:mBlankingTimeString]; if (![ scanner scanInt:&period ]) goto Bad; if (![ scanner isAtEnd ]) goto Bad; if ((period < 0) || (period > 999)) goto Bad; gBlankingTime = period; mDefaultPeriodString = [ mDefaultPeriodTextField stringValue ]; scanner2 = [ scanner initWithString:mDefaultPeriodString]; if (![ scanner2 scanInt:&period ]) goto Bad; if (![ scanner2 isAtEnd ]) goto Bad; if ((period < 0) || (period > 999)) goto Bad; setGFXDefaultPeriod((double)(period * 60)); mSciencePeriodString = [ mSciencePeriodTextField stringValue ]; scanner2 = [ scanner initWithString:mSciencePeriodString]; if (![ scanner2 scanInt:&period ]) goto Bad; if (![ scanner2 isAtEnd ]) goto Bad; if ((period < 0) || (period > 999)) goto Bad; setGFXSciencePeriod((double)(period * 60)); mChangePeriodString = [ mChangePeriodTextField stringValue ]; scanner2 = [ scanner initWithString:mChangePeriodString]; if (![ scanner2 scanInt:&period ]) goto Bad; if (![ scanner2 isAtEnd ]) goto Bad; if ((period < 0) || (period > 999)) goto Bad; setGGFXChangePeriod((double)(period * 60)); // write the defaults [ defaults setInteger:gGoToBlank forKey:@"GoToBlank" ]; [ defaults setInteger:gBlankingTime forKey:@"BlankingTime" ]; period = getGFXDefaultPeriod() / 60; [ defaults setInteger:period forKey:@"DefaultPeriod" ]; period = getGFXSciencePeriod() / 60; [ defaults setInteger:period forKey:@"SciencePeriod" ]; period = getGGFXChangePeriod() / 60; [ defaults setInteger:period forKey:@"ChangePeriod" ]; // synchronize [ defaults synchronize ]; // end the sheet [ NSApp endSheet:mConfigureSheet ]; return; Bad: ; // Empty statement is needed to prevent compiler error NSAlert *alert = [[NSAlert alloc] init]; [alert addButtonWithTitle:@"OK"]; [alert setMessageText:@"Please enter a number between 0 and 999."]; [alert setAlertStyle:NSCriticalAlertStyle]; if ([alert respondsToSelector: @selector(beginSheetModalForWindow: completionHandler:)]){ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-method-access" // [NSAlert beginSheetModalForWindow: completionHandler:] is not available before OS 10.9 [alert beginSheetModalForWindow:mConfigureSheet completionHandler:^(NSModalResponse returnCode){}]; #pragma clang diagnostic pop } #if __MAC_OS_X_VERSION_MIN_REQUIRED < 1090 else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // [NSAlert beginSheetModalForWindow: modalDelegate: didEndSelector: contextInfo:] is deprecated in OS 10.9 [alert beginSheetModalForWindow:mConfigureSheet modalDelegate:self didEndSelector:nil contextInfo:nil]; #pragma clang diagnostic pop } #endif } // Called when the user clicked the CANCEL button - (IBAction) closeSheetCancel:(id) sender { // nothing to configure [ NSApp endSheet:mConfigureSheet ]; } @end