MaCocoa 056

Capitolo 056 - Template

Fino ad ora non c'era la possibilità di salvare il disegno di una copertina. Né in effetti ci sarà; ciò che questo capitolo introduce è il concetto di template: con un template indico l'insieme di parametri che caratterizzano il disegno di una copertina (ovvero, gli elementi presenti, la loro disposizione e la loro caratterizzazione). L'utente può salvare un template, e riutilizzarlo poi per disegnare una data copertina. Nel fare tutto ciò, incontro una serie di problemi, che ovviamente risolvo.

Sorgenti: documentazione

Prima stesura: 1 settembre 2004.

Unità di misura

Fino ad ora, le unità di misura degli elementi sono sempre stati i pixel, nella quantià di 72 pixel per pollice (oppure 28 e rotti per centimetro). In realtà, è piuttosto scomodo (per me) ragionare in pixel, soprattutto quando voglio poi ottenere una stampa della copertina, da ritagliare ed inserire in un jewel box o slim case (che riesco a misurare in centimetri). Mi prefiggo quindi di permettere all'utente di scegliere le unità di misura correnti, e di presentare poi all'utente solo misure espresse nelle unità presente.

figura 01

figura 01

Per prima cosa, certo un meccanismo perché l'utente possa scegliere le unità. Scelgo di modificare il pannello in cui si imposta la griglia, e lo modifico di conseguenza aggiungendo un menu pop up.

Ho deciso (per il momento) di utilizzare tre unità di misura: i centimetri, i pixel e i pollici. Queste tre unità sono appunto quelle presenti nel pannello. Ho definiti tre costanti per caratterizzarle, ed ho attributo i loro valori come tag nelle tre voci di menu. In questo modo, il metodo che risponde ad una operazione dell'utente sopra il controllo è molto semplice:

#define        UNITS_CM        101
#define        UNITS_INCHES    102
#define        UNITS_PIXELS    103

- (IBAction)
updateUnits:(id)sender
{
    int        newU = [[ sender selectedItem ] tag ] ;
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    // non dovrebbe succedere mai...
    if ( cvrView == nil ) return ;
    [cvrView setCurUnits: newU ] ;
    // faccio finta che sia cambiata la selezione perche' cosi'
    // sia ggiornano tutte le finestre ausiliarie
    [ [NSNotificationCenter defaultCenter]
        postNotificationName:@"CoverViewChangedSelection"
        object: cvrView ];
}

All'interno della classe CoverView ho introdotto una nuova variabile d'istanza, curUnits, che conserva appunto l'unità di misura corrente; tale variabile è inizialmente impostata in pixel, ed è caratteristica della CoverView: finestre differenti possono avere unità di misura differenti.

Da notare la presenza dell'ultima istruzione, che scatena il meccanismo delle notifiche e l'aggiornamento di tutte le finestre ausiliarie. Ed infatti, ad un cambio di unità di misura, occorre modificare tutte le rappresentazioni delle grandezze presenti. La prima grandezza presente è proprio la spaziatura della griglia, presente all'interno dello stesso pannello.

Ecco quindi che il metodo che effettua l'aggiornamento del contenuto del pannello in seguito ad un qualche cambiamento della finestra in primo piano:

- (IBAction)
updateGrid:(id)sender
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    float        sliderValue = [ sender floatValue ] ;
    float        tmpVal ;
    [ gridSpacing setFloatValue: sliderValue ];
    [ sliderSpacing setFloatValue: sliderValue ];
    // non dovrebbe succedere mai...
    if ( cvrView == nil ) return ;
    // aggiusto le variabili della griglia
    [ cvrView setGridLineColor: [gridColor color ]];
    [ cvrView setSnapToGrid: ([gridSnap state] == NSOffState ? NO : YES) ] ;    
    // converto sempre e comunque in pixel
    tmpVal = convertMeasure( sliderValue, [cvrView curUnits ], UNITS_PIXELS );
    [ cvrView setGridSpacing: tmpVal ];
    [ cvrView setShowGrid: ([showGrid state] == NSOffState ? NO : YES) ] ;
    // ridisegno la griglia
    [ cvrView setNeedsDisplay: YES ];
}

In questo metodo ho fatto collassare sia l'aggiornamento dello slider e del capo testo collegato (prima esisteva un metodo separato). La modifica principale è tuttavia nell'introduzione della funzione convertMeasure(.), che appunto effettua la conversione della grandezza da una unità di misura ad un'altra. Il valore da convertire e le due unità di misura (di partenza e d'arrivo) sono specificate come parametri. La mia idea infatti è di utilizzare i pixel come misura interna per ogni operazione (dopo tutto, il tracciamento degli elementi avviene in pixel), per cui ogni grandezza è internamente espressa in pixel: la loro modifica e presentazione ha quindi sempre bisogno di una preventiva operazione di conversione.

La funzione di conversione si trova all'interno del file djZeroUtils.m, dal momento che sarà utilizzata da vari oggetti presenti all'interno dell'applicazione. La funzione è piuttosto semplice, ma solo perché si trova già tutto pronto:

#define        CVT_P2I        72.0F
#define        CVT_C2I        2.54F
#define        CVT_P2C        (CVT_P2I / CVT_C2I)

static    float    cvtFactors[] = {
// cm                in                pix
    1.0F,            1.0F/CVT_C2I,    CVT_P2C,         // cm
    CVT_C2I,        1.0F,            CVT_P2I,         // in
    1.0F/CVT_P2C,    1.0F/CVT_P2I,    1.0F        } ;    // pix

float
convertMeasure( float meas, int    fromUnit, int toUnit )
{
    int    rr = fromUnit - UNITS_CM ;
    int    cc = toUnit - UNITS_CM ;
    float    cf ;
    if ( rr < 0 || rr > 2 ) return ( 0 );
    if ( cc < 0 || cc > 2 ) return ( 0 );
    cf = cvtFactors[rr *3 + cc];
    return ( cf * meas );
}

In primo luogo, mi servono tre fattori di conversioni, tra centimetri, pixel e pollici. Poi costruisco una matrice che contenga tutti i moltiplicatori per passare da una unità all'altra. La funzione ricava l'opportuno indice di riga e di colonna per determinare il moltiplicatore all'interno della matrice; poi moltiplica la grandezza per tale fattore e restituisce il valore: più difficile da spiegare che da fare.

figura 02

figura 02

C'è un altro pannello che risente dell'introduzione delle unità di misura, ovvero il pannello delle informazioni relative ad un elemento. Qui occorre convertire di volta in volta lo spessore della linea, il punto di origine e la dimensione dell'elemento. Per ricordare all'utente quali siano le unità di misura corrente, ho modificato la finestra in modo che tale informazione sia ben presente.

Come problema accessorio, ci sono da impostare correttamente i limiti dello slider (in pixel, una linea deve essere compresa tra zero e dieci pixel, ma in centimetri o pollici il limite superiore va modificato di conseguenza). Ecco quindi che il metodo di aggiornamento della finestra ElemInfoWinCtl è stato modificato:

- (void)
updateWindowInfo
{
    float    tmpVal ;
    int        curUnit ;
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;

    curUnit = [cvrView curUnits ] ;
    [ curUnits setStringValue: stringForUnits(curUnit) ];
    // predispongo gli elementi che corrispondono agli attributi comuni
    ...
    // limite massimo e minimo nelle unita' correnti
    [lineWidthSlider setMinValue: 0 ];
    tmpVal = convertMeasure( 10.0, UNITS_PIXELS, curUnit );
    [lineWidthSlider setMaxValue: tmpVal ];
    // valore corrente dello slider
    tmpVal = convertMeasure( [ theSelectedElem cceLineWidth], UNITS_PIXELS, curUnit );
    [lineWidthSlider setFloatValue: tmpVal ];
    [lineWidthTextField setFloatValue: tmpVal ];
    ...
    // prima calcolo il valore secondo le unita', poi lo assegno
    tmpVal = convertMeasure( [theSelectedElem drawPoint].x, UNITS_PIXELS, curUnit );
    [xTextField setStringValue: [ NSString stringWithFormat: @"%f", tmpVal ] ];
    tmpVal = convertMeasure( [theSelectedElem drawPoint].y, UNITS_PIXELS, curUnit );
    [yTextField setStringValue: [ NSString stringWithFormat: @"%f",tmpVal ] ];
    tmpVal = convertMeasure( [theSelectedElem localSize].width, UNITS_PIXELS, curUnit );
    [widthTextField setStringValue: [ NSString stringWithFormat: @"%f", tmpVal ] ];
    tmpVal = convertMeasure( [theSelectedElem localSize].height , UNITS_PIXELS, curUnit );
    [heightTextField setStringValue: [ NSString stringWithFormat: @"%f", tmpVal ] ];
    ...
}

In pratica, prima di presentare ogni grandezza, è eseguita la sua conversione nelle unità correnti della CoverView, passando attraverso la funzione convertMeasure(.). In più, c'è la visualizzazione di una stringa (prodotta dalla funzione stringForUnits(.) nel file djZeroUtils.m) che indica l'unità di misura corrente.

Va da sé che anche l'impostazione di nuovi valori agli elementi deve passare attraverso le operazioni di conversione:

- (IBAction)
changedElem:(id)sender
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    float        p1, p2 ;
    // a seconda dell'attributo modificato
    switch ( [ sender tag ] ) {
    ...
    case 16 :     // origine dell'elemento
    case 17 :
        if ( [ theSelectedElem cceIsLocked] ) break ;
        // riconverto la dimensione in pixel
        p1 = convertMeasure( [xTextField floatValue ] , [cvrView curUnits ], UNITS_PIXELS ) ;
        p2 = convertMeasure( [yTextField floatValue ] , [cvrView curUnits ], UNITS_PIXELS ) ;
        [ theSelectedElem setDrawPoint: NSMakePoint( p1, p2) ];
        [ theSelectedElem calcLocalTranform ];
        break ;
        ...
    }
    ...
}

Le dimensioni contano

Fino ad ora, le dimensioni di una copertina sono sempre state le stesse (una ventina di centimetri per lato, più o meno). In realtà si può avere necessità di dimensioni differenti, per cui mi sono posto il problema di cambiare la dimensioni della CoverView, la vista che appunto contiene tutti gli elementi di una copertina.

figura 03

figura 03

Ho per tanto costruito un nuovo pannello, molto semplice, per impostare le dimensioni della CoverView in larghezza ed altezza. Il controllore è la classe CvrSizeWinCtl e fa parte della categoria delle finestre accessorie: partecipa quindi al meccanismo delle notifiche centralizzate realizzato da AppDelegate; inoltre, presentando grandezze, deve essere realizzato il meccanismo di conversione delle unità.

figura 04

figura 04

Ho aggiunto una voce di menu, anzi, ho riorganizzato l'intero menu Tools per ricordare anche che il pannello di impostazione della griglia è utile anche per cambiare le unità di misura. Ovviamente, le voci fanno riferimento ad un metodo di AppDelegate che va ad aprire l'istanza condivisa (e statica) delle classi relative ai pannelli GridDlgWinCtl e CvrSizeWinCtl.

Il funzionamento del pannello si basa su una (nuova) variabile d'istanza curViewSize (è un NSSize) della CoverView, che appunto conserva la dimensioni correnti della view. Per aggiustare la finestra con i valori correnti, alla sua apertura, si usa il metodo seguente:

- (void)
updateWindow: (CoverView *) cvrView
{
    float    tmpVal ;
    int        curUnit ;
    if ( cvrView == nil)
    {
        [ self setControlsEnabled: NO ];
        return ;
    }
    // se arrivo qui, c'e' una coverView in fronte a tutti
    // predispongo la finestra in accordo
    // abilito tutti i controlli
    [ self setControlsEnabled: YES ];
    // imposto i vari valori della griglia, utilizzando
    // le unita' correnti
    curUnit = [cvrView curUnits ] ;
    [ showUnits setStringValue: stringForUnits(curUnit) ];
    tmpVal = convertMeasure( [ cvrView curViewSize].width,
                            UNITS_PIXELS, curUnit );
    [ widthText setFloatValue: tmpVal ];
    tmpVal = convertMeasure( [ cvrView curViewSize].height,
                            UNITS_PIXELS, curUnit );
    [ heightText setFloatValue: tmpVal ];
}

I due campi che presentano larghezza ed altezza sono riempiti con i valori che si ottengono dalla variabile d'istanza curViewSize opportunamente convertita.

Dall'altra parte, quando l'utente imposta nuovi valori per tali grandezze, si procede in senso inverso:

- (IBAction)
setUnits:(id)sender
{
    CoverView * cvrView = [AppDelegate getTheDrawView ] ;
    float    curW = [ widthText floatValue] ;
    float    curH = [ heightText floatValue] ;
    // non dovrebbe succedere mai...
    if ( cvrView == nil ) return ;
    // converto le dimensioni in pixel
    curW = convertMeasure( curW, [cvrView curUnits ], UNITS_PIXELS ) ;
    curH = convertMeasure( curH, [cvrView curUnits ], UNITS_PIXELS ) ;
    // imposto le dimensioni
    [ cvrView setCurViewSize: NSMakeSize( curW, curH) ];
    // bisogna tenere conto del fattore di zoom corrente
    curW *= [ cvrView curZoomFactor ];
    curH *= [ cvrView curZoomFactor ];
    [ cvrView setFrameSize: NSMakeSize( curW, curH) ];
    [ cvrView setNeedsDisplay: YES ];
    // chiudo questa finestra
    [ [ CvrSizeWinCtl sharedCvrSizeWinCtl ] close ] ;
}

Dopo aver impostato i nuovi valori, devo aggiustare il frame della vista. Nel farlo, devo tenere conto del fattore di zoom corrente, che altrimenti succedono dei pasticci. Ho dovuto quindi aggiungere una variabile d'istanza nella CoverView che tenesse appunto conto di questo fattore.

Template

Fino ad ora, ogni lavoro svolto su di una copertina veniva perduto alla chiusura della finestra: non c'era la possibilità di salvare il lavoro svolto. Con i template, introduco la possibilità di salvare l'aspetto di una copertina; non si salva una copertina legata ad una determinato volume, ma un aspetto generale della copertina, che può essere applicato a vari volumi (con i campi di testo automatico che sono aggiornati di conseguenza).

Un template è quindi costituito da una serie di informazioni relative alla CoverView (dimensioni, unità di misura, eccetera) e dall'elenco degli elementi che appartengono alla view: la loro natura, la loro disposizione, le loro caratteristiche. Intendo salvare tutte queste informazioni all'interno di un file, e di permettere la costruzione di una CoverView a partire dal contenuto di questi file.

In passato, avevo utilizzato il protocollo NSCoding per salvare il contenuto di un catalogo, sia direttamente che indirettamente, dal momento che la procedura è gestita in automatico dalla classe NSDocument. Qui riprendo appunto il concetto di NSCoding, per aggiungere ad ogni elemento i due metodi necessari all'archiviazione ed al ripristino di oggetti su file:

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

Ricordo che il primo metodo serve a salvare la struttura dati di un oggetto, mentre il secondo permette di ripristinare tale struttura a partire dai dati salvati.

Ci sono alcune novità rispetto a quanto visto a suo tempo. L'archiviazione effettuata nel capitolo 16 era seriale: la struttura di ogni oggetto era convertita in un flusso lineare di dati. Tali dati, una volta scritti in un determinato ordine, richiedevano di essere estratti nel medesimo ordine, pena il fallimento dell'operazione. A partire dalla versione 10.2 del sistema operativo, il meccanismo di salvataggio preferenziale è diventato un altro (la serializzazione è consigliata solo per mantenere compatibilità all'indietro con file e con precedenti installazioni), ovvero tramite chiave e valore. Ogni grandezza è adesso salvata all'interno di un file non più in rigoroso ordine sequenziale, ma attraverso una coppia chiave-valore (una sorta di dizionario); ciò permette il ripristino dell'oggetto anche estraendo i dati in ordine differente.

Nel mio caso, il metodo di codifica principale è ovviamente quello relativo a CCE_BasicForm:

- (void)
encodeWithCoder:(NSCoder *)encoder
{
    if ( [encoder allowsKeyedCoding] )
    {        
        // ogni variabile e' codificata con un opportuno identificatore,
        // formato dallo id seguito dal nome
        // alcune variabili si saltano perche' ricostruibili dalle altre
        [ encoder encodeInt32: objID forKey: keyStr( 0, @"objID") ] ;
        [ encoder encodePoint: drawPoint forKey: keyStr(objID, @"drawPoint") ] ;
        [ encoder encodeFloat: rotAngle forKey: keyStr(objID, @"rotAngle") ] ;
        [ encoder encodeInt: numOfHdl forKey: keyStr(objID, @"numOfHdl") ] ;
        [ encoder encodeSize: localSize forKey: keyStr(objID, @"localSize") ] ;
        [ encoder encodeFloat: cceLineWidth forKey: keyStr(objID, @"cceLineWidth") ] ;
        [ encoder encodeObject: cceFillColor forKey: keyStr(objID, @"cceFillColor") ] ;
        [ encoder encodeObject: cceLineColor forKey: keyStr(objID, @"cceLineColor") ] ;
        [ encoder encodeBool: cceIsFilled forKey: keyStr(objID, @"cceIsFilled") ] ;
        [ encoder encodeBool: cceIsStroked forKey: keyStr(objID, @"cceIsStroked") ] ;
        [ encoder encodeBool: cceIsLocked forKey: keyStr(objID, @"cceIsLocked") ] ;
    }
}

Solo le sottoclassi che aggiungono variabili d'istanza hanno bisogno di sovrascrivere tale metodo; ad esempio CCE_ElemGroup:

- (void)
encodeWithCoder:(NSCoder *)encoder
{
    [ super encodeWithCoder: encoder ] ;
    // devo aggiungere la codifica di tutti gli elementi
    if ( [encoder allowsKeyedCoding] )
    {        
        // basta codificare solo lo array
        [ encoder encodeObject: elemArray forKey: keyStr(objID, @"elemArray") ] ;
    }
}

La funzione keyStr(.) produce una stringa che identifica in maniera univoca la grandezza salvata (è normalmente composta dallo identificatore dell'elemento seguito dal nome della grandezza).

Per il ripristino dei dati, ci sono i metodi corrispondenti, per CCE_BasicForm:

- (id)
initWithCoder:(NSCoder *)decoder
{
if ( [decoder allowsKeyedCoding] )
    {
        objID = [decoder decodeInt32ForKey: keyStr( 0, @"objID") ];    
        drawPoint = [decoder decodePointForKey: keyStr( objID, @"drawPoint") ];
        rotAngle = [decoder decodeFloatForKey: keyStr( objID, @"rotAngle") ];
        numOfHdl = [decoder decodeIntForKey: keyStr( objID, @"numOfHdl") ];    
        localSize = [decoder decodeSizeForKey: keyStr( objID, @"localSize") ];
        [ self calcLocalTranform ];
        [ self setTheDrawPath: [NSBezierPath bezierPathWithRect:
            NSMakeRect(0, 0, localSize.width, localSize.height) ] ];
        [ self buildHdlList ];
        cceLineWidth = [decoder decodeFloatForKey: keyStr( objID, @"cceLineWidth") ];
        [ self setCceFillColor: [ decoder decodeObjectForKey: keyStr( objID, @"cceFillColor") ] ];
        [ self setCceLineColor: [ decoder decodeObjectForKey: keyStr( objID, @"cceLineColor") ] ];
        cceIsFilled = [decoder decodeBoolForKey: keyStr( objID, @"cceIsFilled") ];    
        cceIsLocked = [decoder decodeBoolForKey: keyStr( objID, @"cceIsLocked") ];    
        cceIsStroked = [decoder decodeBoolForKey: keyStr( objID, @"cceIsStroked") ];    
        cceIsSelected = FALSE ;
    }
    return ( self );
}

Per la classe CCE_ElemGroup, invece:

- (id)
initWithCoder:(NSCoder *)decoder
{
    [ super initWithCoder: decoder ] ;
    if ( [decoder allowsKeyedCoding] )
    {
        [ self setElemArray: [ decoder decodeObjectForKey: keyStr(objID, @"elemArray") ] ];
    }
    return ( self );
}

Devo inoltre rendere la classe CoverView compatibile col protocollo NSCoding, scrivendo i due metodi seguenti:

- (void)
encodeWithCoder:(NSCoder *)encoder
{
    if ( [encoder allowsKeyedCoding] )
    {
        [ encoder encodeObject: curTemplateName forKey: @"tmpl_Name" ] ;
        [ encoder encodeFloat: gridSpacing forKey: @"tmpl_gridSpacing"] ;
        [ encoder encodeBool: showGrid forKey: @"tmpl_showGrid"] ;
        [ encoder encodeBool: snapToGrid forKey: @"tmpl_snapToGrid"] ;
        [ encoder encodeObject: gridLineColor forKey: @"tmpl_gridLineColor" ] ;
        [ encoder encodeInt32: nextObjId forKey: @"tmpl_nextObjId"] ;
        [ encoder encodeInt: curUnits forKey: @"tmpl_curUnits"] ;
        [ encoder encodeSize: curViewSize forKey: @"tmpl_curViewSize"] ;
        [ encoder encodeObject: theElements forKey: @"tmpl_Elements"] ;
    }
}

- (id)
initWithCoder:(NSCoder *)decoder
{
    if ( [decoder allowsKeyedCoding] )
    {
        // in realta' l'ordine non ha importanza
        [ self setCurTemplateName: [ decoder decodeObjectForKey: @"tmpl_Name" ] ];
        gridSpacing = [decoder decodeFloatForKey: @"tmpl_gridSpacing" ];    
        showGrid = [decoder decodeBoolForKey: @"tmpl_showGrid" ];    
        snapToGrid = [decoder decodeBoolForKey: @"tmpl_snapToGrid" ];    
        [ self setGridLineColor: [ decoder decodeObjectForKey: @"tmpl_gridLineColor" ] ];
        nextObjId = [decoder decodeInt32ForKey: @"tmpl_nextObjId" ];    
        curUnits = [decoder decodeIntForKey: @"tmpl_curUnits" ];    
        curViewSize = [decoder decodeSizeForKey: @"tmpl_curViewSize" ];    
        [ self setCurCreatingElem: nil ] ;
        [ self setCurSelectRect: nil ] ;
        editingInProgress = nil ;
        [ self setTheElements: [ decoder decodeObjectForKey: @"tmpl_Elements" ] ];
    }
    return ( self );
}

Per completare la faccenda, ho scritto un metodo che aggiusta l'aspetto di una CoverView in base ai dati che riesce ad estrarre da un file (o con valori di default se il file non esiste):

- (void)
loadFromTemplate: (NSString *) templatePath
{
    CoverView    * tmpCover ;
    [ winCtl arrangeZoom: 0 ];
    // se il template e' effettivo
    if ( templatePath )
    {
        // estraggo i dati dal template
        tmpCover = [NSKeyedUnarchiver unarchiveObjectWithFile: templatePath ];        
        [ theElements setElemArray: [ [tmpCover theElements ] elemArray ] ] ;
        [ [ self theElements] setOwnerView: self ] ;
        // aggiusto tutti gli altri valori della coverView
        [self setCurTemplateName: [tmpCover curTemplateName ] ];
        [self setGridSpacing: [tmpCover gridSpacing ] ];
        ...
    }
    else
    {
        // non ho un template, riempio con valori di default
        [ theElements setElemArray: [ NSMutableArray array ] ] ;
        [ [ self theElements] setOwnerView: self ] ;
        // aggiusto tutti gli altri valori della coverView
        [self setCurTemplateName: @"---" ];
        [self setGridSpacing: 20 ];
        ...
    }
    // aggiorno il testo automatico
    [ [ theElements elemArray] makeObjectsPerformSelector:
            @selector(updAutoText:) withObject: [[ winCtl document] dataSource] ] ;
    // aggiorno l'intera finestra
    [self setNeedsDisplay: YES ];
}

Adesso devo costruire l'interfaccia per scatenare tutto questo meccanismo.

Dove sono i template

Mi è venuta l'idea di posizionare i template non tanto sparsi per lo hard disk, ma direttamente all'interno dell'applicazione.

figura 06

figura 06

Come dovrebbe essere noto, una applicazione, il file Nome.app, in realtà è una directory (Bundle, secondo la terminologia Apple). All'interno di essa sono disponibili tutte le risorse necessarie all'applicazione, icone, immagini, suoni, file nib, eccetera, ed il codice dell'applicazione stessa. Mi pare un buon posto dove inserire qualche template standard per il disegno di copertine. Ovviamente l'utente potrà scegliere uno di questi template standard, ma anche costruirsene di nuovi e posizionarli dove più gli piace. Tuttavia, i template standard avranno la caratteristica di comparire in un menu pop up all'interno della finestra.

figura 05

figura 05

Ho quindi riarrangiato la finestra CoverWinCtl, spostando in alto la barra per lo zoom (e i soliti infami pulsanti per debug), aggiungendo un menu pop up. In questo menu ho messo alcune voci standard, per caricare e salvare template, ed il richiamo di un template (empty), cioè completamente vuoto. Di seguito inserisco tutte le voci relative ai template presenti all'interno dell'applicazione.

Aggiungo quindi i template all'interno del progetto XCode, in modo che in sede di compilazione e costruzione dell'applicazione, i template stessi siano copiati all'interno dell'applicazione.

In sede di preparazione della finestra, all'interno del metodo windowDidLoad, aggiusto il menu pop up:

- (void)
windowDidLoad
{
    NSArray                * lt ;
    int                    i ;
    NSString            * tmpl ;
    ...
    [super windowDidLoad ];
    ...
    [[self window] makeFirstResponder:cvrView];
    // cerco tutti i file template
    lt = [ [NSBundle mainBundle] pathsForResourcesOfType:@"cdt" inDirectory: @"/" ];
    // e li inserisco nel popup button
    for ( i = 0 ; i < [ lt count ] ; i++ )
    {
        // metto solo il nome, senza extension
        tmpl = [ [ lt objectAtIndex: i ] lastPathComponent ];
        [ tmplList addItemWithTitle: [ tmpl stringByDeletingPathExtension ] ];
        // aggiusto il meccanismo target/Action
        [ [ tmplList itemAtIndex: ( 5 + i) ] setTarget: self ] ;
        [ [ tmplList itemAtIndex: ( 5 + i) ] setAction: @selector(loadTemplateFromFile:) ] ;
    }
    // alla partenza, nulla e' selezionato nel pop up menu
    [ tmplList selectItem: nil ] ;
}

I template sono dei file con estensione .cdt. Il metodo pathsForResourcesOfType: produce un NSArray che contiene l'elenco di tutti i file di data estensione all'interno della directory indicata (nel mio caso, la root dell'applicazione, non quella generale del volume...); le istruzioni successive spazzolano questo vettore ed aggiungono il nome del file (senza estensione, che tanto so qual è) come elemento del menu. Inoltre, impostano queste voci affinché invochino il metodo loadTemplateFromFile: quando sono selezionate. L'ultima istruzione del metodo windowDidLoad fa in modo che nessuna voce del menu sia selezionata.

La voce Load Template è collegata (direttamente all'interno di Interface Builder) a questo metodo:

- (IBAction)
templateLoad: (id)sender
{
    NSOpenPanel        *oPanel = [ NSOpenPanel openPanel ] ;
    int        risposta ;
    // imposto un po' di caratteristiche del dialogo
    [ oPanel setTitle:@"Load Template" ];
    [ oPanel setPrompt:@"Load Template" ];
    // niente directory, un solo file alla volta
    [ oPanel setCanChooseDirectories:NO ];
    [ oPanel setCanChooseFiles:YES ];
    [ oPanel setAllowsMultipleSelection:NO ];
    [ oPanel setResolvesAliases:YES ];
    // posso selezionare solo i file che hanno estensione cdt
    risposta = [ oPanel runModalForTypes: [ NSArray arrayWithObjects: @"cdt" ] ];
    // se c'e' una selezione effettiva...
    if ( risposta == NSOKButton )
    {
        // ... recupero il nome del file selezionato
        NSArray *filesToOpen = [ oPanel filenames ];
        NSString *aFile = [ filesToOpen objectAtIndex: 0 ];
        // ed uso il template per costruire la view
        [ cvrView loadFromTemplate: aFile ];
    }
    [ tmplList selectItem: nil ] ;
}

Si limita a presentare un dialogo di caricamento di un file (niente selezioni multiple) avente estensione .cdt. In caso di individuazione corretta di un file, chiama il metodo loadFromTemplate: della CoverView.

Speculare il metodo associato alla voce Save Template:

- (IBAction)
templateSave: (id)sender
{
    NSSavePanel    *sPanel = [ NSSavePanel savePanel ] ;
    // personalizzo il pannello di salvataggio
    [ sPanel setTitle:@"Save template" ];
    // i file salvati avranno l'estensione lscat
    [ sPanel setRequiredFileType: @"cdt" ];
    if ( [ sPanel runModal ] == NSOKButton )
    {
        // recupero il nome del file
        NSString *aFile = [ sPanel filename ] ;
        // archivio l'intera struttura dati
        [NSKeyedArchiver archiveRootObject: cvrView toFile: aFile ];
    }
    [ tmplList selectItem: nil ] ;
}

Qui si apre un pannello standard per il salvataggio di un file; in caso di riuscita, si comincia la procedura di archiviazione a partire dalla CoverView. A cascata, il salvataggio interessa tutti gli elementi (perché la CoverView salva anche il vettore theElements).

Rimane il metodo principlae per il caricamento di un template a partire dalla CoverWinCtl:

- (IBAction)
loadTemplateFromFile: (id)sender
{
    NSString    * selTmpl ;
    NSString    * itTit ;
    // recupero il nome del template
    itTit = [ sender title ] ;
    // recupero il percorso completo del template
    selTmpl = [ [NSBundle mainBundle] pathForResource: itTit ofType:@"cdt" ];
    // carico il template
    [ cvrView loadFromTemplate: selTmpl ];
}

Si tratta di recuperare il nome del template e di produrre il path completo del file in cui il template è contenuto; una volta fatto ciò, il lavoro è proseguito dal già visto loadFromTemplate:.

Altre modifiche

Nel codice ci sono altre modifiche.

Ho spostato in djZeroUtils.m la funzione che produce un NSDictionary con lo stile ed il font di default per i campi testo. In questo modo può essere chiamata da vari punti, in particolare dall'interno della classe CatDataSrc per produrre testo automatico correttamente formattato anche in assenza di stili predefiniti.

Ho completato la classe CCE_ElemGroup sovrascrivendo tutti i metodi per l'impostazione degli attributi dell'elemento, in modo che l'attributo sia propagato ai componenti del gruppo. In questo modo, operando ad esempio sulla finestra ElemInfoWinCtl si riesce a modificare in un colpo solo, ad esempio, lo spessore delle linee di tutti gli elementi appartenenti al gruppo.

Ci saranno sicuramente altre modifiche qui e lì, ma le ho dimenticate. E comunque, non saranno così importanti...

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