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 }