MaCocoa 034

Capitolo 034 - Prima della pioggia

Dopo lungo tempo, ripiglio in mano Macocoa, e mi trovo a lavorare con Xcode e Panther. Prima che si scateni il diluvio, metto a posto un problema, capisco meglio una cosa, ed aggiungo una piccola cosa. Tuttavia, come si vedrà, la mia ignoranza rimane alta.

Documentazione Apple

Primo inserimento: 27 dicembre 2003

XCode

XCode è il nuovo ambiente di programmazione di Apple, che sostituisce Project Builder. Nonostante le roboanti notizie stampa, non è che all'apparenza sia cambiato molto. C'è però da dire che non ricordo molto i dettagli delle versioni precedenti. È quindi possibile che alcune funzioni e caratteristiche qui date per scontate non siano in realtà presenti sulle versioni precedenti. Non leggo neppure le note di rilascio, ma, nella migliore tradizione degli utenti Macintosh, comincio subito a lavorarci, lasciandomi guidare dall'intuizione. E intendo continuare così, dal momento che finora non ho ancora letto una riga di documentazione su XCode e non ho ancora affrontato grossi problemi.

Ovviamente, non appena apro il mio vecchio progetto CDCat, Xcode mi avvisa che intende convertirlo nel nuovo formato, eccetera. Lo lascio fare. C'è una caratteristica piuttosto interessante; l'indicizzazione del codice, che ha subito profonde modifiche, tanto che adesso propone anche la caratteristica code completion, che spesso viaggia assieme. Detto in parole povere, la code completion è un meccanismo che, mentre state scrivendo del codice, piglia possesso della finestra dell'editor e vi propone il completamento di ciò che state scrivendo; ad esempio, se cominciate a scrivere il nome di un metodo, l'editor vi propone in una bella lista ordinata i vari metodi che potrebbero rispondere al bisogno. Ci si mette di più a spiegarlo che a vederlo in azione. Io l'ho disattivato, per non infierire sul mio povero computer (a corto di megahertz) ed anche perché sono abituato a programmare in altro modo (paradigma copia-incolla). Se avessi una macchina più potente, troverei la cosa molto utile (in altri momenti, su altri sistemi operativi, il meccanismo di code completion è inestimabile).

Dicevo che ho importato il vecchio progetto CDCat nel nuovo XCode. Compilo tutto quanto, nessun problema. Faccio partire l'applicazione, e ci sono, ovviamente, devastanti problemi. Cosa è successo? Chi lo sa. Approfondisco l'esecuzione del codice utilizzando il nuovo debugger (che è sempre gdb con una bella faccia, un po' rifatto rispetto alla vecchia versione, ma è sempre il buon vecchio gdb).

Scopro dopo lungo penare che il problema risiede all'interno della procedura reduceImageToIcon (file djZeroUtils.m). Ad un certo punto, eseguo la chiamata removeRepresentation: che produce una morte spettacolare. Piuttosto che stare lì ad indagare oltre (sono pigro), decido di riscriverlo completamente, in maniera migliore. Ricordo che la procedura cerca di ridurre le dimensioni di un oggetto NSImage rimuovendo tutte le rappresentazioni dell'immagine che non interessano ai miei scopi (in pratica intende mantenere solo quella più piccola). Trasformo la procedura in una funzione, che cerca la rappresentazione più piccola, e con questa produce un nuovo oggetto NSImage ai minimi termini.

La funzione è in realtà molto somigliante alla procedura originale. Ancora una volta, c'è una ricerca della rappresentazione minima. Questa rappresentazione è utilizzata per riempire un oggetto NSImage, che è restituito dalla funzione.


NSImage *
reduceImageToIcon( NSImage * img )
{
    NSImage        * reducedImg ;
    NSSize        smallIconSize ;
    int        minSize, minImgIndex;
    unsigned int    i ;
    NSImageRep     * imageRep ;
    // recupero la lista delle rappresentazioni
    NSArray        * tmp = [ img representations ] ;
    // per ora la rappresentazione minima e' la prima
    minImgIndex = 0 ;
    imageRep = [ tmp objectAtIndex: minImgIndex ];
    minSize = [ imageRep pixelsHigh ];
    // poi guardo tutte le altre rappresentazioni
    for ( i = 1 ; i < [ tmp count ] ; i ++ )
    {
        int        hh ;
        imageRep = [ tmp objectAtIndex: i ];
        // se la dimensione della rappresentazione corrente...
        hh = [ imageRep pixelsHigh ];
        // ...e' maggiore
        if ( hh < minSize )
        {
            // ho trovato una nuova rappresentazione minima
            // me la segno
            minImgIndex = i ;
            minSize = hh ;
        }
    }
    reducedImg = [ [ [NSImage alloc] init ] autorelease ];
    // arrivato qui, ho trovato al rappresentazione piu' piccola
    [ reducedImg addRepresentation: [ tmp objectAtIndex: minImgIndex ]];
    // dico di scalarla quando sara' ridimensionata
    [ reducedImg setScalesWhenResized: TRUE ];
    // perche' adesso la ridimensiono a 16x16
    smallIconSize.width = smallIconSize.height = 16 ;
    // ecco che la ridimensiono
    [ reducedImg setSize: smallIconSize];
    return (reducedImg);
}

Con questa modifica, l'applicazione CDCat continua a funzionare.

Finestra modale

C'è un comportamento dell'applicazione che non è particolarmente corretto (anzi, è sbagliato). Quando si aggiunge un volume al catalogo compare una finestra di attesa (WaitPanCtrl) che mostra il progredire delle operazioni. Questa finestra è modale per l'applicazione: quando c'è questa finestra in primo piano, nessuna altra finestra dell'applicazione è in grado di funzionare (anzi, se fate clic fuori della finestra ottenere un beep di sistema). Ciò è ragionevole dal momento che l'applicazione non sarebbe in grado di compiere altre operazioni. È però perfettamente lecito passare ad un'altra applicazione, e nell'attesa fare qualcos'altro. In effetti, facendo clic altrove, un'altra finestra si porta davanti, e si può lavorare con l'applicazione cui appartiene questa finestra. Tuttavia, la finestra di attesa rimane proditoriamente in primo piano. Non impedisce il lavoro con le altre applicazioni, ma è li fastidiosamente in primo piano a coprire spazio del monitor. Ovviamente, così non va.

Non sapendo cosa fare, leggo la documentazione (è un vecchio trucco degli informatici esperti). Ed infatti qui trovo una risposta (o almeno, un indizio piuttosto grosso). Si dice di invocare spesso il metodo runModalSession: in modo che la finestra risponda correttamente agli eventi. Ecco il problema. Chiamando il metodo solo all'inizio del procedimento di aggiunta file, la finestra non risponde a tutti gli eventi successivi, fino al momento in cui l'operazione non è terminata. In particolare, non risponde agli eventi che le imporrebbero di andare in secondo piano.

Una volta trovato il problema, la soluzione è spesso ovvia. Ma qui c'è il problema che l'avanzamento lavori è nascosto all'interno di un metodo initTreeFromPath: dell'oggetto FileStruct, o meglio nel metodo initWithPath: dell'oggetto LSFileInfo. Una volta partito l'aggiornamento, non è possibile intervenire sulla sessione modale; è invece possibile intervenire sull'aspetto della finestra WaitPanCtrl con il metodo setCurrentValue:, il quale si occupa di aggiornare la barra di avanzamento lavori.

La soluzione più pigra che ho trovato è di informare la classe WaitPanCtrl di quale sia la sessione in corso, e di invocare il metodo runModalSession: per la sessione stessa. Modifico quindi la classe aggiungendo una variabile d'istanza dall'ovvio nome session; aggiungo un paio di metodi accessor. Adesso, all'apertura della sessione, all'interno del metodo performAddFilesModal: di CatalogDoc, informo la classe WaitPanCtrl del valore della variabile. Questo valore è utilizzato poi all'interno del metodo setCurrentValue: per invocare il metodo runModalSession:. Insomma, un brutto trucco di cui non vado orgoglioso, ma che raggiunge lo scopo prefisso. Adesso, non appena si passa ad un'altra applicazione, la finestra di avanzamento lavoro passa diligentemente in secondo piano.

Un nuovo campo

L'ultima modifica per questo capitolo è l'aggiunta di una variabile d'istanza booleana alle informazioni del file. Intendo utilizzare tale variabile successivamente, quando mi avventurerò nelle funzioni di stampa delle copertine per i CD. Col il valore della variabile indico se il file contribuisce alla stampa del catalogo, oppure no. A prescindere dal significato della variabile, introduco alcune modifiche interessanti. In primo luogo, voglio visualizzare il valore di questa variabile non con una stringa, ma attraverso un pulsante di spunta. Inoltre, l'utente deve poter essere in grado di modificare il valore della variabile operando direttamente sul pulsante, spuntando o meno il valore.

Questo implica la modificabilità dei dati visualizzati all'interno della outlineView, cosa finora esclusa. Ma andiamo per ordine.

Aggiungere una variabile d'istanza is2Print all'oggetto LSFileInfo è una cosa facile ma noiosa. ci sono da modificare parecchie cose: aggiungere la variabile e i metodi accessor, poi c'è da tenere conto dei metodi per il salvataggio e ripristino dati, insomma tanti piccoli interventi qui e lì. Decido tuttavia di non far intervenire questa variabile nelle preferenze, nell'ordinamento e nella ricerca.

figura 01

figura 01

Poi arriva la parte divertente. In primo luogo stabilisco che la colonna che visualizza la variabile è sempre presente (come il nome del file). Per fare questo, il metodo più spiccio è di intervenire con Interface Builder ed aggiungere direttamente nell'interfaccia la finestra. Bisogna fare attenzione al come si attribuisce il nome della colonna, visto che sfrutto un meccanismo per ricavare il valore di una cella a partire dal nome della colonna. Dico poi che la colonna avrà una dimensione fissa: per ottenere ciò, spunto l'apposito pulsante nella finestra di informazioni relativa alla colonna scelta.

Arrivo ora al nocciolo del problema. Così com'è, l'applicazione funziona, ma nella nuovo colonna è mostrato Zero oppure Uno a seconda dell'inizializzazione (sempre Uno, perché ho deciso di inizializzare a Vero la nuova variabile d'istanza). Devo fare in modo che la colonna non sia visualizzata come testo, ma come un pulsante di spunta. Ho già fatto (copiato) qualcosa di simile quando ho aggiunto l'icona al nome del file, costruendo una nuova classe ImageAndTextCell. All'inizio, ho provato a fare una cosa simile, utilizzando una cella del tipo NSButtonCell. Ho installato la cella al posto della cella standard alla costruzione della finestra (metodo windowControllerDidLoadNib: di CatalogDoc) :

...
NSButtonCell        * add2print = nil ;    
...    
// una nuova colonna con la visibilita' nella stampa
tableColumn = [outlineView tableColumnWithIdentifier: COLID_ADD2PRINT ];
add2print = [[[ NSButtonCell alloc] init ] autorelease ];
[ add2print setEditable: YES ];
[ add2print setButtonType: NSSwitchButton ];
[ add2print setTitle:@""] ;
[ tableColumn setDataCell:add2print];
...

In piena similitudine, ho cercato di impostare il corretto stato del pulsante all'interno del metodo

- (id) outlineView: objectValueForTableColumn: byItem:

Però, non funziona. Come al solito, ho cercato ispirazione nella documentazione, ed ho ri-scoperto un metodo della outlineView che permette di intervenire all'ultimo momento possibile sulla visualizzazione di una cella.

- (void)outlineView: (NSOutlineView *) oView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn item:(id)item

Si tratta di un metodo delegato della outlineView: si trova quindi all'interno dell'oggetto CatalogDoc. Il metodo è chiamato poco prima che sia visualizzata la cella aCell della colonna aTableColumn con i dati dell'elemento item. Con queste informazioni è estremamente facile impostare lo stato coretto del pulsante che rappresenta la cella:

- (void)
outlineView: (NSOutlineView *) oView
    willDisplayCell:(id)aCell
    forTableColumn:(NSTableColumn *)aTableColumn
    item:(id)item
{
    if ( [[ aTableColumn identifier] isEqualToString: COLID_ADD2PRINT ] )
    {
        BOOL    state = [ item is2Print];    
        [aCell setState: ( state ? NSOnState: NSOffState) ];
    }
}

Se sono nella colonna giusta, leggo lo stato corrente della variabile; a seconda del suo valore, imposto lo stato del pulsante di spunta.

Il passo successivo è di permettere all'utente di modificare lo stato del pulsante. Ora, all'inizio dei tempi, alla prima realizzazione della outlineView, avevo utilizzato il metodo

outlineView: shouldEditTableColumn: item:

per impedire ogni intervento dell'utente sui valori della tabella. Pensavo di dover modificare tale metodo in modo da permettere la modifica della sola colonna coi pulsanti di spunta in questo modo:

- (BOOL)
outlineView:            (NSOutlineView *) outlineView
    shouldEditTableColumn:    (NSTableColumn *) tableColumn
    item:            (id) item
{
    if ( [[ tableColumn identifier] isEqualToString: COLID_ADD2PRINT ] )
        return YES ;
    return NO;
}

In realtà, non serve a nulla (e vai a capire perché). Mi è bastato aggiungere il seguente metodo nella classe sorgente di dati LSDataSource:

- (void)
outlineView:(NSOutlineView *) oView
    setObjectValue:(id)object
    forTableColumn:(NSTableColumn *)tableColumn
    byItem:(id)item
{
    if ( [[ tableColumn identifier] isEqualToString: COLID_ADD2PRINT ] )
    {
        BOOL    state = [ item is2Print];    
        [ item setIs2Print: ! state ] ;
    }
}

Questo metodo, molto semplicemente, nel caso in cui si debba modificare il valore di una cella della colonna che mi interessa, rovescia lo stato della variabile booleana.

figura 02

figura 02

Posso azzardare una spiegazione del meccanismo: normalmente, quando si intende modificare il contenuto di una cella, si fa doppio clic. Se è permesso modificarla, è possibile manipolare il testo scrivendo ciò che si vuole. Una volta terminato, si esce in qualche modo (tabulatore, ritorno carrello, clic altrove) e si cerca di eseguire la modifica. Ora, il metodo shouldEditTableColumn dovrebbe consentire le modifiche in seguito al doppio clic. La cosa è vieppiù complicata dalla presenza in Interface Builder del pulsante di spunta Editable per ogni colonna, e dalla possibilità di modificare lo stato di editabilità del campo da programma, attraverso il metodo setEditable: relativo alla cella. In effetti, lasciando editabile il campo is2Print, un doppio clic permette la modifica del testo (di default vuoto, mi interessa solo lo stato del pulsante).

Quando si è terminato di editare il campo, interviene il metodo setObjectValue:...; con questo metodo è possibile cambiarne effettivamente il valore, aggiornando la sorgente dei dati. Se tale metodo non è fatto intervenire, alla visualizzazione successiva (praticamente da subito) la cella riprende il valore precedente, senza alcuna modifica.

C'è ancora una cosa da mettere a posto: quando si modifica lo stato di uno dei pulsanti, è in pratica cambiato lo stato del documento. Chiudendo la finestra del documento, sarebbe una buona cosa chiedere se si vogliono salvare le modifiche. Purtroppo, ancora una volta, la modifica avviene all'interno di un oggetto LSDataSource, che nulla conosce del documento associato. Ancora una volta, ricorro all'aggiunta di una variabile d'istanza che contenga un riferimento al documento cui la sorgente di dati è associata. Qui bisogna fare attenzione ad entrare in un circolo vizioso: il documento CatalogDoc ha una variabile d'istanza di tipo LSDataSource, ed adesso inserisco in LSDataSource una variabile d'istanza di tipo CatalogDoc. Per poter utilizzare un oggetto occorre prima dichiararlo, ad esempio importando il file .h relativo. Se in CatalogDoc includo LSDatasource.h, in quest'ultimo file non posso includere CatalogDoc.h (che altrimenti ci si morde la coda). Allora utilizzo la dichiarazione di diverso tipo:

@class    CatalogDoc ;

che informa dell'esistenza di una classe di nome CatalogDoc.

Fatto questo, aggiunta la variabile d'istanza ed i metodi accessor, la cosa si risolve facile aggiungendo un'assegnazione al momento del collegamento della sorgente dati al documento:

...
// e' la sorgente di dati di outlineView
[ outlineView setDataSource: dataSource];
// imposto l'ordine iniziale
[ dataSource setOrderColumn: [ NSString stringWithString: COLID_FILENAME]];
[ dataSource setOrderDirection: TRUE ];
[ dataSource setRefDoc: self ];
// carico i dati nella finestra
...

Il metodo che permette la modifica dei valori dello stato del pulsante diventa così

- (void)
outlineView:(NSOutlineView *) oView
    setObjectValue:(id)object
    forTableColumn:(NSTableColumn *)tableColumn
    byItem:(id)item
{
    if ( [[ tableColumn identifier] isEqualToString: COLID_ADD2PRINT ] )
    {
        BOOL    state = [ item is2Print];    
        [ item setIs2Print: ! state ] ;
        // qui dovrei fare in modo di dire che il documento e' cambiato
        [ [ self refDoc ] updateChangeCount: NSChangeDone ] ;
    }
}

Con questo chiudo il capitolo, senza aver risolto alcuni problemi che mi si sono presentati nel frattempo (che non vi dico, ma che vi lascio scoprire da soli). Ma tanto, adesso cambierà tutto.

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