MaCocoa 059

Capitolo 059 - Suonala ancora Sam

Questo capitolo esce dal flusso relativo alla catalogazione di CD affrontando un diverso argomento, o meglio, alcuni argomenti differenti, racchiusi all'interno di una cornice unificante: come interagire con iTunes?

Sorgenti: c'è un tutorial su CocoaDevCentral, che ho ripreso quasi uguale. Di più: questo capitolo è una scopiazzatura...

Prima stesura: 19 settembre 2004

Il problema e la soluzione

Spesso mi accade di lavorare al computer con iTunes in sottofondo che suona brani musicali. La mia età è tale per cui spesso non ricordo più cosa abbia programmato di suonare: mi trovo quindi a chiedermi quale canzona sia suonata in quel momento, sembrandomi nota ma non avendo presente autore e titolo. Altre volte invece, suona il telefono o ci sono altri disturbi, e voglio interrompere il flusso musicale. In entrambi i casi, devo abbandonare l'applicazione in primo piano, richiamare iTunes, guardare cosa sta suonando oppure fermare la musica. Ritornando dopo le incombenze, riprendo la musica e ritorno all'applicazione iniziale. Sarebbe bello, mi sono detto, se tutto questo potesse avvenire con meno fatica.

figura 01

figura 01

L'idea è di utilizzare un menu sulla barra principale, come ad esempio quelli relativi al volume, alla carica della batteria, la rappresentazione della data e ora, eccetera.

Questo menu dovrà presentare le ultime dieci canzoni suonate da iTunes, ed aggiungere un paio di comandi per la gestione di iTunes: l'apertura di iTunes, la fermata o il riavvio della riproduzione.

Gli argomenti principali del capitolo sono sostanzialmente due: la costruzione di un menu nella barra principale e l'interazione con iTunes. Ora, l'interazione con applicazioni esterne può avvenire comodamente solo le applicazioni comunicanti condividono un linguaggio o una struttura comune. L'unico modo che ho trovato per comunicare tra la mia nuova applicazione e iTunes è stato AppleScript. Quindi, all'interno di questo capitolo, ci sarà una programmazione mista ObjectiveC-Applescript, e un meccanismo per eseguire script Applescript all'interno del progetto.

Gli script Applecript

Per prima cosa, scrivo i due script che servono per interagire con iTunes. I due script servono il primo per recuperare informazioni sulla canzone in esecuzione al momento, mentre il secondo serve per fermare o far partire l'esecuzione. Non ho intenzione qui di fare un corso su AppleScript (so cos'è, so scrivere degli script, ma non mi sento ancora di raccontare tutto su questo linguaggio). Tuttavia, gli script presentati sono estesamente commentati, e il linguaggio è in effetti piuttosto semplice.

Redigo gli script avvalendomi dell'applicazione Script Editor, presente in ogni installazione standard di Mac OS X. Parto dal secondo script, quello che controlla l'esecuzione di iTunes; è molto semplice:

tell application "Finder"
    set procList to the name of every process
    if (procList contains "iTunes") then
        tell application "iTunes"
            playpause
        end tell
    end if
end tell

Per prima cosa, si interagisce con il Finder; alla variabile procList attribuisco la lista delle applicazioni in quel momento in esecuzione. Se all'interno di questa lista compare iTunes, allora comunico direttamente con iTunes. Dico di eseguire il comando playpause. Questo comando serve a modificare lo stato corrente di riproduzione: se è attiva, ferma la riproduzione, se ferma, l'attiva.

figura 02

figura 02

Si può conoscere l'intera serie di comandi e di proprietà AppleScript messe a disposizione da iTunes consultando il dizionario AppleScript di iTunes stesso; basta usare Script Editor e la voce di menu apposita (Apri Dizionario...).

figura 03

figura 03

Sempre attraverso Script Editor, è possibile provare l'efficacia dello script, operando sul pulsante di esecuzione, ed anche rendersi conto dell'andamento delle operazioni.

Sono pronto adesso per lo script successivo, un po' più complicato; scopo dello script è di ricavare informazioni sulla canzone (traccia) correntemente in esecuzione. Dal dizionario AppleScript di iTunes ricavo che è possibile conoscere tale canzone attraverso la proprietà current track, dalla quale, a sua volta, è possibile ricavare autore, titolo, e tutte le altre informazioni accessorie di un file mp3 (i tag).

tell application "Finder"
    set procList to the name of every process
    if (procList contains "iTunes") then
        tell application "iTunes"
            if the player state is playing then
                -- leggo qualche informazione sulla traccia
                set curTrack to the current track
                -- indice della playlist e della traccia
                set trkPlayIdx to the index of the current playlist
                set trkIdx to the index of curTrack
                -- autore, titolo, album e durata
                set trkAut to the artist of curTrack
                set trkName to the name of curTrack
                set trkAlb to the album of curTrack
                set trkDur to the duration of curTrack
                -- restituisco tutto in una riga separata da tab
                return (trkPlayIdx & tab & trkIdx & tab & trkAut ¬
                    & tab & trkName & tab & trkAlb & tab & trkDur) as string
            else
                -- errore: iTunes non sta suonando
                return "ERR_ITUNES" & tab & "iTunes not playing"
            end if
        end tell
    else
        -- errore: iTunes proprio non c'e'
        return "ERR_ITUNES" & tab & "iTunes not present"
    end if
end tell

Lo script comincia nello stesso modo del precedente, verificando la presenza di iTunes tra le applicazioni in esecuzione. Se questo non è vero, restituisce una stringa con un messaggio di errore. Se iTunes è presente, verifica che ci sia effettivamente una canzone in esecuzione: basta consultare la proprietà player state, che deve essere playing. A questo punto, si ricava la traccia, ed alcune informazioni relative, giusto per far vedere che ne sono capace. In effetti basterebbe autore e titolo, secondo i miei requisiti iniziali; tuttavia, metto da parte anche un paio di valori (indice di playlist ed indice di traccia) per poter in un momento successivo risuonare la traccia (sarà chiaro più avanti). Tutte queste informazioni sono ritornate all'interno di una stringa, dove i vari campi sono separati da un carattere di tabulazione. Anche di questo script se ne può verificare l'efficacia direttamente tramite Script Editor.

L'interfaccia di LastSongs

Chiamo la mia applicazione LastSongs e la costruisco a partire da XCode; si tratta di una Cocoa Application, ovvero di una applicazione che non è basata su documenti; infatti consta di un menu e di una finestra (che chiamerò accessoria, visto che serve a molto poco).

figura 04

figura 04

Il menu è un altro menu rispetto al MainMenu prodotto di default da XCode; è infatti il menu che poi sarà inserito nella barra principale dei menu, e non il menu dell'applicazione (che per altro non vorrò mai utilizzare). La finestra aggiuntiva serve solo per mostrare in piena gloria (cioè con tutti gli attributi ricavati da iTunes) le canzoni suonate di recente, ma non ha un uso sensato. Questa finestra è visualizzata quando si seleziona la voce Show Last Song dal menu.

La voce di menu Play/Stop avvia o ferma la riproduzione, e dovrà essere associata all'esecuzione del primo script sopra presentato. La voce Bring iTunes to the front intende portare davanti a tutte le alpre applicazioni iTunes. Se iTunes non è attivo, lo lancia direttamente. Infine la voce Quit esce dall'applicazione; è presente esplicitamente dal momento che il menu standard dell'applicazione non sarà visualizzato.

All'interno di Interface Builder dichiaro e definisco subito due oggetti; il primo, LastSongCtl, dell'omonima (nuova) classe, funziona da controllore dell'intera applicazione (sarà in effetti l'unica classe originale dell'applicazione). A questa classe fanno riferimento tutte le voci di menu; funziona anche da classe sorgente di dati e delagata dalla NSTableView presente nella finestra. L'oggetto theWinCtl è uno standard NSWindowController, presente per poter gestire la visualizzazione della finestra, ma non ha alcuna particolarità.

La classe LastSongCtl possiede outlet verso il menu e la NSTableView, possiede come variabile d'istanza la lista delle (massimo dieci) ultime canzone suonate da iTunes e due oggetti della classe NSAppleScript.

La classe NSAppleScript

Cocoa mette a disposizione la classe NSAppleScript per caricare, compilare ed eseguire AppleScript; ad esempio, si può caricare uno script da un file, oppure produrne uno al volo all'interno di una NSString; costruita l'istanza della classe, lo script può essere semplicemente compilato o direttamente mandato in esecuzione; si otterrà un risultato (un oggetto della classe NSAppleEventDescriptor) ed eventualmente un messaggio di errore, il tutto sotto controllo di istruzioni Objective C; che meraviglia.

Utilizzo due oggetti di questa classe per conservare, compilati e pronti all'uso, i due script sopra preparati tramite lo Script Editor. Preparo tutto all'inizializzazione della classe LastSongCtl:

-(id)
init
{
    // inizializzo
    self=[super init] ;
    if( self )
    {
        NSURL        * scriptURL ;
        NSString    * scriptPath ;
        // recupero i due script e li inserisco nelle mie variabili
        scriptPath = [[[NSBundle mainBundle] resourcePath]
            stringByAppendingPathComponent:@"gettrack.scpt"] ;
        scriptURL = [NSURL fileURLWithPath: scriptPath ];
        getTrackScript =[[NSAppleScript alloc]
            initWithContentsOfURL:scriptURL error:nil];
        scriptPath = [[[NSBundle mainBundle] resourcePath]
            stringByAppendingPathComponent:@"playStop.scpt"] ;
        scriptURL = [NSURL fileURLWithPath: scriptPath ];
        playStopScript =[[NSAppleScript alloc]
            initWithContentsOfURL:scriptURL error:nil];
        // inizializzo la lista delle canzoni e le altre variabili
        listaCanzoni = [[NSMutableArray alloc ]init];
        curNumOfTracks = 0 ;
    }
    return self;
}

figura 05

figura 05

I due file che contengono gli script sono stati aggiunti al progetto, in modo che poi saranno incorporati all'interno dell'applicazione. Il percorso del file è quindi dato dalla cartella Resources presente all'interno del bundle dell'applicazione, percorso al quale si deve ovviamente aggiungere il nome del file. Per inizializzare un oggetto NSAppleScript è però necessario un URL o una stringa (un banale file pare non gli basti); ho scelto la strada di produrre un URL e di inizializzare con questo, piuttosto che caricare in una stringa il contenuto del file e inizializzare l'oggetto NSAppleScript dalla stringa ottenuta.

Se ora è abbastanza chiaro quando debba essere eseguito lo script playStop.scpt, c'è il problema di quando eseguire l'altro script, quello che ricava da iTunes le informazioni relative alla canzone in corso di riproduzione. In mancanza di partecipazione attiva da parte di iTunes, l'unico metodo rimane quello di chiederlo ripetutamente e periodicamente. In altre parole, occorre eseguire più volte, ad intervalli di tempo fissi è l'opzione più semplice, un metodo che ricavi le informazioni. Ho già incontrato un meccanismo del genere quando ho realizzato la finestra About con testo in movimento: si tratta di utilizzare un NSTimer.

La classe NSTimer in pratica attende un dato intervallo di tempo (stabilito in sede di inizializzazione), per poi mandare in esecuzione un determinato metodo. La cosa può essere o meno ripetuta in continuazione. Una finestra che rappresenta un orologio con sole ore e minuti, ad esempio, potrebbe avere un timer che scatta ogni sessanta secondi. L'applicazione LastSongs usa un timer come meccanismo principale e unico per svolgere tutte le operazioni.

Aggiornamento del menu

Il metodo awakeFromNib è responsabile della predisposizione di tutte le operazioni: installa il menu nella barra principale, e fa partire il timer periodico:

- (void)
awakeFromNib
{
    // recupero un puntatore alla barra dei menu
    NSStatusBar *bar = [NSStatusBar systemStatusBar];
    // costruisco il menu nella status bar
    lastSongMenu = [bar statusItemWithLength:NSSquareStatusItemLength];
    [ lastSongMenu retain ];
    // metto a posto le caratteristiche del menu
    [lastSongMenu setHighlightMode:YES];
    [lastSongMenu setImage: [ NSImage imageNamed: @"lastSongIcon" ] ];
    [lastSongMenu setMenu: theMenu];
    [lastSongMenu setEnabled: YES];
    // aggiusto la tabella con l'elenco delle ultime canzoni
    [ theTable setDoubleAction: @selector(playThatSong:) ] ;
    [ theTable setTarget: self ] ;
    // predispongo il timer per l'aggiornamento del menu
    menuUpdater = [NSTimer
            scheduledTimerWithTimeInterval: TIMER_UPDATE_INTERVAL
            target:        self        // chiamo questo metodo
            selector:    @selector(readTrackFromITunes:)
            userInfo:    nil
            repeats:    YES] ;    // e continuo
    [ menuUpdater retain] ;
    // faccio partire il timer la prima volta
    // che cosi' aggiorno tutto l'ambaradan
    [menuUpdater fire];
}

figura 06

figura 06

Le prime istruzioni sono responsabili per l'installazione del menu sulla barra. Si recupera l'istanza condivisa a tutte le applicazioni attraverso il metodo systemStatusBar, e si riserva uno spazio fisso dove inserirlo. Le istruzioni successive inseriscono il menu theMenu (costruito all'interno del nib) e fanno in modo che sia visualizzata l'immagine specificata (contenuta in un file che è inserito all'interno del progetto, per cui è sufficiente il metodo imageNamed: per prelevarla). Alternativamente, o contemporaneamente, avrei potuto impostare una stringa con il metodo setTitle:, ma l'icona occupa meno spazio ed è più carina (nei limiti delle mie capacità artistiche).

Le due istruzioni successive si riferiscono alla tabella della finestra accessoria; in questa tabella sono visualizzate le ultime canzoni eseguite. Con un doppio clic su una di queste canzoni, la canzone stessa sarà riprodotta di nuovo: è questo l'effetto finale del metodo playThatSong:. Infine, le ultime tre istruzioni impostano il timer e la funzione da eseguire allo scattare del timer. Il metodo readTrackFromITunes: della classe self (e quindi, LastSongCtl) deve essere eseguito ogni TIMER_UPDATE_INTERVAL secondi (che sono quindici). Il metodo fire serve per far eseguire subito questo metodo e provocare il primo aggiornamento del menu (e della tabella).

Tutto (o quasi) il resto del lavoro è svolto quindi dal metodo readTrackFromITunes:. Le operazioni sono divise in due momenti successivi: l'aggiornamento della base di dati interna (ovvero, la lista delle canzoni suonate), e l'aggiornamento della visualizzazione (il menu e la tabella).

-(void)
readTrackFromITunes:(NSTimer *)timer
{
    int            i, whereIs, numMenu ;
    NSString    * curTrack ;
    NSArray        * trackInfo ;
    NSMenuItem    * item;
    // - - - - - - aggiorno i dati interni
    // recupero autore e titolo della traccia in esecuzione
    curTrack = [ [getTrackScript executeAndReturnError: nil] stringValue ] ;
    trackInfo = [ curTrack componentsSeparatedByString: @"\t" ];
    // se c'e' un errore, lascio perdere
    if([ [trackInfo objectAtIndex: 0 ] isEqualToString:@"ERR_ITUNES"])
        return ;

La prima operazione da eseguire è ovviamente lo script; allo scopo, è sufficiente chiamare il metodo executeAndReturnError:. Questo metodo compila, se necessario, lo script; lo esegue e riporta il risultato all'interno di un oggetto di classe NSAppleEventDescriptor. In realtà l'oggetto in sé non interessa: piuttosto, mi interessa ciò che ha restituito lo script stesso. Lo si ottiene con il metodo stringValue, che produce appunto una stringa. Questa è la stringa che ha prodotto lo script: una riga di elementi separati dal carattere di tabulazione; spezzo allora la stringa in tante sottostringhe, in modo da accedere ai vari campi. In questo modo posso anche verificare se per caso il metodo ha restituito una stringa di errore: in tal caso, non ho nulla da fare (perché iTunes non è presente, o non sta suonando).

    // se la lista e' vuota, aggiungo sen'altro
    if( curNumOfTracks == 0 )
    {
        [listaCanzoni addObject:curTrack];
        curNumOfTracks += 1 ;
    }
    else
    {
        // il piu' delle volte, e' l'ultima inserita
        // in tal caso, devo fare nulla
        if( [ curTrack isEqualToString: [ listaCanzoni objectAtIndex:0 ] ] )
            return ;
        // se arrivo qui, e' cambiata la canzone
        // ma potrebbe essere gia' stata suonata in tempi recenti
        whereIs = [ listaCanzoni indexOfObject:curTrack] ;
        if ( whereIs == NSNotFound )
        {
            // no, proprio non c'e'
            // se ho gia' raggiunto il massimo visualizzabile
            if ( curNumOfTracks == MAX_NUM_OF_TRACKS )
            {
                // elimino l'ultima traccia
                curNumOfTracks -= 1 ;
                [ listaCanzoni removeObjectAtIndex: curNumOfTracks ] ;
            }
            // inserisco la nuova traccia in testa
            [ listaCanzoni insertObject: curTrack atIndex:0 ] ;
            curNumOfTracks += 1 ;
        }
        else
        {
            // c'e' gia', la sposto da dove si trova in testa
            [ listaCanzoni exchangeObjectAtIndex: 0 withObjectAtIndex: whereIs ] ;
        }
    }

A questo punto, sono nella situazione in cui iTunes è vivo e lotta assieme a noi. Aggiorno la base di dati interna, ovvero il vettore listaCanzoni; distinguo vari casi.

Se il vettore è vuoto, aggiungo senz'altro la nuova canzona; se invece c'è già qualcosa, verifico se per caso la canzone in esame è già presente. In effetti, questo metodo è chiamato ogni 15 secondi: nella maggior parte dei casi la canzone in esecuzione è già stata inserita (a meno che non si stia ascoltando qualche disco piuttosto sperimentale di mia conoscenza), ed è proprio l'ultima ad essere stata inserita. In tutto questi casi, continua a non esserci alcunché da fare.

Finalmente arrivano i casi interessanti: la prima cosa che ci può capitare è che la canzone non è presente nella lista. Devo allora aggiungerla in cima, facendo attenzione a non superare il limite massimo di canzoni previste (che ho fissato a dieci, ma solo per avere un numero). Se ho già raggiunto il limite, semplicemente elimino la canzone più vecchia (quella in fondo al vettore), per poi passare senz'altro ad aggiungere la nuova canzone.

Tuttavia, potrebbe capitare che la canzone in corso è stata già eseguita in precedenza (entro le precedenti dieci). In tal caso la porto in prima posizione, ed al suo posto ci infilo la canzone precedente (sarebbe stato meglio scalare tutte le altre canzoni, ma non avevo voglia di pensare).

Insomma, il risultato finale di questa serie di operazione è stata quella di avere modificato il vettore listaCanzoni. Adesso, bisogna modificare l'aspetto dell'applicazione: il menu e la lista presente nella finestra accessoria:

    // - - - - - - aggiorno l'aspetto visuale
    numMenu = [[theMenu itemArray] count ] ;
    // tolgo tutte le voci di traccia presenti
    for ( i = (numMenu-6) ; i >= 0 ; i -- )
    {
        [ theMenu removeItemAtIndex: i ] ;
    }
    // ed adesso lo riempio con le voci da listacanzoni
    for ( i = (curNumOfTracks-1) ; i >= 0 ; i -- )
    {
        // la spezzo nei vari componenti
        trackInfo = [ [ listaCanzoni objectAtIndex: i ]
            componentsSeparatedByString: @"\t" ];
        // produco una stringa autore/titolo
        curTrack = [ NSString stringWithFormat: @"%@ - %@",
                        [trackInfo objectAtIndex: 2 ],
                        [trackInfo objectAtIndex: 3 ] ] ;
        // inserisco la voce nel menu, con una action
        item = [ [NSMenuItem alloc] initWithTitle: curTrack                    
                    action:@selector(playThatSong:)
                    keyEquivalent:@""];
        // assegno il target e il tag
        [ item setTarget: self ] ;
        [ item setTag: i ] ;
        // inserisco effettivamente il menu
        [ theMenu insertItem:item atIndex:0];
    }
    // forzo un ricarico della lista
    [ theTable reloadData ];
}

Uso un approccio piuttosto drastico per aggiornare il menu: cancello tutte le voci del menu, con l'eccezione delle ultime cinque, che sono quelle standard previste da Interface Builder. Poi, procedo ad inserire man mano una serie di voci corrispondenti ai vari elementi del vettore listaCanzoni. La voce del menu è composta dalle sole informazioni di autore e titolo; ad ogni menu è associata l'azione playThatSong: avente come target la classe self (ancora LastSongCtl). Per distinguere di quale canzone si tratta, attribuisco alle varie voci anche un tag (l'indice del vettore listaCanzoni).

E con questo, il menu è servito.

L'ultima istruzione, infine, dice alla tabella di ricaricare i dati perché ci sono state delle modifiche. Infatti, in sede di Interface Builder, avevo collegato la tabella alla classe LastSongCtl come sorgente di dati e come delegata. Tra le altre cose, significa che all'interno di LastSongCtl ci sono i metodi

- (int)    numberOfRowsInTableView: (NSTableView *)tableView;
- (id)    tableView: (NSTableView *)tableView objectValueForTableColumn: (NSTableColumn *)tableColumn row: (int)row ;

necessari appunto alla visualizzazione dei dati: sono piuttosto semplici.

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

- (id)
tableView:            (NSTableView *)tableView
    objectValueForTableColumn:    (NSTableColumn *)tableColumn
    row:             (int)row
{
    NSString * colId ;
    NSArray        * trackInfo ;
    // recupero l'identificatore della colonna
    colId = [ tableColumn identifier] ;
    // e la informazioni relative alal riga da restituire
    trackInfo = [ [ listaCanzoni objectAtIndex: row ]
        componentsSeparatedByString: @"\t" ];
    // a seconda del nome della colonna, restituisco la stringa
    if ( [ colId isEqual: @"autore" ] )
    {
        return ( [trackInfo objectAtIndex: 2 ] );
    }
    if ( [ colId isEqual: @"titolo" ] )
    {
        return ( [trackInfo objectAtIndex: 3 ] );
    }
    if ( [ colId isEqual: @"album" ] )
    {
        return ( [trackInfo objectAtIndex: 4 ] );
    }
    if ( [ colId isEqual: @"durata" ] )
    {
        // la durata e' espressa in secondi
        int min, sec ;
        sec = [ [trackInfo objectAtIndex: 5 ] intValue ] ;
        // la spezzo in minuti e secondi
        min = sec / 60 ;
        sec = sec % 60 ;
        // per mettere uno zero se ci sono pochi secondi
        if ( sec < 10 )
            return ( [NSString stringWithFormat: @"%4d:0%1d", min, sec] );
        return ( [NSString stringWithFormat: @"%4d:%2d", min, sec] );
    }
    return ( [trackInfo objectAtIndex: 2] );
}

Il secondo metodo è un po' lungo, ma assolutamente lineare nel suo svolgimento; dopo aver diviso per l'ennesima volta la stringa nei vari pezzi, il metodo restituisce il campo adatto alla richiesta. Fa qualche lavoro in più nel caso del tempo, in quanto iTunes restituisce il dato sotto forma di secondi, mentre la visualizzazione sarebbe meglio farla in qualche formato più riconoscibile.

Operazioni da menu

Attraverso il menu si svolgono diverse operazioni, come già illustrato in sede di presentazioni. Le voci standard sono presto trattate.

Per visualizzare la finestra accessoria c'è il metodo seguente:

- (IBAction)
showLastSongs:(id)sender
{
    [ winctl showWindow: sender ];
    [ NSApp activateIgnoringOtherApps:YES];
    [ [ winctl window ] makeKeyAndOrderFront: sender ];
}

La finestra, che in Interface Builder mi sono premunito di non visualizzare fin da subito, deve essere per prima cosa costruita e visualizzata. Poi va portata davanti a tutte le altre con il classico makeKeyAndOrderFront (lasciate al momento perdere l'istruzione di mezzo: è necessaria in un secondo tempo).

Per arrestare o riprendere l'esecuzione, si deve eseguire lo script apposito:

- (IBAction)
playStop:(id)sender
{
    [ playStopScript executeAndReturnError:nil] ;
    // aggiorno tutto
    [ menuUpdater fire ];
}

Facile anche qui, adesso che ho visto come si fa. Mi devo ricordare anche di eseguire l'aggiornamento dei dati (soprattutto se faccio ripartire l'esecuzione).

Per portare iTunes davanti o lanciarlo in esecuzione nel caso non lo sia, si usa un unico meccanismo:

- (IBAction)
showiTunes:(id)sender
{
    [ [ NSWorkspace sharedWorkspace ] launchApplication: @"iTunes" ];
}

Utilizzo la classe NSWorkspace, che contiene metodi di utilità per l'interazione con il sistema operativo, per lanciare in esecuzione una applicazione di dato nome.

Per uscire dall'applicazione, c'è il comando Quit, che esegue un brutale uscita di scena:

- (IBAction)
quitMenu:(id)sender
{
    [ NSApp terminate: sender ] ;
}

La parte più difficile (e finale) è quando l'utente seleziona una delle voci di menu corrispondenti ad una delle canzoni memorizzate: l'idea è di eseguire nuovamente la canzone selezionata. La stessa cosa faccio accadere quando l'utente fa doppio clic su una delle righe della tabella all'interno della finestra accessoria. Il metodo è il già citato playThatSong::

- (IBAction)
playThatSong:(id)sender
{
    NSArray        * trackInfo ;
    NSString    * playuotString ;
    NSAppleScript * playuotScript ;
    // per ora nessuna selezione
    int    idx = -1 ;
    // potrei aver selezionato tramite menu
    if ( [ sender isKindOfClass: [ NSMenuItem class ]] )
    {
        // ed allora l'indice la canzone la ottengo attraverso
        // il tag della voce di menu
        idx = [ sender tag ] ;
    }
    // oppure ho fatto doppio clic sulla lista
    if ( [ sender isKindOfClass: [ NSTableView class ]] )
    {
        // ed allora l'indice e' la riga selezionata
        idx = [ sender selectedRow ] ;
    }
    // giusto per paranoia
    if ( idx == -1 ) return ;
    // adesso recupero la canzone
    trackInfo = [ [ listaCanzoni objectAtIndex: idx ]
        componentsSeparatedByString: @"\t" ];

Per prima cosa riconosco l'origine del comando, se menu o tabella. Lo scopo delle istruzioni è di arrivare ad un buon valore per la variabile idx, che riporti in tutti i casi l'indice dell'elemento prescelto all'interno del vettore listaCanzoni. Una volta determinata la canzone, come al solito spezzo la stringa nelle sue varie componenti.

    // produco uno script al volo
    playuotString = [ NSString stringWithFormat:
        @"tell application \"iTunes\" to play track %d of playlist %d",
            [[trackInfo objectAtIndex: 1 ] intValue ],
            [[trackInfo objectAtIndex: 0 ] intValue ] ] ;
    // costruisco uno applescript
    playuotScript = [ [ NSAppleScript alloc ] initWithSource: playuotString ];
    // e lo eseguo
    [ playuotScript executeAndReturnError:nil ];
    // aggiorno tutto (a prescindere dal risultato)
    [menuUpdater fire];
}

Per suonare la canzone prescelta, utilizzo l'indice di playlist e l'indice di track per impartire un comando ad iTunes. Come al solito, devo passare attraverso uno script; questa volta, però, lo script dipende da alcune variabili: piuttosto che avere uno script esterno, conviene produrre uno script direttamente all'interno di una stringa (la variabile playuotString), convertire la stringa in uno script ed eseguirlo brutalmente.

figura 07

figura 07

C'è infine una ultima operazione da fare, ovvero trasformare l'applicazione da una applicazione standard in una applicazione normalmente in background; così facendo, non compare nel Dock e nella lista delle applicazioni che si possono uccidere con la chiusura forzata. Si interviene all'interno del file Info.plist. Ci sono due possibilità; la prima è di aggiungere la seguente coppia chiave- valore (che confesso non ricordare da dove salti fuori):

    <key>NSBGOnly</key>
    <string>1</string>

Questo dice che l'applicazione funziona solo in background. Così facendo, però, quando si seleziona la voce Show Last Songs, la finestra corrispondente non c'è verso di portarla davanti a tutte le altre, ma occorre, una volta visibile, selezionarla esplicitamente.

L'altra possibilità è di usare una coppia leggermente differente (nella documentazione sulle Property List, dopo che nell'esempio da me copiato se ne faceva uso):

    <key>LSUIElement</key>
    <string>1</string>

il cui effetto è simile alla coppia precedente; qui però, con l'uso dell'istruzione prima non commentata

    [ NSApp activateIgnoringOtherApps:YES];

la finestra con la lista delle canzoni è portata direttamente in primo piano.

figura 08

figura 08

Insomma, il risultato finale, benché non del tutto professionale, con ampie possibilità di miglioramento, risponde alle mie esigenze.

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