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