1 /*
2 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25 package optkl.textmodel.ui;
26 import optkl.textmodel.TextModel;
27 import optkl.textmodel.tokens.LineCol;
28 import optkl.textmodel.tokens.Span;
29
30 import javax.swing.JScrollPane;
31 import javax.swing.JTextPane;
32 import javax.swing.SwingUtilities;
33 import javax.swing.event.DocumentEvent;
34 import javax.swing.event.DocumentListener;
35 import javax.swing.text.BadLocationException;
36 import javax.swing.text.Element;
37 import java.awt.Font;
38 import java.awt.Point;
39 import java.awt.event.MouseEvent;
40 import java.awt.geom.Rectangle2D;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.TreeMap;
44
45 public abstract class TextModelViewer {
46 static class TextViewerPane<T extends TextModelViewer> extends JTextPane {
47 protected T viewer;
48 TextViewerPane(Font font, boolean editable) {
49 setFont(font);
50 setEditable(editable);
51 }
52 void setViewer(T viewer) {
53 this.viewer = viewer;
54 }
55 }
56 public TextModel textModel;
57 protected final StyleMapper styleMapper;
58 final public JScrollPane scrollPane;
59 private String text;
60
61 public record Line(int line, int startOffset, int endOffset) implements Span {
62 }
63
64 protected List<Line> lines;
65 protected TreeMap<Integer, Line> offsetToLineTreeMap;
66
67 public abstract TextModel createTextModel(String text);
68 void reparse(String msg) {
69 // System.out.println(msg);
70 var newTextModel = textModel;
71 try{
72 newTextModel =createTextModel(styleMapper.jTextPane.getText());
73 textModel = newTextModel;
74 styleMapper.jTextPane.getStyledDocument().removeDocumentListener(documentListener);
75 setText(textModel.plainText());
76 styleMapper.jTextPane.getStyledDocument().setCharacterAttributes(0, text.length(),styleMapper.defaultStyle, true);
77 styleMapper.applyStyles(textModel);
78 styleMapper.jTextPane.getStyledDocument().addDocumentListener(documentListener);
79 }catch (IllegalStateException e){
80 System.out.println("Parse failed");
81 }
82 }
83
84 final DocumentListener documentListener;
85 final DocumentListener editableDocumentListener=new DocumentListener() {
86 @Override
87 public void insertUpdate(DocumentEvent e) {
88 SwingUtilities.invokeLater(() ->reparse("insert"));
89 }
90
91 @Override
92 public void removeUpdate(DocumentEvent e) {
93 SwingUtilities.invokeLater(() ->reparse("remove"));
94 }
95
96 @Override
97 public void changedUpdate(DocumentEvent e) {
98 SwingUtilities.invokeLater(() ->reparse("changed"));
99 }
100 };
101 final DocumentListener nonEditableDocumentListener=new DocumentListener(){
102 @Override
103 public void insertUpdate(DocumentEvent e) {
104 SwingUtilities.invokeLater(() -> applyHighlighting());
105 }
106
107 @Override
108 public void removeUpdate(DocumentEvent e) {
109 // SwingUtilities.invokeLater(() -> applyHighlighting());
110 }
111
112 @Override
113 public void changedUpdate(DocumentEvent e) {
114 // Plain text attributes changed, not relevant for this highlighter
115 }
116 };
117
118 public TextModelViewer(TextModel textModel, StyleMapper styleMapper) {
119 this.textModel = textModel;
120 this.styleMapper = styleMapper;
121 this.scrollPane = new JScrollPane(this.styleMapper.jTextPane);
122 this.documentListener = styleMapper.jTextPane.isEditable() ?editableDocumentListener:nonEditableDocumentListener;
123
124 this.styleMapper.jTextPane.getStyledDocument().addDocumentListener(documentListener);
125 this.styleMapper.applyStyles(textModel);
126 this.setTextFromDocModel();;
127 }
128
129 public Element getElement(int offset) {
130 return this.styleMapper.jTextPane.getStyledDocument().getCharacterElement(offset);
131 }
132
133 public Element getElementFromMouseEvent(MouseEvent e) {
134 return getElement(getOffset(e));
135 }
136 public void scrollTo(Element funcOpElement) {
137 try {
138 var rectangle2D = this.styleMapper.jTextPane.modelToView2D(funcOpElement.getStartOffset());
139 this.styleMapper.jTextPane.scrollRectToVisible(rectangle2D.getBounds());
140 } catch (BadLocationException e) {
141 throw new RuntimeException(e);
142 }
143 }
144
145 public void highLightLines(LineCol first, LineCol last) {
146 var highlighter = this.styleMapper.jTextPane.getHighlighter();
147 try {
148 var range = getLineRange(first, last);
149 highlighter.addHighlight(range.startOffset(), range.endOffset(), styleMapper.highlightPainter);
150 } catch (BadLocationException e) {
151 throw new IllegalStateException();
152 }
153 }
154
155 public int getOffset(Point p) {
156 return this.styleMapper.jTextPane.viewToModel2D(p);
157 }
158
159 public int getOffset(MouseEvent e) {
160 return getOffset(e.getPoint());
161 }
162
163 public void highLight(Element element) {
164 var highlighter = this.styleMapper.jTextPane.getHighlighter();
165 try {
166 highlighter.addHighlight(element.getStartOffset(), element.getEndOffset(), styleMapper.highlightPainter);
167 } catch (BadLocationException e) {
168 throw new IllegalStateException();
169 }
170 }
171
172 public int getLine(Element element) {
173 var lineSpan = offsetToLineTreeMap.ceilingEntry(element.getStartOffset());
174 return lineSpan.getValue().line + 1;
175 }
176
177 String setText(String text) {
178 this.text = text;
179 String[] linesOfText = text.split("\n");
180 lines = new ArrayList<>();
181 offsetToLineTreeMap = new TreeMap<>();
182 int accumOffset = 0;
183 for (int currentLine = 0; currentLine < linesOfText.length; currentLine++) {
184 Line line = new Line(lines.size(), accumOffset, accumOffset + linesOfText[currentLine].length() + 1);// +1 for newline
185 lines.add(line);
186 accumOffset = line.endOffset();
187 offsetToLineTreeMap.put(accumOffset, line);
188 }
189 return text;
190 }
191
192 public void removeHighlights() {
193 this.styleMapper.jTextPane.getHighlighter().removeAllHighlights();
194 }
195 void applyHighlighting() {
196 SwingUtilities.invokeLater(() -> {
197 this.styleMapper.jTextPane.getStyledDocument().setCharacterAttributes(0, text.length(), styleMapper.defaultStyle, true);
198 this.styleMapper.applyStyles(textModel);
199 });
200 }
201
202 protected void setTextFromDocModel() {
203 try {
204 if (this.text != null && !text.equals("")) {
205 this.styleMapper.jTextPane.getStyledDocument().remove(0, text.length());
206 }
207 setText(textModel.plainText());
208 this.styleMapper.jTextPane.getStyledDocument().insertString(0, text, styleMapper.defaultStyle);
209 } catch (BadLocationException e) {
210 e.printStackTrace();
211 }
212 }
213
214 public Span getLineRange(LineCol start, LineCol end) {
215 Span startLine = lines.get(start.line());
216 Span endLine = lines.get(end.line());
217 return new Span.Impl(startLine.startOffset(), endLine.endOffset());
218 }
219
220 public int getOffset(LineCol lineCol) {
221 return lines.get(lineCol.line() - 1).startOffset() + lineCol.col();
222 }
223
224 public Rectangle2D.Double getRect(Element from) {
225 try {
226 var fromPoint1 = this.styleMapper.jTextPane.modelToView2D(from.getStartOffset());
227 var fromPoint2 = this.styleMapper.jTextPane.modelToView2D(from.getEndOffset());
228 return new Rectangle2D.Double(fromPoint1.getBounds().getMinX(), fromPoint1.getMinY(), fromPoint2.getBounds().getWidth(), fromPoint2.getBounds().getHeight());
229 } catch (Exception e) {
230 return null;
231 }
232 }
233
234 public void highlight(ElementSpan fromElementSpan, List<ElementSpan> toElementSpans) {
235 highLight(fromElementSpan.element());
236 toElementSpans.forEach(targetElementSpan -> {
237 var targetTextViewer = targetElementSpan.textModelViewer();
238 var targetElement = targetElementSpan.element();
239 targetTextViewer.highLight(targetElement);
240 targetTextViewer.scrollTo(targetElement);
241 });
242 }
243 }