rootxx-cocoa.mm
// @(#)root/rootxx-cocoa.mm:$Id$
// Author: Timur Pocheptsov 22/01/2014
//////////////////////////////////////////////////////////////////////////
// //
// Rootxx-cocoa //
// //
// Cocoa based routines used to display the splash screen for rootx, //
// the root front-end program. //
// //
// WaitChildGeneric/StayUp/-main (ROOTWaitpidThread) copy-pasted/based //
// on rootx.cxx/rootxx.cxx (author - Fons Rademakers). //
// //
//////////////////////////////////////////////////////////////////////////
#include <cassert>
#include <fstream>
#include <cstdlib>
#include <csignal>
#include <string>
#include <cerrno>
#include <list>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <pwd.h>
#include <Cocoa/Cocoa.h>
#include "CocoaUtils.h"
#include "RConfigure.h"
#include "RVersion.h"
#include "rootcoreteam.h"
//
//'root' with Cocoa is a quite special application. In principle, it's a background process,
//but it can create a GUI (in our case it's a simple splash-screen - a window with a ROOT's logo
//and "Credits" info. The first version was converting its process into foreground, which
//leads to some undesired effects like ... lost keyboard focus. Now this process never explicitly
//converted into foreground and [NSApp activateAppIgnorinxxxx] is never called.
//Instead, special window style is used - NSNonactivatingPanelMask (splash-screen's class derives NSPanel)
//and I still have to specify activation policy (to avoid NSInternalInconsistencyException on window creation).
//Also, I set a window level to NSFloatingPanelWindow (otherwise window is not visible).
//This behavior is still different from how X11 based root works on Mac, but is very close.
//
namespace ROOT {
namespace ROOTX {
//This had internal linkage before, now must be accessible from rootx-cocoa.mm.
extern int gChildpid;
}
}
namespace {
NSString * const gConception = @"Rene Brun, Fons Rademakers\n\n";
bool showAboutInfo = false;
}
//ROOTSplashScreenView: content view for our panel (splash screen window)
//with a background (ROOT's logo) + scrollview and textview to show info
//about contributors.
@interface ROOTSplashScreenView : NSView
@end
@implementation ROOTSplashScreenView {
NSImage *backgroundImage;
NSScrollView *scrollView;
NSTextView *textView;
NSMutableDictionary *versionTextAttributes;
}
//_________________________________________________________________
- (id) initWithImage : (NSImage *) image text : (NSAttributedString *) textToScroll aboutMode : (BOOL) about
{
assert(image != nil && "initWithImage:text:, parameter 'image' is nil");
assert(textToScroll != nil && "initWithImage:text:, parameter 'textToScroll' is nil");
using ROOT::MacOSX::Util::NSScopeGuard;
NSInteger pixelWidth = 0, pixelHeight = 0;
{//For autorelease pool.
const NSScopeGuard<NSAutoreleasePool> pool([[NSAutoreleasePool alloc] init]);
NSArray * const reps = image.representations;
for (NSImageRep *rep in reps) {
//Find a max. may be?
pixelWidth = rep.pixelsWide;
pixelHeight = rep.pixelsHigh;
//Now I stop on the first representation
//(as I know for the current image - it's ok)
break;
}
}
//minimal sizes required by text view's position (which is 'hardcoded' and
//must be the same as in rootxx (X11 version).
// assert(imageSize.width >= 300 && imageSize.height >= 285 &&
// "initWithImage:text:, unexpected background image sizes");
self = [super initWithFrame : CGRectMake(0, 0, pixelWidth, pixelHeight)];
if (self) {
//Let's create our child views.
backgroundImage = [image retain];
CGRect scrollRect = CGRectMake(110., 25., 455., 80.);
scrollView = [[NSScrollView alloc] initWithFrame : scrollRect];
[self addSubview : scrollView];
scrollRect.origin = CGPoint();
textView = [[NSTextView alloc] initWithFrame : scrollRect];
[textView setEditable : NO];
[[textView textStorage] setAttributedString : textToScroll];
[scrollView setDocumentView : textView];
[scrollView setBorderType : NSNoBorder];
if (about) {
[textView setTextContainerInset : CGSizeMake(0., scrollRect.size.height)];
[textView scrollPoint : NSMakePoint(0., scrollRect.size.height)];
}
const NSScopeGuard<NSShadow> dropShadow([[NSShadow alloc] init]);
[dropShadow.Get() setShadowColor:[NSColor blackColor]];
[dropShadow.Get() setShadowBlurRadius : 10.];
[self setShadow : dropShadow.Get()];
//Hehehehe.
[scrollView setDrawsBackground : NO];
[textView setDrawsBackground : NO];
//
if (NSFont * const font = [NSFont fontWithName : @"Tahoma" size : 12.]) {
//Will be some special font here.
NSScopeGuard<NSMutableDictionary> dict([[NSMutableDictionary alloc] init]);
[dict.Get() setObject : font forKey : NSFontAttributeName];
[dict.Get() setObject : [NSColor whiteColor] forKey : NSForegroundColorAttributeName];
versionTextAttributes = dict.Get();
dict.Release();
}
}
return self;
}
//_________________________________________________________________
- (void) dealloc
{
[backgroundImage release];
[textView release];
[scrollView release];
[versionTextAttributes release];
[super dealloc];
}
//Background image.
//_________________________________________________________________
- (void) drawRect : (NSRect) rect
{
#pragma unused(rect)
assert(backgroundImage != nil && "drawRect:, backgroundImage is nil");
CGRect frame = self.frame;
frame.origin = CGPoint();
const CGSize imageSize = backgroundImage.size;
[backgroundImage drawInRect : frame
fromRect : CGRectMake(0., 0., imageSize.width, imageSize.height)
operation : NSCompositeSourceOver
fraction : 1.];
//Let's now draw a version.
if (versionTextAttributes) {
if (NSString * const version = [NSString stringWithFormat : @"Version %s", ROOT_RELEASE])
[version drawAtPoint : CGPointMake(frame.size.width - 90., 5.) withAttributes : versionTextAttributes];
}
}
//Animation.
//_________________________________________________________________
- (void) scrollText
{
const CGFloat scrollAmountPixels = 1.;
// How far have we scrolled so far?
const CGFloat currentScrollAmount = [scrollView documentVisibleRect].origin.y;
if (currentScrollAmount + scrollAmountPixels >= textView.frame.size.height - scrollView.frame.size.height)
[textView scrollPoint : NSMakePoint(0., 0.)];//Make a "loop".
else {
[textView scrollPoint : NSMakePoint(0., currentScrollAmount + scrollAmountPixels)];
}
// If anything overlaps the text we just scrolled, it won’t get redraw by the
// scrolling, so force everything in that part of the panel to redraw.
NSRect scrollViewFrame = [scrollView bounds];
scrollViewFrame = [self convertRect : scrollViewFrame fromView : scrollView];
// Redraw everything which overlaps it.
[self setNeedsDisplayInRect : scrollViewFrame];
}
@end
//To be set from a signal handler.
namespace {
bool popupDone = false;
}
//Our "top-level-window" - borderless panel.
@interface ROOTSplashScreenPanel : NSPanel
@end
@implementation ROOTSplashScreenPanel
//Events and behaviour.
//_________________________________________________________________
- (BOOL) canBecomeMainWindow
{
return NO;//YES;
}
//_________________________________________________________________
- (BOOL) canBecomeKeyWindow
{
return NO;//YES;
}
//_________________________________________________________________
- (void) sendEvent : (NSEvent *) theEvent
{
if ([theEvent type] == NSKeyDown) {
popupDone = true;
return;
}
if ([theEvent type] == NSLeftMouseDown || [theEvent type] == NSRightMouseDown) {
popupDone = true;
return;
}
[super sendEvent : theEvent];
}
@end
//
//Waitpid is a blocking call and we're a foreground process (a 'normal' Cocoa application).
//Waitpid with WNOHUNG return immediately and does not solve our problems.
//Here's the trick: create a background thread executing ... waitpid
//and performing its special selector on a main thread (when status changed).
//
namespace {
//Timer callbacks 'fire' custom NSEvents:
enum CustomEventSource {//make it enum class when C++11 is here.
kScrollTimer = 1,
kSignalTimer = 2,
kWaitpidThread = 3
};
}
@interface ROOTWaitpidThread : NSThread {
int status;
int result;
bool normalExit;
}
- (int) getStatus;
@end
@implementation ROOTWaitpidThread
//_________________________________________________________________
- (id) init
{
if (self = [super init]) {
status = 0;
result = 0;
normalExit = false;
}
return self;
}
//_________________________________________________________________
- (int) getStatus
{
return status;
}
//_________________________________________________________________
- (void) main
{
using ROOT::ROOTX::gChildpid;
do {
while ((result = ::waitpid(gChildpid, &status, WUNTRACED) < 0)) {
if (errno != EINTR)
break;
errno = 0;
}
if (WIFEXITED(status) || WIFSIGNALED(status)) {
[self performSelectorOnMainThread : @selector(exitEventLoop) withObject : nil waitUntilDone : NO];
return;
}
if (WIFSTOPPED(status)) { // child got ctlr-Z
::raise(SIGTSTP); // stop also parent
::kill(gChildpid, SIGCONT); // if parent wakes up, wake up child
}
} while (WIFSTOPPED(status));
normalExit = true;
[self performSelectorOnMainThread : @selector(exitEventLoop) withObject : nil waitUntilDone : NO];
}
//_________________________________________________________________
- (void) exitEventLoop
{
//assert - we are on a main thread.
NSEvent * const timerEvent = [NSEvent otherEventWithType : NSApplicationDefined
location : NSMakePoint(0, 0) modifierFlags : 0
timestamp : 0. windowNumber : 0 context : nil
subtype : 0 data1 : kWaitpidThread data2 : 0];
[NSApp postEvent : timerEvent atStart : NO];
}
@end
@interface ROOTSplashScreenAppDelegate : NSObject<NSApplicationDelegate>
@end
@implementation ROOTSplashScreenAppDelegate
//_________________________________________________________________
- (void) applicationDidResignActive : (NSNotification *) aNotification
{
#pragma unused(aNotification)
popupDone = true;
}
@end
namespace {
volatile sig_atomic_t popdown = 0;
ROOTSplashScreenPanel *splashScreen = nil;
//Top-level autorelease pool.
NSAutoreleasePool * topLevelPool = nil;
//We use a signal timer to check:
//if we got a SIGUSR1 (see rootx.cxx) or
//if we have to remove a splash-screen after a delay.
CFRunLoopTimerRef signalTimer = 0;
const CFTimeInterval signalInterval = 0.1;
timeval popupCreationTime;
const CFTimeInterval splashScreenDelayInSec = 2.;//4 seconds in rootx.cxx, I'm gonna use 2.
//Timer for a scroll animation.
CFRunLoopTimerRef scrollTimer = 0;
const CFTimeInterval scrollInterval = 0.1;
//Aux. functions:
bool InitCocoa();
bool InitTimers();
void RunEventLoop();
void RunEventLoopInBackground();
bool StayUp();
bool CreateSplashscreen(bool about);
void SetSplashscreenPosition();
//Non GUI aux. function.
NSAttributedString *CreateTextToScroll(bool about);
bool ReadContributors(std::list<std::string> & contributors);
}//unnamed namespace.
//Platform-specific (OS X) versions of PopupLogo, WaitLogo, PopdownLogo, CloseDisplay.
//_________________________________________________________________
void PopupLogo(bool about)
{
if (!InitCocoa()) {
//TODO: diagnostic.
return;
}
//0. For StayUp to check when we should hide our splash-screen.
if (gettimeofday(&popupCreationTime, 0) == -1) {
//TODO: check errno and issue a message,
//we need a valid popup creation time.
return;
}
if (!CreateSplashscreen(about)) {
//TODO: diagnostic.
return;
}
showAboutInfo = about;
SetSplashscreenPosition();
[splashScreen setLevel : NSFloatingWindowLevel];
[splashScreen makeKeyAndOrderFront : nil];
}
//_________________________________________________________________
void PopdownLogo()
{
//This function is called from the signal handler.
popdown = 1;
}
//_________________________________________________________________
void WaitLogo()
{
if (!splashScreen)
//TODO: diagnostic.
return;
ROOTSplashScreenAppDelegate *delegate = nil;
if (showAboutInfo) {
delegate = [[ROOTSplashScreenAppDelegate alloc] init];
[[NSApplication sharedApplication] setDelegate : delegate];
}
RunEventLoop();
//Cleanup.
[splashScreen orderOut : nil];
[splashScreen release];
splashScreen = nil;
if (showAboutInfo) {
[NSApp setDelegate : nil];
[delegate release];
}
}
//_________________________________________________________________
void CloseDisplay()
{
//Noop.
}
//Aux. functions.
extern "C" {
//_________________________________________________________________
void ROOTSplashscreenTimerCallback(CFRunLoopTimerRef timer, void *info)
{
#pragma unused(info)
if (timer == signalTimer) {
NSEvent * const timerEvent = [NSEvent otherEventWithType :
NSApplicationDefined location : NSMakePoint(0, 0) modifierFlags : 0
timestamp : 0. windowNumber : 0 context : nil
subtype : 0 data1 : kSignalTimer data2 : 0];
[NSApp postEvent : timerEvent atStart : NO];
} else {
NSEvent * const timerEvent = [NSEvent otherEventWithType : NSApplicationDefined
location : NSMakePoint(0, 0) modifierFlags : 0
timestamp : 0. windowNumber : 0 context : nil
subtype : 0 data1 : kScrollTimer data2 : 0];
[NSApp postEvent : timerEvent atStart : NO];
}
}
}//extern "C"
namespace {
//_________________________________________________________________
bool InitCocoa()
{
if (!topLevelPool) {
[[NSApplication sharedApplication] setActivationPolicy : NSApplicationActivationPolicyAccessory];//Do I need this?
topLevelPool = [[NSAutoreleasePool alloc] init];
}
return true;
}
//_________________________________________________________________
bool InitTimers()
{
assert(scrollTimer == 0 && "InitTimers, scrollTimer was initialized already");
assert(signalTimer == 0 && "InitTimers, signalTimer was initialized already");
using ROOT::MacOSX::Util::CFScopeGuard;
CFScopeGuard<CFRunLoopTimerRef> guard1(CFRunLoopTimerCreate(kCFAllocatorDefault,//allocator
CFAbsoluteTimeGetCurrent() + signalInterval,//fireDate
signalInterval,//interval in seconds(?)
0,//flags - not used
0,//order - not used
ROOTSplashscreenTimerCallback,
0//info
));
if (!guard1.Get())
return false;
//Scroll animation.
if (showAboutInfo) {
CFScopeGuard<CFRunLoopTimerRef> guard2(CFRunLoopTimerCreate(kCFAllocatorDefault,
CFAbsoluteTimeGetCurrent() + splashScreenDelayInSec,
scrollInterval,
0,
0,
ROOTSplashscreenTimerCallback,
0
));
if (!guard2.Get())
return false;
//TODO: refactor CFScopeGuard to return the pointer from Release.
scrollTimer = guard2.Get();
guard2.Release();
}
signalTimer = guard1.Get();
guard1.Release();
return true;
}
//_________________________________________________________________
void AttachTimers()
{
assert(signalTimer != 0 && "AttachTimer, invalid signalTimer (null)");
CFRunLoopAddTimer(CFRunLoopGetMain(), signalTimer, kCFRunLoopCommonModes);
if (showAboutInfo) {
assert(scrollTimer != 0 && "AttachTimer, invalid scrollTimer (null)");
CFRunLoopAddTimer(CFRunLoopGetMain(), scrollTimer, kCFRunLoopCommonModes);
}
}
//_________________________________________________________________
void RemoveTimers()
{
assert(signalTimer != 0 && "RemoveTimers, signalTimer is null");
CFRunLoopRemoveTimer(CFRunLoopGetMain(), signalTimer, kCFRunLoopCommonModes);
CFRunLoopTimerInvalidate(signalTimer);
//TODO: test if I also have to call release!!!
signalTimer = 0;
if (showAboutInfo) {
assert(scrollTimer != 0 && "RemoveTimers, scrollTimer is null");
CFRunLoopRemoveTimer(CFRunLoopGetMain(), scrollTimer, kCFRunLoopCommonModes);
CFRunLoopTimerInvalidate(scrollTimer);
//TODO: test if I also have to call release!!!
scrollTimer = 0;
}
}
//_________________________________________________________________
void RunEventLoop()
{
//Kind of event loop.
if (!splashScreen)
//TODO: diagnostic.
return;
if (!InitTimers())
return;
AttachTimers();
popupDone = false;
while (!popupDone) {
//Here we (possibly) suspend waiting for event.
using ROOT::MacOSX::Util::NSScopeGuard;
const NSScopeGuard<NSAutoreleasePool> pool([[NSAutoreleasePool alloc] init]);
if (NSEvent * const event = [NSApp nextEventMatchingMask : NSAnyEventMask
untilDate : [NSDate distantFuture] inMode : NSDefaultRunLoopMode dequeue : YES])
{
//Let's first check the type:
if (event.type == NSApplicationDefined) {//One of our timers 'fired'.
if (event.data1 == kSignalTimer) {
popupDone = !showAboutInfo && !StayUp() && popdown;
} else if (showAboutInfo) {
assert([[splashScreen contentView] isKindOfClass : [ROOTSplashScreenView class]] &&
"RunEventLoop, splashScreen.contentView has a wrong type");
[(ROOTSplashScreenView *)[splashScreen contentView] scrollText];
}
} else
[NSApp sendEvent : event];
}
}
RemoveTimers();
//Empty the queue (hehehe, this makes me feel ... uneasy :) ).
while ([NSApp nextEventMatchingMask : NSAnyEventMask untilDate : nil inMode : NSDefaultRunLoopMode dequeue : YES]);
}
//_________________________________________________________________
void WaitChildGeneric()
{
//If things with a waitpid thread went wrong, this function is called.
//Wait till child (i.e. ROOT) is finished. From rootx.cxx.
using ROOT::ROOTX::gChildpid;
int status = 0;
do {
while (::waitpid(gChildpid, &status, WUNTRACED) < 0) {
if (errno != EINTR)
break;
errno = 0;
}
if (WIFEXITED(status))
std::exit(WEXITSTATUS(status));
if (WIFSIGNALED(status))
std::exit(WTERMSIG(status));
if (WIFSTOPPED(status)) { // child got ctlr-Z
::raise(SIGTSTP); // stop also parent
::kill(gChildpid, SIGCONT); // if parent wakes up, wake up child
}
} while (WIFSTOPPED(status));
std::exit(0);
}
//_________________________________________________________________
void RunEventLoopInBackground()
{
using ROOT::MacOSX::Util::NSScopeGuard;
if (!InitCocoa()) {
//It's a serious bug and must be either reported or handled in a different way.
return WaitChildGeneric();
}
int status = 0;
{//Block to force a scope guard's lifetime.
const NSScopeGuard<ROOTWaitpidThread> thread([[ROOTWaitpidThread alloc] init]);
if (!thread.Get()) {
//TODO: diagnostic.
return WaitChildGeneric();
} else {
[thread.Get() start];
while (true) {
//Autorelease pool?
using ROOT::MacOSX::Util::NSScopeGuard;
const NSScopeGuard<NSAutoreleasePool> pool([[NSAutoreleasePool alloc] init]);
if (NSEvent * const event = [NSApp nextEventMatchingMask : NSAnyEventMask
untilDate : [NSDate distantFuture] inMode : NSDefaultRunLoopMode dequeue : YES])
{
if (event.type == NSApplicationDefined && event.data1 == kWaitpidThread) {
[thread.Get() cancel];
status = [thread.Get() getStatus];
break;
}
else
[NSApp sendEvent : event];
}
}
[NSApp hide : nil];//deactivate?
}
}//to force thread release.
if (WIFEXITED(status))
std::exit(WEXITSTATUS(status));
if (WIFSIGNALED(status))
std::exit(WTERMSIG(status));
}
//_________________________________________________________________
bool StayUp()
{
//Taken from rootxx.cxx.
const int splashScreenDelay = int(splashScreenDelayInSec * 1000);
timeval ctv = {};
timeval dtv = {};
timeval tv = {};
timeval ptv = popupCreationTime;
tv.tv_sec = splashScreenDelay / 1000;
tv.tv_usec = (splashScreenDelay % 1000) * 1000;
gettimeofday(&ctv, 0);
if ((dtv.tv_usec = ctv.tv_usec - ptv.tv_usec) < 0) {
dtv.tv_usec += 1000000;
ptv.tv_sec++;
}
dtv.tv_sec = ctv.tv_sec - ptv.tv_sec;
if ((ctv.tv_usec = tv.tv_usec - dtv.tv_usec) < 0) {
ctv.tv_usec += 1000000;
dtv.tv_sec++;
}
ctv.tv_sec = tv.tv_sec - dtv.tv_sec;
if (ctv.tv_sec < 0)
return false;
return true;
}
//_________________________________________________________________
bool CreateSplashscreen(bool about)
{
//Try to create NSImage out of Splash.gif, create NSPanel
//with ROOTSplashscreenView as its content view + our background image + text in a scroll view.
using ROOT::MacOSX::Util::NSScopeGuard;
//0. Text to show.
const NSScopeGuard<NSAttributedString> textToScroll(CreateTextToScroll(about));
if (!textToScroll.Get())
//Diagnostic was issued by CreateTextToScroll (TODO though).
return false;
//1. Image for splash screen's background.
#ifdef ROOTICONPATH
const std::string fileName(std::string(ROOTICONPATH) + "/Root6Splash.png");
#else
const char * const env = std::getenv("ROOTSYS");
if (!env) {
//TODO: diagnostic.
return false;
}
const std::string fileName(std::string(env) + "/icons/Root6Splash.png");
#endif
const NSScopeGuard<NSString> nsStringGuard([[NSString alloc] initWithFormat : @"%s", fileName.c_str()]);
if (!nsStringGuard.Get()) {
//TODO: diagnostic.
return false;
}
const NSScopeGuard<NSImage> imageGuard([[NSImage alloc] initWithContentsOfFile : nsStringGuard.Get()]);
if (!imageGuard.Get()) {
//TODO: diagnostic.
return false;
}
NSInteger pixelWidth = 0, pixelHeight = 0;
{//For autorelease pool.
const NSScopeGuard<NSAutoreleasePool> pool([[NSAutoreleasePool alloc] init]);
NSArray * const reps = [imageGuard.Get() representations];
for (NSImageRep * rep in reps) {
pixelWidth = rep.pixelsWide;
pixelHeight = rep.pixelsHigh;
break;
}
}
if (pixelWidth < 300 || pixelHeight < 285) {
//TODO: diagnostic.
return false;
}
//2. Splash-screen ('panel' + its content view).
NSScopeGuard<ROOTSplashScreenPanel> splashGuard([[ROOTSplashScreenPanel alloc]
initWithContentRect : CGRectMake(0, 0, pixelWidth, pixelHeight)
styleMask : NSNonactivatingPanelMask
backing : NSBackingStoreBuffered
defer : NO]);
if (!splashGuard.Get()) {
//TODO: diagnostic.
return false;
}
const NSScopeGuard<ROOTSplashScreenView> viewGuard([[ROOTSplashScreenView alloc] initWithImage : imageGuard.Get()
text : textToScroll.Get() aboutMode : about]);
if (!viewGuard.Get()) {
//TODO: diagnostic.
return false;
}
[splashGuard.Get() setContentView : viewGuard.Get()];
[splashGuard.Get() setOpaque : NO];
[splashGuard.Get() setBackgroundColor : [NSColor clearColor]];
splashScreen = splashGuard.Get();
splashGuard.Release();
return true;
}
//_________________________________________________________________
void SetSplashscreenPosition()
{
assert(splashScreen != nil && "SetSplashscreenPosition, splashScreen is nil");
//Set the splash-screen's position (can it be wrong for a multi-head setup?)
//TODO: check with a secondary display.
if (NSScreen * const screen = [NSScreen mainScreen]) {
const NSRect screenFrame = screen.frame;
const CGSize splashSize = splashScreen.frame.size;
const CGPoint origin = CGPointMake(screenFrame.origin.x + screenFrame.size.width / 2 - splashSize.width / 2,
screenFrame.origin.y + screenFrame.size.height / 2 - splashSize.height / 2);
[splashScreen setFrameOrigin : origin];
}//else - is it possible? TODO: diagnostic.
}
//Aux. "non-GUI" functions.
//
//Caption like "Conception"/"Core Developers"/"Contributors" - white and bold.
//
//_________________________________________________________________
bool AddCaptionAttributes(NSMutableAttributedString *textToScroll, NSRange captionRange)
{
using ROOT::MacOSX::Util::NSScopeGuard;
assert(textToScroll != nil && "AddAttributedCaption, parameter 'textToScroll' is nil");
if (!captionRange.length)
return true;
assert(captionRange.location < textToScroll.length &&
captionRange.location + captionRange.length <= textToScroll.length &&
"AddCaptionAttributes, invalid range");
NSFont * const font = [NSFont fontWithName : @"Tahoma-Bold" size : 11.];
if (!font)
//TODO: diagnostic.
return false;
const NSScopeGuard<NSMutableDictionary> dict([[NSMutableDictionary alloc] init]);
[dict.Get() setObject : font forKey : NSFontAttributeName];
NSColor * const captionColor = [NSColor colorWithDeviceRed : 176 / 255.
green : 210 / 255. blue : 249 / 255. alpha : 1.];
[dict.Get() setObject : captionColor forKey : NSForegroundColorAttributeName];
[textToScroll addAttributes : dict.Get() range : captionRange];
return true;
}
//
//Names in regular Tahoma and _slightly_ blue color.
//
//_________________________________________________________________
bool AddMainTextBodyAttributes(NSMutableAttributedString *textToScroll, NSRange attributedRange)
{
using ROOT::MacOSX::Util::NSScopeGuard;
assert(textToScroll != nil && "AddMainTextBodyAttributes, parameter 'textToScroll' is nil");
if (!attributedRange.length)
return true;
assert(attributedRange.location < textToScroll.length &&
attributedRange.location + attributedRange.length <= textToScroll.length &&
"AddMainTextBodyAttributes, invalid range");
NSFont * const font = [NSFont fontWithName : @"Tahoma" size : 11.];
if (!font)
//TODO: diagnostic.
return false;
const NSScopeGuard<NSMutableDictionary> dict([[NSMutableDictionary alloc] init]);
[dict.Get() setObject : font forKey : NSFontAttributeName];
//
NSColor * const textColor = [NSColor colorWithDeviceRed : 156 / 255.
green : 190 / 255. blue : 229 / 255. alpha : 1.];
//
[dict.Get() setObject : textColor forKey : NSForegroundColorAttributeName];
[textToScroll addAttributes : dict.Get() range : attributedRange];
return true;
}
//_________________________________________________________________
bool AddSectionTitle(NSMutableAttributedString *textToScroll, NSString *title)
{
assert(textToScroll != nil && "AddSectionTitle, paramater 'textToScroll' is nil");
assert(title != nil && "AddSectionTitle, parameter 'titlle' is nil");
if (!title.length)
return false;
using ROOT::MacOSX::Util::NSScopeGuard;
NSScopeGuard<NSAttributedString> newString([[NSAttributedString alloc] initWithString : title]);
if (!newString.Get())
return false;
const NSRange currentRange(NSMakeRange(textToScroll.length, newString.Get().length));
[textToScroll appendAttributedString : newString.Get()];
return AddCaptionAttributes(textToScroll, currentRange);
}
//_________________________________________________________________
bool AddSectionBody(NSMutableAttributedString *textToScroll, NSString *body)
{
assert(textToScroll != nil && "AddSectionBody, parameter 'textToScroll' is nil");
assert(body != nil && "AddSectionBody, parameter 'body' is nil");
if (!body.length)
return false;
using ROOT::MacOSX::Util::NSScopeGuard;
NSScopeGuard<NSAttributedString> newString([[NSAttributedString alloc] initWithString : body]);
if (!newString.Get())
//TODO: diagnostic.
return false;
const NSRange currentRange(NSMakeRange(textToScroll.length, newString.Get().length));
[textToScroll appendAttributedString : newString.Get()];
return AddMainTextBodyAttributes(textToScroll, currentRange);
}
//_________________________________________________________________
bool AddDeveloperInfo(NSMutableAttributedString *textToScroll)
{
assert(textToScroll != nil && "AddDeveloperInfo, parameter 'textToScroll' is nil");
using ROOT::MacOSX::Util::NSScopeGuard;
//'Conception:' caption.
if (!AddSectionTitle(textToScroll, @"Conception: "))
//TODO: diagnostic.
return false;
if (!AddSectionBody(textToScroll, gConception))
//TODO: diagnostic.
return false;
if (!AddSectionTitle(textToScroll, @"Core Engineering: "))
//TODO: diagnostic.
return false;
std::size_t nLines = sizeof ROOT::ROOTX::gROOTCoreTeam / sizeof ROOT::ROOTX::gROOTCoreTeam[0];
if (nLines > 1) {
nLines -= 1;//There is a "terminating null" in this array, get rid of it.
NSScopeGuard<NSMutableString> coreTeam([[NSMutableString alloc] init]);
for (std::size_t i = 0; i < nLines; ++i)
[coreTeam.Get() appendFormat : (i ? @", %s" : @"%s"), ROOT::ROOTX::gROOTCoreTeam[i]];
[coreTeam.Get() appendFormat : @".\n\n"];
if (!AddSectionBody(textToScroll, coreTeam.Get()))
return false;
} else {
//TODO: diagnostic.
return false;
}
return true;
}
//_________________________________________________________________
void AddContributorsInfo(NSMutableAttributedString *textToScroll)
{
//TODO: diagnostic and error handling + problems with non-ascii.
assert(textToScroll != nil && "AddContributorsInfo, parameter 'textToScroll' is nil");
//
std::list<std::string> contributors;
ReadContributors(contributors);
if (contributors.size()) {//Add more lines here.
using ROOT::MacOSX::Util::NSScopeGuard;
if (!AddSectionTitle(textToScroll, @"Contributors: "))
return;//Simply ignore this part, no diagnostic.
NSScopeGuard<NSAttributedString> newString;
NSRange textRange(NSMakeRange(textToScroll.length, 0));
std::list<std::string>::const_iterator it = contributors.begin(),
end = contributors.end(), begin = contributors.begin();
for (; it != end; ++it) {
//Quite ugly :( NSString/NSAttributedString ARE ugly.
NSString * const nsFromC = [NSString stringWithFormat : it != begin ? @", %s" : @"%s", it->c_str()];
newString.Reset([[NSAttributedString alloc] initWithString : nsFromC]);
if (newString.Get()) {
[textToScroll appendAttributedString : newString.Get()];
textRange.length += newString.Get().length;
}
}
AddMainTextBodyAttributes(textToScroll, textRange);
textRange.location = textToScroll.length;
newString.Reset([[NSAttributedString alloc] initWithString :
@"\n\nOur sincere thanks and apologies to anyone who deserves"
" credit but fails to appear in this list."]);
textRange.length = newString.Get().length;
[textToScroll appendAttributedString : newString.Get()];
AddCaptionAttributes(textToScroll, textRange);
}
}
//_________________________________________________________________
void AddUserInfo(NSMutableAttributedString *textToScroll)
{
//TODO: Fix - probably, it does not work with non-ascii? (if pw_gecos/pw_name can be mb strings?).
assert(textToScroll != nil && "AddUserInfo, parameter 'textToScroll' is nil");
if (const passwd * const pwd = getpwuid(getuid())) {
std::string name;
if (pwd->pw_gecos) {
const char * const comma = std::strchr(pwd->pw_gecos, ',');
if (!comma)
name = pwd->pw_gecos;
else if (pwd->pw_gecos - comma)
name.assign(pwd->pw_gecos, pwd->pw_gecos - comma);
}
if (!name.length() && pwd->pw_name)
name = pwd->pw_name;
if (!name.length())
return;
using ROOT::MacOSX::Util::NSScopeGuard;
NSRange textRange = NSMakeRange(textToScroll.length, 0);
NSString * const nsFromC = [NSString stringWithFormat : @"\n\nExtra special thanks"
" go to %s, one of our favorite users.", name.c_str()];
const NSScopeGuard<NSAttributedString> newString([[NSAttributedString alloc] initWithString : nsFromC]);
if (newString.Get()) {
textRange.length = newString.Get().length;
[textToScroll appendAttributedString : newString.Get()];
AddCaptionAttributes(textToScroll, textRange);
}
}
}
//_________________________________________________________________
NSAttributedString *CreateTextToScroll(bool about)
{
using ROOT::MacOSX::Util::NSScopeGuard;
//the resulting string.
const NSScopeGuard<NSAutoreleasePool> pool;
NSScopeGuard<NSMutableAttributedString> textToScroll([[NSMutableAttributedString alloc] init]);
if (!textToScroll.Get())
//TODO: diagnostic.
return nil;
if (!AddDeveloperInfo(textToScroll.Get()))
return nil;
//Read contributors.
if (about) {
AddContributorsInfo(textToScroll.Get());
AddUserInfo(textToScroll.Get());
}
NSAttributedString * const result = textToScroll.Get();
textToScroll.Release();
return result;
}
//_________________________________________________________________
bool ReadContributors(std::list<std::string> & contributors)
{
//TODO: Fix - does not work with something except ascii.
#ifdef ROOTDOCDIR
const std::string fileName(std::string(ROOTDOCDIR) + "/CREDITS");
#else
const char * const env = std::getenv("ROOTSYS");
if (!env)
//TODO: diagnostic?
return false;
const std::string fileName(std::string(env) + "/README/CREDITS");
#endif
std::ifstream inputFile(fileName.c_str());
if (!inputFile)
return false;
std::list<std::string> tmp;
std::string line(200, ' ');
while (std::getline(inputFile, line)) {
if (line.length() > 3) {
if (line[0] == 'N' && line[1] == ':' && line[2] == ' ')
//Let's hope this substring is not a pile of whitespaces.
tmp.push_back(line.substr(3, line.length() - 3));
}
}
tmp.swap(contributors);
return true;
}
}//unnamed namespace.
namespace ROOT {
namespace ROOTX {
//This is a 'replacement' version.
//_________________________________________________________________
void WaitChild()
{
RunEventLoopInBackground();
}
}
}