MaCocoa 018

Capitolo 018 - Menu e Palette

In questo capitolo aggiungo due voci di menu e una palette di comandi all'applicazione finora costruita. Il menu l'ho già introdotto il capitolo precedente, con due voci all'interno di un menu specifico; intendo con questo sostituire i pulsanti che, ancora un capitolo precedente, utilizzavo per aggiungere elementi al catalogo di file. I pulsanti, usciti di scena con i menu, rientrano invece all'interno di una palette che voglio provare ad inserire in questo ambiente multi-documento.

Primo inserimento: 4 Febbraio 2002

figura 01

figura 01

Sorgenti: Tentativi e molti errori.

Interfaccia

figura 02

figura 02

Come al solito, la prima cosa da fare è di entrare in IB e di costruire l'interfaccia utente. Se le due voci di menu Add... e Delete erano già state definite in precedenza, manca totalmente la palette. Come è giusto, questa si trova in un file nib separato, che chiamo ls1pal.nib (ovvero, la prima palette di livio sandel...). è una cosa molto semplice, prevedo quattro bottoni, uno per aggiungere elementi al catalogo, uno per eliminarli, uno per aprire cataloghi (equivalente alla voce di menu Open...) ed uno per salvare il contenuto dei cataloghi (equivale a Save).

Visto che ci sono, scopro come sia molto facile aggiungere immagini e suoni ai pulsanti.

Per prima cosa, occorre importare le immagini che si intendono usare all'interno del progetto XCode. Io ho recuperato dei disegni non so più da dove in formato pict.

Questi disegni sono poi disponibili anche in IB una volta che si seleziona il pannello Images. Per aggiungere una immagine ad un pulsante si tratta semplicemente di fare drag'n'ndrop dalla finestra al pulsante. Scopro poi altri giochini interessanti che si possono fare con i pulsanti. Se ad esempio utilizzate una seconda immagine, assegnandola al campo alt.icon, si possono scambiare tra loro le immagini all'attivazione del pulsante, semplicemente selezionando la voce opportuna nel menu Behaviour della palette di info relativa al pulsante (funziona anche per pulsanti di solo testo, scambia tra loro il Title e alt.Title).

Per aggiungere un suono, stesso meccanismo. Tuttavia, trovo piuttosto noioso il suono sul pulsante (alla lunga, lavorandoci un po', mi infastidisce...).

Ovviamente, posso provare subito al momento l'effetto che fa la palette dei pulsanti da dentro IB, selezionando il menu Test Interface.

Collegamenti

figura 03

figura 03

Adesso comincio la parte interessante, ovvero i collegamenti. Una cosa che era passata inosservata nel capitolo precedente, erano i collegamenti predefiniti del menu principale. Apro il file MainMenu.nib dentro IB, ed esamino le singole voci di menu. La maggior parte di queste è già associata ad una qualche operazione. Ad esempio, la voce"New ha già realizzato, nel paradigma target/action, il collegamento tra la selezione del menu e l'azione NewDocument. Sì, ma a chi è diretto il messaggio? Al First Responder, l'oggetto misterioso di questo paragrafo.

La piglio da lontano: una applicazione, una volta che ha effettuato tutte le inizializzazioni necessarie, entra nell'infinito loop degli eventi. L'oggetto NSApplication è il gestore principale dell'applicazione: esegue un passo del loop, estrae, se presente, un evento, e lo distribuisce a chi di dovere. La distribuzione dell'evento dipende anche dal tipo di evento stesso. Ad esempio, un clic del mouse è diretto all'oggetto visuale immediatamente sotto il mouse. Le cose si complicano se l'evento è un carattere da tastiera. Chi è che riceve l'evento? Diciamo la finestra davanti a tutte le altre (cosa per altro non sempre vera...), ma, all'interno della finestra, quale dei vari possibili elementi?

Ebbene, l'elemento all'interno di una finestra che riceve per primo tutti gli eventi non dedicati, è il First Responder (risponditore designato).

Se il risponditore designato è in grado di trattare l'evento, lo gestisce, consumandolo. Se non è in grado di farlo, passa l'evento al risponditore successivo nella catena dei risponditori. Tipicamente, lo passa al suo superiore gerarchico. Alla fine, i gestori degli eventi generici sono la finestra, il documento e l'applicazione. Ad esempio, nel caso dei menu, la maggior parte dei comandi del menu File va a riferirsi al documento (apri, chiudi, salva, stampa), e quindi alla classe controllore del documento, CatalogDoc (l'unica, in questo momento).

Mi viene quindi naturale associare ai due nuovi menu Add... e Delete una accoppiata target/action, inserendo l'azione come uno dei metodi della classe CatalogDoc.

figura 04

figura 04

figura 05

figura 05

Vado quindi nel file nib del menu principale, seleziono la classe First Responder nella finestra principale, ed aggiungo le azioni addFileItem: e delItem: nell'elenco. Poi passo ai nuovi menu e, con la classica azione control+drag, collego i menu all'istanza di First Responder.

Torno a questo punto in XCode, e a mano aggiungo ii metodi corrispondenti nei file della classe CatalogDoc.

- (void) addFileItem: (id)sender ;
- (void) delItem: (id)sender ;

Per la scrittura effettiva dei metodi, il primo non presenta preblemi, dal momento che ricorda molto da vicino un metodo già scritto negli esempi precedenti (anzi, praticamente non cambia):

- (void) addFileItem: (id)sender
{
    int risposta;
    NSOpenPanel *oPanel = [ NSOpenPanel openPanel ];

    [ oPanel setFrame: NSMakeRect(0, 0, 500, 200) display: NO ];
    [ oPanel setTitle:@"Aggiungi File" ];
    [ oPanel setPrompt:@"Seleziona un file qualsiasi" ];
    [ oPanel setCanChooseDirectories:YES ];
    [ oPanel setCanChooseFiles:YES ];
    [ oPanel setAllowsMultipleSelection:NO ];
    [ oPanel setResolvesAliases:NO ];

    risposta = [oPanel runModalForTypes: nil];

    if (risposta == NSOKButton) {
        NSArray *filesToOpen = [ oPanel filenames ];
        NSString *aFile = [ filesToOpen objectAtIndex: 0 ];
        FileStruct * fInfo = [[ FileStruct alloc ] initTreeFromPath: aFile ];
        [ dataSource addFileEntry: fInfo ];
        [ outlineView reloadData ];
    }
}

Ricorsione

Arrivo adesso alla novità del metodo di eliminazione di un elemento dalla finestra del catalogo. Dopo aver inserito con leggerezza il menu nell'interfaccia, mi sono pentito, dal momento che mi sembrava un compito improbo. Poi, riflettendoci, diventa molto più semplice. Il problema è che ho un elemento all'interno di una gerarchia, e non ho alcun punto di riferimento... bisogna esplorare tutta le gerarchia alla sua ricerca. Comincio così:

- (void) delItem: (id)sender
{
    FileStruct * fInfo ;
    int    rowsel = [ outlineView selectedRow ];
    if ( rowsel == -1 ) return ;
    // se arrivo qui, c'e' una riga selezionata
    fInfo = [ outlineView itemAtRow: rowsel ] ;
    // adesso voglio eliminare questo elemento da dataSource
    // e' un vero pasticcio: devo cercare la directory di cui
    // e' figlio, cosa non facile...
    // ed allora, considero ricorsivamente gli start points e vado
    DeleteItemFromHierarchy( [dataSource startPoint], fInfo );    
    [ outlineView reloadData ];
}

All'inizio, discrimino il fatto se effettivamente c'è una riga selezionata all'interno del documento. Se non c'è, il metodo selectedRow: della classe NSOutlineView restituisce (-1), e quindi il metodo non ha nulla da fare. Una volta scoperta che c'è una riga selezionata, ricavo l'elemento selezionato. Noto che l'elemento è l'oggetto vero e proprio, e non ha una sua rappresentazione. Non basta adesso eliminare l'oggetto, devo anche eliminare il collegamento a questo oggetto, in altre parole, devo cercare la directory in cui l'elemento (il file) è contenuto. Mi appoggio ad una funzione che scrivo appositamente, e che sarà chiamata ricorsivamente (e se qui non avete mai visto una funzione ricorsiva, fate un bel respiro, prendetevi un po' di tempo, e preparatevi ad un ragionamento non facile).

La struttura dati è costituita da una serie di NSMutableArray, in cui ogni elemento del vettore è potenzialmente un altro NSMutableArray. La mia ricerca comincia dal livello più alto, quindi dal vettore dei punti di partenza della sorgente dati.

La funzione DeleteItemFromHierarchy riceve come parametri il vettore e l'elemento da eliminare. Resituisce il valore 1 non appena trova ed elimina l'oggetto, 0 in caso contrario. A questo punto, esamino il codice:

int
DeleteItemFromHierarchy ( NSMutableArray * hier , FileStruct * fItem )
{
    int ni ;
    unsigned int    i;

    // se lo item e' nell'array, bene
    if ([ hier containsObject: fItem ])
    {
        [ hier removeObject: fItem ] ;
        return ( 1 );
    }
    // non c'e', lo cerco in tutti i suoi figli
    ni = [ hier count ] ;
    for ( i = 0 ; i < ni ; i ++ )
    {
        FileStruct * locRoot = [ hier objectAtIndex: i ];
        if (DeleteItemFromHierarchy ( [locRoot fileList] , fItem ) == 1 )
            return ( 1 );
    }
    // se arrivo qui, non ho ancora trovato il file
    return ( 0 );
}

Se sono fortunato, l'oggetto fItem si trova subito all'interno del vettore. Uso il metodo containsObject: proprio della classe NSMutableArray per verificare questo fatto. Se così è, elimino l'elemento utilizzando ancora una volta un metodo di NSMutableArray, ovvero removeObject:. L'effetto di questo metodo è molto di più pesante di quanto appare a prima vista. Infatti il metodo di suo invia un release all'oggetto. Così facendo, l'oggetto non ha più nessuno che si riferisca a lui, e quindi riceve anche un messaggio di dealloc. Però io avevo sovrascrittto il metodo dealloc, facendo in modo che non venisse eliminato solo l'oggetto, ma anche il vettore che, nel caso il file fosse una directory, contiene tutti i file contenuti nella directory. Per tanto, l'eliminazione di un oggetto provoca automaticamente l'eliminazione di tutta la sotto-gerarchia di file che da questo oggetto si dipana.

Vado avanti. Sono sfortunato, e l'elemento cercato non è direttamente all'interno del vettore di partenza. Mi tocca quindi esaminare tutte le sotto-gerarchie di ciascun elemento del vettore. Ecco quindi un ciclo for che itera su tutti gli elementi (che ho opportunamente contato). Estraggo dall'elemento i-esimo del vettore considerato il vettore con tutti i suoi file, ed invoco nuovamente la funzione DeleteItemFromHierarchy scendendo di un livello nella gerarchia.

Il motivo per cui la funzione restituisce 0 in caso di fallimento e 1 in caso di riuscita della cancellazione è per fermare la ricerca al momento del reperimento dell'oggetto. È infatti inutile cercare l'oggetto nel resto della gerarchia ancora da esplorare nel momento in cui l'ho trovato ed eliminato.

Palette

Giunti qui con tutto funzionante, non mi resta che collegare i pulsanti della palette. Con l'idea del risponditore designato, la cosa è semplicissima. Collego con la tecnica del control+drag il pulsante Add al metodo addFileItem:, Del a delItem:; poi, sfrutto i metodi esistenti openDocument: e saveDocument: per collegarei pulsanti Load e Save. Gli ultimi due metodi sono molto semplicemente quelli che IB collega di default alle voci di menu Open... e Save.

C'è da gestire l'apparizione e la chiusura della palette stessa. Infatti, la palette non è creata automaticamente al lancio dell'applicazione, in quanto si trova in file nib differente da quello principale (seguo il principio ogni finestra il suo nib). Devo quindi aggiungere il controllore della finestra, sotto forma della classe palette.

Ora, all'interno dell'applicazione esiste sempre una ed una sola palette; utilizzo allora una tecnica differente dal solito. Dico che esiste un metodo per la classe (attenzione: per la classe, non per ogni istanza) palette che permette di recuperare l'oggetto palette attivo

+ (id) sharedPalette
{
    static palette * _sharedPalette = nil ;

    if ( ! _sharedPalette )
    {
        _sharedPalette = [ [palette alloc] init ];
    }
    
    return ( _sharedPalette );
}

In primo luogo, c'è da notare il metodo, che comincia con '+' invece che con il classico '-'. Con questo, si indica che il metodo è della classe piuttosto che di ogni istanza. Poi c'è una variabile definita static. Questa variabile è unica per la classe, ed è inizializzata a nil (cioé, nulla). La prima volta che si invoca il metodo, _sharedPalette vale nil, e il metodo alloca ed inizializza la palette. Tutte le altre volte, restituisce l'oggetto palette costruito la prima volta. In questo modo, sono sicuro di costruire una sola volta la palette.

Il metodo init è molto semplice:

- (id) init ;
{
    self = [ self initWithWindowNibName: @"ls1pal"];
    return ( self );
}

Uso il metodo initWithWindowNibName: per caricare la palette dal file nib che la contiene.

figura 06

figura 06

Non mi rimane adesso che aggiungere un meccanismo per visualizzare la palette. Torno in IB ed aggiungo una voce di menu Show Palette nel menu Window. A questa voce ci collego il metodo showPalette: che mi sono premunito di definire all'interno di First Responder. Poi, all'interno della classe CatalogDoc, aggiungo in XCode il metodo corrispondente:

- (void) showPalette: (id)sender
{
    [[palette sharedPalette] showWindow: sender] ;
}

E qui c'è il problema sul quale mi sono arenato. Il meccanismo funziona la prima volta che seleziono il menu: la finestra appare nel suo splendore. Se però la chiudo, e provo a riaprirla, non succede alcunché. E non capisco. Magari, bisogna leggere un po' di documentazione.

Infatti (qualche ora più tardi). Ho dimenticato una cosa stupidissima. Il collegamento tra il File's Owner del file nib che contiene la palette e la finestra della palette stessa. Detto per esteso: quando ho costruito la finestra della palette, data la semplicità della classe controllante (palette è appunto una sotto-classe di NSWindowController), mi sono dimenticato della cosa più semplice, ovvero, quale sia la finestra che la classe controlla. In pratica, si tratta di importare in IB il file palette.h in modo da rendere noto il tipo di File's Owner, e poi di collegare tramite lo outlet predefinito window di File's Owner la finestra che ho definito.

Facile, banale e vitale.

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