MaCocoa 075

Capitolo 075 - DownQueue

Ricomincio a programmare riprendendo cURL.

Sorgenti: documentazione Apple, ripesco vecchi capitoli, di tutto un po'.

Prima stesura: 29 dicembre 2006.

C'era una coda

Nel capitolo 65 avevo realizzato un wrapper Cocoa per il comando Unix cURL, adatto al trasferimento di file tramite protocollo http e ftp. Ripendo in mano l'idea e provo a fare una nuova applicazione, semplice ed inutile, ma come al solito educativo per il programmatore.

I requisiti

L'idea è di trasferire una lista di file, non tanto in parallelo (come ad esempio avviene con Safari o con altri programmi che funzionano da acceleratori del trasferimento; un giretto su VersionTracker o siti similari ve ne fornirà qualcuno migliore di quello che andrò a produrre), quanto in serie, uno dopo l'altro. La cosa nasce da una mia esigenza personale, che consiste nel mettere da parte una lista di file (di dimensioni generose), che poi sono trasferiti con comodo durante la notte o comunque mentre non sono presente davanti al computer (tranquilli, nulla di illegale in tutto ciò).

L'ambizione è di poter costruire un'applicazione completa, e fruibile da altre persone che non siano me stesso, non tanto per ricavarci gloria e denaro, quanto per prefissarmi la presenza di localizzazioni, help, e varie altre piccolezze che fanno di un pezzo di codice una applicazione a tutti gli effetti.

L'interfaccia

In base a questi requisiti, incomincio a rendere le cose un po' più specifiche, andando a disegnare un'interfaccia utente. Comincio da quella in inglese, ricordandomi che poi dovrò farne anche una versione in italiano.

figura 01

figura 01

Ci sono un'ampia zona sulla sinistra (un'immagine che sarà anche l'icona dell'applicazione) sulla quale si possono droppare dei collegamenti per inserirli nella lista dei trasferimenti. A completare la fascia superiore le informazioni relative al file in corso di trasferimento, con nome, stato di avanzamento, ed anche tutte le informazioni che si potranno ricavare in termini di tempo trascorso e residuo, e byte trasferiti. Una immagine rossa, gialla o verde rende conto dello stato corrente del trasferimento: rosso indica nessun trasferimento in corso, giallo indica trasferimenti attivi ma coda vuota, ed infine verde indica trasferimento in corso.

La parte inferiore della finestra contiene invece la lista dei file da trasferire e un pulsante di Aiuto. Ho aggiunto un classico controllore specifico per la finestra (DownQueueWinCtrl) e outlet a iosa per collegare tutti gli elementi dell'interfaccia.

figura 02

figura 02

figura 03

figura 03

Per visualizzare informazioni sui singoli file, ho pensato di utilizzare un Drawer, anche perché non ne ho mai utilizzato uno e voglio vedere come si fa. Per aggiungere un drawer ad una finestra, non ho nemmeno letto una riga di documentazione. Ho utilizzato Interface Builder, aggiungendo un oggetto NSDrawer direttamente all'interno del MainMenu.nib. Quest'operazione non ha però introdotto una zona dove disegnarne il contenuto; bisogna quindi aggiungere anche una NSView. L'ho fatto droppando sopra l'icona dell'oggetto myDrawer una CustomView prelevata dalla pelette degli oggetti di Interface Builder. L'operazione, contestualmente, imposta l'outlet contentView di myDrawer alla view appena inserita, e mostra tale view in una finestra a parte di Interface Builder (che non corrisponde però ad una finestra dell'applicazione). Bisogna anche collegare l'outlet parentWindow di myDrawer alla finestra madre (nel mio caso, l'unica finestra operativa dell'applicazione) e, nel mio caso, il delegate per lasciare fare al controllore della finestra alcune operazioni. In effetti, il collegamento tra finestra e drawer si effettua dall'altro lato non tanto dalla finestra, quanto attraverso il controllore della finestra (il mio DownQueueWinCtrl). Anche tutti i controlli del drawer sono stati collegati tramite outlet a DownQueueWinCtrl, come pure questo controllore è il riferimento per le operazioni scatenate dai controlli del drawer. Così facendo la classe controller diventa piuttosto piena di outlet, e forse valeva la pena separare o semplificare la gestione in altro modo. Tuttavia, data la semplicità dell'applicazione, lascio tutto così.

Oltre agli outlet, all'interno della classe controllore inserisco anche una variabile d'istanza NSMutableArray, destinata a contenere l'elenco di tutti gli URL da trasferire (e le informazioni accessorie). In effetti, per gli URL da trasferire ho costruito una classe apposita, Url2DL, che contenga appunto le informazioni relative allo URL, la destinazione del file trasferito, e di atre informazioni sullo stato corrente del trasferimento. In pratica quindi, una prima bozza dell'applicazione consiste nella visualizzazione di una lista di oggetti Url2DL all'interno di una finestra.

Detta così, la cosa si presenta piuttosto facile. In realtà, le cose da fare sono parecchie, e comincio a metterle in un qualche ordine.

Drop sull'immagine

Comincio dal realizzare le funzioni di drop sull'immagine.

Costruisco una nuova classe, la DropImage, che è una sottoclasse di NSImageView, dove aggiungo appunto la gestione del drop. Ho aggiunto una variabile d'istanza, e l'ho classificata come IBOutlet. Fatto questo, entro in interface builder, ed attribuisco all'immagine che voglio rendere sensibile al drop proprio NSImageView come Custom Class. Effettuo anche il collegamento verso il controllore della finestra come outlet. In questo modo l'immagine saprà indirizzare al posto giusto gli URL che gli vengono droppati sopra.

figura 04

figura 04

Per quanto riguarda i metodi che gestiscono l'operazione di droppatura, non faccio altro che ricopiare ed adattare i metodi che avevo già scritto nel capitolo 19. Riassumendo e riscrivendo, i metodi da considerare sono sei.

Si comincia con draggingEntered, quando qualcosa trascinato al mouse entra nella zona sensibile (nel mio caso, sopra l'immagine). Il metodo serve a stabilire cosa che ne farà il destinatario degli oggetti trascinati.

Ho introdotto una specie di giochino grafico sull'immagine, che ne modifica leggermente la rappresentazione. Normalmente infatti l'immagine presenta una cornice in rilievo NSImageFrameGrayBezel; quando ci entra qualcosa, la cornice diventa NSImageFramePhoto. L'effetto è che l'immagine sembra gonfiarsi un poco quando ci si passa sopra con qualcosa in trascinamento. Il metodo dice che degli oggetti trascinati sopra ci farà nulla nella maggior parte dei casi; solo quando gli oggetti sono di tipo NSURLPboardType (in pratica, degli URL ben formati), ne farà qualcosa (un collegamento, NSDragOperationLink, un collegamento, che in pratica può significare molte cose...).

Altri due metodi sono facili da fare. Il primo, draggingUpdated, è attivo finchè si rimane sopra l'immagine, e l'altro, draggingExited, è chiamato quando si esce dall'immagine senza aver rilasciato il mouse (in pratica, facendo nulla). Ovviamente, il primo metodo non fa che ridabire l'operazione che sarà svolta, mentre il secondo rimette a posto lo stile della cornice dell'immagine.

Il primo metodo serio è prepareForDragOperation, che prepara le operazioni conseguenti al drop, accettando il compito nel caso di URL.

Il metodo che fa in pratica tutto il lavoro, performDragOperation, si limita ad invocare un metodo del controllore della finestra (ecco dove viene utile lo outlet) indicandogli quale URL aggiungere alla lista.

Infine, a conclusione delle operazioni, si rimette a posto l'aspetto dell'immagine con metodo concludeDragOperation.

Fino a qui, tutto bene e tutto chiaro (almeno per me). Ho già un primo problema. Quando trascino un URL sopra l'immagine, il punto di partenza dell'operazione di trasferimento è perfettamente chiaro. Completamente ignoto, invece, il punto di arrivo del trasferimento; in altre parole, non ho idea di dove andare a salvare il file risultato dell'operazione di trasferimento. Del resto, l'operazione di drop è piuttosto naturale, ma non permette in modo naturale la specifica della destinazione. Mi giovo di alcuni valori di default e del fatto che molto spesso ci si aspetta che il file trasferito abbia lo stesso nome del file di partenza.

Aggiungere un file

L'azione scatenata da un drop riuscito sull'immagine richiama il metodo seguente (della classe DownQueuWinCtrl):

- (void) addClipFile2Download: (NSPasteboard *) pstBoard
{
    NSArray        * pList ;
    NSEnumerator     * enumerator ;
    NSString        * anUrl ;
    // e ne estraggo solo gli elementi url
    pList = [ pstBoard propertyListForType: NSURLPboardType ] ;
    enumerator = [ pList objectEnumerator ];
    // gli spazzolo tutti
    while ( anUrl = [enumerator nextObject] )
    {
        NSString    * tmpURL = [[ NSURL URLWithString: anUrl ] absoluteString ];
        // se ho una stringa, e questa non e' vuota
        if ( tmpURL && [ tmpURL length] )
        {
            // costruisco un nuovo elemento
            Url2DL    * theNewElem = [[ [ Url2DL alloc] initWithUrl: tmpURL andDLFolder: defaultDLDir ] autorelease ];
            // e lo aggiungo
            [ self addFile2Download: theNewElem ];
        }
    }
}

Ci sono le solite operazioni che recuperano le informazioni dalla pasteboard; anche se non riesco a capire come un utente normale possa droppare una lista di url, gestisco la pasteboard come se fosse in grado di restituire un array di URL. Per ogni URL ben formato che trovo, costruisco un elemento della classe Url2DL (ci arrivo in un attimo), e poi lo aggiungo alla lista degli elementi, attraverso il metodo seguente:

- (void )
addFile2Download: (Url2DL *) theNewElem
{
    if ( [ self shouldAdd2Queue: theNewElem ])
    {
        [ theURLlist addObject: theNewElem ];
        [ theQueue reloadData ] ;
        [ self performCurl ];
    }
}

Mi sono infatti accorto che potrebbero esserci problemi di varia natura quando gestisco un nuovo URL da trasferire. Ho così condizionato l'effettivo inserimento alla verifica di un metodo shouldAdd2Queue che effetta qualche controllo aggiuntivo sulla natura del file (più che altro, sulla sua possibile destinazione):

- (BOOL) shouldAdd2Queue: (Url2DL *) theNewElem
{
    NSString * finalFile ;
    // se la sovrascittura e' attiva, va tutto bene
    if ( checkOverwrite == NO )
        return ( YES ) ;
    // se arrivo qui, devo controllare eventuali sovrascritture
    // nome completo del file destinazione
    finalFile = [[ theNewElem theDestFolder] stringByAppendingPathComponent: [ theNewElem theName] ];
    // vedi se esiste gia' un file con questo nome
    if ([ [ NSFileManager defaultManager] fileExistsAtPath: finalFile ] )
    {
        // si, chiedo cosa fare
        NSAlert *alert = [[NSAlert alloc] init];        // tre possibilita'
        [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")];
        [alert setAlertStyle:NSWarningAlertStyle];
        [ theNewElem retain ];
        [ alert beginSheetModalForWindow: [ self window] modalDelegate: self
                didEndSelector: @selector(checkOverWrite:returnCode:contextInfo:) contextInfo: (void*) theNewElem ];
        return ( NO );
    }
    return ( YES );
}

figura 05

figura 05

Tra le preferenze dell'applicazione, ci sarà anche la possibilità di ignorare eventuali sovrascritture di un file (il flag checkOverwrite). è tuttavia meglio controllare se per caso il trasferimento del file indicato non vada a sovrascrivere un file già presente. Costruisco quindi il path completo della destinazione, e controllo se non esista già un file con lo stesso nome. Nel caso, preparo un messaggio di avvertimento che lascia all'utente la scelta definitiva di sovrascrivere o meno il file.

In definitiva, il metodo visto restituisce YES se si può procedere tranquillamente (nessuna sovrascrittura, oppure chissenefrega se c'è una sovrascrittura); risponde NO se l'utente decide fare altro.

Le opzioni che si possono scegliere sono racchiuse all'interno del metodo di chiusura del dialogo:

- (void)
checkOverWrite:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
    Url2DL    * theNewElem = (Url2DL *) contextInfo ;
    // overwrite, faccio nulla
    if ( returnCode == NSAlertFirstButtonReturn)
    {
        // overwrite, faccio nulla
        [ theURLlist addObject: theNewElem ];
        [ theQueue reloadData ] ;
        [ self performCurl ];
    }
    if (returnCode == NSAlertSecondButtonReturn)
    {
        // autorename
        short        i = 0 ;
        // piglio l'estensione e la metto da parte
        NSString    * finalFile ;
        NSString    * newName ;
        NSString    * extension = [ [ theNewElem theName] pathExtension];
        // il nuovo nome e' quello vecchio con aggiunta in fondo
        // contnuo a cambiare l'estensione numerica...
        do
        {
            // proviamo con una estensione numerica progressiva
            NSString    * numext = [ NSString stringWithFormat: @"-%d", ++ i ];
            // nuovo nome del file
            NSString    * newshname = [ [ [ theNewElem theName] stringByDeletingPathExtension] stringByAppendingString: numext] ;
            newName = [ newshname stringByAppendingPathExtension: extension];
            finalFile = [[ theNewElem theDestFolder] stringByAppendingPathComponent: newName ];
        }
        // ... finche' ci sono file con lo stesso nome
        while ([ [ NSFileManager defaultManager] fileExistsAtPath: finalFile ] ) ;
        
        // imposto il nuovo nome
        [ theNewElem setTheName: newName ];
        [ theURLlist addObject: theNewElem ];
        [ theQueue reloadData ] ;
        [ self performCurl ];
    }
    // negli altri casi, faccio nulla
    [ theNewElem release ];
    [alert release];
}

Se l'utente decide di sovrascrivere comunque, si inserisce l'elemento nella coda. La seconda opzione è di lasciare all'applicazione il compito di rinominare il file. Ciò avviene aggiungendo un numero progressivo tra il nome e l'estensione del file, in modo da avere nomi molto simili, eppure diversi (si tratta della stessa strategia realizzata da Safari, ad esempio; tuttavia, a differenza di Safari, qui ho pasticciato le cose, per cui se si aggiungono nella coda file con lo stesso nome, non esistendo già tali file nel file system dell'utente, saranno comunque sovrascritti...; presumo che ci saranno modifiche in questa parte del codice, ma non ora).

Infine, se l'utente decide di soprassedere, nessun file è aggiunto alla coda, e tutto rimane come prima.

Elementi in coda

Le informazioni relative ad un singolo elemento nella coda sono racchiuse all'interno di una classe:

@interface Url2DL : NSObject {
    NSString            * theURL ;
    NSString            * theDestFolder ;
    NSString            * theName ;
    unsigned short        dlStatus ;
}

Le variabili d'istanza sono l'URL completo da dove avviene il trasferimento, mentre la specifica della destinazione è divisa nel nome e nel percorso sul disco fisso. Infine, una variabile intera discrimina lo stato corrente del trasferimento: terminato, in trasferimento, in attesa, sospeso, parziale ed in errore. Questa variabile sarà responsabile della modalità di visualizzazione di queste informazioni nella coda.

I metodi associati alla classe sono i soliti metodi standard di gestione (inizializzazione, accessor, eccetera). Segnalo solo i due metodi per il salvataggio su file ed il recupero dei dati da file, ovvero encodeWithCoder e initWithcoder, metodi che permettono il salvataggio di una coda su di un file per poter effettuare i trasferimenti in un secondo momento o su altro computer. Anche il metodo description è degno di interesse (ne parlavo il capitolo passato a proprosito del debugging): riporta in forma amena le informazioni contenuta in una istanza di Url2DL.

Non voglio inoltre tediare il lettore con la riproposizione dei soliti metodi per la sorgente dati della tabella che presenta la lista (per comodità, ho usato DownQueueWinCtrl stesso come sorgente dei dati). Mi limito a segnalare il seguente metodo, che colora in maniera diversa la riga a seconda dello stato del trasferimento:

- (void)
tableView:(NSTableView *)aTableView
    willDisplayCell:(id)aCell
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    switch ( [[theURLlist objectAtIndex: rowIndex] dlStatus]) {
    case    URLSTS_FINISHED :        // gia' scaricato
        [ aCell setTextColor: [ NSColor darkGrayColor]];
        break ;
    case    URLSTS_PARTIAL :
        [ aCell setTextColor: [ NSColor orangeColor]];
        break ;    
    case    URLSTS_SUSPENDED :        // sospeso
        [ aCell setTextColor: [ NSColor lightGrayColor]];
        break ;
    case    URLSTS_DOWNLOADING :        // in corso di scaricamento
        [ aCell setTextColor: [ NSColor brownColor]];
        break ;
    case    URLSTS_WAITING :        // in attesa di essere scaricato
        [ aCell setTextColor: [ NSColor blackColor]];
        break ;
    case    URLSTS_DWLDERROR :        // in attesa di essere scaricato
        [ aCell setTextColor: [ NSColor redColor]];
        break ;
    }
}

figura 06

figura 06

Una caratteristica interessante e nuova nella gestione della tabella è la possibilità di riordinare gli elementi presenti nella lista. Ci sono due momenti da tenere presenti: configurare la NSTableView in modo che possa gestire le operazioni di drag and drop all'interno di se stessa, e poi riordinare la sorgente dati in modo da riflettere i cambiamenti effettuati.

Di default, la NSTableView non è soggetta al drag and drop interno. Occorre specificarlo esplicitamente, ad esempio nel metodo awakeFromNib della finestra che contiene la tabella:

#define        localDragType        @"localDragType"
[ theQueue registerForDraggedTypes: [NSArray arrayWithObject: localDragType] ];

A questo punto, la NSTableView riceve tutti i messaggi tipici del drag and drop. In particolare, come si trova nelle guide di riferimento sull'argomento fornite da Apple, ci sono alcuni metodi da mettere a posto. Il primo è utilizzato per mettere da parte le informazioni alla partenza delle operazioni di drag. Il metodo fornisce come argomento l'elenco degli indici delle righe interessate (nel nostro caso, avendo scelto che la selezione della tabella può essere sempre e sola una riga, sarà un solo valore) e la pasteboard dove salvare i dati. Per semplicità, i dati salvati consistono in pratica nel solo indice della riga selezionata.

- (BOOL)
tableView:(NSTableView *)tv
        writeRowsWithIndexes:(NSIndexSet *)rowIndexes
        toPasteboard:(NSPasteboard*)pboard
{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject: rowIndexes ];
[pboard declareTypes:[NSArray arrayWithObject:localDragType] owner:self];
[pboard setData: data forType:localDragType];
return YES;
}

Durante le operazioni di drag and drop, occorre assicurare il sistema che le cose stanno procedendo bene: è compito del metodo seguente.

- (NSDragOperation)
tableView:(NSTableView*)tv
        validateDrop:(id <NSDraggingInfo>)info
        proposedRow:(int)row
        proposedDropOperation:(NSTableViewDropOperation)op
{
return NSDragOperationEvery;
}

Vista la semplicità della cosa, non sono effettuate operazioni, ma solamente si conferma il proseguimento delle operazioni.

La parte conclusiva delle operazioni è svolta dal metodo successivo.

- (BOOL)
tableView:(NSTableView *)aTableView
        acceptDrop:(id <NSDraggingInfo>)info
        row:(int)row
        dropOperation:(NSTableViewDropOperation)operation
{
NSPasteboard* pboard = [info draggingPasteboard];
NSData* rowData = [pboard dataForType:localDragType];
NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
int dragRow = [rowIndexes firstIndex];

    Url2DL    * elem = [ theURLlist objectAtIndex: dragRow];
    [ elem retain ];    // il successivo remove elimina l'oggetto !
    [ theURLlist removeObjectAtIndex: dragRow] ;
    // aggiusto il conteggio delel righe, ora che ho eliminato un elemento
    if ( row > 0 )
        row -= 1 ;
    [ theURLlist insertObject: elem atIndex: row ];
    [ elem release ];    // ripristino il giusto retainCount
    [ theQueue reloadData ] ;
    return ( TRUE );
}

Si comincia recuperando le informazioni sulla sorgente delle operazioni (quindi, il numero della riga dragRow che è stata inizialmente trascinata). Come argomento del metodo, è fornito l'indice della riga row sulla quale è avvenuto il drop. Si tratta adesso di riordinare la sorgente dati theURLlist in modo da riflettere la modifica effettuata. Per farlo, estraggo l'elemento e lo cancello dalla lista. Devo effettuare una operazione di retain, altrimenti andrebbe perduto per sempre, essendo la lista l'unico punto che trattiene l'elemento in questione. Adesso inserisco l'oggetto nella nuova posizione (cui ho sottratto uno per tenere conto del fatto che ho appena tolto un elemento...). La classica richiesta di ridisegnare la lista chiude il metodo. Metodo che restituisce TRUE per indicare che tutto è andato bene.

Toolbar ed altre azioni

Una toolbar semplifica l'interazione con l'applicazione in maniera drastica, per cui ho inserito molti comandi nel suo interno.

Piuttosto che discutere a lunga sulla sua realizzazione, che non presenta grosse novità, illustro più in dettaglio le varie operazioni che si possono effettuare attraverso i pulsanti della toolbar e le voci di menu corrispondenti.

L'aggiunta di un elemento alla lista dei trasferimenti si può al momento effettuare solo tramite drag and drop sulla immagine presente sull'interfaccia. Ecco altre tecniche.

Si può aggiungere un elemento alla lista attraverso specifica diretta dell'indirizzo. Allo scopo, apro una finestrella ausiliaria.

- (IBAction)addFile2Queue:(id)sender
{
    // costruisco lo sheet e lo apro
    SetDLFileWinCtrl *tmpCVCtrl = [[ SetDLFileWinCtrl alloc] init ] ;
    [ tmpCVCtrl setTheCaller: self ] ;
[NSApp beginSheet: [ tmpCVCtrl window ]
modalForWindow: [ NSApp mainWindow ]
modalDelegate: nil
            didEndSelector: nil
contextInfo: nil];    
}

figura 07

figura 07

La finestra (o meglio, il suo controllore) appartiene alla classe SetDLFileWinCtrl, che funziona come una sheet. Questo significa che la finestra si apre sopra la finestra principale, intercettando tutti gli eventi relativi alla finestra (e, dal momento che la mia applicazione ha una sola finestra, dell'intera applicazione). Da notare come alla finestra impongo come variabile d'istanza un riferimento alla classe chiamante. In questo modo, dall'interno dei metodi di SetDLFileWinCtrl posso richiamare i metodi propri di DownqueueWinCtrl, ad esempio per aggiungere un file alla lista dei trasferimenti.

La finestra modale presenta semplicemente un campo per immettere l'indirizzo completo del file da trasferire ed un pulsante per selezionare la directory locale di destinazione. Due pulsanti, uno per acconsentire al trasferimento e l'altro per annullarlo, completano il tutto.

Il primo pulsante scatena l'esecuzone del seguente metodo:

- (IBAction)downloadButton:(id)sender
{
    // assegno un nuovo url da scaricare
    Url2DL    * theNewElem ;
    // butto via la finestra
    [[self window] setIsVisible: FALSE ];
    // ho finito di lavorare modale con la sheet
    // il codice di ritorno e' la riga selezionata
    [NSApp endSheet: [self window] returnCode: 0 ];
    // predispongo i valori dell'oggetto
    theNewElem = [[ [ Url2DL alloc] initWithUrl: [ theURL stringValue] andDLFolder: [ theDestFolder stringValue] ] autorelease ];
    // lo aggiungo alla lista
    [ theCaller addFile2Download: theNewElem ];
}

Semplicemente, si costruisce un elemento della classe Url2DL con i valori ottenuti dall'utente, e lo si aggiunge alla lista.

Più articolata la selezione della directory di destinazione: in prima battuta interviene il metodo seguente, che mostra il tipico pannello di selezione directory

- (IBAction)setDLDestination:(id)sender
{
    // per scegliere uan directory, paradossalmente si usa il pannello
    // per scegliere un file... opportunamente configurato
    NSOpenPanel    *sPanel = [ NSOpenPanel openPanel ] ;
    // preparo il pannello di salvataggio file
    [ sPanel setTitle: NSLocalizedString(@"addFile: Saving into folder", @"SDLFWC:, save file panel, titolo") ];
    [ sPanel setPrompt: NSLocalizedString(@"addFile: Select the destination folder", @"SDLFWC:, save file panel, prompt") ];
    [ sPanel setCanChooseDirectories: YES ];
    [ sPanel setCanChooseFiles: NO ];
    [ sPanel beginSheetForDirectory:nil file:nil types: nil modalForWindow: [ self window]
        modalDelegate: self didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:nil ];
}

Questo metodo, tuttavia, passa la palla alla gestione standard, e lascia la gestione del valore di ritorno (la directory scelta) al metodo delegato:

- (void)openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
    if ( returnCode == NSOKButton )
    {
        // se e' andato bene, prelevo il nome del file
        NSString *aFile = [ panel filename ] ;
        // e lo assegno alla variabile interna
        [ theDestFolder setStringValue: aFile ] ;
    }
}

Questo metodo, se è stato premuto il pulsante di scelta, recupera il percorso della directory e l'assegna al campo della finestra (da dove il metodo downloadButton la preleverà quando necessario).

Un giochino interessante avviene all'apertura della finestra, all'interno del metodo windowDidLoad

- (void)
windowDidLoad
{
    // se in clipboard c'e' un indirizzo, uso quello
    NSPasteboard    * pstBoard = [NSPasteboard generalPasteboard];
    NSArray            * pList = [ pstBoard propertyListForType: NSURLPboardType ] ;
    if ( [ pList count] )
    {
        NSString    * tmpURL = [[ NSURL URLWithString: [ pList objectAtIndex: 0 ] ] absoluteString ];
        [ theURL setStringValue: tmpURL ];
    }
    // altrimenti, un url di default
    else
    {
        [ theURL setStringValue: @"http://macocoa.altervista.org/download/MaCocoa.pdf" ] ;
    }    
    // metto la directory di default
    [ theDestFolder setStringValue: [ theCaller defaultDLDir] ];
}

Se all'interno degli appunti si trova un indirizzo URL ben formato, ebbene, questo indirizzo è utilizzato come punto di partenza per la scelta dell'utente. Altrimenti, è utilizzato un indirizzo di default (un po' banale). La directory di destinazione iniziale è ricavata ancora una volta attraverso la classe chiamante; sarà un valore di default nelle preferenze dell'utente.

In effetti, questa cosa di recuperare dagli appunti un indirizzo ben formato mi piace così tanto che ho aggiunto un metodo apposta, in modo da associare l'azione ad un pulsante della toolbar e ad una voce di menu:

- (IBAction)addFileFromClip:(id)sender
{
    // recupero il testo dalla clipboard
    NSPasteboard    * pstBoard = [NSPasteboard generalPasteboard];
    // e ne estraggo solo gli elementi url
    [ self addClipFile2Download: pstBoard];
}

Oltretutto, la cosa è piuttosto semplice. Dopo aver individuato la pasteboard corretta, è sufficiente invocare lo stesso metodo utilizzato per il drag and drop sul'immagine (che utilizza sempre concetti legati alle pasteboard) per effettuare l'operazione.

Per eliminare un elemento dalla coda, la cosa è piuttosto semplice. Basta selezionare l'elemento nella coda, ed utilizzare l'apposito pulsante della toolbar o voce di menu.

- (IBAction)deleteItem:(id)sender
{
    // recupero l'elemento
    Url2DL        * theElem = [theURLlist objectAtIndex: [ theQueue selectedRow] ] ;
    // se e' in corso di scaricamento, faccio nulla
    if ( [ theElem dlStatus] == URLSTS_DOWNLOADING )
        return ;
    // elimino l'elemento selezionato
    [theURLlist removeObjectAtIndex: [ theQueue selectedRow] ] ;
    // riaggiorno un po' tutto
    [ theQueue reloadData ] ;
    if ( [ theDrawer state ] == NSDrawerOpenState )
        [ self refreshDrawer ];
    [ self saveListStatus ];
}

Il drawer

La presenza nel metodo precedente di una istruzione che riguarda il drawer mi consente di introdurre il concetto. Ho già descritto come lo abbia definito in Interface Builder, aggiungendolo al file nib direttamente dalla palette, ed associandoci una NSView isolata (non contenuta all'interno di una finestra). A quanto pare, è tutto ciò che è necessario fare per la sua gestione.

figura 08

figura 08

Un drawser ha uno stato, che può essere visibile o invisibile, ed una sua disposizione preferita (alto, basso, destra, sinistra), ovvero il lato da cui uscirà quando se ne richiede la visualizzazione. Si tratta di una direzione preferita, in quanto il lato di uscita dipende anche dalla geometria della finestra a cui fa riferimento. Ho scelto la direzione "basso", ma in realtà il drawser uscirà dal lato inferiore solo se c'è abbastanza spazio per visualizzarlo (almeno in parte), altrimenti uscirà dal lato superiore. Uguale sorte se avessi scelto sinistra o destra, esce sempre dal lato dove trova spazio.

Ho stabilito un paio di modi con cui visualizzarlo. Direttamente da voce di menu/pulsante della toolbar, oppure facendo un doppio clic su di un elemento della lista. Nel primo caso, il metodo è direttamente collegato come action al pulsante/menu; nel secondo, occorre esplicitamente indicare la cosa, ad esempio all'interno del metodo awakeFromNib della finestra

    [ theQueue setDoubleAction: @selector(showInfo:) ];

In entrambi i casi, il metodo showInfo funziona allo scopo:

- (IBAction)showInfo:(id)sender
{
    if ( [ theDrawer state ] == NSDrawerClosedState )
    {
        [ self refreshDrawer ];
        [ theDrawer open ] ;
    }
    else if ( [ theDrawer state ] == NSDrawerOpenState )
        [ theDrawer close ] ;
}

La gestione è bistabile, ovvero, il comando alterna tra lo stato visibile e lo stato invisibile. In entrambi i casi, il drawer compie una amorevole animazione nell'uscire o nel rientrare nel lato preferito.

Il drawer contiene le informazioni associate ad un Url2DL, quindi URL, nome e cartella destinazione. il metodo refreshDrawer serve proprio ad aggiustare i valori a seconda della selezione corrente in lista.

- (void)
refreshDrawer
{
    // se non c'e' alcuna riga selezionata
    if ( [ theQueue selectedRow] == -1 )
    {
        // metto tutto in condizioni nulle
        [ drwURL setEnabled: NO ] ;
        [ drwURL setStringValue: NSLocalizedString(@"DrwDefaultURL", @"DQWC: drw defautl url") ] ;
        [ drwDLFolder setStringValue: NSLocalizedString(@"DrwDownloadFolder", @"DQWC: drw download folder") ] ;
        [ drwSkipThisFile setEnabled: NO ] ;
        [ drwShowFinder setEnabled: NO ] ;
        [ drwChangeFolder setEnabled: NO ] ;
        [ drwFileSts setStringValue: NSLocalizedString(@"DrwFileStatus", @"DQWC: file sts, nessuno") ];
        [ drwFileSts setEnabled: NO ] ;
    }
    else
    {
        // recupero l'elemento selezionato
        Url2DL    * theElem = [theURLlist objectAtIndex: [ theQueue selectedRow] ] ;
        // imposto le informazioni
        [ drwURL setEnabled: YES ] ;
        [ drwURL setStringValue: [ theElem theURL ] ] ;
        [ drwDLFolder setStringValue: [ theElem theDestFolder ] ] ;

        switch ( [ theElem dlStatus] ) {
        // gestione dei vari casi ...
        // ...
        case    URLSTS_DOWNLOADING :        // in corso di scaricamento
            [ drwURL setEnabled: NO ] ;
            [ drwSkipThisFile setEnabled: NO ] ;
            [ drwChangeFolder setEnabled: NO ] ;
            [ drwShowFinder setEnabled: NO ] ;
            [ drwSkipThisFile setState: NO ] ;
            [ drwFileSts setStringValue: NSLocalizedString(@"Status: Downloading", @"DQWC: file sts, downloading") ];
            break ;
        case    URLSTS_WAITING :        // in attesa di essere scaricato
            [ drwURL setEnabled: YES ] ;
            [ drwChangeFolder setEnabled: YES ] ;
            [ drwSkipThisFile setEnabled: YES ] ;
            [ drwShowFinder setEnabled: NO ] ;
            [ drwSkipThisFile setState: NO ] ;
            [ drwFileSts setStringValue: NSLocalizedString(@"Status: Waiting", @"DQWC: file sts, in attesa") ];
            break ;
        // gestione degli altri casi ...
        }
    }
}

Il metodo è piuttosto prolisso. Nel caso non ci siano selezioni attive, provvede a disabilitare tutti i controlli e a inserire stringhe neutre al posto di nome ed url. Con un elemento selezionato, si recuperano le informazioni, mentre i controlli presenti sono gestiti a seconda dello stato del trasferimento. Ad esempio, se il trasferimento è in corso, non è possibile cambiare URL e nome, anzi, non è possibile fare alcuna modifica. Se invece l'elemento è in attesa di essere scaricato, è possibile modificare ogni singolo attributo (url, destinazione, eccetera) ed anche manipolarne lo stato (ad esempio, escludendolo dal trasferimento, ma mantenendolo nella lista).

All'interno del drawer sono infatti presenti alcuni pulsanti che permettono di modificare alcune caratteristiche dell'elemento. Per modificare lo URL, si opera direttamente sulla stringa che lo rappresenta. Perché l'applicazione riconosca le modifiche in questo campo di testo, bisogna che ne sia informata. Una tecnica classica è di attribuire un delegato al campo di testo (l'ho fatto in Interface Builder, ed ho assegnato sempre la classe DownQueueWinCtrl), e di intercettare alcune notifiche, o meglio, la notifica controlTextDidChange. In questo modo, ogni volta che il testo all'interno del campo è modificato dall'utente, l'ambiente operativo informa della cosa il delegato, che può agire di conseguenza:

- (void) controlTextDidChange:(NSNotification *) aNotification
{
    NSTextField *quale = [ aNotification object ];
    // verifico che sia il mio campo
    if ( [quale tag] == 11 )
    {
        // recupero l'elemento
        Url2DL    * theElem = [theURLlist objectAtIndex: [ theQueue selectedRow] ] ;
        // e gli cambio l'url
        [ theElem setTheDestFolder: [ drwURL stringValue] ];
        // salvo i dati
        [ self saveListStatus ];
    }
}

Nel caso, nulla di speciale: controllo che sia proprio il campo di testo ad essere stato modificato (verifica un po' paranoica, ma tant'è), e modifico la variabile d'istanza dell'elemento Url2DL corrente con il nuovo valore

Per modificare la cartella di destinazione, c'è un pulsante del tutto simile a quello visto nella finestra di immissione di un nuovo URL (e che svolge in pratica le medesime operazioni). C'è poi un pulsante di spunta, che permette di escludere il file dal trasferimento (in pratica, modifica la variabile d'istanza dlStatus di Url2DL da uno stato di attesa ad uno di sospensione, e viceversa). Grazioso poi il pulsante che permette, per i file già scaricati, di individuarli nel Finder (nel caso non si riesca a trovarli, o per raggiungerli rapidamente in una selva di finestra aperte).

- (IBAction) drwShowInFinder:(id)sender
{
    // recupero l'elemento
    Url2DL        * theElem = [theURLlist objectAtIndex: [ theQueue selectedRow] ] ;
    // il nome completo del file
    NSString    * finalFile = [[ theElem theDestFolder] stringByAppendingPathComponent: [ theElem theName] ];
    // ed uso l'apposito metodo di NSWorkspace
    [ [NSWorkspace sharedWorkspace] selectFile: finalFile inFileViewerRootedAtPath: [ theElem theDestFolder] ];
}

Una volta determinato il path completo, giungendo percorso e nome del file, si utilizza un metodo della classe NSWorkspace per aprire una nuova finestra del Finder, quella appunto che contiene il file trasferito.

Supporto all'applicazione

Qui e lì nel codice compare un'istruzione che invoca il metodo saveListStatus. L'idea è di mantenere da qualche parte l'elenco dei file in corso di trasferimento, e di ripristinare la lista tra una chiusura dell'applicazione e la successiva riapertura; una specie di salvataggio della sessione in corso, insomma. Il posto deputato dove scrivere queste cose è la cartella Application Support dell'utente corrente (la si trova all'interno della cartella Libreria. In effetti, ci sono diverse cartelle Application Support in giro per il sistema, ma a me interessa solo quella specifica dell'utente). Molte applicazioni professionali in effetti scrivono informazioni per il proprio funzionamento in quella cartella, piuttosto che utilizzare altri posti in giro per il sistema.

Mescolo il tutto con le caratteristiche legate alle preferenze, e descrivo il metodo init del controllore:

- (id )
init
{
    NSArray            * paths ;
    NSMutableArray    * tmpArray ;
    NSString        * destPath ;
    NSUserDefaults    * defaults = [NSUserDefaults standardUserDefaults];
    
    self = [ super init ];
    // inizializzo un po' di variabili
    currItem = nil ;
    downLoadStartTime = TickCount() ;
    theCurlServer = nil ;
    [ self setTheURLlist: [NSMutableArray array]] ;
    // queste dovrei pigliarle dalle preferenze
    destPath = [ defaults stringForKey: UD_DefaultDLFolder ] ;
    if ( destPath )
        [ self setDefaultDLDir: destPath ] ;
    else
        [ self setDefaultDLDir: @"/Users/Shared" ] ;
    checkOverwrite = [ defaults boolForKey: UD_CheckOverwrite ] ;
    autoCleanQueue = [ defaults boolForKey: UD_AutoCleanQueue ] ;
    startAtStartup = [ defaults boolForKey: UD_StartAtStartup ] ;
    downloadStatus = startAtStartup ;

Fino a qui, tutto piuttosto tranquillo, Ci sono un po' di inizializzazioni sparse, la maggior parte delle quali prelevate dalle preferenze utilizzando la classe NSUserDefaults (vedi poi).

    
    // guardo se ce' gia' la directory "Application Support"
    paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
    if ([paths count] == 0)
    {
        // non c'e' neppure la cartella "Application support"...
        NSAlert *alert = [[NSAlert alloc] init];
        [alert addButtonWithTitle: NSLocalizedString(@"AppSuppFld1 Ok", @"DQWinCtrl: alert OK manca cartella app supp")];
        [alert setMessageText: NSLocalizedString(@"AppSuppFld1 AppError", @"DQWinCtrl: msgtxt manca cartella app supp")];
        [alert setInformativeText:
                    NSLocalizedString(    @"Can't find the application Support folder. Quitting DownQueue",
                                        @"DQWinCtrl: infoTxt manca cartella app supp") ];
        [alert setAlertStyle:NSCriticalAlertStyle];
        [ alert beginSheetModalForWindow: [ self window] modalDelegate: self
                    didEndSelector: @selector(stopMe:returnCode:contextInfo:) contextInfo: nil ];
        return ( self );
    }

Comincia la saga della cartella Application Support. In primo luogo si controlla se esiste: a meno di una installazione pura del sistema operativo, dovrebbe essere già presente. Non lo fosse, segnalo la cosa con un bel dialogo di avvertimento, e poi chiudo l'applicazione (non provo neppure a costruire la cartella, insomma).

    // la mia cartella all'interno di Application Support
    destPath = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"DownQueue"];
    // il nome del file nella mia cartella
    [ self setDefaulfFileList: [ destPath stringByAppendingPathComponent:@"initQueue.plist"] ];
    // vedo se esiste il file
    if ([ [ NSFileManager defaultManager] fileExistsAtPath: defaulfFileList ] )
    {
        // il file esiste, lo leggo
        tmpArray = [NSKeyedUnarchiver unarchiveObjectWithFile: defaulfFileList ];
        // ci sono perfino degli elementi
        if ( tmpArray )
            // ci inizializzo la lista
            [ self setTheURLlist: tmpArray ];                
    }

Questo dovrebbe essere il pezzo di codice standard di ogni esecuzione. Costruisco il path di un file, che si chiama initQueue.plist, all'interno della cartella DownQueue interna alla cartella Application Support. Con questo path, vado a leggerne il contenuto, che è una proprety list con la quale inizializzo la lista dei trasferimenti theURLList.

Va tuttavia tenuto presente che la prima volta che si lancia l'applicazione, la cartella e tanto meno il file sono presenti.

    else
    {
        // il file non esiste
        BOOL    isdir = YES;
        // vedo se esiste almeno la cartella (prima esecuzione)
        if ([ [ NSFileManager defaultManager] fileExistsAtPath: destPath isDirectory: & isdir] == NO || isdir == NO)
        {
            // non c'e' la cartella, la creo
            if ( [ [ NSFileManager defaultManager] createDirectoryAtPath: destPath attributes:nil] == NO )
            {
                // non riesco a creare la cartella...
                NSAlert *alert = [[NSAlert alloc] init];
                    [alert addButtonWithTitle: NSLocalizedString(@"AppSuppFld2 Ok", @"DQWinCtrl: alert ok manca cartella appsupp/DQ")];
                [alert setMessageText: NSLocalizedString(@"AppSuppFld2 AppError", @"DQWinCtrl: msgTxt manca cartella appsupp/DQ")];
                [alert setInformativeText: NSLocalizedString(@"Cannot create Application Support folder. Quitting DownQueue",
                                                @"DQWinCtrl: infoTxt manca cartella appsupp/DQ") ];
                [alert setAlertStyle:NSCriticalAlertStyle];
                [alert runModal] ;    
                [ alert beginSheetModalForWindow: [ self window] modalDelegate: self
                            didEndSelector: @selector(stopMe:returnCode:contextInfo:) contextInfo: nil ];
                return ( self );
            }
        }
        // a questo punto provo a creare il file
        if ( [ [ NSFileManager defaultManager] createFileAtPath: defaulfFileList contents:nil attributes:nil ] == NO )
        {
            // non riesco a creare il file
            NSAlert *alert = [[NSAlert alloc] init];
            [alert addButtonWithTitle: NSLocalizedString(@"SuppFileCrt Ok", @"DQWinCtrl: alert ok fallita creazione file lista")];
            [alert setMessageText: NSLocalizedString(@"SuppFileCrt AppError", @"DQWinCtrl: msgTxt fallita creazione file lista")];
            [alert setInformativeText: NSLocalizedString(@"Cannot create Queue file list. Quitting Downqueue",
                                                        @"DQWinCtrl: infoTxt fallita creazione file lista") ];
            [alert setAlertStyle:NSCriticalAlertStyle];
            [ alert beginSheetModalForWindow: [ self window] modalDelegate: self
                        didEndSelector: @selector(stopMe:returnCode:contextInfo:) contextInfo: nil ];
            return ( nil );
        }
}

Se il file non esiste, vado in primo luogo a verificare se per caso non esiste neppure la cartella, sintomo di prima esecuzione dell'applicazione (o di un utente che cancella cartelle più o meno a caso). Se necessario, la creo nuova. In tutte queste operazioni, mi giovo dei metodi della classe NSFileManager, che appunto fornisce comode funzioni adatte alla bisogna. Concludo quindi con la creazione del file (vuoto) nella posizione corretta.

Se queste operazioni per qualche motivo non riescono (il file system deve essere in tal caso in condizioni ben disgraziate), l'applicazione mostra un dialogo informativo e termina l'esecuzione dell'applicazione.

    // ripulisco la lista dei file presenti
    [ self adjustURLList ];
    return ( self );
}

Le ultime operazioni svolte consistono nell'aggiustare la lista dei trasferimenti, in accordo con le preferenze.

- (void) adjustURLList
{
    NSEnumerator     * enumerator ;
    Url2DL            * theNewElem ;
    // spazzolo i dati
    enumerator = [ theURLlist objectEnumerator ];
    while ( theNewElem = [enumerator nextObject] )
    {
        // se sono in downloading, li cambio in waiting
        if ( [ theNewElem dlStatus] == URLSTS_DOWNLOADING )
            [ theNewElem setDlStatus: URLSTS_WAITING ];
        if ( autoCleanQueue && ( [ theNewElem dlStatus] == URLSTS_FINISHED ) )
            [ theURLlist removeObject: theNewElem ];
    }
}

Qui sono esaminati tutti gli elementi in lista, e sono messi nello stato di attesa trasferimento. Se ci sono poi elementi il cui trasferimento è terminato, e l'utente ha scelto di mantenere pulita la lista, allora questi elementi sono eliminati.

A questo punto si può ritornare al metodo saveListStatus, da cui sono partito. Ogni volta che la lista dei trasferimenti subisce modifiche (perché è stato aggiunto o eliminato un elemento, o perché un elemento ha cambiato il proprio stato), conviene salvare il tutto nel file, invocando appunto il metodo citato:

- ( void) saveListStatus
{
    if ( [NSKeyedArchiver archiveRootObject: theURLlist toFile: defaulfFileList ] == NO )
        NSLog(@"Errore archiviazione 1");    
}

I metodi di NSKeyedArchiver producono dei file che sono delle property list. Sono file binari, che però possono essere facilmente consultati con l'applicazione Property List Editor, disponibile una volta installati i Developer's Tool (e quindi, visto l'argomento di questo sito, dovreste già avere installato). Non che si capisca moltissimo, ma intanto...

A questo punto, viene naturale la possibilità di salvare la lista dei trasferimenti. Facile associare i metodi per la scrittura e la lettura alle voci di menu classiche Open e Save.

- (IBAction)listSave:(id)sender
{
    NSSavePanel    *sPanel = [ NSSavePanel savePanel ] ;
    // personalizzo il pannello di salvataggio
    [ sPanel setTitle: NSLocalizedString(@"Save list", @"DQWC: list save panel, titolo") ];
    // i file salvati avranno l'estensione dql
    [ sPanel setRequiredFileType: @"dql" ];
    [ sPanel beginSheetForDirectory:nil file:nil modalForWindow: [self window] modalDelegate: self
            didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo: nil ] ;    
}

- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
{
    if ( returnCode == NSOKButton )
    {
        // recupero il nome del file
        NSString *aFile = [ sheet filename ] ;
        // archivio l'intera struttura dati
        [NSKeyedArchiver archiveRootObject: theURLlist toFile: aFile ];
    }
    
}

Il salvataggio avviene in due passaggi. Nel primo si invoca il dialogo tipico, nella modalità modale (scusate il gioco di parole); nel secondo, si procede alla scrittura su file usando la stessa primitiva di NSKeyedArchiver.

Per quanto riguarda la lettura, si procede anche qui in due passaggi:

- (IBAction)listLoad:(id)sender
{
    NSOpenPanel        *oPanel = [ NSOpenPanel openPanel ] ;
    // imposto un po' di caratteristiche del dialogo
    [ oPanel setTitle: NSLocalizedString(@"LoadList Title", @"DQWC: list load panel, titolo") ];
    [ oPanel setPrompt: NSLocalizedString(@"LoadList Prompt", @"DQWC: list load panel, prompt") ];
    // niente directory, un solo file alla volta
    [ oPanel setCanChooseDirectories:NO ];
    [ oPanel setCanChooseFiles: YES ];
    [ oPanel setAllowsMultipleSelection:NO ];
    [ oPanel setResolvesAliases:YES ];
    [ oPanel beginSheetForDirectory:nil file:nil types: [ NSArray arrayWithObject: @"dql" ] modalForWindow: [ self window]
        modalDelegate: self didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:nil ];
}

- (void)openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
    if ( returnCode == NSOKButton )
    {
        // ... recupero il nome del file selezionato
        NSArray *filesToOpen = [ panel filenames ];
        NSString *aFile = [ filesToOpen objectAtIndex: 0 ];
        // ed uso il template per costruire la view
        // estraggo i dati dal file
        [ self setTheURLlist: [NSKeyedUnarchiver unarchiveObjectWithFile: aFile ]];    
        [ self adjustURLList ];
        [ self performCurl ];
        [ theQueue reloadData ] ;    
    }
}

La logica utilizzata è esattamente la stessa (preparazione del dialogo, esecuzione delle operazioni sottese); da notare che caricare un nuovo file significa buttare via la lista corrente (la variabile theURLlist è brutalmente assegnata con i nuovi valori), e non ho neppure inserito un dialogo che informi l'utente della cosa.

Chiusura dell'applicazione

Prima di arrivare al cuore dell'applicazione (ovvero, come effettuare il trasferimento), aggiungo qualche parola sulla presenza di un dialogo delle preferenze, in cui appunto l'utente può stabilire alcune caratteristiche dell'applicazione. Ci sono pulsanti di spunta ed altri controlli per stabilire il percorso di default dove salvare i file trasferiti, il comportamento ell'applicazione nei confronti delle (possibili) sovrascritture di file, se la coda dei trasferimento debba essere tenuta pulita (eliminazione automatica dei trasferimenti completati) o meno, se il trasferimento parte automaticamente al lancio dell'applicazione o meno. tutti questi valori sono caricati dalle preferenze all'apertura dell'applicazione, replicati nel dialogo delle preferenze per poterli modificare, riscritti poi nelle preferenze alla chiusura dell'applicazione:

- (void)windowWillClose:(NSNotification *)theNotification
{
    NSUserDefaults    * defaults = [NSUserDefaults standardUserDefaults];
    [ defaults setObject: defaultDLDir forKey: UD_DefaultDLFolder ] ;
    [ defaults setBool: checkOverwrite forKey: UD_CheckOverwrite ] ;
    [ defaults setBool: autoCleanQueue forKey: UD_AutoCleanQueue ] ;
    [ defaults setBool: startAtStartup forKey: UD_StartAtStartup ] ;
    [ defaults synchronize];
    [NSApp terminate:self];
}

Questo metodo è chiamato quando la finestra dell'applicazione viene chiusa, vuoi per l'uscita dall'applicazione, vuoi perché l'utente la chiude.

Da notare come con questo metodo forzo comunque la chiusura dell'applicazione nel secondo caso.

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