MaCocoa 076

Capitolo 076 - Continua DownQueue

La libreria cURL

Riprendo adesso cURL come applicazione di appoggio per il trasferimento di file. Esplorando meglio il sito, scopro l'esistenza della libreria libcURL, che svolge le stesse funzioni di cURL, ma a livello di libreria. In altre parole, non è più necessario utilizzare il meccanismo dei pipe per eseguire l'applicazione da linea di comando e pescare il risultato dell'esecuzione in modo da visualizzarlo. è sufficiente eseguire alcune chiamate a funzioni C rese appunto disponibili dalla libreria stessa. Il comando cURL stesso non è altro che una piccola interfaccia (a linea di comando) costruita attorno alla libreria. La libreria, così come il comando cURL, sono presenti nell'installazione standard del sistema operativo. Tuttavia, per poter utilizzare le funzioni, occorre indicare al progetto XCode di utilizzare tale libreria. Ciò avviene aggiungendo (tramite drag and drop, ad esempio), il file libcurl.dlyd (al'interno della cartella /usr/lib) all'elenco dei framework del progetto.

Il sito di cURL fornisce abbondante documentazione ed esempi in proposito all'uso. Ho seguito il tutorial presente nel sito ed ho cominciato a costruire una classe, CurlServer, che potesse funzionare da thread per il trasferimento dei file.

Per poter inizializzare la classe e la libreria, occorre effettuare un paio di operazioni notevoli:

- (id) initWithCaller: (id) localCaller
{
    self = [ super init ];
    [ self setTheCaller: localCaller] ;
    stopDownload = NO ;
    // ne approfitto per inizializzare curl
    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();
    if ( curl )
        return ( self ) ;
    // se non ho curl, e' inutile continuare
    return ( nil ) ;
}

Il metodo deve chiamare un paio di funzioni per inizializzare correttamente la libreria. In particolare, la funzione curl_easy_init fornisce una variabile di tipo CURL che deve essere utilizzare ogni volta che si accede alla funzioni della libreria. È per tanto una variabile d'istanza della classe.

Già che ci sono, il metodo dealloc deve chiudere pulitamente la libreria:

- (void) dealloc
{
    // chiudo curl
    curl_global_cleanup();
    [super dealloc];
}

La parte più difficile di tutta la faccenda è rappresentata dalle callback, ovvero da funzioni che sono chiamate dalla libreria e che devono essere realizzate dall'utilizzatore della libreria per avere qualcosa di funzionante.

Sostanzialmente, occorre scrivere la funzione che si occupa di trattare i dati ricevuti nel trasferimento; se la funzione non fosse presente, i dati ricevuti verrebbero scritti sulla console e quindi sarebbero a tutti gli effetti perduti. Questa funzione deve essere fatta più o meno così:

size_t function( void *ptr, size_t size, size_t nmemb, void *stream);

La funzione è chiamata da libcURL ogni volta che nel trasferimento sono disponibili dei dati che conviene siano salvati da qualche parte. Sono presenti size elementi, presenti in memoria a partire dal puntatore ptr, con ciascun elemento di dimensione nmemb (in pratica, significa che sono disponibili size * nmemb byte). La funzione deve restituire il numero di byte che sono stati effettivamente trattati, che deve coincidere con quelli arrivati, altrimenti il trasferimento si blocca.

Interessante l'ultimo parametro della funzione, il puntatore stream, che è a cura dell'utilizzatore della libreria. C'è infatit la possibilità di impostare tale puntatore affinché riconduca ad informazioni di varia natura, ed è di uso strategico in tutta la faccenda. Infatti qui stiamo parlando di funzioni C, che non hanno alcun rapporto con metodi ed altro. Quindi, scrivendo la funzione callback, sarà una normale funzione C e non un metodo della classe CurlServer. Questo significa che, ad esempio, non è possibile accedere alle variabili d'istanza della classe. Ecco quindi che posso usare il puntatore stream per passare alla funzione callback tutte le informazioni che le occorrono per ben funzionare.

Nella realizzazione minimale. la callback è sufficiente conosca solo il nome del file dove scrivere i dati. Quindi, normalmente, all'interno di stream è conservato il puntatore al file. In effetti all'inizio avevo scritto le cose in questo modo; poi però mi sono accorto che sarebbe comodo, all'interno della funzione callback, accedere alla classe CurlServer. Alla fine, ho costruito una struttura dati che comprende entrambe le cose:

typedef    struct    _cbData {
    CurlServer        * myself ;        // un puntatore all'oggetto
    NSFileHandle    * writtenFile ;    // il puntatore al file corrente
}
CalldackData, * CallbackDataPtr ;

Da qualche parte, in preparazione al trasferimento, in uno dei metodi di CurlServer, farò più o meno così:

    CalldackData    cbData ;
    ...
    cbData.myself = self ;
    cbData.writtenFile = <puntatore al file dove scrivere i dati> ;

In questo modo, dall'interno della funzione callback, posso accedere facilmente sia alla classe CurlServer sia al file dove scrivere i dati.

Ecco quindi come ho messo in piedi la faccenda:

size_t    WriteCallback(    
                void    *buffer,        /* puntatore ai dati */
                size_t    size,            /* dimensione in byte di un elemento */
                size_t    nmemb,            /* numero di elementi presenti*/
                void    * cbDataHdl )    /* struttura dati privata */
{
    unsigned long long    prevSize, delta ;
    // recupero puntatori all'oggetto ed al file
    NSFileHandle    * fh = ((CallbackDataPtr) cbDataHdl )->writtenFile ;
    CurlServer    * ohmy = ((CallbackDataPtr) cbDataHdl )->myself ;
    // se devo fermare tutto, imbroglio curl
    if ( [ ohmy stopDownload ])
        // questo dovrebbe fermare tutto
        return ( 0 ) ;
    // guardo la dimensione corrente del file
    prevSize = [ fh seekToEndOfFile ];
    // scrivo i dati sul file
    [ fh writeData: [ NSData dataWithBytes: buffer length: (size * nmemb) ]];
    // calcolo i byte aggiunti dalla scrittura
    delta = [ fh seekToEndOfFile ] - prevSize ;
    // dovrebbero essere uguali a (size * nmemb); non lo fossero
    // curl automaticamente produce un errore
    return ( delta );
}

Ricordo che questa funzione è chiamata direttamente dalla libreria, che ha riempito per me di quattro argomenti della funzione; i primi tre, con i dati giunti per il trasferimento, il quarto, con i dati che ho predisposto io. Nel campo writtenFile c'è una variabile NSFileHandle che individua il file dove scrivere i dati. Mi posiziono alla fine del file, scrivo i dati, e verifico quanti dati sono stati scritti, in maniera molto semplice. Uso invece le variabili d'istanza dell classe CurlServer per, eventualmente, fermare il trasferimento (mi è sembrata questa la maniera più pulita per farlo, ma sono sicuro che ci sono altre strade).

Già che ci sono, ho scritto una seconda funzione callback, non indispensabile, ma molto utile per l'interfaccia utente. Infatti, una funzione fatta più o meno come segue è utile per informare l'utente sullo stato delle cose

int        progress( void * stream, double dltotal, double dlnow, double ultotal, double ulnow) ;

Questa funzione è chiamata dalla libreria libcURL periodicamente (diciamo una volta al secondo), sia che ci siano nuovi dati sia che il trasferimento sia fermo per traffico. Il primo argomento è il solito puntatore definito dall'utilizzatore, mentre gli altri quattro argomenti dicono quanti byte dlnow sono stati trasferiti in download su un totale di dltotal, oppure quanti byte ulnow sono stati trasferiti in upload su un totale di ultotal (se i dati non sono noti o non pertinenti, valgono zero). La funzione deve ritornare zero se tutto va bene, altro in caso di problemi, con conseguente bloccaggio del trasferimento).

La funzione che ho scritto è fatta così:

int        ProgressFuncCallback(
                void    * cbDataHdl,    /* struttua dati privata */
                double    t,                /* byte totali da download */
                double    d,                /* byte finora ricevuti */
                double    ultotal,        /* byte totali in upload */
                double    ulnow)            /* byte finora inviati */
{
    // recupero puntatori all'oggetto
    CurlServer    * ohmy = ((CallbackDataPtr) cbDataHdl )->myself ;
    // informo il chiamante dello stato avanzamento
    // lo faccio in questo modo per evitare conflitti di grafica coi thread
    [ [ ohmy theCaller] performSelectorOnMainThread:@selector(advanceMsg:)
            withObject:[ NSArray arrayWithObjects: [ NSNumber numberWithDouble: d], [ NSNumber numberWithDouble: t], nil ]
            waitUntilDone: NO ];
    if ( [ ohmy stopDownload ])
        // questo dovrebbe fermare tutto
        return ( 1 ) ;
    return ( 0 );
}

Qui sfrutto pesantemente il fatto che all'interno del parametro cbDataHdl è presente un riferimento alla variabile CurlServer per far eseguire alcuni metodi. E soprattutto, per dire alla classe che ha fatto partire il tutto (ovvero, il caller di CurlServer, quindi DownQueueWinCtrl) di eseguire il metodo advanceMsg che, si vedrà poi, aggiusta l'aspetto dell'interfaccia mostrando lo stato di avanzamento dei lavori.

Gestione dei trasferimenti

Ora ce ho scritto le callback, che svolgono il lavoro effettivo, devo solo par partire tutto il meccanismo. Questo avviene all'interno del metodo principale della classe CurlServer

- (void) getFileWithCurl: (Url2DL*) theItem
{
    CURLcode        res;
    NSFileHandle    * fh ;
    NSString    * finalFile ;
    CalldackData    cbData ;
    
    // un pool per l'autorelease, visto che siamo in un thread
pool = [[NSAutoreleasePool alloc] init];
    // nome completo del file destinazione
    finalFile = [[ theItem theDestFolder] stringByAppendingPathComponent: [ theItem theName] ];
    // dico al chiamante che sto iniziando
    [ theCaller performSelectorOnMainThread:@selector(startingDownload) withObject: nil waitUntilDone: NO ];
    [ theItem setDlStatus: URLSTS_DOWNLOADING ];
    // costruisco il file di destinazione SOVRASCRIVENDO qualsiasi cosa
    // il controllo dovrebbe essere fatto dal chiamante al momento della scelta del file
    [ [ NSFileManager defaultManager] createFileAtPath: finalFile contents: nil attributes: nil ];
    fh = [ NSFileHandle fileHandleForUpdatingAtPath: finalFile ];
    // nel caso di problemi, mi fermo
    if ( fh == nil)
    {
        // informo il chiamante del problema
        [ theCaller performSelectorOnMainThread:@selector(failedDownload:)
                withObject: NSLocalizedString(@"Fail creating file", @"curlServer: errore NSFileHandle")
                waitUntilDone: NO ];
    }

Poiché questo metodo fa parte di un thread, bisogna costruire un autorelease pool per gestire gli oggetti in memoria. Dopo di che, le cose filano lisce: si costruisce il nome del file destinazione, e quindi anche il file stesso, producendo un oggetto del tipo NSFileHandle (che sarà poi utilizzato dalla funzione callback). Se per qualche motivo la creazione del file non ha successo, si informa la classe chiamante (DownQueueWinCtrl) della cosa. Altrimenti, comincia il ballo.

    else
    {
        // preparo la struttura dati
        cbData.myself = self ;
        cbData.writtenFile = fh ;
        // preparo le opzioni per il comando curl
        // lo url da scaricare
        curl_easy_setopt(curl, CURLOPT_URL, [[ theItem theURL] UTF8String ] );
        // la funzione di callback per la scrittura dei dati
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*) & cbData );
        // opzioni per lo stadio avanzamento
        curl_easy_setopt(curl, CURLOPT_VERBOSE, FALSE);
        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, FALSE);
        // la funzione di callback per lo stato di avanzamento
        curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, ProgressFuncCallback);
        curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, (void*) & cbData );
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE );
        // eseguo le operazioni di download
        res = curl_easy_perform(curl);

Qui preparo la struttura dati per le funzioni callback; poi, configuro il comportamento di cURL attraverso una successione di chiamate alla funzione curl_easy_setopt. Sono così individuati, nell'ordine: lo url del file da trasferire (che va trasformato in una stringa masticabile dalla libreria, che è in C, e non come NSString, tipico di Cocoa); la funzione di callback per la scrittura dei dati, e la predisposizione del quarto argomento della funzione stessa; si evita la scrittura di informazioni non necessarie sullo standard output (CURLOPT_VERBOSE) e si inibiscono le funzioni interne alla libreria per tenere conto dell'avanzamento lavori (CURLOPT_NOPROGRESS); infatti, l'avanzamento lavori è gestito direttamente attraverso la funzione di callback ed il suo argomento; infine, impostando a vero il parametro CURLOPT_FOLLOWLOCATION si dice alla libreria di seguire gli eventuali reindirizzamenti dello Url principale (cosa che ho scoperto accadere spesso quando scarico i file che mi interessano; cosa sia e come funzioni il reindirizzamento, bisognerebbe chiederlo a qualcuno più esperto di me nei protocolli di rete).

A questo punto, la chiamata alla funzione curl_easy_perform esegue magicamente il tutto, ovvero fa partire il trasferimento. La funzione continua a lavorare; chiama periodicamente la funzione ProgressFuncCallback k per comunicare lo stato di avanzamento, e la funzione WriteCallback per la scrittura dati quando ne sono disponibili di nuovi; termina quando l'intero processo di trasferimento si è concluso(oppure si è fermato in errore per qualche motivo).

        // verifico il risultato
        if(CURLE_OK != res)
        {
            NSString    * locerr = NSLocalizedString( @"Fail downloading file", @"curlServer: risultato curl non ok");
            // mi sono fermato per qualche motivo, ristabilisco lo stato dell'elemento
            [ theItem setDlStatus: URLSTS_PARTIAL ];
            // se non ho fermato volontariamente
            // oppure sono fermo ma con errori strani
            if (stopDownload == NO)
            {
                // informo il chiamante
                [ theCaller performSelectorOnMainThread:@selector(failedDownload:)
                        withObject: [ NSString stringWithFormat: @"%@: %d : %s", locerr, res, curl_easy_strerror(res)]
                        waitUntilDone: NO ];
                [ theItem setDlStatus: URLSTS_DWLDERROR ];
            }
            else if ( (res != CURLE_WRITE_ERROR) && (res != CURLE_ABORTED_BY_CALLBACK) )
            {
                // informo il chiamante
                [ theCaller performSelectorOnMainThread:@selector(failedDownload:)
                        withObject: [ NSString stringWithFormat: @"%@: %d : %s", locerr, res, curl_easy_strerror(res)]
                        waitUntilDone: NO ];
                [ theItem setDlStatus: URLSTS_DWLDERROR ];
            }
        }

Quando si esegue questo pezzo di codice, significa che la funzione curl_easy_perform che ha gestito il trasferimento è terminata in uno stato di errore. La cosa potrebbe essere stata voluta dall'utente, oppure dovuta ad un oggettivo errore nel processo di trasferimento. Qui ho distinto i due casi, ma le operazioni eseguite sono le stesse (la chiamata del metodo failedDownload di DownQueueWinCtrl).

        // negli altri casi, e' andato tutto bene
        else
            // metto a posto le info dell'oggetto
            [ theItem setDlStatus: URLSTS_FINISHED ];

Questo pezzo di codice invece è eseguito quando il trasferimento è andato a buon fine. Mi limito a cambiare lo stato dell'elemento.

        // se ho un file aperto, lo chiudo in ogni caso
        if( fh )
            [ fh closeFile ];
    }        
    // fine dello scaricamento, ed anche del thread
    [ theCaller performSelectorOnMainThread:@selector(endingDownload) withObject: nil waitUntilDone: NO ];
    [ pool release ];
    [ NSThread exit ] ;
}

Le operazioni conclusive sono la chiusura del file (che altrimenti rimarrebbe aperto provocando qualche scompiglio a livello di sistema operativo) e la chiusura del thread.

Gestione delle operazioni

Una parte che mi ha fatto penare un po' è stata la comunicazione tra il thread del trasferimento ed il thread principale della finestra. Intanto, il metodo di DownQueueWinCtrl da cui parte tutto è performCurl. è chiamato senza argomenti ogni volta che la lista dei trasferimenti è modificata in qualche modo per aggiunta di elementi) oppure, all'inizio delle operazioni (all'interno di awakeFromNib)

- (void)
performCurl
{
    Url2DL            * item2dl ;
    unsigned short    ii ;

    // se il download e' fermo, faccio nulla
    if ( downloadStatus == NO )
    {
        [ self saveListStatus ];
        return ;
    }
    // spazzolo tutti i vari volumi interessati
    for ( ii = 0 ; ii < [ theURLlist count]; ii ++ )
    {
        // recupero l'elemento
        item2dl = [ theURLlist objectAtIndex: ii ];
        // e a seconda del suo stato...
        switch ( [ item2dl dlStatus] ) {
        case    URLSTS_FINISHED :            // gia' scaricato
        case    URLSTS_SUSPENDED :            // sospeso
        case    URLSTS_DWLDERROR :            // in errore
            break ;    // procedo col successivo
        case    URLSTS_DOWNLOADING :        // in corso di scaricamento
            currItem = item2dl ;    // me lo segno
            return ;        // ho finito
        case    URLSTS_WAITING :            // in attesa di essere scaricato
        case    URLSTS_PARTIAL :            // in parte scaricato
            currItem = item2dl ;    // me lo segno
            // ripulisco i dati
            [ self resetDownloadData: [ item2dl theName ] ] ;
            // faccio partire curl
            theCurlServer = [[CurlServer alloc] initWithCaller: self] ;
            [NSThread detachNewThreadSelector:@selector(getFileWithCurl:)
                toTarget: theCurlServer withObject: item2dl ];
            // salvo i dati
            [ self saveListStatus ];
            return ;
        }
    }

}

Il metodo in pratica si occupa di rendere valida la variabile d'istanza currItem, in modo da sapere quale sia il file in trasferimento corrente (se uno deve essercene; le operazioni potrebbero essere sospese, come si vede dal test sul flag downloadStatus). Sono esaminati tutti gli elementi nella lista. Se uno è individuato come in corso di trasferimento, la cosa finisce lì. Se si arriva ad un elemento che sta attendendo il trasferimento, si costruisce un nuovo thread a partire dalla classe CurlServer, e si fanno partire le operazioni (descritte sopra).

La comunicazione tra i due thread avviene in pratica attraverso quattro metodi

- (void) advanceMsg: (id) statusArg ;
- (void) startingDownload ;
- (void) endingDownload ;
- (void) failedDownload: (NSString *) errMsg ;

A suo tempo, avevo chiamato un gruppo di metodi di questo tipo un protocollo. Sempre a suo tempo, non mi ero preoccupato troppo della faccenda, visto che tutto sembrava funzionare correttamente. Inoltre, pur avendolo scritto (nel capitolo 63), non ho fatto troppa attenzione a come due thread diversi si comportassero quando tentavano di aggiornare l'interfaccia grafica in concorrenza.

figura 01

figura 01

Ma andiamo per ordine. Illustro l'interfaccia che tiene conto dello stato di avanzamento dei lavori. Ci sono molte informazioni da presentare: il nome del file in corso di trasferimento, o comunque primo nella coda. Una specie di semaforo che indica, col rosso, trasferimento non attivo, col verde, trasferimento in corso, col giallo, trasferimento attivo, ma non ci sono file da trasferire. Durante un trasferimento, poi, sono gestite le seguenti informazioni: il tempo trascorso dalla partenza del trasferimento ed i byte effettivamente trasferiti; se è disponibile poi l'informazione sulla quantità totale di byte da trasferire (anche qui, una informazione che non sempre è disponibile, penso dipenda da come i server gestiscano la comunicazione), questa quantità totale di byte, assieme ad una stima del tempo necessario a trasferire quelli che restano; una stima del tempo totale di trasferimento; una misura, approssimativa fin che si vuole, ma utile, della velocità di trasferimento. Vediamo come ottenere tutte queste informazioni.

Il punto di partenza è la funzione callback sullo stato di avanzamento. Questa è chiamata periodicamente dalla libreria, ed ha a disposizione il numero di byte trasferiti e quelli totali. Con questi due soli valori (ed un po' di aiuto dal sistema operativo) riesco a produrre tutto il resto.

In primo luogo, quando faccio partire il trasferimento, faccio in modo che CurlServer chiami il metodo

- (void) startingDownload
{
    // metto da parte il tempo iniziale
    downLoadStartTime = TickCount( );
    [ graphicStatus setImage: [ NSImage imageNamed:@"stsVerde"]];
}

Qui metto da parte il tempo corrente del sistema (in tick, ovvero sessantesimi di secondo) attravero l'utile (e di Carbon) funzione TickCount. Metto anche l'immagine semaforica a verde, indicando trasferimento in corso.

Il grosso del lavoro è svolto dal metodo advanceMsg, che ha come argomento un array con i due valori di byte totali e byte finora trasferiti

- (void) advanceMsg: (id) statusArg
{
    NSArray    * status = (NSArray*) statusArg ;
    double    sts = [[ status objectAtIndex:0] doubleValue] ;
    double    tot = [[ status objectAtIndex:1] doubleValue] ;
    // tempo trascorso dall'inizio del download, in tick (circa 1/60 di secondo)
    UInt32    deltatime = TickCount() - downLoadStartTime ;    
    // numero di byte finora scaricati
    [ downCurrSize setFloatValue: sts ] ;
    // imposto il tempo trascorso
    [ elaspedTime setFloatValue: (deltatime / 60.0f) ];
    // calcolo la velocita' media
    [ averageSpeed setFloatValue: (sts * 60.0f / deltatime ) ];
    // se ci sono entrambi i dati
    if ( tot && sts)
    {
        // calcolo del tempo residuo
        [ residueTime setFloatValue: ((deltatime / 60.0f)*( tot / sts - 1 )) ];
        // ed il tempo totale
        [ totalTime setFloatValue: ((deltatime / 60.0f)*( tot / sts )) ];
    }
    else
    {
        // manca la dimensione totale, dico che so nulla
        [ residueTime setStringValue: NSLocalizedString(@"resTime Unknown", @"DQWC: advance info, tempo residuo")];
        [ totalTime setStringValue: NSLocalizedString(@"totTime Unknown", @"DQWC: advance info, tempo totale")];
    }
    // se e' nota la dimensione complessiva
    if ( tot )
    {
        // imposto la dimensione complessiva e la barra di avanzamento
        [ downTotSize setFloatValue: tot ] ;
        [ currentStatus setIndeterminate: FALSE ];
        [ currentStatus setDoubleValue: sts/tot ];
        [ downPercent setStringValue: [NSString stringWithFormat: @"%3d%%", (short)(( 100.0*sts / tot) + 0.5F)]];
    }
    else
    {
        // non so che dire
        [ downTotSize setStringValue: NSLocalizedString(@"totsize Unknown", @"DQWC: advance info, numero totale byte") ] ;
        [ currentStatus setIndeterminate: TRUE ];
        [ downPercent setStringValue: @"???" ];
    }
}

Il metodo è piuttosto lungo e noioso, ma lineare nella sua sua struttura. Anche se teoricamente chiamato periodicamente (una volta al secondo) in base a quanto stabilito da libCurl, la prima operazione consiste nel vedere quanto tempo è trascorso dall'inizio del trasferimento. Una volta convertito in secondi, ho già pronta l'informazione sul tempo trascorso. E dividendo il numero di byte trasferiti con il tempo trascorso, ecco che ho una indicazione della velocità media di trasferimento.

Se poi è nota (diversa da zero) la dimensione totale del file da trasferire, si possono anche fare le stime delle altre grandezze. Con una semplice proporzione ( "tempo trascorso" sta a "tempo totale" come "byte trasferiti finora" stanno a "byte totali da trasferire") si ottiene una stima sul tempo totale necessario. Togliendo dalla stima del tempo totale il tempo trascorso, si ha il tempo rimanente.

Con tutto ciò, è possibile anche calcolare la percentuale di avanzamento, ed utilizzarla per animare una simpatica barra di avanzamento.

Quando il trasferimento è terminato, si chiama il metodo seguente, che ripulisce le cose:

- (void) endingDownload
{
    // aspetto un tot per mettere a posto la grafica
    // ripulisco i dati
    [ graphicStatus setImage: [ NSImage imageNamed:@"stsGiallo"]];
    [ self resetDownloadData: NSLocalizedString(@"nothing to download", @"DQWC: advance info, stringa file default") ] ;
    if ( autoCleanQueue )    
    {
        if ( [ currItem dlStatus] == URLSTS_FINISHED )
            [ theURLlist removeObject: currItem ];
        [ theQueue reloadData ] ;
    }
    [ self saveListStatus ];
    // nessun elemento corrente
    currItem = nil ;
    // aggiorno un po' tutto
    [ self performCurl ];    // e riparto con curl
    if ( [ theDrawer state ] == NSDrawerOpenState )
        [ self refreshDrawer ];
}

L'immagine del semaforo è rimessa a gialla, si ripuliscono i campi del trasferimento, e si rilancia performCurl. In questo modo, se ci sono altri elementi in coda, si passa a quello successivo.

Vengo ora a spiegare il problema della grafica. In un primo momento, questi metodi erano chiamati direttamente dalla classe CurlServer. Ad esempio, nella funzione callback ProgressFuncCallback avevo scritto

    [ [ ohmy theCaller] advanceMsg:[ NSArray arrayWithObjects: [ NSNumber numberWithDouble: d], [ NSNumber numberWithDouble: t], nil ] ];

mentre nel codice visto sopra la chiamata è più complicata:

    [ [ ohmy theCaller] performSelectorOnMainThread:@selector(advanceMsg:)
            withObject:[ NSArray arrayWithObjects: [ NSNumber numberWithDouble: d], [ NSNumber numberWithDouble: t], nil ]
            waitUntilDone: NO ];

Qual è la differenza? Nel primo caso, il metodo, benchè della classe DownqueuWinCtrl, è chiamato all'interno del thread associato alla classe CurlServer. Questo significa che ci sono due thread separati che intervengono sull'interfaccia utente, o meglio sulla gestione della grafica e del ridisegno delle finestre. Questo, come già accennato in precedenza, non va bene, in quanto le classi dello Application Kit non sono rientranti (non possono essere chiamate contemporaneamente da thread differenti). Se le cose non sono troppo complicate, non ci sono problemi evidenti. Tuttavia, non appena si comincia ad avere un po' di finestre, interazioni varie, eccetera, cominciano i problemi, non tanto di funzionalità, quando di cattivi ridisegni dell'interfaccia (i thread sono eseguiti in tempi differenti, talvolta prima uno poi l'altro, e visto che entrambi disegnano qualcosa sullo schermo, a seconda di chi arriva prima, si hanno disegni diversi). La soluzione, semplice a dirsi, è di fare in modo che tutte le operazioni che hanno a che fare con lo schermo e con le finestre siano svolte da un unico thread, quello principale. Questo solleva il problema di come modificare l'aspetto dell'applicazione da parte dei thread secondari (che spesso, come nel mio caso, sono loro che svolgono il lavoro interessante). Allo scopo viene in aiuto proprio il metodo performSelectorOnMainThread (nelle varie incarnazioni), il quale aggiunge il metodo indicato dagli argomenti all'interno delle operazioni da effettuare nel main thread. Detto così è piuttosto semplice, ma ho penato parecchio prima di arrivare a questa soluzione.

Tocchi finali (si fa per dire)

A chiudere l'esposizione dell'interfaccia, noto che ad ogni campo (tempo e dimensioni) che presenta informazioni sul trasferimento in corso ho associato un formatter, in modo da lavorare all'interno della classe non numeri puri, e lasciare appunto ai formatter il compito di presentare in bella vista i dati. Non vale nemmeno la pena di presentare il codice, dato che è molto semplice ed in qualche caso un bel riciclo da formatter utilizzati in precedenza.

Infine, c'è da aggiustare l'aspetto della toolbar e dei menu a seconda dello stato corrente del trasferimento e della selezione corrente.

È compito di due metodi, simili nello svolgimento ma diverse nelle prescrizioni, che riporto per la sola toolbar.

Ci sono dei protocolli informali che permettono di selezionare lo stato di attivo o disattivo di un elemento di una toolbar, o di una voce di menu: nel caso della toolbar, abbiamo il metodo validateToolbarItem

- (BOOL)
validateToolbarItem: (NSToolbarItem *) toolbarItem
{
    // il pulsante di stop
if ([[toolbarItem itemIdentifier] isEqual:TBItId_StopAll])
    {
        // riflette lo stato di download
        return ( downloadStatus ) ;
    }
    // in pulsantedi resume
else if ([[toolbarItem itemIdentifier] isEqual:TBItId_Resume])
    {
        // e' il rovescio del precedente
        return ( downloadStatus == NO );
}
    // la cancellazione di un elemento
else if ([[toolbarItem itemIdentifier] isEqual:TBItId_DeleteItem])
    {
        // funziona solo se ci sono elementi selezionati
        return ( ( [ theQueue selectedRow] != -1 ) );
}
    // tutti gli altri sono abilitati
    return ( YES );
}

Il metodo restituisce YES se il pulsante della toolbar deve essere attivo, NO altrimenti. Con questo metodo si dovrebbero intercettare a volo tutte le situazioni che richiedono diverse condigurazioni dei pulsanti, senza dover necessariamente abilitare/disabilitare esplicitamente i pulsanti quando cambiano le condizioni operative.

Come simpatica caratteristica dell'applicazione, ecco un metodo per aprire una pagina web, che ho collegato ad una voce di menu, in modo che dall'intero dell'applicazione si possa accedere al sito di Macocoa:

- (IBAction)goToSite:(id)sender
{
    [ [ NSWorkspace sharedWorkspace] openURL: [ NSURL URLWithString: @"http://www.macocoa.omitech.it"]] ;
}

figura 02

figura 02

A completamento dell'applicazione, ho realizzato una cosa che solo qualche ottimista inguaribile potrebbe definire help (ed in più solo in italiano). Ho scritto qualche pagina in HTML, facendo bene attenzione ad utilizzare quanto Apple consiglia nella realizzazione. La pagina principale dello Help contiene un meta tag del tipo:

    <meta name="AppleTitle" content="DownQueueHelp" />

Con questo, ho stabilito la presenza di un libro (book) di help denominato DownQueueHelp. Devo tenere presente questo identificativo, perché, ad esempio, ogni collegamento interno ai file dello Help sono scritti in questa maniera bizzarra:

<a href="help:anchor='themainpage' bookID=DownQueueHelp">DownQueue</a>>

A parte queste stranezze specifiche del meccanismo di Help della Apple, le pagine sono scritte in un bel codice HTML moderno, ovvero con un foglio di stile in CSS separato dalla pagina html vera e propria. Io ho scritto tutto a mano, ma sospetto che si possa usare facilmente un moderno editor grafico.

Una volta realizzato il contenuto dello Help, bisogna pensare ad associarlo all'applicazione. In primo luogo, occorre compilare l'indice dello help, utilizzando l'apposita applicazione Help Indexer fornita coi Developer Tools.

Poi, ho fatto in modo che ogni finestra dell'applicazione (o quasi) presenti nell'angolo in basso a sinistra un pulsante con punto di domanda. Facendo clic su questo pulsante, si chiama un metodo centralizzato (cioé, su DownQueueWinCtrl) che apre la pagina appropriata dello Help.

- (IBAction)contextHelp:(id)sender
{
    NSString *locBookName = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleHelpBookName"];

    switch ( [sender tag] ) {
    case 20:    // main page
        [[NSHelpManager sharedHelpManager] openHelpAnchor:@"mainwindow" inBook:locBookName];
        break ;
    case 21:    // drawer
        [[NSHelpManager sharedHelpManager] openHelpAnchor:@"thedrawer" inBook:locBookName];
        break ;
    case 30:    // preference
        [[NSHelpManager sharedHelpManager] openHelpAnchor:@"prefwindow" inBook:locBookName];
        break ;
    case 40:    // addfile
        [[NSHelpManager sharedHelpManager] openHelpAnchor:@"addfile" inBook:locBookName];
        break ;
    default :
        break ;
    }
}

Tutto ciò realizza una sorta di aiuto contestuale (per lo meno, relativamente alle finestre) certamente molto amichevole.

Localizzazione

Ho cercato di localizzare l'applicazione nelle due lingue, italiano ed inglese, che potrebbero essere utili agli utilizzatori di questa applicazione, ma soprattutto per rinfrescarmi le operazioni da fare.

In primo luogo, all'interno del codice, ogni volta che utilizzo una stringa suscettibile di localizzazione, non lo faccio direttamente, ma uso un costrutto piuttosto brutto a vedersi ma efficace nella sua realizzazione. Ad esempio, nel costruire un dialogo di avvertimento:

        [alert addButtonWithTitle: NSLocalizedString(@"Overwrite", @"DQWC: alert overwrite, overwrite btn")];        // sovrascirvo
        [alert addButtonWithTitle: NSLocalizedString(@"AutoRename", @"DQWC: alert overwrite, autorename btn")];        // piglio un nome a caso
        [alert addButtonWithTitle: NSLocalizedString(@"Cancel", @"DQWC: alert overwrite, cancel btn")];            // lascio stare
        [alert setMessageText: NSLocalizedString(@"Overwrite the existing file?", @"DQWC: alert overwrite, msgTxt")];
        [alert setInformativeText: NSLocalizedString(@"The content of the previous file is lost.", @"DQWC: alert overwrite, infoTxt")];

La funzione NSLocalizedString serve appunto a caricare da un file apposito la versione nella lingua corrente dell'espressione indicata nel primo argomento. Il secondo argomento è invece una sorta di memento sulla natura della stringa, utilizzato spesso da chi deve tradurre le stringhe nella versione locale (che non sempre è lo sviluppatore, e che quindi non sa bene a cosa serve quella stringa).

L'utile comando (standard Unix, almeno così mi pare) genstring è utilizzabile per estrarre dai file dell'applicazione tutte le occorrenze di NSLocalizedString e di riunirle in un apposito file. Ad esempio il comando

genstrings -o English.lproj *.m

Raccoglie nel file Localizable.strings tutte le stringhe estratte dei file <qualcosa> .m (quindi, il codice dell'applicazione, se tutti i file .m dell'applicazione si trovano nella directory in cui eseguo il comando). Questo file è costruito in questo modo:

...
/* DQWC: alert overwrite, autorename btn */
"AutoRename" = "AutoRename";
...
/* DQWC: alert overwrite, overwrite btn */
"Overwrite" = "Overwrite";
...

dove appunto sono riportate (in ordine alfabetico) gli identificatori delle stringhe da tradurre; il secondo argomento di NSLocalizedString è messo come commento nella riga precedente.

Adesso, il traduttore deve semplicemente trasformare la parte a destra del segno di uguale nella stringa da visualizzare nella lingua desiderata. Ad esempio, in italiano ho una cosa del tipo:

...
/* DQWC: alert overwrite, autorename btn */
"AutoRename" = "Auto-Rinomina";
...
/* DQWC: alert overwrite, overwrite btn */
"Overwrite" = "Sovrascrivi";
...

Per quanto riguarda invece finestre e quant'altro, anche qui sono disponibili una serie di strumenti. Il punto di partenza è la versione iniziale (ad esempio, in inglese) del file nib. Nella mia applicazione, ce ne sono tre (la principale, l'aggiunta di un file, le preferenze). Di tutte, costruisco la versione localizzata in italiano attraverso le informazioni del nib, all'interno di XCode. Così facendo, sono costruiti tre file nib all'interno della cartella Italian.lproj, del tutto uguali alla versione inglese. Adesso, bisogna tradurre tutte le stringhe (ed eventualmente spostare, aggiustare campi, eccetera).

È disponibile un simpatico comando da terminale, nibtool, che può essere utilizzato per semplificare il lavoro. Scrivendo (all'intero della cartella Italian.lproj)

nibtool -L ../English.lproj/MainMenu.nib >./MainMenu.strings
nibtool -L ../English.lproj/SetDLWindow.nib >./SetDLWindow.strings
nibtool -L ../English.lproj/PrefWin.nib >./PrefWin.strings

sono generati tre file che contengono le stringhe presenti all'interno del file nib. Ad esempio, il contenuto assomiglia alle righe seguenti:

...
/* NSMenu : <title:action> (oid:341) */
"Action" = "Action";

/* NSMenuItem : <title:add from clipboard> (oid:223) */
"Add from Clipboard" = "Add from Clipboard";

/* NSMenuItem : <title:add...> (oid:82) */
"Add..." = "Add...";
...

Con un editor di testi (XCode stesso va benissimo) si possono modificare le stringhe alla destra del segno di uguale, traducendole nella lingua desiderata:

...
/* NSMenu : <title:action> (oid:341) */
"Action" = "Azioni";

/* NSMenuItem : <title:add from clipboard> (oid:223) */
"Add from Clipboard" = "Aggiungi dagli Appunti";

/* NSMenuItem : <title:add...> (oid:82) */
"Add..." = "Aggiungi...";
...

A questo punto, si ritorna al comando nibtool per effettuare l'operazione inversa

nibtool -d MainMenu.strings ../English.lproj/MainMenu.nib -W MainMenu.nib
nibtool -d SetDLWindow.strings ../English.lproj/SetDLWindow.nib -W SetDLWindow.nib
nibtool -d PrefWin.strings ../English.lproj/PrefWin.nib -W PrefWin.nib

Il comando unisce il file English.lproj/MainMenu.nib sostituendo le stringhe che trova in MainMenu.strings e costruisce un nuovo file MainMenu.nib. Magari qualcosa bisogna aggiustare ancora (perché l'italiano è una lingua che occupa più spazio dell'inglese, occorre verificare tutti i campi di testo, che riescano a contenere la stringa), ma l'operazione è piuttosto semplice ed efficace per avere una versione in lingua dell'interfaccia.

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