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.WrapLayout; 020 021import org.jdom2.Element; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025/** 026 * A JInternalFrame that contains buttons for each decoder function. 027 */ 028public class FunctionPanel extends JInternalFrame implements FunctionListener, java.beans.PropertyChangeListener, AddressListener { 029 030 private static final int DEFAULT_FUNCTION_BUTTONS = 24; // just enough to fill the initial pane 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 JPanel(); 206 mainPanel.setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2)); 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 setContentPane(scrollPane); 216 setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 217 } 218 219 private void setUpDefaultLightFunctionButton() { 220 try { 221 functionButtons[0].setIconPath("resources/icons/functionicons/svg/lightsOff.svg"); 222 functionButtons[0].setSelectedIconPath("resources/icons/functionicons/svg/lightsOn.svg"); 223 } catch (Exception e) { 224 log.debug("Exception loading svg icon : {}", e.getMessage()); 225 } finally { 226 if ((functionButtons[0].getIcon() == null) || (functionButtons[0].getSelectedIcon() == null)) { 227 log.debug("Issue loading svg icon, reverting to png"); 228 functionButtons[0].setIconPath("resources/icons/functionicons/transparent_background/lights_off.png"); 229 functionButtons[0].setSelectedIconPath("resources/icons/functionicons/transparent_background/lights_on.png"); 230 } 231 } 232 } 233 234 /** 235 * Apply preferences 236 * + global throttles preferences 237 * + this throttle settings if any 238 */ 239 public final void applyPreferences() { 240 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 241 RosterEntry re = null; 242 if (mThrottle != null && addressPanel != null) { 243 re = addressPanel.getRosterEntry(); 244 } 245 for (int i = 0; i < functionButtons.length; i++) { 246 if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 247 setUpDefaultLightFunctionButton(); 248 } else { 249 functionButtons[i].setIconPath(null); 250 functionButtons[i].setSelectedIconPath(null); 251 } 252 if (re != null) { 253 if (re.getFunctionLabel(i) != null) { 254 functionButtons[i].setDisplay(re.getFunctionVisible(i)); 255 functionButtons[i].setButtonLabel(re.getFunctionLabel(i)); 256 if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 257 functionButtons[i].setIconPath(re.getFunctionImage(i)); 258 functionButtons[i].setSelectedIconPath(re.getFunctionSelectedImage(i)); 259 } else { 260 functionButtons[i].setIconPath(null); 261 functionButtons[i].setSelectedIconPath(null); 262 } 263 functionButtons[i].setIsLockable(re.getFunctionLockable(i)); 264 } else { 265 functionButtons[i].setDisplay( ! (preferences.isUsingExThrottle() && preferences.isHidingUndefinedFuncButt()) ); 266 } 267 } 268 functionButtons[i].updateLnF(); 269 } 270 } 271 272 /** 273 * Rebuild function buttons 274 * 275 */ 276 private void rebuildFnButons(int n) { 277 mainPanel.removeAll(); 278 functionButtons = new FunctionButton[n]; 279 for (int i = 0; i < functionButtons.length; i++) { 280 functionButtons[i] = new FunctionButton(); 281 resetFnButton(functionButtons[i],i); 282 mainPanel.add(functionButtons[i]); 283 // Copy mouse and keyboard controls to new components 284 for (MouseWheelListener mwl:getMouseWheelListeners()) { 285 functionButtons[i].addMouseWheelListener(mwl); 286 } 287 } 288 } 289 290 /** 291 * Update function buttons 292 * - from selected throttle setting and state 293 * - from roster entry if any 294 */ 295 private void updateFnButtons() { 296 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 297 if (mThrottle != null && addressPanel != null) { 298 RosterEntry rosterEntry = addressPanel.getRosterEntry(); 299 if (rosterEntry != null) { 300 fnBtnUpdatedFromRoster = true; 301 log.debug("RosterEntry found: {}", rosterEntry.getId()); 302 } 303 for (int i = 0; i < functionButtons.length; i++) { 304 // update from selected throttle setting 305 functionButtons[i].setEnabled(true); 306 functionButtons[i].setIdentity(i); // full reset of function 307 functionButtons[i].setThrottle(mThrottle); 308 functionButtons[i].setState(mThrottle.getFunction(i)); // reset button state 309 functionButtons[i].setIsLockable(!mThrottle.getFunctionMomentary(i)); 310 functionButtons[i].setDropFolder(FileUtil.getUserResourcePath()); 311 // update from roster entry if any 312 if (rosterEntry != null) { 313 functionButtons[i].setDropFolder(Roster.getDefault().getRosterFilesLocation()); 314 boolean needUpdate = false; 315 String imgButtonSize = rosterEntry.getAttribute("function"+i+"_ThrottleImageButtonSize"); 316 if (imgButtonSize != null) { 317 try { 318 functionButtons[i].setButtonImageSize(Integer.parseInt(imgButtonSize)); 319 needUpdate = true; 320 } catch (NumberFormatException e) { 321 log.debug("setFnButtons(): can't parse button image size attribute "); 322 } 323 } 324 String text = rosterEntry.getFunctionLabel(i); 325 if (text != null) { 326 functionButtons[i].setDisplay(rosterEntry.getFunctionVisible(i)); 327 functionButtons[i].setButtonLabel(text); 328 if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 329 functionButtons[i].setIconPath(rosterEntry.getFunctionImage(i)); 330 functionButtons[i].setSelectedIconPath(rosterEntry.getFunctionSelectedImage(i)); 331 } else { 332 functionButtons[i].setIconPath(null); 333 functionButtons[i].setSelectedIconPath(null); 334 } 335 functionButtons[i].setIsLockable(rosterEntry.getFunctionLockable(i)); 336 needUpdate = true; 337 } else if (preferences.isUsingExThrottle() 338 && preferences.isHidingUndefinedFuncButt()) { 339 functionButtons[i].setDisplay(false); 340 needUpdate = true; 341 } 342 String fontSize = rosterEntry.getAttribute("function"+i+"_ThrottleFontSize"); 343 if (fontSize != null) { 344 try { 345 functionButtons[i].setFont(new Font("Monospaced", Font.PLAIN, Integer.parseInt(fontSize))); 346 needUpdate = true; 347 } catch (NumberFormatException e) { 348 log.debug("setFnButtons(): can't parse font size attribute "); 349 } 350 } 351 if (needUpdate) { 352 functionButtons[i].updateLnF(); 353 } 354 } 355 } 356 } 357 } 358 359 360 private void resetFnButton(FunctionButton fb, int i) { 361 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 362 fb.setThrottle(mThrottle); 363 if (mThrottle!=null) { 364 fb.setState(mThrottle.getFunction(i)); // reset button state 365 fb.setIsLockable(!mThrottle.getFunctionMomentary(i)); 366 } 367 fb.setIdentity(i); 368 fb.addFunctionListener(this); 369 fb.setButtonLabel( i<3 ? Bundle.getMessage(Throttle.getFunctionString(i)) : Throttle.getFunctionString(i) ); 370 fb.setDisplay(true); 371 if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) { 372 setUpDefaultLightFunctionButton(); 373 } else { 374 fb.setIconPath(null); 375 fb.setSelectedIconPath(null); 376 } 377 fb.updateLnF(); 378 379 // always display f0, F1 and F2 380 if (i < 3) { 381 fb.setVisible(true); 382 } 383 } 384 385 /** 386 * Reset function buttons : 387 * - rebuild function buttons 388 * - reset their properties to default 389 * - update according to throttle and roster (if any) 390 * 391 */ 392 public void resetFnButtons() { 393 // rebuild function buttons 394 if (mThrottle == null) { 395 rebuildFnButons(DEFAULT_FUNCTION_BUTTONS); 396 } else { 397 rebuildFnButons(mThrottle.getFunctions().length); 398 } 399 // reset their properties to defaults 400 for (int i = 0; i < functionButtons.length; i++) { 401 resetFnButton(functionButtons[i],i); 402 } 403 // update according to throttle and roster (if any) 404 updateFnButtons(); 405 repaint(); 406 } 407 408 /** 409 * Update the state of this panel if any of the functions change. 410 * {@inheritDoc} 411 */ 412 @Override 413 public void propertyChange(java.beans.PropertyChangeEvent e) { 414 if (mThrottle!=null){ 415 for (int i = 0; i < mThrottle.getFunctions().length; i++) { 416 if (e.getPropertyName().equals(Throttle.getFunctionString(i))) { 417 setButtonByFuncNumber(i,false,(Boolean) e.getNewValue()); 418 } else if (e.getPropertyName().equals(Throttle.getFunctionMomentaryString(i))) { 419 setButtonByFuncNumber(i,true,!(Boolean) e.getNewValue()); 420 } 421 } 422 } 423 } 424 425 private void setButtonByFuncNumber(int function, boolean lockable, boolean newVal){ 426 for (FunctionButton button : functionButtons) { 427 if (button.getIdentity() == function) { 428 if (lockable) { 429 button.setIsLockable(newVal); 430 } else { 431 button.setState(newVal); 432 } 433 } 434 } 435 } 436 437 /** 438 * Collect the prefs of this object into XML Element. 439 * <ul> 440 * <li> Window prefs 441 * <li> Each button has id, text, lock state. 442 * </ul> 443 * 444 * @return the XML of this object. 445 */ 446 public Element getXml() { 447 Element me = new Element("FunctionPanel"); // NOI18N 448 java.util.ArrayList<Element> children = new java.util.ArrayList<>(1 + functionButtons.length); 449 children.add(WindowPreferences.getPreferences(this)); 450 for (FunctionButton functionButton : functionButtons) { 451 children.add(functionButton.getXml()); 452 } 453 me.setContent(children); 454 return me; 455 } 456 457 /** 458 * Set the preferences based on the XML Element. 459 * <ul> 460 * <li> Window prefs 461 * <li> Each button has id, text, lock state. 462 * </ul> 463 * 464 * @param e The Element for this object. 465 */ 466 public void setXml(Element e) { 467 Element window = e.getChild("window"); 468 WindowPreferences.setPreferences(this, window); 469 470 if (! fnBtnUpdatedFromRoster) { 471 java.util.List<Element> buttonElements = e.getChildren("FunctionButton"); 472 473 if (buttonElements != null && buttonElements.size() > 0) { 474 // just in case 475 rebuildFnButons( buttonElements.size() ); 476 int i = 0; 477 for (Element buttonElement : buttonElements) { 478 functionButtons[i++].setXml(buttonElement); 479 } 480 } 481 } 482 } 483 484 /** 485 * Get notification that a throttle has been found as we requested. 486 * 487 * @param t An instantiation of the DccThrottle with the address requested. 488 */ 489 @Override 490 public void notifyAddressThrottleFound(DccThrottle t) { 491 log.debug("Throttle found for {}",t); 492 if (mThrottle != null) { 493 mThrottle.removePropertyChangeListener(this); 494 } 495 mThrottle = t; 496 mThrottle.addPropertyChangeListener(this); 497 int numFns = mThrottle.getFunctions().length; 498 if (addressPanel != null && addressPanel.getRosterEntry() != null) { 499 // +1 because we want the _number_ of functions, and we have to count F0 500 numFns = Math.min(numFns, addressPanel.getRosterEntry().getMaxFnNumAsInt()+1); 501 } 502 log.debug("notifyAddressThrottleFound number of functions {}", numFns); 503 resizeFnButtonsArray(numFns); 504 updateFnButtons(); 505 setEnabled(true); 506 } 507 508 private void adressReleased() { 509 if (mThrottle != null) { 510 mThrottle.removePropertyChangeListener(this); 511 } 512 mThrottle = null; 513 fnBtnUpdatedFromRoster = false; 514 resetFnButtons(); 515 setEnabled(false); 516 } 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override 522 public void notifyAddressReleased(LocoAddress la) { 523 log.debug("Throttle released"); 524 adressReleased(); 525 } 526 527 /** 528 * Ignored. 529 * {@inheritDoc} 530 */ 531 @Override 532 public void notifyAddressChosen(LocoAddress l) { 533 } 534 535 /** 536 * Ignored. 537 * {@inheritDoc} 538 */ 539 @Override 540 public void notifyConsistAddressChosen(LocoAddress l) { 541 } 542 543 /** 544 * Ignored. 545 * {@inheritDoc} 546 */ 547 @Override 548 public void notifyConsistAddressReleased(LocoAddress la) { 549 log.debug("Consist throttle released"); 550 adressReleased(); 551 } 552 553 /** 554 * Ignored. 555 * {@inheritDoc} 556 */ 557 @Override 558 public void notifyConsistAddressThrottleFound(DccThrottle t) { 559 log.debug("Consist throttle found"); 560 if (mThrottle == null) { 561 notifyAddressThrottleFound(t); 562 } 563 } 564 565 private final static Logger log = LoggerFactory.getLogger(FunctionPanel.class); 566}