1 /* 2 * This file is part of the LibreOffice project. 3 * 4 * This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 * 8 * This file incorporates work covered by the following license notice: 9 * 10 * Licensed to the Apache Software Foundation (ASF) under one or more 11 * contributor license agreements. See the NOTICE file distributed 12 * with this work for additional information regarding copyright 13 * ownership. The ASF licenses this file to you under the Apache 14 * License, Version 2.0 (the "License"); you may not use this file 15 * except in compliance with the License. You may obtain a copy of 16 * the License at http://www.apache.org/licenses/LICENSE-2.0 . 17 */ 18 package com.sun.star.script.framework.provider.beanshell; 19 20 import java.awt.Color; 21 import java.awt.Dimension; 22 import java.awt.Font; 23 import java.awt.FontMetrics; 24 import java.awt.Graphics; 25 import java.awt.Polygon; 26 import java.awt.Rectangle; 27 import java.awt.event.ActionEvent; 28 import java.awt.event.InputEvent; 29 import java.awt.event.KeyAdapter; 30 import java.awt.event.KeyEvent; 31 32 import javax.swing.AbstractAction; 33 import javax.swing.JComponent; 34 import javax.swing.JScrollPane; 35 import javax.swing.JTextArea; 36 import javax.swing.KeyStroke; 37 import javax.swing.UIManager; 38 import javax.swing.event.DocumentEvent; 39 import javax.swing.event.DocumentListener; 40 import javax.swing.event.UndoableEditEvent; 41 import javax.swing.event.UndoableEditListener; 42 import javax.swing.text.BadLocationException; 43 import javax.swing.undo.CompoundEdit; 44 import javax.swing.undo.UndoManager; 45 import java.util.List; 46 import java.util.ArrayList; 47 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 public class PlainSourceView extends JScrollPane implements 52 ScriptSourceView, DocumentListener { 53 54 private final ScriptSourceModel model; 55 private JTextArea ta; 56 private GlyphGutter gg; 57 private int linecount; 58 private boolean isModified = false; 59 private static final String undoKey = "Undo"; 60 private static final String redoKey = "Redo"; 61 private CompoundEdit compoundEdit = null; 62 private static final int noLimit = -1; 63 UndoManager undoManager; 64 private List<UnsavedChangesListener> unsavedListener = new ArrayList<UnsavedChangesListener>(); 65 66 private static final Pattern tabPattern = Pattern.compile("^ *(\\t)"); 67 private static final Pattern indentationPattern = Pattern.compile("^([^\\S\\r\\n]*)(([^\\{])*\\{\\s*)*"); 68 PlainSourceView(ScriptSourceModel model)69 public PlainSourceView(ScriptSourceModel model) { 70 this.model = model; 71 initUI(); 72 model.setView(this); 73 } 74 undo()75 public void undo(){ 76 if(compoundEdit!=null){ 77 compoundEdit.end(); 78 undoManager.addEdit(compoundEdit); 79 compoundEdit = null; 80 } 81 if(undoManager.canUndo()){ 82 undoManager.undo(); 83 } 84 // check if it's the last undoable change 85 if(undoManager.canUndo() == false){ 86 setModified(false); 87 } 88 } redo()89 public void redo(){ 90 if(undoManager.canRedo()){ 91 undoManager.redo(); 92 } 93 } clear()94 public void clear() { 95 ta.setText(""); 96 } 97 update()98 public void update() { 99 /* Remove ourselves as a DocumentListener while loading the source 100 so we don't get a storm of DocumentEvents during loading */ 101 ta.getDocument().removeDocumentListener(this); 102 103 if (!isModified) { 104 int pos = ta.getCaretPosition(); 105 ta.setText(model.getText()); 106 107 try { 108 ta.setCaretPosition(pos); 109 } catch (IllegalArgumentException iae) { 110 // do nothing and allow JTextArea to set its own position 111 } 112 } 113 114 // scroll to currentPosition of the model 115 try { 116 int line = ta.getLineStartOffset(model.getCurrentPosition()); 117 Rectangle rect = ta.modelToView(line); 118 if (rect != null) { 119 ta.scrollRectToVisible(rect); 120 } 121 } catch (BadLocationException e) { 122 // couldn't scroll to line, do nothing 123 } 124 125 gg.repaint(); 126 127 // Add back the listener 128 ta.getDocument().addDocumentListener(this); 129 } 130 isModified()131 public boolean isModified() { 132 return isModified; 133 } 134 notifyListeners(boolean isUnsaved)135 private void notifyListeners (boolean isUnsaved) { 136 for (UnsavedChangesListener listener : unsavedListener) { 137 listener.onUnsavedChanges(isUnsaved); 138 } 139 } 140 setModified(boolean value)141 public void setModified(boolean value) { 142 if(value != isModified) { 143 notifyListeners(value); 144 isModified = value; 145 } 146 } 147 initUI()148 private void initUI() { 149 try{ 150 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 151 } 152 catch(Exception e){ 153 // What to do here 154 } 155 ta = new JTextArea(); 156 ta.setTabSize(4); 157 ta.setRows(15); 158 ta.setColumns(40); 159 ta.setLineWrap(false); 160 ta.insert(model.getText(), 0); 161 ta.setFont(new Font("Monospaced", ta.getFont().getStyle(), ta.getFont().getSize())); 162 undoManager = new UndoManager(); 163 undoManager.setLimit(noLimit); 164 ta.getDocument().addUndoableEditListener(new UndoableEditListener(){ 165 @Override 166 public void undoableEditHappened(UndoableEditEvent editEvent) { 167 if(compoundEdit == null){ 168 compoundEdit = new CompoundEdit(); 169 } 170 compoundEdit.addEdit(editEvent.getEdit()); 171 } 172 }); 173 174 ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_MASK), undoKey); 175 ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK), redoKey); 176 177 ta.addKeyListener(new KeyAdapter(){ 178 @Override 179 public void keyPressed(KeyEvent ke) { 180 // if shift + tab was pressed, remove the first tab before any code begins 181 if (ke.isShiftDown() && ke.getKeyCode() == KeyEvent.VK_TAB) { 182 try { 183 int caretOffset = ta.getCaretPosition(); 184 int lineOffset = ta.getLineOfOffset(caretOffset); 185 int startOffset = ta.getLineStartOffset(lineOffset); 186 int endOffset = ta.getLineEndOffset(lineOffset); 187 188 Matcher matcher = tabPattern.matcher(ta.getText(startOffset, endOffset - startOffset)); 189 if (matcher.find()) { 190 ta.replaceRange(null, startOffset + matcher.start(1), startOffset + matcher.end(1)); 191 } 192 } catch (BadLocationException e) { 193 // could not find correct location of the tab 194 } 195 } 196 // if the enter key was pressed, adjust indentation of the current line accordingly 197 if (ke.getKeyCode() == KeyEvent.VK_ENTER) { 198 try { 199 int caretOffset = ta.getCaretPosition(); 200 int lineOffset = ta.getLineOfOffset(caretOffset); 201 int startOffset = ta.getLineStartOffset(lineOffset); 202 int endOffset = ta.getLineEndOffset(lineOffset); 203 204 Matcher matcher = indentationPattern.matcher(ta.getText(startOffset, endOffset - startOffset)); 205 // insert new line including indentation of the previous line 206 ta.insert("\n", caretOffset++); 207 if (matcher.find()) { 208 if (matcher.group(1).length() > 0) { 209 ta.insert(matcher.group(1), caretOffset++); 210 } 211 // if there is an open curly bracket in the current line, increase indentation level 212 if (matcher.group(3) != null) { 213 ta.insert("\t", caretOffset); 214 } 215 } 216 ke.consume(); 217 } catch (BadLocationException e) { 218 // could not find correct location of the indentation 219 } 220 } 221 } 222 223 @Override 224 public void keyReleased(KeyEvent ke){ 225 if(ke.getKeyCode() == KeyEvent.VK_SPACE || ke.getKeyCode() == KeyEvent.VK_ENTER){ 226 compoundEdit.end(); 227 undoManager.addEdit(compoundEdit); 228 compoundEdit = null; 229 } 230 } 231 }); 232 233 ta.getActionMap().put(undoKey, new AbstractAction(undoKey){ 234 @Override 235 public void actionPerformed(ActionEvent event) { 236 undo(); 237 } 238 }); 239 240 ta.getActionMap().put(redoKey, new AbstractAction(redoKey){ 241 @Override 242 public void actionPerformed(ActionEvent event) { 243 redo(); 244 } 245 }); 246 247 linecount = ta.getLineCount(); 248 249 gg = new GlyphGutter(this); 250 251 setViewportView(ta); 252 setRowHeaderView(gg); 253 254 ta.getDocument().addDocumentListener(this); 255 } 256 257 /* Implementation of DocumentListener interface */ insertUpdate(DocumentEvent e)258 public void insertUpdate(DocumentEvent e) { 259 doChanged(); 260 } 261 removeUpdate(DocumentEvent e)262 public void removeUpdate(DocumentEvent e) { 263 doChanged(); 264 } 265 changedUpdate(DocumentEvent e)266 public void changedUpdate(DocumentEvent e) { 267 doChanged(); 268 } 269 270 /* If the number of lines in the JTextArea has changed then update the 271 GlyphGutter */ doChanged()272 private void doChanged() { 273 setModified(true); 274 275 if (linecount != ta.getLineCount()) { 276 gg.update(); 277 linecount = ta.getLineCount(); 278 } 279 } 280 getText()281 public String getText() { 282 return ta.getText(); 283 } 284 getTextArea()285 public JTextArea getTextArea() { 286 return ta; 287 } 288 getCurrentPosition()289 public int getCurrentPosition() { 290 return model.getCurrentPosition(); 291 } 292 addListener(UnsavedChangesListener toAdd)293 public void addListener(UnsavedChangesListener toAdd) { 294 unsavedListener.add(toAdd); 295 } 296 } 297 298 class GlyphGutter extends JComponent { 299 300 private final PlainSourceView view; 301 private static final String DUMMY_STRING = "99"; 302 GlyphGutter(PlainSourceView view)303 GlyphGutter(PlainSourceView view) { 304 this.view = view; 305 update(); 306 } 307 update()308 public void update() { 309 JTextArea textArea = view.getTextArea(); 310 Font font = textArea.getFont(); 311 setFont(font); 312 313 FontMetrics metrics = getFontMetrics(font); 314 int h = metrics.getHeight(); 315 int lineCount = textArea.getLineCount() + 1; 316 317 String dummy = Integer.toString(lineCount); 318 319 if (dummy.length() < 2) { 320 dummy = DUMMY_STRING; 321 } 322 323 Dimension d = new Dimension(); 324 d.width = metrics.stringWidth(dummy) + 16; 325 d.height = lineCount * h + 100; 326 setPreferredSize(d); 327 setSize(d); 328 } 329 330 @Override paintComponent(Graphics g)331 public void paintComponent(Graphics g) { 332 JTextArea textArea = view.getTextArea(); 333 334 Font font = textArea.getFont(); 335 g.setFont(font); 336 337 FontMetrics metrics = getFontMetrics(font); 338 Rectangle clip = g.getClipBounds(); 339 340 g.setColor(getBackground()); 341 g.fillRect(clip.x, clip.y, clip.width, clip.height); 342 343 int ascent = metrics.getMaxAscent(); 344 int h = metrics.getHeight(); 345 int lineCount = textArea.getLineCount() + 1; 346 347 int startLine = clip.y / h; 348 int endLine = (clip.y + clip.height) / h + 1; 349 int width = getWidth(); 350 351 if (endLine > lineCount) { 352 endLine = lineCount; 353 } 354 355 for (int i = startLine; i < endLine; i++) { 356 String text; 357 text = Integer.toString(i + 1) + " "; 358 int y = i * h; 359 g.setColor(Color.blue); 360 g.drawString(text, 0, y + ascent); 361 int x = width - ascent; 362 363 // if currentPosition is not -1 then a red arrow will be drawn 364 if (i == view.getCurrentPosition()) { 365 drawArrow(g, ascent, x, y); 366 } 367 } 368 } 369 drawArrow(Graphics g, int ascent, int x, int y)370 private void drawArrow(Graphics g, int ascent, int x, int y) { 371 Polygon arrow = new Polygon(); 372 int dx = x; 373 y += ascent - 10; 374 int dy = y; 375 arrow.addPoint(dx, dy + 3); 376 arrow.addPoint(dx + 5, dy + 3); 377 378 for (x = dx + 5; x <= dx + 10; x++, y++) { 379 arrow.addPoint(x, y); 380 } 381 382 for (x = dx + 9; x >= dx + 5; x--, y++) { 383 arrow.addPoint(x, y); 384 } 385 386 arrow.addPoint(dx + 5, dy + 7); 387 arrow.addPoint(dx, dy + 7); 388 389 g.setColor(Color.red); 390 g.fillPolygon(arrow); 391 g.setColor(Color.black); 392 g.drawPolygon(arrow); 393 } 394 } 395 396