Ancora una deviazione dal consueto flusso di Macocoa (ho l'impressione che ce ne saranno sempre più; la catalogazione ha un po' smesso di stimolarmi...). Questa volta produco un salvaschermo, e non è per nulla difficile.
Sorgenti: documenti ed esempi apple
Stesura: 28 novembre 2004
Un salvaschermo è un programma che prende possesso dello schermo dopo un certo periodo di inattività del computer. A parte le funzioni di preservare il funzionamento dei componenti fisici di uno schermo (cosa sempre meno utile, dal momento che spesso i monitor ormai si spengono da soli anche per conservare energia), forniscono talvolta intrattenimento e condivisione di risorse. In Mac OS X ci sono due tipi di salvaschermo: il primo è molto semplice, e consiste in uno slide show. Chiunque può produrne uno indicando una cartella colma di immagini. Qui mi interessa produrre un salvaschermo più sofisticato (si fa per dire), utilizzando Cocoa.
Dal punto di vista pratico, un salvaschermo è un bundle, ovvero un conglomerato di file, racchiusi all'interno di una cartella di estensione .saver. Questo bundle deve essere installato all'interno di una apposita cartella perché possa funzionare correttamente (/Library/Screen Savers oppure ~/Library/Screen Savers, dove il carattere tilde indica la propria home directory o cartella d'inizio). Tuttavia, facendo doppio clic su di un bundle salvaschermo, il sistema operativo è così gentile da installarlo al posto giusto.
Cocoa mette a disposizione del programmatore un framework (collezione di oggetti, funzioni e quant'altro) per la progettazione di salvaschermi. si tratta di ScreenSaver.framework, che si trova ovviamente all'interno di /Library/Frameworks. In realtà, non occorre preoccuparsi più di tanto di questi dettagli, perché XCode provvede da solo a fornirci tutto il necessario.
È infatti sufficiente generare un nuovo progetto, e scegliere il template opportuno nella lista sottoposta. Così facendo, XCode produce quasi tutto ciò che è necessario alla programmazione di un salvaschermo.
Il framework ScreenSaver consiste in una classe (ScreenSaverView) che fornisce i servizi di base; per scrivere un salvaschermo, se ne deve definire una sottoclasse e realizzare alcuni metodi necessari all'animazione ed alla configurazione del salvaschermo. Di fianco a questa classe, c'è una specifica sottoclasse di NSUserDefaults adatta alla gestione dei default dei salvaschermi (ScreenSaverDefaults) ed altre funzioni utili alla realizzazioni di salvaschermi (che infatti userò nel mio codice).
Per scrivere un salvaschermo, occorre dunque realizzare una sottoclasse di ScreenSaverView, ed in particolare sovrascrivere alcuni metodi. Fondamentalmente, i metodi da scrivere sono due:
- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview ;
- (void) animateOneFrame ;
Il primo metodo è ovviamente chiamato quando il sistema operativo decide di far partire il salvaschermo. L'argomento booleano indica se il salvaschermo parte nella finestra di anteprima piuttosto che a tutto schermo; può essere utilizzato per differenziare il comportamento nei due casi.
Il secondo metodo è chiamato periodicamente ed è utilizzato per disegnare il contenuto dello schermo. Normalmente, il salvaschermo è una animazione, e quindi il metodo è utilizzato per disegnare un fotogramma di questa animazione.
A corredo di questi due metodi, ce ne sono molti altri per gestire le operazioni di contorno. Ad esempio, c'è un metodo per impostare la frequenza di aggiornamento, per eseguire operazioni poco prima o subito al termine delle operazioni, e due metodi per poter gestire una finestra di configurazione del salvaschermo.
Il salvaschermo che mi accingo a realizzare è piuttosto semplice, e ricalca un tipico salvaschermo che inspiegabilmente è assente da Mac OS X. L'idea è che il salvaschermo presenti a video una scritta mobile, definita dall'utente oppure che riporta l'ora corrente. Il movimento della scritta può essere scelto lineare, con rimbalzo sulle pareti laterali dello schermo, oppure può essere una passeggiata casuale (o dell'ubriaco). Per poter scegliere tra le varie opzioni, avrò bisogno di una finestra di configurazione, e di poter conservare le opzioni utente in un file di defaults.
Comincio con il lungo metodo di inizializzazione.
- (id)
initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
// chiamo il metodo ereditato
self = [super initWithFrame:frame isPreview:isPreview];
// aggiusto le particolarita' di questo salvaschermo
if (self)
{
NSSize requiredSize ;
NSRange glyphRange ;
int version ;
ScreenSaverDefaults * defaults ;
// prelevo i default, se presenti
defaults = [ScreenSaverDefaults defaultsForModuleWithName:DEFAULTFILENAME];
Per conservare i valori di default tra successivi lanci del salvaschermo, si utilizza una sottoclasse specifica di NSUserDefaults. La specializzazione si rende necessaria per poter indicare il nome del file (che normalmente piglia il nome direttamente dall'applicazione, recuperandolo all'interno del file Info.plist). Per il resto, si utilizza in maniera identica, recuperando e salvando valori di default coi classici metodi.
Per essere sicuro dell'esistenza del file (e quindi evitare di caricare i valori da un file vuoto) verifico che uno dei valori estratti dal file abbia un valore sensato. Utilizzo allo scopo un campo di versione, utile anche in vista di successiva aggiornamenti del salvaschermo.
Gli altri valori conservati nelle preferenze sono il tipo di movimento (lineare o casuale), natura e colore del font utilizzato per la visualizzazione del testo, il tipo di testo da visualizzare (l'ora corrente piuttosto che un testo definito dall'utente) ed il testo stesso; infine, la massima velocità di movimento, in pixel per unità di tempo (il valore vero e proprio non sarà visualizzato all'utente, ma potrà qualitativamente sceglierlo attraverso uno slider).
// uso un default 'versione' per verificare l'esistenza del file
// oppure, un eventuale aggiornamento
version = [defaults integerForKey: DEF_FILEPRESENT ];
// se versione non e' zero, il file esiste gia'
if ( version > 0 )
{
// leggo i parametri di default
// tipo di movimento, font, tipo di testo, stringa e velocita'
userSSType = [defaults integerForKey: DEF_USERSSTYPE ];
[ self setUserFont: [ NSFont fontWithName: [defaults stringForKey: DEF_USERFONT ]
size: [defaults floatForKey: DEF_USERFONTSIZE ] ] ];
[ self setUserColor: [ NSUnarchiver unarchiveObjectWithData:
[defaults objectForKey: DEF_USERCOLOR ] ] ];
userTextType = [defaults integerForKey: DEF_USERTEXTTYPE ];
userString = [defaults stringForKey: DEF_USERSTRING ];
userMaxSpeed = [defaults integerForKey: DEF_USERMAXSPEED ];
}
else
{
// predispongo valori di default cablati
userSSType = SSTYPE_RANDOMWALK ;
[ self setUserFont: [ NSFont fontWithName: @"Georgia" size: 48 ] ];
[ self setUserColor: [ NSColor redColor] ];
userTextType = SSTXT_CURRTIME;
userString = @"Macocoa Screen Saver";
userMaxSpeed = 10 ;
// predispongo i valori iniziali nelle preferenze
[defaults setInteger: 1 forKey: DEF_FILEPRESENT];
[defaults setInteger: userSSType forKey: DEF_USERSSTYPE];
[defaults setObject: [userFont fontName] forKey: DEF_USERFONT ];
[defaults setFloat: [userFont pointSize] forKey: DEF_USERFONTSIZE ];
[defaults setObject: [ NSArchiver archivedDataWithRootObject: userColor]
forKey: DEF_USERCOLOR ];
[defaults setInteger: userTextType forKey: DEF_USERTEXTTYPE ];
[defaults setObject: userString forKey: DEF_USERSTRING ];
[defaults setInteger: userMaxSpeed forKey: DEF_USERMAXSPEED];
[defaults synchronize];
}
Nel caso in cui il file dei default non sia presente, seleziono alcuni valori adatti alla prima partenza del salvaschermo, scrivendoli anche all'interno del file stesso. Ne segue che questa parte else del codice è eseguita una volta sola, al primo lancio in assoluto del salvaschermo per un dato utente (oppure, ogni volta che qualcuno cancella le preferenze...).
Adesso è tempo di costruire la struttura del salvaschermo.
La prima istruzione imposta l'intervallo di tempo tra due animazioni successive. Decido che tale tempo sarà di un decimo di secondo, per non caricare troppo il processore con attività poco redditizie. Per quanto riguarda il testo, decido di utilizzare per intero l'architettura di Cocoa, piuttosto che limitarmi alla singola NSAttributedString, in modo da sfruttare tutte le capacità di layout. Infatti, dopo tutta la sequenza di costruzione delle classi necessari, calcolo espressamente il rettangolo che racchiude il testo (o meglio, le sue dimensioni); queste dimensioni sono poi usate successivamente per cancellare solo la zona dello schermo interessata dal testo. Altrimenti, avrei dovuto cancellare tutta l'area dello schermo prima di disegnare nuovamente il testo in un'altra posizione. Poiché le operazioni non grafiche sono molto più veloci delle operazioni grafiche, cancellare solo il minimo indispensabile è più elegante.
// predispongo i valori interni del salvaschermo
// frequenza di aggiornamento: ogni freqValue secondi sara' chiamato
// il successivo metodo animateOneFrame
[self setAnimationTimeInterval: freqValue ];
// costruisco un textStorage (e conseguenti) per la gestione del testo
textStorage = [ [ NSTextStorage alloc ] initWithAttributedString:
[ self prepareDspString ] ] ;
layoutManager = [ [ [ NSLayoutManager alloc ] init ] autorelease ];
[ textStorage addLayoutManager: layoutManager ];
textContainer = [ [ [ NSTextContainer alloc ] init ] autorelease ];
[ layoutManager addTextContainer:textContainer ];
// calcolo una buona dimensione per il textContainer; prima dico
// che la sua dimensione e' piuttosto grande
[ textContainer setContainerSize: NSMakeSize( 10000, 10000 ) ];
// poi effettuo il primo rendering della stringa
glyphRange = [layoutManager glyphRangeForTextContainer: textContainer ];
// estraggo l'effettivo rettangolo necessario per il disegno del testo
requiredSize = [layoutManager usedRectForTextContainer:textContainer].size;
// e lo attribuisco al textcontainer
[ textContainer setContainerSize: requiredSize ];
// aggiusto i parametri iniziali del movimento
[ self newStartPoint ] ;
// predispongo due filtri per velocita' ed angolo
// utilizzati solo nel caso di random walk
// per ridurre il nervosismo del movimento
// filtro sulla velocita', taglia a 10 Hz
InitIIR2Filter( & spdFlt, freqValue );
spdFlt.filtType = FLTTYPE_DBLPOLE ;
SetIIR2FilterParam( & spdFlt, 2, 1, 20 );
SetIIR2FilterParam( & spdFlt, 2, 2, 1 );
ConfigIIR2Filter( & spdFlt, ZERO );
ResetIIR2Filter( & spdFlt, ZERO );
// filtro sull'incremento dell'angolo, taglia a 20Hz
InitIIR2Filter( & angFlt, freqValue );
angFlt.filtType = FLTTYPE_DBLPOLE ;
SetIIR2FilterParam( & angFlt, 2, 1, 20 );
SetIIR2FilterParam( & angFlt, 2, 2, 1 );
ConfigIIR2Filter( & angFlt, ZERO );
ResetIIR2Filter( & angFlt, ZERO );
}
return self;
}
L'ultima parte del metodo inizializza i parametri del movimento. Il metodo newStartPoint calcola appunto i parametri iniziali del movimento (ci arrivo tra un attimo); seguono poi una serie di istruzioni piuttosto criptiche e che eviterò di spiegare. Mi limito a dire che qui costruisco due filtri numerici per smussare i movimenti casuali (quando il salvaschermo usa la passeggiata casuale); trovate il codice in due file filter.c e filter.h, scritti in puro C e che derivano da altri miei lavori in tutt'altro ambito. Poiché li avevo già pronti, puliti e funzionanti, li ho utilizzati senza troppe modifiche. La loro comprensione richiede troppo cultura che possa essere descritta in queste povere pagine, ma sono disposto a spiegazioni ulteriori a richiesta.
Il metodo newStartPoint utilizza altre funzioni specifiche per i salvaschermi:
- (void)
newStartPoint
{
// zona occupata dal salvaschermo
NSRect vRect = [ self bounds ];
// zona occupata dal testo
NSSize tSize = [ textContainer containerSize ] ;
// una posizione casuale in x e y
lastPosition.x = SSRandomFloatBetween( 0,
vRect.origin.x + vRect.size.width - tSize.width ) ;
lastPosition.y = SSRandomFloatBetween( 0,
vRect.origin.y + vRect.size.height - tSize.height ) ;
// un angolo casuale, evitando angoli troppo vicini a zero o a novanta
// o multipli, che darebbero movimenti troppo banali
lastAngle = M_PI * (SSRandomFloatBetween( 0.05, 0.45 ) + 0.5F * SSRandomIntBetween(-2,1));
}
Spesso infatti si ha bisogno di avere dei numeri (pseudo) casuali per poter variare l'aspetto di un salvaschermo. Le funzioni SSRandomFloatBetween e SSRandomIntBetween producono appunto numeri casuali compresi tra i due argomenti della funzione. Utilizzo queste due funzioni per calcolare un punto iniziale di visualizzazione all'interno della vista, e per avere una direzione iniziale di movimento.
L'espressione che calcola lastAngle appare piuttosto complicata; in realtà cerca di evitare angoli troppo vicini a zero e 90 in modo da non avere movimenti (lineari) troppo banali.
Prima di dettagliare il metodo di animazione, occorre spiegare come avviene il movimento.
Il primo tipo di movimento è lineare. Si parte con una direzione (un angolo compreso tra 0 e 360 gradi, o meglio, in radianti, tra meno pi greco e più greco) ed una velocità (cioé, un incremento di posizione nell'unità di tempo, dove l'unità di tempo è l'intervallo di aggiornamento del salvaschermo). Dalla posizione corrente ci si sposta nella direzione prescelta dell'ammontare della velocità, fino a raggiungere o superare uno dei quattro bordi dello schermo. In questo caso, si rimbalza elasticamente indietro, come se la scritta fosse una palla di biliardo.
Il secondo tipo di movimento è la passeggiata casuale o, come preferisco dire, da ubriaco. Si parte da una posizione e con una certa direzione; da qui si generano due numeri casuali (sempre con le funzioni viste sopra). Il primo numero è l'incremento di posizione (un numero compreso tra 1 e la massima velocità specificata dall'utente); il secondo numero è la variazione della direzione, un numero compreso tra -90 e 90 (l'ubriaco cerca di mantenere un percorso più o meno in avanti), che si somma alla direzione corrente.
Ecco il metodo che esegue un passo dell'animazione:
- (void)
animateOneFrame
{
float newAngle, themargin ;
float newSpeed, tmpInc ;
NSRect rect;
NSSize limit ;
NSRange glyphRange ;
// in primo luogo, ripulisco la zona precedente
[ self cleanLastText ];
// ricalcolo le dimensioni della stringa da visualizzare
[ textStorage setAttributedString: [ self prepareDspString ] ] ;
[ textContainer setContainerSize: NSMakeSize( 10000, 10000 ) ];
glyphRange = [layoutManager glyphRangeForTextContainer: textContainer ];
limit = [layoutManager usedRectForTextContainer:textContainer].size;
[ textContainer setContainerSize: limit ];
// questo e' il rettangolo occupato dal salvaschermo
rect = [self bounds] ;
Una buona animazione procede in questo modo: cancello il frame precedente, e poi disegno il nuovo frame. Meglio ancora, cancello solo la porzione del frame precedente che è cambiata, e poi scrivo il nuovo frame. La prima istruzione del metodo serve quindi a cancellare la zona di schermo occupata dal testo nel frame precedente. Di seguito, si produce la nuova stringa (il codice è inefficiente, in quanto la maggior parte delle volte la stringa non è cambiata: cambia solo nel caso di visualizzazione dell'ora corrente, e solo una volta su dieci. Ma non avevo voglia di complicarmi la vita), e si ricalcola lo spazio occupato da questa stringa (utile al passo di animazione successivo).
Adesso il codice si biforca per tenere conto delle due possibilità di movimento:
// calcolo una nuova posizione, in dipendenza dal tipo di movimento
switch ( userSSType ) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case SSTYPE_RANDOMWALK : // passeggiata da ubriaco
// variazione dell'angolo, piu' o meno in avanti
tmpInc = M_PI * SSRandomFloatBetween( -0.5, 0.5 );
// filtro le asperita' del movimento
newAngle = CalcIIR2Filter( & angFlt, tmpInc ) ;
// calcolo la nuova direzione di movimento
lastAngle += newAngle ;
// spostamento, casuale tra 1 e massimo
tmpInc = SSRandomFloatBetween( 1, (float) userMaxSpeed );
// evito accelerazioni troppo brusche
newSpeed = CalcIIR2Filter( & spdFlt, tmpInc ) ;
// calcolo la nuova posizione aggiungendo lo spostamento
lastPosition.x += newSpeed * cos ( lastAngle ) ;
lastPosition.y += newSpeed * sin ( lastAngle ) ;
// costringo la posizione all'interno del rettangolo massimo
// se la posizione supera i limiti, rimango a contatto col margine
// (l'ubriaco incontra il muro, e ci rimane attaccato...)
if ( lastPosition.x + limit.width > rect.origin.x + rect.size.width )
lastPosition.x = rect.origin.x + rect.size.width - limit.width ;
if ( lastPosition.x < rect.origin.x )
lastPosition.x = rect.origin.x ;
// credo che si debba tenere conto anche delle caratteristiche del font
tmpInc = [ userFont ascender ] + [ userFont descender ] ;
themargin = lastPosition.y + tmpInc + limit.height ;
if ( themargin > (rect.origin.y + rect.size.height) )
lastPosition.y = rect.origin.y + rect.size.height
- limit.height - tmpInc ;
themargin = lastPosition.y + tmpInc ;
if ( themargin < rect.origin.y )
lastPosition.y = rect.origin.y - tmpInc ;
break ;
Siamo nel caso della passeggiata casuale; genero un incremento di direzione (un angolo compreso tra -90 e 90 gradi), lo filtro per evitare cambi di direzione troppo bruschi, e poi ottengo la nuova direzione di movimento. Produco poi un nuovo spostamento, compreso tra un pixel ed il massimo numero previsto, filtro nuovamente per evitare accelerazioni eccessive; finalmente, arrivo a calcolare una nuova posizione del testo. Questa posizione però potrebbe essere tale da portare la rappresentazione del testo al di fuori dello spazio ammissibile. Vado quindi a correggere la posizione, facendo in modo che il testo sbatta contro il bordo, e lì rimanga finché la casualità non l'allontana nuovamente in direzione opposta. Nel correggere la posizione, devo tenere conto dello spazio occupato dalla scritta: uso quindi le dimensioni calcolate dalla classe NSLayoutManager per i margini destro e superiore. Devo tenere conto anche delle caratteristiche del font (fatto riconosciuto dopo lungo penare, come al solito). Non sono per nulla convinto della correttezza del mio codice (anzi, secondo me ho sbagliato in pieno); tuttavia, produce risultati accettabili (e, come si dice, quando va che tanto basta, non toccare, ché si guasta). Ad ogni modo, alla posizione corrente, a partire dalla quale verrà disegnato il testo, aggiungo un paio di contributi (ascent e descent) tipiche del font.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case SSTYPE_LINEAR : // movimento lineare con rimbalzo
// mantengo direzione e spostamento
lastPosition.x += userMaxSpeed * cos ( lastAngle ) ;
lastPosition.y += userMaxSpeed * sin ( lastAngle ) ;
// gestione del rimbalzo
// se vado troppo a destra, rimbalzo indietro verso sinistra
themargin = lastPosition.x + limit.width ;
if ( themargin > rect.origin.x + rect.size.width )
{
// spazio che manca da percorrere
float delta = themargin - (rect.origin.x + rect.size.width) ;
// aggiorno la posizione
lastPosition.x = (rect.origin.x + rect.size.width - limit.width) - delta ;
// cambia l'angolo di movimento
lastAngle = M_PI - lastAngle ;
}
// se vado troppo a sinistra, rimbalzo verso destra
// (c'e' una metafora politica in tutto cio'?)
if ( lastPosition.x < rect.origin.x )
{
// spazio che manca da percorrere
float delta = rect.origin.x - lastPosition.x ;
// aggiorno la posizione
lastPosition.x = rect.origin.x + delta ;
// cambia l'angolo di movimento
lastAngle = M_PI - lastAngle ;
}
// se vado troppo in alto, rimbalzo in basso
// credo che si debba tenere conto anche delle caratteristiche del font
tmpInc = [ userFont ascender ] + [ userFont descender ] ;
themargin = lastPosition.y + tmpInc + limit.height ;
if ( themargin > (rect.origin.y + rect.size.height) )
{
float delta = themargin - (rect.origin.y + rect.size.height) ;
lastPosition.y = rect.origin.y + rect.size.height - limit.height
- delta -tmpInc ;
lastAngle = - lastAngle ;
}
// se vado troppo i nbasso, rimbalzo in alto
themargin = lastPosition.y + tmpInc ;
if ( themargin < rect.origin.y )
{
float delta = rect.origin.y - themargin ;
lastPosition.y = rect.origin.y + delta - tmpInc ;
lastAngle = - lastAngle ;
}
break ;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
default : // non dovrei mai capitare qui...
NSLog( @"Non dovrei mai scrivere questo, ma non si sa mai - 1");
break ;
}
// riporto la direzione all'interno dell'angolo fondamentale
while ( lastAngle < - M_PI ) lastAngle += 2 * M_PI ;
while ( lastAngle > M_PI ) lastAngle -= 2 * M_PI ;
// scrivo di nuovo la stringa, nella nuova posizione
[ layoutManager drawGlyphsForGlyphRange:glyphRange atPoint: lastPosition ];
}
A questo punto l'altro tipo di movimento, quello lineare, è solo leggermente più complicato. Aggiorno la posizione aggiungendo lo spostamento previsto, e poi verifico se ho raggiunto e superato uno dei bordi. Nel caso, devo cambiare la direzione in modo da simulare un rimbalzo elastico. Allo scopo, ci sono due elementi da tenere in considerazione: lo spazio e l'angolo. Comincio con l'angolo che è più facile. In un rimbalzo elastico l'angolo di incidenza deve essere uguale all'angolo di riflessione; a seconda del bordo il nuovo angolo si ottiene pigliando l'angolo piatto e sottraendo l'angolo di incidenza, oppure semplicemente rovesciando il segno. Per lo spazio rimasto da percorrere, piglio la differenza tra la nuova posizione ed il bordo; questa differenza va sottratta alla coordinata del bordo per ottenere la posizione riflessa. La cosa è complicata vieppiù dal fatto che bisogna considerare lo spazio occupato dal testo e le onnipresenti caratteristiche del font.
Finalmente, l'ultima istruzione del metodo disegna il testo nella nuova posizione.
Mancano un paio di metodi per completare il disegno del testo.
Il metodo per cancellare lo spazio occupato in precedenza:
- (void)
cleanLastText
{
NSRect rect;
NSSize limit = [layoutManager usedRectForTextContainer: textContainer ].size;
// calcolo il rettangolo da cancellare; parto dalla posizione precedente
// ma devo tenere conto delle caratteristiche del font
rect = NSMakeRect( lastPosition.x,
lastPosition.y + [ userFont ascender ] + [ userFont descender ],
limit.width, limit.height );
[[NSColor blackColor] set];
NSRectFill( NSInsetRect( rect, -2, -2) );
}
Qui utilizzo la posizione corrente e le dimensioni del testo per costruire un rettangolo, che riempio di nero. Ancora una volta, le caratteristiche del font aiutano a calcolare meglio il rettangolo.
Infine, il metodo che prepara il testo:
- (NSMutableAttributedString *)
prepareDspString
{
NSMutableAttributedString * thestring ;
NSMutableDictionary * txtAtt ;
switch ( userTextType ) {
case SSTXT_CURRTIME : // ora corrente
thestring = [ [NSMutableAttributedString alloc] initWithString:
[ [NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil] ];
break ;
case SSTXT_USERTEXT : // testo stabilito dall'utente
thestring = [ [NSMutableAttributedString alloc] initWithString: userString ];
break ;
default : // non dovrei mai capitare qui...
NSLog( @"Non dovrei mai scrivere questo, ma non si sa mai - 2");
break ;
}
// inizializzo un dizionario per contenere gli attributi
txtAtt = [[[NSMutableDictionary alloc] initWithCapacity:2] autorelease];
[ txtAtt setObject: userFont forKey: NSFontAttributeName ];
[ txtAtt setObject: userColor forKey: NSForegroundColorAttributeName ];
[ thestring setAttributes: txtAtt range: NSMakeRange(0, [thestring length])];
return ( thestring );
}
Qui non c'è molto di nuovo rispetto a quanto visto nei precedenti capitoli di Macocoa. A seconda del tipo di testo da visualizzare, costruisco una NSMutableAttributeString, attribuisco font e colore, e la restituisco indietro.
La parte disegno del salvaschermo è adesso completa. C'è da scrivere tutta la sezione di configurazione. A parte un paio di dettagli, non è che ci siano grandi concetti. Si devono sovrascrivere due metodi della classe SceenSaverView.
Il primo metodo serve ad indicare al gestore dei salvaschermo se è presente o meno un pannello di configurazione. La versione di default di questo metodo dice di no, e quindi tutto si ferma lì. Qui invece il pannello è presente. Occorre allora scrivere un secondo metodo che restituisca la finestra che appunto presenta l'interfaccia di configurazione.
Ecco i due metodi necessari
- (BOOL)hasConfigureSheet
{
// c'e' la finestra, ritorno YES
return YES;
}
- (NSWindow*)configureSheet
{
NSString * thestr ;
// se la finestra non e' presente, la carico dal file nib
if (! prefSheet)
[NSBundle loadNibNamed:@"prefSheet" owner:self];
// questo e' un buon posto per predisporre la finestra in modo
// che mostri i valori correnti
[ ssType selectItemAtIndex: (userSSType - SSTYPE_BASEVAL4MENU) ] ;
[ textType selectItemAtIndex: (userTextType - SSTXT_BASEVAL4MENU) ] ;
if ( userTextType == SSTXT_USERTEXT )
[ displayText setEnabled: YES ] ;
else
[ displayText setEnabled: NO ] ;
[ displayText setStringValue: userString ] ;
[ textColor setColor: userColor ];
[ travelSpeed setIntValue: userMaxSpeed ] ;
// aggiusto la visualizzazione del font di esempio
thestr = [ NSString stringWithFormat: @"%@ %4.1f",
[ userFont displayName ], [ userFont pointSize ] ] ;
// il nome del font lo imposto con font e col colore prescelti
[ choosenfont setTextColor: userColor ] ;
[ choosenfont setStringValue: thestr ];
return ( prefSheet );
}
Il secondo metodo, come si può facilmente capire leggendo il codice, carica la finestra dal nib che mi sono premunito di costruire. Utilizzando poi i valori di default presenti, predispone i vari controlli della finestra in maniera da rispecchiare lo stato corrente.
Un punto interessante è che il proprietario di questo nib (e della finestra contenuta) non è come al solito una classe NSWindowController, ma una classe NSView; in effetti non ci sono particolari differenze di trattamento, basta aggiungere gli opportuni outlet verso i controlli, realizzare le action che i controlli attivano, proprio come una normale classe controllore di finestre.
Per il resto, è la solita ordinaria amministrazione. Quando si chiude la finestra facendo clic sul pulsante di Ok, si devono effettuare le modifiche ai valori di default, ed aggiornarli nelle preferenze:
- (IBAction)
prefSheetSave:(id) sender
{
ScreenSaverDefaults * defaults ;
// recupero la stringa utente
[ self setUserString: [ displayText stringValue ]] ;
// chiudo la finestra
[NSApp endSheet:prefSheet];
// aggiorno i defaults
defaults = [ScreenSaverDefaults defaultsForModuleWithName:DEFAULTFILENAME];
if ( [defaults integerForKey: DEF_FILEPRESENT ] <= 0 )
[defaults setInteger: 1 forKey: DEF_FILEPRESENT];
[defaults setInteger: userSSType forKey: DEF_USERSSTYPE];
[defaults setObject: [userFont fontName] forKey: DEF_USERFONT ];
[defaults setFloat: [userFont pointSize ] forKey: DEF_USERFONTSIZE ];
[defaults setObject: [ NSArchiver archivedDataWithRootObject: userColor]
forKey: DEF_USERCOLOR ];
[defaults setInteger: userTextType forKey: DEF_USERTEXTTYPE ];
[defaults setObject: userString forKey: DEF_USERSTRING ];
[defaults setInteger: userMaxSpeed forKey: DEF_USERMAXSPEED ];
[defaults synchronize];
}
Se invece si fa clic sul pulsante Cancel, ci si limita a chiudere la finestra
- (IBAction)
prefSheetCancel:(id) sender
{
[NSApp endSheet:prefSheet];
}
In entrambi i casi la finestra va chiusa esplicitamente, proprio perché il suo proprietario non è un NSWindowController, e quindi non eredita alcune funzionalità.
Tutti i controlli che hanno un effetto diretto sul salvaschermo chiamano il seguente metodo come action, metodo che aggiusta i valori interni dei parametri:
- (IBAction)
updateWindow:(id) sender
{
int thetag = [ sender tag ] ;
switch ( thetag ) {
case 1 : // tipo di movimento
userSSType = [[ ssType selectedItem ] tag] - SSTYPE_BASETAG4MENU ;
break ;
case 2 : // velocita' di movimento
userMaxSpeed = [ travelSpeed floatValue ] ;
break ;
case 3 : // cambio tipo di testo
userTextType = [[ textType selectedItem ] tag] - SSTXT_BASETAG4MENU ;
if ( userTextType == SSTXT_USERTEXT )
{
[ displayText setEnabled: YES ] ;
[ displayText setStringValue: userString ] ;
}
else
{
[ displayText setEnabled: NO ] ;
}
break ;
case 5 : // colore del testo
[ self setUserColor: [ textColor color] ];
[ choosenfont setTextColor: userColor ] ;
break ;
default :
break ;
}
[ self cleanLastText ];
}
Un controllo che non passa per questo metodo è il campo di testo dove l'utente specifica il testo a visualizzare, che è gestito direttamente dal metodo prefSheetSave.
Un altro controllo è il pulsante che permette di selezionare il font, che apre direttamente il pannello di scelta font:
- (IBAction)
selectFont:(id) sender
{
[ [ NSFontManager sharedFontManager] orderFrontFontPanel: nil ];
}
A sua volta, il pannello attiva come action il metodo changeFont, che ho così realizzato:
- (void)
changeFont:(id)sender
{
NSString * thestr ;
// ottengo il nuovo font
NSFont * newFont = [sender convertFont:userFont] ;
// lo imposto internamente
[ self setUserFont: newFont ];
// aggiorno la stringa visualizzata
thestr = [ NSString stringWithFormat: @"%@ %4.1f", [ newFont displayName ], [ newFont pointSize ] ] ;
// e cambia la visualizzazione della stessa in accordo al nuovo font e colore
newFont = [ NSFont fontWithName: [userFont fontName] size: 12 ] ;
[ choosenfont setFont: newFont ] ;
[ choosenfont setStringValue: thestr ];
// pulisco il testo attuale
[ self cleanLastText ];
return;
}
Ho infatti pensato che fosse cosa carina (ed educativa) fare in modo che una stringa con il nome del font prescelto fosse rappresentata autoreferenzialmente (cioé, con lo stesso font che indica). Così facendo, ho scoperto la differenza tra il metodo displayName (produce una stringa adatta alla visualizzazione verso un utente umano) e fontName (produce una stringa più adatta ad un utente elettronico).
Infine, un trucco per permettere all'utente di interagire con il salvaschermo, e cambiarne alcune proprietà mentre è in esecuzione:
-(void)
keyDown:(NSEvent *)theEvent
{
// recupero i caratteri associati all'evento
NSString *characters = [theEvent characters];
// se ci sono effettivamente caratteri
if ([characters length])
{
// ricavo il primo carattere
unichar character = [characters characterAtIndex:0];
// ed eseguo quanto richiesto
switch (character) {
case 'r': // riparto con nuovi valori di movimento
[ self cleanLastText ];
[ self newStartPoint ];
break ;
case 't': // cambio tipo di salvaschermo
if ( userSSType == SSTYPE_RANDOMWALK )
userSSType = SSTYPE_LINEAR ;
else if ( userSSType == SSTYPE_LINEAR )
userSSType = SSTYPE_RANDOMWALK ;
// ...
[ self cleanLastText ];
[ self newStartPoint ];
break ;
default :
break ;
}
}
}
Il metodo intercetta ogni pressione di tasto, e tratta in maniera particolare un paio di caratteri; se l'utente preme il tasto 'r', il salvaschermo riparte daccapo, ma con lo stesso tipo di movimento. Con il tasto 't', invece, il movimento cambia natura (da casuale diventa lineare e viceversa), ed ovviamente riparte daccapo.
Un problema che si è presentato abbastanza presto nello sviluppo di questo salvaschermo è stato come eseguire il debug. Infatti, il processo di compilazione produce un file .saver, che non è una applicazione. Per provare il funzionamento, non è possibile utilizzare i comandi Run e Debug presenti in XCode. Il mio primo tentativo era allora di compilare, passare da XCode al Finder, fare doppio clic sul risultato della compilazione, installare il salvaschermo, e finalmente, all'interno dell'applicazione Preferenze di Sistema, eseguire il salvaschermo.
Un primo passo è stata l'eliminazione del passo di installazione. Si può fare in modo che XCode costruisce il salvaschermo in una directory che non è quella di default (all'interno della cartella del progetto), ma a scelta. Ho quindi scelto come cartella destinazione quella dei salvaschermi utente.
Si tratta d modificare il percorso di default nel tab General della finestra delle informazioni del progetto. Poi, sfruttando quella immensa mole di informazioni che è la mailing list degli sviluppatori Cocoa, ho trovato una brillante soluzione, anzi due.
In effetti è possibile eseguire le operazioni di Run e Debug di un salvaschermo direttamente all'interno di XCode (in altre parole: si può utilizzare il debugger!); si tratta di costruire un nuovo Custom Executable (c'è una voce apposita nel menu Project di XCode) e di stabilire l'applicazione Preferenze di Sistema come applicazione da utilizzare. Così facendo, il comando Run di Xcode fa partire appunto questa applicazione (e così salto un ulteriore passo). Addirittura, inserendo dei breakpoint nel codice del salvaschermo, con il comando Debug è possibile eseguire passo passo il codice del salvaschermo (ovviamente, solo la parte specifica del salvaschermo in via di costruzione, non essendo disponibile il sorgente di Preferenze di Sistema).
C'è addirittura un passaggio ulteriore; esiste una applicazione dal nome di SaverLab, open source, che fornisce un ambiente dove eseguire sperimentazioni con i salvaschermi. Tra le cose interessanti, la possibilità di rallentare o accelerare l'animazione, utilizzare finestre di varie dimensioni, avere più istanze contemporanee del salvaschermo, produrre un filmato quicktime con l'animazione eseguita dal salvaschermo. A parte qualche idiosincrasia minore (cose che funzionano con Preferenze di Sistema e non funzionano con SaverLab), è decisamente un programma da avere quando si sviluppano salvaschermi.
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).