Prace nad projektem JAutomaton trwają, raczej to już końcówka. Program ten ma za zadanie wykonywanie zdefiniowanych za pomocą słów kluczowych sekwencji poleceń typu: klikanie, poruszanie myszą i wciskanie klawiszy. Zachciało mi się dodać kolorowanie składni, w związku z tym, pomyślałem, że przedstawię swoje rozwiązanie tego problemu. Może komuś pomoże, albo podsunie jakiś pomysł. Tekst kierowany do początkujących, ale z podstawową znajomością Javy. Tekst podczas pisania strasznie się rozrósł.
Tworzymy edytor.
Tworzymy klasę Editor, którą chcemy traktować jak komponent Swing, dlatego będzie dziedziczyć po JScrollPane (zamiast tego, można dziedziczyć po JTextPane, ale my chcemy mieć możliwość scrollowania). Edytor zawiera pole tekstowe JTextArea i metody do pobrania i ustawienia jego tekstu.
public class Editor extends JScrollPane { public Editor() { super(); textPane = new JTextPane(); setViewportView(textPane); } public String getText() { return textPane.getText(); } public void setText(final String txt) { textPane.setText(txt); } private JTextPane textPane; }Jak na razie mamy tylko prosty edytor, ale gdzie to kolorowanie poleceń? Podejść na pewno jest więcej, ale moim zdaniem najwygodniej jest użyć DocumentListener'a. Pozwala on na przechwytywanie zdarzeń zmian w dokumencie tekstowym:
- insertUpdate(DocumentEvent e) - powiadamia o wstawieniu tekstu do dokumentu.
- removeUpdate(DocumentEvent e) - powiadamia o usunięciu tekstu z dokumentu.
- changedUpdate(DocumentEvent e) - nie interesuje nas za bardzo, powiadamia o zmianach w atrybutach dokumentu.
public class Editor extends JScrollPane implements DocumentListener { public Editor() { // Tutaj jest poprzedni kod textPane.getDocument().addDocumentListener(this); } // Tutaj jest poprzedni kod public void insertUpdate(DocumentEvent e) { } public void removeUpdate(DocumentEvent e) { } public void changedUpdate(DocumentEvent e) { } }
Wykrywanie zmian po wstawieniu/usunięciu tekstu.
Załóżmy, że mamy taką sytuację: zwykły tekst to tekst już istniejący, marmolada to słowo kluczowe, wytłuściłem tekst wstawiony. Na pomarańczowo oznaczyłem ciąg słów do ponownej analizy składniowej.
Wlazł kotek na marmoladaALA MA KOTA marmolada A KOT MA ALĘ płotekTworzymy metodę, która odszuka indeksy (lewy i prawy) ciągu słów, który wymaga ponownego przeanalizowania składniowego i zwróci listę z informacjami o słowach, które trzeba ponownie przeanalizować. Informacje te przechowamy w obiektach klasy WordUpdateInfo. Dodajemy prywatną klasę (bo potrzebna nam jest tylko w tej klasie) do klasy Editor.
private class WordUpdateInfo { WordUpdateInfo(String word, int offset, int length) { this.word = word; this.offset = offset; this.length = length; } public String word; public int offset; public int length; }Wspomniana wyżej metoda przyjmuje dwa parametry, DocumentEvent niosący informacje o zmianach w tekście oraz flagę, czy dokonano wstawienia czy usunięcia tekstu.
private List<WordUpdateInfo> getChangedWords(DocumentEvent e, boolean inserting) {Po pierwsze potrzebujemy zawartości JTextPane jako tablicy znaków.
char[] text = textPane.getText().toCharArray();Następnie korzystając z dobrodziejstw klasy DocumentEvent możemy określić położenie początku (lewego krańca) wstawianego tekstu przy pomocy metody getOffset. Będziemy z tego miejsca sprawdzać, czy z lewej strony nie zostało utworzone nowe słowo, poprzez połączenie się liter występujących w tekście z literami tekstu wstawianego. Jeśli tak, to wchłaniamy je do ciągu słów podlegającego ponownej analizie. Wyszukiwanie zaczynamy bez pierwszego znaku wstawionego tekstu, dlatego odejmujemy jedynkę aż do napotkania spacji.
int left = e.getOffset(); while(left - 1 >= 0 && text[left - 1] != ' ') { left--; }Z prawej strony natomiast sprawdzamy, czy mamy do czynienia ze wstawieniem tekstu. Jeśli tak, to zaczynamy sprawdzanie od znaku po wstawionym tekście. Długość wstawienia otrzymujemy funkcją DocumentEvent.getLength. Jeżeli to usuwanie tekstu, to w szukamy w prawo tak jak z lewej - od miejsca w którym znajduje się kursor.
int right = e.getOffset(); if(inserting) { right += e.getLength(); } while(right < text.length && text[right] != ' ' ) { right++; }Mamy już granice ciągu słów, które trzeba sprawdzić, mamy gdzie je schować, więc przystępujemy do tworzenia listy słów, które wymagają ponownego kolorowania. Do wykrycia białych znaków używamy wyrażenia regularnego \\s+, oznacza ono mniej więcej: "biały znak występujący raz lub więcej razy". Myślę, że kod mówi sam za siebie, więc nie będę go opisywać.
// Utwórz listę słów do aktualizacji List<WordUpdateInfo> words = new ArrayList<WordUpdateInfo>(); int wordOffset = left, wordLength = 0; boolean isWhiteSpace = false; for(int offset = left; offset < right; offset++) { // Nie mamy do czynienia z białym znakiem? if(new String(text, offset, 1).matches("\\s+")) { isWhiteSpace = true; } else { isWhiteSpace = false; } if(!isWhiteSpace) { // Czy to początek słowa? if(wordLength == 0) { wordOffset = offset; } wordLength++; } // Napotkałem biały znak lub koniec ciągu słów? if(isWhiteSpace || offset == right - 1) { // Spacja kończy przebieganie po słowie? - zapisz i zeruj if(wordLength > 0) { words.add(new WordUpdateInfo(new String(text, wordOffset, wordLength), wordOffset, wordLength)); System.out.println(new String(text, wordOffset, wordLength)); wordLength = 0; } } } return words;Kolorowanie słów kluczowych.
Przyszła kolej na sprawdzanie listy słów, zajmuje się tym funkcja updateColoring. U nas jedynym słowem kluczowym jest słowo marmolada.Musimy w klasie utworzyć zbiory atrybutów wykorzystywane przez funkcję StyledDocument.setCharacterAttributes. Pierwszym parametrem metody StyleContext.addAttribute jest stary zbiór atrybutów, następnie nazwa atrybutu oraz jego wartość. Więcej atrybutów znajduje się w dokumentacji.
static private StyleContext sc = StyleContext.getDefaultStyleContext(); static private AttributeSet COMMANDS_ASET = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.BLUE); static private AttributeSet NORMAL_ASET = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.BLACK);Metoda updateColoring przegląda otrzymaną listę słów, sprawdza, czy to słowo kluczowe, czy nie i ustawia odpowiednie atrybuty znaków za pomocą metody StyledDocument.setCharacterAttributes. Jej parametry to kolejno: położenie od początku dokumentu (offset), długość ciągu znaków, zbiór atrybutów i ostatni, czy atrybuty mają być nadpisane (true), czy połączone z istniejącymi (false).
private void updateColoring(List<WordUpdateInfo> words) { for(final WordUpdateInfo w : words) { SwingUtilities.invokeLater(new Runnable() { public void run() { if(w.word.equals("marmolada")) { textPane.getStyledDocument().setCharacterAttributes(w.offset, w.length, COMMANDS_ASET, true); } else { textPane.getStyledDocument().setCharacterAttributes(w.offset, w.length, NORMAL_ASET, true); } } }); } }Teraz zostało nam tylko wykorzystać napisane funkcje. Dodajemy do insertUpdate
updateColoring(getChangedWords(e, true));oraz do removeUpdate
updateColoring(getChangedWords(e, false));
Jest to mój pierwszy wpis techniczny, więc będę wdzięczny za wszelkie uwagi.
OdpowiedzUsuń