Pagine

2012-11-18

I segreti degli NSSplitView - II

Nella scorsa puntata abbiamo esaminato come sia possibile limitare le dimensioni delle viste di uno NSSplitView ed anche come fare in modo che l'utente possa far scomparire una vista, senza che questa si offenda e distribuisca a caso gli oggetti contenuti.
Ora continuiamo nel nostro viaggio per scoprire altre cose interessanti.

Ridimensionare educatamente

Il comportamento di uno SplitView quando la finestra in cui è contenuto si ridimensiona è generico e spesso irritante: non fa altro che mantenere le proporzioni tra le viste. Così, se a schermo pieno abbiamo ridotto una delle viste a 30 pixel, quando usciamo dal full screen potremmo trovare che la stessa vista è ora di 5 pixel o meno! Non c'è differenza se le dimensioni della finestra sono cambiate da codice o a mano dall'utente: il signor SplitView non ne vuol sapere di controllare il suo delegate per sapere se può stringere una vista.
Considerando l'esempio della prima parte, con uno SplitView fatto di 3 viste, noi vorremmo che la prima (di solito una SourceList) e l'ultima (di solito un Inspector) mantenessero costanti le proprie dimensioni, variando solamente la vista centrale.
Con MountainLion, Apple ha introdotto la priorità delle viste, con cui si può risolvere il problema, ma al momento non ce la sentiamo di tagliar fuori tutti gli utenti che stanno utilizzando Lion e SnowLeopard, per cui dobbiamo di nuovo rimboccarci le maniche e cominciare a battere sulla tastiera.
Il metodo che ci viene in aiuto è sempre del delegate:
- (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
Questo viene continuamente chiamato durante il ridimensionamento dello SplitView (conseguente a quello della finestra) e propone una nuova dimensione (oldSize) che il delegate può accettare o meno; dobbiamo fare in modo che la prima e la terza vista restino costanti, modificando solo quella centrale.
Cominciamo col registrarci alcuni dati che ci torneranno utili nel metodo:
- (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
{
    NSView *leftView = [[sender subviews] objectAtIndex:0];
    NSView *centerView = [[sender subviews] objectAtIndex:1];
    NSView *rightView = [[sender subviews] objectAtIndex:2];
    float dividerThickness = [sender dividerThickness];
    NSRect splitFrame = [sender frame];
    NSRect leftFrame = [leftView frame];
    NSRect centerFrame = [centerView frame];
    NSRect rightFrame = [rightView frame];
    BOOL collapsed = [sender isSubviewCollapsed:rightView];
Notiamo che alla riga 11 teniamo conto se la vista di destra è collassata o meno.
Ora impostiamo le cose più semplici: per ogni sotto-view impostiamo che l'altezza sia sempre quella dello SplitView e che la view a sinistra resti sempre incollata all'angolo sinistro:
. leftFrame.size.height = splitFrame.size.height;
 centerFrame.size.height = splitFrame.size.height;
 rightFrame.size.height = splitFrame.size.height;
 leftFrame.origin = NSMakePoint(0, 0);
Ora arriva la parte importante: modifichiamo la larghezza della view centrale, calcolando quanto deve essere grande se le altre viste restano costanti, ma tenendo anche conto della condizione di collapsed (e ricordiamoci della larghezza dei divider!):
. centerFrame.size.width = !collapsed ? (splitFrame.size.width - leftFrame.size.width - rightFrame.size.width - 2*dividerThickness) : (splitFrame.size.width - leftFrame.size.width - dividerThickness);
 centerFrame.origin = NSMakePoint(leftFrame.size.width + dividerThickness, 0);
Ora osserviamo che in questo modo la vista centrale potrebbe diventare più piccola di quanto abbiamo impostato: non viene impedito dal delegate, per cui lo dobbiamo impedire noi! Se succede, impostiamo la view centrale al valore minimo ammesso e passiamo a cambiare la dimensione della view di sinistra, sempre tenendo conto se la terza view è collassata o meno:
. if (centerFrame.size.width <= kMinWidthTable)
 {
  centerFrame.size.width = kMinWidthTable;
  leftFrame.size.width = !collapsed ? (splitFrame.size.width - 2*dividerThickness - kMinWidthTable - rightFrame.size.width) : (splitFrame.size.width - kMinWidthTable - dividerThickness);
  centerFrame.origin = NSMakePoint(leftFrame.size.width + dividerThickness, 0);
 }
Ricordiamoci di impostare la posizione della vista destra (se non collassata):
. if ( ! collapsed )
 {
  rightFrame.origin = NSMakePoint(splitFrame.size.width - rightFrame.size.width, 0);
 }
e concludiamo dando i nuovi frame alle viste:
. [leftView setFrame:leftFrame];
 [centerView setFrame:centerFrame];
 if (! collapsed) [rightView setFrame:rightFrame];
}
Compiliamo e divertiamoci con uno splitView ora beneducato!

2012-11-04

I segreti degli NSSplitView - I

È difficile trovare un'applicazione per MacOS X che non faccia uso degli oggetti NSSplitView: in questo modo si lascia all'utente la scelta di quanto mostrare delle varie viste necessarie per le attività.
Come al solito, quando una cosa è semplice per l'utente, aumentano le difficoltà per il programmatore! A questo si aggiunge che Cocoa fornisce tutti i mezzi necessari ad un funzionamento di base, ma se stiamo cercando di costruire un'interfaccia all'altezza delle aspettative, allora dobbiamo rimboccarci le maniche e cominciare a scrivere del codice aggiuntivo.

Creazione dello split

La cosa più semplice è, nell'IB, di creare le le view che saranno contenuti nello split, selezionarli tutti e scegliere il menu Editor > Embed In > splitView. Oppure si può inserire prima lo split e poi riempire le view che questo si porta dietro, eventualmente aggiungendone di nuove. Il numero di NSView al primo livello dello split determina quanti saranno i divisori e quindi le view gestite.
Nel seguito supporremo sempre di avere uno split view con viste affiancate: nel caso in cui le volessimo sovrapposte basterà scambiare le larghezze con le altezze.

Dimensioni massime - minime

Diciamo subito che un NSSplitView, proprio perché usato per suddividere una finestra, di solito la riempie tutta; allora le dimensioni dello split seguono allegramente quelle della finestra (posto di aver messo i giusti vincoli nell'inspector dell'XCode per il dimensionamento automatico), per cui basta impostare il massimo ed il minimo per la finestra, tramite l'inspector di XCode.
Il problema invece riguarda le view contenute nello split: di default un divisore può essere spostato fino a che non arriva ad un altro divisore, azzerando la dimensione della vista che contiene; in questo modo scopriamo subito che le viste non amano essere ridotte a zero: quando vengono riaperte gli oggetti contenuti vagano senza ritegno in posti impensati. Purtroppo la classe NSSplitView non possiede metodi per impostare l'ampiezza massima e minima di una view e di conseguenza non possono nemmeno essere inseriti da Interface Builder. È il momento di scrivere codice!
Tutte i metodi interessanti si trovano nel protocollo del delegate. Quindi, per prima cosa dobbiamo informare lo splitView che da ora possiede un delegate: questo può essere fatto da XCode (collegando il suo outlet delegate all'oggetto delegate) oppure direttamente da codice con il metodo -setDelegate: seguito dall'oggetto eletto a questo rango.
Personalmente, preferisco dedicare un oggetto apposito per queste cose, per cui creiamo una nuova classe, sottoclasse di NSObject e dichiariamo che seguirà le leggi previste per il delegate di uno splitView. In termini semplici, il nostro header si presenterà come:
interface mySplitDelegate : NSObject <NSSplitViewDelegate>
end
e non dichiariamo alcun metodo, poiché l'essere un delegate ne prevede già una serie, tra cui quelli che servono a noi. In particolare, quelli preposti alla sorveglianza delle dimensioni sono:
- splitView:constrainMinCoordinate:ofSubviewAt:
- splitView:constrainMaxCoordinate:ofSubviewAt:
dove la prima gestisce il minimo, la seconda il massimo.
Questi metodi sono chiamati dallo split quando vuole sapere se permettere uno spostamento di un divisore e per saperlo passa al delegate lo split in questione (primo parametro), la proposta di valore minimo nelle coordinate dello splitView (secondo parametro, è quello che userebbe se questi metodi non fossero implementati), il divisore che si sta muovendo (i divisori sono numerati da 0, come se si riferissero alla vista alla loro sinistra).
Come esempio, consideriamo il caso di uno split a 3 viste (2 divisori) in cui vogliamo che ciascuna di esse non possa essere azzerata; definiamo con delle macro queste dimensioni:
#define kMinWidthView1   100   //minimo vista 1, a sinistra
#define kMinWidthView2   200   //minimo vista 2, centrale
#define kMinWidthView3    50   //minimo vista 3, a destra
Supponiamo di muovere il divisore 0 (tra la prima e la seconda vista). La proposta che arriva al delegate è per un valore minimo di 0 (divisore tutto a sinistra, collassando la prima vista); noi invece ritorneremo un valore di 100 pixel. Se invece muoviamo il secondo divisore, la proposta non sarà 0 ma sarà uguale alla larghezza della prima vista a cui viene aggiunta la larghezza del divisore. Ma noi siamo inflessibili e gli rispondiamo che a questa posizione dovrà aggiungere 100. Sotto forma di codice:
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex
{
  CGFloat constrained = proposedMinimumPosition;
  float dividerThickness = [splitView dividerThickness];
  switch (dividerIndex) {
    case 0:
      constrained = proposedMinimumPosition + kMinWidthView1;
      break;
    case 1:
      constrained = proposedMinimumPosition + dividerThickness + kMinWidthView2;
      break;
  }
  return constrained;
}
I casi sono tutti quelli possibili, dato che abbiamo solo 2 divisori. Ma come controllo la larghezza della terza view? Ovvio: inserendo un limite sulla dimensione massima della seconda view! Perciò implementiamo anche il secondo metodo (in effetti, devono sempre andare in coppia). La trovata infatti è che la dimensione massima di una vista è quella che non costringe la vista vicina a scendere sotto la sua dimensione minima. In pratica:
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex
{
  CGFloat constrained = proposedMaximumPosition;
  float dividerThickness = [splitView dividerThickness]; 
  switch (dividerIndex) {
    case 0:
      constrained = proposedMaximumPosition - 200 - dividerThickness;
      break;
    case 1:
      constrained = proposedMaximumPosition - 50;
      break;
  }
  return constrained;
}
Ora lanciamo l'applicazione e scopriremo che in effetti lo split si comporta come previsto, qualunque cosa facciamo con i divisori!
Notiamo che non abbiamo avuto bisogno di utilizzare la dimensione dello splitView: è lui stesso che ne tiene conto, passandoci la sua proposta di dimensione massima (che infatti non può andare oltre il collasso della vista vicina oppure oltre la dimensione della finestra che lo contiene).

La vista che scompare

Abbiamo ancora un problema: se l'utente decidesse che la terza vista è troppo noiosa, potrebbe volerla togliere di mezzo, ma non potrebbe perché il delegate lo impedisce! Ma è lo stesso delegate che si riscatta, mettendo a disposizione un altro metodo, che può coesistere con i due precedenti: si tratta di -splitView:canCollapseSubview:. Se si tratta della vista che vogliamo lasciar chiudere, ritorniamo TRUE altrimenti FALSE:
#define kViewToCollapse  2
- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview
{
  NSView *rightView = [[splitView subviews] objectAtIndex:kViewToCollapse];
  return [subview isEqual:rightView];
}
dove abbiamo approfittato del fatto che la proprietà -subviews ritorna un NSArray contenente le viste dello split.
Ora l'utente può far collassare la terza vista continuando a trascinare il divisore oltre il limite minimo: se mantiene la pressione sul mouse, quando arriva a circa la metà dell'ampiezza della vista, la vista stessa collassa e scompare! Per tornare indietro, basta andare col mouse sul bordo e spostare il divisore verso sinistra: la vista comparirà nuovamente, prendendo la dimensione minima prevista.
Notiamo che questo non crea problemi agli oggetti all'interno della vista: in effetti il collasso della vista non riduce la sua dimensione a 0, ma semplicemente la fa scomparire; quando la riapriamo, la ritroviamo con la stessa dimensione di prima, con gli oggetti ordinatamente al loro posto.