MaCocoa 068

Capitolo 068 - Fortuna stravolta

Ho parecchio stravolto l'applicazione YahFortune ed il pannello delle preferenze relativo; ho introdotto nuove funzioni, e razionalizzato quelle già presenti. Piuttosto che esaminare tutto il codice nel dettaglio (che spesso è rimasticatura di concetti già esposti), mi concentro sui punti più interessanti.

Sorgenti: documentazione Apple

Prima stesura: 30 dicembre 2004

Scrittura di tagline

Una tagline è una serie di linee di testo aggiunte in coda ad un messaggio di posta, che può contenere o meno informazioni utili al ricevente (che so, indirizzo o telefono del mittente). Spesso però la tagline è un pretesto per aggiungere un motto, una frase spiritosa, qualcosa insomma che caratterizzi il mettente. Talvolta è carino che la tagline non sia sempre la stessa, ma che possa cambiare nel tempo, più o meno casualmente. La maggior parte dei programmi di posta (Mail, Thunderbird, eccetera) ha la possibilità di inserire automaticamente una tagline, meno programmi invece hanno la possibilità di generarla automaticamente e casualmente. Però Thunderbird utilizza come tagline il contenuto di un file esterno all'applicazione. Se esiste una qualche applicazione in grado di modificare il contenuto di tale file, ecco che Thunderbird è in gado di inserire una tagline casuale all'interno dei messaggi.

La possibilità di inserire un messaggio di fortune all'interno di uno specifico file è proprio una della caratteristiche desiderate fin dall'inizio per la mia applicazione YahFortune. Si tratta di una cosa piuttosto facile da realizzare.

Per prima cosa, inserisco nuove grandezze nel file delle preferenze. Il nome del file è ovviamente la prima cosa, poi aggiungo due valori booleani per inserire o meno una riga di prefisso e di postfisso (si dirà così) al fortune generato. Va da sé che poi devo aggiungere nelle preferenze le due stringhe desiderate. In questo modo, inserendo sia il prefisso che il postfisso, il contenuto del file risulta qualcosa del tipo:

---------------
"Non permettete a un vogon, per nessuna ragione al mondo, di leggervi
le sue poesie."
        -- Douglas Adams, "Guida Galattica per gli Autostoppisti"
-- fornito da Yahfortune!

Oltre ad inserire le grandezze nelle preferenze, devo ovviamente provvedere a gestirle; aggiungo quindi gli opportuni controlli e la loro gestione nel panello delle preferenze.

figura 01

figura 01

Non c'è del codice particolarmente notevole per gestire il tutto: associato al pulsante di scelta file c'è un metodo per selezionare un file (il cui nome sarà inserito nel campo a fianco, mentre ai due check button è associato un metodo per abilitare o meno il campo di testo associato.

All'interno dell'applicazione YahFortune, invece, ho inserito un nuovo metodo, chiamato dal metodo makeAFortune in occasione della generazione di un nuovo fortune, che si occupa della scrittura del testo su di un file.

- (void )
writeFortuneOnfile: (NSString *) theString
{
    NSString     * ft ;
    NSString     * fn ;

    // se arrivo qui, devo scrivere su file
    fn = [[ currPrefs objectForKey: PFC_yahf_tagTextFile ] stringByExpandingTildeInPath ];
    if ( [[ currPrefs valueForKey: PFC_yahf_usePrefix ] boolValue ] )
    {
        NSString     * s1 ;
        s1 = [ currPrefs objectForKey: PFC_yahf_tagTextPrefix ] ;
        ft = [ NSString stringWithFormat: @"%@\n%@", s1, theString ];
    }
    else    ft = [ NSString stringWithString: theString ] ;
    if ( [[ currPrefs valueForKey: PFC_yahf_usePostfix ] boolValue ] )
    {
        NSString     * s2 ;
        s2 = [ currPrefs objectForKey: PFC_yahf_tagTextPostfix ] ;
        ft = [ NSString stringWithFormat: @"%@ %@\n", ft, s2 ];
    }
    [ ft writeToFile: fn atomically: YES ];
}

Come si può vedere, è piuttosto semplice. L'argomento del metodo è il nuovo fortune appena generato. A seconda del valore delle preferenze, si aggiungono il testo di prefisso ed il testo postfisso, per poi inserire il risultato ottenuto all'interno del file specificato.

Lancio ed arresto dell'applicazione

Mi piacerebbe gestire l'applicazione direttamente dall'interno del pannello delle preferenze. In altre parole, vorrei lanciare l'applicazione YahFortune dall'interno del pannello, verificarne lo stato, ed eventualmente arrestarne l'esecuzione.

Allo scopo ho aggiunto un pulsante, che svolge una duplice funzione. Il testo del pulsante indica l'azione che si può eseguire, e così facendo informa anche sullo stato dell'applicazione. Se il testo è Avvia YahFortune, significa che l'applicazione non è in esecuzione; facendo clic sul pulsante, l'applicazione verrà allora lanciata. Il pulsante diventa quindi Arresta YahFortune, indicando che l'applicazione sta lavorando onestamente. Facendo nuovamente clic, l'applicazione termina la propria esistenza.

Mi sono trovato subito in difficoltà. Infatti, il metodo launchedApplications di NSWorkspace, che pensavo di usare, ha un difetto (se così posso esprimermi): nell'elenco delle applicazioni restituite non sono presenti le applicazioni background only, come appunto è YahFortune.

Piuttosto che impegolarmi su altre strade d'interazione con il sistema operativo (ad esempio, avrei potuto eseguire il comando unix ps -ax, cercare nella lista risultante YahFortune, cose del genere), utilizzo un altro costrutto, la cui esistenza ho appreso dalla documentazione Apple, proprio nel tutorial dedicato ai Pannelli delle Preferenze.

Si tratta della classe NSConnection, che mette a disposizione un meccanismo perché due oggetti in diversi thread o task possano comunicare tra loro. Copio abbastanza pedissequamente dall'esempio, quindi non aspettatevi spiegazioni approfondite. L'idea è che un oggetto dichiari la propri disponibilità ad eseguire servizi. Nel metodo awakeFromNib dell'applicazione YahFortune aggiungo allora questo pezzo di codice:

    theConnection = [NSConnection defaultConnection];
    [ theConnection setRootObject: self];
    [ theConnection registerName: CONNECTION_PREF2APPL ];

Qui dico che la classe self (cioé ftrWinCtrl) si rende disponibile come root object ad eseguire dei servizi (ovvero, dei metodi...); per accedere a questi servizi si deve effettuare una connessione utilizzando il nome CONNECTION_PREF2APPL (che ho scelto astrusamente complicato per evitare che altre applicazioni in giro per il mondo abbiamo lo stesso nome).

Dall'altra parte, all'interno del pannello delle preferenze, per verificare se l'applicazione YahFortune è in esecuzione, basta che controlli se la connessione è disponibile: ho un metodo che si occupa della cosa:

- ( BOOL )
checkIfYFIsRunning
{
    NSConnection            * tc;
    tc = [ NSConnection connectionWithRegisteredName: CONNECTION_PREF2APPL host:nil];
    if ( tc == nil )
        return ( NO ) ;
    // e' vero' che c'e' l'applicazione, ma potrebbe essere in chiusura...
    return ( YES );
}

C'è un piccolo problema (l'applicazione potrebbe sì essere in esecuzione, ma essere nella fase di chiusura, con il fading della finestra), ma che non ha conseguenze, come si vedrà più avanti.

Uso poi una variabile d'istanza per tenere traccia di questo stato. Per tanto, all'apertuan del pannello, all'interno del metodo mainViewDidLoad ho aggiunto queste due istruzioni:

    appIsActive = [ self checkIfYFIsRunning ] ;
    [ self adjustLaunchButton ] ;

La seconda istruzione chiama il metodo seguente per aggiustare l'aspetto del pulsante:

- (void)
adjustLaunchButton
{
    if ( appIsActive )
        [ launchYF setTitle: @"Arresta YahFortune" ];
    else
        [ launchYF setTitle: @"Avvia YahFortune" ];
}

A questo punto, il metodo scatenato dal clic sul pulsante è:

- (IBAction)
handleLaunchStopYF: (id) sender
{
    if ( appIsActive )
    {
        // arresto l'applicazione: le mando un messaggio apposito
        ...
    }
    else
    {
        // lancio l'applicazione
        [[ NSWorkspace sharedWorkspace ] launchApplication: @"YahFortune" ] ;
    }
}

Non è presente il codice per terminare l'applicazione; ci arrivo in un attimo. Infatti, è il sottoprodotto della soluzione alla questione di riconoscere quando l'applicazione YahFortune è lanciata in esecuzione o terminata in maniera indipendente dal pannello delle preferenze.

Sfrutto ancora il concetto di NSConnection, però dalla parte opposta. Nel pannello delle preferenze dichiaro la disponibilità ai servizi all'interno del metodo di inizializzazione (dove ho spostato anche la lettura delle preferenze)

- (id)
initWithBundle:(NSBundle *)bundle
{
    if ( ( self = [super initWithBundle:bundle] ) != nil )
    {
        NSConnection    * theConnection ;
        // ricavo le preferenze dal file e le inserisco in un dizionario
        [ self setCurrPrefs: getPrefFromFile( ) ] ;
        // rendo disponibile un connessione in ingresso
        theConnection = [NSConnection defaultConnection];
        [ theConnection setRootObject: self];
        [ theConnection registerName: CONNECTION_APPL2PREF ];
    }
    return self;
}

Poi, all'interno dell'applicazione YahFortune, al lancio della stessa (nel metodo awakeFromNib, per esempio, esegue le poche linee di codice:

    // verifico se c'e' il prefpane
    if ( (theConnection = [ NSConnection connectionWithRegisteredName: CONNECTION_APPL2PREF host:nil] ))
    {
        yahFortunePref    * theYahFortPref = [[theConnection rootProxy] retain];
        [ theYahFortPref applicationNotification: YES ];
    }

Così facendo, eseguo il metodo applicationNotification, con argomento YES. Parallelamente, ogni volta che l'applicazione YahFortune decide di chiudersi, esegue il seguente segmento di codice:

    if ( (theConnection = [ NSConnection connectionWithRegisteredName: CONNECTION_APPL2PREF host:nil] ))
    {
        yahFortunePref    * theYahFortPref = [[theConnection rootProxy] retain];
        [ theYahFortPref applicationNotification: NO ];
    }
    [ NSApp terminate: self ] ;

A questo punto, all'interno della classe yahFortunePref, nel pannello delle preferenze, basta realizzare il metodo applicationNotification in maniera piuttosto semplice:

- ( void )
applicationNotification: (BOOL) uot
{
    appIsActive = uot ;
    [ self adjustLaunchButton ] ;
}

Con queste poche istruzioni, ho risolto il problema di conoscere lo stato di esecuzione dell'applicazione YahFortune. Con la stessa tecnica, invio anche il messaggio di chiusura dell'applicazione; basta eseguire un metodo opportuno:

- (IBAction)
handleLaunchStopYF: (id) sender
{
    if ( appIsActive )
    {
        // arresto l'applicazione: le mando un messaggio apposito
        NSConnection    * tc = [ NSConnection connectionWithRegisteredName:
                                    CONNECTION_PREF2APPL host:nil];
        frtWinCtrl        * theYahFortApp = [[tc rootProxy] retain];
        [ theYahFortApp killMeKillMeKillMe ] ;
    }
    ...

All'interno della classe frtWinCtrl c'è quindi il metodo:

- (void )
killMeKillMeKillMe
{
    [ fadeTimer invalidate ];
    fadeTimer = [NSTimer scheduledTimerWithTimeInterval: BASIC_INTERVAL_TIME target:self
        selector: @selector(updateAlpha:)
        userInfo: [ NSNumber numberWithBool: NO ]
        repeats: YES ];
    ...
}

In questo modo la prossima operazione eseguita (dopo un minimo intervallo di tempo) è il fading della finestra fino a scomparire (e quindi poi la chiusura del tutto).

Per finire, un piccolo tocco interessante; il pannello modifica il contenuto delle preferenze. Se l'applicazione è aperta, questa non se ne accorge se non al prossimo riavvio, dal momento che il file delle preferenze è letto solo alla partenza e poi basta. Basta allora, sempre con il meccanismo di NSConnection, informare l'applicazione dell'avvenuta modifica. Aggiungo, all'interno del metodo updatePrefFile le solite istruzioni:

    // se l'applicazione e' aperta, le dico di aggiornare le preferenze
    if ( (theConnection = [ NSConnection connectionWithRegisteredName: CONNECTION_PREF2APPL host:nil] ))
    {
        frtWinCtrl    * theYahFortApp = [[theConnection rootProxy] retain];
        [ theYahFortApp updateWindowDisplay: thePrefs ];
    }

Dall'altra parte, il metodo corrispondente cambia tutte le preferenze, utilizzando i nuovi valori:

- ( void )
updateWindowDisplay: (NSMutableDictionary *) theNewPrefs
{
    ...
    [ self setCurrPrefs: theNewPrefs ] ;
    [ self setUserFont: [ NSFont fontWithName: [ currPrefs valueForKey: PFC_yahf_fontName ]
            size: [ [ currPrefs valueForKey: PFC_yahf_fontSize ] intValue] ] ];
    ...
}

Mettiamoci un titolo

All'inizio del progetto avevo fatto in modo che la finestra di YahFortune non avesse la barra del titolo e che si mantenesse sempre al centro della scena. Andando avanti, mi sono accorto che in effetti potrebbe essere comodo mantenere la barra del titolo, in modo da posizionare la finestra dove si preferisce.

figura 02

figura 02

Ho quindi aggiunto due preferenze nel pannello; la prima indica se disegnare o meno il titolo della finestra, la seconda se la finestra sarà sempre centrata, oppure se mantiene la posizione. Se la finestra è sempre centrata, l'effetto di animazione quando cambia il fortune visualizzato coinvolge tutti e quattro i lati della finestra. Se invece la finestra deve mantenere la posizione, allora l'animazione coinvolge solo il lato destro e il lato inferiore. Va da sé che, in baso di mantenimento della posizione, al lancio successivo dell'applicazione la finestra mantiene l'ultima posizione utilizzata in precedenza.

Ora però, non c'è un metodo in NSWindow che permetta di aggiungere o togliere il titolo a piacimento. A quanto pare, si può fare solo al momento della creazione, utilizzando il parametro styleMask, come avevo fatto nella sottoclasse Yahwindow, forzandolo a NSBorderlessWindowMask.

Ma poco fa ho aggiunto la possibilità di modificare le preferenze al volo, con piena accettazione dei nuovi valori alla visualizzazione del fortune successivo. Ho quindi stravolto ancora il meccanismo di costruzione della finestra, che ho raccolto nel metodo seguente, all'interno della classe frtWinCtrl:

- ( void )
makeANewWindowMoving: (BOOL) titleChange
{
    YahWindow        * tempWindow ;
    NSView            * winContent ;
    unsigned int    winStyle ;
    NSRect            theRect ;
    // faccio scomparire la finestra presente con un fading a zero
    // titleChange indica che ho aggiunto/tolto la barra del titolo,
    // quindi occorre far scomparire la vecchia finestra e poi far
    // comparire la nuova finestra al suo posto
    if ( titleChange )
    {
        while ( currentFade > 0 )
        {
            long    xxx ;
            [ theWindow setAlphaValue: currentFade ];
            Delay ( 1, & xxx );
            currentFade -= 0.2 ;
        }
        currentFade = 0 ;
        [ theWindow setAlphaValue: currentFade ];
    }
    theRect = [[theWindow contentView] frame] ;

Fino a questo punto, ho fatto scomparire graziosamente la finestra corrente: ho utilizzato un ciclo while piuttosto che il solito meccanismo col timer, ma il concetto è lo stesso.

Ricordo che theWindow è una finestra che è stata prelevata da un file nib, ed ha quindi già tutto predisposto da Interface Builder. Adesso invece ne costruisco una del tutto nuova, chiamato direttamente un metodo di inizializzazione con opportuni parametri:

    // adatto lo stile a seconda delle preferenze
    if ( [[ currPrefs valueForKey: PFC_yahf_showWinTitle ] boolValue ] )
        winStyle = NSTitledWindowMask | NSClosableWindowMask ;
    else
        winStyle = NSBorderlessWindowMask ;
    // costruisco una nuova finestra fuori schermo
    tempWindow = [ [ YahWindow alloc ]
        initWithContentRect: NSMakeRect( -350, -200, 300, 160 )
        styleMask: winStyle
        backing: [theWindow backingType]
        defer: YES ];
    // aggiunsto un po' di parametri
    [ tempWindow setAlphaValue: currentFade ];
    [ tempWindow setDelegate: self ];
    [ tempWindow setHasShadow: YES ];
    [ tempWindow setBackgroundColor: [ theWindow backgroundColor ] ];
    [ tempWindow setFrameAutosaveName: @"YahFortWin" ];
    // piglio il contenuto della vecchia finestra
    // e lo inserisco in quella nuova
    winContent = [ [ theWindow contentView ] retain ];
    [ winContent removeFromSuperview ];
    [ tempWindow setContentView: winContent ];
    [ winContent release ];
    // sostituisco la finestra vecchia con quella nuova
    [ theWindow release ] ;
    theWindow = [ tempWindow retain ] ;

Dopo aver costruito la finestra nuova, ho impostato qualche suo parametro (che sono costretto a fare così, visto che non c'è Interface Builder ad aiutare). Poi, piuttosto che costruire a mano i vari elementi presenti al suo interno, li prelevo direttamente dall'altra finestra (!). Sposto quindi la vista contenuta nella vecchia finestra, con tutte le sue sottoviste (ed anche i collegamenti con le altri classi, outlet e target/Action compresi, impostati tramite Interface Builder), all'interno della nuova finestra. Infine, rilascio la vecchia finestra e tengo buona quella nuova.

    // vedo se e' il caso di centrarla
    if ( [[ currPrefs valueForKey: PFC_yahf_remeberWinPos ] boolValue ] )
    {
        // recupero la posizione dal file delle preferenze
        [ theWindow setFrameUsingName: @"YahFortWin" ];
    }
    else
    {
        // centro la finestra
        [ theWindow setFrame: theRect display: YES animate: NO ];
        [ theWindow center ];
    }
    // la porto davanti a tutti
    [ theWindow makeKeyAndOrderFront: self ];
    // la faccio comparire davanti a tutti con un fade
    if ( titleChange )
    {
        while (currentFade < 1 )
        {
            long    xxx ;
            [ theWindow setAlphaValue: currentFade ];
            Delay ( 1, & xxx );
            currentFade += 0.2 ;
        }
        currentFade = 1 ;
        [ theWindow setAlphaValue: currentFade ];
    }
}

Il resto del metodo centra se il caso la finestra, e la fa comparire col solito effetto sfumato.

Ciò che scatena tutto questo procedimento è la chiamata del metodo all'interno di awakeFromNib (col parametro a NO, dal momento si tratta della prima visualizzazione e non c'è bisogno del meccanismo di fading) e poi ogni volta che si modifica la preferenza relativa alla barra del titolo (questa volta col parametro a YES)

figura 03

figura 03

Il funzionamento non è ancora corretto in tutti i casi (sto pasticciando con la questione della posizione, che non so ancora bene cosa voglio fare), ma mi pare un buon punto d'inizio.

Diverso comportamento

La modifica più appariscente dell'applicazione è il fatto che ho individuato sette diverse modalità di comportamento, che vado nell'ordine ad elencare:

figura 04

figura 04

- Generazione di un fortune, presentazione dello stesso in una finestra, e tutto finisce lì. è la modalità classica: si mette l'applicazione in avvio automatico al login, ed ogni volta che accende il Mac, eccovi una frase memorabile.

- Generazione di un fortune, presentazione in finestra, daccapo. è un metodo comodo per perdere tempo, leggendo un po' di frasi più o meno celebri.

- Come la prima, con in più la scrittura del fortune su un file. Utile se si intende utilizzare il programma come generatore di tagline, e non si intende aggiornare la tagline troppo spesso.

- Come la seconda, con la scrittura del fortune su un file. Alla ricerca della perfetta tagline per la propria posta.

- Generazione di un fortune, scrittura dello stesso su di un file; niente finestra. Per avere una tagline nuova ad ogni login, senza lasciare finestre aperte

- Generazione continua di fortune e scrittura degli stessi su file. Ottimo per avere una tagline casuale; si imposta il tempo e si usa un apposito programma di posta.

- L'applicazione parte, mostra un fortune in una finestra, poi chiude la finestra, ma l'applicazione non muore: continua a scrivere periodicamente nuovi fortune su di un file; combina il precedente comportamento con il primo, unendo l'utile al dilettevole.

Per di più, con la storia di aggiornare le preferenze e farle recepire subito dall'applicazione, si può anche saltare dall'una all'altra modalità.

In tutta questa confusione, ho praticamente riscritto la logica di funzionamento dell'applicazione YahFortune (e giusto per farvi vedere quanto sono bravo, dirò che si tratta di una macchina a stati finiti).

La storia dell'applicazione comincia col metodo initWithBundle, che ho già illustrato sopra. Ho trasferito qui per buona misura la lettura del file delle preferenze. Poi si carica il file nib e viene eseguito il metodo awakeFromNib. Come ho descritto sopra, qui la finestra caricata dal nib è buttata via, e al suo posto ne viene costruita un'altra con le caratteristiche desiderate. Se il pannello delle preferenze è attivo (c'è una connessione), gli si comunica che l'applicazione è viva ed in salute.

Sempre all'interno del metodo awakeFromNib c'è la prima discriminazione sul comportamento da tenere:

    // adesso distinguo il comportamento a seconda del codice
    switch ( [[ currPrefs valueForKey: PFC_yahf_timeBehaviour ] intValue ] ) {
    case PFC_BT_WINDOW_ONESHOT :
    case PFC_BT_WINTAG_ONESHOT :
    case PFC_BT_WINTAG_FIRST :
    case PFC_BT_WINTAG_CONTINUE :
    case PFC_BT_WINDOW_CONTINUE :
        // c'e' da visualizzare la finestra
        // genero il fortune e preparo la finestra
        [ self resizeWindowToNewFortune ];
        // faccio partire il processo di fading
        fadeTimer = [NSTimer scheduledTimerWithTimeInterval: BASIC_INTERVAL_TIME target:self
            selector: @selector(updateAlpha:)
            userInfo: [ NSNumber numberWithBool: YES ]
            repeats: YES ];
        break ;
    case PFC_BT_TAG_ONESHOT :
    case PFC_BT_TAG_CONTINUE :
        // c'e' da gestire solo il file con la tagline
        // genero il fortune
        [ self makeAFortune ] ;
        // aspetto il tempo imposto
        fadeTimer = [NSTimer scheduledTimerWithTimeInterval: [ self getSec ] target:self
            selector: @selector(endOfDisplay:) userInfo: nil repeats: NO ];
        break ;
    }

Se c'è una finestra da visualizzare (i primi cinque casi dello switch), ebbene, si genera il fortune, si aggiusta la finestra e si fa partire il timer che chiama il metodo updateAlpha:. Questo metodo modifica gradualmente il canale alpha fino ad 1. Quando lo raggiunge, aspetta il tempo indicato nelle preferenze (recuperato dal metodo getSec) e poi dice di eseguire il metodo endOfDisplay:.

Se non ci sono finestre, allora si genera il fortune, si attende il tempo necessario e poi si passa direttamente al metodo endOfDisplay.

Questo metodo è un po' il cuore di tutto il meccanismo:

- (void)
endOfDisplay: (NSTimer *)timer
{
    switch ( [[ currPrefs valueForKey: PFC_yahf_timeBehaviour ] intValue ] ) {
    case PFC_BT_WINDOW_ONESHOT :
    case PFC_BT_WINTAG_ONESHOT :
        // chiusura della finestra,
        // faccio ripartire il fading in dissolvenza
        fadeTimer = [NSTimer scheduledTimerWithTimeInterval: BASIC_INTERVAL_TIME target:self
            selector: @selector(updateAlpha:)
            userInfo: [ NSNumber numberWithBool: NO ]
            repeats: YES ];        
        break;

Questo è il caso di di generazione di un solo fortune (con o meno scrittura del file); faccio ripartire il timer che chiama updateAlpha: per far svanire la finestra. Alla scomparsa della finestra, l'applicazione termina.

    case PFC_BT_WINTAG_FIRST :
        // se ho gia' fatto un passaggio
        if ( shooted )
        {
            // devo solo generare il file
            [ self makeAFortune ] ;
            // aspetto il tempo imposto
            fadeTimer = [NSTimer scheduledTimerWithTimeInterval: [ self getSec ] target:self
                selector: @selector(endOfDisplay:) userInfo: nil repeats: NO ];
        }
        else
        {
            // e' la prima volta che passo di qui, dico che l'ho fatto
            shooted = YES ;
            // faccio partire il fading in dissolvenza
            fadeTimer = [NSTimer scheduledTimerWithTimeInterval: BASIC_INTERVAL_TIME target:self
                selector: @selector(updateAlpha:)
                userInfo: [ NSNumber numberWithBool: NO ]
                repeats: YES ];
        }
        break;

Questo è il caso più complicato, in cui la finestra è visualizzata una prima volta, ma poi l'applicazione continua a scrivere periodicamente i fortune su un file. Uso una variabile d'istanza aggiuntiva, inizialmente posta a NO. La prima volta che si passa di qui, shooted vale NO, quindi si esegue la parte else del codice: si mette a YES la variabile, poi comincia la fase di scomparsa della finestra. Questa volta (ci arrivo tra poco), alla scomparsa della finestra, l'applicazione non viene chiusa, me è eseguito nuovamente il metodo endOfDisplay. Si ripassa quindi di qui, ma questa volta shooted è YES, quindi si genera un fortune, lo si scrive su file, si aspetta il tempo indicato, e si ritorna qui. E via così.

    case PFC_BT_WINTAG_CONTINUE :
    case PFC_BT_WINDOW_CONTINUE :
        // ridisegno la finestra
        [ self resizeWindowToNewFortune ];
        // faccio ripartire il timer di ciclo/chiusura
        fadeTimer = [NSTimer scheduledTimerWithTimeInterval: [ self getSec ] target:self
            selector: @selector(endOfDisplay:) userInfo: nil repeats: NO ];
        break ;
    case PFC_BT_TAG_CONTINUE :
        [ self makeAFortune ] ;
        // aspetto il tempo imposto
        fadeTimer = [NSTimer scheduledTimerWithTimeInterval: [ self getSec ] target:self
            selector: @selector(endOfDisplay:) userInfo: nil repeats: NO ];
        break ;

Qui le operazioni di generazione di nuovi fortune continuano fino a che l'utente non si stufa. Però c'è una finestra da visualizzare nei primi due casi (metodo resizeWindowToNewFortune), mentre è assente nel terzo (semplice makeAFortune). Si finisce con l'aspettare il solito tempo e a ripassare di qui.

    case PFC_BT_TAG_ONESHOT :
        // mi limito a chiudere l'applicazione direttamente
        [ self endOftheStory ] ;
        break ;
    }
}

L'ultimo caso è quello in cui si scrive il fortune sul file, e poi basta. Eseguo allora il metodo endOfTheStory, che comunica al pannello delle preferenze (se disponibile all'ascolto) la chiusura dell'applicazione.

- (void )
endOftheStory
{
    NSConnection * theConnection ;
    if ( (theConnection = [ NSConnection connectionWithRegisteredName: CONNECTION_APPL2PREF host:nil] ))
    {
        yahFortunePref    * theYahFortPref = [[theConnection rootProxy] retain];
        [ theYahFortPref applicationNotification: NO ];
    }
    [ NSApp terminate: self ] ;
}

C'è infine il metodo updateAlpha:, che non ha subito modifiche d'interesse nella parte iniziale (fading verso 1), ma deve tenere conto dei nuovi comportamenti quando fa scomparire la finestra:

- (void)
updateAlpha: (NSTimer *)timer
{
    // recupero la direzione del fading
    BOOL    dir = [ [ timer userInfo] boolValue ];
    // se YES, fading in crescendo
    if ( dir )
    {
        ...    
    }
    else
    {
        // fading da Uno a Zero, sparizione
        currentFade -= (BASIC_INTERVAL_TIME / [[ currPrefs valueForKey: PFC_yahf_fadeOutSpeed ] floatValue ] ) ;
        // se sotto o uguale a zero, ho finito
        if ( currentFade <= 0 )
        {
            // uccido il timer
            [ fadeTimer invalidate ];
            // se e' il caso speciale di finestra one shot ma tag continuo
            if ( [[ currPrefs valueForKey: PFC_yahf_timeBehaviour ] intValue ] == PFC_BT_WINTAG_FIRST )
            {
                // eseguo subito dopo un nuovo aggiornamento
                fadeTimer = [NSTimer scheduledTimerWithTimeInterval: BASIC_INTERVAL_TIME target:self
                    selector: @selector(endOfDisplay:) userInfo: nil repeats: NO ];
            }
            else
            {
                // negli altri casi uccido anche l'applicazione
                [ self endOftheStory ] ;
            }
        }
    }
    // aggiorno la finestra con il nuovo valore alpha
    [ theWindow setAlphaValue: currentFade ];
}

Quando il valore di currentFade è nullo, si chiede se è il caso speciale della finestra che si chiude, ma l'applicazione resta viva (ed allora lancia subito il metodo endOfdisplay), oppure è proprio il caso di chiudere tutto e finire qui con endOftheStory.

Per ora mi fermo qui, con l'applicazione ed il pannello che non sono in forma smagliante (ci sono ancora alcuni problemi noti, e molti altri ignoti), non prima di notare come adesso sia possibile, con la finestra di fortune in primo piano, copiare il fortune corrente con la combinazione di tasti Mela C. Mi è bastato aggiungere in frtWinCtrl il metodo seguente

- (IBAction)
copy:(id)sender
{
    // accedo alla clipboard
    NSPasteboard    * pb = [NSPasteboard generalPasteboard];
    // dico che nella clipboard ci sara' una stringa
    [ pb declareTypes: [NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil];
    // inserisco la stringa
    [ pb setString: [ theText stringValue ] forType: NSStringPboardType ];
}

La cosa funziona perché la classe è indicata come delegata della finestra.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).