MATSIM
ConfigEditor.java
Go to the documentation of this file.
1 
2 /* *********************************************************************** *
3  * project: org.matsim.*
4  * ConfigEditor.java
5  * *
6  * *********************************************************************** *
7  * *
8  * copyright : (C) 2019 by the members listed in the COPYING, *
9  * LICENSE and WARRANTY file. *
10  * email : info at matsim dot org *
11  * *
12  * *********************************************************************** *
13  * *
14  * This program is free software; you can redistribute it and/or modify *
15  * it under the terms of the GNU General Public License as published by *
16  * the Free Software Foundation; either version 2 of the License, or *
17  * (at your option) any later version. *
18  * See also COPYING, LICENSE and WARRANTY file *
19  * *
20  * *********************************************************************** */
21 
22 package org.matsim.run.gui;
23 
24 import java.awt.Color;
25 import java.awt.Graphics;
26 import java.awt.event.ActionEvent;
27 import java.awt.event.InputEvent;
28 import java.awt.event.KeyEvent;
29 import java.io.BufferedReader;
30 import java.io.BufferedWriter;
31 import java.io.File;
32 import java.io.IOException;
33 import java.util.HashMap;
34 import java.util.Map;
35 import java.util.SortedMap;
36 import java.util.TreeMap;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 
40 import javax.swing.AbstractAction;
41 import javax.swing.GroupLayout;
42 import javax.swing.GroupLayout.Alignment;
43 import javax.swing.JButton;
44 import javax.swing.JDialog;
45 import javax.swing.JFileChooser;
46 import javax.swing.JFrame;
47 import javax.swing.JScrollPane;
48 import javax.swing.JTextPane;
49 import javax.swing.KeyStroke;
50 import javax.swing.LayoutStyle.ComponentPlacement;
51 import javax.swing.event.DocumentEvent;
52 import javax.swing.event.DocumentListener;
53 import javax.swing.text.BadLocationException;
54 import javax.swing.text.Document;
55 import javax.swing.text.Element;
56 import javax.swing.text.PlainDocument;
57 import javax.swing.text.PlainView;
58 import javax.swing.text.Segment;
59 import javax.swing.text.StyledEditorKit;
60 import javax.swing.text.Utilities;
61 import javax.swing.text.ViewFactory;
62 import javax.swing.undo.CannotRedoException;
63 import javax.swing.undo.CannotUndoException;
64 import javax.swing.undo.UndoManager;
65 
66 import org.matsim.core.utils.io.IOUtils;
67 
71 class ConfigEditor extends JDialog {
72 
73  private static final boolean IS_MAC = System.getProperty("os.name").startsWith("Mac");
74 
75  private JButton btnSave;
76  private File configFile;
77  private JTextPane xmlPane;
78  private ConfigChangeListener configChangeListener;
79 
80  ConfigEditor(JFrame parent, File configFile, ConfigChangeListener configChangeListener) {
81  super(parent);
82  setTitle("Config Editor");
83  this.configChangeListener = configChangeListener;
84  this.configFile = configFile;
85 
86  this.btnSave = new JButton("Save");
87  this.btnSave.addActionListener(e -> this.save());
88 
89  JButton btnSaveAs = new JButton("Save as…");
90  btnSaveAs.addActionListener(e -> this.saveAs());
91 
92  JScrollPane scrollPane = new JScrollPane();
93 
94  GroupLayout groupLayout = new GroupLayout(getContentPane());
95  groupLayout.setHorizontalGroup(groupLayout.createParallelGroup(Alignment.LEADING)
96  .addGroup(groupLayout.createSequentialGroup()
97  .addContainerGap()
98  .addComponent(this.btnSave)
99  .addPreferredGap(ComponentPlacement.RELATED)
100  .addComponent(btnSaveAs)
101  .addContainerGap(471, Short.MAX_VALUE))
102  .addComponent(scrollPane, Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, 661, Short.MAX_VALUE));
103  groupLayout.setVerticalGroup(groupLayout.createParallelGroup(Alignment.LEADING)
104  .addGroup(groupLayout.createSequentialGroup()
105  .addContainerGap()
106  .addGroup(groupLayout.createParallelGroup(Alignment.BASELINE)
107  .addComponent(this.btnSave)
108  .addComponent(btnSaveAs))
109  .addPreferredGap(ComponentPlacement.RELATED)
110  .addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 456, Short.MAX_VALUE)));
111 
112  this.xmlPane = new JTextPane();
113  this.xmlPane.setContentType("text/xml");
114  this.xmlPane.setEditorKit(new XmlEditorKit());
115  scrollPane.setViewportView(this.xmlPane);
116  getContentPane().setLayout(groupLayout);
117 
118  UndoManager undoManager = this.addUndoFunctionality(this.xmlPane);
119  addSaveFunctionality();
120  addCloseFunctionality();
121 
122  this.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
123  this.pack();
124 
125  this.loadConfig();
126  undoManager.discardAllEdits(); // clear history, otherwise the loading of the initial config could be redone.
127  this.btnSave.setEnabled(false);
128 
129  this.xmlPane.getDocument().addDocumentListener(new DocumentListener() {
130  @Override
131  public void insertUpdate(DocumentEvent e) {
132  ConfigEditor.this.btnSave.setEnabled(true);
133  }
134 
135  @Override
136  public void removeUpdate(DocumentEvent e) {
137  ConfigEditor.this.btnSave.setEnabled(true);
138  }
139 
140  @Override
141  public void changedUpdate(DocumentEvent e) {
142  ConfigEditor.this.btnSave.setEnabled(true);
143  }
144  });
145  }
146 
147  void showEditor() {
148  setVisible(true);
149  this.xmlPane.requestFocus();
150  }
151 
152  void closeEditor() {
153  setVisible(false);
154  }
155 
156  private void saveAs() {
157  SaveFileSaver chooser = new SaveFileSaver();
158  chooser.setSelectedFile(this.configFile);
159  int saveResult = chooser.showSaveDialog(null);
160  if (saveResult == JFileChooser.APPROVE_OPTION) {
161  this.configFile = chooser.getSelectedFile();
162  save();
163  }
164  }
165 
166  private void save() {
167  String fullXml = this.xmlPane.getText();
168  try (BufferedWriter writer = IOUtils.getBufferedWriter(this.configFile.getAbsolutePath())) {
169  writer.write(fullXml);
170  } catch (IOException e) {
171  e.printStackTrace();
172  }
173  this.configChangeListener.configChanged(this.configFile);
174  this.btnSave.setEnabled(false);
175  }
176 
177  private UndoManager addUndoFunctionality(JTextPane textPane) {
178  String UNDO_ACTION = "__UNDO__";
179  String REDO_ACTION = "__REDO__";
180 
181  final UndoManager undoManager = new UndoManager();
182 
183  // Add listener for undoable events
184  textPane.getDocument().addUndoableEditListener(pEvt -> undoManager.addEdit(pEvt.getEdit()));
185 
186  // Add undo/redo actions
187  textPane.getActionMap().put(UNDO_ACTION, new AbstractAction(UNDO_ACTION) {
188  public void actionPerformed(ActionEvent pEvt) {
189  try {
190  if (undoManager.canUndo()) {
191  undoManager.undo();
192  }
193  } catch (CannotUndoException e) {
194  e.printStackTrace();
195  }
196  }
197  });
198  textPane.getActionMap().put(REDO_ACTION, new AbstractAction(REDO_ACTION) {
199  public void actionPerformed(ActionEvent pEvt) {
200  try {
201  if (undoManager.canRedo()) {
202  undoManager.redo();
203  }
204  } catch (CannotRedoException e) {
205  e.printStackTrace();
206  }
207  }
208  });
209 
210  // Create keyboard accelerators for undo/redo actions (Ctrl+Z/Ctrl+Y)
211  textPane.getInputMap()
212  .put(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
213  IS_MAC ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK), UNDO_ACTION);
214  textPane.getInputMap()
215  .put(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
216  IS_MAC ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK), REDO_ACTION);
217 
218  return undoManager;
219  }
220 
221  private void addSaveFunctionality() {
222  String SAVE_ACTION = "__SAVE__";
223 
224  this.xmlPane.getActionMap().put(SAVE_ACTION, new AbstractAction(SAVE_ACTION) {
225  public void actionPerformed(ActionEvent pEvt) {
226  save();
227  }
228  });
229  this.xmlPane.getInputMap()
230  .put(KeyStroke.getKeyStroke(KeyEvent.VK_S,
231  IS_MAC ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK), SAVE_ACTION);
232  }
233 
234  private void addCloseFunctionality() {
235  String CLOSE_ACTION = "__CLOSE__";
236 
237  this.xmlPane.getActionMap().put(CLOSE_ACTION, new AbstractAction(CLOSE_ACTION) {
238  public void actionPerformed(ActionEvent pEvt) {
239  ConfigEditor.this.setVisible(false);
240  }
241  });
242  this.xmlPane.getInputMap()
243  .put(KeyStroke.getKeyStroke(KeyEvent.VK_W,
244  IS_MAC ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK), CLOSE_ACTION);
245  }
246 
247  private void loadConfig() {
248  StringBuilder buffer = new StringBuilder(1024);
249  try (BufferedReader in = IOUtils.getBufferedReader(this.configFile.getAbsolutePath())) {
250  String line;
251  while ((line = in.readLine()) != null) {
252  buffer.append(line);
253  buffer.append(IOUtils.NATIVE_NEWLINE);
254  }
255  } catch (IOException e) {
256  e.printStackTrace();
257  }
258  this.xmlPane.setText(buffer.toString());
259  this.xmlPane.setCaretPosition(0);
260  }
261 
262  interface ConfigChangeListener {
263  void configChanged(File newConfigFile);
264  }
265 
273  private static class XmlEditorKit extends StyledEditorKit {
274 
275  private static final long serialVersionUID = 2969169649596107757L;
276  private ViewFactory xmlViewFactory;
277 
278  public XmlEditorKit() {
279  xmlViewFactory = XmlView::new;
280  }
281 
282  @Override
283  public ViewFactory getViewFactory() {
284  return xmlViewFactory;
285  }
286 
287  @Override
288  public String getContentType() {
289  return "text/xml";
290  }
291  }
292 
303  private static class XmlView extends PlainView {
304 
305  private static HashMap<Pattern, Color> patternColors;
306  private static String TAG_PATTERN = "(</?[a-z\\-]*)\\s?>?";
307  private static String TAG_END_PATTERN = "(/>)";
308  private static String TAG_ATTRIBUTE_PATTERN = "\\s(\\w*)\\=";
309  private static String TAG_ATTRIBUTE_VALUE = "[a-z-]*\\=(\"[^\"]*\")";
310  private static String TAG_COMMENT = "(<!--.*-->)";
311  private static String TAG_CDATA_START = "(\\<!\\[CDATA\\[).*";
312  private static String TAG_CDATA_END = ".*(]]>)";
313 
314  static {
315  // NOTE: the order is important!
316  patternColors = new HashMap<>();
317  patternColors.put(Pattern.compile(TAG_CDATA_START), new Color(128, 128, 128));
318  patternColors.put(Pattern.compile(TAG_CDATA_END), new Color(128, 128, 128));
319  patternColors.put(Pattern.compile(TAG_PATTERN), new Color(63, 127, 127));
320  patternColors.put(Pattern.compile(TAG_ATTRIBUTE_PATTERN), new Color(127, 0, 127));
321  patternColors.put(Pattern.compile(TAG_END_PATTERN), new Color(63, 127, 127));
322  patternColors.put(Pattern.compile(TAG_ATTRIBUTE_VALUE), new Color(42, 0, 255));
323  patternColors.put(Pattern.compile(TAG_COMMENT), new Color(63, 95, 191));
324  }
325 
326  public XmlView(Element element) {
327 
328  super(element);
329 
330  // Set tabsize to 4 (instead of the default 8)
331  getDocument().putProperty(PlainDocument.tabSizeAttribute, 4);
332  }
333 
334  @Override
335  protected int drawUnselectedText(Graphics graphics, int x, int y, int p0, int p1) throws BadLocationException {
336 
337  Document doc = getDocument();
338  String text = doc.getText(p0, p1 - p0);
339 
340  Segment segment = getLineBuffer();
341 
342  SortedMap<Integer, Integer> startMap = new TreeMap<>();
343  SortedMap<Integer, Color> colorMap = new TreeMap<>();
344 
345  // Match all regexes on this snippet, store positions
346  for (Map.Entry<Pattern, Color> entry : patternColors.entrySet()) {
347 
348  Matcher matcher = entry.getKey().matcher(text);
349 
350  while (matcher.find()) {
351  startMap.put(matcher.start(1), matcher.end());
352  colorMap.put(matcher.start(1), entry.getValue());
353  }
354  }
355 
356  int i = 0;
357 
358  // Colour the parts
359  for (Map.Entry<Integer, Integer> entry : startMap.entrySet()) {
360  int start = entry.getKey();
361  int end = entry.getValue();
362 
363  if (i < start) {
364  graphics.setColor(Color.black);
365  doc.getText(p0 + i, start - i, segment);
366  x = Utilities.drawTabbedText(segment, x, y, graphics, this, i);
367  }
368 
369  graphics.setColor(colorMap.get(start));
370  i = end;
371  doc.getText(p0 + start, i - start, segment);
372  x = Utilities.drawTabbedText(segment, x, y, graphics, this, start);
373  }
374 
375  // Paint possible remaining text black
376  if (i < text.length()) {
377  graphics.setColor(Color.black);
378  doc.getText(p0 + i, text.length() - i, segment);
379  x = Utilities.drawTabbedText(segment, x, y, graphics, this, i);
380  }
381 
382  return x;
383  }
384 
385  }
386 }
int drawUnselectedText(Graphics graphics, int x, int y, int p0, int p1)
static HashMap< Pattern, Color > patternColors