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