MaCocoa 026

Capitolo 026 - Fritto Misto

Questo capitolo è composto da vari pezzi slegati da loro; ci sono la correzione di alcuni errori, il miglioramento di alcuni comportamenti bizzarri, qualche nuova caratteristica, cose del genere.

Sorgenti: Tanto lavoro di debugger; un esempio di Apple.

Primo inserimento: 2 settembre 2002

Due Errori

Nel codice finora sviluppato, ci sono due errori; meglio, comportamenti non conformi quando si lavora in condizioni eccezionali (sì, mettiamola così...). Entrambi gli errori sono venuti alla luce quando ho tentato di catalogare il contenuto dell'intero hard disk.

Il primo errore è concettuale, ed è dovuto alle caratteristiche di Unix.

Dovete sapere che all'interno del mio hard disk (ma non credo di essere speciale...) c'è una cartella, nascosta, chiamata /Network (tra parentesi... ho scoperto una quantità enorme di cartelle nascoste). All'interno di questa cartella sono presenti altre cartelle, ed in particolare c'è una cartella che mantiene collegamenti ai volumi di rete. Poiché con MacOSX lo hard disk è uno dei volumi di rete presenti (si chiama sempre localhost), la catalogazione del contenuto dello Hard disk riprendeva daccapo. Questo conduce l'applicazione in un ciclo infinito, che la porta ad esplorare continuamente il contenuto dello hard disk (non è vero: l'applicazione muore clamorosamente dopo un po' di tempo). Il problema sta nella natura dei collegamenti. Quindi, digressione sui collegamenti in Mac OS X.

All'interno del sistema operativo sono possibili tre diversi modi per effettuare un collegamento. Il primo metodo è il classico Alias, ereditato dai sistemi operativi precedenti. Altri due collegamenti sono invece ereditati da Unix, e li chiamerò soft link e hard link. Un hard link (che pare si possa fare solo a file e non a directory) è totalmente indistinguibile dal file collegato. È come avere più copie del file, sempre mantenute sincronizzate ed uguali tra loro. Per effettuare un hard link, avete bisogno del terminale Unix e scrivere l'istruzione ln <nomefile>.

Un soft link è invece un collegamento ad un file molto più semplice: creando un soft link viene costruito un file nel cui interno di trova il nome (completo di path) del file collegato. Un soft link è utilizzando anche per effettuare collegamenti verso directory. Anche un soft link si costruisce tramite terminale con il comando ls -s <nomefile>. Da evidenziare la differenza tra un soft link ed un alias. Se sposto o rinomino il file puntato, l'alias continua a funzionare, il soft link no. D'altra parte, se sostituisco il file con una nuova versione, l'alias mantiene il riferimento alla vecchia versione, il soft link punta alla nuova. Fine digressione.

Il problema nasce con i soft link che il Mac OS X utilizza a man bassa per rendere le cose più semplici a se stesso, al Finder, all'utente, ai programmatori, insomma a tutti. Ora, il codice di catalogazione non segue i collegamenti dati da un alias perché, all'interno del codice stesso, quanto si recuperano le informazioni sul file, ho detto di non farlo: è l'istruzione

NSDictionary    *fattrs     = [manager fileAttributesAtPath: aFile traverseLink:NO];

presente all'interno del metodo initWithPath della classe LSFileInfo. Questa istruzione però, segue gli hard link (non può farne a meno) ma soprattutto segue i soft link. Ed è proprio questa cosa che non va bene. Per evitare l'espansione dei soft link mi sono trovato ad usare quel campo delle informazioni di un file che qualche capitolo fa lamentavo di non usare, ovvero fileType.

Ho quindi introdotto la seguente condizione

if ( [ [self fileType] isEqual: NSFileTypeSymbolicLink ])
    isAdir = FALSE ;

all'interno del metodo initTreeFromPath della classe FileStruct. La condizione dice, molto semplicemente, che se il file è in realtà un soft link, sicuramente non è una directory, o meglio, va trattato come un normale file: insomma, non va espanso. Questo risolve il primo problema.

Giga Giga Bum

Il secondo problema si è presentato quando ho catalogato directory il cui contenuto supera i due giga. Anche questo errore è saltato fuori quando ho catalogato l'intero hard disk (dopo aver risolto il problema precedente, ovviamente). Causa dell'errore è che la dimensione prevista dei file era mantenuta all'interno di un long, ovvero un numero intero a 32 bit, capace di mantenere una dimensione in byte fino a due giga. In effetti, guardando meglio la documentazione, il metodo che recupera la dimensione di un file non usa un long, ma piuttosto un long long. A prescindere dal buffo nome, si tratta di un intero a 64 bit, in grado di conservare correttamente dimensioni fino a otto milioni di Terabyte (più che sufficienti per qualche anno a venire). Cambiare la dimensione della variabile d'istanza da long a long long è parsa la più ovvia e semplice delle soluzioni, se non fosse che si è portata dietro un altro po' di problemi.

In primo luogo, occorre modificare anche i metodi encodeWithCoder e initWithCoder (cosa che, ancora una volta, rende incompatibili i file salvati su disco in precedenza), per non parlare dei metodi accessor. Poi, nella classe FileStruct, c'è da promuovere a long long la variabile che tiene conto delle dimensioni della directory man mano che si aggiungono file. Un grosso problema si è presentato nella classe LSDataSource, perché, a quanto pare, il meccanismo valueForKey non funziona con una variabile di tipo long long (o almeno, così sembra a me; mi pare strano...).

- (id)
outlineView:                (NSOutlineView *) outlineView
    objectValueForTableColumn:    (NSTableColumn *) tableColumn
    byItem:                (id)item
{
    NSString * colId ;
    
    // se l'oggetto e' vuoto, c'e' qualche problema...
    if (item == nil)
        return ( @"????" ) ;
    // recupero l'identificatore della colonna
    colId = [ tableColumn identifier] ;
    // e da qui l'elemento che mi serve
    if ( [ colId isEqual: COLID_FILESIZE ] )
    {
        long long    tmp ;
        tmp = [ item fileSize ] ;
        return ( [ NSNumber numberWithLongLong: [ item fileSize ]] );    
    }
    return ( [ item valueForKey: colId ] ) ;
}

Senza saper né leggere né scrivere (ma soprattutto programmare), ho deciso di non perdere troppo tempo sull'argomento e di inserire il caso specifico della dimensione del file per... per costruire un oggetto di tipo NSNumber. Infatti, il metodo serve per restituire il valore di una casella della NSOutlineView, valore che poi passa attraverso un formatter. Il formatter, per funzionare, si aspetta un generico oggetto da cui estrarre un valore. Il metodo più spiccio per inviare un valore numerico è allora di produrre un oggetto NSNumber con il valore corretto.

Va da sé che anche il formatter va modificato, perché estragga long long piuttosto che long. E qui, già che c'ero, ho aggiunto anche la visualizzazione in Giga quando la dimensione lo consente.

- (NSString *)
stringForObjectValue:    (id)anObject
{
    unsigned long long    fSize, fSizeK ;
    float             fsizeM, fsizeG ;
    
    // controllo che l'oggetto sia un numero...
    if (![anObject isKindOfClass:[NSNumber class]]) {
        return nil;
    }
    // ricavo la dimensione del file
    fSize = [ ((NSNumber*) anObject) longLongValue ];
    // se il file e' piccolo, mostro byte
    if ( fSize < 1024 )
    {
        long    tmp = fSize ;
        return ( [ NSString stringWithFormat: @" %4d b", tmp] );
    }
    // la dimensione e' in byte, divido per 1024 ed arrotondo, ottengo K
    fSizeK = (long) (( fSize / 1024.0 ) + 0.5 );
    // se il file e' medio, mostro K
    if ( fSizeK < 1024 )
    {
        long    tmp = fSizeK ;
        return ( [ NSString stringWithFormat: @" %4d K", tmp] );
    }
    // se inferiore al giga, restituisco mega con tre decimali
    fsizeM = ( fSizeK / 1024.0 ) ;
    if ( fsizeM < 1024 )
        return ( [ NSString stringWithFormat: @" %6.3f M", fsizeM] );
    //negli altri casi, ritorno Giga, con tre cifre decimali
    fsizeG = ( fsizeM / 1024.0 ) ;
    return ( [ NSString stringWithFormat: @" %6.3f G", fsizeG] );
}

Se poi volete rendermi infelice, chiedetemi perché all'interno di una coppia di parentesi graffe utilizzo una variabile temporanea long per la scrittura della stringa. A quanto pare, non esiste una specifica di formato per i long long (improbabile), oppure, non sono riuscito a trovarla (probabile).

figura 01

figura 01

C'è un'ultima cosa, che non c'entra con l'errore, ma che pertiene alla visualizzazione: quando installo il formatter della dimensione del file, ho pensato di allineare la colonna a destra piuttosto che lasciare l'allineamento a sinistra di default (sono nella funzione attachFormatter del file CatalogDoc.m):

#### codice codice ####
if ( [ colId isEqual: COLID_FILESIZE ] )
{
    FileSizeForm     *myDF2 = [[[ FileSizeForm alloc ] init ] autorelease ];
    [[tc dataCell ] setFormatter: myDF2 ];
    // gia' che ci sono, sbandiero a destra
    [[tc dataCell ] setAlignment: NSRightTextAlignment];
    [[tc headerCell ] setAlignment: NSRightTextAlignment];

    return ;
}
#### codice codice ####

L'Icona del file

Peregrinando nella documentazione, mi sono imbattuto in una classe e un metodo molto interessante; qualche capitolo fa mi crucciavo di non riuscire a recuperare l'icona del file. Ecco, ho trovato il modo per leggerla e conservarla.

Esiste una classe denominata NSWorkspace; con questa classe si può interagire in modo più o meno compiuto con l'ambiente operativo. In pratica l'unico oggetto di questa classe costruito automaticamente al lancio dell'applicazione (e che si trova col metodo di classe sharedWorkspace) è in grado di fornire servizi adatti a:

1. la manipolazione di file (copia, sposta,...) e il recupero di informazioni accessorie sugli stessi;

2. riconoscere eventi globali come modifiche al file system, dispositivi e utenti (ad esempio, si è reso disponibile un nuovo Volume, che so, un CD);

3. lanciare applicazioni dall'interno dell'applicazione.

Di tutto questo, al momento mi interessa un solo metodo,

- (NSImage *)iconForFile:(NSString *)fullPath

che restituisce, come un oggetto della classe NSImage, l'icona posseduta dal file.

Scoperto questo, aggiungere all'interno della classe LSFileInfo la variabile d'istanza fileIcon, i metodi accessor relativi, il recupero e la memorizzazione dell'icona del file è stato un gioco da ragazzi. Voglio solo riportare qualche riga di codice del metodo initWithPath:

// prelevo l'icona del file
tmpImg = [ [ NSWorkspace sharedWorkspace] iconForFile: aFile ] ;
// dico di scalarla quando sara' ridimensionata
[ tmpImg setScalesWhenResized: TRUE ];
// perche' adesso la ridimensiono a 16x16
smallIconSize.width = smallIconSize.height = 16 ;
// ecco che la ridimensiono
[ tmpImg setSize: smallIconSize];
[ self setFileIcon: tmpImg ];

L'idea è di ridimensionarla subito alla grandezza di 16 bit di larghezza per 16 bit di altezza, perché utilizzerò sempre queste dimensioni. Faccio tutto ciò da programma, dicendo dapprima di mantenerla proporzionata durante le operazioni di ridimensionamento, poi ridimensionandola appunto a 16x16, per infine assegnarla alla variabile d'istanza.

Aggiungo alcune considerazioni a questo argomento nel prossimo capitolo.

Il passo successivo, quello più semplice, consiste nel visualizzare l'icona nella finestra delle Info. Allo scopo, l'ho modificata facendo spazio per un'oggetto della classe NSImageView, di dimensioni appunto 16x16, inserendo un apposito outlet, e modificando di concerto il metodo selectionChanged:. Non trovate qui il codice perché il metodo subirà fra un momento una profonda revisione.

Arriva adesso la cosa teoricamente più difficile: far comparire l'icona all'interno della NSOutlineView, di fianco al nome del file. Eppure, il compito si è rivelato facilissimo, ma non per merito mio. In cerca di ispirazione, mi sono imbattuto nell'esempio DragNDropOutlineView, fornito a corredo dell'installazione di XCode. Ebbene, la cosa è già fatta! Nell'esempio, il programmatore ha costruito una classe proprio a questo scopo. Copio pedissequamente proprio i due file, ImageAndTextCell.h e ImageAndTextCell.m, e li uso brutalmente. Le bellezze della programmazione object-orientes stanno anche qui, nel fatto che ignoro bellamente il contenuto dei file (la realizzazione della classe) e mi interesso solamente della sua interfaccia come usare la classe).

Il procedimento è il seguente: una NSOutlineView contiene diverse colonne; ogni colonna è costituita da un insieme di oggetti NSCell, uno per ogni riga, responsabili della visualizzazione del contenuto. L'oggetto NSCell utilizzato di default è in grado di visualizzare testo; la classe ImageAndTextCell definisce una sottoclasse di NSCell (anzi, di NSTextFieldCell, la cella che mostra il testo) e fa in modo di visualizzare non solo il testo, ma anche una immagine che qualcuno ha provveduto ad inserire all'interno dell'oggetto come variabile d'istanza.

Per effettuare questo scambio di celle, occorre scrivere qualche riga di codice all'interno del metodo windowControllerDidLoadNib della classe CatalogDoc, lì dove si predispone il funzionamento della NSOutlineView (anche questo segmento di codice è stato copiato, mutatis mutandis, dall'esempio predetto):

tableColumn = [outlineView tableColumnWithIdentifier: COLID_FILENAME];
imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease];
[imageAndTextCell setEditable: NO];
[tableColumn setDataCell:imageAndTextCell];

Si ricava l'oggetto NSTableColumn responsabile della visualizzazione della colonna, si costruisce la cella con la nuova classe, si assegna tale cella come deputata alla visualizzazione (di passaggio, si dice anche che la cella non gestisce la modifica del contenuto).

Da qualche porte, infine, bisogna inserire l'immagine all'interno della cella. Per fare ciò, copio ancora una volta dall'esempio. Tra le varie notifiche che la NSOutlineView invia, e che la classe delegata può realizzare, ce ne è una che dice: 'guarda che mi sto preparando a visualizzare questa cella...'. Basta intercettare questa notifica, e se il caso impostare l'immagine:

- (void)
outlineView:             (NSOutlineView *) outlineView
    willDisplayCell:    (NSCell *)cell
    forTableColumn:        (NSTableColumn *) tableColumn
    item:            (id)item
{    
    if ( [[tableColumn identifier] isEqualToString: COLID_FILENAME] )
    {
        // Set the image here since the value returned from
        // outlineView:objectValueForTableColumn:... didn't specify the image part...
        [((ImageAndTextCell*) cell) setImage: [item fileIcon] ];
    }
    // negli altri casi, faccio nulla
}

figura 03

figura 03

La cosa è molto facile: se la cella appartiene alla colonna che mostra il nome del file, inserisco nella cella l'immagine corrispondente all'elemento in corso di visualizzazione. Dopo essersi ricordati di impostare la classe delegata della NSOutlineView (si può fare direttamente in IB, nel file CatalogWin.nib, collegando la finestra col File's Owner, che è un oggetto della classe CatalogDoc), le cose funzionano a meraviglia.

Alternativamente, posso sfruttare il metodo outlineView:objectValueForTableColumn:byItem:, già presente all'interno della classe LSDataSource. È il metodo che fornisce i valori da visualizzare. Intercettando la richiesta relativa al nome del file, inserisco in quel momento l'immagine come variabile d'istanza:

- (id)
outlineView:                    (NSOutlineView *) outlineView
    objectValueForTableColumn:     (NSTableColumn *) tableColumn
    byItem:                    (id)item
{
    NSString * colId ;
    
    // se l'oggetto e' vuoto, c'e' qualche problema...
    if (item == nil)
        return ( @"????" ) ;
    // recupero l'identificatore della colonna
    colId = [ tableColumn identifier] ;
    // e da qui l'elemento che mi serve
#if    1
    if ( [colId isEqualToString: COLID_FILENAME] )
    {
        // Set the image here since the value returned from
        // outlineView:objectValueForTableColumn:... didn't specify the image part...
        [((ImageAndTextCell*) [tableColumn dataCell]) setImage: [item fileIcon] ];
    }
#endif

    if ( [ colId isEqual: COLID_FILESIZE ] )
    {
        long long     tmp ;
        tmp = [ item fileSize ] ;
        return ( [ NSNumber numberWithLongLong: [ item fileSize ]] );    
    }
    return ( [ item valueForKey: colId ] ) ;
}

Tutto sommato, preferisco questa seconda versione, che non sparpaglia troppo il codice nei vari metodi.

La finestra di Info

figura 02

figura 02

Lavorando con la finestra di Info (ma anche con la palette dei comandi) mi sono accorto di alcune cose piuttosto fastidiose. La prima è che le due finestre rimanevano davanti a tutte anche se l'applicazione non era attiva. Mi sono dimenticato di impostare a TRUE il flag HideOnDeactivate su entrambe le finestre. Per far ciò, basta usare IB e visualizzare le informazioni della finestra.

La cosa è ora eseguita in modo automatico se la finestra è dichiarata essere Utility Panel

L'altro problema fastidioso è che la finestra delle Info non reagisce correttamente all'apertura (visualizza sempre nulla, anche se c'è un elemento selezionato nella finestra di catalogo) ed allo scambio di finestra (passando da una finestra di catalogo ad un'altra, la finestra delle info non si adegua di conseguenza, ma mantiene le informazioni dell'ultimo elemento cliccato, e non dell'elemento correntemente selezionato).

La cosa si risolve (mi pare) tenendo conto di alcuni eventi (notifiche) che si verificano durante la manipolazione delle finestre. Estendo in pratica gli abbonamenti alle notifiche notevoli (sono nel metodo windowDidLoad della classe InfoWinCtrl)

// dico che il winctrl osserva le notifiche riguardanti
// 1. il cambiamento di selezione di una outlineView
// eseguendo il metodo selectionChanged:
[[ NSNotificationCenter defaultCenter] addObserver: self
    selector: @selector( selectionChanged: )
    name: NSOutlineViewSelectionDidChangeNotification object: nil ] ;
// 2. il cambiamento di finestra
// eseguendo il metodo mainWindowChanged:
[[ NSNotificationCenter defaultCenter] addObserver: self
    selector: @selector( mainWindowChanged: )
    name: NSWindowDidBecomeMainNotification object: nil ] ;
// 3. la chiusura di finestra
// eseguendo il metodo mainWindowChanged:
[[ NSNotificationCenter defaultCenter] addObserver: self
    selector: @selector( mainWindowResigned: )
    name: NSWindowDidResignMainNotification object: nil ] ;

Nella versione precedente del metodo era presente solamente la prima sottoscrizione: la finestra delle Info reagisce solo quando qualcuno cambia la selezione all'interno della NSOutlineView corrente. La seconda sottoscrizione fa in modo che sia attivato il metodo mainWindowChanged: quando una finestra di catalogo è portata in primo piano (in particolare ,quanto è aperta nuova nuova). La terza sottoscrizione attiva il metodo mainWindowResign: quando una finestra di catalogo non è più la finestra principale, perché lo è diventata un'altra oppure perché è stata chiusa.

I tre metodi sono molto simili tra loro, e svolgono per la gran parte le stsse operazioni. Ho allora raccolto in un unico metodo le operazioni di aggiornamento della finestra:

- (void)
updateInfo: (NSOutlineView *) outView
{
    long        row ;
    FileStruct    * locItem ;
    // vedo se ci sono selezioni in corso
    if ( outView != nil )
        row = [ outView selectedRow] ;
    else     row = -1 ;
    // se non ce ne sono, faccio nulla
    if ( row == -1 )
    {
        // ripulisco la finestra
        [ fileType setStringValue: @""];
        [ groupName setStringValue: @""];
        [ ownerName setStringValue: @""];
        [ fullPath setStringValue: @""];
        [ fsFileNum setStringValue: @"" ];
        [ fsNum setStringValue: @"" ] ;
        [ fileSize setObjectValue: nil ];    
        [ osCreator setObjectValue: nil ] ;
        [ osType setObjectValue: nil ];
        [ posixPerm setObjectValue: nil ];
        [ modDate setObjectValue: nil ];
        [ fileIcon setObjectValue : nil ];
        [[ self window ] setTitle: @"Info" ] ;
        return ;
    }
    // altrimenti, uso l'elemento selezionato
    locItem = [ outView itemAtRow: row ] ;
    // se l'oggetto e' reale, aggiorno il contenuto
    [ fileType setStringValue: [ locItem fileType]];
    [ groupName setStringValue: [ locItem ownGroupName]];
    [ ownerName setStringValue: [ locItem ownerName]];
    [ fullPath setStringValue: [ locItem fileFullPath]];
    // piglio l'intero e lo traformo in stringa
    [ fsFileNum setStringValue: [ NSString stringWithFormat: @"%d", [ locItem fsFileNum]] ];
    [ fsNum setStringValue: [ NSString stringWithFormat: @"%d", [ locItem fsNum]]] ;
    // questi hanno un formattatore appiccicato, gli passo direttamente l'intero
    [ fileSize setObjectValue: [NSNumber numberWithLongLong: [ locItem fileSize] ]];    
    [ osCreator setIntValue: [ locItem creatorCode]] ;
    [ osType setIntValue: [ locItem typeCode] ];
    [ posixPerm setIntValue: [ locItem filePosixPerm] ];
    // qui, con il formattatoredi data, gli passo direttamente l'oggetto NSDate
    [ modDate setObjectValue: [ locItem modDate]];
    // recupero l'icona del file
    [ fileIcon setObjectValue : [locItem fileIcon] ];
    // infine, il nome del file e' il titolo della finestra
    [[ self window ] setTitle: [ locItem fileName ]] ;
}

Qui non c'è molto da dire (è il vecchio metodo selectionChanged:, con l'aggiunta della pulizia della finestra se non sono selezionati elementi) se non notare l'impostazione dell'icona del file.

I tre metodi sottoscrittori sono invece i seguenti.

Per primo il nuovo metodo selectionChanged:; estrae il nuovo elemento da visualizzare e poi invoca l'aggiornamento della NSOutlineView argomento della notifica:

- (void )
selectionChanged: ( NSNotification *) notification
{
    [ self updateInfo: [ notification object ] ];
}

Quando invece la finestra scompare o passa in secondo piano, pulisco la finestra delle info:

- (void)
mainWindowResigned:(NSNotification *) notification
{
    [self updateInfo: nil];
}

Più complicato l'aggiornamento delle Info quando si presenta una nuova finestra. Seguo passo passo la catena dei messaggi: ricavo dapprima quale finestra è passata in primo piano con il messaggio object inviato all'argomento notification; dalla finestra, recupero l'oggetto controllore (metodo windowController) per poi arrivare finalmente al documento CatalogDoc. Il documento mi serve per rintracciare (attraverso un outlet, per il quale ho dovuto definire metodi accessor) la NSOutlineView; finalmente, posso utilizzare questo oggetto per invocare l'aggiornamento della finestra delle info:

- (void )
mainWindowChanged: ( NSNotification *) notification
{
    [ self updateInfo: [[[[ notification object] windowController] document] outlineView] ];
}

In realtà, nella maggior parte dei casi la notifica resign è ridondante: se chiudo una finestra di catalogo, normalmente diventa principale un'altra finestra di catalogo, e sarà il metodo mainWindowChanged: che aggiorna la finestra delle Info di conseguenza. L'unico caso in cui il metodo mainWindowResign: ha un uso preciso è quando non ci sono più finestre presenti.

Salvataggio File

Mi sono accorto che manca una delle funzioni caratteristiche della gestione dei documenti, ovvero la richiesta di salvataggio di un file che è stato modificato. Come tutte le buone applicazioni, occorre in qualche modo visualizzare lo stato corrente del documento. All'inizio il documento è creato vuoto, e quindi non richiede alcun salvataggio. Non appena si comincia a lavorarci sopra, aggiungendo o togliendo elementi dal catalogo, il documento si sporca; è bene allora, prima di chiudere la finestra o abbandonare l'applicazione, chiedere all'utente cosa fare del documento sporcato. Un documento è sporco se non è mai stato salvato su disco, oppure se il contenuto di ciò che è salvato su disco differisce da quanto rappresentato.

Cocoa fornisce a corredo del paradigma NSDocument un meccanismo molto semplice per tenere traccia delle modifiche; si tratta di utilizzare il metodo updateChangeCount: passandogli come argomento un valore predefinito che indica come lo stato del documento si è modificato. Nel mio caso, per il momento, utilizzo sempre e solo la seguente istruzione:

[ self updateChangeCount: NSChangeDone ] ;

all'interno dei metodi della classe CatalogDoc che si occupano di modificare il numero degli elementi presenti all'interno di una finestra di catalogo, ovvero addFileItem:, delItem: e performDragOperation:.

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