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