001package jmri.jmrix.swing; 002 003import java.awt.BorderLayout; 004import java.awt.Color; 005import java.awt.Component; 006import java.awt.Dimension; 007import java.awt.FlowLayout; 008import java.awt.GridBagConstraints; 009import java.awt.GridBagLayout; 010import java.awt.Insets; 011import java.awt.event.ActionEvent; 012import java.beans.IndexedPropertyChangeEvent; 013import java.beans.PropertyChangeEvent; 014import java.text.MessageFormat; 015import java.util.ArrayList; 016import java.util.List; 017import java.util.ResourceBundle; 018 019import javax.annotation.CheckForNull; 020import javax.annotation.Nonnull; 021import javax.swing.Icon; 022import javax.swing.ImageIcon; 023import javax.swing.JButton; 024import javax.swing.JCheckBox; 025import javax.swing.JComboBox; 026import javax.swing.JComponent; 027import javax.swing.JLabel; 028import javax.swing.JPanel; 029import javax.swing.JTabbedPane; 030import javax.swing.event.ChangeEvent; 031import javax.swing.event.ChangeListener; 032 033import jmri.InstanceManager; 034import jmri.SystemConnectionMemo; 035import jmri.jmrix.ConnectionConfig; 036import jmri.jmrix.ConnectionConfigManager; 037import jmri.jmrix.ConnectionStatus; 038import jmri.jmrix.JmrixConfigPane; 039import jmri.profile.ProfileManager; 040import jmri.swing.ManagingPreferencesPanel; 041import jmri.swing.PreferencesPanel; 042import jmri.util.FileUtil; 043import jmri.util.startup.AbstractActionModel; 044import jmri.util.startup.StartupActionsManager; 045import jmri.util.swing.JmriJOptionPane; 046 047import org.openide.util.lookup.ServiceProvider; 048 049/** 050 * 051 * @author Randall Wood randall.h.wood@alexandriasoftware.com 052 */ 053@ServiceProvider(service = PreferencesPanel.class) 054public class ConnectionsPreferencesPanel extends JTabbedPane implements ManagingPreferencesPanel { 055 056 private static final ResourceBundle rb = ResourceBundle.getBundle("apps.AppsConfigBundle"); // for some items // NOI18N 057 058 059 private final ImageIcon deleteIcon; 060 private final ImageIcon deleteIconRollOver; 061 private final Dimension deleteButtonSize; 062 private ImageIcon addIcon; 063 private boolean restartRequired = false; 064 065 private ArrayList<JmrixConfigPane> configPanes = new ArrayList<>(); 066 067 public ConnectionsPreferencesPanel() { 068 super(); 069 deleteIconRollOver = new ImageIcon( 070 FileUtil.findURL("program:resources/icons/misc/gui3/Delete16x16.png")); 071 deleteIcon = new ImageIcon( 072 FileUtil.findURL("program:resources/icons/misc/gui3/Delete-bw16x16.png")); 073 deleteButtonSize = new Dimension( 074 deleteIcon.getIconWidth() + 2, 075 deleteIcon.getIconHeight() + 2); 076 addIcon = new ImageIcon( 077 FileUtil.findURL("program:resources/icons/misc/gui3/Add16x16.png")); 078 ConnectionConfigManager ccm = InstanceManager.getDefault(ConnectionConfigManager.class); 079 ConnectionConfig[] connections = ccm.getConnections(); 080 if (connections.length != 0) { 081 for (int i = 0; i < connections.length; i++) { 082 addConnection(i, JmrixConfigPane.createPanel(i)); 083 } 084 } else { 085 addConnection(0, JmrixConfigPane.createNewPanel()); 086 } 087 ccm.addPropertyChangeListener(ConnectionConfigManager.CONNECTIONS, (PropertyChangeEvent evt) -> { 088 int i = ((IndexedPropertyChangeEvent) evt).getIndex(); 089 log.debug("PrefPanel ChangeListener of tab index i = {} of {} list", i+1, configPanes.size()); 090 //if (evt.getNewValue() == null 091 // && i < configPanes.size() 092 // && evt.getOldValue().equals(configPanes.get(i).getCurrentObject())) { 093 // //removeTab(null, i); // called same method removeTab again, conn tab was already removed by the user click 094 //} else 095 if ((evt.getOldValue() == null) && (evt.getNewValue() != null)) { 096 for (JmrixConfigPane pane : this.configPanes) { 097 if (pane.getCurrentObject() == null ) { 098 log.error("did not expect pane.getCurrentObject()==null here for {} {} {}", i, evt.getNewValue(), configPanes); 099 } else if (pane.getCurrentObject().equals(evt.getNewValue())) { 100 return; // don't add the connection again 101 } 102 } 103 addConnection(i, JmrixConfigPane.createPanel(i)); 104 } 105 }); 106 this.addChangeListener(addTabListener); 107 newConnectionTab(); 108 this.setSelectedIndex(0); 109 } 110 111 transient ChangeListener addTabListener = (ChangeEvent evt) -> { 112 // This method is called whenever the selected tab changes 113 JTabbedPane pane = (JTabbedPane) evt.getSource(); 114 int sel = pane.getSelectedIndex(); 115 if (sel == -1) { 116 addConnectionTab(); 117 return; 118 } else { 119 Icon icon = pane.getIconAt(sel); 120 if (icon == addIcon) { 121 addConnectionTab(); 122 return; 123 } 124 } 125 activeTab(); 126 }; 127 128 private void activeTab() { 129 for (int i = 0; i < this.getTabCount() - 1; i++) { 130 JPanel panel = (JPanel) this.getTabComponentAt(i); 131 if (panel != null) { 132 panel.invalidate(); 133 Component[] comp = panel.getComponents(); 134 for (Component c : comp) { 135 if (c instanceof JButton) { 136 if (i == this.getSelectedIndex()) { 137 c.setVisible(true); 138 } else { 139 c.setVisible(false); 140 } 141 } 142 } 143 } 144 } 145 } 146 147 private void addConnection(int tabPosition, final JmrixConfigPane configPane) { 148 JPanel p = new JPanel(); 149 p.setLayout(new BorderLayout()); 150 p.add(configPane, BorderLayout.CENTER); 151 152 JButton tabCloseButton = new JButton(deleteIcon); 153 tabCloseButton.setPreferredSize(deleteButtonSize); 154 tabCloseButton.setBorderPainted(false); 155 tabCloseButton.setRolloverIcon(deleteIconRollOver); 156 tabCloseButton.setVisible(false); 157 158 JPanel c = new JPanel(); 159 c.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); 160 final JCheckBox disable = new JCheckBox(rb.getString("ButtonDisableConnection")); 161 disable.setSelected(configPane.getDisabled()); 162 disable.addActionListener((ActionEvent e) -> { 163 configPane.setDisabled(disable.isSelected()); 164 }); 165 c.add(disable); 166 p.add(c, BorderLayout.SOUTH); 167 String title; 168 169 if (configPane.getConnectionName() != null) { 170 title = configPane.getConnectionName(); 171 } else if ((configPane.getCurrentProtocolName() != null) 172 && (!configPane.getCurrentProtocolName().equals( 173 JmrixConfigPane.NONE))) { 174 title = configPane.getCurrentProtocolName(); 175 } else { 176 title = rb.getString("TabbedLayoutConnection") + (tabPosition + 1); 177 if (this.indexOfTab(title) != -1) { 178 for (int x = 2; x < 12; x++) { 179 title = rb.getString("TabbedLayoutConnection") 180 + (tabPosition + 2); 181 if (this.indexOfTab(title) != -1) { 182 break; 183 } 184 } 185 } 186 } 187 188 final JPanel tabTitle = new JPanel(new BorderLayout(5, 0)); 189 tabTitle.setOpaque(false); 190 p.setName(title); 191 192 if (configPane.getDisabled()) { 193 title = "(" + title + ")"; 194 } 195 196 JLabel tabLabel = new JLabel(title, JLabel.LEFT); 197 tabTitle.add(tabLabel, BorderLayout.WEST); 198 tabTitle.add(tabCloseButton, BorderLayout.EAST); 199 200 this.configPanes.add(configPane); 201 this.add(p); 202 this.setTabComponentAt(tabPosition, tabTitle); 203 204 tabCloseButton.addActionListener((ActionEvent e) -> { // initial user Delete conn click 205 removeTab(e, this.indexOfTabComponent(tabTitle)); 206 }); 207 208 this.setToolTipTextAt(tabPosition, title); 209 210 if (configPane.getCurrentObject() != null) { 211 if (ConnectionStatus.instance().isConnectionOk( 212 configPane.getCurrentObject().getAdapter().getSystemConnectionMemo())) { 213 tabLabel.setForeground(Color.black); 214 } else { 215 tabLabel.setForeground(Color.red); 216 } 217 } 218 if (configPane.getDisabled()) { 219 tabLabel.setForeground(Color.ORANGE); 220 } 221 222 } 223 224 void addConnectionTab() { 225 this.removeTabAt(this.indexOfTab(addIcon)); 226 addConnection(configPanes.size(), JmrixConfigPane.createNewPanel()); 227 newConnectionTab(); 228 } 229 230 private void newConnectionTab() { 231 this.addTab(null, addIcon, null, rb.getString("ToolTipAddNewConnection")); 232 this.setSelectedIndex(this.getTabCount() - 2); 233 } 234 235 private void removeTab(ActionEvent e, int x) { 236 int i; 237 i = x; // copy x for handling 238 239 if (i != -1) { 240 // only prompt if triggered by action event 241 if (e != null && !confirmDelete(getSystemPrefix(this.configPanes.get(i)), this.getTitleAt(i))) { 242 return; 243 } 244 245 log.debug("i = {}, this.getTabCount() = {}, configPanes.size()={}", i, this.getTabCount(), configPanes.size()); 246 JmrixConfigPane configPane = this.configPanes.get(i); 247 this.removeChangeListener(addTabListener); 248 this.remove(i); 249 log.debug("start of connection #{} disposal", i); 250 try { 251 JmrixConfigPane.dispose(configPane); 252 } catch (NullPointerException ex) { 253 log.error("Caught Null Pointer Exception while removing connection tab {}", i); 254 } 255 log.debug("connection tab #{} successfully disposed. configPanes.size() = {}", i+1, configPanes.size()); 256 this.configPanes.remove(i); // on delete of connection a loop (listener) called this method 2x 257 restartRequired = true; 258 if (this.getTabCount() == 1) { // normally not required, stays in place while adding or deleting 259 addConnectionTab(); 260 } 261 if (x != 0) { 262 this.setSelectedIndex(x - 1); 263 } else { 264 this.setSelectedIndex(0); 265 } 266 this.addChangeListener(addTabListener); 267 } 268 activeTab(); 269 } 270 271 /** 272 * Get the system prefix of the connection in a pane. 273 * 274 * @param configPane the connection pane 275 * @return the system prefix, or null if the pane has no connection 276 */ 277 @CheckForNull 278 static String getSystemPrefix(@CheckForNull JmrixConfigPane configPane) { 279 if (configPane == null) { 280 return null; 281 } 282 ConnectionConfig config = configPane.getCurrentObject(); 283 if (config == null || config.getAdapter() == null 284 || config.getAdapter().getSystemConnectionMemo() == null) { 285 return null; 286 } 287 return config.getAdapter().getSystemConnectionMemo().getSystemPrefix(); 288 } 289 290 /** 291 * Ask the user to confirm deleting a connection. 292 * <p> 293 * If the connection has Start Up actions, one dialog both confirms the 294 * delete and asks what to do with each action: remove it (the default), 295 * keep it, or move it to another connection that offers the same action 296 * and does not already have it. Otherwise the plain Yes/No question is 297 * asked. Nothing is changed unless the user confirms. 298 * 299 * @param prefix the system prefix of the connection being deleted; may be 300 * null if it has no connection 301 * @param title the name of the connection, for the dialog 302 * @return true to carry on deleting the connection, false if cancelled 303 */ 304 boolean confirmDelete(@CheckForNull String prefix, String title) { 305 String question = MessageFormat.format(rb.getString("MessageDoDelete"), title); 306 StartupActionsManager manager = InstanceManager.getOptionalDefault(StartupActionsManager.class).orElse(null); 307 List<AbstractActionModel> models = (prefix == null || manager == null) 308 ? new ArrayList<>() : manager.getActionsForConnection(prefix); 309 if (models.isEmpty()) { 310 return JmriJOptionPane.showConfirmDialog(this, question, 311 rb.getString("MessageDeleteConnection"), 312 JmriJOptionPane.YES_NO_OPTION) == JmriJOptionPane.YES_OPTION; 313 } 314 315 JPanel panel = new JPanel(new GridBagLayout()); 316 GridBagConstraints c = new GridBagConstraints(); 317 c.gridx = 0; 318 c.gridy = 0; 319 c.gridwidth = 2; 320 c.anchor = GridBagConstraints.WEST; 321 c.insets = new Insets(2, 2, 2, 2); 322 panel.add(new JLabel(question), c); 323 c.gridy++; 324 c.insets = new Insets(8, 2, 6, 2); 325 panel.add(new JLabel(rb.getString("MessageStartupActionsForConnection")), c); 326 c.gridwidth = 1; 327 c.insets = new Insets(2, 2, 2, 8); 328 List<JComboBox<StartupActionChoice>> boxes = new ArrayList<>(); 329 for (AbstractActionModel model : models) { 330 JComboBox<StartupActionChoice> box = new JComboBox<>(); 331 for (StartupActionChoice choice : getChoices(manager, model, prefix)) { 332 box.addItem(choice); 333 } 334 boxes.add(box); 335 c.gridy++; 336 c.gridx = 0; 337 panel.add(new JLabel(model.getName()), c); 338 c.gridx = 1; 339 panel.add(box, c); 340 } 341 342 String[] options = {rb.getString("ButtonDeleteConnection"), Bundle.getMessage("ButtonCancel")}; 343 int n = JmriJOptionPane.showOptionDialog(this, panel, rb.getString("MessageDeleteConnection"), 344 JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]); 345 if (n != 0) { 346 return false; 347 } 348 for (int k = 0; k < models.size(); k++) { 349 applyStartupActionChoice(manager, models.get(k), (StartupActionChoice) boxes.get(k).getSelectedItem()); 350 } 351 return true; 352 } 353 354 /** 355 * Get the choices for a Start Up action of a connection being deleted: 356 * remove (first, the default), keep, and a move to each other running 357 * connection that offers the action and does not already have it. 358 * 359 * @param manager the Start Up actions manager 360 * @param model the action 361 * @param prefix the system prefix of the connection being deleted 362 * @return the choices, in the order shown 363 */ 364 @Nonnull 365 static List<StartupActionChoice> getChoices(@Nonnull StartupActionsManager manager, 366 @Nonnull AbstractActionModel model, @CheckForNull String prefix) { 367 List<StartupActionChoice> choices = new ArrayList<>(); 368 choices.add(new StartupActionChoice(StartupActionChoice.Kind.REMOVE, null, 369 rb.getString("StartupActionRemove"))); 370 choices.add(new StartupActionChoice(StartupActionChoice.Kind.KEEP, null, 371 rb.getString("StartupActionKeep"))); 372 for (SystemConnectionMemo memo : StartupActionsManager.getConnectionsOffering(model, prefix)) { 373 if (!hasAction(manager, model.getClassName(), memo.getSystemPrefix())) { 374 choices.add(new StartupActionChoice(StartupActionChoice.Kind.MOVE, memo.getSystemPrefix(), 375 MessageFormat.format(rb.getString("StartupActionMoveTo"), 376 memo.getUserName(), memo.getSystemPrefix()))); 377 } 378 } 379 return choices; 380 } 381 382 private static boolean hasAction(StartupActionsManager manager, String className, String systemPrefix) { 383 return manager.getActionsForConnection(systemPrefix).stream() 384 .anyMatch(other -> className.equals(other.getClassName())); 385 } 386 387 /** 388 * Carry out the choice made for a Start Up action of a connection being 389 * deleted. 390 * 391 * @param manager the Start Up actions manager 392 * @param model the action 393 * @param choice what to do; null is treated as remove 394 */ 395 static void applyStartupActionChoice(@Nonnull StartupActionsManager manager, 396 @Nonnull AbstractActionModel model, @CheckForNull StartupActionChoice choice) { 397 if (choice == null || choice.kind == StartupActionChoice.Kind.REMOVE) { 398 manager.removeAction(model); 399 } else if (choice.kind == StartupActionChoice.Kind.MOVE && choice.systemPrefix != null) { 400 manager.reassignAction(model, choice.systemPrefix); 401 } 402 // KEEP: left as it is; shown in red until moved or removed 403 } 404 405 /** 406 * One choice for a Start Up action of a connection being deleted. 407 */ 408 static class StartupActionChoice { 409 410 enum Kind { 411 REMOVE, KEEP, MOVE 412 } 413 414 final Kind kind; 415 final String systemPrefix; // the connection to move to, for MOVE only 416 private final String text; 417 418 StartupActionChoice(@Nonnull Kind kind, @CheckForNull String systemPrefix, @Nonnull String text) { 419 this.kind = kind; 420 this.systemPrefix = systemPrefix; 421 this.text = text; 422 } 423 424 @Override 425 public String toString() { 426 return text; 427 } 428 } 429 430 @Override 431 public String getPreferencesItem() { 432 return "CONNECTIONS"; // NOI18N 433 } 434 435 @Override 436 public String getPreferencesItemText() { 437 return rb.getString("MenuConnections"); // NOI18N 438 } 439 440 @Override 441 public String getTabbedPreferencesTitle() { 442 return null; 443 } 444 445 @Override 446 public String getLabelKey() { 447 return null; 448 } 449 450 @Override 451 public JComponent getPreferencesComponent() { 452 return this; 453 } 454 455 @Override 456 public boolean isPersistant() { 457 return false; 458 } 459 460 @Override 461 public String getPreferencesTooltip() { 462 return null; 463 } 464 465 @Override 466 public void savePreferences() { 467 InstanceManager.getDefault(ConnectionConfigManager.class).savePreferences(ProfileManager.getDefault().getActiveProfile()); 468 } 469 470 @Override 471 public boolean isDirty() { 472 return this.configPanes.stream().anyMatch(JmrixConfigPane::isDirty); 473 } 474 475 @Override 476 public boolean isRestartRequired() { 477 return this.restartRequired 478 || this.configPanes.stream().anyMatch(JmrixConfigPane::isRestartRequired); 479 } 480 481 @Override 482 public boolean isPreferencesValid() { 483 return this.configPanes.stream().allMatch(JmrixConfigPane::isPreferencesValid); 484 } 485 486 @Override 487 public List<PreferencesPanel> getPreferencesPanels() { 488 return new ArrayList<>(this.configPanes); 489 } 490 491 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ConnectionsPreferencesPanel.class); 492 493}