MaCocoa 069

Capitolo 069 - Continua Fortuna

Continuo lo sviluppo di YahFortune, aggiungendo nuove funzionalità, modificando quelle presenti, facendo insomma una grande confusione.

Sorgenti: documentazione Apple

Prima stesura: 10 gennaio 2005

Integrazione con Apple Mail

Il motivo per cui tutto ciò è cominciato era l'utilizzo di fortune come generatore più o meno automatico e casuale di tagline da inserire all'interno dei messaggi di posta elettronica. Nel precedente capitolo, la cosa era risolta per tutti quelle applicazioni di posta che basano la propria signature su un file di testo esterno al programma. Ciò non accade purtroppo per il programma di posta più diffuso di Mac OS X, ovvero Apple Mail. Le signature di questa applicazione si trovano infatti memorizzate all'interno di un file sì di testo, ma strutturato XML. Il file in questione si chiama Signatures.plist e si trova all'interno della cartella ~/Library/Mail, dove la tilde indica la vostra cartella d'inizio.

figura 01

figura 01

Ho cercato a lungo un modo per interpretare tale file (che è la rappresentazione XML di una array di signature, dove ogni signature è un dizionario con due termini, il nome della signature ed il testo vero e proprio). Dopo molto penare, e senza avere una soluzione in tasca buona in ogni occasione, ho deciso di utilizzare un Applescript che piloti direttamente Apple Mail. Ma nemmeno in questa versione funziona come vorrei, per cui presumo che modificherò ancora...

Riporto direttamente il metodo della classe frtWinCtrl che esegue il tutto:

- ( void)
execScript4Mail: (NSString*) sigName withContent: (NSString *) signature
{
    // costruisco lo script giuntando un po' di stringhe
    NSMutableString * s1 = [ NSMutableString string ];
    // verifico che Mail sia aperto (altrimenti lo script lo apre)
    [ s1 appendString: @"tell application \"Finder\" \n" ];
    [ s1 appendString: @"set procList to the name of every process \n" ];
    [ s1 appendString: @"if (procList contains \"Mail\") then \n" ];
    // Mail e' aperto
    [ s1 appendString: @"tell application \"Mail\" \n" ];
    // assegno la signature ad una variabile
    [ s1 appendFormat: @"set theSignature to \"%@\" \n", signature ];
    // assegno alla signature col nome indicato il nuovo valore
    [ s1 appendFormat: @"set the content of signature \"%@\" to theSignature \n", sigName ];
    [ s1 appendString: @"end tell \n" ];
    // chiusura dello script
    [ s1 appendString: @"end if \n" ];
    [ s1 appendString: @"end tell \n" ];
    // costruisco lo script e lo eseguo
    NSAppleScript    * as = [[ NSAppleScript alloc ] initWithSource: s1 ];
    [ [ as executeAndReturnError: nil ] stringValue ] ;
}

Parto dal nome della signature e dalla signature stessa, ed uso l'istruzione Applescript

    set the content of signature "nomeSig" to "testoSig""

Attorno a questa istruzione, c'è il controllo che l'applicazione sia effettivamente in uso, e la preparazione dell'istruzione stessa a partire dalla stringhe passate come parametri al metodo. Il tutto è concluso dal lancio in esecuzione dello script.

Va da sé che mentre il testo della signature è quello generato dall'applicazione YahFortune, occorre conoscere il nome della signature utilizzato. Ovviamente, tale nome si trova all'interno del file delle preferenze. L'integrazione con Mail o la scrittura su file dipende ancora dalle preferenze:

- (void )
writeFortuneOnfile: (NSString *) theString
{
    ...
    // distinguo se devo scrivere su di un file o integrarmi con Apple Mail
    if ( [[ currPrefs valueForKey: PFC_yahf_mailIntegration ] boolValue ] )
    {
        // integrazione con Apple Mail; uso la signature col nome indicato
        // dalle preferenze
        NSString    * subSig = [ currPrefs valueForKey: PFC_yahf_mailSignName ] ;
        [ self execScript4Mail: subSig withContent: ft ];
    }
    else
    {
        NSString     * fn ;
        // banale (!) scrittura su di un file
        fn = [[ currPrefs objectForKey: PFC_yahf_tagTextFile ] stringByExpandingTildeInPath ];
        [ ft writeToFile: fn atomically: YES ];
    }
}

A questo punto, all'interno del pannello delle preferenze, ho dovuto aggiungere qualche elemento per poter impostare queste preferenze

figura 02

figura 02

A parte il pulsante di spunta per l'integrazione o meno con Mail, c'è un menu pop-up che permette di scegliere quale signature di mail utilizzare (presuppongo che tale signature esista già; ne segue che YahFortune la modifica). Questo menu deve essere riempito con l'elenco delle signature presenti. L'operazione avviene all'interno del metodo mainViewDidLoad. La cosa è più semplice del previsto; sfrutto infatti il fatto che il file Signature.plist è una property list che rappresenta un NSArray di NSDictionary (come lo so? guardando il file, mi sembrava... ho provato... è andata). Quindi il metodo arrayWithContentsOfFile è sufficiente per costruire l'oggetto NSArray a partire dal file. Poi ho spazzato il vettore e ne ho estratto i nomi dal dizionario usando la chiave apposita. Questi nomi li ho inseriti man mano come voci del menu.

- (void)
mainViewDidLoad
{
    ...
    // integrazione con Apple Mail
    tmpBool = [[ currPrefs valueForKey: PFC_yahf_mailIntegration ] boolValue ] ;
    [ mailIntegBtn setState: ( tmpBool ? NSOnState : NSOffState )];    
    [ mailSignature removeAllItems ];
    // per l'integrazione con mail, recupero i nomi delle signature
    NSString    * aPath = [ @"~/Library/Mail/Signatures.plist" stringByExpandingTildeInPath ];
    NSArray        * ams = [ NSArray arrayWithContentsOfFile: aPath ] ;
    for ( tmpInt = 0 ; tmpInt < [ ams count ] ; tmpInt ++ )
    {
        NSDictionary * di = [ ams objectAtIndex: tmpInt ];
        [ mailSignature addItemWithTitle: [ di objectForKey: @"SignatureName" ]];        
    }
    [ mailSignature selectItemWithTitle: [ currPrefs objectForKey: PFC_yahf_mailSignName ] ];
    // - - - - - - - - - - - - - - - - -
    ...

Quando poi l'utente seleziona una della voci di menu, faccio nulla, o meglio, inserisco all'interno del file delle preferenze il nome selezionato, lo stesso nome che poi è utilizzato dall'applicazione per costruire lo script.

Centraggio della finestra

Nel capitolo precedente mi lamentavo di non aver ancora deciso di cosa fare della finestra e di come centrarla. Come mio solito, ho stravolto la cosa.

figura 03

figura 03

Ho individuato cinque modi per gestire la posizione della finestra, tenendo fisso uno tra i cinque punti notevoli (i quattro angoli, oppure il centro). Ovviamente, la scelta della modalità è come al solito gestita dal pannello delle preferenze, dove è possibile anche indicare se ricordare la posizione della finestra tra un lancio ed il successivo. Non c'è nulla di particolarmente interessante nella gestione del menu pop up, per cui passo direttamente a come l'applicazione gestisce la situazione.

Il metodo makeANewWindowMoving è responsabile della costruzione della finestra; è qui dunque che si deve tenere conto della posizione:

    ...
    if ( titleChange == NO && [[ currPrefs valueForKey: PFC_yahf_remeberWinPos ] boolValue ] )
    {
        // recupero la posizione dal file delle preferenze
        float    xpos = [[ currPrefs valueForKey: PFC_yahf_winStartXPos ] floatValue ] ;
        float    ypos = [[ currPrefs valueForKey: PFC_yahf_winStartYPos ] floatValue ] ;
        // se le posizioni sono negative, centro e non ci penso piu'
        if ( xpos <= 0 || ypos <= 0 )
        {
            [ theWindow center ];
        }
        else
        {
            // cambio le coordinate della finestra
            theRect.origin.x = xpos ;
            theRect.origin.y = ypos - theRect.size.height ;
            [ theWindow setFrame: theRect display: YES animate: NO ];
        }
    }
    else
    {
        // centro la finestra
        [ theWindow setFrame: theRect display: YES animate: NO ];
        [ theWindow center ];
    }
    ...

Se occorre ricordare la posizione della finestra, prelevo le coordinate dalle preferenze e, se hanno un valore sensato, cambio le coordinate della finestra in accordo ai valori letti. Altrimenti, piazzo la finestra al centro e non ci penso più.

Inoltre, occorre modificare il metodo endOfThestory per tenere traccia dell'ultima posizione nota della finestra:

- (void )
endOftheStory
{
    NSConnection * theConnection ;
    NSUserDefaults            * theDefs = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary        * thePrefs = [ NSMutableDictionary dictionaryWithCapacity: 1 ] ;
    NSRect                    theRect = [ theWindow frame] ;

    // un nuovo dizionario con tutti i valori di default
    [ thePrefs addEntriesFromDictionary: currPrefs ];
    // salvo la posizione corrente della finestra
    [ thePrefs    setValue: [ NSNumber numberWithFloat: theRect.origin.x ]
                forKey: PFC_yahf_winStartXPos ] ;
    [ thePrefs    setValue: [ NSNumber numberWithFloat: (theRect.origin.y + theRect.size.height )]
                forKey: PFC_yahf_winStartYPos ] ;
    // salvo il file delle preferenze al suo posto
    [ theDefs setPersistentDomain: thePrefs forName: PFC_yahf_prefFileName ];
    [ theDefs synchronize ];
    // dico all'eventuale pannello che l'applicazione muore
    if ( (theConnection = [ NSConnection connectionWithRegisteredName: CONNECTION_APPL2PREF host:nil] ))
    {
        yahFortunePref    * theYahFortPref = [[theConnection rootProxy] retain];
        [ theYahFortPref applicationNotification: NO ];
    }
    [ NSApp terminate: self ] ;
}

Già che ci sono, faccio notare una modifica al metodo killMeKillMeKillMe, che, nel caso, fa scomparire graziosamente la finestra prima di terminare l'applicazione (in precedenza, era piuttosto brutale).

- (void )
killMeKillMeKillMe
{
    // c'e' da chiudere la finestra
    [ fadeTimer invalidate ];
    // faccio un fading tutto mio
    while ( currentFade > 0 )
    {
        long    xxx ;
        [ theWindow setAlphaValue: currentFade ];
        Delay ( 1, & xxx );
        currentFade -= 0.2 ;
    }
    currentFade = 0 ;
    [ theWindow setAlphaValue: currentFade ];
    // muoio infelice
    [ self endOftheStory ] ;
}

Infine, c'è la questione di come gestire il passaggio da un fortune ad un altro fortune, ovvero di come muovere la finestra. All'interno del metodo resizeWindowToNewFortune c'è il meccanismo che tiene conto delle cinque possibilità:

    ...
    // il nuovo punto origine si sposta secondo il valore delle preferenze
    switch ( [ [ currPrefs valueForKey: PFC_yahf_growingMode ] intValue ] ) {
    case PCF_GM_CENTERWIN :
        winFrame.origin.x -= 0.5 * (winFrame.size.width - prevFrame.size.width) ;
        winFrame.origin.y -= 0.5 * (winFrame.size.height - prevFrame.size.height) ;
        break ;
    case PCF_GM_FIXTOPLEFT :
        winFrame.origin.y -= (winFrame.size.height - prevFrame.size.height) ;        
        break ;
    case PCF_GM_FIXTOPRIGHT :
        winFrame.origin.x -= (winFrame.size.width - prevFrame.size.width) ;
        winFrame.origin.y -= (winFrame.size.height - prevFrame.size.height) ;        
        break ;
    case PCF_GM_FIXBOTTOMLEFT :
        break ;
    case PCF_GM_FIXBOTTOMRIGHT :
        winFrame.origin.x -= (winFrame.size.width - prevFrame.size.width) ;
        break ;
    }
    ...

Le coordinate della finestra sono modificate in accordo al punto fisso individuato, ed il codice mostra il tutto in maniera abbastanza esplicativa; c'è solo da tener presente che le coordinate della finestra sono date a partire dall'angolo in basso a sinistra dello schermo.

Ho anche unificato la gestione del tempo di visualizzazione/chiusura dell'applicazione, prima effettuato separatamente dall'applicazione e dal pannello delle preferenze. Adesso ci sono un po' di funzioni in commonUtils.h che gestiscono i tempi:

// etichette corrispondenti delle tacche dello slider
static int    dspValues[ 12 ] = { 0, 1, 2, 5, 10, 20, 30, 1, 2, 6, 12, 24 };
// effettivo valore (in minuti per chiarezza) del tempo da utilizzare
static    long    time2wait[12] = { 0, 1, 2, 5, 10, 20, 30, 60, 120, 300, 600, 1200 };

int
labelFromCode( int idx )
{
    return( dspValues[ idx] ) ;
}

long
secsFromCode( int idx )
{
    return( time2wait[ idx] * 60 ) ;
}

NSString *
unitFromCode( int idx )
{
    switch ( idx ) {
    case 1 :    
        // unita', minuti
        return( [ NSString stringWithString: @"Minuto"] );
    case 2 :        case 3 :        case 4 :        
    case 5 :        case 6 :
        // unita', minuti
        return( [ NSString stringWithString: @"Minuti"] );
    case 7 :    // unita', ore
        return( [ NSString stringWithString: @"Ora"] );
    case 8 :        case 9 :        case 10 :
    case 11 :
        // unita', ore
        return( [ NSString stringWithString: @"Ore"] );
    case 0 :    // giusto per precauzione
    default :
        return( [ NSString stringWithString: @"nessuno"] );
    }    
}

Il metodo che gestisce lo slider all'interno del pannello delle preferenze è quindi:

- ( void )
adjustTimeSlider
{
    int        theTime = [ cycleTimeSlider intValue ] ;
    [ cycleTimeText setIntValue: labelFromCode( theTime) ];
    [ cycleTimeUnit setStringValue: unitFromCode( theTime) ];
}

Il metodo che recupera il tempo di aggiornamento/chiusura dell'applicazione diventa:

- ( long )    
getSec
{
    int cycleTime = [[ currPrefs valueForKey: PFC_yahf_cycleTime ] intValue ] ;
    return ( secsFromCode( cycleTime) );
}

Gestione dei file

La parte più consistente delle modifiche all'applicazione riguarda la gestione dei file. Avevo già notato come il comando fortune si basi su una serie di file di testo (e file di indici accessori) presenti all'interno di una serie di directory predefinite. Attualmente il comando è compilato in modo che cerchi questi file all'interno delle cartelle fortunes e fortunes/off presenti nella directory in cui il comando stesso è lanciato (ricordo che questa cartella è interna al bundle dell'applicazione YahFortune). Ogni file presente, per essere considerato come sorgente di frasi, deve avere come compagno un altro file, avente lo stesso nome ma suffisso dat. Per produrre questi file compagni (che sono una specie di tabella per facilitare l'accesso alle singole frasi) si deve utilizzare un altro comando, strfile.

Quando si lancia il comando, fortune esamina i file contenuti all'interno delle cartelle indicate, alla ricerca dei file che hanno suffisso dat. Attraverso questi file riesce a calcolare il numero complessivo di frasi presenti; tra tutte, ne sceglie una caso. Il risultato è che ogni frase ha la medesima probabilità di essere selezionata; di conseguenza, la probabilità che sia selezionata una frase all'interno di un dato file è proporzionale al numero di frasi contenute all'interno del file medesimo.

figura 04

figura 04

Tutto questo discorso mi serve per arrivare a spiegare la vista dell'elenco dei file considerati dal comando nella generazione dei fortune, secondo quella che io chiamo la modalità classica (quella appunto che utilizza le directory predefinite). In questa vista presento l'elenco dei file utilizzati, comprensivi di una serie di informazioni: numero delle frasi presenti, probabilità che il fortune scelto sia in loro contenuto, se il file è considerato pieno di frasi offensive o meno.

Non è possibile modificare la lista (si tratta appunto di file predefiniti, che si trovano, ribadisco ancora una volta, all'interno del bundle dell'applicazione). Però, utilizzando due opzioni di fortune, è possibile cambiare il modo in cui il fortune è generato. Attraverso l'opzione di equiprobabilità, ogni file è considerato equiprobabile, indipendentemente dal numero di frasi in esso contenuto. Con l'opzione di scelta se utilizzare o meno i fortune considerati offessivi, invece, si selezionano l'uno o l'altro o entrambi i gruppi di file.

Ma prima di poter fare qualsiasi cosa, occorre costruire un modello dei dati.

Il modello dei fortune

Per sapere quanti e quali file di fortune sono interessati dall'applicazione, ho definito due nuove classi. La classe FortuneFile modella un singolo file di frasi adatte ad essere utilizzate con fortune. La classe FortFileCollection invece gestisce una collezione di file di fortune.

La classe FortuneFile mi serve a contenere le informazioni specifiche di ogni file di fortune:

@interface FortuneFile : NSObject
{
    // percorso del file
    NSString    * fortuneFilePath ;
    // probabilita' associata
    float        fileProb ;
    // numero di messaggi presenti
    int            numOfFortunes ;
    // lunghezza massima e minima delle stringhe
    int            minLenght ;
    int            maxLenght ;
    // se e' considerato offensivo o meno
    BOOL        isOffensive ;
    // se va utilizzato nella generazione o meno
    BOOL        isUsed ;
}

Il problema fondamentale di questa classe è come recuperare i dati. In effetti, tutto quello che posso sperare è di poter chiamare il seguente metodo, fornendogli appunto il path completo del file che si suppone contenere frasi di fortune:

- (id)        initWithPath: (NSString *) thePath ;

Con approccio pragmatico, decido che il file è considerato di fortune se, sottoposto alle operazioni del comando strfile, produce un risultato sensato, ovvero un file dat accettabile. In altre parole, si tratta di eseguire il comando

strfile thePath

e vedere cosa salta fuori; in effetti, come era già stato visto qualche capitolo fa, il comando produce sul terminale una serie di righe come le seguenti:

djzeropb:~/temp_fortune/fortunes djzero00$ ../strfile adams.txt
"adams.txt.dat" created
There were 115 strings
Longest string: 569 bytes
Shortest string: 4 bytes

Metto tutto assieme e produco il metodo:

- (id)
initWithPath: (NSString *) thePath
{
    self = [ super init ] ;
    if ( self )
    {
        NSPipe            * pipe = [ NSPipe pipe ] ;    
        NSFileHandle    * handle = [ pipe fileHandleForReading ] ;
        NSString        * fortunePath ;
        NSTask            * fortuneTask ;
        NSMutableArray    * argList = [ NSMutableArray arrayWithCapacity: 1 ];
        NSData            * data;
        NSString        * theNewFortune = [ NSString string ];
        long            xxx ;
        NSBundle        * mb = [ NSBundle bundleWithIdentifier: @"yahFortunePref" ] ;

        // recupero il percorso dell'eseguibile all'interno dell'applicazione
        fortunePath = [ mb pathForResource: @"strfile" ofType: @"" ] ;
        // costruisco un task apposito
        fortuneTask = [[NSTask alloc] init];
        // predispongo i parametri di lancio del task
        [ fortuneTask setLaunchPath: fortunePath ];
        // la directory di lavoro e' quella di destinazione
        [ fortuneTask setCurrentDirectoryPath: [ thePath stringByDeletingLastPathComponent] ];
        [ argList addObject: thePath ];        
        [ fortuneTask setArguments: argList ];
        // dico che l'uscita del comando va su una pipe
        // ridirigo sia lo standard output che lo standard error
        [ fortuneTask setStandardOutput: pipe ] ;
        [ fortuneTask setStandardError: pipe ];
        // lancio il programma
        [ fortuneTask launch];
        // gli do un attimo per partire
        Delay ( 1, & xxx );
        // aspetto, in maniera sincrona, che il programma risponda con tutta la stringa
        // prodotta secondo i suoi criteri
        while ((data = [handle availableData]) && [data length])
        {
            NSString    * tmpData ;
            // prelevo i caratteri arrivati
            tmpData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;
            // e li aggiungo a quelli gia' arrivati
            theNewFortune = [ theNewFortune stringByAppendingString: tmpData ];
        }

Fino a qui, son tutti concetti già noti: costruisco task, pipe, eseguo il comando, leggo il risultato. Adesso mi trovo con la stringa theNewfortune che contiene tutto ciò che l'esecuzione del comando avrebbe scritto sul terminale. Si tratta di interpretarlo estraendone in po' di informazioni; risolvo il problema passando la palla ad un metodo specifico, che chiamo scanStringForValues:

        // da qui devo estrarre le varie informazioni
        if ( [ self scanStringForValues: theNewFortune ] )
        {
            // aggiusto le altre variabili
            NSString    * fn = [ thePath lastPathComponent ];
            [ self setFortuneFilePath: fn ] ;
            // discrimino se e' offensivo dalla terminazione
            if ( [ fn hasSuffix: @"-o" ] )
                isOffensive = YES ;
            else
                isOffensive = NO ;
            // e' inizialmente sicuramente utilizzato
            isUsed = YES ;
        }
        else
        {
            // non ho trovato informazioni sensate
            return ( nil );
        }
    }
    return ( self );
}

Poi completo il tutto riempiendo le variabili d'istanza che è facile desumere: il nome del file, se è un file offensivo o meno in base alla presenza del suffisso -o al nome del file, e poi dico che per il momento il file sarà utilizzato da fortune per la ricerca di una nuova frase.

Scanners!!!

Ho solo spostato il problema: adesso devo scrivere un metodo scanStringForValues che riesca ad estrarre dalla stringa prodotta le informazioni che sono lì raccolte. Pensavo di faticare parecchio (dopotutto, si tratta di guardare i vari caratteri della stringa, e pescare i numeri che ci si aspetta dove si pensa di trovarli), ma ho trovato una affascinante classe di Cocoa che si preoccupa di fare il tutto al posto mio: NSScanner.

Questa classe rappresenta una specie di topo di biblioteca a cui si da in pasto una stringa; poi, tramite opportuni comandi, gli si dice di andare avanti fino a trovare una determinata combinazione di caratteri, e magari di metterla da qualche parte, o vedere se per caso c'è un numero, cose del genere. Molto bello. Il codice allora del metodo diventa piuttosto semplice e noioso:

- (BOOL)
scanStringForValues: (NSString *) theString
{
    // questa e' la stringa che strfile produce:
    /*    "path/to/file.dat" created
        There were <numofstrings> strings
        Longest string: <maxlenght> bytes
        Shortest string: <minlenght> bytes
    */
    // utilizzo quest'utile classe NSScanner
    NSScanner    * theScanner;
    int            numofstrings, maxlenght, minlenght ;
    BOOL        tmpVal ;
    NSString    * scanned ;
        // inizializzo lo scanner
    theScanner = [NSScanner scannerWithString: theString ];
    // leggo fino a trovare created
    tmpVal = [theScanner scanUpToString: @"created" intoString: & scanned] ;
    if ( tmpVal == NO ) return ( NO );
    tmpVal = [theScanner scanString: @"created" intoString: & scanned] ;
    if ( tmpVal == NO ) return ( NO );
    tmpVal = [theScanner scanString: @"There were" intoString: & scanned] ;
    if ( tmpVal == NO ) return ( NO );
    // adesso dovrebbe esserci il numero delel stringhe presenti
    tmpVal = [theScanner scanInt:& numofstrings ] ;
    if ( tmpVal == NO ) return ( NO );
    // se non riconosco almeno una stringa, lascio perdere
    if ( numofstrings <= 0 ) return ( NO );
    tmpVal = [theScanner scanString: @"strings" intoString: & scanned] ;
    if ( tmpVal == NO ) return ( NO );
    tmpVal = [theScanner scanString: @"Longest string:" intoString: & scanned] ;
    if ( tmpVal == NO ) return ( NO );
    // la lunghezza della stringa piu' lunga
    tmpVal = [theScanner scanInt:& maxlenght ] ;
    if ( tmpVal == NO ) return ( NO );
    tmpVal = [theScanner scanString: @"bytes" intoString: & scanned] ;
    if ( tmpVal == NO ) return ( NO );
    tmpVal = [theScanner scanString: @"Shortest string:" intoString: & scanned ] ;
    if ( tmpVal == NO ) return ( NO );
    // la lunghezza della stringa piu' corta
    tmpVal = [theScanner scanInt:& minlenght ] ;
    if ( tmpVal == NO ) return ( NO );
    numOfFortunes = numofstrings ;
    minLenght = maxlenght ;
    maxLenght = minlenght ;
    return ( YES ) ;
}

La comprensione è facile, una volta noto che scanUpToString: mangia caratteri fino a trovare la stringa che gli si passa come argomento, mentre scanString: cerca la combinazione di caratteri specificata dal suo argomento. Così, le prime tre istruzioni riguardanti theScanner cercano prima la parola created (e ripuliscono la stringa fino a quella parola) e poi le stringhe created e there were, eliminandole. A questo punto potrebbe esserci un numero intero; e quindi uso scanInt. Ogni operazione su NSScanner produce un risultato booleano: se questo è NO, significa che la scansione non ha avuto successo, e quindi il prodotto non è quello che ci si aspetta che sia; piuttosto che chiedermi cosa potrebbe essere andato storto, dico che è inutile continuare e dico che il file non è un buon file di fortune.

Il metodo continua mangiando man mano pezzi della stringa, e conservando i valori che sono utili; alla fine, li assegna tutti alle variabili d'istanza, e considera il file esaminato come un buon file di fortune.

È però una bella noia dover ogni volta eseguire il comando strfile per sapere quanti fortune si trovano all'interno di un file. Per questo ho studiato un meccanismo (che sarà esplicitato all'interno della classe FortFileCollection) per mantenere le informazioni su di un file XML. Mi occorrono quindi due nuovi metodi per costruire un FortuneFile e salvarlo a partire da un dizionario. Nulla di più facile:

- (id)
initWithDict: (NSDictionary *) theDict
{
    if ( self = [ super init ] )
    {
        // assegno i vari dati a partire dagli elementi del dizionario
        [ self setFortuneFilePath: [ theDict objectForKey: @"FilePath" ]];
        fileProb = [ [ theDict objectForKey: @"fileProb" ] floatValue ];
        numOfFortunes = [ [ theDict objectForKey: @"numOfFortunes" ] intValue ];
        minLenght = [ [ theDict objectForKey: @"minLenght" ] intValue ];
        minLenght = [ [ theDict objectForKey: @"minLenght" ] intValue ];
        isOffensive = [ @"YES" isEqualToString: [ theDict objectForKey: @"isOffensive" ] ];
        isUsed = [ @"YES" isEqualToString: [ theDict objectForKey: @"isUsed" ] ];
    }
    return ( self );
}

- (NSMutableDictionary *)
getXMLDescription
{
    // un dizionario per contenere le varie proprieta' XML
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    // nome della classe
    [dict setObject: NSStringFromClass([self class])
            forKey: @"className" ];
    // tutte le altre variabili d'istanza fondamentali
    [dict setObject: fortuneFilePath forKey: @"FilePath" ];
    [dict setObject: [NSString stringWithFormat:@"%f", fileProb]
            forKey: @"fileProb" ];
    [dict setObject:[NSString stringWithFormat:@"%d", numOfFortunes]
            forKey: @"numOfFortunes" ];
    [dict setObject:[NSString stringWithFormat:@"%d", minLenght]
            forKey: @"minLenght" ];
    [dict setObject:[NSString stringWithFormat:@"%d", maxLenght]
            forKey: @"maxLenght" ];
    [dict setObject:(isOffensive ? @"YES" : @"NO")
            forKey: @"isOffensive" ];
    [dict setObject:(isUsed ? @"YES" : @"NO")
            forKey: @"isUsed" ];
    return dict;
}

Una collezione di file

Una collezione di file fortune è, agli scopi della mia applicazione, l'elenco di tutti i file fortune validi all'interno di una serie di directory. Ragione per cui la classe FortFilesCollection è presto fatta:

@interface FortFilesCollection : NSObject
{
    NSMutableArray    * theFortList ;
}

C'è il problema di inizializzarla; verosimilmente, avrò un metodo cui passo l'elenco delle directory da esaminare. Dal metodo ho tolto alcune istruzioni che spiegherò più avanti.

- (id )
initWithDirList: (NSArray *) thePaths
{
    if ( self = [ super init ] )
    {
        NSFileManager     *fileManager = [NSFileManager defaultManager];
        NSEnumerator * filelist = [ thePaths objectEnumerator ];
        NSString     * aPath ;
        // la collezione e' inizialmente vuota
        [ self setTheFortList: [ NSMutableArray arrayWithCapacity: 0 ] ];
        // spazzolo tutti i percorsi
        while ( aPath = [filelist nextObject])
        {
            BOOL            isAdir, fileOK, retVal ;
            NSMutableArray    * theArr ;
            ...
            if ( ... )
            {
                ...
            }
            else
            {
                // non c'e', oppure non va bene, parto dalla directory e la esploro
                // controllo se esiste veramente la cartella, che non si sa mai
                fileOK = [ fileManager fileExistsAtPath: aPath isDirectory: & isAdir ];
                if ( fileOK && isAdir)
                {
                    // esiste ed e' proprio una cartella
                    // produco i dat file e le informazioni necessarie
                    theArr = [ self produceFileAndLoadData: aPath ];
                    // aggiungo la lista dei file alla collezione
                    [ theFortList addObjectsFromArray: theArr ] ;
                    ...
                }
            }
        }
    }
    return ( self );
}

Essenzialmente, c'è un ciclo che esamina tutte le directory passate come argomento (dopo averne verificato l'esistenza, che non si sa mai), e per ciascuna di esse esegue il metodo produceFileandLoadData:; il risultato di questo metodo è un array con i file contenuti nella directory che sono stati considerati validi. Questo array è aggiunto alla lista corrente.

Per ogni directory quindi si esegue:

- ( NSMutableArray * )
produceFileAndLoadData: (NSString*) fullPath
{
    int                i ;
    // elenco dei file presenti
    NSFileManager     *fileManager = [NSFileManager defaultManager];
    NSArray            * dirContent = [ fileManager directoryContentsAtPath: fullPath ];
    // costruisco un nuovo vettore per i FortuneFile
    int                numFile = [ dirContent count ];
    NSMutableArray    * theFileList = [ NSMutableArray arrayWithCapacity: 1 ] ;
    // adesso, per ogni elemento, costruisco l'elemento e poi lo aggiungo
    for ( i = 0; i < numFile; i++)
    {
        BOOL        isAdir ;
        FortuneFile    * ff ;
        // nome del file
        NSString     * myfile = [ dirContent objectAtIndex: i ] ;
        // percorso completo
        NSString    * fileFullPath = [ fullPath stringByAppendingPathComponent: myfile] ;
        // se il file non va bene, passo oltre
        if ( ! [ fileManager fileExistsAtPath: fileFullPath isDirectory: & isAdir ] )
            continue ;
        // se la sua estensione e' .dat, passo oltre
        if ( [ [ fileFullPath pathExtension ] isEqualToString: @"dat" ] )
            continue ;
        // se il nome comincia con un punto, passo oltre
        if ( [ myfile hasPrefix: @"."] )
            continue ;
        // se e' una directory, passo oltre
        if ( isAdir )    
            continue ;    
        // se arrivo qui, ho un vero e proprio file
        // costruisco un fortuneFile con i dati che ne ricavo
        ff = [[ FortuneFile alloc ] initWithPath: fileFullPath ];
        // se ci sono riuscito, lo aggiungo alla collezione
        if ( ff )
            [ theFileList addObject: ff ];
    }
    // restituisco l'elenco dei FortuneFile
    return ( theFileList );
}

Qui si ricava l'elenco dei file presenti nella directory, e si esaminano uno ad uno alla ricerca di candidati validi. Si saltano senz'altro una serie di file che si è sicuri non andare bene (quelli che hanno estensione dat, quelli che cominciano con un punto, le directory...). Per i file rimanenti, si prova ad eseguire il metodo initWithPath. Se il risultato ha senso, abbiamo un file e lo aggiungiamo alla lista.

A questo punto, aggiungendo alla classe yahFortunePref la variabile d'istanza

    FortFilesCollection            * stdFortFiles ;

alla quale è associato un valore nelle preferenze

    [ np setObject: [ NSArray arrayWithObjects:    @"/Contents/Resources/fortunes",
                                                @"/Contents/Resources/fortunes/off",
                                                nil ]
                forKey: PFC_yahf_stdFortFiles ] ;

composto dalle due directory con le quali è stato configurato il comando di fortune, all'apertura del pannello delle preferenze posso recuperare l'intera lista dei file di fortune in questo modo:

        tmpColl = [[ FortFilesCollection alloc] initWithPath: yahFortuneAppPath
                        andDirList: [ currPrefs objectForKey: PFC_yahf_stdFortFiles ] ] ;
        [ self setStdFortFiles: tmpColl ];

dove per mia comodità ho costruito un altro metodo di inizializzazione per FortFileCollection ed utilizzato la variabile yahFortuneAppPath che contiene il percorso dell'applicazione YahFortune. Il nuovo metodo altro non fa che chiamare il metodo esposto in precedenza dopo aver adattato opportunamente gli argomenti.

- (id )
initWithPath: (NSString*) appPath andDirList: (NSArray *) thePaths
{
    NSMutableArray    * newList = [ NSMutableArray array ] ;
    NSEnumerator    * nl = [ thePaths objectEnumerator ];
    NSString        * aPath ;
    // costruisco un vettore con i path completi
    while ( aPath = [nl nextObject])
    {
        NSString * ns = [ NSString stringWithFormat: @"%@%@", appPath, aPath ];
        [ newList addObject: ns ];
    }
    // e faccio partire l'inizializzatore designato
    return ( [ self initWithDirList: newList ] );
}

Ora, è piuttosto seccante ogni volta riscostruire da zero l'elenco dei file fortune. Ho pensato quindi di salvare la lista e di conservarla da qualche parte

Salvo la lista

L'idea è che in ogni directory che contiene file di fortune sia presente anche un file, dal nome fortFile.plist, che mantenga l'elenco dei file di fortune presenti, completi delle informazioni relative.

figura 05

figura 05

Quindi, il metodo initWithDirList:, prima di lanciarsi nella costruzione della lista, verifica se non sia già presente il file (ecco le istruzioni mancanti prima sostituite con puntini):

- (id )
initWithDirList: (NSArray *) thePaths
{
        ...
        while ( aPath = [filelist nextObject])
        {
            BOOL            isAdir, fileOK, retVal ;
            NSMutableArray    * theArr ;
            // all'interno di ogni cartella dovrebbe esserci il file fortFile.plist
            // che conserva le info relative ai file
            NSString        * indexFile = [ aPath stringByAppendingString: @"/fortFile.plist" ];
            // vedo se esiste il file indicato
            if ( [ fileManager fileExistsAtPath: indexFile isDirectory: & isAdir ] )
            {
                // il file esiste, carico le info a partire dal file
                theArr = [ self loadXMLDataFromFile: indexFile ];
                // controllo che non ci siano state variazioni
                retVal = [ self checkForModifiedFile: theArr inDir: aPath ] ;
                // aggiungo la lista dei file alla collezione
                [ theFortList addObjectsFromArray: theArr ] ;
                // se ci sono aggiornamenti sui file, riscrivo fortFile.plist
                if ( retVal )
                    [ self writeFortList: theArr toFile: indexFile ];
            }
            else
            {
            ...

Se il file è pre-esistente, preleva le informazioni proprio da questo file con il metodo apposito:

- ( NSMutableArray * )
loadXMLDataFromFile: (NSString*) fullPath
{
    int                i ;
    // estraggo i dati dal file
    NSArray            * arrFromFile = [ NSArray arrayWithContentsOfFile: fullPath ] ;
    // costruisco un nuovo vettore per i FortuneFile
    int                numFile = [ arrFromFile count ];
    NSMutableArray    * fortArray = [ NSMutableArray arrayWithCapacity: numFile ];
    // esamino i record uno alla volta
    for ( i = 0; i < numFile; i++)
    {
        FortuneFile    * ff ;
        // costruisco un Fortunefile con i dati presenti nel dizionario
        ff = [[ FortuneFile alloc ] initWithDict: [ arrFromFile objectAtIndex: i ] ];
        // se ci sono riuscito, lo aggiungo alla collezione
        if ( ff )
            [ fortArray addObject: ff ];
    }
    // restituisco l'elenco dei FortuneFile
    return ( fortArray );
}

Trattandosi di un file XML, le cose sono piuttosto semplici (o meglio, sono rese semplici da Cocoa): nel file è contenuta una rappresentazione XML di un NSArray. Ogni elemento di questo array è un NSDictionary, che posso utilizzare per costruire un oggetto della classe FortuneFile. Alla fine, ho costruito una lista di file di fortune senza dover passare per il comando strfile.

Però qualche utente birichino portrebbe aver modificano qualcosa, per cui verifico che non ci siano incongruenze palesi utilizzando il metodo checkForModifiedFile:

- ( BOOL )
checkForModifiedFile: (NSMutableArray*) theArr inDir: (NSString *) aPath
{
    NSEnumerator    * filelist = [ theArr objectEnumerator ];
    FortuneFile        * theFile ;
    NSFileManager     * fm = [NSFileManager defaultManager];
    BOOL            isToBeWritten = NO ;
    // spazzolo tutti i file presenti
    while ( theFile = [ filelist nextObject ] )
    {
        BOOL            isAdir, fileOK ;
        NSString        * fullPath ;
        NSDictionary    * fattrs     ;
        NSDate            * fileModDate ;
        NSString        * datPath ;
        NSDate            * fileDatDate ;
        // path completo del file fortune
        fullPath = [ aPath stringByAppendingPathComponent: [ theFile fortuneFilePath]];
        // ne pesco la sua ultima data di modifica
        fattrs     = [fm fileAttributesAtPath: fullPath traverseLink:NO];
        fileModDate = [fattrs fileModificationDate] ;
        // path completo del file .dat corrispondente
        datPath = [ fullPath stringByAppendingPathExtension: @"dat"];
        // verifico che questo file esista
        fileOK = [ fm fileExistsAtPath: datPath isDirectory: & isAdir ];
        if ( fileOK )
        {
            // il file esiste, ma potrebe essere troppo vecchio
            NSComparisonResult    xx ;
            // pesco la sua ultima data di modifica
            fattrs     = [ fm fileAttributesAtPath: datPath traverseLink:NO];
            fileDatDate = [fattrs fileModificationDate] ;
            // la confronto con quella del suo file
            xx = [ fileModDate compare: fileDatDate] ;
            // si suppone che il file .dat sia piu' giovane
            // se non lo e', non va bene...
            if ( xx == NSOrderedDescending )
                fileOK = NO ;
        }
        if ( ! fileOK )
        {
            // qui dentro entro se il file .dat non esiste
            // oppure e' troppo vecchio
            FortuneFile    * ff ;
            // costruisco un nuovo oggetto FortuneFile
            // (cosa che produce un .dat)
            ff = [[ FortuneFile alloc ] initWithPath: fullPath ];
            // ed assegno tutti i valori ottenuti al vecchio oggetto
            [ theFile setFileProb: [ ff fileProb ]];
            [ theFile setNumOfFortunes: [ ff numOfFortunes ]];
            [ theFile setIsOffensive: [ ff isOffensive ]];
            [ theFile setIsUsed: [ ff isUsed ]];
            [ theFile setMinLenght: [ ff minLenght ]];
            [ theFile setMaxLenght: [ ff maxLenght ]];
            // dico che occorre riscrivere il file delle info
            isToBeWritten = YES ;
        }
    }
    return ( isToBeWritten ) ;
}

Per ogni file della collezione, verifico che esista il corrisponde file indice con estensione dat, e che la data di questo file sia posteriore a quella del file stesso (se fosse anteriore, potrebbe esserci stata una modifica sul file...). Nel caso infelice in cui queste condizioni non siano soddisfatte, per stare sul sicuro ricostruisco le informazioni (ed il file dat) chiamando il metodo initWithPath sull'oggetto FortuneFile.

Ma non basta; per evitare che al lancio successivo debba rifare l'operazione, è bene che aggiorni il file fortFile.plist, cosa invero piuttosto semplice. Ho scritto il metodo writeFortList: toFile:

- ( void )
writeFortList: (NSMutableArray *) theArr toFile: (NSString *) aPath
{
    int    i ;
    int    numElem = [ theArr count ] ;
    NSMutableArray * dictList = [NSMutableArray arrayWithCapacity: numElem ];
for (i = 0; i < numElem; i++)
    {
        // ogni elemento dello array e' un dictionary che raccoglie
        // la descrizione dell'elemento in XML
        NSMutableDictionary * elemDict ;
        // descrizione dell'elemento in XML
        elemDict = [[theArr objectAtIndex:i] getXMLDescription] ;
        // aggiungo il dizionario allo array
[dictList addObject: elemDict ];
}
    // scrittura del vettore sul file
    [ dictList writeToFile: aPath atomically: YES ];
}

Per ogni elemento della lista dei file, costruisco un dizionario con la descrizione delle variabili d'istanza, li infilo all'interno di un array, e scrivo il tutto sul file apposito, sfruttando ancora una volta la potenza di Cocoa nella gestione di file XML.

figura 06

figura 06

Ora che tutte le informazioni relative ai file di fortune si trovano all'interno degli oggetti della classi FortFilesCollection, questa stessa classe costituisce il modello dei dati della tabella che li rappresenta: ne segue che la tabella che presenta la lista degli stessi conviene che utilizzi appunto FortFileCollection come classe sorgente dei dati. Questo fatto va dichiarato esplicitamente all'interno del metodo mainViewDidLoad del pannello delle preferenze con l'istruzione:

    [ stdFileList setDataSource: stdFortFiles ];

Il collegamento tra la NSTableView e la sua sorgente dei dati non può essere stabilito all'interno di Interface Builder, dal momento che lì non è disponibile la variabile stdFortFiles...

I due metodi necessari alla gestione della tabella sono molto semplici:

- (int)
numberOfRowsInTableView:    (NSTableView *)tableView
{
    return [ theFortList count ] ;
}

- (id)
tableView:            (NSTableView *)tableView
    objectValueForTableColumn:    (NSTableColumn *)tableColumn
    row:             (int)row
{
    NSString        * colId = [ tableColumn identifier ] ;
    FortuneFile        * ff = [ theFortList objectAtIndex: row ] ;
    if ( [ colId isEqual: @"filename" ] )
    {
        return ( [ [ ff fortuneFilePath ] lastPathComponent ] );
    }
    else if ( [ colId isEqual: @"percvalue" ] )
    {
        return ( [ NSNumber numberWithFloat: [ ff fileProb ] * 100 ] );
    }
    else if ( [ colId isEqual: @"numoffort" ] )
    {
        return ( [ NSNumber numberWithInt: [ ff numOfFortunes ]] );
    }
    else if ( [ colId isEqual: @"offensive" ] )
    {
        return ( [ NSNumber numberWithInt:( [ ff isOffensive ] ? NSOnState: NSOffState) ] );
    }
    else if ( [ colId isEqual: @"used" ] )
    {
        return ( [ NSNumber numberWithInt:( [ ff isUsed ] ? NSOnState: NSOffState) ] );
    }

    return ( [ ff fortuneFilePath ] );
}

Calcolo delle probabilità

Il motivo per cui mi serve la lista dei file ed il numero delle frasi contenute è per calcolare le probabilità di utilizzo di ogni singolo file: è molto semplice. All'apertura della vista (mainViewDidLoad) eseguo la seguente istruzione:

        [ stdFortFiles adjustProbValues: [ offensiveUse selectedTag ] updProbZero: YES ];

che calcola le probabilità di ogni singolo file. Bisogna tenere conto di quale tipo di file sono utilizzati: offensiveUse è un outlet al menu pop-up che mi consente tre possibilità: usare solo i file normali (PFC_OU_NORMAL); usare solo i file offensivi (PFC_OU_ONLYOFFENSIVE); usare tutti i file (PFC_OU_ADDOFFENSIVE). Per semplificarmi la vita, utilizzo la seguente funzione:

BOOL
useMe ( BOOL std, BOOL off, BOOL isOff )
{
    // un file partecipa se si accettano standard e offensivi
    // oppure se si accettano i soli standard ed il file non e' offensivo
    // oppure se si accettano i soli offensivi ed il file e' offensivo
    return ( ( std && off) || ( std && ! off && ! isOff ) || ( ! std && off && isOff ) ) ;
}

Il metodo presenta anche il parametro updProb; se questo è vero, forza un ricalcolo completo delle probabilità, piuttosto che fare affidamento sui valori contenuti all'interno dei singoli oggetti FortuneFile (ma questa è un'opzione non ancora attivata...).

- ( BOOL )
adjustProbValues: (int) useOff updProbZero: (BOOL) updProb
{
    NSEnumerator * fl = [ theFortList objectEnumerator ];
    FortuneFile     * aFF ;
    long        totNumOfFort = 0 ;
    float        sumFFileProb = 0 ;
    float        eps ;
    // vedo se devo includere i file standard e quelli offensivi
    BOOL        incStd = (useOff != PFC_OU_ONLYOFFENSIVE ) ;
    BOOL        incOff = (useOff != PFC_OU_NORMAL ) ;
    // conto il numero di fortune e sommo le varie probabilita'
    while ( aFF = [ fl nextObject ] )
    {
        // un file partecipa alla storia solo se e' considerato utilizzato
        // e se risponde alla caratteristiche desiderate
        if ( [ aFF isUsed ] && useMe( incStd, incOff, [ aFF isOffensive ]) )
        {
            totNumOfFort += [ aFF numOfFortunes ] ;
            sumFFileProb += [ aFF fileProb ] ;
        }
    }
    // se non ci sono fortune, non c'e' proprio nulla da aggiornare
    if ( totNumOfFort <= 0 )
        return ( NO ) ;
    // se arrivo qui, totNumOfFort e' sicuramente positivo
    // aggiorno le probabilita' da zero, nei seguenti casi:
    // - me lo dicono con il parametro
    // - le probabilita' assegnate sono veramente basse
    if ( updProb || sumFFileProb <= 1E-6 )
    {
        // ricalcolo tutto in base al numero di fortune nei file
        float    baseProb = 1.0F / totNumOfFort ;
        fl = [ theFortList objectEnumerator ];
        // rispazzolo tutti i file
        while ( aFF = [ fl nextObject ] )
            // e se il file fa parte della festa, aggiorno la probabilita'
            if ( [ aFF isUsed ] && useMe( incStd, incOff, [ aFF isOffensive ]) )
                [ aFF setFileProb: baseProb * [ aFF numOfFortunes ] ] ;
            else    // altrimenti, ha probabilita' nulla
                [ aFF setFileProb: 0.0F ] ;
        // ho finito e tutto va bene
        return ( YES ) ;
    }
    // se arrivo qui, devo rimodulare le probabilita'
    // sono anche sicuro che sumFFileProb e' significativo
    // non lo faccio se la loro somma e' abbastanza vicina ad 1
    eps = sumFFileProb - 1 ;
    if ( eps < 0 ) eps = - eps ;
    if ( eps > 1E-6 )
    {
        // rimudulo le probabilita' in maniera proporzionale
        float    baseProb = 1.0F / sumFFileProb ;
        fl = [ theFortList objectEnumerator ];
        // rispazzolo tutti i file
        while ( aFF = [ fl nextObject ] )
            // e se il file fa parte della festa, aggiorno la probabilita'
            if ( [ aFF isUsed ] && useMe( incStd, incOff, [ aFF isOffensive ]) )
                [ aFF setFileProb: baseProb * [ aFF fileProb ] ] ;
            else    // altrimenti, ha probabilita' nulla
                [ aFF setFileProb: 0.0F ] ;
        // ho finito e tutto va bene
        return ( YES ) ;
    }
    return ( YES ) ;
}

Le probabilità sono proporzionali al numero di frasi (numOfFortunes) contenute nel file. Calcolando il numero complessivo di fortune, e riscalando tutto utilizzando questo valore, si ottiene per ogni file un numero compreso tra Zero ed Uno che appunto rispecchia la probabilità che il fortune scelta appartenga al file.

Qualora invece l'utente decidessi di attribuire ad ogni file identica probabilità, occorre ricalcolare il tutto in altra maniera: c'è un metodo apposta.

- ( BOOL )
setStdEqualProb: (int) useOff
{
    NSEnumerator * fl = [ theFortList objectEnumerator ];
    FortuneFile     * aFF ;
    long        totNumOfFort = 0 ;
    float        baseProb = 0 ;
    BOOL        incStd = (useOff != PFC_OU_ONLYOFFENSIVE ) ;
    BOOL        incOff = (useOff != PFC_OU_NORMAL ) ;
    // conto quanti file partecipano alla festa
    while ( aFF = [ fl nextObject ] )
        if ( [ aFF isUsed ] && useMe( incStd, incOff, [ aFF isOffensive ]) )
            totNumOfFort += 1 ;
    // se non ci sono file, non c'e' proprio nulla da aggiornare
    if ( totNumOfFort <= 0 )
        return ( NO ) ;
    // se arrivo qui, totNumOfFort e' sicuramente positivo
    // la probabilita' e' la stessa per tutti
    baseProb = 1.0F / totNumOfFort ;
    fl = [ theFortList objectEnumerator ];
    // rispazzolo i file
    while ( aFF = [ fl nextObject ] )
        // se partecipano alla festa, hanno una probabilita'
        if ( [ aFF isUsed ] && useMe( incStd, incOff, [ aFF isOffensive ]) )
            [ aFF setFileProb: baseProb ] ;
        else    // altrimenti, probabilita' nulla
            [ aFF setFileProb: 0.0F ] ;
    return ( YES ) ;
}

Questa volta la probabilità dipende solo dal numero di file che partecipano alla scelta del fortune. La probabilità attribuita ad ogni file è proprio l'inverso del numero dei file.

Per finire, manca il metodo di yahFortunePref che scatena il tutto ad ogni modifica: è l'action associata ad una scelta del menu pop-up e al pulsante di spunta sulle probabilità:

- (IBAction)
updateStdProb: (id) sender
{
    if ( [ equalProbBtn state ] == NSOnState )
        [ stdFortFiles setStdEqualProb: [ offensiveUse selectedTag ] ];
    else
        [ stdFortFiles adjustProbValues: [ offensiveUse selectedTag ] updProbZero: YES ];
    [ stdFileList reloadData ];
    [ self updatePrefFile: sender ] ;
}

Alla ricerca dell'applicazione perduta

Mi sono accorto che i semplici meccanismi di lancio dell'applicazione non sempre funzionavano, per cui ho battezzato una variabile d'istanza di yahFortunePref che contenga il percorso completo dell'applicazione, ed un metodo per inserirci un buon valore. Il metodo è utilizzato ogni volta che è necessario conoscere il percorso dell'applicazione YahFortune. La prima volta che viene lanciato, il percorso non è noto, e quindi il metodo si affanna a cercare l'applicazione. Se invece è noto, il metodo verifica comunque che l'applicazione sia nel percorso indicato, e si allarma se così non è.

- (NSString * )
getYahFortuneAppPath
{
    // prima provo col path
    if ( yahFortuneAppPath )
    {
        NSString    * appName ;
        NSString    * type ;
        BOOL        appExists ;
        // vedo se corrisponde effettivamente
        appExists = [[ NSWorkspace sharedWorkspace ] getInfoForFile: yahFortuneAppPath
                application: & appName type: & type ] ;
        // se e' a posto, lo restituisco
        if ( appExists )
            return ( yahFortuneAppPath ) ;
    }

Verosimilmente, qui la variabile è già stata inizializzata da una invocazione precedente del metodo; tuttavia, si controlla se l'applicazione è proprio lì dove indicato (non sia mai che l'utente la sposti). Se va tutto bene, si restituisce il percorso; altrimenti, si continua, perché è come se il percorso non fosse noto.

    // se arrivo qui, o l'applicazione indicata non esiste, oppure il path e' nullo
    // provo a vedere se e' stata spostata, ma registrata dal sistema operativo
    NSString * fp = [[ NSWorkspace sharedWorkspace ] fullPathForApplication: @"YahFortune" ];
    // c'e', ho finito
    if ( fp )
        return ( fp ) ;

A quanto pare, se l'applicazione è stata lanciata almeno una volta, oppure si trova nella directory predefinite (come la cartella Applicazioni), il sistema operativo ne tiene traccia, e il metodo fullPathForApplication della classe NSWorkspace è sufficiente a determinare il percorso. Se invece l'applicazione non è mai stata lanciata, non ho altra ppossibilità che chiedere all'utente di cercarla per me.

    // se arrivo qui, non c'e' verso di trovarla, chiedo di cercarla per me
    NSOpenPanel    * sPanel ;
    int            risposta ;
    // apro un pannello di avvertimento
    NSAlert *alert = [[NSAlert alloc] init];    
    [alert setMessageText:@"Non riesco a trovare l'applicazione YahFortune"];
    [alert setInformativeText:@"Localizza per me l'applicazione"];
    [alert addButtonWithTitle:@"OK"];
    [alert setAlertStyle:NSWarningAlertStyle];
    [alert runModal] ;
    // e all'ok apro un pannello di selezione file
    sPanel = [ NSOpenPanel openPanel ] ;
    [ sPanel setTitle:@"Cerca applicazione YahFortune" ];
    [ sPanel setPrompt:@"Eccola" ];
    [ sPanel setCanChooseDirectories: NO ];
    [ sPanel setCanChooseFiles: YES ];
    risposta = [ sPanel runModalForDirectory: nil file: nil
                    types: [ NSArray arrayWithObject: @"app"] ] ;
    [alert release];

Per farlo, apro il solito pannello di apertura file, e permetto la selezione dei soli file che hanno estensione app (in pratica, le applicazioni). L'utente però potrebbe essere dispettoso e selezionare una applicazione che c'entra nulla con fortune. Devo allora verificare che il percorso corrisponda proprio all'applicazione YahFortune.

    if ( risposta == NSOKButton )
    {
        // se e' andato bene, prelevo il nome del file
        fp = [ sPanel filename ] ;
        // verifico che e' effettivamente lei
        // confronto il bundle identifier
        NSString    * bi = [ [ NSBundle bundleWithPath: fp ] bundleIdentifier ] ;
        if ( [ bi isEqualToString: PFC_yahf_prefFileName ] )
            return ( fp );
    }
    // se arrivo qui, proprio non l'ho trovata...
    return ( nil );
}

Piuttosto che controllare il nome (un utente ancora più dispettoso potrebbe prendere un file qualsiasi e rinominarlo...) verifico che il bundleIdentifier corrisponda a quello registrato, ovvero alla stringa it.macocoa.djzero00.yahFortune. Se così è, sono riuscito nell'impresa di localizzare l'applicazione; altrimenti, ci rinuncio in maniera definitiva.

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).