Un'altra
funzionalità essenziale per un editor di testo è il Replace,
che offre la possibilità di sostituire, all'interno di un testo,
tutte le occorrenze di una certa parola con un'altra, scelta dall'utente.
Questa funzionalità può essere sviluppata rapidamente a partire
dalle classi realizzate il mese scorso. L'esistenza di numerosi punti di
contatto tra i due esempi offre il pretesto per illustrare come una certa
disciplina nella stesura del codice favorisca ampliamente diverse forme
di riuso.
Find & Replace:
modalità di interazione con l'utente
Per
offrire una funzionalità tipo Find & Replace, è necessario
instaurare un'interazione User Driven di una certa complessità;
il normale svolgimento dell'operazione richiede ben quattro passaggi:
-
l'inserimento,
tramite un campo di testo, di una stringa da ricercare e di una con cui
sostituirla
-
l'individuazione,
all'interno del testo, della prima occorrenza della stringa da sostituire
-
la richiesta
all'utente di scegliere una delle seguenti possibilità:
-
rimpiazza
l'occorrenza attuale
-
rimpiazza
tutte le occorrenze
-
vai alla
successiva
-
termina
l'operazione
-
l'esecuzione
dell'operazione scelta
Come
si può notare, questa situazione prevede due passaggi di interrogazione
utente, visibili al primo ed al terzo punto. Anche in questo esempio si
vuole modellare l'interazione in un modo da rispettare la divisione tra
lo strato di presentazione e quello di gestione dei dati, secondo la modalità
illustrata nel precedente articolo.
Figura
1 - In un'architettura a strati esiste una netta separazione tra la
logica
di sistema e la gestione della presentazione
Strato di presentazione:
creazione del Frame di Dialogo principale
Per
prima cosa procederemo a realizzare la finestra di dialogo con l'utente.
Tale finestra deve contenere due campi di testo, uno per inserire la parola
da ricercare ed uno per inserire quella da sostituire, e una coppia di
pulsanti per attivare le due funzionalità.
Figura
2 - La finestra di dialogo per il Find & Replace può essere
realizzata
come estensione della finestra del Find
Una
simile finestra di dialogo può essere realizzata per specializzazione
a partire da quella del Find, realizzata il mese scorso. Avendo adottato
un'architettura modulare, e' possibile realizzarla con poche righe di codice,
limitandosi a sovrascrivere i metodi che devono implementare un comportamento
differente rispetto alla superclasse:
public
class FindAndReplaceDialog extends FindDialog {
private JTextField replaceTextField;
private JButton replaceButton;
public FindAndReplaceDialog(TextEditor owner) {
super(owner);
}
protected void setupFrame() {
setTitle("Find And Replace");
setResizable(false);
}
protected Border createTitledBorder() {
return BorderFactory.createTitledBorder("Find And Replace");
}
protected void setupComponents() {
super.setupComponents();
replaceTextField = new JTextField(15);
replaceButton = new JButton(new ImageIcon("Replace24.gif"));
}
protected Box createMainBox() {
Box box = super.createMainBox();
Box replaceBox = Box.createHorizontalBox();
replaceBox.add(new JLabel("Replace with:"));
replaceBox.add(replaceTextField);
replaceBox.add(replaceButton);
box.add(replaceBox);
return box;
}
protected void registerListeners() {
super.registerListeners();
ActionListener replaceListener = new ReplaceButtonListener();
replaceButton.addActionListener(replaceListener);
replaceTextField.addActionListener(replaceListener);
}
protected void enableFind() {
super.enableFind();
replaceButton.setEnabled(true);
replaceTextField.setEnabled(true);
}
protected void disableFind() {
super.disableFind();
replaceButton.setEnabled(false);
replaceTextField.setEnabled(false);
}
[......]
class ReplaceButtonListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
replace();
}
}
[....]
}
Nel
sorgente di esempio, il costruttore si limita a richiamare quello della
superclasse, che definisce un ordine preciso di chiamata dei metodi di
set up. I metodi setupFrame() e createTitledBorder() sovrascrivono i corrispondenti
metodi nella superclasse, in modo da impostare correttamente il nome del
pannello. Nelle righe sucessive, vengono estesi alcuni metodi presenti
nella superclasse, in modo da adattarli al nuovo ambiente: il metodo setupComponents()
crea un nuovo campo di testo ed un nuovo pulsante; il metodo createMainBox()
posiziona i nuovi componenti all'interno della finestra; registerListener()
aggiunge un ascoltatore ai due componenti e cosi' via, il tutto senza essere
costretti a duplicare il codice che imposta le dimensioni ed il layout
della finestra.
La logica del
Replace
Per
implementare la logica della funzione Replace, è necessario definire
alcuni metodi nuovi nella classe TextEditor. Tali metodi devono lavorare
su una coppia di stringhe: quella da ricercare, e quella da sostituire.
Come nell'esempio del mese scorso, anche in questo caso è utile
definire un insieme di metodi che offrano accesso alla stessa funzionalità,
utilizzando nei diversi casi la possibilità di fornire un insieme
di parametri diverso. Vediamo anzitutto come implementare un metodo "replace"
che effettui la ricerca a partire dalla posizione attuale del cursore:
public void replaceStringFromActualOffset(String toFind,
String toReplace)
throws NotFoundException {
try {
replaceStringFromOffset(toFind,
toReplace,
getEditor().getSelectionStart());
}
catch(BadLocationException e) {} // Non può capitare
}
La
richiesta specificata da questo metodo viene inoltrata al metodo replaceStringFromOffset,
che come parametri riceve, oltre alle stringhe, un intero che specifica
la posizione da cui iniziare la ricerca. Qualora la posizione specificata
fosse illegale, viene generata una BadLocationException.
public void replaceStringFromOffset(String toFind,
String toReplace,
int startOffset)
throws NotFoundException,
BadLocationException{
goToString(toFind,startOffset); // throws NotFoundException
getEditor().replaceSelection(toReplace);
}
Un'altra
modalità di fruizione della funzionalità "Replace" è
il "Replace All", che permette di sostituire tutte le occorrenze si una
certa stringa con un'altra. Per implementare questa funzionalità
è sufficiente spostare il cursore all'inizio del testo, e quindi
avviare un ciclo che prosegue fino a quando tutte le occorrenze della stringa
da cercare non siano state sostituite.
public void replaceAll(String toFind,String toReplace) {
getEditor().setCaretPosition(0);
while(true) {
try {
replaceStringInRange(toFind,
toReplace,
getEditor().getCaretPosition(),
getEditor().getDocument().getLength());
}
catch(BadLocationException ble) {
// non puo' capitare
}
catch(NotFoundException nfe) {
// fine ricerca
return;
}
}
Questo
metodo si appoggia su replaceStringInRange, che effettua la ricerca ed
il rimpiazzo solamente nel range specificato dai parametri "startOffset"
ed "endOffset":
public void replaceStringInRange(String toFind,
String toReplace,
int startOffset,
int endOffset)
throws NotFoundException,
BadLocationException {
goToString(toFind,startOffset,endOffset);
// throws NotFoundException
getEditor().replaceSelection(toReplace);
}
E'
interessante notare come l'implementazione di questi metodi ricorra ai
metodi "goToString" definiti il mese scorso: questa forma di riutilizzo
è resa possibile dal fatto di aver operato una valida separazione
tra sintassi e semantica delle operazioni che concorrono a realizzare la
funzionalità Find.
Mettiamo tutto
assieme
Dopo
aver illustrato la realizzazione del lato presentazione (la finestra di
dialogo) e del livello logico (i comandi replaceXxx() di TextEditor), è
arrivato il momento di mettere tutto insieme. Per concretizzare l'operazione
di Replace, bisogna definire il metodo replace(), che viene chiamato alla
pressione del pulsante "replace" del pannello di dialogo:
private void replace() {
while(true)
try {
String ft=getFindTextField().getText();
getTarget().goToStringFromActualOffset(ft);
int returnVal = promptReplaceAll();
if(returnVal==REPLACE){
String ft=getFindTextField().getText();
String rt=replaceTextField.getText();
getTarget().replaceStringFromActualOffset(ft,rt);
}
else if(returnVal==REPLACE_ALL) {
// Replace All
String ft=getFindTextField().getText();
String rt=replaceTextField.getText();
getTarget().replaceAll(ft,rt);
return;
}
else if(returnVal==NEXT){
String ft=getFindTextField().getText();
getTarget().goToStringFromNextOffset(ft);
}
else if(returnVal==CANCEL)
return;
}
catch(NotFoundException ex) {
JOptionPane.showMessageDialog(this,
"Finished!",
"Finished!",
JOptionPane.WARNING_MESSAGE);
return;
}
}
private int promptReplaceAll() {
String ft=getFindTextField().getText();
return JOptionPane.showOptionDialog(this,
"Replace this occourrence of "+ft,
"Replace?",
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE,
null, options, options[0]);
}
Questo
metodo avvia un ciclo, durante il quale vengono eseguite le seguenti operazioni:
-
Il cursore
viene portato sulla prima occorrenza della parola da cercare
-
All'utente
vene richiesto di scegliere tra le seguenti opzioni
-
-
rimpiazza l'occorrenza attuale della parola
-
-
rimpiazza tutte le occorrenze
-
-
vai alla successiva occorrenza
-
-
termina l'operazione
Figura
3 - Il Find & Replace richiede un secondo passaggio di interrogazione
utente
Il
ciclo termina in tre casi:
-
Non ci
sono più occorrenze disponibili, evento segnalato da una NotFoundException
-
Viene
eseguito un ReplaceAll
-
L'utente
preme il tasto CANCEL, ed interrompe esplicitamente l'interazione.
Figura
4 - Diagramma di interazione tra FindDialog e
TextEditor
durante un'operazione di Replace
Integrazione del
comando all'interno dell'editor
Ancora
una volta, l'integrazione della nuova funzionalità richiede alcune
modifiche ai metodi che effettuano il set up dell'applicazione, ossia ai
metodi:
setupComponents()
buildMenuItems()
buildButtons()
createMenuBar()
createToolBar()
Anche
in questo caso, il progetto di base dell'applicazione ha mantenuto l'integrità
originale, restando comunque aperto a nuove modifiche. In figura 5 possiamo
avere una veduta d'insieme sulla struttura attuale del progetto.
Figura
5 - Il diagramma di classe completo dell'applicazione all'attuale stadio
di sviluppo
Conclusioni
Questo
mese abbiamo visto altre pratiche di riuso, utili durante lo sviluppo di
programmi grafici. L'uso di queste tecniche può facilmente essere
esteso a progetti su vasta scala, come vedremo a partire dal mese prossimo:
è in arrivo una grossa novità per tutti i lettori di MokaByte,
che risulterà particolarmente interessante a tutti quelli che hanno
seguito con interesse questa rubrica.
L'esempio
con i sorgenti può essere trovato qui |