001package jmri.jmrit.throttle.panels; 002 003import java.awt.*; 004import java.awt.event.*; 005import java.beans.PropertyChangeListener; 006import java.util.Arrays; 007 008import javax.swing.*; 009import javax.swing.border.Border; 010import javax.swing.border.EmptyBorder; 011import javax.swing.event.*; 012 013import jmri.DccThrottle; 014import jmri.InstanceManager; 015import jmri.LocoAddress; 016import jmri.Throttle; 017import jmri.jmrit.roster.Roster; 018import jmri.jmrit.roster.RosterEntry; 019import jmri.jmrit.throttle.interfaces.AddressListener; 020import jmri.jmrit.throttle.interfaces.FunctionListener; 021import jmri.jmrit.throttle.preferences.ThrottlesPreferences; 022import jmri.util.FileUtil; 023import jmri.util.gui.GuiLafPreferencesManager; 024import jmri.util.swing.OptionallyTabbedPanel; 025 026import org.jdom2.Element; 027 028/** 029 * A Panel that contains buttons for each decoder function. 030 * 031 * <hr> 032 * This file is part of JMRI. 033 * <p> 034 * JMRI is free software; you can redistribute it and/or modify it under the 035 * terms of version 2 of the GNU General Public License as published by the Free 036 * Software Foundation. See the "COPYING" file for a copy of this license. 037 * <p> 038 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 039 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 040 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 041 * 042 */ 043public class FunctionPanel extends OptionallyTabbedPanel implements FunctionListener, PropertyChangeListener, AddressListener { 044 045 private static final int DEFAULT_FUNCTION_BUTTONS = 24; // just enough to fill the initial pane 046 private static final int MAX_FUNCTION_BUTTONS_PER_TAB = 33; 047 private DccThrottle mThrottle; 048 049 private FunctionButton[] functionButtons; 050 private boolean withPopupMenuOnFnButtons; 051 private boolean fnBtnUpdatedFromRoster = false; // avoid to reinit function button twice (from throttle xml and from roster) 052 053 private AddressPanel addressPanel = null; // to access roster infos 054 055 /** 056 * Constructor 057 * 058 * @param withPopupMenu popup menu on function button available if true 059 * 060 */ 061 public FunctionPanel(boolean withPopupMenu) { 062 super(MAX_FUNCTION_BUTTONS_PER_TAB); 063 InstanceManager.getDefault(ThrottlesPreferences.class).addPropertyChangeListener(this); 064 withPopupMenuOnFnButtons = withPopupMenu; 065 initGUI(); 066 applyPreferences(); 067 } 068 069 public FunctionPanel() { 070 this(true); 071 } 072 073 public void dispose() { 074 InstanceManager.getDefault(ThrottlesPreferences.class).removePropertyChangeListener(this); 075 if (functionButtons != null) { 076 for (FunctionButton fb : functionButtons) { 077 fb.destroy(); 078 fb.removeFunctionListener(this); 079 } 080 functionButtons = null; 081 } 082 if (addressPanel != null) { 083 addressPanel.removeAddressListener(this); 084 addressPanel = null; 085 } 086 if (mThrottle != null) { 087 mThrottle.removePropertyChangeListener(this); 088 mThrottle = null; 089 } 090 } 091 092 public FunctionButton[] getFunctionButtons() { 093 return Arrays.copyOf(functionButtons, functionButtons.length); 094 } 095 096 /** 097 * Resize inner function buttons array 098 * 099 */ 100 private void resizeFnButtonsArray(int n) { 101 FunctionButton[] newFunctionButtons = new FunctionButton[n]; 102 System.arraycopy(functionButtons, 0, newFunctionButtons, 0, Math.min( functionButtons.length, n)); 103 if (n > functionButtons.length) { 104 for (int i=functionButtons.length;i<n;i++) { 105 newFunctionButtons[i] = new FunctionButton(withPopupMenuOnFnButtons); 106 add(newFunctionButtons[i]); 107 resetFnButton(newFunctionButtons[i],i); 108 // Copy mouse and keyboard controls to new components 109 for (MouseWheelListener mwl:getMouseWheelListeners()) { 110 newFunctionButtons[i].addMouseWheelListener(mwl); 111 } 112 } 113 } 114 functionButtons = newFunctionButtons; 115 } 116 117 118 /** 119 * Get notification that a function has changed state. 120 * 121 * @param functionNumber The function that has changed. 122 * @param isSet True if the function is now active (or set). 123 */ 124 @Override 125 public void notifyFunctionStateChanged(int functionNumber, boolean isSet) { 126 log.debug("notifyFunctionStateChanged: fNumber={} isSet={} " ,functionNumber, isSet); 127 if (mThrottle != null) { 128 log.debug("setting throttle {} function {}", mThrottle.getLocoAddress(), functionNumber); 129 mThrottle.setFunction(functionNumber, isSet); 130 } 131 } 132 133 /** 134 * Get notification that a function's lockable status has changed. 135 * 136 * @param functionNumber The function that has changed (0-28). 137 * @param isLockable True if the function is now Lockable (continuously 138 * active). 139 */ 140 @Override 141 public void notifyFunctionLockableChanged(int functionNumber, boolean isLockable) { 142 log.debug("notifyFnLockableChanged: fNumber={} isLockable={} " ,functionNumber, isLockable); 143 if (mThrottle != null) { 144 log.debug("setting throttle {} function momentary {}", mThrottle.getLocoAddress(), functionNumber); 145 mThrottle.setFunctionMomentary(functionNumber, !isLockable); 146 } 147 } 148 149 /** 150 * Enable or disable all the buttons. 151 * @param isEnabled true to enable, false to disable. 152 */ 153 @Override 154 public void setEnabled(boolean isEnabled) { 155 for (FunctionButton functionButton : functionButtons) { 156 functionButton.setEnabled(isEnabled); 157 } 158 } 159 160 /** 161 * Enable or disable all the buttons depending on throttle status 162 * If a throttle is assigned, enable all, else disable all 163 */ 164 public void setEnabled() { 165 setEnabled(mThrottle != null); 166 } 167 168 public void setAddressPanel(AddressPanel ap) { 169 if (addressPanel != null) { 170 addressPanel.removeAddressListener(this); 171 } 172 addressPanel = ap; 173 if (addressPanel != null) { 174 addressPanel.addAddressListener(this); 175 if (addressPanel.getThrottle() != null ) { 176 notifyAddressThrottleFound(addressPanel.getThrottle()); 177 } else { 178 notifyAddressReleased(addressPanel.getCurrentAddress()); 179 } 180 } 181 } 182 183 public void saveFunctionButtonsToRoster(RosterEntry rosterEntry) { 184 log.debug("saveFunctionButtonsToRoster"); 185 if (rosterEntry == null) { 186 return; 187 } 188 for (FunctionButton functionButton : functionButtons) { 189 int functionNumber = functionButton.getIdentity(); 190 String text = functionButton.getButtonLabel(); 191 boolean lockable = functionButton.getIsLockable(); 192 boolean visible = functionButton.getDisplay(); 193 String imagePath = functionButton.getIconPath(); 194 String imageSelectedPath = functionButton.getSelectedIconPath(); 195 if (functionButton.isDirty()) { 196 if (!text.equals(rosterEntry.getFunctionLabel(functionNumber))) { 197 if (text.isEmpty()) { 198 text = null; // reset button text to default 199 } 200 rosterEntry.setFunctionLabel(functionNumber, text); 201 } 202 String fontSizeKey = "function"+functionNumber+"_ThrottleFontSize"; 203 if (rosterEntry.getAttribute(fontSizeKey) != null && functionButton.getFont().getSize() == InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) { 204 rosterEntry.deleteAttribute(fontSizeKey); 205 } 206 if (functionButton.getFont().getSize() != InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) { 207 rosterEntry.putAttribute(fontSizeKey, ""+functionButton.getFont().getSize()); 208 } 209 String imgButtonSizeKey = "function"+functionNumber+"_ThrottleImageButtonSize"; 210 if (rosterEntry.getAttribute(imgButtonSizeKey) != null && functionButton.getButtonImageSize() == FunctionButton.DEFAULT_IMG_SIZE) { 211 rosterEntry.deleteAttribute(imgButtonSizeKey); 212 } 213 if (functionButton.getButtonImageSize() != FunctionButton.DEFAULT_IMG_SIZE) { 214 rosterEntry.putAttribute(imgButtonSizeKey, ""+functionButton.getButtonImageSize()); 215 } 216 if (rosterEntry.getFunctionLabel(functionNumber) != null ) { 217 if( lockable != rosterEntry.getFunctionLockable(functionNumber)) { 218 rosterEntry.setFunctionLockable(functionNumber, lockable); 219 } 220 if( visible != rosterEntry.getFunctionVisible(functionNumber)) { 221 rosterEntry.setFunctionVisible(functionNumber, visible); 222 } 223 if ( (!imagePath.isEmpty() && rosterEntry.getFunctionImage(functionNumber) == null ) 224 || (rosterEntry.getFunctionImage(functionNumber) != null && imagePath.compareTo(rosterEntry.getFunctionImage(functionNumber)) != 0)) { 225 rosterEntry.setFunctionImage(functionNumber, imagePath); 226 } 227 if ( (!imageSelectedPath.isEmpty() && rosterEntry.getFunctionSelectedImage(functionNumber) == null ) 228 || (rosterEntry.getFunctionSelectedImage(functionNumber) != null && imageSelectedPath.compareTo(rosterEntry.getFunctionSelectedImage(functionNumber)) != 0)) { 229 rosterEntry.setFunctionSelectedImage(functionNumber, imageSelectedPath); 230 } 231 } 232 functionButton.setDirty(false); 233 } 234 } 235 Roster.getDefault().writeRoster(); 236 } 237 238 /** 239 * Place and initialize all the buttons. 240 */ 241 private void initGUI() { 242 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 243 resetFnButtons(); 244 JScrollPane scrollPane = new JScrollPane(this); 245 scrollPane.getViewport().setOpaque(false); // container already gets this done (for play/edit mode) 246 scrollPane.setOpaque(false); 247 setOpaque(false); 248 Border empyBorder = new EmptyBorder(0,0,0,0); // force look'n feel, no border 249 scrollPane.setViewportBorder( empyBorder ); 250 scrollPane.setBorder( empyBorder ); 251 scrollPane.setWheelScrollingEnabled(false); // already used by speed slider 252 scrollPane.getViewport().addChangeListener((e) -> viewPortSizeChanged(e)); 253 } 254 255 private void viewPortSizeChanged(ChangeEvent e) { 256 // make sure function button area is laid out consistent with sizing 257 revalidate(); 258 } 259 260 private void setUpDefaultLightFunctionButton() { 261 try { 262 functionButtons[0].setIconPath("resources/icons/functionicons/svg/lightsOff.svg"); 263 functionButtons[0].setSelectedIconPath("resources/icons/functionicons/svg/lightsOn.svg"); 264 } catch (Exception e) { 265 log.debug("Exception loading svg icon : {}", e.getMessage()); 266 } finally { 267 if ((functionButtons[0].getIcon() == null) || (functionButtons[0].getSelectedIcon() == null)) { 268 log.debug("Issue loading svg icon, reverting to png"); 269 functionButtons[0].setIconPath("resources/icons/functionicons/transparent_background/lights_off.png"); 270 functionButtons[0].setSelectedIconPath("resources/icons/functionicons/transparent_background/lights_on.png"); 271 } 272 } 273 } 274 275 /** 276 * Apply preferences 277 * + global throttles preferences 278 * + this throttle settings if any 279 */ 280 private void applyPreferences() { 281 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 282 RosterEntry re = null; 283 if (mThrottle != null && addressPanel != null) { 284 re = addressPanel.getRosterEntry(); 285 } 286 for (int i = 0; i < functionButtons.length; i++) { 287 functionButtons[i].setDisplay(true); // default to true 288 if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 289 setUpDefaultLightFunctionButton(); 290 } else { 291 functionButtons[i].setIconPath(null); 292 functionButtons[i].setSelectedIconPath(null); 293 } 294 if (re != null) { 295 if (re.getFunctionLabel(i) != null) { 296 functionButtons[i].setDisplay(re.getFunctionVisible(i)); 297 functionButtons[i].setButtonLabel(re.getFunctionLabel(i)); 298 if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 299 functionButtons[i].setIconPath(re.getFunctionImage(i)); 300 functionButtons[i].setSelectedIconPath(re.getFunctionSelectedImage(i)); 301 } else { 302 functionButtons[i].setIconPath(null); 303 functionButtons[i].setSelectedIconPath(null); 304 } 305 functionButtons[i].setIsLockable(re.getFunctionLockable(i)); 306 } else { 307 functionButtons[i].setDisplay( ! (preferences.isUsingExThrottle() && preferences.isHidingUndefinedFuncButt()) ); 308 } 309 } 310 functionButtons[i].updateLnF(); 311 } 312 } 313 314 /** 315 * Rebuild function buttons 316 * 317 */ 318 private void rebuildFnButons(int n) { 319 removeAll(); 320 functionButtons = new FunctionButton[n]; 321 for (int i = 0; i < functionButtons.length; i++) { 322 functionButtons[i] = new FunctionButton(withPopupMenuOnFnButtons); 323 resetFnButton(functionButtons[i],i); 324 add(functionButtons[i]); 325 // Copy mouse and keyboard controls to new components 326 for (MouseWheelListener mwl:getMouseWheelListeners()) { 327 functionButtons[i].addMouseWheelListener(mwl); 328 } 329 } 330 } 331 332 /** 333 * Update function buttons 334 * - from selected throttle setting and state 335 * - from roster entry if any 336 */ 337 private void updateFnButtons() { 338 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 339 if (mThrottle != null && addressPanel != null) { 340 RosterEntry rosterEntry = addressPanel.getRosterEntry(); 341 if (rosterEntry != null) { 342 fnBtnUpdatedFromRoster = true; 343 log.debug("RosterEntry found: {}", rosterEntry.getId()); 344 } 345 for (int i = 0; i < functionButtons.length; i++) { 346 // update from selected throttle setting 347 functionButtons[i].setEnabled(true); 348 functionButtons[i].setIdentity(i); // full reset of function 349 functionButtons[i].setThrottle(mThrottle); 350 functionButtons[i].setState(mThrottle.getFunction(i)); // reset button state 351 functionButtons[i].setIsLockable(!mThrottle.getFunctionMomentary(i)); 352 functionButtons[i].setDropFolder(FileUtil.getUserResourcePath()); 353 // update from roster entry if any 354 if (rosterEntry != null) { 355 functionButtons[i].setDropFolder(Roster.getDefault().getRosterFilesLocation()); 356 boolean needUpdate = false; 357 String imgButtonSize = rosterEntry.getAttribute("function"+i+"_ThrottleImageButtonSize"); 358 if (imgButtonSize != null) { 359 try { 360 functionButtons[i].setButtonImageSize(Integer.parseInt(imgButtonSize)); 361 needUpdate = true; 362 } catch (NumberFormatException e) { 363 log.debug("setFnButtons(): can't parse button image size attribute "); 364 } 365 } 366 String text = rosterEntry.getFunctionLabel(i); 367 if (text != null) { 368 functionButtons[i].setDisplay(rosterEntry.getFunctionVisible(i)); 369 functionButtons[i].setButtonLabel(text); 370 if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 371 functionButtons[i].setIconPath(rosterEntry.getFunctionImage(i)); 372 functionButtons[i].setSelectedIconPath(rosterEntry.getFunctionSelectedImage(i)); 373 } else { 374 functionButtons[i].setIconPath(null); 375 functionButtons[i].setSelectedIconPath(null); 376 } 377 functionButtons[i].setIsLockable(rosterEntry.getFunctionLockable(i)); 378 needUpdate = true; 379 } else if (preferences.isUsingExThrottle() 380 && preferences.isHidingUndefinedFuncButt()) { 381 functionButtons[i].setDisplay(false); 382 needUpdate = true; 383 } 384 String fontSize = rosterEntry.getAttribute("function"+i+"_ThrottleFontSize"); 385 if (fontSize != null) { 386 try { 387 functionButtons[i].setFont(new Font("Monospaced", Font.PLAIN, Integer.parseInt(fontSize))); 388 needUpdate = true; 389 } catch (NumberFormatException e) { 390 log.debug("setFnButtons(): can't parse font size attribute "); 391 } 392 } 393 if (needUpdate) { 394 functionButtons[i].updateLnF(); 395 } 396 } 397 } 398 } 399 } 400 401 402 private void resetFnButton(FunctionButton fb, int i) { 403 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 404 fb.setThrottle(mThrottle); 405 if (mThrottle!=null) { 406 fb.setState(mThrottle.getFunction(i)); // reset button state 407 fb.setIsLockable(!mThrottle.getFunctionMomentary(i)); 408 } 409 fb.setIdentity(i); 410 fb.addFunctionListener(this); 411 fb.setButtonLabel( i<3 ? Bundle.getMessage(Throttle.getFunctionString(i)) : Throttle.getFunctionString(i) ); 412 fb.setDisplay(true); 413 if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 414 setUpDefaultLightFunctionButton(); 415 } else { 416 fb.setIconPath(null); 417 fb.setSelectedIconPath(null); 418 } 419 fb.updateLnF(); 420 421 // always display f0, F1 and F2 422 if (i < 3) { 423 fb.setVisible(true); 424 } 425 } 426 427 /** 428 * Reset function buttons : 429 * - rebuild function buttons 430 * - reset their properties to default 431 * - update according to throttle and roster (if any) 432 * 433 */ 434 public void resetFnButtons() { 435 // rebuild function buttons 436 if (mThrottle == null) { 437 rebuildFnButons(DEFAULT_FUNCTION_BUTTONS); 438 } else { 439 rebuildFnButons(mThrottle.getFunctions().length); 440 } 441 // reset their properties to defaults 442 for (int i = 0; i < functionButtons.length; i++) { 443 resetFnButton(functionButtons[i],i); 444 } 445 // update according to throttle and roster (if any) 446 updateFnButtons(); 447 repaint(); 448 } 449 450 /** 451 * Update the state of this panel if any of the functions change. 452 * {@inheritDoc} 453 */ 454 @Override 455 public void propertyChange(java.beans.PropertyChangeEvent e) { 456 if (e == null) { 457 return; 458 } 459 log.debug("Property change event received {} / {}", e.getPropertyName(), e.getNewValue()); 460 if (mThrottle!=null){ 461 for (int i = 0; i < mThrottle.getFunctions().length; i++) { 462 if (e.getPropertyName().equals(Throttle.getFunctionString(i))) { 463 setButtonByFuncNumber(i,false,(Boolean) e.getNewValue()); 464 } else if (e.getPropertyName().equals(Throttle.getFunctionMomentaryString(i))) { 465 setButtonByFuncNumber(i,true,!(Boolean) e.getNewValue()); 466 } 467 } 468 } 469 if (ThrottlesPreferences.prefPopertyName.compareTo(e.getPropertyName()) == 0) { 470 applyPreferences(); 471 } 472 } 473 474 private void setButtonByFuncNumber(int function, boolean lockable, boolean newVal){ 475 for (FunctionButton button : functionButtons) { 476 if (button.getIdentity() == function) { 477 if (lockable) { 478 button.setIsLockable(newVal); 479 } else { 480 button.setState(newVal); 481 } 482 } 483 } 484 } 485 486 /** 487 * Collect the prefs of this object into XML Element. 488 * <ul> 489 * <li> Window prefs 490 * <li> Each button has id, text, lock state. 491 * </ul> 492 * 493 * @return the XML of this object. 494 */ 495 public Element getXml() { 496 Element me = new Element("FunctionPanel"); // NOI18N 497 java.util.ArrayList<Element> children = new java.util.ArrayList<>(1 + functionButtons.length); 498 for (FunctionButton functionButton : functionButtons) { 499 children.add(functionButton.getXml()); 500 } 501 me.setContent(children); 502 return me; 503 } 504 505 /** 506 * Set the preferences based on the XML Element. 507 * <ul> 508 * <li> Window prefs 509 * <li> Each button has id, text, lock state. 510 * </ul> 511 * 512 * @param e The Element for this object. 513 */ 514 public void setXml(Element e) { 515 if (! fnBtnUpdatedFromRoster) { 516 java.util.List<Element> buttonElements = e.getChildren("FunctionButton"); 517 518 if (buttonElements != null && buttonElements.size() > 0) { 519 // just in case 520 rebuildFnButons( buttonElements.size() ); 521 int i = 0; 522 for (Element buttonElement : buttonElements) { 523 functionButtons[i++].setXml(buttonElement); 524 } 525 } 526 } 527 } 528 529 @Override 530 public void notifyAddressThrottleFound(DccThrottle t) { 531 log.debug("Throttle found for {}",t); 532 if (mThrottle != null) { 533 mThrottle.removePropertyChangeListener(this); 534 } 535 mThrottle = t; 536 mThrottle.addPropertyChangeListener(this); 537 int numFns = mThrottle.getFunctions().length; 538 if (addressPanel != null && addressPanel.getRosterEntry() != null) { 539 // +1 because we want the _number_ of functions, and we have to count F0 540 numFns = Math.min(numFns, addressPanel.getRosterEntry().getMaxFnNumAsInt()+1); 541 } 542 log.debug("notifyAddressThrottleFound number of functions {}", numFns); 543 resizeFnButtonsArray(numFns); 544 updateFnButtons(); 545 setEnabled(true); 546 } 547 548 private void adressReleased() { 549 if (mThrottle != null) { 550 mThrottle.removePropertyChangeListener(this); 551 } 552 mThrottle = null; 553 fnBtnUpdatedFromRoster = false; 554 resetFnButtons(); 555 setEnabled(false); 556 } 557 558 @Override 559 public void notifyAddressReleased(LocoAddress la) { 560 log.debug("Throttle released"); 561 adressReleased(); 562 } 563 564 @Override 565 public void notifyAddressChosen(LocoAddress l) { 566 } 567 568 @Override 569 public void notifyRosterEntrySelected(RosterEntry re) { 570 } 571 572 @Override 573 public void notifyConsistAddressChosen(LocoAddress l) { 574 } 575 576 @Override 577 public void notifyConsistAddressReleased(LocoAddress la) { 578 log.debug("Consist throttle released"); 579 adressReleased(); 580 } 581 582 @Override 583 public void notifyConsistAddressThrottleFound(DccThrottle t) { 584 log.debug("Consist throttle found"); 585 if (mThrottle == null) { 586 notifyAddressThrottleFound(t); 587 } 588 } 589 590 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FunctionPanel.class); 591}