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