001 package org.LiveGraph.gui.dss;
002
003 import java.awt.BorderLayout;
004 import java.awt.Color;
005 import java.awt.Component;
006 import java.awt.Dimension;
007 import java.awt.FlowLayout;
008 import java.awt.FontMetrics;
009 import java.awt.event.ActionEvent;
010 import java.awt.event.ActionListener;
011 import java.util.ArrayList;
012 import java.util.List;
013 import java.util.StringTokenizer;
014
015 import javax.swing.AbstractCellEditor;
016 import javax.swing.BorderFactory;
017 import javax.swing.DefaultCellEditor;
018 import javax.swing.JButton;
019 import javax.swing.JColorChooser;
020 import javax.swing.JComboBox;
021 import javax.swing.JLabel;
022 import javax.swing.JOptionPane;
023 import javax.swing.JPanel;
024 import javax.swing.JScrollPane;
025 import javax.swing.JTable;
026 import javax.swing.JTextField;
027 import javax.swing.ListSelectionModel;
028 import javax.swing.table.AbstractTableModel;
029 import javax.swing.table.TableCellEditor;
030 import javax.swing.table.TableCellRenderer;
031
032 import org.LiveGraph.LiveGraph;
033 import org.LiveGraph.dataCache.CacheEvent;
034 import org.LiveGraph.dataCache.DataCache;
035 import org.LiveGraph.events.Event;
036 import org.LiveGraph.events.EventType;
037 import org.LiveGraph.gui.GUIEvent;
038 import org.LiveGraph.gui.LiveGraphSettingsPanel;
039 import org.LiveGraph.settings.DataSeriesSettings;
040 import org.LiveGraph.settings.GraphSettings;
041 import org.LiveGraph.settings.SettingsEvent;
042 import org.LiveGraph.settings.DataSeriesSettings.TransformMode;
043
044 import com.softnetConsult.utils.collections.Pair;
045 import com.softnetConsult.utils.collections.ReadOnlyIterator;
046 import com.softnetConsult.utils.exceptions.Bug;
047 import com.softnetConsult.utils.exceptions.UnexpectedSwitchCase;
048 import com.softnetConsult.utils.swing.DisEnablingPanel;
049 import com.softnetConsult.utils.swing.SwingTools;
050 import com.softnetConsult.utils.sys.SystemTools;
051
052
053 /**
054 * The data series settings panel of the application. This is the only component contained in
055 * the content pane of the application's data series settings window. API users may request
056 * {@link org.LiveGraph.gui.GUIManager} to create additional instances of a
057 * {@code SeriesSettingsPanel} if they wish to integrate the LiveGraph GUI into their application.
058 *
059 * <p>
060 * <strong>LiveGraph</strong>
061 * (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>).
062 * </p>
063 * <p>Copyright (c) 2007-2008 by G. Paperin.</p>
064 * <p>File: SeriesSettingsPanel.java</p>
065 * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
066 * without modification, are permitted provided that the following terms and conditions are met:
067 * </p>
068 * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
069 * acknowledgement of the LiveGraph project and its web-site, the above copyright notice,
070 * this list of conditions and the following disclaimer.<br />
071 * 2. Redistributions in binary form must reproduce the above acknowledgement of the
072 * LiveGraph project and its web-site, the above copyright notice, this list of conditions
073 * and the following disclaimer in the documentation and/or other materials provided with
074 * the distribution.<br />
075 * 3. All advertising materials mentioning features or use of this software or any derived
076 * software must display the following acknowledgement:<br />
077 * <em>This product includes software developed by the LiveGraph project and its
078 * contributors.<br />(http://www.live-graph.org)</em><br />
079 * 4. All advertising materials distributed in form of HTML pages or any other technology
080 * permitting active hyper-links that mention features or use of this software or any
081 * derived software must display the acknowledgment specified in condition 3 of this
082 * agreement, and in addition, include a visible and working hyper-link to the LiveGraph
083 * homepage (http://www.live-graph.org).
084 * </p>
085 * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY
086 * OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
087 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
088 * THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
089 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
090 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
091 * </p>
092 *
093 * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
094 * @version {@value org.LiveGraph.LiveGraph#version}
095 *
096 */
097 public class SeriesSettingsPanel extends LiveGraphSettingsPanel {
098
099 private static final long HIGHLIGHT_LEN = 1500;
100
101 private List<String> seriesLabels = null;
102 private AbstractTableModel tableModel = null;
103 private JTable table = null;
104 private ListSelectionModel selectionModel = null;
105
106 private JComboBox scaleTypeCombo = null;
107
108 private DisEnablingPanel topPanel;
109 private JButton advPanelButt = null, advGoButt = null;
110 private JPanel advPanel = null;
111 private int helpCols = 0;
112 private JTextField advFrom = null, advTo = null, advEvery = null;
113 private JComboBox advAction = null;
114
115 /**
116 * This is the default constructor.
117 */
118 public SeriesSettingsPanel() {
119 super();
120 initialize();
121 }
122
123 /**
124 * This method initializes the panel.
125 */
126 private void initialize() {
127
128 // General settings:
129
130 Dimension panelDim = new Dimension(450, 200);
131 this.setPreferredSize(panelDim);
132 this.setSize(panelDim);
133 this.setLayout(new BorderLayout(0, 0));
134
135 // Buttons at the top:
136
137 JPanel panel = null;
138 JButton button = null;
139 JLabel label = null;
140
141 topPanel = new DisEnablingPanel(new BorderLayout());
142 this.add(topPanel, BorderLayout.NORTH);
143
144 panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 1, 1));
145 topPanel.add(panel, BorderLayout.NORTH);
146
147 button = new JButton("Show all");
148 button.addActionListener(new ActionListener() {
149 public void actionPerformed(ActionEvent e) {
150 if (null == tableModel) return;
151 DataSeriesSettings dss = LiveGraph.application().getDataSeriesSettings();
152 dss.setShowAll(0, tableModel.getRowCount() - 1, true);
153 tableModel.fireTableDataChanged();
154 }
155 });
156 panel.add(button);
157
158 button = new JButton("Hide all");
159 button.addActionListener(new ActionListener() {
160 public void actionPerformed(ActionEvent e) {
161 if (null == tableModel) return;
162 DataSeriesSettings dss = LiveGraph.application().getDataSeriesSettings();
163 dss.setShowAll(0, tableModel.getRowCount() - 1, false);
164 tableModel.fireTableDataChanged();
165 }
166 });
167 panel.add(button);
168
169 button = new JButton("Toggle all");
170 button.addActionListener(new ActionListener() {
171 public void actionPerformed(ActionEvent e) {
172 if (null == tableModel) return;
173 DataSeriesSettings dss = LiveGraph.application().getDataSeriesSettings();
174 dss.setShowToggleAll(0, tableModel.getRowCount() - 1);
175 tableModel.fireTableDataChanged();
176 }
177 });
178 panel.add(button);
179
180 // Advanced selection panel:
181 advPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 1, 1));
182
183 advPanelButt = new JButton(">>");
184 advPanelButt.addActionListener(new ActionListener() {
185 public void actionPerformed(ActionEvent e) {
186 setAdvancedPanelVisibility(!advPanel.isVisible());
187 }
188 });
189 panel.add(advPanelButt);
190
191 topPanel.add(advPanel, BorderLayout.CENTER);
192
193 advPanel.add(label = new JLabel("From:"));
194 label.setFont(SwingTools.getPlainFont(label));
195 advFrom = new JTextField("0", 4);
196 advPanel.add(advFrom);
197
198 advPanel.add(label = new JLabel("To:"));
199 label.setFont(SwingTools.getPlainFont(label));
200 advTo = new JTextField("10000", 4);
201 advPanel.add(advTo);
202
203 advPanel.add(label = new JLabel("Every:"));
204 label.setFont(SwingTools.getPlainFont(label));
205 advEvery = new JTextField("10", 8);
206 advPanel.add(advEvery);
207
208 advAction = new JComboBox(new String[] {"Show", "Hide", "Toggle"});
209 advAction.setFont(SwingTools.getPlainFont(advAction));
210 advPanel.add(advAction);
211
212 advGoButt = new JButton("Go");
213 advGoButt.addActionListener(new ActionListener() {
214 public void actionPerformed(ActionEvent e) {
215 runAdvancedSelector();
216 }
217 });
218 advPanel.add(advGoButt);
219
220 // Main table:
221
222 panel = new JPanel(new BorderLayout(0, 0));
223
224 table = createTable();
225 panel.add(new JScrollPane(table), BorderLayout.CENTER);
226 this.add(panel, BorderLayout.CENTER);
227
228 setAdvancedPanelVisibility(false);
229
230 } // private void initialize()
231
232 /**
233 * Creates and initialises the labels table.
234 * @return The labels table.
235 */
236 private JTable createTable() {
237
238 final String[] scaleModes = new String[] {"Actual value",
239 "Transform into [0..1]",
240 "Scale by specified value",
241 "Log to specified base"};
242
243 final String[] colNames = new String[] {"Show", "Label", "Colour",
244 "Transformation", "Transform parameter"};
245
246 final String[] helperColumnNames = new String[] {"Index"};
247
248 seriesLabels = new ArrayList<String>();
249
250 tableModel = new AbstractTableModel() {
251
252 public int getColumnCount() {
253 return colNames.length + helpCols;
254 }
255
256 public int getRowCount() {
257 return seriesLabels.size();
258 }
259
260 @Override
261 public String getColumnName(int col) {
262 if (col < helpCols)
263 return helperColumnNames[col];
264 return colNames[col - helpCols];
265 }
266
267 public Object getValueAt(int row, int col) {
268
269 if (1 == helpCols && 0 == col)
270 return row;
271
272 DataSeriesSettings dsSetts = LiveGraph.application().getDataSeriesSettings();
273 if (null == dsSetts)
274 dsSetts = new DataSeriesSettings();
275
276 switch (col - helpCols) {
277 case 0: return dsSetts.getShow(row);
278 case 1: try {
279 return seriesLabels.get(row);
280 } catch(IndexOutOfBoundsException e) {
281 return "";
282 }
283 case 2: return dsSetts.getColour(row);
284 case 3: switch(dsSetts.getTransformMode(row)) {
285 case Transform_None: return scaleModes[0];
286 case Transform_In0to1: return scaleModes[1];
287 case Transform_ScaleBySetVal: return scaleModes[2];
288 case Transform_Logarithm: return scaleModes[3];
289 default: throw new UnexpectedSwitchCase(dsSetts.getTransformMode(row));
290 }
291 case 4: return dsSetts.getTransformParam(row);
292 default: throw new Bug("Forgot to provide getValueAt for table column " + col + ".");
293 }
294 }
295
296 @Override
297 public void setValueAt(Object val, int row, int col) {
298 super.setValueAt(val, row, col);
299 switch (col - helpCols) {
300 case 0: LiveGraph.application().getDataSeriesSettings().setShow(row, ((Boolean) val).booleanValue());
301 break;
302 //case 1: same as the default case.
303 case 2: LiveGraph.application().getDataSeriesSettings().setColour(row, (Color) val);
304 break;
305 case 3: if (scaleModes[0].equals(val))
306 LiveGraph.application().getDataSeriesSettings().setTransformMode(row, TransformMode.Transform_None);
307 else if (scaleModes[1].equals(val))
308 LiveGraph.application().getDataSeriesSettings().setTransformMode(row, TransformMode.Transform_In0to1);
309 else if (scaleModes[2].equals(val))
310 LiveGraph.application().getDataSeriesSettings().setTransformMode(row, TransformMode.Transform_ScaleBySetVal);
311 else if (scaleModes[3].equals(val))
312 LiveGraph.application().getDataSeriesSettings().setTransformMode(row, TransformMode.Transform_Logarithm);
313 else
314 throw new Bug("Unexpected scale mode (" + val + ")!");
315 break;
316 case 4: LiveGraph.application().getDataSeriesSettings().setTransformParam(row, ((Double) val).doubleValue());
317 break;
318 default: throw new Bug("Column " + col + " is not supposed to be editable.");
319 }
320 }
321
322 @Override
323 public boolean isCellEditable(int row, int col) {
324 return (col >= helpCols && col != 1 + helpCols);
325 }
326
327 @Override
328 public Class<?> getColumnClass(int col) {
329 if (1 == helpCols && 0 == col)
330 return Integer.class;
331 switch (col - helpCols) {
332 case 0: return Boolean.class;
333 case 1: return String.class;
334 case 2: return Color.class;
335 case 3: return String.class;
336 case 4: return Double.class;
337 default: throw new Bug("Forgot to provide getColumnClass for table column " + col + ".");
338 }
339 }
340
341 }; // AbstractTableModel model = new AbstractTableModel()
342
343
344 //JTable table = new JTable(tableModel);
345
346 JTable table = new JTable(tableModel) {
347 @Override public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) {
348 ; // Prevent the table selection to be changed by user input.
349 }
350 };
351
352
353 selectionModel = table.getSelectionModel();
354 selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
355
356 scaleTypeCombo = new JComboBox(scaleModes);
357 scaleTypeCombo.setFont(SwingTools.getPlainFont(scaleTypeCombo));
358
359 table.getColumnModel().getColumn(3 + helpCols).setCellEditor(new DefaultCellEditor(scaleTypeCombo));
360
361 table.setDefaultEditor(Color.class, new ColourEditor());
362 table.setDefaultRenderer(Color.class, new TableCellRenderer() {
363 private JLabel label = new JLabel();
364 public Component getTableCellRendererComponent(JTable table, Object colour,
365 boolean isSelected, boolean hasFocus, int row, int column) {
366 label.setOpaque(true);
367 Color brdCol = isSelected ? table.getSelectionBackground() : table.getBackground();
368 label.setBorder(BorderFactory.createLineBorder(brdCol, 3));
369 label.setBackground((Color) colour);
370 return label;
371 }
372
373 });
374
375 Component rc = table.getTableHeader().getDefaultRenderer().getTableCellRendererComponent(
376 table, null, false, false, 0, 0);
377 FontMetrics fm = rc.getFontMetrics(rc.getFont());
378 int w, sumW;
379
380 sumW = w = fm.stringWidth(colNames[0 + helpCols]) + 10;
381 table.getColumnModel().getColumn(0 + helpCols).setPreferredWidth(w);
382
383 sumW += w = fm.stringWidth(colNames[2 + helpCols]) + 10;
384 table.getColumnModel().getColumn(2 + helpCols).setPreferredWidth(w);
385
386 w = 0;
387 for (String sm : scaleModes) { w = Math.max(w, fm.stringWidth(sm)); }
388 sumW += w = Math.max(w, fm.stringWidth(colNames[3 + helpCols])) + 10;
389 table.getColumnModel().getColumn(3 + helpCols).setPreferredWidth(w);
390
391 sumW += w = fm.stringWidth(colNames[4 + helpCols]) + 10;
392 table.getColumnModel().getColumn(4 + helpCols).setPreferredWidth(w);
393
394 w = table.getPreferredScrollableViewportSize().width - sumW - 20;
395 table.getColumnModel().getColumn(1 + helpCols).setPreferredWidth(w);
396
397 return table;
398
399 } // private JTable createTable()
400
401
402 /**
403 * Shows and hides the advanced selector panel.
404 * @param show Whether to show the advanced selector panel.
405 */
406 private void setAdvancedPanelVisibility(boolean show) {
407
408 advPanelButt.setText(show ? "<<" : ">>");
409 advPanel.setVisible(show);
410
411 if (show) {
412 helpCols = 1;
413 } else {
414 helpCols = 0;
415 }
416
417 tableModel.fireTableStructureChanged();
418 table.getColumnModel().getColumn(3 + helpCols).setCellEditor(new DefaultCellEditor(scaleTypeCombo));
419 }
420
421 /**
422 * Executes the advanced selection of data series.
423 */
424 private void runAdvancedSelector() {
425
426 // Get values:
427 int start, end;
428 List<Integer> mods;
429 int action;
430 try {
431 start = Integer.parseInt(advFrom.getText());
432 end = Integer.parseInt(advTo.getText());
433
434 StringTokenizer tok = new StringTokenizer(advEvery.getText(), ",; ");
435 mods = new ArrayList<Integer>();
436 while(tok.hasMoreTokens()) {
437 int t = Integer.parseInt(tok.nextToken());
438 if (t > 0)
439 mods.add(t);
440 }
441
442 Object act = advAction.getSelectedItem();
443 if ("Show".equals(act))
444 action = 1;
445 else if ("Hide".equals(act))
446 action = 2;
447 else if ("Toggle".equals(act))
448 action = 3;
449 else
450 action = -1;
451
452 } catch(NumberFormatException e) {
453 JOptionPane.showMessageDialog(this, e.getMessage(), "Invalid number format", JOptionPane.ERROR_MESSAGE);
454 return;
455 }
456
457 if (0 > start)
458 start = 0;
459 if (tableModel.getRowCount() <= start)
460 start = tableModel.getRowCount() - 1;
461
462 if (0 > end || tableModel.getRowCount() <= end)
463 end = tableModel.getRowCount() - 1;
464
465 if (start > end) {
466 int t = start; start = end; end = t;
467 }
468
469 advFrom.setText(Integer.toString(start));
470 advTo.setText(Integer.toString(end));
471
472 topPanel.setEnabled(false);
473 DataSeriesSettings setts = LiveGraph.application().getDataSeriesSettings();
474 for (int r = start; r <= end; r++) {
475
476 advGoButt.setText(Integer.toString(r));
477
478 int offs = r - start;
479 boolean apply = false;
480 for (int m = 0; !apply && m < mods.size(); m++) {
481 apply = (0 == offs % mods.get(m));
482 }
483
484 if (!apply)
485 continue;
486
487 switch(action) {
488 case 1: setts.setShow(r, true); break;
489 case 2: setts.setShow(r, false); break;
490 case 3: setts.setShow(r, !setts.getShow(r)); break;
491 default: throw new UnexpectedSwitchCase(action);
492 }
493 }
494 advGoButt.setText("Go");
495 topPanel.setEnabled(true);
496 }
497
498
499 /**
500 * Updates the series labels from the specified iterator.
501 * @param labels Series labels.
502 */
503 public void setSeriesLabels(ReadOnlyIterator<String> labels) {
504
505 seriesLabels = new ArrayList<String>();
506
507 if (null == labels)
508 return;
509
510 while (labels.hasNext())
511 seriesLabels.add(labels.next());
512 }
513
514 /**
515 * Processes events.
516 *
517 * @param event Event to process.
518 */
519 @Override
520 public void eventRaised(Event<? extends EventType> event) {
521
522 super.eventRaised(event);
523
524 if (event.getDomain() == CacheEvent.class) {
525 processCacheEvent(event.cast(CacheEvent.class));
526 return;
527 }
528
529 if (event.getDomain() == GUIEvent.class) {
530 processGUIEvent(event.cast(GUIEvent.class));
531 return;
532 }
533 }
534
535 /**
536 * When the application's settings change, this method is called in order
537 * to update the GUI accordingly.<br/>
538 * - Updates the table display when series settings were loaded from a file
539 * or when a setting changes.<br />
540 * - When a series was selected as x-axis, the corresponding setting is highlighted for a second.<br />
541 *
542 * @param event Describes the change event.
543 */
544 @Override
545 protected void processSettingsEvent(Event<SettingsEvent> event) {
546
547 switch(event.getType()) {
548
549 case DSS_Load:
550 tableModel.fireTableDataChanged();
551 break;
552
553 case DSS_SeriesRange_Visibility:
554 Pair range = (Pair) event.getInfoObject();
555 tableModel.fireTableRowsUpdated((Integer) range.elem1, (Integer) range.elem2);
556 break;
557
558 case DSS_Series_Visibility:
559 case DSS_Series_Colour:
560 case DSS_Series_TransformMode:
561 case DSS_Series_TransformParam:
562 int si = (int) event.getInfoLong();
563 tableModel.fireTableRowsUpdated(si, si);
564 break;
565
566 case GS_XAxisType:
567 case GS_XAxisSeriesIndex:
568 if (event.getInfoObject() == GraphSettings.XAxisType.XAxis_DSNum)
569 break;
570 final int serInd = (int) event.getInfoLong();
571 tableModel.fireTableRowsUpdated(serInd, serInd);
572 selectionModel.setSelectionInterval(serInd, serInd);
573 (new Thread(new Runnable() {
574 public void run() {
575 SystemTools.sleep(HIGHLIGHT_LEN);
576 if (!selectionModel.getValueIsAdjusting()
577 && selectionModel.getMinSelectionIndex() == serInd
578 && selectionModel.getMaxSelectionIndex() == serInd) {
579 selectionModel.clearSelection();
580 }
581 }
582 }, "SeriesMarkedAsXAxis-TableSelectionController")).start();
583 break;
584
585 default:
586 break;
587
588 }
589
590 } // protected void processSettingsEvent
591
592 /**
593 * Locally updates the series-lables when they have been changed in the data cache.
594 *
595 * @param event The cache event.
596 */
597 private void processCacheEvent(Event<CacheEvent> event) {
598
599 if (CacheEvent.CACHE_UpdatedLabels == event.getType()) {
600
601 DataCache cache = (DataCache) event.getProducer();
602 synchronized (cache) {
603 setSeriesLabels(cache.iterateDataSeriesLabels());
604 }
605 tableModel.fireTableDataChanged();
606 }
607 } // private void processCacheEvent
608
609 /**
610 * Updates local view on GUI events.
611 *
612 * @param event The GUI event.
613 */
614 private void processGUIEvent(Event<GUIEvent> event) {
615
616 if (GUIEvent.GUI_DataSeriesHighlighted == event.getType()) {
617
618 @SuppressWarnings("unchecked")
619 List<Integer> seriesIndices = (List<Integer>) event.getInfoObject();
620
621 if (null == seriesIndices)
622 return;
623
624 selectionModel.clearSelection();
625
626 if (seriesIndices.isEmpty())
627 return;
628
629 for (int s : seriesIndices)
630 selectionModel.addSelectionInterval(s, s);
631 /*
632 (new Thread(new Runnable() {
633 public void run() {
634 try { Thread.sleep(HIGHLIGHT_LEN); } catch (InterruptedException e) {}
635 selectionModel.clearSelection();
636 }
637 }, "HighlightSeries-TableSelectionController")).start();
638 */
639 }
640 } // private void processGUIEvent
641
642 /**
643 * A colour selection cell editor for the settings table.
644 */
645 private class ColourEditor extends AbstractCellEditor implements TableCellEditor {
646
647 private Color selColor;
648 private JButton button;
649
650 public ColourEditor() {
651 button = new JButton();
652 button.addActionListener(new ActionListener() {
653 public void actionPerformed(ActionEvent e) {
654 button.setBackground(selColor);
655 Color c = JColorChooser.showDialog(button, "Choose a colour for the data series.", selColor);
656 if (null != c)
657 selColor = c;
658 fireEditingStopped();
659 }
660 });
661 }
662
663 public Object getCellEditorValue() {
664 return selColor;
665 }
666
667 public Component getTableCellEditorComponent(JTable table, Object value,
668 boolean isSelected, int row, int column) {
669 selColor = (Color) value;
670 Color brdCol = isSelected ? table.getSelectionBackground() : table.getBackground();
671 button.setBorder(BorderFactory.createLineBorder(brdCol, 3));
672 return button;
673 }
674 } //private class ColourEditor
675
676 } // public class SeriesSettingsPanel