Pagine

2014-01-01

La NSWindow che non voleva ilFrame...

Quanti di voi, amici sviluppatori, cambiano posizione e dimensioni ad una NSWindow usando il suo bravo metodo

[mainWindow setFrame:presentWindowRect display:YES];

oppure il metodo esteso con l'animazione

[mainWindow setFrame:presentWindowRect display:YES animate:YES];

per esempio per aggiungere in modo elegante una NSView per determinati scopi? Siete in molti? Bene.
E quanti di voi pensano ai propri utenti e salvano in automatico la posizione delle finestre, inserendo un nome a piacere nel campo Autosave di Interface Builder? Sempre molti, direi.
Ancora una domanda: quanti cercano di mantenere la compatibilità con Lion? Suppongo una buona parte.
Ok, ora che abbiamo selezionato l'uditorio, partiamo!

In questi giorni, sto mettendo a punto una nuova versione di InerziaTimer, passando ad una interfaccia più moderna e semplice, magari utilizzando una finestra NSBorderlessWindow costruita ad hoc ed ovviamente sviluppando con SDK 10.9 (ma la compatibilità deve andare indietro fino al 10.6).
Decido di mostrare in ogni momento solo gli elementi necessari alla funzione selezionata: quindi la solita trafila della NSView caricata da uno xib aggiuntivo tramite un NSViewController, con la finestra principale che si allunga per far posto alla nuova vista.
Dato che OS X calcola le coordinate dello schermo partendo dal basso, mentre l’utente si aspetta che la finestra mantenga fissa la parte superiore, bisogna fare un po’ di calcoli; in pratica:

NSRect presentWindowRect = [mainWindow frame]; //frame della NSWindow
NSRect viewRect = [accessoryView frame]; //frame della nuova vista da aggiungere
presentWindowRect.size.height = presentWindowRect.size.height + viewRect.size.height;
presentWindowRect.origin.y = presentWindowRect.origin.y - viewRect.size.height;
[mainWindow setFrame:presentWindowRect display:YES animate:YES];
[[mainWindow contentView] addSubview:accessoryView];

Questo codice funziona già per la finestra delle preferenze, dove ogni pannello ha altezze diverse e la finestra aggiusta la propria in conseguenza.

La prima implementazione aveva un problema: la finestra principale viene caricata all'inizio senza vista, mentre alla chiusura dell’app laNSView era presente. Per cui la posizione salvata all'uscita dall'app era relativa ad una finestra allungata, non a quella senza la NSView aggiuntiva che veniva caricata nella sessione successiva; il risultato era che ad ogni chiusura e riapertura, la finestra principale si posizionava sempre più in basso sullo schermo, fino ad uscirne! Ok, colpa mia (è sempre così); prima di chiudere l’app bisogna togliere la NSView, in modo che venga salvata la posizione della NSWindow da sola. È semplice: nel metodo -applicationShouldTerminate basta togliere la NSView e accorciare la finestra, sempre tenendo conto che l’origine del frame è in basso a sinistra:

NSRect presentWindowRect = [mainWindow frame]; //frame attuale della finestra con vista
CGFloat altezzaVista = [accessoryView frame].size.height;  //altezza della NSView
presentWindowRect.origin.y += altezzaVista;            //spostiamo la NSWindow in su..
presentWindowRect.size.height -= altezzaVista;            //e la accorciamo di conseguenza
[mainWindow setFrame:presentWindowRect display:NO];        //nessun display tanto la stiamo chiudendo

Il risultato è che la parte superiore resta fissa, si accorcia l’altezza e la finestra si presenta pronta per la chiusura.
Bene. Effettuo la modifica e lancio il tutto: perfetto, come aspettavo! Poi, dopo un altro po’ di sviluppo, provo il funzionamento di una nuova caratteristica sul 10.7 e... scopro che ad ogni riavvio la finestra si sposta in alto, fin quasi a scomparire! Penso si tratti di un baco: lo correggo su Lion, torno su Mavericks e.. qui la finestra si sposta ogni volta verso il basso!
Ok, riassumo: se lo correggo per Lion, non va su Mavericks e viceversa! Non potevo credere di dover fare un IF per poter mantenere una finestra allo stesso posto...
Allora ho cominciato a fare delle prove: inserimento di NSLog un po’ dovunque ed ho scoperto che su Lion il mio codice veniva seguito alla lettera ma, prima di presentarla a video, la finestra ritornava alla posizione iniziale, come se il mio codice non fosse mai stato eseguito! Ho provato ad inserire observer un po’ ovunque per catturare il momento in cui veniva rimodificato il frame, ma senza risultato: la finestra andava a posizionarsi dove voleva lei!
Anche San Google non sembrava aiutarmi, finché alla fine non trovo un problema simile sul solito StackOverflow: anche lì la finestra ribelle non voleva onorare il setFrame su Lion.
Il consiglio era di togliere il check alla caratteristica Restorable in Interface Builder; dato che la mia finestra è costruita da codice in una sottoclasse di NSWindow, ho inserito nel codice della sottoclasse:

- (BOOL)isRestorable
{
  return NO;
}

che da Lion in poi rappresenta un override, mentre per il 10.6 Snow Leopard sarà un semplice metodo aggiuntivo non utilizzato.
Compilato e lanciato: perfetto su Lion e perfetto su Mavericks!

Ho poi consultato la documentazione Apple sulla User Interface Preservation. Si tratta del meccanismo secondo il quale le finestre di una app sono riportate, ad una successiva riapertura, nello stato in cui erano alla chiusura, sempre che nelle Preferenze di Sistema l’utente abbia così scelto. Il risultato è stato:
  1. non c’è scritto da nessuna parte che il comportamento sia cambiato in qualche momento tra Lion e Mavericks (non ho provato su Mountain Lion, ma non mi aspetto differenze).
  2. il Restore richiede di definire un oggetto che faccia questo lavoro tramite un completionHandler; non conoscendo questa funzionalità, non avevo fatto nulla per implementarla.
  3. su entrambi i Mac utilizzati, questa funzione era disabilitata.
  4. la documentazione della classe NSWindow afferma che il metodo standard ritorna YES solo se lo styleMask della NSWindow comprende NSTitledWindow, mentre nel mio caso si trattava di una NSBorderlessWindow.
Risultato: ho risolto il problema attuale; ma forse qualcosa mi sfugge ancora nel generale...