MaCocoa 035

Capitolo 035 - Rifondazione

Essendo passato molto tempo dall'ultima volta che ho seriamente programmato con Cocoa, ho deciso di ricominciare daccapo. Chiamo il nuovo progetto, direttamente specificato in XCode, CDCat2. Comunque, nessuna paura: molto del codice scritto sarà riutilizzato, qualche volta senza modifica.

Leggo la documentazione...

Primo inserimento: 30 dicembre 2003

Obiettivo

Il punto di partenza della ristrutturazione è l'apertura di due finestre appartenenti allo stesso documento. La prima finestra è ancora la cara vecchia finestra con l'elenco completo di tutti i file del catalogo in una NSOutlineView. La seconda finestra vorrebbe essere un contenitore dove cominciare a disegnare la copertina per ogni CD che è stato catalogato. Per il momento, sarà semplicemente un modo differente di vedere il contenuto del catalogo.

Con l'occasione rivedo un po' tutte le classi finora definite, cambiando qualche nome e cercando di renderle più pulite. Ad esempio, la classe che contiene le informazioni di un file sarà semplificata, evitando di trascinarsi dietro un po' di informazioni francamente inutili. Contestualmente, modifico un po' anche l'aspetto delle finestre accessorie.

Poiché sarà un discorso piuttosto lungo, spezzerò questo capitolo in varie parti, continuando in altri due capitoli.

Struttura Dati

La struttura dati che contiene le informazioni di un file è divisa in tre classi: CatFileInfo (prende il posto di LSFileInfo), FileStruct e VolInfo. Gli oggetti CatFileInfo contengono tutte le informazioni relative ad un file considerato da solo: quindi nome, data di creazione, di modifica, permessi di accesso, dimensione, eccetera. Continuo ad utilizzare le funzioni messe a disposizione da Carbon per meglio caratterizzare le dimensioni del file. Dalla struttura dati ho eliminato un po' di informazioni che ritengo inutili o comunque poco interessanti ai fini della catalogazione; altre informazioni invece non saranno più visualizzabili all'interno delle finestre; rimangono comunque come appoggio per svolgere altre funzioni interessanti. In questo modo la struttura definitiva della classe CatFileInfo è la seguente:

// nomi delle colonne dei dati
#define        COLID_FILENAME        @"fileName"
#define        COLID_MODDATE        @"modDate"
#define        COLID_CREATDATE        @"creatDate"
#define        COLID_FILESIZE        @"fileSize"
#define        COLID_GROUPNAME        @"ownGroupName"
#define        COLID_OWNERNAME        @"ownerName"
#define        COLID_POSIXPERM        @"filePosixPerm"
#define        COLID_OSCREATOR        @"creatorCode"
#define        COLID_OSTYPE        @"typeCode"
#define        COLID_ADD2PRINT        @"is2Print"

@interface CatFileInfo : NSObject {
    NSString         *fileName ;
    NSDate             *modDate ;        // NSFileModificationDate
    NSDate             *creatDate ;    // NSFileCreationDate
    unsigned long long    fileSize;    // NSFileSize
    NSString         *ownGroupName ;    // NSFileGroupOwnerAccountName
    NSString         *ownerName ;    // NSFileOwnerAccountName
    unsigned long    filePosixPerm ;    // NSFilePosixPermissions
    OSType            creatorCode ;    // NSFileHFSCreatorCode
    OSType            typeCode ;        // NSFileHFSTypeCode
    BOOL            is2Print ;        // definito dall'utente
    NSImage            * fileIcon ;
    // caratteristiche che non saranno visualizzate
    NSString         *fileFullPath ;
    NSString         *fileType ;        // NSFileType
}

- (id)        initWithPath:        (NSString*) fullPath ;
- (void)    dealloc ;

- (void)    encodeWithCoder:    (NSCoder *)encoder ;
- (id)        initWithCoder:        (NSCoder *)decoder ;

// seguono tutti i metodi accessor

Alla fine, sono rimaste dieci caratteristiche visualizzabili (quelle per le quali ho riportato le #define) ed altre due non visualizzate ma utili a vari scopi.

Ovviamente, assieme a questa ristrutturazione dell'interfaccia della classe, segue una uguale modifica dei metodi presenti. Le modifiche sono tuttavia abbastanza semplici (si tratta di eliminare le istruzioni che fanno riferimento a variabili d'istanza non più esistenti), per cui non riporto il codice.

Le altre due classi utilizzate per modellare un file system sono le 'vecchie' FileStruct e VolInfo. Non hanno subito modifiche importanti. Dalla classe VolInfo ho tolto alcune variabili d'istanza poco significative, ed ho aggiunto i due metodi:

- (void)encodeWithCoder:(NSCoder *)encoder ;
- (id)initWithCoder:(NSCoder *)decoder ;

per effettuare un corretto salvataggio dei dati (in effetti, me ne ero dimenticato a suo tempo...).

- (void)encodeWithCoder:(NSCoder *)encoder
{
    // salvo le variabili ereditate
    [ super encodeWithCoder: encoder ];
    // vedo quanti file ci sono
    [ encoder encodeValueOfObjCType: @encode(unsigned long long) at: & volSize];
    [ encoder encodeValueOfObjCType: @encode(unsigned long long) at: & volFreeSize];
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// metodo per il salvataggio/ripristino dell'istanza da file
- (id)initWithCoder:(NSCoder *)decoder
{
    // recupero le variabili ereditate
    [ super initWithCoder: decoder ];
    [ decoder decodeValueOfObjCType: @encode(unsigned long long) at: & volSize];
    [ decoder decodeValueOfObjCType: @encode(unsigned long long) at: & volFreeSize];
    return self ;
}

figura 01

figura 01

Volevo notare una funzionalità presente in XCode, ovvero un menu che raccoglie una serie di script utili per la programmazione. Tra questi script ce ne sono un paio che aiutano la scrittura dei metodi accessor: sono le voci Place Accessor Decls on Clipboard e Place Accessor Defs on Clipboard del sottomenu Code. Il funzionamento è molto semplice: si selezionano le righe che contengono la variabili d'istanza e si esegue il comando da menu. Negli appunti sono presenti tutte le dichiarazioni delle funzioni (nel primo caso) oppure il codice vero e proprio delle funzioni accessor (il secondo caso).

Ho provato ad utilizzare tale funzione, ma mi sono trovato di fronte a diversi problemi. Il primo problema è noto, ed è l'incapacità dello script di riconoscere tipi di variabili un po' più complicate della struttura

<tipo con singola parola>    <nome della variabile> ;

Ad esempio con unsigned long non funziona. Di più, alcuni tipi semplici (uno tra tutti, il tipo BOOL) non è riconosciuto come scalare, e subisce quindi un trattamento da oggetto. Questo secondo problema si può risolvere facilmente, modificando lo script ed aggiungendo i tipi mancanti (lo script si trova nel file /Library/Application Support/Apple/Developer Tools/Scrip ts/10-User Scripts/40-Code/10-accessorsFromIvars.sh. Ci ho messo un bel po' a trovarlo...).

Tuttavia, il più grosso problema è dato dal codice prodotto; per quanto sia scritto da Apple, ho forti dubbi non tanto sul corretto funzionamento quanto sull'efficienza. Ad esempio, ogni metodo accessor di tipo setNomeVar esegue le seguenti operazioni:

TipoVar        * nomeVar ;

- (TipoVar *)nomeVar {
    return [[nomeVar retain] autorelease];
}

- (void)setNomeVar:(TipoVar *)newNomeVar {
    if (nomeVar != newNomeVar) {
        [nomeVar release];
        nomeVar = [newNomeVar copy];
    }
}

Non trovo particolarmente felice l'operazione di copy qualsiasi sia il tipo di oggetto; in taluni casi (ad esempio, l'intero catalogo!) è tremendamente pesante dal punto di vista delle risorse di calcolo. Per oggetti di dimensioni notevoli, un messaggio di retain dovrebbe essere sufficiente.

Particolarmente bizzarro il comportamento nel caso di variabili 'scalari', quali un intero, come si può vedere nel seguente esempio. L'assegnazione immediata del valore, senza prima il confronto col valore precedente, non provoca alcun problema.

int            nomeVar1 ;
- (int)nomeVar1 {
    return nomeVar1;
}

- (void)setNomeVar1:(int)newNomeVar1 {
    if (nomeVar1 != newNomeVar1) {
        nomeVar1 = newNomeVar1;
    }
}

Ad ogni modo, in questa classe (ma anche in tutte le altre) ho riscritto tutti i metodi accessor basandosi sullo schema proposto; ho modificato sistematicamente i metodi setNomeVar per le variabili scalari e i metodi nomeVar per gli oggetti. Per quanto riguarda i metodi setNomeVar per gli oggetti, ho lasciato l'operazione di copy per gli oggetti piccoli (stringhe, ad esempio), e modificato il messaggio in retain per gli oggetti più consistenti.

Controllori per finestre

Uno dei motivi principali della ristrutturazione è la possibilità di aprire due finestre che rappresentino lo stesso documento. Con la tecnica sviluppata fino a questo momento, non è possibile; occorre passare ad un altro meccanismo. Ricordo che tre sono le classi preposta alla realizzazione di una applicazione document-based. C'è la classe NSDocumentController, unica all'interno dell'applicazione, che si occupa di gestire i documenti. Questi sono presenti sotto forma di classi NSDocument, una classe per ogni documento. Il documento è visualizzato poi all'interno di una finestra, il cui funzionamento è gestito da un oggetto della classe NSWindowController, uno per ogni finestra. Normalmente, quando si usa il principio 'un documento, una finestra', non occorre gestire esplicitamente l'oggetto NSWindowController, ed utilizzarne uno di default.

Tuttavia, per come intendo lavorare, devo gestire esplicitamente all'interno di NSDocument la creazione degli oggetti NSWindowController (perché a questo punto ce ne saranno almeno due). Bisogna cambiare un po' di cose: la cosa interessante è che in effetti si riutilizza la maggior parte del codice già scritto, ma lo si sposta qui e lì.

figura 02

figura 02

Per prima cosa bisogna intervenire sul file NIB, o meglio, sulla caratterizzazione del File's Owner. Ricordo che il File's Owner è il proprietario del file NIB, l'oggetto che estrae e rappresenta a video tutti gli oggetti conservati nel file NIB. Attraverso il File's Owner passa tutta la comunicazione tra l'applicazione ed il contenuto del file NIB. Mentre nella versione precedente il File's Owner era un oggetto del tipo NSDocument (in particolare , CatalogDoc), adesso deve diventare un oggetto del tipo NSWindowController. Sfrutto le potenzialità di Interface Builder per dichiarare una nuova classe ListWinCtl, appunto sottoclasse di NSWindowController. Già che ci sono, faccio qualche modifica estetica alla finestra, aggiungendo la texture ed un campo di testo a mo' di barra di stato; ne cambio anche il nome in CdListWin. Completo la classe ListWinCtl con gli outlet necessari, poi genero i file relativi alla classe. Torno in Xcode per scrivere un po' di codice.

Prima di tutto, occorre caricare il file NIB. Poiché ho creato il nuovo progetto da zero, utilizzando l'apposito template fornito da XCode, mi trovo a dover riempire gli spazi vuoti della classe CdCatDoc, il nome con cui ho ribattezzato la nuova sottoclasse di documento. Nella versione precedente, era sufficiente scrivere il metodo windowNibName affinché ritornasse come stringa il nome del file NIB. Adesso non è più il caso: occorre utilizzare il metodo seguente.

- (void)
makeWindowControllers
{
    // istanza del controllore
    ListWinCtl * ctl =
        [[ ListWinCtl alloc ] initWithWindowNibName: @"CdListWin" ];
    // autrelease, che tanto poi sara' ritenuta
    [ ctl autorelease ] ;
    // aggiungo la fienstra al documento
    [ self addWindowController: ctl ];
}

In questo metodo, si costruisce un oggetto della classe ListWinCtl inizializzandolo col nome del NIB desiderato. Tale oggetto è stabilito essere autorelease, che tanto il messaggio successivo effettua un retain, associandolo al documento.

Presumo che ogni documento abbia come variabile d'istanza un vettore che contiene l'elenco di tutti gli oggetti NSWindowController associati. Nella procedura di inizializzazione dell'oggetto documento ad un certo punto è inviato il messaggio makeWindowControllers che procede alla costruzione di tutte le finestre associate. La realizzazione standard del metodo (quella utilizzata finora) presuppone un unico NSWindowController standard, che invia il messaggio windowNibName al documento stesso per avere un nome di file NIB da caricare. In questo caso invece il metodo makeWindowControllers fa di testa sua e costruisce un NSWindowController di tipo opportuno.

Passo adesso a costruire la finestra, e quindi alla classe ListWinCtl. Nella versione precedente, la finestra era meglio caratterizzata attraverso il codice del metodo windowControllerDidLoadNib:. Questo metodo è specifico di NSDocument, e qui non può essere utilizzato (non è vero, ripensandoci lo si potrebbe fare, tuttavia è meglio procedere in altro modo). Si utilizza il metodo windowDidLoad tipico degli oggetti NSWindowController. Presumo che quando il file NIB sia stato interamente caricato, sia inviato all'oggetto NSWindowController il messaggio windowDidLoad. Sospetto che la versione di default si limiti ad inviare il messaggio windowControllerDidLoadNib: al documento cui il controllore appartiene.

Ad ogni modo, all'interno del metodo ci vanno più o meno quelle istruzioni che prima erano presenti all'interno dell'oggetto CatalogDoc.

- (void)
windowDidLoad
{
    ImageAndTextCell    * imageAndTextCell = nil;
    NSTableColumn        * tableColumn = nil;
    CatDataSrc            * myDataSrc ;
    
    [super windowDidLoad ];
    // associo questa finestra alla sorgente dati
    myDataSrc = [ [self document] dataSource] ;
    // inserisco una cella custom nella outlineView; la cella standard gestisce solo
    // testo, questa nuova testo ed una immagine
    tableColumn = [fullList tableColumnWithIdentifier: COLID_FILENAME];
    // creo l'oggetto autorelease perche' passa in carico alla tableColumn
    imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease];
    [tableColumn setDataCell:imageAndTextCell];
    // assegno alla colonna presente l'immagine che indica l'ordinamento
    [ fullList setIndicatorImage: [NSImage imageNamed: @"imgUpS"] inTableColumn: tableColumn ];
    [ fullList setDelegate: myDataSrc ];
    [ fullList setDataSource: myDataSrc ];
    // salvo nelle prefs le dimensioni delle colonne della outlineView
    [ fullList setAutosaveName: @"FullListViewOptions" ];
    [ fullList setAutosaveTableColumns: YES ];
    // facendo doppio clic, si cambia la direzione di ordinamento
    [ fullList setTarget: self ];
    [ fullList setDoubleAction: @selector( changeSortOrder:) ];
#if 0
    // registro la finestra che accetti drag
    [ [ aController window] registerForDraggedTypes:
        [NSArray arrayWithObject: NSFilenamesPboardType] ];
#endif    
    // attacco la toolbar alla finestra
    [self addToolbarToWin: [ self window] ];    

    // 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];
}

Qui ci sono alcune osservazioni. In primo luogo, dall'oggetto ListWinCtl si risale immediatamente al documento di appartenenza utilizzando il messaggio document. Presumo che la variabile d'istanza associata sia predisposta automaticamente dalla procedura di inizializzazione. In questo modo è possibile risalire immediatamente a tutte le altre variabili d'istanza del documento necessarie per il corretto funzionamento della finestra. La finestra in pratica consiste nella outlineView (e nel campo di testo che funziona da barra di stato, di cui per il momento non mi occupo). Per la corretta gestione della outlineView occorre individuare due enti ausiliari: la sorgente dei dati e la classe delegata. Nella versione precedente attribuivo le funzioni di sorgente di dati ad una classe apposita, e le funzioni di classe delegata erano attribuite al documento. Questa volta decido di attribuire entrambe le funzioni ad una unica classe; poiché la funzione principale è di essere sorgente di dati, dico che la classe nel codice individuata come

[self document] dataSource

svolgerà questi compiti. Rispetto alla versione precedente mancano alcuni pezzi di codice. La creazione e l'inizializzazione della sorgente dei dati avviene all'interno della classe CdCatDoc, all'interno del metodo init (è giusto qui sotto). È scomparsa la gestione della colonna COLID_ADD2PRINT introdotta più di recente, in quanto sarà gestita altrove. Per il momento, non attivo il funzionamento del drag and drop.

- (id)
init
{
    self = [super init];
    if (self)
    {
        CatDataSrc        * dataSrc = [[CatDataSrc alloc] init ] ;
        [ dataSrc autorelease ] ;
        // devo costruire l'oggetto sorgente dei dati
        [ self setDataSource: dataSrc ];
        [ dataSource setRefDoc: self ];
    }
    return self;
}

Per il resto, nulla di nuovo.

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