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