Negli
articoli precedenti, abbiamo introdotto alcune tecniche di programmazione
e di ingegnerizzazione del software che permettono di raggiungere una maggiore
efficienza nelle fasi della manutenzione del codice. Per confermare l'importanza
dei concetti esposti precedentemente, andremo ora a sviluppare una funzionalita'
estremamente utile nell'elaborazione dei testi: il Find.
La
sintassi del Find e' abbastanza diversa rispetto a quella dei comandi trattati
fino ad ora, dal momento che richiede una modalita' di interazione con
l'utente di gran lunga piu' complessa. Malgrado tali differenze, non sarà
necessario stravolgere l'architettura dell'applicazione per aggiungere
questa funzionalità.
La Sintassi del
Find
Durante
la stesura di un testo, torna sempre utile una funzione di ricerca. La
classica modalita' di interazione avviene attraverso una finestra di dialogo
non modale, vale a dire una finestra che non blocca il frame principale,
lasciando libero l'utente di alternare le operazioni di ricerca con le
normali operazioni di editing. La finestra di dialogo deve contenere un
campo di testo, un pulsante "Find Next" associato al Text Field ed un pulsante
"Cancel" per la chiusura. Mentre si inserisce il testo nell'apposita casella,
il cursore si sposta sulla prima occorrenza della parola cercata (ricerca
incrementale). Premendo il pulsante "Find Next" (o premendo invio nel campo
di testo), il cursore viene posizionato sull'occorrenza successiva. La
ricerca deve avvenire in modo circolare a partire dalla posizione corrente
del cursore: prima si esplora il testo che segue il cursore, quindi quello
che lo precede.
Figura
1 - La finestra di dialogo del Find
La
realizzazione di pannelli di dialogo solitamente non richiede le stesse
attenzioni necessarie per sviluppare l'applicazione principale. Nella maggior
parte dei casi i pannelli di dialogo rientrano nella casistica di "applicazioni
mono-uso di piccole dimensioni", nelle quali una codifica sintetica puo'
risultare preferibile ad una modulare. Tuttavia la realizzazione di un
simile pannello offre il pretesto per osservare una variante del Framework
descritto negli articoli precedenti, cosa che si rivelerà molto
utile in seguito.
Costruiamo la
finestra di dialogo
Per
costruire una finestra di dialogo occorre definire un'opportuna sottoclasse
di JDialog, che nel nostro caso chiameremo FindDialog. Una finestra di
dialogo deve essere associata ad un JFrame, che deve essere fornito al
momento della costruzione. Il costruttore di FindDialog assomiglia molto
a quello del TextEditor, dal momento che le fasi di costruzione individuate
sembrano coprire una casistica piuttosto ampia:
public
FindDialog(TextEditor owner) {
super(owner);
this.target = owner;
setupFrame();
setupFrameBehaviour();
setupComponents();
buildGui();
registerListeners();
disableFind();
}
Il
codice dei primi tre metodi non presenta particolari difficolta', e per
questa ragione non verrà trattato. La parte più interessante
si trova senza dubbio negli ultimi due metodi, che si occupano di impostare
gli ascoltatori di questa interfaccia utente.
protected
void registerListeners() {
ActionListener nextListener = new NextButtonListener();
nextButton.addActionListener(nextListener);
findTextField.addActionListener(nextListener);
cancelButton.addActionListener(new CancelButtonListener());
findTextField.getDocument().addDocumentListener(
new TextFieldDocumentListener());
}
Nelle
prime tre righe viene creato un ActionListener che si occupa di associare
una funzione al pulsante "Find Next" (quello con il canocchiale con sopra
una freccia): lo stesso ascoltatore viene impostato come ActionListener
per il campo di testo: in questo modo la pressione del tasto "Invio" svolge
la stessa funzione del tasto "Find Next". L'ascoltatore si limita a chiamare
il metodo findNext() che verrà descritto in seguito:
class
NextButtonListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
findNext();
}
}
Nella
riga successiva viene associato un ActionListener al pulsante "Cancel":
tale ascoltatore, che si limita a rendere invisibile la finestra di dialogo,
non necessita di ulteriori approfondimenti.
L'ultima
riga associa al campo di testo (o, per meglio dire, al suo Document) un
ascoltatore che, ad ogni inserimento o rimozione di un carattere, chiama
la funzione findCurrent(): questo ascoltatore rende possibile la ricerca
incrementale.
class
TextFieldDocumentListener implements DocumentListener {
public void changedUpdate(DocumentEvent e) {}
public void insertUpdate(DocumentEvent e) {
findCurrent();
}
public void removeUpdate(DocumentEvent e) {
findCurrent();
}
}
L'ultima
riga del costruttore effettua una chiamata al metodo disableFind(): tale
metodo disabilita il pulsante "Find Next" e colora di rosso il testo all'interno
del TextField, per segnalare all'utente che la stringa inserita non e'
presente nel testo
protected
void disableFind() {
findTextField.setForeground(Color.red);
nextButton.setEnabled(false);
}
Figura
2 - Un uso corretto della programmazione ad eventi permette di
fornire
un utile feedback all'utente: il testo si colora di rosso quando
non
esistono occorrenze nel testo esaminato
Questo
metodo, in seguito, verra' chiamato dal metodo findCurrent() ogni volta
che la ricerca non ha dato un buon esito: in questa maniera viene offerto
all'utente un utile feedback operativo. Ogni volta che la ricerca va a
buon fine, viene chiamato il metodo speculare, enableFind(), che esegue
l'operazione inversa:
protected
void enableFind() {
findTextField.setForeground(Color.black);
nextButton.setEnabled(true);
}
La semantica del
Find
La
sintassi del Find cosi' come l'abbiamo descritta e' abbastanza familiare;
resta ora da individuarne la semantica funzionale: ancora una volta ci
proponiamo di ottenere dei metodi che svolgano il compito desiderato a
partire dai parametri necessari, e che segnalino con delle eccezioni gli
eventuali malfunzionamenti.
Per
individuare con chiarezza una funzione che descriva il Find, e' utile ribaltare
il punto di vista: dal momento che il TextEditor incapsula un testo, la
funzione Find deve a sua volta essere incapsulata. La maniera piu' naturale
di esprimere la semantica del Find è attraverso una funzione del
tipo:
"Vai
alla prima occorrenza di della parola che ti indico a partire dall'attuale
posizione del cursore, e avvertimi se non la trovi"
public
void goToStringFromActualOffset(String toFind)
throws NotFoundException
NotFoundException
e' una eccezione sviluppata apposta per l'occasione. Le eccezioni sono
tra le classi piu' semplici da creare: nella maggior parte dei casi e'
sufficiente definire una sottoclasse vuota di Exception, il cui nome rifletta
una particolare condizione di errore:
public
class NotFoundException extends Exception {
}
Il
Find Next e il Find Incrementale hanno una semantica differente: il primo
puo' essere espresso da una funzione del tipo:
"Vai
alla prima occorrenza della parola che ti indico a partire dalla posizione
successiva a quella in cui ora si trova il cursore, e avvertimi se non
la trovi"
public
void goToStringFromNextOffset(String toFind)
throws NotFoundException{}
Il
pannello di dialogo si comportera' come un front end a queste funzioni,
come si vede nel diagramma successivo.
Figura
3 - Interazione tra FindDialog e TextEditor durante una ricerca
La
tipica chiamata a goToStringFromNextOffset() si svolgerà nel modo
seguente:
public
void findNext() {
try {
target.goToStringFromNextOffset(findTextField.getText());
}
catch(NotFoundException ex) {
JOptionPane.showMessageDialog(this,
"Not Found",
"String Not Found",
JOptionPane.WARNING_MESSAGE);
}
}
come
negli esempi della volta scorsa, si inserisce la chiamata in un blocco
try, mentre il blocco catch gestisce l'eventuale situazione di eccezione.
Un occhio all'implementazione
L'implementazione
della funzione Find offre il pretesto di osservare come una funzionalità
complessa possa essere sviluppata grazie ad un insieme di metodi piccoli
e specializzati, piuttosto che attraverso un unico metodo lungo di uso
generale.
I metodi
goToStringFromActualOffset() e goToStringFromNextOffset() possono essere
considerati due casi particolari del metodo
public void goToString(String toFind,int startOffset)
throws NotFoundException,BadLocationException
che
posiziona il cursore sulla prima occorenza della stringa specificata dal
primo parametro, effettuando una ricerca circolare a partire dall'offset
specificato dal secondo parametro. Se l'indice è troppo grosso o
negativo, viene lanciata una BadLocationException. Ecco di seguito l'implementazione
dei due metodi goToStringFromActualOffset() e goToStringFromNextOffset()
in funzione di goToString(String toFind,int startOffset).
public
void goToStringFromActualOffset(String toFind)
throws NotFoundException {
int fromIndex = getEditor().getSelectionStart();
try {
goToString(toFind,fromIndex);
}
catch(BadLocationException ble) {}
}
Nella
prima riga viene interrogato il TextComponent per conoscere la posizione
di inizio della ricerca, quindi viene effettuata la chiamata a goToString()
a partire da quell'indice. Il blocco catch è vuoto, dal momento
che la posizione specificata è sicuramente legale, essendo stata
ottenuta attraverso un'interrogazione al TextComponent stesso.
public
void goToStringFromNextOffset(String toFind)
throws NotFoundException {
int fromIndex = getEditor().getSelectionStart();
int length = getEditor().getDocument().getLength();
try {
goToString(toFind,(fromIndex+1)%length);
}
catch(BadLocationException ble) {}
}
Questo
metodo e' simile al precedente, tranne che in questo caso l'indice viene
ricavato con un'operazione di modulo sulla lunghezza del testo. L'operazione
di modulo, caratterizzata in Java dal simbolo "%", garantisce che, una
volta raggiunta l'ultima posizione sul testo, la ricerca riparta da zero.
Il
metodo goToString(String toFind,int startOffset) puo' a sua volta essere
visto come un caso particolare di un metodo
public
void goToString(String toFind,int startOffset,int endOffset)
throws NotFoundException,BadLocationException
che
limita la ricerca al range specificato dai parametri startOffset e endOffset.
Disponendo
di un simile metodo, possiamo descrivere il precedente in questa maniera:
public
void goToString(String toFind,int startOffset)
throws NotFoundException,BadLocationException {
int endOffset = getEditor().getDocument().getLength();
try {
goToString(toFind,startOffset,endOffset);
}
catch(NotFoundException nfe) {
goToString(toFind,0,startOffset);
// throws NotFoundException
}
}
Il
blocco try tenta di posizionare il cursore sulla prima occorrenza della
stringa analizzando il testo che segue il cursore; se questa ricerca fallisce,
il blocco catch tenta con la meta' superiore del testo. Se anche questa
ricerca fallisce, la NotFoundException viene fatta rimbalzare fuori dal
metodo.
Come
si puo' implementare il metodo goToString(String toFind,int startOffset,int
endOffset)? Lo scaricabarile finisce qui: all'interno di questo metodo
viene praticata la ricerca vera e propria e il corrispondente riposizionamento
del cursore
public
void goToString(String toFind,int startOffset,int endOffset)
throws NotFoundException,BadLocationException {
if(toFind.length()==0)
throw new NotFoundException();
int length = endOffset-startOffset;
String text =
getEditor().getDocument().getText(startOffset,length);
// throws BadLocationException
int index = text.indexOf(toFind);
if(index!=-1) {
getEditor().requestFocus();
getEditor().select(startOffset+index,
startOffset+index+toFind.length());
return;
}
else
throw new NotFoundException();
}
Figura
4 - La parola cercata viene evidenziata all'interno dell'Editor
Le
prime due righe verificano che la stringa da cercare non sia vuota: in
un simile caso la ricerca viene interrotta e viene sollevata una NotFoundException.
Nelle righe successive viene prelevato il testo compreso tra gli indici
specificati, ricorrendo al metodo getText() di JTextComponent. Questo metodo,
nel caso si specifichino degli indici illegali, genera una BadLocationException:
in questo caso l'esecuzione del metodo goToString() viene interrotta, e
l'eccezione viene fatta rimbalzare al chiamante. Infine viene praticata
la ricerca vera e propria, ricorrendo ai metodi di ricerca su stirnghe:
se la ricerca va a buon fine, la parola cercata viene evidenziata; in caso
contrario viene sollevata una NotFoundException.
Integrazione del
comando all'interno dell'editor
Una
volta definita la sintassi e la semantica del comando e' un gioco da ragazzi
integrarlo nell'editor preesistente. Per aggiornare l'interfaccia grafica
e' sufficiente modificare i metodi
setupComponents()
buildMenuItems()
buildButtons()
createMenuBar()
createToolBar()
L'aggiunta
non ha creato disordine nel sorgente, che rimane equivalente a quello presentato
il mese scorso.
Per
rendere attivo il pulsante "Find" bisogna agire sui metodi
registerListeners()
actionPerformed(ActionEvent ae)
facendo
in modo che, alla pressione del pulsante venga chiamato il seguente metodo:
public
void promptFind() {
findDialog.setVisible(true);
}
Conclusioni
In
questo articolo abbiamo studiato l'integrazione di una nuova funzionalità
nel nostro editor, assecondando il design preesistente. Questo lavoro ha
permesso di mettere in luce come alcune precise scelte di design possano
condizionare la manutenzione e la rielaborazione del codice. Il mese prossimo
proseguiremo con l'integrazione di una nuova funzionalità, che ci
permetterà di mettere in luce un'altro importantissimo aspetto della
Programmazione ad Oggetti: il riuso.
L'esempio
con i sorgenti può essere trovato qui |