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