001package jmri.jmrit.beantable.beanedit; 002 003import java.awt.BorderLayout; 004import java.awt.Color; 005import java.awt.Component; 006import java.awt.Dimension; 007import java.awt.GridBagConstraints; 008import java.awt.GridBagLayout; 009import java.awt.Insets; 010import java.awt.event.ActionEvent; 011 012import java.util.ArrayList; 013import java.util.Iterator; 014import java.util.List; 015import java.util.Vector; 016 017import javax.annotation.OverridingMethodsMustInvokeSuper; 018import javax.swing.*; 019import javax.swing.event.ChangeEvent; 020import javax.swing.table.AbstractTableModel; 021 022import jmri.*; 023import jmri.NamedBean.DisplayOptions; 024import jmri.jmrit.display.layoutEditor.LayoutBlock; 025import jmri.jmrit.display.layoutEditor.LayoutBlockManager; 026import jmri.util.JmriJFrame; 027import jmri.util.swing.JmriJOptionPane; 028 029/** 030 * Provides the basic information and structure for for a editing the details of 031 * a bean object. 032 * 033 * @param <B> the type of supported NamedBean 034 * 035 * @author Kevin Dickerson Copyright (C) 2011 036 */ 037public abstract class BeanEditAction<B extends NamedBean> extends AbstractAction { 038 039 public BeanEditAction(String s) { 040 super(s); 041 } 042 043 public BeanEditAction() { 044 super("Bean Edit"); 045 } 046 047 B bean; 048 049 public void setBean(B bean) { 050 this.bean = bean; 051 } 052 053 /** 054 * Call to create all the different tabs that will be added to the frame. 055 */ 056 protected void initPanels() { 057 basicDetails(); 058 } 059 060 /** 061 * Initialise panels to be at start of Tabbed Panel menu. 062 * Default empty. 063 */ 064 protected void initPanelsFirst() { 065 } 066 067 /** 068 * Initialise panels to be at end of Tabbed Panel menu. 069 * Startup usage details and Properties. 070 */ 071 protected void initPanelsLast() { 072 usageDetails(); 073 propertiesDetails(); 074 } 075 076 JTextField userNameField = new JTextField(20); 077 JTextArea commentField = new JTextArea(3, 30); 078 JScrollPane commentFieldScroller = new JScrollPane(commentField); 079 private JLabel statusBar = new JLabel(Bundle.getMessage("ItemEditStatusInfo", Bundle.getMessage("ButtonApply"))); 080 081 /** 082 * Create a generic panel that holds the basic bean information System Name, 083 * User Name, and Comment. 084 * 085 * @return a new panel 086 */ 087 BeanItemPanel basicDetails() { 088 BeanItemPanel basic = new BeanItemPanel(); 089 090 basic.setName(Bundle.getMessage("Basic")); 091 basic.setLayout(new BoxLayout(basic, BoxLayout.Y_AXIS)); 092 093 basic.addItem(new BeanEditItem(new JLabel(bean.getSystemName()), Bundle.getMessage("ColumnSystemName"), null)); 094 //Bundle.getMessage("ConnectionHint", "N/A"))); // TODO get connection name from nbMan.getSystemPrefix() 095 096 basic.addItem(new BeanEditItem(userNameField, Bundle.getMessage("ColumnUserName"), null)); 097 098 basic.addItem(new BeanEditItem(commentFieldScroller, Bundle.getMessage("ColumnComment"), null)); 099 100 basic.setSaveItem(new AbstractAction() { 101 @Override 102 public void actionPerformed(ActionEvent e) { 103 saveBasicItems(e); 104 } 105 }); 106 basic.setResetItem(new AbstractAction() { 107 @Override 108 public void actionPerformed(ActionEvent e) { 109 resetBasicItems(e); 110 } 111 }); 112 bei.add(basic); 113 return basic; 114 } 115 116 /** 117 * Create a generic panel that holds Bean usage details. 118 * 119 * @return a new panel 120 */ 121 BeanItemPanel usageDetails() { 122 BeanItemPanel usage = new BeanItemPanel(); 123 124 usage.setName(Bundle.getMessage("Usage")); 125 usage.setLayout(new BoxLayout(usage, BoxLayout.Y_AXIS)); 126 127 usage.addItem(new BeanEditItem(null, null, Bundle.getMessage("UsageText", bean.getDisplayName()))); 128 129 ArrayList<String> listeners = new ArrayList<>(); 130 for (String ref : bean.getListenerRefs()) { 131 if (!listeners.contains(ref)) { 132 listeners.add(ref); 133 } 134 } 135 136 Object[] strArray = new Object[listeners.size()]; 137 listeners.toArray(strArray); 138 JList<Object> list = new JList<>(strArray); 139 list.setLayoutOrientation(JList.VERTICAL); 140 list.setVisibleRowCount(-1); 141 list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); 142 JScrollPane listScroller = new JScrollPane(list); 143 listScroller.setPreferredSize(new Dimension(250, 80)); 144 listScroller.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black))); 145 usage.addItem(new BeanEditItem(listScroller, Bundle.getMessage("ColumnLocation"), null)); 146 147 bei.add(usage); 148 return usage; 149 } 150 private BeanPropertiesTableModel<B> propertiesModel; 151 152 /** 153 * Create a generic panel that holds Bean Property details. 154 * 155 * @return a new panel 156 */ 157 BeanItemPanel propertiesDetails() { 158 BeanItemPanel properties = new BeanItemPanel(); 159 properties.setName(Bundle.getMessage("Properties")); 160 properties.addItem(new BeanEditItem(null, null, Bundle.getMessage("NamedBeanPropertiesTableDescription"))); 161 properties.setLayout(new BoxLayout(properties, BoxLayout.Y_AXIS)); 162 propertiesModel = new BeanPropertiesTableModel<>(); 163 JTable jtAttributes = new JTable(); 164 jtAttributes.setModel(propertiesModel); 165 JScrollPane jsp = new JScrollPane(jtAttributes); 166 Dimension tableDim = new Dimension(400, 200); 167 jsp.setMinimumSize(tableDim); 168 jsp.setMaximumSize(tableDim); 169 jsp.setPreferredSize(tableDim); 170 properties.addItem(new BeanEditItem(jsp, "", null)); 171 properties.setSaveItem(new AbstractAction() { 172 @Override 173 public void actionPerformed(ActionEvent e) { 174 propertiesModel.updateModel(bean); 175 } 176 }); 177 properties.setResetItem(new AbstractAction() { 178 @Override 179 public void actionPerformed(ActionEvent e) { 180 propertiesModel.setModel(bean); 181 } 182 }); 183 184 bei.add(properties); 185 return properties; 186 } 187 188 @OverridingMethodsMustInvokeSuper 189 protected void saveBasicItems(ActionEvent e) { 190 String uname = bean.getUserName(); 191 if (uname == null && !userNameField.getText().isEmpty()) { 192 renameBean(userNameField.getText()); 193 } else if (uname != null && !uname.equals(userNameField.getText())) { 194 if (userNameField.getText().isEmpty()) { 195 removeName(); 196 } else { 197 renameBean(userNameField.getText()); 198 } 199 } 200 bean.setComment(commentField.getText()); 201 } 202 203 @OverridingMethodsMustInvokeSuper 204 protected void resetBasicItems(ActionEvent e) { 205 userNameField.setText(bean.getUserName()); 206 commentField.setText(bean.getComment()); 207 } 208 209 abstract protected String helpTarget(); 210 211 protected ArrayList<BeanItemPanel> bei = new ArrayList<>(5); 212 JmriJFrame f; 213 214 protected Component selectedTab = null; 215 private final JTabbedPane detailsTab = new JTabbedPane(); 216 217 /** 218 * Apply Button. 219 * Accessible so Edit Actions can set custom tool tip. 220 */ 221 protected JButton applyBut; 222 223 public void setSelectedComponent(Component c) { 224 selectedTab = c; 225 } 226 227 @Override 228 public void actionPerformed(ActionEvent e) { 229 if (bean == null) { 230 // display message in status bar TODO 231 log.error("No bean set so unable to edit a null bean"); // NOI18N 232 return; 233 } 234 if (f == null) { 235 f = new JmriJFrame(Bundle.getMessage("EditBean", bean.getBeanType(), bean.getDisplayName()), false, false); 236 f.addHelpMenu(helpTarget(), true); 237 applyBut = new JButton(Bundle.getMessage("ButtonApply")); // create before initPanels() 238 java.awt.Container containerPanel = f.getContentPane(); 239 initPanelsFirst(); 240 initPanels(); 241 initPanelsLast(); 242 243 int i=0; 244 for (BeanItemPanel bi : bei) { 245 addToPanel(bi, bi.getListOfItems()); 246 detailsTab.add(bi, bi.getName(), i); 247 detailsTab.setEnabledAt(i, bi.isEnabled()); 248 detailsTab.setToolTipTextAt(i, bi.getToolTipText()); 249 i++; 250 } 251 containerPanel.add(detailsTab, BorderLayout.CENTER); 252 253 // shared bottom panel part 254 JPanel bottom = new JPanel(); 255 bottom.setLayout(new BoxLayout(bottom, BoxLayout.PAGE_AXIS)); 256 // shared status bar above buttons 257 JPanel panelStatus = new JPanel(); 258 statusBar.setFont(statusBar.getFont().deriveFont(0.9f * userNameField.getFont().getSize())); // a bit smaller 259 statusBar.setForeground(Color.gray); 260 panelStatus.add(statusBar); 261 bottom.add(panelStatus); 262 263 // shared buttons 264 JPanel buttons = new JPanel(); 265 applyBut.addActionListener(this::applyButtonAction); 266 JButton okBut = new JButton(Bundle.getMessage("ButtonOK")); 267 okBut.addActionListener((ActionEvent e1) -> { 268 applyButtonAction(e1); 269 f.dispose(); 270 }); 271 JButton cancelBut = new JButton(Bundle.getMessage("ButtonCancel")); 272 cancelBut.addActionListener(this::cancelButtonAction); 273 buttons.add(applyBut); 274 buttons.add(okBut); 275 buttons.add(cancelBut); 276 bottom.add(buttons); 277 containerPanel.add(bottom, BorderLayout.SOUTH); 278 } 279 for (BeanItemPanel bi : bei) { 280 bi.resetField(); 281 } 282 persistSelectedTab(); // use persistence unless specified by overriding class 283 if (selectedTab != null) { 284 detailsTab.setSelectedComponent(selectedTab); 285 } 286 f.addWindowListener(new java.awt.event.WindowAdapter() { 287 @Override 288 public void windowClosing(java.awt.event.WindowEvent e) { 289 cancelButtonAction(null); 290 } 291 }); 292 f.pack(); 293 f.setVisible(true); 294 } 295 296 /** 297 * Selects previously selected Tab Index for override class name. 298 * Adds listener when Tab changed update UI preference. 299 */ 300 private void persistSelectedTab(){ 301 String TAB_SELECT_STRING = "selectedTabIndex"; // NOI18N 302 Object obj = InstanceManager.getDefault(UserPreferencesManager.class) 303 .getProperty(getClass().getName(), TAB_SELECT_STRING); 304 int previoustab = (obj!=null ? (Integer) obj : 0); 305 // make sure that valid index selected in case a tab is removed in future. 306 detailsTab.setSelectedIndex(Math.max(Math.min(detailsTab.getTabCount()-1, previoustab),0)); 307 // add listener 308 detailsTab.getModel().addChangeListener((ChangeEvent evt) -> { 309 InstanceManager.getDefault(UserPreferencesManager.class) 310 .setProperty(getClass().getName(), TAB_SELECT_STRING, detailsTab.getSelectedIndex()); 311 }); 312 313 } 314 315 protected void applyButtonAction(ActionEvent e) { 316 save(); 317 } 318 319 protected void cancelButtonAction(ActionEvent e) { 320 f.dispose(); 321 } 322 323 /** 324 * Set out the panel based upon the items passed in via the ArrayList. 325 * 326 * @param panel JPanel to add stuff to 327 * @param items a {@link BeanEditItem} list of key-value pairs for the items 328 * to add 329 */ 330 protected void addToPanel(JPanel panel, List<BeanEditItem> items) { 331 GridBagLayout gbLayout = new GridBagLayout(); 332 GridBagConstraints cL = new GridBagConstraints(); 333 GridBagConstraints cD = new GridBagConstraints(); 334 GridBagConstraints cR = new GridBagConstraints(); 335 cL.fill = GridBagConstraints.HORIZONTAL; 336 cL.insets = new Insets(4, 0, 0, 15); // inset for left hand column (description) 337 cR.insets = new Insets(4, 10, 13, 15); // inset for help (right hand column, multi line text area) 338 cD.insets = new Insets(4, 0, 0, 0); // top inset 4, up from 2 to align JLabel with JTextField 339 cD.anchor = GridBagConstraints.NORTHWEST; 340 cL.anchor = GridBagConstraints.NORTHWEST; 341 342 int y = 0; 343 JPanel p = new JPanel(); 344 345 for (BeanEditItem it : items) { 346 // add the 3 elements on a JPanel to the parent panel grid layout 347 if (it.getDescription() != null && it.getComponent() != null) { 348 JLabel descript = new JLabel(it.getDescription() + ":", JLabel.LEFT); 349 if (it.getDescription().isEmpty()) { 350 descript.setText(""); 351 } 352 cL.gridx = 0; 353 cL.gridy = y; 354 cL.ipadx = 3; 355 356 gbLayout.setConstraints(descript, cL); 357 p.setLayout(gbLayout); 358 p.add(descript, cL); 359 360 cD.gridx = 1; 361 cD.gridy = y; 362 363 Component thing = it.getComponent(); 364 //log.debug("descript: '" + it.getDescription() + "', thing: " + thing.getClass().getName()); 365 if (thing instanceof JComboBox 366 || thing instanceof JTextField 367 || thing instanceof JCheckBox 368 || thing instanceof JRadioButton) { 369 cD.insets = new Insets(0, 0, 0, 0); // put a little higher than a JLabel 370 } else if (thing instanceof JColorChooser) { 371 cD.insets = new Insets(-6, 0, 0, 0); // move it up 372 } else { 373 cD.insets = new Insets(4, 0, 0, 0); // reset 374 } 375 gbLayout.setConstraints(thing, cD); 376 p.add(thing, cD); 377 378 cR.gridx = 2; 379 cR.gridwidth = 1; 380 cR.anchor = GridBagConstraints.WEST; 381 382 } else { 383 cR.anchor = GridBagConstraints.CENTER; 384 cR.gridx = 0; 385 cR.gridwidth = 3; 386 } 387 cR.gridy = y; 388 if (it.getHelp() != null) { 389 JTextPane help = new JTextPane(); 390 help.setText(it.getHelp()); 391 gbLayout.setConstraints(help, cR); 392 formatTextAreaAsLabel(help); 393 p.add(help, cR); 394 } 395 y++; 396 } 397 panel.add(p); 398 } 399 400 void formatTextAreaAsLabel(JTextPane pane) { 401 pane.setOpaque(false); 402 pane.setEditable(false); 403 pane.setBorder(null); 404 } 405 406 public void save() { 407 String feedback = Bundle.getMessage("ItemUpdateFeedback", bean.getBeanType()) 408 + " " + bean.getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME); 409 // provide feedback to user, can be overwritten by save action error handler 410 statusBar.setText(feedback); 411 statusBar.setForeground(Color.gray); 412 for (BeanItemPanel bi : bei) { 413 bi.saveItem(); 414 } 415 } 416 417 static boolean validateNumericalInput(String text) { 418 if (text.length() != 0) { 419 try { 420 Integer.parseInt(text); 421 } catch (java.lang.NumberFormatException ex) { 422 return false; 423 } 424 } 425 return true; 426 } 427 428 NamedBeanHandleManager nbMan = InstanceManager.getDefault(NamedBeanHandleManager.class); 429 430 abstract protected B getByUserName(String name); 431 432 /** 433 * Generic method to change the user name of a Bean. 434 * 435 * @param _newName string to use as the new user name 436 */ 437 public void renameBean(String _newName) { 438 if (!allowBlockNameChange("Rename", _newName)) return; // NOI18N 439 B nBean = bean; 440 String oldName = nBean.getUserName(); 441 442 String value = _newName; 443 444 if (value.equals(oldName)) { 445 //name not changed. 446 return; 447 } else { 448 B nB = getByUserName(value); 449 if (nB != null) { 450 log.error("User name is not unique {}", value); // NOI18N 451 String msg; 452 msg = java.text.MessageFormat.format(Bundle.getMessage("WarningUserName"), 453 new Object[]{("" + value)}); 454 JmriJOptionPane.showMessageDialog(f, msg, 455 Bundle.getMessage("WarningTitle"), 456 JmriJOptionPane.ERROR_MESSAGE); 457 return; 458 } 459 } 460 461 nBean.setUserName(value); 462 if (!value.isEmpty()) { 463 if (oldName == null || oldName.isEmpty()) { 464 if (!nbMan.inUse(nBean.getSystemName(), nBean)) { 465 return; 466 } 467 String msg = Bundle.getMessage("UpdateToUserName", 468 new Object[]{nBean.getBeanType(), value, nBean.getSystemName()}); 469 int optionPane = JmriJOptionPane.showConfirmDialog(f, 470 msg, Bundle.getMessage("UpdateToUserNameTitle"), 471 JmriJOptionPane.YES_NO_OPTION); 472 if (optionPane == JmriJOptionPane.YES_OPTION) { 473 //This will update the bean reference from the systemName to the userName 474 try { 475 nbMan.updateBeanFromSystemToUser(nBean); 476 } catch (jmri.JmriException ex) { 477 //We should never get an exception here as we already check that the username is not valid 478 } 479 } 480 481 } else { 482 nbMan.renameBean(oldName, value, nBean); 483 } 484 485 } else { 486 //This will update the bean reference from the old userName to the SystemName 487 nbMan.updateBeanFromUserToSystem(nBean); 488 } 489 } 490 491 /** 492 * Generic method to remove the user name from a bean. 493 */ 494 public void removeName() { 495 if (!allowBlockNameChange("Remove", "")) return; // NOI18N 496 String msg = java.text.MessageFormat.format(Bundle.getMessage("UpdateToSystemName"), 497 new Object[]{bean.getBeanType()}); 498 int optionPane = JmriJOptionPane.showConfirmDialog(f, 499 msg, Bundle.getMessage("UpdateToSystemNameTitle"), 500 JmriJOptionPane.YES_NO_OPTION); 501 if (optionPane == JmriJOptionPane.YES_OPTION) { 502 nbMan.updateBeanFromUserToSystem(bean); 503 } 504 bean.setUserName(null); 505 } 506 507 /** 508 * Determine whether it is safe to rename/remove a Block user name. 509 * <p>The user name is used by the LayoutBlock to link to the block and 510 * by Layout Editor track components to link to the layout block. 511 * @param changeType This will be Remove or Rename. 512 * @param newName For Remove this will be empty, for Rename it will be the new user name. 513 * @return true to continue with the user name change. 514 */ 515 boolean allowBlockNameChange(String changeType, String newName) { 516 if (!bean.getBeanType().equals("Block")) return true; // NOI18N 517 518 // If there is no layout block or the block has no user name, Block rename and remove are ok without notification. 519 String oldName = bean.getUserName(); 520 if (oldName == null) return true; 521 LayoutBlock layoutBlock = jmri.InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(oldName); 522 if (layoutBlock == null) return true; 523 524 // Remove is not allowed if there is a layout block 525 if (changeType.equals("Remove")) { 526 log.warn("Cannot remove user name for block {}", oldName); // NOI18N 527 JmriJOptionPane.showMessageDialog(f, 528 Bundle.getMessage("BlockRemoveUserNameWarning", oldName), // NOI18N 529 Bundle.getMessage("WarningTitle"), // NOI18N 530 JmriJOptionPane.WARNING_MESSAGE); 531 return false; 532 } 533 534 // Confirmation dialog 535 int optionPane = JmriJOptionPane.showConfirmDialog(f, 536 Bundle.getMessage("BlockChangeUserName", oldName, newName), // NOI18N 537 Bundle.getMessage("QuestionTitle"), // NOI18N 538 JmriJOptionPane.YES_NO_OPTION); 539 return optionPane == JmriJOptionPane.YES_OPTION; 540 } 541 542 /** 543 * TableModel for edit of Bean properties. 544 * <p> 545 * At this stage we purely use this to allow the user to delete properties, 546 * not to add them. Changing properties is possible but only for strings. 547 * Based upon the code from the RosterMediaPane 548 */ 549 private static class BeanPropertiesTableModel<B extends NamedBean> extends AbstractTableModel { 550 551 Vector<KeyValueModel> attributes; 552 String titles[]; 553 boolean wasModified; 554 555 private static class KeyValueModel { 556 557 public KeyValueModel(String k, Object v) { 558 key = k; 559 value = v; 560 } 561 public String key; 562 public Object value; 563 } 564 565 public BeanPropertiesTableModel() { 566 titles = new String[2]; 567 titles[0] = Bundle.getMessage("NamedBeanPropertyName"); 568 titles[1] = Bundle.getMessage("NamedBeanPropertyValue"); 569 } 570 571 public void setModel(B nb) { 572 attributes = new Vector<>(nb.getPropertyKeys().size()); 573 Iterator<String> ite = nb.getPropertyKeys().iterator(); 574 while (ite.hasNext()) { 575 String key = ite.next(); 576 KeyValueModel kv = new KeyValueModel(key, nb.getProperty(key)); 577 attributes.add(kv); 578 } 579 wasModified = false; 580 } 581 582 public void updateModel(B nb) { 583 if (!wasModified()) { 584 return; //No changed made 585 } // add and update keys 586 for (int i = 0; i < attributes.size(); i++) { 587 KeyValueModel kv = attributes.get(i); 588 if ((kv.key != null) 589 && // only update if key value defined, will do the remove too 590 ((nb.getProperty(kv.key) == null) || (!kv.value.equals(nb.getProperty(kv.key))))) { 591 nb.setProperty(kv.key, kv.value); 592 } 593 } 594 //remove undefined keys 595 596 Iterator<String> ite = nb.getPropertyKeys().iterator(); 597 while (ite.hasNext()) { 598 if (!keyExist(ite.next())) // not a very efficient algorithm! 599 { 600 ite.remove(); 601 } 602 } 603 wasModified = false; 604 } 605 606 private boolean keyExist(Object k) { 607 if (k == null) { 608 return false; 609 } 610 for (int i = 0; i < attributes.size(); i++) { 611 if (k.equals(attributes.get(i).key)) { 612 return true; 613 } 614 } 615 return false; 616 } 617 618 @Override 619 public int getColumnCount() { 620 return 2; 621 } 622 623 @Override 624 public int getRowCount() { 625 return attributes.size(); 626 } 627 628 @Override 629 public String getColumnName(int col) { 630 return titles[col]; 631 } 632 633 @Override 634 public Object getValueAt(int row, int col) { 635 if (row < attributes.size()) { 636 if (col == 0) { 637 return attributes.get(row).key; 638 } 639 if (col == 1) { 640 return attributes.get(row).value; 641 } 642 } 643 return "..."; 644 } 645 646 @Override 647 public void setValueAt(Object value, int row, int col) { 648 KeyValueModel kv; 649 650 if (row < attributes.size()) // already exist? 651 { 652 kv = attributes.get(row); 653 } else { 654 kv = new KeyValueModel("", ""); 655 } 656 657 if (col == 0) // update key 658 //Force keys to be save as a single string with no spaces 659 { 660 if (!keyExist(((String) value).replaceAll("\\s", ""))) // if not exist 661 { 662 kv.key = ((String) value).replaceAll("\\s", ""); 663 } else { 664 setValueAt(value + "-1", row, col); // else change key name 665 return; 666 } 667 } 668 669 if (col == 1) // update value 670 { 671 kv.value = value; 672 } 673 if (row < attributes.size()) // existing one 674 { 675 attributes.set(row, kv); 676 } else { 677 attributes.add(row, kv); // new one 678 } 679 if ((col == 0) && (kv.key.isEmpty())) { 680 attributes.remove(row); // actually maybe remove 681 } 682 wasModified = true; 683 fireTableCellUpdated(row, col); 684 } 685 686 @Override 687 public boolean isCellEditable(int row, int col) { 688 return true; 689 } 690 691 public boolean wasModified() { 692 return wasModified; 693 } 694 } 695 696 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BeanEditAction.class); 697 698}