MaCocoa 046

Capitolo 046 - Sposta, Allinea, Raggruppa e Blocca

Questo capitolo, piuttosto lungo, realizza alcune tipiche funzioni presenti in un programma di disegno vettoriale: lo spostamento nell'ordine di visualizzazione; l'allineamento di più elementi secondo un certo criterio; la creazione e la distruzione di gruppi di elementi; il blocco e lo sblocco di elementi, in modo che non siano spostati o mossi accidentalmente. Tante cose, ma tutte piuttosto semplici.

Documentazione Apple.

Primo inserimento: 22 aprile 2004

Tre cose

Ma prima di cominciare, voglio predisporre un po' di cose.

figura 01

figura 01

La prima è una quasi indispensabile modifica all'interfaccia, l'inserimento di un menu per effettuare tutte le operazioni prima descritte. Ho chiamato il menu Tool ed ho aggiunto una serie di voci del menu e di sottomenu. È importante notare come ho assegnato alle varie voci un tag di diverso valore. Ho quindi una voce Group con tag 200 ed una voce Ungroup con tag 201; nei due sottomenu per l'allineamento e per lo spostamento dell'ordine di visualizzazione, le voci hanno tag che vanno da 1 a 4 o 5. Tutto questo mulinare di tag serve per associare un solo metodo a più di una voce. Ad esempio, tutte le voci relative all'allineamento invocano lo stesso metodo. Poi, all'interno del metodo, chiederò qual è il tag dell'elemento chiamante per capire l'operazione da effettuare. In questo modo evito l'eccessivo proliferare di metodi, raggruppando funzionalità simili (o opposte) all'interno dello stesso blocco di codice.

figura 02

figura 02

La seconda funzionalità che ho aggiunto è un meccanismo per capire come sono organizzati gli elementi all'interno della vista. Ho quindi associato ad uno dei pulsanti di servizio presenti sulla finestra un metodo da eseguire quando si fa clic su di esso:

- ( void)
clicBtn02: (id)sender
{
    [ [ cvrView theElements] printMeWithLevel: 0 ];
}

Per ogni elemento ho quindi aggiunto il metodo printMeWithLevel: che cerca di descrivere come sia costruito l'elemento. Detto così, sembra un duplicato del metodo generico description:, che appunto riporta alcune informazioni sull'oggetto. Qui invece ho aggiunto un parametro, il livello, che aggiunge un po' di spazi davanti alla riga che lo descrive, in dipendenza del fatto che l'elemento appartenga o meno ad un gruppo. Il metodo per un rettangolo è piuttosto semplice:

- (void)
printMeWithLevel: (int) lev
{
    int i;
    char    blankChar[40];
    NSString    * msg ;
    if ( lev > 10 ) lev = 10 ;
    for ( i = 0; i < lev*4 ; i ++ )
        blankChar[i]= ' ';
    blankChar[lev*4] = 0 ;
    msg = [ NSString stringWithCString: blankChar];
    NSLog( @"%@ -> Rect (R: %7.2f %7.2f S: %7.2f %7.2f) Lw: %6.2f, F: %2d, S: %2d, Sel: %2d, Lk: %2d", msg,
        elemLimit.origin.x, elemLimit.origin.y,
        elemLimit.size.width, elemLimit.size.height,
        cceLineWidth, cceIsFilled,cceIsStroked, cceIsSelected, cceIsLocked );
}

La prima operazione del metodo consiste nel costruire una stringa di spazi, pari al numero di livello per quattro. Poi, la funzione NSLog scrive sulla console una stringa con un po' informazioni relative: le coordinate del rettangolo, lo spessore della linea, se l'elemento è selezionato, cose del genere.

Il metodo diventa più interessante per un elemento CCE_ElemGroup:

- (void)
printMeWithLevel: (int) lev
{
    int        i , numElem ;
    char    blankChar[40];
    CCE_BasicForm * elem ;
    NSString    * msg ;
    if ( lev > 10 ) lev = 10 ;
    for ( i = 0; i < lev*4 ; i ++ )
        blankChar[i]= ' ';
    blankChar[lev*4] = 0 ;
    msg = [ NSString stringWithCString: blankChar];
    NSLog( @"%@ -> ElemGroup of %d elements (R: %7.2f %7.2f S: %7.2f %7.2f) Sel: %2d, Lk: %2d",
        msg, [elemArray count],
        elemLimit.origin.x, elemLimit.origin.y,
        elemLimit.size.width, elemLimit.size.height,
        cceIsSelected, cceIsLocked );

    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem printMeWithLevel: (lev+1) ];
    }
}

Oltre alle operazioni standard, poi c'è un ciclo su tutti gli elementi del gruppo, per i quali si ripete la procedura, ma partendo da un livello più alto di uno. Si ottiene così, in risposta ad un clic sul pulsante, una descrizione un po' criptica ma indentata per livello degli elementi presenti. La cosa sarà molto utile più avanti per capire se il meccanismo di raggruppamento/separazione funziona correttamente.

2004-04-19 20:56:03.845 CdCat2[1054] -> ElemGroup of 11 elements (R:    0.00    0.00 S:    1.00    1.00) Sel: 0, Lk: 0
2004-04-19 20:56:03.845 CdCat2[1054]     -> Line (P: 480.46 70.86 P: 480.46 405.35) Lw: 1.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.846 CdCat2[1054]     -> Line (P: 89.29 70.86 P: 89.29 405.35) Lw: 1.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.846 CdCat2[1054]     -> Rect (R: 70.86 70.86 S: 428.02 334.48) Lw: 1.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.846 CdCat2[1054]     -> Line (P: 513.06 405.35 P: 541.41 405.35) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.846 CdCat2[1054]     -> Line (P: 28.35 405.35 P: 56.69 405.35) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.846 CdCat2[1054]     -> Line (P: 498.89 419.52 P: 498.89 447.87) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.847 CdCat2[1054]     -> Line (P: 70.86 419.52 P: 70.86 447.87) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.847 CdCat2[1054]     -> Line (P: 513.06 70.86 P: 541.41 70.86) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.847 CdCat2[1054]     -> Line (P: 28.35 70.86 P: 56.69 70.86) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.848 CdCat2[1054]     -> Line (P: 498.89 28.35 P: 498.89 56.69) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0
2004-04-19 20:56:03.848 CdCat2[1054]     -> Line (P: 70.86 28.35 P: 70.86 56.69) Lw: 0.00, F: 0, S: 1, Sel: 0, Lk: 0

Infine, ci sono alcune modifiche a livello di CCE_ElemGroup. Ho scritto un metodo specifico della classe, utile in momenti successivi.

- (NSMutableArray *)
getSelectedGraphics
{
    NSMutableArray * selArr = [[ NSMutableArray alloc] init ] ;
    int        i , numElem ;
    CCE_BasicForm * elem ;
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        if ([ elem cceIsSelected] )
            [ selArr addObject: elem ];
    }    
    return ( selArr );
}

Molto semplicemente, costruisco un vettore con tutti gli elementi correntemente selezionati.

Poi, ho rimosso la versione di setCceIsSelected: specifica della classe, trasformandola nel metodo cascadeSelection:, ed ho trasformato il metodo moveMeIfSelectBy: in moveSelectedElements:. Il motivo di tutto ciò sarà chiaro molto più avanti.

Blocco e sblocco

Può accadere talvolta di eseguire operazioni non volute, perché un elemento sul quale non volevamo operare era accidentalmente selezionato. In più, nel disegno di copertine, dovrei contare sul fatto che alcuni elementi grafici non dovrebbero essere manipolati per alcun motivo. Sto ad esempio parlando dei rettangoli che segnano i contorni della copertina stessa, delle linee di taglio, eccetera. Per facilitare le cose, ho introdotto il concetto di blocco di un elemento. Dico che un elemento può essere bloccato (Lock Elem), ed in tal caso non partecipa alle operazioni di movimento e spostamento, insomma, non può essere manipolato; normalmente, invece, l'elemento è sbloccato, è può essere manipolato a piacere. Ho racchiuso questo stato dell'elemento in una nuova variabile d'istanza di CCE_BasicForm, chiamata cceIsLocked. Questa variabile nasce con valore falso, ma può essere modificata con i soliti metodi accessor. A livello di interfaccia utente, ci sono due voci di menù che fanno riferimento ad un unico metodo:

- (void)
lockElems: (id)sender
{
    NSMutableArray * selArr ;
    int        numElem, i ;
    CCE_BasicForm * elem ;
    int        menuTag = [ sender tag ];

    selArr = [ theElements getSelectedGraphics ];
    numElem = [ selArr count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ selArr objectAtIndex: i ];
        if ( menuTag == 100 )
                [ elem setCceIsLocked: YES ];
        else    [ elem setCceIsLocked: NO ];
    }
    [ self setNeedsDisplay: YES ];
}

Il metodo raccoglie tutti gli elementi correntemente selezionati, e li costringe ad impostare il valore della variabile cceIsLocked secondo quanto deciso dall'utente; la volontà dell'utente è nota attraverso il valore del tag della voce di menu, che ho opportunamente assegnato ai valori 100 per il blocco e 101 per lo sblocco.

Va da sé che il metodo accessor va riscritto per la classe CCE_ElemGroup in modo da propagare la modifica fino in fondo della gerarchia.

- (void)
setCceIsLocked:(BOOL)newVal
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    //assegno il valore a me stesso
    [ super setCceIsLocked: newVal ];
    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem setCceIsLocked: newVal ];
    }
}

figura 03

figura 03

Ho preferito utilizzare un richiamo al metodo della superclasse piuttosto che la semplice assegnazione diretta del valore per buona pratica di programmazione object-oriented.

Ho anche inserito una modifica alla funzione ccex_drawHandleAt per evidenziare in qualche modo gli elementi bloccati da quelli liberi. Adesso ogni chiamata alla funzione richiede un parametro booleano che dice se l'elemento è bloccato o meno (il parametro serve perchè si tratta di una funzione, e in quanto tale, diversamente da un metodo di una classe, non fa parte di alcuna classe e tanto meno ha accesso alle variabili d'istanza):

void    
ccex_drawHandleAt( NSPoint where, BOOL how )
{
    // per ora la dimensione del manico e' fissa
    int        handleDim = 3 ;
    NSRect    hRect, rRect ;
    // disegno un rettangolo pieno attorno al punto
    hRect = NSMakeRect( where.x, where.y,2*handleDim, 2*handleDim );
    rRect = NSOffsetRect( hRect , -handleDim, -handleDim);
    hRect = NSOffsetRect(rRect, 1.0, 1.0);
    if ( how )
            [[NSColor darkGrayColor] set];
    else    [[NSColor lightGrayColor] set];
    NSRectFill(hRect);
    if ( how )
            [[NSColor lightGrayColor] set];
    else    [[NSColor darkGrayColor] set];
    NSRectFill(rRect);
}

In questo modo un elemento bloccato e selezionato presenta gli handle di un color grigio, piuttosto che il solito nero.

Spostamento di livello

Gli elementi presenti in una coverView sono conservati in un vettore, NSMutableArray; giocoforza, gli elementi hanno un loro ordine, che si riflette nell'ordine con cui sono disegnati. Ciò ha importanti conseguenze nel caso di elementi pieni, che coprono elementi disegnati in precedenza. Questa è una situazione tipica di tutti i programmi di disegno, e praticamente tutti i programmi di disegno possiedono un meccanismo per modificare l'ordine con cui sono rappresentati gli elementi grafici. In pratica, esistono quasi sempre quattro comandi, per spostare un elemento dietro a tutti gli altri (back), dietro di un livello (backward), avanti di un livello (forward), davanti a tutti gli altri (front). Le quattro voci di menu fanno riferimento ad uno stesso metodo, sendElemFB:, che sfrutta l'accennato meccanismo del tag per capire cosa fare.

- (void)
sendElemFB: (id)sender
{
    int        menuTag = [ sender tag ];
    switch ( menuTag ) {
    case 1 : [ theElements sendElemFrontAndBack: +2 ]; break ;
    case 2 : [ theElements sendElemFrontAndBack: +1 ]; break ;
    case 3 : [ theElements sendElemFrontAndBack: -1 ]; break ;
    case 4 : [ theElements sendElemFrontAndBack: -2 ]; break ;
    default :
        return ;
    }
    [ self setNeedsDisplay: YES ];
}

Ho convenuto quattro valori, tra -2 e +2, per indicare le quattro possibilità citate. Come spesso accade, il lavoro sporco viene compiuto da un altro metodo. Questo metodo, sendElemFrontAndBack:, è specifico della classe CCE_ElemGroup, e merita una discussione approfondita. Fondamentalmente, il metodo è diviso in due parti: prima mi chiedo in quale direzione mi devo spostare (in avanti o all'indietro), e poi eseguo gli spostamenti richiesti. Da notare che l'operazione sarà eseguita solo sugli elementi selezionati e non bloccati, quindi con le variabili d'istanza cceIsSelected a Vero e cceIsLocked a Falso.

- (void )
sendElemFrontAndBack: (int) howMuch
{
    int        i , numElem, newIdx ;
    CCE_BasicForm * elem ;
    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    // devo distinguere tra l'avanti e l'indietro
    if ( howMuch > 0 )
    {
        // sposto in avanti
    }
    else
    {
        // sposto indietro
    }
}

Ho diviso il commento in tre blocchi. Nel blocco precedente c'è lo scheletro di massima, di seguito gli altri due blocchi.

Gli spostamenti in avanti sono regolati dal seguente blocco di codice. Giova ricordare che spostare in avanti un elemento significa spostare la sua posizione all'interno del vettore degli elementi verso indici più bassi.

        for ( i = 0 ; i < numElem ; i ++ )
        {
            elem = [ elemArray objectAtIndex: i ];
            if ( [elem cceIsLocked] || ! [ elem cceIsSelected ] )
                continue ;
            // se arrivo qui, devo spostare l'elemento
            [ elem retain ];
            // comunque vada; lo tolgo dallo array
            [ elemArray removeObjectAtIndex: i ];
            // vedo cosa ne devo fare
            if ( howMuch == +1 )
                    newIdx = i-1 ;
            else    newIdx = 0 ;
            // adesso ho un nuovo indice, che aggiusto
            if ( newIdx < 0 ) newIdx = 0;
            [ elemArray insertObject: elem atIndex: newIdx ];
            [ elem release ];
        }

figura 04

figura 04

Il ciclo procede da elementi ad indice più basso verso elementi ad indice più alto. In questo modo lo spostamento avviene verso zone del vettore già esaminate, che non producono ulteriori effetti.

In primo luogo esamino l'elemento; se questo è bloccato, oppure non è selezionato, passo al successivo con l'istruzione continue. Se invece l'elemento è selezionato e libero, eseguo una operazione di retain e poi lo estraggo dal vettore. Il messaggio di retainè fondamentale (provate a toglierlo: l'applicazione muore al solito in maniera spettacolare). Infatti, rimuovendo l'elemento dal vettore, l'ambiente operativo gli invia un messaggio di release. Se l'elemento è posseduto solamente dal vettore (cosa che succede normalmente) il famoso contatore va a zero e l'elemento è deallocato (cosa sconveniente, visto che lo sto solo spostando).

figura 05

figura 05

A questo punto, devo rimpiazzare l'elemento estratto. Se lo spostamento è davanti a tutti gli altri, la cosa è semplice: basta inserirlo all'indice zero del vettore. Se invece lo sposto in avanti di una sola posizione, lo devo inserire ad un nuovo indice, che l'indice che aveva l'elemento in precedenza decrementato di 1. Dopo questa operazione, il vettore ha di nuovo numElem elementi, e si può procedere coll'elemento successivo. Occorre però ricordarsi di inviare un messaggio di release all'elemento. Infatti l'inserimento all'interno del vettore esegue anche una operazione di retain sull'elemento, che a questo punto è una volta di troppo.

Speculare il blocco di codice che esegue lo spostamento all'indietro, verso indici più alti. Qui è bene partire da elementi con indici alti per scender via via, in modo da evitare di considerare nuovamente elementi già spostati.

        for ( i = numElem-1 ; i >= 0 ; i -- )
        {
            elem = [ elemArray objectAtIndex: i ];
            if ( [elem cceIsLocked] || ! [ elem cceIsSelected ] )
                continue ;
            // se arrivo qui, devo spostare l'elemento
            [ elem retain ];
            // comunque vada; lo tolgo dallo array
            [ elemArray removeObjectAtIndex: i ];
            // vedo cosa ne devo fare
            if ( howMuch == -1 )
                    newIdx = i+1 ;
            else    newIdx = numElem-1 ;
            // adesso ho un nuovo indice, che aggiusto
            if ( newIdx >= numElem ) newIdx = numElem-1 ;
            [ elemArray insertObject: elem atIndex: newIdx ];
            [ elem release ];
        }

Da notare l'indice da utilizzare quando si vuole spostare l'elemento in fondo, che è uno di meno della dimensione originale del vettore. In effetti, ho appena estrato un elemento, quindi il vettore è più piccolo di uno.

In entrambi i pezzi di codice, prima dell'effettivo inserimento dell'elemento, c'è un controllo sul valore dell'indice; se troppo basso (minore di zero o superiore a numElem), occorre aggiustarlo. La cosa accade, ad esempio, quando intendo spostare in avanti un elemento che è già davanti a tutti (e che quindi ha l'indice zero; spostandoli in avanti sottraggo uno...).

Ecco, come ho scritto la frase precedente, mi sono accorto di un miglioramento immediato. In effetti, non occorre considerare il primo elemento del vettore negli spostamenti in aventi, e l'ultimo negli spostamenti all'indietro. Questi elementi non si sposteranno comunque. Ecco quindi che le istruzioni di controllo sull'indice non servono più, a patto che i due cicli for diventino rispettivamente:

        for ( i = 1 ; i < numElem ; i ++ )
        for ( i = numElem-2 ; i >= 0 ; i -- )

A questo punto, ci si può divertire a disegnare elementi sulla coverView, e poi, attraverso semplici comandi di menu, modificare l'aspetto del disegno, spostando avanti ed indietro gli elementi nell'ordine di disegno.

Allineamento

Un'altra operazione di manipolazione spesso presente nei programmi di disegno è quella di allineamento di un insieme di oggetti secondo diversi criteri: a destra, a sinistra, in alto, in basso, al centro (orizzontalmente), nel mezzo (verticalmente). Per non altre particolari ragioni se non complicarmi la vita, ho costruito quattro voci di menu per allineare secondo i primi quattro criteri, poi ho aggiunto una quinta voce di menu per aprire una finestra dove poter allineare gli elementi utilizzando anche gli altri due criteri rimasti. Le quattro voci di menu fanno riferimento allo stesso metodo elemAlign:, che distingue il suo comportamento attraverso il tag della voce di menu da cui il metodo è stato invocato.

Ma prima di procedere con l'allineamento di oggetti, mi occorrono un po' di metodi utili. Poiché in seguito mi occorre sapere quali sono le coordinate limite destra, sinistra, superiore ed inferiore di un elemento, mi premuro di definire i metodi che le calcolino per ogni elmento grafico. Sono piuttosto facili, ne riporto solo un paio:

- (NSNumber*) getElemBottom
{
    return ( [ NSNumber numberWithFloat: (elemLimit.origin.y + elemLimit.size.height)] );
}
- (NSNumber*) getElemMiddle
{
    return ( [ NSNumber numberWithFloat: (elemLimit.origin.y + 0.5 * elemLimit.size.height)] );
}

Ad esempio, la coordinata bottom, inferiore, di un elemento, è data dalla ordinata dell'origine del rettangolo cui è sommata la dimensione verticale (da ricordare qui che le coordinate veriticali, nella coverView, sono rovesciate). La coordinata middle, centrale in verticale, è banalmente l'ordinata più metà dell'altezza. I più accorti dei lettori potranno chiedersi perché diavolo costruisco un oggetto della classe NSNumber per fare questa semplice operazione, dove un valore di ritorno float sarebbe stato più che sufficiente. La cosa sarà chiara tra qualche riga.

Torno adesso al metodo invocato direttamente dalle voci di menu. Conviene soffermarsi perché contiene qualche concetto nuovo, specifico di Cocoa.

- (void)
elemAlign: (id)sender
{
    int        menuTag = [ sender tag ];
    [ self executeAlignment: menuTag ];
}

Come al solito, vi ho mentito. Il lavoro sporco è svolto da un altro metodo. Qui mi limito ad estrarre il tag ed a comandare una ulteriore operazione.

Per eseguire un allineamento, occorre determinare la coordinata di allineamento. Fissiamo le idee sull'allineamento a sinistra. Allora, occorre determinare tra tutti gli elementi selezionati una ascissa sulla quale eseguire l'allineamento. Tipicamente, si tratta dell'ascissa più piccola degli estremi sinistri dei rettangoli dei vari elementi (getElemLeft). Per una allineamento a destra, bisogna cercare l'ascissa più grande degli estremi destri (getElemRight), e così via per l'allineamento in alto (getElemTop) ed in basso (getElemBottom).

Se uno non sapesse programmare, scriverebbe quattro metodi distinti, uno per ciascuna direzione di allineamento. Ma io invece ho deciso di avere un unico metodo in grado di fare tutte le cose.

- (void)
executeAlignment: (int) menuTag
{
    NSMutableArray * selArr ;
    int        numElem, i ;
    float curTop, newTop, delta ;
    CCE_BasicForm * elem ;
    BOOL    chooseDir ;
    SEL        getDim ;

    switch ( menuTag ) {
    case 1 : chooseDir = TRUE ; getDim = @selector(getElemLeft) ; break ;
    case 2 : chooseDir = FALSE ; getDim = @selector(getElemRight) ; break ;
    case 3 : chooseDir = TRUE ; getDim = @selector(getElemTop) ; break ;
    case 4 : chooseDir = FALSE ; getDim = @selector(getElemBottom) ; break ;
    default : return ;
    }
    selArr = [ theElements getSelectedGraphics ];
    numElem = [ selArr count ];
    elem = [ selArr objectAtIndex: 0 ];
    curTop = [[ elem performSelector: getDim ] floatValue ];
    for ( i = 1 ; i < numElem ; i ++ )
    {
        elem = [ selArr objectAtIndex: i ];
        newTop = [[ elem performSelector: getDim ] floatValue ];
        if ( chooseDir && newTop < curTop )
            curTop = newTop ;
        else if ( ! chooseDir && newTop > curTop )
            curTop = newTop ;
    }
    // adesso che ho il top, sposto tutto
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ selArr objectAtIndex: i ];
        delta = curTop - [[ elem performSelector: getDim ] floatValue ];
        if ( menuTag == 1 || menuTag == 2 )
                [ elem moveMeIfSelectBy: NSMakePoint( delta, 0 )];
        else    [ elem moveMeIfSelectBy: NSMakePoint( 0, delta )];
    }
    [ self setNeedsDisplay: YES ];
}

figura 06

figura 06

Per prima cosa, determino due variabili che mi saranno molto utili. La prima, chooseDir, mi serve per capire se devo cercare un minimo (TRUE) o un massimo (FALSE). La seconda variabile è una cosa strana: è un metodo, o selettore, nella più corretta terminologia ObjC. In altre parole: nella variabile getDim si trova il metodo che devo utilizzare per determinare l'estremo. Da qui in poi è tutta discesa: costruisco un vettore con tutti gli elementi selezionati, e su questo vettore eseguo la ricerca di estremo. Il valore lo ottengo eseguendo il metodo indicato dalla variabile getDim sull'elemento, ed il criterio di scelta dipende dal valore di chooseDim. Ora, il meccanismo performSelector: richiede che il selettore restituisca un oggetto, ed è questo il motivo per cui i metodi getElemQualcosa restituiscono tutti un NSNumber, invece che un più semplice float.

figura 07

figura 07

Quando, alla fine del primo ciclo for, ho determinato la coordinata estremale, eseguo il secondo ciclo for, che esegue tutti gli spostamenti. Anche qui, sfrutto il meccanismo performSelector: per determinare l'ammontare dello spostamento (che è sempre in una sola direzione, per poi discriminare la direzione con il valore del tag.

Centraggio

Argomento molto simile all'allineamento è il centraggio, in orizzontale e verticale, di un insieme di elemento. Ero rimasto con il fatto che queste operazioni sono invocate attraverso una finestra piuttosto che da menu.

figura 08

figura 08

La finestra che ho prodotto è particolarmente brutta, ma svolge il suo ruolo. Come al solito, ho dovuto costruire una classe controllore AlignPaletteWinCtl con un po' di metodi aggiuntivi per costruirla, aprirla, e tenere traccia in maniera acconcia della coverView presente in prima fila. L'unico pulsante chiama il metodo executeAlign, all'interno del quale si recupera quale dei sei pulsanti è correntemente selezionato. Ho fatto in modo che il tag di questi pulsanti corrispondesse a quello della voce di menu con la stessa funzione. Quindi il codice è molto semplice.

- (IBAction)executeAlign:(id)sender
{
    // recupero il tag della cella selezionata
    if ( theDrawView )
    {
        int selTag = [[ selectedAlign selectedCell] tag] ;
        if ( selTag == 5 || selTag == 6 )
                [ theDrawView executeCentering: selTag ];
        else    [ theDrawView executeAlignment: selTag ];
    }
}

Se c'è una coverView davanti a tutti, recupero il tag. Se questo è compreso tra 1 e 4, ricado nel caso di allineamento, e quindi chiamo il metodo executeAlignment:. Negli altri due casi, chiamo un nuovo metodo, executeCentering:, che esegue quanto necessario.

Ancora una volta mi complico la vita, e decido che la coordinata di riferimento è calcolata in modo particolare. Calcolo per ogni elemento la coordinata massima e quella minima, e cerco ciascun valore estremale. Alla fine, la coordinata di riferimento è quella di mezzo. Il giochino è sempre lo stesso: determino quale metodo serve per calcolare la coordinata. Poi eseguo una ricerca contemporanea di massimo e di minimo sulla stessa coordinata.

- (void)
executeCentering: (int) menuTag
{
    NSMutableArray * selArr ;
    int        numElem, i ;
    float curMax, curMin, curMid, newTop, delta ;
    CCE_BasicForm * elem ;
    SEL        getDim ;
    // middle: centro le ordinate, center: centro le ascisse
    switch ( menuTag ) {
    case 5 : getDim = @selector(getElemMiddle) ; break ;
    case 6 : getDim = @selector(getElemCenter) ; break ;
    default : return ;
    }
    selArr = [ theElements getSelectedGraphics ];
    numElem = [ selArr count ];
    elem = [ selArr objectAtIndex: 0 ];
    curMax = curMin = [[ elem performSelector: getDim ] floatValue ];
    for ( i = 1 ; i < numElem ; i ++ )
    {
        elem = [ selArr objectAtIndex: i ];
        newTop = [[ elem performSelector: getDim ] floatValue ];
        if ( newTop < curMin )
            curMin = newTop ;
        else if ( newTop > curMax )
            curMax = newTop ;
    }
    // adesso che ho il max ed il min, sposto tutto
    curMid = 0.5 * (curMax + curMin );
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ selArr objectAtIndex: i ];
        delta = curMid - [[ elem performSelector: getDim ] floatValue ];
        if ( menuTag == 6 )
                [ elem moveMeIfSelectBy: NSMakePoint( delta, 0 )];
        else    [ elem moveMeIfSelectBy: NSMakePoint( 0, delta )];
    }
    [ self setNeedsDisplay: YES ];
}

La coordinata di riferimento è la semisomma dei due estremi. Poi, spazzolo tutti gli elementi e li sposto secondo necessità. Tutto come prima, senza troppi problemi.

Gruppi

È arrivato finalmente il momento di parlare dei gruppi (è da qualche capitolo che mi tiro dietro questo argomento). Ho già le due solite voci di menu per costruire un gruppo (Group) e per disfarlo (ungroup). Il metodo è lo stesso, e distingue le operazioni in base al tag.

- (void)
groupElem: (id)sender
{
    int        menuTag = [ sender tag ];

    if ( menuTag == 200 )
    {
        [ theElements makeGroupFromSelected ];
    }
    else
    {
    // istruzioni per disfare un gruppo
    }
    [ self setNeedsDisplay: YES ];
}

Il metodo chiama come al solito un altro metodo, makeGroupFromSelected, per costruire un gruppo, mentre esegue direttamente le operazioni per disfarlo (ma le mostro poi).

Il metodo è piuttosto lungo e lo commento in vari segmenti.

- (void)
makeGroupFromSelected
{
    int        i , numElem, firstIdx = -1, lastIdx ;
    CCE_BasicForm * elem ;
    CCE_ElemGroup * newGrp ;
    float        xmin, xmax, ymin, ymax ;
    // ciclo su tutti gli elementi del gruppo
    lastIdx = numElem = [ elemArray count ];
    newGrp = [[[ CCE_ElemGroup alloc] init ] autorelease ];
    for ( i = numElem-1 ; i >= 0; i -- )
    {
        elem = [ elemArray objectAtIndex: i ];
        if ( [elem cceIsLocked] || ! [ elem cceIsSelected ] )
            continue ;
        // se arrivo qui, l'elemento e' libero e selezionato
        [ elem retain ];
        if ( firstIdx == -1 )
            firstIdx = i ;
        if ( lastIdx > i )
            lastIdx = i ;
        // comunque vada; lo tolgo dallo array
        [ elemArray removeObjectAtIndex: i ];
        // e lo aggiungo al nuovo gruppo
        [ [newGrp elemArray] addObject: elem ];
        [ elem release ];
    }
    // quando arrivo qui, ho costruito un nuovo gruppo

L'idea è di esaminare tutti gli elementi e, nel caso siano selezionarli, toglierli dal vettore principale ed inserirli in un nuovo oggetto della classe CCE_ElemGroup creato allo scopo. Il vettore è spazzolato da indici alti fino a zero dal momento che, estraendo elementi, il vettore stesso si accorcia. Nello spazzolare il vettore, mi metto da parte due indici: la variabile firstIdx conserva l'indice del primo elemento selezionato, mentre lastIdx conserva l'indice dell'elemento selezionato di fronte a tutti gli altri.

    // se non c'e' nulla, faccio nulla
    if ( firstIdx == -1)
        return ;
    // se c'e' un solo elemento, lo rimetto dov'era
    if ( [ [newGrp elemArray] count ] == 1 )
    {
        [ elemArray insertObject: [[newGrp elemArray] objectAtIndex:0] atIndex: firstIdx];
        return ;
    }

L'indice firstIdx è utilizzato allo scopo di considerare due casi particolari. In primo luogo, se la variabile non è stata modificata, significa che non ci sono elementi selezionati; non ha quindi senso proseguire con le operazioni. Se invece è selezionato un singolo elemento, non ha senso costruire un gruppo di un solo elemento. Quindi, lo reinserisco nel posto in cui si trovava, ed è finita lì.

    // finalmente, se arrivo qui, ho un gruppo vero e proprio
    // aggiusto tutti i valori del gruppo
    // calcolo il rettangolo che include il tutto
    numElem = [ [newGrp elemArray] count ];
    elem = [ [newGrp elemArray] objectAtIndex: 0 ];
    ymin = [[ elem getElemTop] floatValue];
    xmin = [[ elem getElemLeft] floatValue];
    xmax = [[ elem getElemRight] floatValue];
    ymax = [[ elem getElemBottom] floatValue];
    // ciclo su tutti gli elementi del gruppo
    for ( i = 1 ; i < numElem ; i ++ )
    {
        elem = [ [newGrp elemArray] objectAtIndex: i ];
        // cerco i limiti del rettangolo circoscritto
        if ( ymin > [[ elem getElemTop] floatValue])
            ymin = [[ elem getElemTop] floatValue];
        if ( xmin > [[ elem getElemLeft] floatValue] )
            xmin = [[ elem getElemLeft] floatValue];
        if ( xmax < [[ elem getElemRight] floatValue] )
            xmax = [[ elem getElemRight] floatValue];
        if ( ymax < [[ elem getElemBottom] floatValue] )
            ymax = [[ elem getElemBottom] floatValue];
    }
    // attribuisco il rettangolo che include il gruppo
    [ newGrp setElemLimit: NSMakeRect( xmin, ymin, xmax-xmin, ymax-ymin) ];

Una volta costruito l'elemento gruppo, occorre predisporre alcune sue variabili interne. Ce ne è una che è particolarmente utile, ovvero il rettangolo che racchiude tutti gli elementi. Il gioco è facile: basta spazzolare tutti gli elementi del gruppo, e pigliare le coordinate estreme. Alla fine, costruisco il rettangolo in maniera molto semplice.

    // tutti gli elementi interni non sono selezionati
    [ newGrp cascadeSelection: NO ];
    // il gruppo invece rimane selezionato
    [ newGrp setCceIsSelected: YES ];
    // inserisco il gruppo nel posto opportuno
    [ elemArray insertObject: newGrp atIndex: lastIdx];
}

figura 09

figura 09

Sono arrivato alla fine del metodo. Occorre in primo luogo deselezionare tutti gli elementi che adesso sono all'interno del gruppo, attraverso il metodo cascadeSelection:, per trasferire la proprietà di essere selezionata al gruppo stesso. Qui si vede la ragione per cui il metodo setCceIsSelected: non ha più una realizzazione specifica per CCE_ElemGroup: nella vecchia realizzazione, tutti gli elementi del gruppo avrebbero ereditato la caratteristica di essere selezionati. Ciò non è normalmente un problema, se non fosse che un elemento selezionato disegna tutti gli handle del contorno, cosa che invece io voglio evitare. Voglio che un gruppo selezionato mostri solo gli handle relativi al contorno (rettangolo) principlae, e non tutti quelli dei singoli elementi.

figura 10

figura 10

Infine, ultima istruzione, inserisco il gruppo all'interno del vettore degli elementi della coverView. Lo faccio a partire dall'indice lastIdx: in questo modo, il gruppo si posiziona, nell'ordine fronte/retro, nel posto in cui si trovava il primo degli elementi selezionati che adesso fanno parte del gruppo.

C'è adesso da esaminare come effettuare la rottura del gruppo, eliminando quindi l'elemento della classe CCE_ElemGroup, rimpiazzandolo con tutti gli elementi del gruppo stesso. Ecco il pezzo di codice relativo.

        NSMutableArray * selArr ;
        int        numElem, i ;
        CCE_BasicForm * elem ;
    
        selArr = [ theElements elemArray ];
        numElem = [ selArr count ];
        for ( i = numElem-1 ; i >= 0; i -- )
        {
            elem = [ selArr objectAtIndex: i ];
            if ( ! [ elem cceIsSelected ])
                continue ;
            // se arrivo qui, l'elemento e' selezionato
            // ma potrebbe non essere un gruppo
            if ( ! [elem isKindOfClass:[CCE_ElemGroup class]] )
                continue ;
            // se arrivo qui, l'elemento e' selezionato ed e' un gruppo
            [ elem retain ];
            // lo elimino dall'insieme degli oggetti
            [ [theElements elemArray] removeObjectAtIndex: i ];
            // e poi appiccico gli elementi
            [ elem ungroupAndInsertIn: theElements startingFrom: i ];
            [ elem release ];
        }

Anche qui, si spazzolano gli elementi dal più alto al più basso. Se l'elemento non è selezionato, o non è un gruppo, si salta. Se si arriva ad un vero e proprio gruppo, lo si toglie dal vettore, e poi, utilizzando un altro metodo, si inseriscono gli elementi del gruppo all'interno del vettore principale.

- (void )
ungroupAndInsertIn: (CCE_ElemGroup *) masterArray startingFrom: (int) idx
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    // per ogni elemento presento, lo aggiungo
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem setCceIsSelected: YES ] ;
        [ [masterArray elemArray] insertObject: elem atIndex: idx ];
    }
}

La cosa è molto semplice: si spazzolano tutti gli elementi del gruppo, e li si inseriscono uno dopo l'altro al posto giusto. Da notare come ogni singolo elemento adesso è selezionato (sono partito da un gruppo selezionato, ed ottengo un insieme di elementi selezionati).

Non è finita qui. Ho scoperto molti problemi nella gestione dei gruppi. Sono tante piccole cose, che però ci metterei troppo a discutere, e questo capitolo è fin troppo lungo. Cercate nel codice, anche se temo che, nei prossimi capitoli, scoprirò tante cose che non funzionano.

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