In questo capitolo tratto alcuni argomenti legati tra loro dalla tastiera, ed in particolar modo dal tasto maiuscole. Nel fare ciò scopro qualcosa diinteressante sul sistema operativo.
Da Sketch, ma poi documentazione Apple.
Primo inserimento: 5 aprile 2004
Attraverso il mouse, ho imparato come selezionare un elemento (facendoci sopra clic) o più elementi contemporaneamente (disegnando un rettangolo, e selezionando gli elementi compresi nel suo interno). Tuttavia, un metodo classico di selezione multipla di elementi consiste nell'utilizzare il tasto delle maiuscole. In pratica, esiste il concetto di selezione corrente, come insieme degli elementi selezionati. Normalmente vuota (nessun elemento selezionato), facendo clic sopra un elemento non ancora selezionato col tasto maiuscole premuto, l'elemento si aggiunge alla selezione; se l'elemento è già selezionato, il tipico risultato è di toglierlo dall'insieme. Provo a riassumere il tutto in quattro casi possibili:
Ora, ero partito veloce definendo un gruppo (temporaneo) CCE_ElemGroup dove conservare tutti gli elementi selezionati, ma poi mi sono ricreduto. In effetti, non occorre definire esplicitare l'insieme: basta considerare implicitamente l'insieme come composto da tutti quegli elementi che hanno la variabile d'istanza cceIsSelected a Vero. Quindi, le operazioni di gestione si limitano a cambiare lo stato di questa variabile nei vari casi:
Sembra una cosa facile, ma come tutte le cose facili nasconde un problema enorme (si fa per dire). Il meccanismo di spostamento non funziona più. Se eseguo una selezione multipla, mi aspetto che l'operazione di spostamento lavori su tutti gli elementi selezionati. Tuttavia, per come ho finora predisposto i metodi, il meccanismo di spostamento è ad personam: incarico l'elemento stesso di eseguire lo spostamento (gestendo gli eventi successivi legati al movimento del mouse) invocando il metodo della classe CCE_BasicForm
- (void) handleMoveElem: (NSEvent *)theEvent inView: (CoverView *)view ;
C'è di peggio: all'interno di questo metodo trattavo anche il caso in cui il clic avviene sopra uno degli handle. Allora interpretavo l'intenzione dell'utente non più come uno spostamento, ma come un ridimensionamento. Nel caso di selezione multipla, le due operazioni si pestano i piedi a vicenda: se, con una selezione multipla, l'utente fa clic sopra uno degli handle, come interpreto la cosa? Mi sono risposto nel seguente modo (è solo una delle possibili soluzioni): se l'utente fa clic sopra un handle, intende ridimensionare solo l'elemento cui lo handle appartiene; esegue il ridimensionamento, e l'insieme degli elementi selezionati non subisce modifiche. Se l'utente fa clic all'interno di uno qualsiasi degli elementi selezionati, intende spostare tutti gli elementi selezionati.
Per mettere in pratica quanto detto, ho riscritto quasi completamente il seguente metodo:
- (void)
handleMouseClick: (NSEvent *)theEvent
{
NSPoint whereClick;
CCE_BasicForm * curElem = nil;
BOOL shiftKeyPress = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
// vado a vedere dove e' avvenuto il click
whereClick = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// niente niente l'utente vuole selezionare un elemento
curElem = [self getClickedElem:whereClick];
// potrebbe aver fatto clic su uno degli hangle
// se c'e' un elemento sotto il mouse
if ( curElem )
{
short clicInHandle = [ curElem clicIntoHandle: whereClick ] ;
// abbiamo qualcosa sotto il mouse
// vedo se e' gia' selezionato
if ( [ curElem cceIsSelected ] )
{
if ( shiftKeyPress ) // caso 1
{
// lo tolgo dagli elementi selezionati
[ curElem setCceIsSelected: NO ];
}
else // caso 2
{
// provo a vedere se e' il caso di muoverlo
if ( clicInHandle == -1 )
[ self handleMoveAll: theEvent ];
else [ curElem handleResize: theEvent inView: self movingHandle: clicInHandle ];
}
}
else
{
if ( ! shiftKeyPress ) // caso 4
{
// deseleziono tutto
[ theElements setCceIsSelected: NO ] ;
}
// da qui in poi, caso 3 (ed anche caso 4)
// seleziono questo elemento
[ curElem setCceIsSelected: YES ];
// ridisegno tutto che la vista e' cambiata...
[ self setNeedsDisplay: YES ];
// provo a vedere se e' il caso di muoverlo
if ( clicInHandle == -1 )
[ self handleMoveAll: theEvent ];
else [ curElem handleResize: theEvent inView: self movingHandle: clicInHandle ];
}
}
else
{
NSRect selRect ;
// comincio la selezione multipla
selRect = [ self handleRectSelect: theEvent ];
[ theElements selectMeIfInRect: selRect addingToSelez: shiftKeyPress ];
}
// ridisegno tutto, in ogni caso
[ self setNeedsDisplay: YES ];
}
Per prima cosa, cerco di capire se l'utente ha premuto il tasto maiuscole, guardando una delle tante informazioni accessorie fornite con l'evento (appunto, se il tasto shift -maiuscole- è premuto). Metto da parte questa informazione, che mi serve poi.
Ho spostato qui alcune istruzioni prima presenti nel metodo handleMoveElem:..., per capire se il clic dell'utente è sopra un elemento e se è addirittura sopra uno degli handle.
Finalmente, comincio a valutare i quattro casi, annidando un paio di istruzioni if. Nel caso 2, sostituisco la precedente chiamata al metodo handleMoveElem con una ulteriore istruzione if, che si chiede se il clic è avvenuto sopra un handle oppure no. Nel caso di clic sopra un handle, ritorno ad utilizzare il metodo handleResize..., ed è finita lì. Se invece si intende spostare gli elementi, ho il nuovo messaggio handleMoveAll:... che gestisce per l'intera view il movimento del mouse e di tutti gli elementi selezionati (ne parlo poi). Il caso 3 ed il caso 4 sono molto simili; il caso 4, prima di tutto, svuota la selezione corrente col tipico meccanismo di porre a Falso cceIsSelected dell'insieme di tutti gli elementi (che poi a catena si riverbera su tutti gli altri). In entrambi i casi, seleziono l'elemento considerato, e continuo a pormi la solita questione se il cli è sopra un handle o meno.
C'è da fare una ulteriore modifica, perché adesso anche la selezione muovendo il mouse deve tenere conto del tasto maiuscole. Il metodo handleRectSelect ha subito una piccola modifica.
- (NSRect)
handleRectSelect: (NSEvent *)theEvent
{
NSRect selRect = NSMakeRect(1, 1, 0, 0);
// costruisco l'elemento grafico
CCE_Rect * curElem = [[ [CCE_Rect alloc ] init] autorelease ] ;
BOOL shiftKeyPress = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
// metto a posto un di attributi base dell'elemento
[ curElem setCceLineWidth: 0.0F ];
[ curElem setCceLineColor: [NSColor blackColor] ];
[ curElem setCceFillColor: [NSColor lightGrayColor] ];
[ curElem setCceIsFilled: NO ];
[ curElem setCceIsStroked: YES ];
if ( ! shiftKeyPress )
{
// deseleziono tutto
[ theElements setCceIsSelected: NO ] ;
}
// dico che l'elemento che sto disegnando e' selezionato
[ curElem setCceIsSelected: NO ];
[ self setCurSelectRect: curElem ] ;
// gestisco la costruzione col mouse
[curElem handleCreation: theEvent inView: self ] ;
selRect = [ curElem elemLimit] ;
[ self setCurSelectRect: nil ] ;
return ( selRect );
}
Molto semplicemente, se il tasto maiuscole è premuto, non devo cancellare la selezione corrente.
Più complicata invece la gestione di questa selezione rettangolare, che mi ha portato a modificare sostanzialmente il metodo utilizzato da questa istruzione:
[ theElements selectMeIfInRect: selRect addingToSelez: shiftKeyPress ];
Ho aggiunto un parametro, che dice se aggiungere o meno gli elementi compresi nel rettangolo selRect alla selezione corrente. Anche qui, ci sono un po' di considerazioni da fare, con quattro casi possibili:
La versione standard del metodo (classe CCE_BasicForm) è la seguente.
- (void)
selectMeIfInRect: (NSRect) selRect addingToSelez: (BOOL) add2sel
{
NSRect mioRect = [self elemLimit] ;
BOOL inrect = NSContainsRect( selRect, mioRect ) ;
// se non devo aggiungere alla selezione
if ( ! add2sel ) // casi a e c
{
// vince il criterio del rettangolo
[ self setCceIsSelected: inrect ];
}
else
{
// qui e' da aggiungere alla selezione
// se sono dentro il rettangolo
if ( inrect ) // caso d
// per certo lo seleziono
[ self setCceIsSelected: YES ];
// altrimenti lascio la selezione precedente, caso b
}
}
Le classi CCE_Line e CCE_ElemGroup possiedono versioni specializzate (la linea utilizza un criterio differente, mentre il gruppo itera il metodo sugli elementi appartenenti).
Torno adesso sul metodo che avevo lasciato indietro, quello necessario per effettuare movimenti di tutti gli elementi selezionati. Non è altro che una versione riveduta e corretta del metodo handleMoveElem::
- (void)
handleMoveAll: (NSEvent *)theEvent
{
// recupero la coordinata locale
NSPoint point1 = [self convertPoint:[theEvent locationInWindow] fromView:nil];
NSPoint point2 ;
// ciclo piu' o meno lungo di attesa eventi
do {
// recupero il prossimo evento, che sia un movimento del mouse
// oppure un mouseup (che finisce il tutto)
theEvent = [[self window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
// recupero la coordinata locale
point2 = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// aggiusto l'aspetto dell'oggetto
[ theElements moveMeIfSelectBy: NSMakePoint(point2.x-point1.x,point2.y-point1.y)];
// il nuovo punto di partenza
point1 = point2 ;
// forzo un ridisegno della finestra
[ self setNeedsDisplay: YES ];
}
while ( [theEvent type] != NSLeftMouseUp) ;
}
La parte interessante è che, una volta stabilita l'entità dello spostamento, mando a tutti gli elmenti presenti nella view (all'elemento theElements, e da qui a cascata su tutti gli altri, sempre col solito meccanismo) il messaggio moveMeIfSelectBy:, di facile realizzazione (è sostanzialmente il vecchio shiftMeFrom:to:):
- (void)
moveMeIfSelectBy: (NSPoint) distance
{
if ( cceIsSelected )
{
[ self setElemLimit: NSOffsetRect(elemLimit, distance.x, distance.y)];
[ self buildMeWithStartPt: NSMakePoint(elemLimit.origin.x, elemLimit.origin.y)
andPt: NSMakePoint( elemLimit.origin.x + elemLimit.size.width,
elemLimit.origin.y + elemLimit.size.height) ];
}
}
L'unica modifica è che l'operazione è svolta solamente se l'elemento è selezionato. Ancora una volta, le classi CCE_Line e CCE_ElemGroup sono dotate di un metodo proprio, per trattare le differenze col caso standard.
Un altro uso classico del tasto shift si ha nel momento in cui si costruisce un elemento, ad esempio un rettangolo; tenendo premuto il tasto maiuscole, molti programmi di disegno forzano il rettangolo ad essere un quadrato, come pure il tracciamento di una ellisse diventa quello di un cerchio. Voglio fare la stessa cosa, con in più la possibilità di forzare il tracciamento di segmenti orizzontali, verticali e inclinati a 45 gradi quando il solito tasto maiuscole è tenuto premuto.
Per fare questo, devo modificare il metodo handleCreation. Comincio dalla versione per quadrati e cerchi, che mi pare più comprensibile. L'idea è di inserire una correzione sul secondo punto (quello in movimento) per far si che questo punto sia l'altro vertice di un quadrato.
Allora, per prima cosa, calcolo le due distanze delta1 e delta2, orizzontale e verticale, tra il punto fisso point1 ed il punto mobile point. Tra le due, scelgo quella più piccola: l'effetto è di tirare il quadrato da fuori. Devo tenere conto del fatto che più piccola significa in valore assoluto, e devo anche tenere conto della posizione relativa del secondo punto rispetto al primo. Per questo tengo conto della posizione relativa con due variabili dirx e diry che dicono, col valore positivo, che il punto point sta a destra (sopra) il punto point1, e col valore negativo che sta a sinistra (o sotto). Stabilito questo, forzo l'una o l'altra coordinata del punto mobile in modo da dare luogo ad un quadrato. È come al solito più difficile da spiegare che da vedere.
- (BOOL)
handleCreation: (NSEvent *)theEvent
inView: (CoverView *)view
{
// recupero la coordinata locale
NSPoint point1 = [view convertPoint:[theEvent locationInWindow] fromView:nil];
NSPoint point ;
BOOL shiftKeyPress ;
// inizializzo l'aspetto dell'oggetto (praticamente vuoto)
[ self buildMeWithStartPt: point1 andPt: point1 ] ;
// continuo a ciclare in attesa di un mouseup
do {
// recupero il prossimo evento, che sia un movimento del mouse
// oppure un mouseup (che finisce il tutto)
theEvent = [[view window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
shiftKeyPress = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
// recupero la coordinata locale
point = [view convertPoint:[theEvent locationInWindow] fromView:nil];
if ( shiftKeyPress )
{
// costringo il rettangolo ad essere quadrato
float delta1 = ( point.x - point1.x ) ;
float delta2 = ( point.y - point1.y ) ;
float dirx = 1 ;
float diry = 1 ;
if ( delta1 < 0 )
{
delta1 = - delta1 ; dirx = -1 ;
}
if ( delta2 < 0 )
{
delta2 = - delta2 ; diry = -1 ;
}
// piglio la dimensione piu' corta
if ( delta1 < delta2 )
{
point.y = point1.y + diry * delta1 ;
}
else
{
point.x = point1.x + dirx * delta2 ;
}
}
// aggiusto l'aspetto dell'oggetto
[ self buildMeWithStartPt: point1 andPt: point ] ;
// forzo un ridisegno della finestra
[ view setNeedsDisplay: YES ];
}
while ( [theEvent type] != NSLeftMouseUp) ;
// arrivato qui, ho finito il tutto
return YES;
}
Per quanto riguarda la classe CCE_Line, mi complico la cosa perché voglio tracciare linee orizzontali, verticali e a 45 gradi. Devo capire bene dove si posiziona il punto mobile rispetto al punto fisso. Ecco il frammento di codice eseguito a tasto maiuscole premuto:
// costringo il rettangolo ad essere quadrato
float delta1 = ( point.x - point1.x ) ;
float delta2 = ( point.y - point1.y ) ;
float dirx = 1 ;
float diry = 1 ;
if ( delta1 < 0 )
{
delta1 = - delta1 ; dirx = -1 ;
}
if ( delta2 < 0 )
{
delta2 = - delta2 ; diry = -1 ;
}
// piglio la dimensione piu' corta
if ( delta1 < delta2*0.2 )
{
point.x = point1.x ;
}
else if ( delta2 < delta1*0.2 )
{
point.y = point1.y ;
}
else if ( delta1 < delta2 )
{
point.y = point1.y + diry * delta1 ;
}
else
{
point.x = point1.x + dirx * delta2 ;
}
Dopo aver calcolato, come nel caso precedente, la differenza tra le ascisse e le ordinate, ed aver predisposto la direzione di movimento, mi chiedo se la distanza in ascissa è molto più piccola della distanza in ordinata. In tal caso, presuppongo di dover tracciare una riga in verticale, e quindi impongo al punto mobile la stessa ascissa del punto fisso. Se invece vale il caso opposto, la differenza in verticale è molto minore della differenza in orizzontale, presumo di dover tracciare una linea orizzontale, ed impongo uguali ordinate. In tutti gli altri casi, ricado nella situazione precedente, e calcolo la nuova ascissa o ordinata nel modo solito.
Ho trovato un errore nel meccanismo di costruzione degli oggetti di classe CCE_Line. Il problema riguarda la variabile d'istanza numOfHdl. Se la costruzione di una CCE_Line avviene passando attraverso il metodo:
- (id) initStartPt: (NSPoint) stPt toPt: (NSPoint) endPt ;
la cosa funziona correttamente. Se invece l'inizializzazione avviene attraverso un metodo generico di init, come ad esempio accade all'interno del metodo mouseDown: della classe CoverView, la variabile rimane inizializzata a otto, invece che a due come vorrei.
Per risolvere la cosa, non ho trovato nulla di meglio che assegnare direttamente il valore all'interno del metodo:
- (void) buildMeWithStartPt: (NSPoint) stPt andPt: (NSPoint) endPt ;
Non mi pare una buona soluzione (la variabile è continuamente assegnata), ma non ho voglia di cercare una soluzione più intelligente.
Visto che ho cominciato ad utilizzare in qualche misura la tastiera, volevo rendere disponibile una funzionalità tipica di un programma per il disegno, ovvero lo spostamento degli oggetti utilizzando i tasti freccia della tastiera. Per capire come fare, ho guardato il solito esempio Sketch fornito da Apple. Qui in effetti è disponibile tale funzionalità; bene, mi son detto, copio biecamente. Ho visto un semplice metodo, con una unica istruzione:
- (void)keyDown:(NSEvent *)event
{
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
}
E basta. Nient'altro. Non ho trovato da nessun parte un meccanismo che associasse alla tastiera l'esecuzione di particolari operazioni. Guardando qui e lì il codice, ho trovato un po' di metodi che a quanto pare eseguono il lavoro necessario; ad esempio, per lo spostamento verso sinistra, è disponibile
- (void)moveLeft:(id)sender ;
Non c'è stato a lungo verso che trovassi l'associazione tra i tasti freccia e l'invocazione di questi metodi. Dopo infinita pena, uso estensivo del Debugger, ma soprattutto lettura della documentazione, ho trovato il bandolo della matassa. Si tratta di una funzionalità standard offerta da Cocoa nel quadro del sistema di gestione del testo. Spendo due parole per ricapitolare la documentazione letta.
La gestione del testo utilizza tre classi: un input server, che si preoccupa di ricevere caratteri, elaborarli e poi inviarli ad un sistema di visualizzare; una text view, che appunto visualizza i caratteri; un input manager che si piazza davanti ad un input server, gestendo direttamente la tastiera e mandando messaggi al server stesso. La situazione classica vede la text view che riceve i caratteri grezzi; li passa al suo input manager, che li filtra. Normalmente, la maggior parte dei caratteri è passata allo input server che se li manipola, li trasforma, ne fa quel che crede, e poi li restituisce alla text view perché li visualizzi. Vi sembra complicato? in effetti lo è, ma ci sono molte cose interessanti eseguite in tutti questi passaggi. Ad esempio, lo input manager potrebbe accorgersi che l tasto premuto non è un normale carattere alfanumerico, ma, ad esempio, una freccia (!). Ora, non serve passare tale informazione al server, che non saprebbe che farsene. In effetti, lo input manager cerca di chiamare un metodo apposito. Lo input manager ha quindi a disposizione un dizionario, il key-binding dictionary, in cui a particolari combinazioni di tasti sono associati dei metodi. Dov'è questo dizionario? Basta leggere la documentazione per scoprire che esiste il seguente file:
/System/Library/Frameworks/AppKit.framework/Resources/StandardKeyBinding.dict
dove sono appunto conservate le associazioni standard. Sono andato a guardare questo dizionario, ad esempio con l'applicazione Property List Editor (come al solito, si tratta di un file in un formato XML). E qui sono presenti circa duecento combinazioni di tasti, descritti in un incomprensibile formato unicode; ad alcune combinazioni sono associati dei nomi di metodo. Verso il fondo della lista, ad esempio, sono indicati quattro metodi, moveUp, moveDown, moveLeft e moveRight, che sono associati normalmente alle quattro frecce. Se poi la freccia è premuta assieme al tasto maiuscole, i metodi di riferimento sono moveUpAndModifySelection, moveDownAndModifySelection, moveLeftAndModifySelection e moveRightAndModifySelection (come ho fatto a saperlo? Prove, prove e prove).
A questo punto, tutto si risolve in un gioco da ragazzi. Basta scrivere i seguenti metodi, i quali, appunto, di volta in volta chiedono gentilmente di spostarsi del giusto (di un solo pixel nel caso di freccia semplice, di dieci pixel quando c'è anche il tasto maiuscole premuto) a tutti gli elementi selezionati.
// binding per la quattro frecce
- (void)moveLeft:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(-1.0, 0.0)];
[ self setNeedsDisplay: YES ];
}
- (void)moveRight:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(1.0, 0.0)];
[ self setNeedsDisplay: YES ];
}
- (void)moveUp:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(0.0, -1.0)];
[ self setNeedsDisplay: YES ];
}
- (void)moveDown:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(0.0, 1.0)];
[ self setNeedsDisplay: YES ];
}
// binding per la quattro frecce con tasto maiuscole
- (void)moveLeftAndModifySelection:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(-10.0, 0.0)];
[ self setNeedsDisplay: YES ];
}
- (void)moveRightAndModifySelection:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(10.0, 0.0)];
[ self setNeedsDisplay: YES ];
}
- (void)moveUpAndModifySelection:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(0.0, -10.0)];
[ self setNeedsDisplay: YES ];
}
- (void)moveDownAndModifySelection:(id)sender {
[ theElements moveMeIfSelectBy: NSMakePoint(0.0, 10.0)];
[ self setNeedsDisplay: YES ];
}
È forse finita qui? Nemmeno per sogno. Infatti, non funziona. Ancora una volta, dopo lungo peregrinare, esame di esempi e di documentazione, mi sono accorto di una cosa ovvia. Occorre che la CoverView sia indicata come la prima destinataria degli eventi associati alla tastiera (che altrimenti son dirottati verso altre gerarchie). Per fare questo, occorre aggiungere, all'interno del metodo windowDidLoad della classe CoverWinCtl la seguente istruzione (magari verso la fine del metodo stesso):
[[self window] makeFirstResponder:cvrView];
che appunto stabilisce che la CoverView è il primo destinatario di ogni evento (in particolare, del fatto che qualcuno pesti sui tasti).
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).