MaCocoa 061

Capitolo 061 - Meno finestre per tutti

L'interfaccia utente è sempre stato uno dei miei punti deboli (a vari livelli). Mi sono accorto che questa applicazione ha una quantità di finestre accessorie veramente eccessiva, ed ho pensato di ridurle un po'. In questo breve capitolo, pertanto, non ci sono discorsi eclatanti, ma solo la riduzione del numero delle finestre, ed un piccolo appunto sui menu.

Sorgenti: nessuna

Prima stesura: 1 novembre 2004

Finestre ausiliarie

L'applicazione presenta una gran quantità di finestre ausiliare: vale a dire, tutti quei pannelli che sono utilizzati a diversi scopi e di ausilio per le due finestre principali, l'elenco dei file del catalogo ed il disegno di copertine.

Alcune finestre sono autosufficienti e non saranno toccate, come la finestra About e quella delle ricerche. Lascio anche intonsa la finestra che mostra le informazioni relative ad un elemento del catalogo, e mi concentro sulle finestre di ausilio al disegno della copertina.

figura 01

figura 01

In primo luogo, unisco la finestra che permette di stabilire le dimensioni della copertina con la finestra per impostare unità di misura e griglia. L'unico file UnitsSizeGrid.nib piglia il posto di due precedenti file nib, e l'unica classe UnitSizeGridWinCtl comprende le precedenti classi CvrSizeWinCtl e GridDlgWinCtl.

Questa classe non ha nulla di speciale: deriva infatti semplicemente dall'unione delle due classi precedenti, conglobando le variabili d'istanza e i metodi delle due classi da cui deriva. Ovviamente, ci sono alcune semplificazioni e modifiche che si ripercuotono sull'intera applicazione.

figura 02

figura 02

figura 03

figura 03

La seconda e fondamentale modifica è la riduzione ad una sola finestre della finestre accessorie legate alla manipolazione degli elementi grafici: in questo modo le classi ElemInfoWinCtl (informazioni e manipolazione delle caratteristiche comuni degli elementi), AlignWinCtl (gestione dell'allineamento), RotateWinCtl (rotazione di uno o più elementi), TxtParWinCtl (parametri di un campo di testo) e TranspWinCtl (gestione della trasparenza degli elementi immagine) sono tutte raccolte all'interno dell'unica classe ElemParamWinCtl. Corrispondentemente, scompaiono i vari file nib delle finestre gestite e costruisco una nuova finestra nel file ElemParam.nib. Per evitare di costruire una finestra di dimensioni enorme, divido i vari controlli all'interno di una NSTabView tripartita. La prima tabview contiene le informazioni relative all'elemento, la seconda raccoglie i controlli per rotazione ed allineamento, la terza i parametri per i testi e le immagini.

figura 04

figura 04

La classe ElemParamWinCtl si occupa di gestire tutti i vari controlli; anche qui, le variabili d'istanza ed i metodi derivano direttamente dalle classi da cui deriva, per semplice accumulo. Solo qualche metodo è stato modificato per tenere conto delle mutate condizioni. L'unica parte degna di nota è quella che attiva o disattiva i controlli a seconda della selezione corrente. Le operazioni sono controllate dal metodo setSelectedElem, invocato dalla classe AppDelegate in seguito ad una notifica di cambio selezione (il metodo updateCoverViewInfo: la quantità di finestre da aggiornare si è ridotta di moto: sono sole le due finestre residue descritte).

- (void)
setSelectedElem: (CCE_BasicForm *) selElem
{
    unsigned long maskSts ;
    
    theSelectedElem = selElem;
    // se non ci sono eselementi selezionati
    if ( theSelectedElem == nil )
    {
        // dico che se ne fa nulla
        [ elemId setStringValue: @"No graphic element selected"];
        [ self setControlsEnabled: ENABCTL_NULLENAB ] ;
        return ;
    }
    // ... o ce ne sono troppi
    else if ( theSelectedElem == (CCE_BasicForm*)(-1) )
    {
        [ elemId setStringValue: @"Too many graphic elements selected"];
        // attivo solo rotazione ed allineamento
        [ self setControlsEnabled: (ENABCTL_ALIGN | ENABCTL_ROTATE) ] ;
        return ;
    }
    // se arrivo qui, c'e' un solo elemento selezionato
    // attivo tutti gli elementi della finestra
    maskSts = (ENABCTL_ELEMINFO | ENABCTL_ROTATE) ;
    // i parametri testo se il caso
    if ( [theSelectedElem isKindOfClass:[CCE_Text class]] )
        maskSts += ENABCTL_TXTPARAM ;
    // i parametri immagine se il caso
    if ( [theSelectedElem isKindOfClass:[CCE_Image class]] )
        maskSts += ENABCTL_TRANSPIMG ;    
    // attivo o disattivo i controlli
    [ self setControlsEnabled: maskSts ] ;
    // aggiorno il contenuto dei vari campi
    [ self updateWindowInfo ];
}

Qui c'è una fondamentale modifica: il metodo setControlsEnabled non ha più un parametro booleano, ma un parametro intero. Questo parametro è in realtà un campo di bit: è un numero che deriva dalla sovrapposizione (or bit per bit o anche semplice somma) di più elementi, ciascuno caratterizzato dall'avere un singolo bit ad uno (si tratta di una tecnica standard del programmatore assembler e C per mettere in un unico parametro una serie di variabili booleane).

#define        ENABCTL_NULLENAB        0x0000
#define        ENABCTL_ELEMINFO        0x0001
#define        ENABCTL_ALIGN            0x0002
#define        ENABCTL_ROTATE            0x0004
#define        ENABCTL_TXTPARAM        0x0008
#define        ENABCTL_TRANSPIMG        0x0010

C'è quindi un bit per ogni porzione di finestra che deve essere attivata o meno a seconda della selezione corrente. Se ad esempio c'è più di un elemento selezionato, solo la porzione della finestra relativa all'allineamento ed alla rotazione sono attive, mentre tutte le altre sono tenute disattive.

Le operazioni sono svolte dal metodo seguente:

- (void)
setControlsEnabled: (unsigned long) statusMask
{
    BOOL    status ;
    // informazioni elemento
    status = ( statusMask & ENABCTL_ELEMINFO ) ? YES : NO ;
    [fillCheckbox setEnabled: status ];
    [fillColorWell setEnabled: status ];
    ...
    [heightTextField setEnabled: status ];
    // allineamento
    status = ( statusMask & ENABCTL_ALIGN ) ? YES : NO ;
    [ selectedAlign setEnabled: status ];
    [ execBtn setEnabled: status ];
    // rotazione
    status = ( statusMask & ENABCTL_ROTATE ) ? YES : NO ;
    [ crcSlider setEnabled: status ];
    [ linSlider setEnabled: status ];
    [ linSlider setEnabled: status ];
    [ dirRadioBtn setEnabled: status ];
    // parametri testo
    status = ( statusMask & ENABCTL_TXTPARAM ) ? YES : NO ;
    [ autoTxtMenu    setEnabled: status ];
    [ columnNum        setEnabled: status ];
    // trasparenza immagine
    status = ( statusMask & ENABCTL_TRANSPIMG ) ? YES : NO ;
    [ transpSlider setEnabled: status ];
    [ transpText setEnabled: status ];
    [ makeImgOriginalsize setEnabled: status ] ;
}

Prima di ogni sezione di controlli, si ricava lo stato corrispondente estraendolo con l'operazione di and bit per bit dal parametro; con questo valore si adegua l'aspetto dei controlli alla situazione.

Abilitazione automatica dei menu

Nell'operazione di riunificazione delle finestre, mi sono imbattuto del problema di come abilitare o disabilitare le voci di menu che permettevano di aprire queste finestre. Finora avevo collegato le voci di menu a dei metodi della classe AppDelegate, che a loro volta aprivano le finestre. Questa situazione, benché molto comoda, ha delle controindicazioni: permette di aprire delle finestre che in effetti non hanno significato nel contesto. Ad esempio, con la finestra del catalogo dei file in primo piano, era possibile aprire la finestra che imposta le dimensioni della copertina. Sarebbe più corretto che in questo contesto la voce sia disabilitata. Questo mi ha portato ad approfondire l'argomento del First Responder e dell'abilitazione automatica dei menu.

Quando costruisco una finestra all'interno di un file nib, ho sempre disponibili due classi: la classe File's Owner e la classe First Responder. La prima in genere diventa un'istanza della classe controllore della finestra, mentre la seconda identifica la classe destinata a ricevere in prima battuta gli eventi. Si tratta spesso della finestra, ma non appena l'utente si focalizza su di elemento (ad esempio, un campo testo), il primo risponditore diventa quel campo stesso. Ci sono due cose importanti da puntualizzare: il primo risponditore può essere impostato da programma o comunque indipendentemente dall'utente. Questo accade ad esempio se si valorizza l'outlet initialFirstResponder di una finestra con uno degli elementi presenti all'interno della finestra stessa, oppure se si usa il metodo makeFirstResponder. La seconda cosa da tenere presente è che il primo risponditore è solo il primo (appunto) di una lunga catena, che porta da un elemento dell'interfaccia al suo superiore gerarchico (ogni NSView è una sottoclasse di NSResponder, classe che appunto raccoglie tutti i meccanismi per rispondere agli eventi) fino ad arrivare alla finestra, al delegato della finestra, al controllore, al documento, all'applicazione.

Ora che questo argomento è chiaro (spero) passo a parlare dei menu. Ad ogni evento, ogni voce di menu valuta il proprio stato e la voce corrispondente si abilita o si disabilita a seconda delle condizioni operative. Nella situazione più semplice, la cosa avviene automaticamente. Ogni voce di menu che intende eseguire qualche operazione ha associato un target ed una action. Ad ogni evento, allora, verifica se il target esiste, e prova a vedere se il target stesso o una classe della catena dei risponditori appesa al target, è in grado di rispondere al metodo indicato dalla action. Se ciò accade, il menu è abilitato; se nessuno è in grado di rispondere, il menu è disabilitato.

Nel caso dei menu che aprivano le finestre accessorie, non sapendo quale target attribuire, avevo deputato la classe AppDelegate. Tuttavia, poiché questa classe è sempre presente e disponibile alla risposta, le voci di menu corrispondenti sono sempre abilitate.

Per avere voci di menu più corrette semanticamente, occorre spostare il target. Ho diviso le finestre accessorie in quattro categorie: quelle pertinenti al catalogo di file, quelle pertinenti al disegno della copertina, quelle relative a qualche caratteristica di un elemento grafico, e quelle globali dell'applicazione. Le finestre globali sono ad esempio la finestra di About e delle preferenze. Le voci di menu corrispondenti sono quindi rimaste collegate alla classe AppDelegate, che contiene anche i metodi per aprire le finestre. Le finestre relative al catalogo dei file sono quelle delle rierche e delle informazioni sul file; i metodi corrispondenti sono stati quindi spostati all'interno della classe ListWinCtl. Sempre qui ho portato anche il metodo per aprire la finestra che contiene la copertina, in modo che il menu per mostrare la copertina sia attivo solo se in primo piano c'è una finestra di catalogo. Infine, le finestre relative alla copertina nel suo complesso (la finestra unificata con dimensioni, unità e griglia e il pannello con gli strumenti) passano in carico alla classe CoverWinCtl, mentre la finestra unificata con informazioni, rotazione, eccetera rimane in carico alla classe CoverView.

Perché tutto questo meccanismo funzioni, occorre modificare il target delle voci di menu nel file MainMenu.nib. Il target non è più la classe AppDelegate, ma il generico First Responder (occorre aggiungere a tale classe i vari metodi per poter attribuire le action alle voci) presente nel file.

Per completare le operazioni, ci sono due istruzioni da aggiungere. La prima istruzione serve a selezionare il giusto tab nella finestra ElemParam; ho quindi aggiunto un metodo alla classe ElemParamWinCtl che selezioni il tab corretto a seconda del comando impartito; ad esempio:

- (IBAction)
showRotateWin: (id)sender
{
    // mostro la finestra invocando l'istanza condivisa
    [ [ ElemParamWinCtl sharedElemParamWinCtl ] showWindow: sender ] ;
    [ [ ElemParamWinCtl sharedElemParamWinCtl ] selectPanel: SECTSEL_ALIGNROTATE ] ;
}

La seconda istruzione serve a riportare il focus e il primo risponditore al posto giusto dopo qualche operazione sulla finestra della copertina. All'interno del metodo handleMouseClick aggiungo l'istruzione seguente:

    [[winCtl window] makeFirstResponder: self];

in modo che la catena di risponditori parta (nuovamente) dalla CoverView.

Mantenere la dimensione delle colonne

Una cosa che mi irritava nella finestra del catalogo era la dimensione delle colonne, sempre ballerina e che non si mantiene uguale tra una finestra ed un'altra. Il problema è dovuto al fatto che la NSOutlineView possiede due colonne fisse (nome del file e campo is2Print), mentre le altre colonne sono costruite di volta in volta quando necessario. In pratica questo significa che il meccanismo di persistenza, fornito di serie con Cocoa per conservare le dimensioni delle colonne, non funziona. Ed allora, lo realizzo io, salvando alla chiusura della finestra le larghezze delle varie colonne, ed impostandole all'apertura.

Ovviamente, il posto più sensato dove conservare queste informazioni è il file delle preferenze.

Per prima cosa, arricchisco la classe UserPrefs con due metodi per recuperare ed impostare valori generici di preferenze:

+ (void)
setSinglePrefValue: (id) val forKey: (id) key
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [ defaults setObject: val forKey: key ] ;
}

+ (id)
getSinglePrefValue: (id) key
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    return ( [ defaults objectForKey: key ]) ;
}

Ora arricchisco il dizionario delle preferenze con un po' di valori: tutte le larghezze delle colonne, contraddistinte da una stringa che le identifica e che funziona da chiave nel dizionario.

Così facendo, all'apertura della finestra, nel metodo windowDidLoad, imposto la larghezza della colonna che contiene il nome del file:

- (void)
windowDidLoad
{
    ...
    imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease];
    cw = [ [UserPrefs getSinglePrefValue: keyCDLCW_Filename ] intValue ];
    if ( cw <= 0 ) cw = 100 ;
    [ tableColumn setWidth: cw ];    
    [ tableColumn setDataCell:imageAndTextCell ];
    ...

Ugualmente procedo all'interno della funzione configureColumn (riportata solo per alcuni casi):

void
configureColumn( NSTableColumn *tc, NSString * colId )
{
    ...
    if ( [ colId isEqual: COLID_POSIXPERM ] )
    {
        FilePosixPerm     *myDF3 = [[[ FilePosixPerm alloc ] init ] autorelease ];
        int    cw = [ [UserPrefs getSinglePrefValue: keyCDLCW_PosixPerm ] intValue ];
        if ( cw <= 0 ) cw = 100 ;
        [ tc setWidth: cw ];
        [[tc dataCell ] setFormatter: myDF3 ];
        return ;
    }
    ...
    if ( [ colId isEqual: COLID_OWNERNAME ] )
    {
        int    cw = [ [UserPrefs getSinglePrefValue: keyCDLCW_OwnerName ] intValue ];
        if ( cw <= 0 ) cw = 100 ;
        [ tc setWidth: cw ];
    }
}

Ho preso l'accortezza di impostare un valore di 100 pixel nel caso in cui il valore per qualche motivo sia negativo o nullo (ad esempio, perché è la prima volta che parte l'applicazione, ed il file delle preferenze è ancora vuoto).

figura 05

figura 05

Rimane da determinare il posto in cui salvare questi valori; nella classe ListWinCtl c'è già un metodo che è chiamato alla chiusura della finestra, e che serve a restituire YES per indicare alla classe CdCatDoc che si intende chiudere il documento. Lo utilizzo anche per salvare le dimensioni delle colonne.

- (BOOL)
shouldCloseDocument
{
    NSTableColumn        * tc ;
    NSNumber            * cw ;
    // ne approfitto per mettere a posto le larghezze di default
    tc = [ fullList tableColumnWithIdentifier: COLID_FILENAME];
    cw = [ NSNumber numberWithInt: [ tc width ] ] ;
    [ UserPrefs setSinglePrefValue: cw forKey: keyCDLCW_Filename ];
    tc = [ fullList tableColumnWithIdentifier: COLID_MODDATE];
    if ( tc )
    {
        cw = [ NSNumber numberWithInt: [ tc width ] ] ;
        [ UserPrefs setSinglePrefValue: cw forKey: keyCDLCW_ModDate ];
    }
    ...
    tc = [ fullList tableColumnWithIdentifier: COLID_OSTYPE];
    if ( tc )
    {
        cw = [ NSNumber numberWithInt: [ tc width ] ] ;
        [ UserPrefs setSinglePrefValue: cw forKey: keyCDLCW_OSType ];
    }
    return ( YES );
}

Con questo, le dimensioni sono conservate all'interno del file delle preferenze e si mantengono tra un lancio dell'applicazione ed il successivo.

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