MaCocoa 028

Capitolo 028 - Barra degli Strumenti

In questo capitolo intendo inserire una toolbar alla finestra di catalogo, duplicando (quasi) tutte le funzioni presenti nella palette dei comandi. La cosa si rivela molto semplice, vuoi per sé, vuoi perché un esempio di Apple fornisce praticamente tutto ciò che mi serve.

Sorgenti: Un esempio di Apple (/Developer/Examples/AppKit/SimpleToolbar).

Primo inserimento: 16 settembre 2002

Toolbar

Anche al più casuale utente di Mac OS X non può sfuggire la toolbar (Barra degli Strumenti) presente nelle finestre del Finder.

figura 01

figura 01

Questa toolbar raccoglie alcuni comandi di uso comune, ed ha l'interessante caratteristica di essere personalizzabile dall'utente, aggiungendo comandi propri col drag'n'drop oppure tramite un apposito dialogo.

Pensavo che l'aggiunta di una tale caratteristica alle mie finestre di catalogo fosse un'impresa improba; invece, si è rilevata molto semplice. Non solo: a puro titolo gratuito, Cocoa fornisce anche un menu ed un dialogo di personalizzazione della toolbar stessa. Anche se il lavoro mi è stato facilitato da un esempio di Apple a corredo dei Developer's Tools, la faccenda è veramente semplice.

Una toolbar normalmente contiene semplici elementi cliccabili che funzionano come dei pulsanti. L'elemento classico di una toolbar è quindi definito da una icona (image), un nome corto per mostrarlo nella toolbar (label), un nome più lungo per mostrarlo nella finestra di personalizzazione (paletteLabel), una descrizione del suo funzionamento mostrata come testo di aiuto (tooltip), l'operazione scatenata da un clic (target e action). Normalmente, una immagine (tiff o pict) è quanto basta per un elemento della toolbar, ma è possibile definire elementi più complicati, come menu pop-up, immagini speciali, eccetera (in cui tuttavia non mi avventuro).

Prevedo che la mia toolbar avrà elementi per svolgere le seguenti funzioni: aggiungere un volume al catalogo; cancellare un elemento dal catalogo; salvare il catalogo su disco; stampare la finestra del catalogo.

Il delegato alla toolbar

Per la realizzazione della toolbar, il concetto base di tutto è ancora una volta la delega. Occorre individuare una classe delegata per la toolbar, che si preoccupi di svolgere i compiti di coordinamento e controllo della toolbar stessa. Poiché la toolbar spesso contiene comandi relativi ad un documento, la classe che gestisce il documento è la naturale candidata al meccanismo di delega.

Dire che la classe CatalogDoc è la delegata della toolbar significa che bisogna realizzare un gruppo di metodi ben precisi: li si trova nella documentazione della classe NSToolbar, che appunto descrive come si realizza una toolbar. Nella documentazione esiste anche un Programming Topic che inquadra meglio l'intero problema.

Benché una toolbar sia un elemento visuale, di interfaccia utente, non si può costruire una toolbar tramite IB, ma bisogna farlo da programma, direttamente in XCode.

Il primo metodo (obbligatorio) realizzato dalla classe delegata fornisce infatti l'elenco degli elementi di default presenti all'interno della toolbar. Questa lista di elementi è utilizzata alla prima creazione della toolbar e per presentare la toolbar di default all'interno del dialogo di personalizzazione.

Un secondo metodo, pur'esso obbligatorio, deve invece fornire la lista di tutti gli elementi che in qualche maniera possono comparire all'interno della toolbar. Questa lista è utilizzata dal dialogo di personalizzazione per consentire la scelta degli elementi da parte dell'utente.

Il terzo ed ultimo metodo obbligatorio, finalmente, deve fornire l'elemento della toolbar.

Per indicare gli elementi di una toolbar, si utilizzano degli identificatori; la toolbar stessa ha un identificatore, che permette di utilizzare la stessa toolbar in posti differenti, mantenendo ovunque lo stesso aspetto.

Alcuni identificatori di toolbar sono predefiniti: il separatore, lo spazio vuoto di dimensione fissa; lo spazio vuoto di dimensione variabile; elementi per la scelta di caratteri e colori; l'elemento per la stampa; l'elemento per la configurazione della toolbar. Tutti gli altri elementi devono essere definiti dal programmatore.

Io prevedo tre elementi aggiuntivi:

#define        TBItId_AddItem        @"tbItem_AddItem"
#define        TBItId_DelItem        @"tbItem_DelItem"
#define        TBItId_SaveItem        @"tbItem_SaveItem"

l'elemento per aggiungere un volume al catalogo, un elemento per cancellare una voce di catalogo, l'elemento per salvare il documento corrente.

Detto questo, mi viene facile scrivere i primi due metodi delegati.

// questo metodo dice quali sono gli item di default della toolbar
    // sono gli item presenti alla creazione e quando l'utente ripristina
- (NSArray *)
toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
{
    // la lista e' l'elenco ordinato degli identificatori degli elementi
    return [NSArray arrayWithObjects:    
        TBItId_AddItem,             // aggiunta elemento
        TBItId_SaveItem,             // salvataggio catalogo
        NSToolbarSeparatorItemIdentifier,     // separatore
        TBItId_DelItem,                // cancellazione
        NSToolbarSeparatorItemIdentifier,     // separatore
        NSToolbarPrintItemIdentifier,         // stampa catalogo
        NSToolbarFlexibleSpaceItemIdentifier,     // spazio flessibile
        NSToolbarCustomizeToolbarItemIdentifier,// personalizzazione
        nil];
}

// metodo delegato obbligatorio
// questo metodo dice quali sono gli item possibili della toolbar
    // sono TUTTI gli item che possono entrare nella toolbar, e quindi
    // mostrati nel dialogo di personalizzazione
- (NSArray *)
toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
{
    // normalmente, nessun elemento e' previsto presente, quindi bisogna
    // inserire tutti gli elementi che si pensa possano essere presenti;
    // in particolare, separatori e spazi vari...
    return [NSArray arrayWithObjects:
        TBItId_AddItem, TBItId_SaveItem, TBItId_DelItem,    // i miei tre
        // e poi tutti gli altri ragionevoli...
        NSToolbarPrintItemIdentifier, NSToolbarShowFontsItemIdentifier,
        NSToolbarCustomizeToolbarItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
        NSToolbarSpaceItemIdentifier, NSToolbarSeparatorItemIdentifier, nil];
}

Sono piuttosto semplici: si limitano a costruire un elenco degli elementi e di inserirlo all'interno di un array. È da notare l'ordine degli elementi all'interno del metodo toolbarDefaultItemIdentifiers:, dal momento che sarà proprio l'ordine con cui gli elementi saranno poi visualizzati (da sinistra verso destra) all'interno della toolbar.

figura 02

figura 02

Per completezza, ho inserito nella toolbar anche l'elemento che comanda la personalizzazione della toolbar. L'uso dello spazio di dimensione variabile permette di posizionare questo elemento tutto a destra, e di mantenere tale posizione anche ridimensionando la finestra.

Il terzo metodo delegato è lungo ma piuttosto noioso. Ripeto per tre volta la stessa sequenza di operazioni, una per ogni elemento non predefinito della toolbar.

- (NSToolbarItem *)
toolbar: (NSToolbar *)toolbar
    itemForItemIdentifier: (NSString *) itemIdent
    willBeInsertedIntoToolbar:(BOOL) willBeInserted
{
    // costruisco una istanza di item della toolbar
    // come al solito, e' autorelase, verra' ritenuto dalla toolbar
    NSToolbarItem *toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdent] autorelease];
    
    // item della toolbar per aggiungere un volume
    if ([itemIdent isEqual: TBItId_AddItem])
    {
        // do due nomi all'icona di aggiunta volume: nome per la toolbar
        [toolbarItem setLabel: @"Add"];
        // nome per il dialogo di personalizzazione
        [toolbarItem setPaletteLabel: @"Add Volume"];
        // predispongo la scritta di aiuto (il tooltip)
        [toolbarItem setToolTip: @"Add a Volume to the Catalog"];
        // predispongo l'icona di questo item
        [toolbarItem setImage: [NSImage imageNamed: @"Add"]];
        // quando l'item e' cliccato, deve mandare al documento (self)...
        [toolbarItem setTarget: self];
        // ...il messaggio opportuno
        [toolbarItem setAction: @selector(addFileItem:)];
        return toolbarItem;
    }
    // ripeto il giochetto per la cancellazione di un elemento di catalogo
    if ([itemIdent isEqual: TBItId_DelItem])
    {
        [toolbarItem setLabel: @"Delete"];
        [toolbarItem setPaletteLabel: @"Delete Item"];
        [toolbarItem setToolTip: @"Remove an Item from the Catalog"];
        [toolbarItem setImage: [NSImage imageNamed: @"del"]];
        [toolbarItem setTarget: self];
        [toolbarItem setAction: @selector(delItem:)];
        return toolbarItem;
    }
    // e per il salvataggio del catalogo
    if ([itemIdent isEqual: TBItId_SaveItem])
    {
        [toolbarItem setLabel: @"Save"];
        [toolbarItem setPaletteLabel: @"Save Item"];
        [toolbarItem setToolTip: @"Save the Catalog"];
        [toolbarItem setImage: [NSImage imageNamed: @"save"]];
        [toolbarItem setTarget: self];
        // chiamo un metodo di default
        [toolbarItem setAction: @selector(saveDocument:)];
        return toolbarItem;
    }
    // gli altri dovrebbero essere standard
    return ( nil );
}

figura 06

figura 06

Per i nomi ed i tooltip, faccio la cosa più semplice (un buon programmatore preleverebbe i nomi da un dizionario, in modo da supportare la localizzazione in lingua dell'applicazione). Per le immagini, riciclo quelle che ho utilizzato per la palette dei comandi (che a questo punto non ha più senso di esistere...). Infine, per ogni elemento, dico di inviare un apposto messaggio (l'argomento di setAction:) a self, ovvero alla classe documento stessa. Le azioni sono le stesse che erano associate alle icone della palette (quindi, nessun lavoro aggiuntivo...).

Non rimane altro che attaccare la toolbar alla finestra. Allo scopo ho definito un metodo apposito, addToolbarToWin:, da chiamare all'interno del metodo windowControllerDidLoadNib: nel seguente modo:

...
    // attacco la toolbar alla finestra
    [self addToolbarToWin: [ aController window] ];
...

Il metodo utilizza un identificatore della toolbar che mi sono premunito di definire:

#define        CatDocToolbarId        @"tbName_CatDoc"

- (void)
addToolbarToWin: (NSWindow *) currWin
{
    // costruisco la nuova istanza della toolbar
    // la faccio autorelease perche' sara' poi ritenuta dalla finestra
    NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier: CatDocToolbarId] autorelease];

    // imposto le proprieta' di base della toolbar
    // permetto che l'utente la modifichi
    [toolbar setAllowsUserCustomization: YES];
    // lascio che conservi lo stato tra successivi lanci
    [toolbar setAutosavesConfiguration: YES];
    // dico che mostri solo le icone
    [toolbar setDisplayMode: NSToolbarDisplayModeIconOnly];
    // dico che il delegato della toolbar e' il documento stesso
    [toolbar setDelegate: self];
    // attacco la toolbar alla finestra
    [ currWin setToolbar: toolbar ];
}

I commenti inseriti nel codice spiegano a sufficienza le operazioni in corso.

figura 02

figura 02

Questi tre metodi già permettono di vedere qualcosa funzionante. Da notare come sia comparsa (automaticamente, dal momento che non ho scritto una riga di codice al proposito) sulla barra del titolo un piccolo pulsante aggiuntivo che permette di mostrare/nascondere la toolbar; e come inoltre sia disponibile un menu contestuale sulla toolbar stessa.

figura 03

figura 03

Inoltre, il dialogo di personalizzazione della toolbar è perfettamente funzionante. Di più, la toolbar stessa mantiene l'ultima configurazione utilizzata tra un lancio e l'altro dell'applicazione.

figura 04

figura 04

(Nota personale: quanto mi diverto a programmare, quando scopro cose che non mi aspettavo di vedere; ad esempio, non avevo notato che è possibile ridurre tutte le toolbar a solo testo fino a che non ho esplorato questo argomento per aggiungerlo alla mia finestra).

figura 05

figura 05

Non tutto è ancora a posto: per questo occorre aggiungere qualche metodo delegato opzionale.

Metodi opzionali

Se uno prova a fare clic sull'icona della stampante, succede nulla. In effetti, leggendo la documentazione, scopro che per default, il pulsante di stampa invia il messaggio printDocument: al firstResponder. Verosimilmente, questo messaggio cade nel vuoto. Piuttosto che mettermi a scrivere questo metodo, preferisco rimandare l'argomento della stampa e copiare ciò che avviene selezionando la voce Print dal menu dell'applicazione. Esaminando infatti il file MainMenu.nib, il messaggio inviato dalla voce è il più stringato print:.

Per cambiare il comportamento dell'elemento, uso il metodo delegato opzionale toolbarWillAddItem:, chiamato quando l'applicazione sta per inserire un elemento nella toolbar. È proprio il posto dove inserire codice per predisporre un elemento al suo funzionamento. L'argomento del metodo è una notifica: si recupera l'elemento sotto esame estraendo dal dizionario interno alla notifica l'oggetto avente la chiave item.

- (void)
toolbarWillAddItem: (NSNotification *) notif
{
    // ricavo l'item che mi interessa
    NSToolbarItem *addedItem = [[notif userInfo] objectForKey: @"item"];
    // se l'elemento e' l'icona per stampare...
    if ([[addedItem itemIdentifier] isEqual: NSToolbarPrintItemIdentifier])
    {
        // aggiungo il tooltip
        [addedItem setToolTip: @"Print the Catalog"];
        [addedItem setAction: @selector(print:)];
    }
}

Una volta individuato l'elemento di stampa, ne cambio il tooltip (altrimenti uno scarno Print) ed assegno la nuova azione.

Per finire, ho voluto gestire l'elemento che salva il catalogo in modo tale che sia abilitato quando il file è stato modificato, e disabilitato altrimenti. In altre parole, l'elemento è abilitato solo quando ha senso salvare il catalogo su file.

Per realizzare ciò, si sfrutta il fatto che la toolbar esamina ogni elemento presente, ed per ogni elemento avente un valido target, invia a questo il messaggio validateToolbarItem:. È il posto adatto dove adeguare lo stato dell'elemento a seconda del documento (ad esempio, potrei pensare di cambiare immagine, cose del genere). A me interessa solamente abilitare o disabilitare l'elemento per la stampa:

- (BOOL)
validateToolbarItem: (NSToolbarItem *) toolbarItem
{
    // l'item salva va bene solo se il documento e' stato cambiato
    if ([[toolbarItem itemIdentifier] isEqual: TBItId_SaveItem])
        return ( [self isDocumentEdited] ) ;
    // tutti gli altri sono abilitati
    return ( YES );
}

Il metodo isDocumentEdited restituisce lo stato corrente del documento. Inizialmente a NO, diventa YES quando, ad esempio, si aggiunge un volume: il cambiamento di stato è dovuto all'istruzione

    [ self updateChangeCount: NSChangeDone ] ;

presente, ad esempio, all'interno del metodo addVolume2Cat:... .

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