MaCocoa 036

Capitolo 036 - Altre finestre

Continuo dal capitolo precedente: qui si apre una seconda finestra sullo stesso documento.

Leggo la documentazione...

Primo inserimento: 2 gennaio 2004

Preferenze e gestione colonne

La classe UserPrefs per la gestione delle preferenze non è sostanzialmente cambiata: ho eliminato solamente i valori che si riferiscono a campi non più presenti o non più visualizzati. È invece leggermente cambiata la gestione della visualizzazione delle colonne. Adesso questa operazione è volta all'interno della classe ListWinCtl, come per altro dovrebbe essere stato chiaro quando ho indicato self come target delle notifiche di aggiornamento delle preferenze. Ho aggiustato il metodo prefsUpdated per tenere conto della mutata numerosità delle colonne. Ho anche trasferito qui la gestione della colonna COLID_ADD2PRINT, cosa che mi permette di eseguire un giochino interessante. Decido infatti che questa colonna sia sempre l'ultima della serie. Per fare questo, ho aggiunto tre righe (ma solo per chiarezza, avrei potuto fare tutto con una unica istruzione...).

- (void)
prefsUpdated: ( NSNotification *) notification
{
    int fi, ti ;
    // recupero le preferences e mostro o meno la colonna
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColModDate] boolValue], COLID_MODDATE );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColCreatDate] boolValue], COLID_CREATDATE );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColFileSize] boolValue], COLID_FILESIZE );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColGroupName] boolValue], COLID_GROUPNAME );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColOwnerName] boolValue], COLID_OWNERNAME );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColPosixPerm] boolValue], COLID_POSIXPERM );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColCreator] boolValue], COLID_OSCREATOR );
    setupColumn( fullList, [[ UserPrefs getPrefValue: keyColOSType] boolValue], COLID_OSTYPE );
    // questa colonna e' sempre visualizzata
    setupColumn( fullList, TRUE , COLID_ADD2PRINT );
    // sposto la colonna is2Print in ultima posizione
    fi = [ fullList columnWithIdentifier: COLID_ADD2PRINT ];
    ti = [ fullList numberOfColumns] - 1 ;
    [ fullList moveColumn: fi toColumn: ti ];
    [ fullList reloadData ];
}

Sfrutto alcuni metodi presenti all'interno della classe per trovare l'indice corrente della colonna, individuare quante sono le colonne, e spostare poi la colonna in ultima posizione (gli indici, a quanto pare, partono da Zero, quindi l'ultima colonna è data dal numero di colonne presenti meno uno).

La funzione setupColumn non è stata modificata, se non per il fatto che la funzione in precedenza chiamata attachFormatter adesso è meglio individuata con configureColumn; all'interno di questa funzione infatti procedo alla personalizzazione delle colonne. Nella maggior parte dei casi si tratta solamente di attaccare un formattatore per la corretta visualizzazione del valore; fa eccezione proprio la colonna COLID_ADD2PRINT che subisce un trattamento particolare:

    // ... altro codice ...
    if ( [ colId isEqual: COLID_ADD2PRINT ] )
    {
        NSButtonCell        * add2print = nil ;
        add2print = [[[ NSButtonCell alloc] init ] autorelease ];
        [ add2print setButtonType: NSSwitchButton ];
        [ add2print setTitle:@""] ;
        [ tc setWidth: 16 ];
        [ tc setResizable: FALSE ];
        [ [ tc headerCell] setStringValue: @"P?" ];
        [ tc setDataCell:add2print];
    }
    // ... altro codice ...

Qui, oltre all'impostazione del tipo di cella, la si costringe ad una dimensione fissa di 16 pixel (giusto quelli necessari per visualizzare il pulsante di spunta).

L'ordine degli elementi

Ho modificato largamente il meccanismo preposto alla gestione dell'ordinamento degli elementi all'interno della outlineView. Ho già stabilito all'interno del metodo windowDidLoad che con un doppio clic si seleziona un diverso criterio, in base alla colonna prescelta. Questa serie di operazioni è volta dal metodo seguente, che è stato modificato (e spero migliorato):

- (void)
changeSortOrder: (id) sender
{
    NSTableColumn    * tableColumn ;
    CatDataSrc        * myDataSrc ;

    // associo questa finestra alla sorgente dati
    myDataSrc = [ [self document] dataSource] ;
    // voglio che sia selezionata una intera colonna
    if ( [ sender selectedColumn ] == -1 ) return ;
    // determino allora quale colonna e' selezionata
    tableColumn = [ [fullList tableColumns] objectAtIndex: [ fullList clickedColumn ]];
    if ([ [ tableColumn identifier] isEqualToString: COLID_ADD2PRINT]) return ;
    // se la colonna di ordinamento non cambia
    if ([ [ tableColumn identifier] isEqual: [ myDataSrc orderColumn ]])
    {
        // rovescio l'ordine e cambio l'immagine (lo faccio poi)
        [ myDataSrc setOrderDirection: ! [ myDataSrc orderDirection ] ];
    }
    else
    {
        [ fullList setIndicatorImage: nil
            inTableColumn: [ fullList
                tableColumnWithIdentifier: [ myDataSrc orderColumn ]
            ]
        ];
        // rovescio l'ordine negando il valore corrente
        [ myDataSrc setOrderColumn: [ tableColumn identifier]];
    }
    // in ogni caso imposto l'immagine nello header della colonna
    if ( [ myDataSrc orderDirection ])
            [ fullList setIndicatorImage: [NSImage imageNamed: @"imgUpS"] inTableColumn: tableColumn ];
    else    [ fullList setIndicatorImage: [NSImage imageNamed: @"imgDwS"] inTableColumn: tableColumn ];
    // rinfresco la finestra con i nuovi dati
    [ fullList reloadData ];
}

In primo luogo stabilisco che non si possono ordinare gli elementi secondo la colonna COLID_ADD2PRINT; in effetti in questo caso il metodo fa nulla e lascia tutto come trovato.

Poi verifico se la colonna selezionata è la stessa che governa l'ordine corrente; in tal caso significa che devo solamente rovesciare l'ordinamento, ma non cambiare la colonna. Altrimenti, significa che devo cambiare colonna (nella versione precedente, un doppio clic generava un cambiamento di colonna ed anche di direzione di ordinamento; adesso cambia solo la colonna, ma la direzione non cambia). Da notare anche il meccanismo con cui tolgo il triangolino dalla colonna in cui si trovava (basta rintracciare la colonna che governa l'ordinamento corrente: lo si fa attraverso il suo identificatore, conservato nella sorgente dei dati).

In ogni caso, imposto la nuova immagine triangolo nella colonna cliccata, ed aggiorno il contenuto della finestra (è questo aggiornamento forzato che provoca materialmente l'ordinamento dei dati all'interno della sorgente).

Contestualmente è stato modificato e semplificato un metodo delegato della outlineView, che svolgeva alcune funzioni che si sovrapponevano (adesso fa parte della classe sorgente di dati; nella versione precedente era invece dentro la classe CatalogDoc):

- (BOOL)
outlineView:(NSOutlineView *) oView
    shouldSelectTableColumn:(NSTableColumn *)tableColumn
{
    return YES;
}

figura 03

figura 03

Adesso la selezione di una colonna e l'ordinamento sono concetti slegati tra loro. Facendo doppio clic su di una colonna, ovviamente si seleziona la colonna, ed il contenuto della finestra è ordinato secondo la colonna stessa. Tuttavia, è possibile selezionare altre colonne senza provocare riordinamenti. Facendo doppio clic sulla colonna COLID_ADD2PRINT si ottiene la sola selezione della colonna, ma nessun riordinamento.

Ultimi tocchi alla finestra

Ci sono ancora alcuni argomenti da toccare per concludere la trattazione di questa finestra.

La prima cosa è un bel tocco di classe per cambiare il nome visualizzato; dal momento che potrebbero esserci più viste dello stesso documento, è bene dare un nome più significativo alla finestra.

- (NSString *)
windowTitleForDocumentDisplayName:(NSString *)displayName
{
    return ( [ @"Catalog List: " stringByAppendingString: displayName] );
}

figura 04

figura 04

Il metodo sopra indicato è invocato ogni volta che deve essere visualizzata una finestra; l'argomento displayName è tipicamente la stringa Untitled seguita da un numero progressivo. Il metodo di default fa nulla, e restituisce la stringa intonsa. Qui faccio lo overload del metodo e inserisco davanti al nome una caratterizzazione della finestra. Questo metodo è anche comodo se voglio utilizzare un nome differente, al posto di Untitled, per scopi non meglio imprecisati.

C'è poi un metodo che prima mostro poi spiego:

- (BOOL)shouldCloseDocument
{
    return ( YES );
}

A quanto pare, Cocoa si occupa di gestire per me una serie di faccende noiose. In particolare, gestisce autonomamente lo stato dei documenti, e chiede gentilmente se è il caso di salvare un documento quando chiudo la finestra associata. Normalmente, essendoci una sola finestra per documento, la cosa non dovrebbe ingenerare equivoci: chiudendo la finestra, si chiude il documento associato. Cosa succede quando un documento presenta più di una finestra? Cocoa di default è tanto gentile da gestire per me la cosa: finché ci sono finestre aperte relative ad un documento, non sono inviate richieste di salvataggio. Non appena si chiude l'ultima finestra, chiede, se il caso, se salvare o meno il documento. Questo comportamento di default potrebbe non sempre essere quello gradito. Ad esempio, potrei avere una finestra principale sul documento, e le altre finestre sono solo accessorie. In tal caso, finché si chiudono finestre accessorie, non si sono problema. Tuttavia, potrei volere che alla chiusura della finestra principale si chiudesse tutto. Ecco, questo è il mio caso: dico che quando chiudo la finestra gestita da ListWinCtl, anche il documento debba essere chiuso (e che quindi si scateni, se il caso, il processo di salvataggio). In altri termini, quando il controllore chiude la finestra, si invia il messaggio shouldCloseDocument; normalmente il metodo corrispondente risponde NO. A meno che non sia l'ultima finestra associata al documento, il documento è lasciato aperto. Se però si fa lo overload di questo metodo, restituendo YES (qui per partito preso, ma la risposta potrebbe dipendere da altre questioni), il controllore della finestra invia al documento una richiesta di chiusura. Se questa richiesta procede, si chiude il documento (e quindi, tutte le altre finestre eventualmente aperte).

La toolbar è rimasta immutata. L'unico accorgimento è stato quello di ridirigere verso il documento alcuni messaggi (uno solo, in effetti, quello per il salvataggio del file) inviati dai pulsanti. Uguale sorte per il metodo che permette di aggiungere elementi al catalogo:

- (void)
addFileItem: (id)sender
{
    ChooseVolCtrl *tmpCVCtrl = [[ ChooseVolCtrl alloc] init ] ;
    // comincio la procedura: mi metto modale per il documento
    // alla chisura della sheet, sara' invocato il metodo addVolume:...
    [ NSApp beginSheet: [ tmpCVCtrl window ]
        modalForWindow: [ NSApp mainWindow ]
        modalDelegate: [ self document]
        didEndSelector: @selector( addVolume2Cat:returnCode:contextInfo: )
        contextInfo: nil
    ];
}

Qui non ci sono differenze se non appunto la delega al documento al termine delle operazioni di selezione del volume attraverso la finestra apposita (che non ha subito modifiche).

Di diverso avviso invece il metodo invocato per la cancellazione di un elemento dal catalogo.

- (void)
delItem: (id)sender
{
    FileStruct * fInfo ;
    // guardo se c'e' una riga selezionata
    int    rowsel = [ fullList selectedRow ];
    if ( rowsel == -1 )
    {
        // no, lascio stare
        NSBeep();    // bip!!
        return ;
    }
    // se arrivo qui, c'e' una riga selezionata
    fInfo = [ fullList itemAtRow: rowsel ] ;
    // adesso voglio eliminare questo elemento da dataSource
    [ [[ self document] dataSource] removeEntry: fInfo ];
}

Qui ho preferito raccogliere le informazioni sull'oggetto selezionato, e poi passare la palla alla sorgente dati utilizzando un nuovo, apposito, metodo, per la rimozione di un elemento (che vedrò più avanti).

Una diversa finestra

figura 05

figura 05

figura 06

figura 06

È giunto il momento di aprire una nuova finestra sul documento. L'obiettivo finale è che questa finestra sia utilizzata per predisporre la stampa della copertina del CD, secondo i vari formati disponibili (slim, jewel box, eccetera). Per il momento, tuttavia, mi limito a dare una diversa rappresentazione della sorgente dei dati. La rappresentazione individuata assomiglia a quella del Finder secondo Panther, ovvero una vista laterale in cui sono elencati i Volumi, ed una outlineView centrale in cui sono elencati i file del volume selezionato. Si fa prima a vedere una figura che raccontarlo. Costruisco la finestra all'interno di Interface Builder. Per poter avere un comportamento corretto, occorre rendere la NSTableView di sinistra e la NSOutlineView di destra delle sottoviste di una unica classe NSSplitView. Per fare questo, occorre selezionare le due View e selezionare la voce corretta dal menu Layout -& Make subviews of -& Split View. Decido che la outlineView non sarà configurabile, ma mostra solo due colonne: il nome del file e la colonna COLID_ADD2PRINT sotto forma di pulsante di spunta. La tableView invece mostrerà i volumi completi di icona.

Sempre all'interno di Interface Builder battezzo la nuova classe CoverWinCtl che funziona come controllore della finestra. Assegno questo tipo al File's Owner, aggiungo i soliti outlet e faccio i collegamenti standard. Costruisco i file e passo a scrivere un po' di codice.

Fondamentalmente c'è un unico metodo da scrivere, il seguente:

- (void) windowDidLoad
{
    NSTableColumn        * tableColumn = nil;
    ImageAndTextCell    * imageAndTextCell = nil;
    CatDataSrc            * myDataSrc ;
    NSButtonCell        * add2print = nil ;
    
    [super windowDidLoad ];
    // associo questa finestra alla sorgente dati
    myDataSrc = [ [self document] dataSource] ;
    [ volList setDelegate: myDataSrc ];
    [ volList setDataSource: myDataSrc ];
    // salvo nelle prefs le dimensioni delle colonne della outlineView
    [ volList setAutosaveName: @"VolumeViewOptions" ];
    tableColumn = [volList tableColumnWithIdentifier: @"volumeList"];
    // creo l'oggetto autorelease perche' passa in carico alla tableColumn
    imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease];
    [imageAndTextCell setEditable: NO];
    [tableColumn setDataCell:imageAndTextCell];

    // inserisco una cella custom nella outlineView; la cella standard gestisce solo
    // testo, questa nuova testo ed una immagine
    tableColumn = [fileList tableColumnWithIdentifier: COLID_FILENAME];
    // creo l'oggetto autorelease perche' passa in carico alla tableColumn
    imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease];
    [tableColumn setDataCell:imageAndTextCell];
    // una nuova colonna con la visibilita' nella stampa
    tableColumn = [fileList tableColumnWithIdentifier: COLID_ADD2PRINT ];
    add2print = [[[ NSButtonCell alloc] init ] autorelease ];
    [ add2print setButtonType: NSSwitchButton ];
    [ add2print setTitle:@""] ;
    [ tableColumn setDataCell:add2print];
    [ fileList setAutosaveName: @"FileListOptions" ];
    [ fileList setDelegate: myDataSrc ];
    [ fileList setDataSource: myDataSrc ];

    // mi abbono all'evento: pref aggiornate
    [[ NSNotificationCenter defaultCenter] addObserver: self
        selector: @selector( prefsUpdated: )
        name: PREF_UPDATE_NOTIFICATION object: nil ] ;
    // recupero le pref ed adeguo la finestra
    // in realta', faccio finta che siano state aggiornate le prefs
    [[NSNotificationCenter defaultCenter]
        postNotificationName: PREF_UPDATE_NOTIFICATION object:self];
}

Non ci sono grosse cose da notare. Lo outlet volList rappresenta la tableView; poiché deve mostrare i volumi completi di icona, assegno la cella della colonna in modo opportuno. Ugualmente, configuro la fileList (outlet che rappresenta la outlineView) con le due colonne col nome del file e la variabile is2Print.

Sia volList che fileList utilizzano la classe sorgente di dati come sorgente di dati e classe delegata. Questo fatto porrà un interessante problema sulla gestione della sorgente dati. Infatti questa classe dovrà fornire i valori da rappresentare all'interno di due diverse outlineView, in due modi differenti. Poiché non avrebbe senso avere due diverse sorgenti che rappresentano sostanzialmente la stessa base di dati, all'interno della classe sorgente dati occorre un meccanismo per distinguere tra le due outlineView.

Ma prima di passare alla soluzione del problema, completo l'esposizione della classe CoverWinCtl con due metodi residui:

- (NSString *)
windowTitleForDocumentDisplayName:(NSString *)displayName
{
    return ( [ @"Cover List: " stringByAppendingString: displayName] );
}

- (void)
prefsUpdated: ( NSNotification *) notification
{
    [ fileList reloadData ];
    [ fileList sizeLastColumnToFit ] ;
}

Il primo metodo impone un prefisso al nome della finestra, come ho già fatto per l'altra finestra. Il secondo metodo forza un rinfresco dei dati della outlineView quando c'è una variazione nelle Preferenze. Qui le preferenze che hanno effetto sono quelle riguardanti la visualizzazione dei dotFiles e l'espansione dei bundle.

figura 07

figura 07

A questo punto, bisogna trovare un meccanismo per visualizzare questa finestra. Una prima soluzione è quella di visualizzare entrambe le finestre alla creazione del documento; basta aggiungere tre righe al metodo makeWindowControllers sulla falsariga delle istruzioni già presenti. Invece, ho deciso di utilizzare un comando da menu, aggiunto all'interno del file MainMenu.nib. Questa voce di menu invia un messaggio alla classe delegata dell'applicazione per invocare il metodo seguente (nel file AppDelegate.m):

- (IBAction)showCoversWindow:(id)sender
{
    // piglio il documento davanti e gli dico di mostrare l'altra finestra
     [ [[ NSApp orderedDocuments] objectAtIndex: 0] showCoversWindow ] ;
}

Il metodo a sua volta invia un messaggio al documento che si trova di fronte a tutti gli altri.

- ( void)
showCoversWindow
{
    int numOfWC = [ [ self windowControllers ] count ];
    CoverWinCtl * ctl1 ;

    // se ci sono due finestre, devo fare nulla
    if ( numOfWC == 2 ) return ;
    // se arrivo qui, c'e' una sola finestra
    // per come ho predisposto le cose, puo' essere solo quella della
    // completa lista di file, e quindi manca proprio la finestra
    // delle covers
    ctl1 = [[ CoverWinCtl alloc ] initWithWindowNibName: @"CoversWin" ];
    [ ctl1 autorelease ] ;
    [ self addWindowController: ctl1 ];
    [[ ctl1 window ] makeKeyAndOrderFront: self ];
}

Il metodo conta quanti sono gli oggetti NSWindowsController associati al documenti. Per come ho costruito la cosa, ce ne possono essere uno oppure due. Se ce ne sono due, sono aperte entrambe le finestre desiderate, e quindi faccio nulla. Se è presente una sola finestra, allora passo a costruirne una nuova, utilizzando la classe CoverWinCtl. Da notare l'ultima istruzione del metodo, che serve a portare davanti a tutte le altre finestra la finestra appena costruita.

Val la pena di commentare la frase 'per come ho costruito la cosa'. Alla creazione di un documento nuovo, è aperta automaticamente una finestra governata dalla classe ListWinCtl. Questa rimane l'unica finestra per tutta la vita del documento, a meno che non sia invocato il metodo showCoversWindow. In questo caso è costruita una nuova finestra, governata dalla classe CoverWinCtl. Se l'utente chiude la finestra CoverWinCtl, finestra e controllore sono distrutti, e nulla altro accade: il documento e la finestra ListWinCtl rimangono aperti. Se invece l'utente chiude la finestra ListWinCtl, sono chiusi il documento e tutte le finestre relative (è questo l'effetto del metodo shouldCloseDocument sopra discusso), compresa, se presente, la finestra CoverWinCtl. Effetto collaterale di questa costruzione è lo sheet di salvataggio file, che è sempre attaccato alla finestra ListWinCtl, anche se il comando di salvataggio è inviato al documento con la finestra CoverWinCtl di fronte a tutti.

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