MaCocoa 070

Capitolo 070 - Un servizio di fortuna

Aggiungo un piccolo tassello a YahFortune, attraverso la realizzazione di un servizio (sì, quello che si trova nel menu Servizi di ogni applicazione).

Sorgenti: l'esempio Apple Simple Service.

Prima stesura: 16 agosto 2005

Servizi

Una delle caratteristiche di Mac OS X è la presenza, in ogni applicazione, del menu Servizi, proprio all'interno del menu principale di ogni applicazione.

figura 01

figura 01

Qui le normali applicazioni possono inserire delle voci tali da attivare alcune funzioni proprie dell'applicazione. Ad esempio, l'applicazione Mail inserisce delle funzioni: una per inviare una mail all'indirizzo che si suppone selezionato all'interno di una qualche altra applicazione, oppure per inserire il testo selezionato (in un contesto generico, anche nel Finder) in una nuova mail; cose del genere. Il mio intento è di aggiungere una voce a questo menu che produca, utilizzando tutto quanto già sviluppato finora in YahFortune, una stringa con un nuovo fortune.

Si tratta di far comunicare tra loro due applicazioni; la comunicazione avviene in maniera molto slegata, attraverso l'uso degli appunti. In altre parole, una applicazione fornisce servizi ricevendo informazioni attraverso gli appunti (un oggetto di tipo NSPasteboard) e restituisce risultati usando lo stesso meccanismo. Un servizio funziona da utilizzatore (processor) se riceve informazioni e le utilizza per eseguire qualche operazione (come appunto l'esempio di Mail citato sopra); oppure da fornitore (provider) se produce qualche informazione utilizzata dall'applicazione in primo piano (che è appunto quello che intendo fare con il servizio di Fortune). Ovviamente, il servizio può funzionare sia da utilizzatore che da fornitore, se le operazioni svolte sono in qualche modo di filtro (ad esempio, il servizio di Istantanea).

Le applicazioni fornitrici di servizi sono potenzialmente tutte le applicazioni disponibili. Mac OS X si preoccupa in piena autonomia di rintracciare tutte le applicazioni fornitrici di servizi e di inserire le voci di menu al loro posto. È inoltre possibile costruire della applicazioni specializzate nell'esclusiva fornitura di servizi. Queste applicazioni devono trovarsi all'interno di una cartella Services all'interno di una delle Librerie standard (di sistema, globali o per ogni singolo utente), ed avere come suffisso la dicitura .service al posto della classica .app. Per informare il sistema operativo su quali siano i servizi messi a disposizione da una applicazione, occorre riempire in maniera opportuna il file Info.plist dell'applicazione, secondo uno schema predefinito.

Dichiarazione di Servizio

All'interno del file Info.plist dell'applicazione, oltre agli elementi standard, occorre infatti aggiungere qualcosa del genere.

    <key>NSServices</key>
    <array>
        <dict>
            <key>NSMenuItem</key>
            <dict>
                <key>default</key>
                <string>YahFortune/Nuovo fortune</string>
            </dict>
            <key>NSMessage</key>
            <string>makeFortuneService</string>
            <key>NSPortName</key>
            <string>YahFortunex</string>
            <key>NSReturnTypes</key>
            <array>
                <string>NSStringPboardType</string>
            </array>
        </dict>
    </array>>

La chiave NSServices avverte il sistema operativo che l'applicazione è in grado di fornire servizi. L'elenco dei servizi è indicato dallo array successivo, che nel mio caso è costituito da un solo elemento. Ogni singolo servizio è indicato attraverso un dizionario, composto da varie voci. In primo luogo, stabilisco il nome della voce che sarà inserita nel menu (il meccanismo key-string è utilizzato per l'internazionalizzazione). Nel menu Servizi comparirà quindi la voce YahFortune, dotata di un sottomenu (come indicato dal carattere /) denominato Nuovo Fortune.

figura 02

figura 02

Poi indico quale metodo verrà chiamato quando l'utente seleziona la voce. Detto meglio, quando si seleziona la voce Nuovo fortune, il sistema operativo richiederà all'applicazione l'esecuzione del seguente metodo:

- (void)makeFortuneService:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error ;

dove il primo argomento riporta la selezione corrente, il secondo una eventuale stringa (specificata sempre all'interno di Info.plist) per caratterizzare meglio l'esecuzione del servizio, ed il terzo argomento è il posto dove, se il caso, riportare errori nell'esecuzione del servizio.

Con l'attributo NSPortName si attribuisce un identificatore al servizio. Deve corrispondere alla registrazione che si vedrà in seguito. Con NSReturnTypes si stabilisce il tipo di dati forniti dal servizio; nel mio caso, si tratta di una stringa. Questo tipo di dati è poi utilizzato da ogni applicazione per validare o meno la voce di menu del servizio. Dire che il servizio restituisce una stringa equivale ad avere la voce quasi sempre abilitata (in pratica in ogni situazione in cui è possibile incollare una stringa...).

Preparazione di YahFortune

Per poter realizzare il servizio, devo modificare un po' il codice di YahFortune per poterlo riutilizzare il più possibile. La modifica principale, che a cascata richiede minori modifiche altrove, è la trasformazione del metodo makeAFortune della classe frtWinCtrl in una funzione all'interno del file communUtils.m. In questo modo posso eseguire la produzione di una stringa di fortune semplicemente utilizzando una funzione, slegandola dall'interfaccia dell'applicazione. Nell'operazione, le cose non cambiano molto:

NSString *
makeAFortune( NSBundle * fromBundle, NSMutableDictionary * currPrefs )
{
    ...
    // recupero il percorso dell'eseguibile all'interno dell'applicazione
    fortunePath = [ fromBundle pathForResource: @"fortune" ofType: @"" ] ;
    // costruisco un task apposito
    fortuneTask = [[NSTask alloc] init];
    ...

La presenza di una funzione modifica ovviamente il suo uso; ad esempio, all'interno del metodo awakeFromNib di frtWinCtrl si passa da

        [ self makeAFortune ] ;

a

        [ self writeFortuneOnfile: makeAFortune( [ NSBundle mainBundle ] , currPrefs) ];

E così via per tutti gli altri punti in cui era invocato il metodo.

figura 03

figura 03

figura 04

figura 04

A questo punto, sono pronto per creare all'interno del progetto un nuovo Target per la realizzazione del servizio. Aggiungo quindi il target YahFortuneSrv, scegliendo tra le varie possibilità quella di una applicazione Cocoa.

Ci sono due importanti modifiche da fare sulle opzioni di default del target. La prima consiste nel cambiare l'estensione del prodotto compilato da .app a .service. Per fare questo, devo aprire la finestra delle informazioni del target, e poi, nel tab Build, modificare la voce apposita.

La seconda operazione, che non sarebbe strettamente necessaria eseguire all'interno di XCode, ma che sveltisce il lavoro, è di spostare (o meglio, copiare) il prodotto YahFortuneSrv.service all'interno della cartella ~/Library/Services. In questo modo, ad ogni compilazione, non si deve eseguire manualmente questa operazione dal Finder. Per fare questo, ho aggiunto una nuova fase alle operazioni di Build (c'è una apposita voce nel menu), una fase di copiatura di file. Ho definito la directory di destinazione nel pannello di informazioni relativo, e quale file copiare semplicemente trascinandolo sopra la cartella Copy Files.

Posso cominciare con lo scrivere il codice vero e proprio dell'applicazione.

Codice di servizio

Copiando brutalmente dall'esempio fornito da Apple, ecco il file principale:

#import <foundation foundation.h>
#import <cocoa cocoa.h>

#import "FortuneService.h"

int main (int argc, const char *argv[]) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    FortuneService *serviceProvider = [[FortuneService alloc] init];

    NSRegisterServicesProvider(serviceProvider, @"YahFortunex");

    NS_DURING
        [[NSRunLoop currentRunLoop] configureAsServer];
        [[NSRunLoop currentRunLoop] run];
    NS_HANDLER
        NSLog(@"%@", localException);
    NS_ENDHANDLER

    [serviceProvider release];
    [pool release];

    exit(0);     // insure the process exit status is 0
    return 0;     // ...and make main fit the ANSI spec.
}

Sostanzialmente, si costruisce una istanza di una classe (che sarà la destinataria del metodo makeAFortune chiamato attraverso la voce del menu), e poi si entra nel classico loop infinito tipico di una applicazione. All'interno di questo loop, si aspetta che qualcuno chiami il metodo. Da notare il collegamento con la voce del menu dei servizi, in cui l'applicazione si registra come fornitrice di servizi sulla porta con il nome presente all'interno del file Info.plist.

La classe FortuneService è piuttosto semplice (si fa per dire, ovviamente, ho penato un po'...). Il metodo principe è ovviamente il seguente:

- (void)makeFortuneService:(NSPasteboard *)pboard
            userData:(NSString *)userData
            error:(NSString **)error
{
    NSString *newString = nil;
    NSString *thePath = nil;
    NSArray *types;

    if ( thePath = getYahFortuneAppPath( yahFortuneAppPath, PFC_yahf_prefFileName ) )
    {
        [ self setYahFortuneAppPath: thePath ];
        newString = makeAFortune( [NSBundle bundleWithPath: thePath] , getPrefFromFile( ) );
    }

    types = [NSArray arrayWithObject:NSStringPboardType];
    [pboard declareTypes:types owner:nil];
    [pboard setString:newString forType:NSStringPboardType];
    return;
}

La prima cosa da fare è di rintracciare il percorso dell'applicazione YahFortune, all'interno della quale risiede l'eseguibile fortune utilizzato per la produzione delle stringhe (questo perché ho voluto evitare di inserire una nuova copia del tutto all'interno dell'applicazione di servizio; mi sono complicato la vita, ma ho risparmiato quel mega di spazio su disco rigido...). Per svolgere questo compito, ho usato il metodo getYahFortuneAppPath della classe yahFortunePref (anzi, ho trasformato anche questo metodo in una funzione, l'ho spostato dalla classe nel file commonUtils.m).

Con il percorso dell'applicazione, è un gioco da ragazzi produrre una stringa newString con un nuovo fortune. Di seguito, ci sono le tre istruzioni che mettono il risultato a disposizione del mondo. Si prepara l'oggetto NSPasteboard che serve da comunicazione dicendo che al suo interno ci sarà appunto una stringa, e si inserisce la stringa al suo interno. Fine.

Però per fare funzionare le cose, ho penato parecchio, finché non ho spento tutto e riacceso (una delle regole fondamentali di ogni informatico). Infatti, i servizi sono esaminati dal sistema operativo allo startup; occorre quindi effettuare il logout e quindi il login per avere nel menu dei servizi l'apposita voce.

Che funziona. Quasi ovunque. TextEdit, XCode, ogni editor di testi funziona. Non funziona però, stranamente, con Mail (che, va detto, era lo scopo principale del servizio...). Chissà perché.

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