MaCocoa 052

Capitolo 052 - Rotazioni e Rivoluzioni, parte 2

Seconda parte di un lungo capitolo, in cui si completa la storia della rotazioni e si cambia anche la strategia di gestione delle notifiche.

Sorgenti: documentazione Apple.

Prima stesura: 16 agosto 2004.

Gli altri metodi di CCE_BasicForm

La classe CCE_BasicForm ha bisogno di molti altri metodi per la gestione degli elementi. il fatto interessante è che i metodi descritti in questo paragrafo vanno bene per tutti i tipi di elemento, e non richiedono di versioni particolari per classi specifiche (come invece succedeva in precedenza); fa sempre eccezione la classe CCE_ElemGroup, che ha metodi specifici per la gestione di gruppi di elementi (che non sono cambiati).

Parto dal metodo che determina se il clic del mouse è avvenuto sopra l'elemento. è cambiato profondamente:

- (BOOL)
checkClickOnMe:(NSPoint)point
{
    float ev = cceLineWidth + 2;
    // trasformo il punto in coordinate locali
    NSPoint locPt = [ reverTF transformPoint: point ];
    BOOL    insideX = NO, insideY = NO ;
    // qui faccio un po' di confusione, in quanto PointInRect
    // va in confusione con dimensioni negative o nulle
    if ( localSize.width >= 0 )
    {
        // se la dimensione e' positiva ...
        // controllo se la coordinata del punto e' compresa
        // tra un Zero e la lunghezza (con tolleranza...)
        if ( (-ev < locPt.x) && (locPt.x < (localSize.width + ev)) )
            insideX = YES ;
    }
    else
    {
        // se la dimensione e' negativa...
        // controllo se la coordinata del punto e' compresa tra la
        // lunghezza (negativa) e zero (con tolleranza)
        if ( ((- ev + localSize.width) < locPt.x) && (locPt.x < ev) )
            insideX = YES ;
    }
    // faccio lo stesso per l'altra dimensione
    if ( localSize.height >= 0 )
    {
        if ( (-ev < locPt.y) && (locPt.y< (localSize.height + ev)) )
            insideY = YES ;
    }
    else
    {
        if ( ((- ev + localSize.height) < locPt.y) && (locPt.y < ev) )
            insideY = YES ;
    }
    // il punto sta dentro se e' compreso in entrambe le coordinate
    return ( insideX && insideY );
}

La prima cosa da fare è convertire il punto di clic in coordinate locali. Poi, si tratta di capire se questo punto si trova all'interno del rettangolo che inscrive l'elemento grafico. Tuttavia, a causa del fatto che la funzione NSPointInRect rimane confusa da rettangoli con dimensioni nulle e dimensioni negative, ho sbrogliato la matassa facendo esplicito riferimento ai valori in esame. Ogni controllo avviene utilizzando una tolleranza sul posizionamento pari allo spessore della linea più altri due pixel, in modo che sia facilitato il clic sopra elementi di infimo spessore. Giova notare che il metodo esposto funziona anche per elementi di classe CCE_Line, senza bisogno di una versione specifica del meotdo e senza bisogno di calcoli particolari di distanza di un punto da una retta.

Per capire se il cli è avvenuto sopra un handle, la cosa è ancora semplice:

- (short)
clicIntoHandle: (NSPoint) point
{
    int i;
    // ricavo le coordinate locali del punto
    point = [ reverTF transformPoint: point ];
    for ( i = 0 ; i < numOfHdl ; i ++ )
    {
        // se il punto designato e lo handle sono abbastanza vicini
        float a1 = hdlList[i].x - point.x ;
        float a2 = hdlList[i].y - point.y ;
        if ( a1 * a1 + a2 * a2 <= 4 )
        {
            return ( i );
        }
    }
    return ( -1 ) ;
}

Rispetto alla versione precedente, c'è la trasformazione del punto in coordinate locali.

C'è poi il metodo che controlla se l'elemento si trova all'interno di un rettangolo, che si presume derivi da una operazione di selezione multipla da parte dell'utente. Non avevo voglia di fare cose sofisticate, ed ho quindi scritto:

- (void)
selectMeIfInRect: (NSRect) selRect addingToSelez: (BOOL) add2sel
{
    BOOL    inrect ;
    // calcolo il rettangolo che circoscrive il percorso dell'elemento
    NSRect limRect = [ [ localTF transformBezierPath: theDrawPath ] bounds ] ;
    // lo allargo di un pelo per tenere conto di rettangoli nulli
    // (che non fanno funzionare le cose)
    limRect = NSInsetRect(limRect, -1, -1 );
    // vedo se il rettangolo di selezione comprende l'elemento
    inrect = NSContainsRect( selRect, limRect ) ;
    // se non devo aggiungere alla selezione
    if ( ! add2sel )
        ... // come in precedenza
}

Il rettangolo di selezione è espresso in coordinate della view. Determino quindi il rettangolo che circoscrive il percorso, trasformando prima il percorso nel SdR della view, e poi calcolandone gli estremi, ottenendo il rettangolo limRect. Aggiungo una certa tolleranza al rettangolo (anche per gestire correttamente percorsi che rappresentano linee orizzontali e verticali, che danno luogo a rettangoli con una dimensione nulla), e poi verifico l'inclusione colla classica funzione NSContainsRect. Ancora una volta, lo stesso metodo va bene per tutte le classi derivate.

Ho poi collassato i due metodi di movimento in un solo metodo; in precedenza erano infatti presenti due diversi metodi per muovere un elemento, la cui differenza risiedeva nel fatto che in un caso il movimento era condizionato dall'essere l'elemento selezionato o meno. Ho mantenuto la distinzione fornendo un opportuno parametro al metodo:

- (void)
moveMeBy: (NSPoint) distance onlyIfSelected: (BOOL) selOnly
{
    // se selOnly e' Falso, muovo sicuro
    // se invece e' vero, solo se non locked e selected
    if ( (selOnly == NO) || ((! cceIsLocked) && cceIsSelected) )
    {
        // gestione dello undo del movimento
        [[self undoManager] setActionName: @"BasicForm -> moveMeBy"];
        [[[self undoManager] prepareWithInvocationTarget: [ownerView theElements]]
            moveElem: self by: NSMakePoint(-distance.x, -distance.y)];
        // effettuo il movimento vero e proprio: sposto il punto base
        drawPoint.x += distance.x ;
        drawPoint.y += distance.y ;        
        // aggiorno la trasformazione affine
        [ self calcLocalTranform ];
        // ricosctruisco gli handle
        [ self buildHdlList ] ;
        // devo sicuramente ridisegnare qualcosa
        [ [ self ownerView ] setNeedsDisplay: YES ];
    }
}

Se il valore del parametro selOnly è Falso (NO), il movimento avviene comunque; se invece il parametri è Vero (YES), il movimento avviene solo se l'elemento è selezionato e non è bloccato. C'è da notare come avviene il movimento dell'elemento: è sufficiente modificare le coordinate dell'origine del SdR dell'elemento e ricalcolare le trasformazioni (e gli handle).

Infine, ci sono i metodi utilizzati per ottenere le coordinate degli estremi; in precedenza, i metodi erano molto semplici ed utilizzano a man bassa il fatto che il rettangolo elemLimit già forniva tutte le informazioni richiesta. Ora che elemLimit non esiste più, occorre calcolarlo esplicitamente. Ad esempio:

- (NSNumber*)
getElemTop
{
    // piglio il percorso e lo trasformo nelle coordinate della finestra
    // da qui, ricavo il rettangolo circoscritto, e poi l'estremo di interesse
    NSRect limR = [ [ localTF transformBezierPath: theDrawPath ] bounds ] ;
    return ( [ NSNumber numberWithFloat: limR.origin.y] );
}

Rotazioni

Posso finalmente discutere come effettuare rotazioni di elementi. In effetti, dopo tutto il lavoro precedente, la cosa è piuttosto semplice. Liquido subito il metodo della classe CCE_ElemGroup per gestire le rotazioni degli elementi selezionati, in maniera incrementale o assoluta:

- (void)
rotateElemAngle: (float) angle absolute: (BOOL) absInc
{
    int        i , numElem ;
    CCE_BasicForm * elem ;

    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        if ( [ elem cceIsLocked] || ( ! [ elem cceIsSelected] ))
            continue ;
        if ( absInc )
                [ elem rotateMeAtAngle: angle ] ;    
        else    [ elem rotateMeAtAngle: ([ elem rotAngle] + angle) ] ;
    }
}

Questo è il metodo chiamato sia dalle voci di menu che richiedono incrementi di 90 gradi in senso orario o antiorario, sia dalla prossimamente descritta palette di gestione delle rotazioni. Il metodo principale è proprio della classe CCE_BasicForm, va bene per tutte le classi di elementi ed è così composto:

- (void)
rotateMeAtAngle: (float) angle
{
    NSAffineTransform * transform;
    // preparo lo undo
    [[self undoManager] setActionName: @"BasicForm -> rotateMeAtAngle"];
    [[[self undoManager] prepareWithInvocationTarget: [ownerView theElements]]
        rotateElem: self byAngle: rotAngle ];
    // parto dalla trasformazione corrente
    transform = [ [ NSAffineTransform alloc] initWithTransform: localTF ] ;
    // mi spostoal centro dell'elemento
    [ transform translateXBy: localSize.width*0.5 yBy: localSize.height*0.5 ];
    // eseguo la rotazione residua sull'oggetto
    [ transform rotateByDegrees: (angle - rotAngle) ];
    // ritorno nel punto base
    [ transform translateXBy: -localSize.width*0.5 yBy: -localSize.height*0.5 ];
    // bisogna aggiornare la variabili interne
    [ self setRotAngle: angle ] ;
    // ma guarda dov'e' finita l'origine...
    drawPoint = [ transform transformPoint: NSMakePoint(0, 0) ];
    // calcolo ed assegno le varie trasformazioni
    [ self setLocalTF: transform ];
    [ transform invert ];
    [ self setReverTF: transform ];
    // aggiorno gli handle
    [ self buildHdlList ] ;
    [ [ self ownerView ] setNeedsDisplay: YES ];
}

Dopo aver congelato la situazione corrente per il meccanismo di undo, procedo con le operazioni di rotazione. Ora, voglio che la rotazione avvenga non tanto attorno al punto origine, ma attorno al punto centrale dell'elemento. Per fare questo, devo costruire una corretta matrice di trasformazione, eseguendo le trasformazioni elementari nel giusto ordine. Parto dalla trasformazione corrente: poi, mi sposto nel centro dell'elemento (mi sposto di metà larghezza e metà altezza); in questo punto, eseguo la rotazione residua. Infine, ritorno nel punto origine, spostandomi nuovamente di metà larghezza e metà altezza. Nel compiere queste operazioni, l'origine del SdR dell'elemento si è spostata: la recupero, nelle coordinate della view, trasformando il nuovo punto origine (quello di coordinate nulle) attraverso la trasformazione corrente.

Raccontata così, sembra piuttosto semplice ed ovvia, ma la cosa ha richiesto molto lavoro (non è che le trasformazioni affini mi fossero chiare fin dall'inizio), esperimenti, rifacimenti, molti disegnini su carta per capire cosa succedendo. Però mi pare che il risultato sia semplice ed anche, nel suo piccolo, elegante.

figura 01

figura 01

Rimane da discutere come comandare le operazioni di rotazione. Ho previsto almeno due modalità. La prima è quella classica da menu: ci sono due voci per eseguire rotazioni orarie e antiorarie di novanta gradi. Questi comandi sono incrementali, nel senso che la rotazione di novanta gradi si aggiunge all'angolo corrente. Di conseguenza il metodo che risponde a questi menu è così fatto (si trova nella classe CoverView):

- (void)
rotateElem: (id) sender
{
    float        angle = ([ sender tag] == 90) ? +90.0 : -90.0 ;
    [ theElements rotateElemAngle: angle absolute: NO ] ;
    [ [NSNotificationCenter defaultCenter]
        postNotificationName:@"CoverViewChangedSelection"
        object: self ];
}

Ho attributo alle due voci di menu lo stesso metodo come action, e distinguo il comando effettivo attraverso i tag 90 e 91 che ho loro attribuito.

figura 02

figura 02

C'è poi una terza voce che apre un pannello dove è possibile comandare rotazioni arbitrarie, secondo una modalità assoluta. In questo pannello mi sono sbizzarrito con tre possibilità: si può stabilire la rotazione attraverso uno slider orizzontale, uno slider circolare ed impostando direttamente il valore attraverso un campo di testo. Non appena l'utente modifica uno di questi tre controlli, si ha subito l'effetto sugli elementi selezionati, che si dispongono secondo l'angolo indicato. Al di là di alcune imperfezioni ed idiosincrasie nell'interfaccia (non so bene come gestire il cambiamento tra rotazione oraria ed antioraria, o se gestirla del tutto, visto che l'angolo spazia in realtà in modalità assoluta tra 0 e 360 gradi; non so bene come comportarmi quando ci sono più elementi selezionati: adesso assumono tutti lo stesso angolo, cosa giusta dal punto di vista teorica, ma bruttissima a vedersi; cose del genere), la cosa sembra funzionare piuttosto bene, con effetti spettacolari.

figura 03

figura 03

La finestra è gestita dalla nuova classe RotateWinCtl, secondo le procedure ormai standard di molte finestre del genere. Una operazione su uno dei tre controlli (i due slider ed il campo testo) attiva la seguente azione:

- (IBAction)
updateSliders:(id)sender
{
    float value = [ sender floatValue ];
    
    [ crcSlider setFloatValue: value ] ;
    [ linSlider setFloatValue: value ] ;
    [ valField setFloatValue: value ] ;
    [ self execRotation: sender ];
}

Una generica operazione sul pannello di rotazione quindi provoca l'esecuzione del seguente metodo:

- (IBAction)
execRotation:(id)sender
{
    CoverView * cv = [AppDelegate getTheDrawView ] ;
    float        angle1, angle2 ;
    // non dovrebbe mai succedere...
    if ( cv == nil ) return ;

    angle1 = [ linSlider floatValue ] ;
    angle2 = ([ [ dirRadioBtn selectedCell] tag ] == 95) ? angle1 : -angle1 ;
    [ [ cv theElements] rotateElemAngle: angle2 absolute: YES ] ;
    // dopo la rotazione, occorre aggiornare la selezione corrente
    // nota che l'oggetto di riferimento e' la coverView
    [ [NSNotificationCenter defaultCenter]
            postNotificationName:@"CoverViewChangedSelection"
            object: cv ];
}

Lasciate per il momento perdere la misteriosa istruzione che parte da AppDelegate; il metodo recupera l'angolo di rotazione, lo aggiusta a seconda della direzione di rotazione (ancora una volta discriminata attrverso l'attribuzione di appositi tag ai radio button), e poi comanda l'esecuzione della rotazione. Come nel metodo rotateElem: visto poco sopra, il metodo termina inviando una notifica; in questo modo le finestre che dipendono dalla selezione corrente per le loro funzionalità (la finestre della informazioni relative ad un elemento, ad esempio), possono aggiornare il loro contenuto di conseguenza.

Nuova gestione delle notifiche

L'ultimo argomento del capitolo non ha molto a che fare con le rotazioni, ma è solamente un diverso meccanismo di gestione delle notifiche. Fino a questo momento, ogni volta che costruivo un pannello le cui funzioni dipendevano dalla finestra principale (ad esempio: informazioni di un elemento di catalogo; informazioni di un elemento grafico; il pannello di impostazione della griglia, eccetera), mi trovavo costretto a sottoscrivere almeno un paio di notifiche per gestire in modo congruente una variabile d'istanza che tenesse traccia della finestra principale; la variabile serviva per poter poi operare correttamente all'interno di questa finestra. Pigliamo ad esempio la palette per effettuare allineamenti e centraggi; avevo bisogno di almeno tre metodi per tenere aggiornata la variabile theDrawView:

- (void)
windowDidLoad
{
    [super windowDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(mainWindowChanged:)
        name:NSWindowDidBecomeMainNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(mainWindowResigned:)
        name:NSWindowDidResignMainNotification object:nil];
    [self setMainWindow:[NSApp mainWindow]];
}

- (void)
mainWindowChanged: (NSNotification *)notification
{
    [self setMainWindow:[notification object]];
}

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

- (void)
setMainWindow: (NSWindow *) mainWindow
{
    NSWindowController *controller = [mainWindow windowController];
    if (controller && [controller isKindOfClass:[CoverWinCtl class]])
        theDrawView = [(CoverWinCtl *)controller cvrView];
    else    
        theDrawView = nil;
}

Con il proliferare di questo tipo di finestre, ad ogni cambio di finestra principale si scatenava una cascata di notifiche, che eseguono fondamentalmente le stesse operazioni (controllo che finestra si trova ora davanti alle altre, ed aggiorno di conseguenza la variabile d'istanza).

Una situazione del genere mi rode un po', per cui ho centralizzato le operazioni, facendo in modo che un unico attore nell'applicazione ricevesse le notifiche e procedesse a distribuire i compiti di aggiornamento. Contestualmente, ho messo in piedi un meccanismo per la gestione di variabili globali d'applicazione.

Ho scelto la classe AddDelegate come artefice del tutto. Finora, la classe era sostanzialmente utilizzata come destinatario delle voci di menu che richiedevano apertura di finestre e pannelli. L'istanza è unica per l'applicazione ed è costruita automaticamente al caricarsi del file MainMenu.nib.

Alla classe ho aggiunto tre variabili d'istanza, destinate a diventare le variabili globali dell'applicazione:

    NSOutlineView        * theOutlineView ;
    CoverView            * theDrawView ;
    CCE_BasicForm        * theSelectedElem ;

Queste tre variabili contengono: un puntatore alla NSOutlineView quando è di fronte a tutti una finestra di catalogo, o nil altrimenti; un puntatore alla CoverView e all'elemento selezionato (se disponibile) quando la finestra principale è quella di disegno della copertina. La classe AppDelegate è l'unica classe a sottoscrivere le notifiche. Ce ne sono quattro: abbandono di una finestra principale, nuova finestra principale, cambio di selezione all'interno di una finestra di catalogo, cambio di selezione all'interno di una finestra di copertina:

static AppDelegate        * applicationGlobals = nil ;

- (id)
init
{
    if ( applicationGlobals )
        return ( applicationGlobals );
    // se arrivo qui devo costruire l'istanza condivisa
    self = [ super init ] ;
    // l' istanza condivisa e' proprio questa
    applicationGlobals = self ;
    // sottoscrivo alcune notifiche
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(mainWindowChanged:)
        name:NSWindowDidBecomeMainNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(mainWindowResigned:)
        name:NSWindowDidResignMainNotification object:nil];
    // osservatore sul cambiamento della selezione corrente
    [[ NSNotificationCenter defaultCenter] addObserver: self
        selector: @selector( outViewSelChange: )
        name: NSOutlineViewSelectionDidChangeNotification object: nil ] ;
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(cvrViewSelChange:)
        name:@"CoverViewChangedSelection" object:nil];
    return (self );    // restituisco l'istanza
}

In seguito al cambiamento della finestra principale, i metodi invocati sono:

- (void)
mainWindowChanged: (NSNotification *)notification
{
    NSWindowController *controller = [[notification object] windowController];
    if (controller && [controller isKindOfClass:[CoverWinCtl class]])
    {
        [ self updateListWinInfo: nil ] ;
        [ self updateCoverViewInfo: [(CoverWinCtl *)controller cvrView] ] ;
    }
    else if (controller && [controller isKindOfClass:[ListWinCtl class]])
    {
        [ self updateListWinInfo: [(ListWinCtl *)controller getMainView ] ] ;
        [ self updateCoverViewInfo: nil ] ;
    }
    else
    {
        [ self updateListWinInfo: nil ] ;
        [ self updateCoverViewInfo: nil ] ;
    };
}

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

A seconda del tipo di finestra che si presenta, richiedono l'aggiornamento dei pannelli collegati.

Ugualmente il cambio di selezione in una delle finestre provoca l'esecuzione di:

- (void )
outViewSelChange: ( NSNotification *) notification
{
    NSOutlineView * outV = (NSOutlineView*) [ notification object ];
    if ( [ outV tag] == TAG_CDCATDOC_FULLLIST )
        [ self updateListWinInfo: [ notification object ] ];
}

- (void)
cvrViewSelChange: (NSNotification *)notification
{
    id notifObj = [notification object] ;
    if ( [notifObj isKindOfClass:[CoverView class]])
            [ self updateCoverViewInfo: (CoverView*) notifObj ];
    else    [ self updateCoverViewInfo: nil ];
}

Il primo metodo controlla che sia modificata la selezione nella outlineView della finestra del catalogo (verifica che il tag corrisponda per essere sicuro), mentre il secondo verifica che la notifica provenga da una CoverView.

Le operazioni di aggiornamento vero e proprio sono svolte dai due metodi seguenti:

- (void)
updateListWinInfo: (NSOutlineView *) outView
{
    InfoWinCtrl *tempWC1 = (InfoWinCtrl*) [ InfoWinCtrl sharedInfoWinCtrl ];    
    theOutlineView = outView ;
    // impongo l'aggiornamento alle varie finestre accessorie
    [ tempWC1 updateInfo: theOutlineView ] ;
}

Al momento, l'unica finestra che dipende dalla finestra di catalogo è quella delle informazioni relative ad un file. Molto più articolato invece il metodo relativo alla finestra della copertina; qui ci sono ben quattro pannelli che ne dipendono (il pannello di informazioni su di un elemento grafico; il pannello per eseguire la rotazione; il pannello per il controllo della griglia; il pannello per i comandi di allineamento). Inoltre, i primi due pannelli utilizzano anche l'elemento correntemente selezionato (se ne è selezionato uno solo) per adeguare il loro aspetto. è questo il motivo per cui il metodo si preoccupa di verificare la presenza di elementi selezionati e di impostare valori adeguati alla variabile theSelectedElem:

- (void)
updateCoverViewInfo: (CoverView *) coverView
{
    ElemInfoWinCtl *tempWC1 = (ElemInfoWinCtl*)
            [ ElemInfoWinCtl sharedElemInfoWinCtl ];
    RotateWinCtl    *tempWC2 = (RotateWinCtl*)
        [ RotateWinCtl sharedRotateWinCtl ];
    GridDlgWinCtl    *tempWC3 = (GridDlgWinCtl*)
        [ GridDlgWinCtl sharedGridDialogWinCtl ];
    AlignWinCtl        *tempWC4 = (AlignWinCtl*)
        [ AlignWinCtl sharedAlignPaletteWinCtrl ];

        // aggiorno la coverview in primo piano
    theDrawView = coverView ;
    if ( theDrawView == nil )
    {
        // se non c'e' la coverview, non ci sono elementi selezionati
        theSelectedElem = nil ;        
    }
    else
    {
        NSMutableArray * selArr ;
        int                numElem ;
        // guardo quanti elementi sono correntemente selezionati
        selArr = [ [ theDrawView theElements] getSelectedGraphics ];
        numElem = [ selArr count ];
        // se non ce ne sono...    
        if ( numElem == 0 )
        {
            // dico che se ne fa nulla
            theSelectedElem = nil ;        
        }
        // ... o ce ne sono troppi
        else if ( numElem > 1 )
        {
            // dico che se ne fa nulla
            theSelectedElem = (CCE_BasicForm*)(-1) ;                
        }
        else
        {
            // c'e' un solo elemento, imposto il puntatore
            theSelectedElem = [ selArr objectAtIndex: 0 ] ;
        }
    }
    // impongo l'aggiornamento alle varie finestre accessorie
    [ tempWC1 setSelectedElem: theSelectedElem ] ;
    [ tempWC2 setSelectedElem: theSelectedElem ] ;
    [ tempWC3 updateWindow: theDrawView ] ;
    [ tempWC4 updateWindow: theDrawView ] ;
}

Informazioni riviste

Piuttosto che illustrare noiosamente tutte le modifiche effettuate sulle classi AlignPaletteWinCtl (che ora si chiama AlignWinCtl), ElemInfoWinCtl, GridDialogWinCtl (che ora si chiama GridDlgWinCtl) e InfoWinCtl, discuto solo quelli relativi a ElemInfoWinCtl, pannello che per altro ha subito anche qualche ritocco a livello grafico.

figura 04

figura 04

Al caricamento del pannello, non ci sono più sottoscrizioni a notifiche, ma solo la predisposizione dell'aspetto della finestra:

- (void)
windowDidLoad
{
    [super windowDidLoad];
    // predisposizione dei vari elementi della finestra
    [fillCheckbox setState:NSOffState];
    [fillColorWell setColor:[NSColor whiteColor]];
    [lineCheckbox setState:NSOnState];
    [lineColorWell setColor:[NSColor blackColor]];
    [lineWidthSlider setFloatValue:0.0];
    [lineWidthTextField setFloatValue:0.0];
    [rotationSlider setFloatValue: 0.0 ];
    [rotationText setFloatValue: 0.0 ];
    [xTextField setStringValue:@""];
    [yTextField setStringValue:@""];
    [widthTextField setStringValue:@""];
    [heightTextField setStringValue:@""];
    [ self setSelectedElem: [ AppDelegate getTheSelectedElem ] ];
}

La prima predisposizione avviene utilizzando l'elemento correntemente selezionato; questo è recuperato attraverso un metodo di classe di AppDelegate:

+ (CCE_BasicForm *)
getTheSelectedElem
{
    return ( [ applicationGlobals theSelectedElem ] );
}

che sfrutta l'istanza condivisa per restituire il valore della variabile d'istanza corrispondente (questo sì aggiornato dal meccanismo delle notifiche).

L'aggiornamento della finestra è effettuato dal metodo setSelectedElem:, che è anche il metodo chiamato da updateCoverViewInfo: quando pensa che sia necessario.

- (void)
setSelectedElem: (CCE_BasicForm *) selElem
{
    theSelectedElem = selElem;
    // se non ci sono eselementi selezionati
    if ( theSelectedElem == nil )
    {
        // dico che se ne fa nulla
        [ elemId setStringValue: @"No graphic element selected"];
        [ self setControlsEnabled: NO ] ;        
        return ;
    }
    // ... o ce ne sono troppi
    else if ( theSelectedElem == (CCE_BasicForm*)(-1) )
    {
        // dico che se ne fa nulla
        [ elemId setStringValue: @"Too many graphic elements selected"];                
        [ self setControlsEnabled: NO ] ;        
        return ;
    }
    // se arrivo qui, c'e' un solo elemento selezionato
    // attivo tutti gli elementi della finestra
    [ self setControlsEnabled: YES ] ;        
    // aggiorno il contenuto dei vari campi
    [ self updateWindowInfo ];
}

In base al valore dell'elemento selezionato, aggiorna il contenuto del pannello, fornendo una adeguata stringa e predispondendo i controlli abilitati o disabilitati. L'ulteriore metodo updateWindowInfo si occupa di aggiustare i valori dei controlli stessi:

- (void)
updateWindowInfo
{
    // predispongo gli elementi che corrispondono agli attributi comuni
    [fillCheckbox setState: [ theSelectedElem cceIsFilled] ];
    [fillColorWell setColor: [ theSelectedElem cceFillColor] ];
    [lineCheckbox setState: [ theSelectedElem cceIsStroked] ];
    [lineColorWell setColor:[ theSelectedElem cceLineColor] ];
    [lineWidthSlider setFloatValue: [ theSelectedElem cceLineWidth] ];
    [lineWidthTextField setFloatValue: [ theSelectedElem cceLineWidth] ];
    [rotationSlider setFloatValue: [ theSelectedElem rotAngle]];
    [rotationText setFloatValue: [ theSelectedElem rotAngle]];
    [xTextField setStringValue: [ NSString stringWithFormat: @"%f",
            [theSelectedElem drawPoint].x ] ];
    [yTextField setStringValue: [ NSString stringWithFormat: @"%f",
            [theSelectedElem drawPoint].y ] ];
    [widthTextField setStringValue: [ NSString stringWithFormat: @"%f",
            [theSelectedElem localSize].width ] ];
    [heightTextField setStringValue: [ NSString stringWithFormat: @"%f",
            [theSelectedElem localSize].height ] ];
    // preparo una scritta a seconda dell'elemento
    [ elemId setStringValue: [ theSelectedElem describeMe ] ];        
}

È da notare che con la nuova struttura degli elementi, cade la distinzione tra CCE_BasicForm e CCE_Line (che richiedeva in precedenza trattamento distinto), per cui il metodo è molto semplice e lineare.

Ci sono due nuovi elementi, uno slider ed un campo testo per rappresentare l'angolo di rotazione corrente dell'elemento. Ancora una volta, modificando il campo di testo od operando sullo slider circolare, si modifica lo stato dell'elemento, fornendo così un ulteriore meccanismo di rotazione.

Anche il metodo changedElem: si giova della uniformità di trattamento dei vari elementi:

- (IBAction)
changedElem:(id)sender
{
    float p1, p2, p3, p4 ;
    // a seconda dell'attributo modificato
    switch ( [ sender tag ] ) {
    case 10 :     // stato di riempimento
        [ theSelectedElem setCceIsFilled: [ fillCheckbox state ]];
        break ;
    case 11 :     // colore di riempimento
        [ theSelectedElem setCceFillColor: [ fillColorWell color ]];
        break ;
    case 12 :     // stato di disegno linea/contorno
        [ theSelectedElem setCceIsStroked: [ lineCheckbox state ]];
        break ;
    case 13 :     // colore della linea
        [ theSelectedElem setCceLineColor: [ lineColorWell color ]];
        break ;
    case 14 :     // dimensione della linea
    case 15 :
        [ theSelectedElem setCceLineWidth: [ lineWidthSlider floatValue ]];
        break ;
    case 16 :     // coordinate limite dell'elemento
    case 17 :
        p1 = [xTextField floatValue ];
        p2 = [yTextField floatValue ];
        [ theSelectedElem setDrawPoint: NSMakePoint( p1, p2) ];
        [ theSelectedElem calcLocalTranform ];
        break ;
    case 18 :
    case 19 :
        // recupero i quattro valori
        p3 = [widthTextField floatValue ];
        p4 = [heightTextField floatValue ];
        [theSelectedElem resizeMeWith: NSMakeSize( p3, p4 ) ];
        break ;
    case 20 :     // angolo di rotazione
    case 21 :
        [ theSelectedElem rotateMeAtAngle: [ rotationSlider floatValue ] ];
        break ;
    default:
        NSLog( @"changed %@", sender );
    }
    // forzo un ridisegno
    [ [theSelectedElem ownerView] setNeedsDisplay: YES ];
    // ed anche un aggiornamento delle informazioni
    [ self updateWindowInfo ];
}

Oltre ai due nuovi casi relativi alla rotazione, sono stati divisi i casi relativi allo spostamento del punto origine ed alla variazione delle dimensioni dell'elemento.

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