001package jmri.jmrit.throttle.panels; 002 003import java.awt.*; 004import java.awt.event.ActionEvent; 005import java.io.File; 006import java.util.ArrayList; 007 008import javax.annotation.CheckForNull; 009import javax.annotation.Nonnull; 010import javax.swing.*; 011 012import jmri.InstanceManager; 013import jmri.Throttle; 014import jmri.jmrit.throttle.interfaces.FunctionListener; 015import jmri.jmrit.throttle.preferences.ThrottlesPreferences; 016import jmri.util.FileUtil; 017import jmri.util.swing.JmriMouseAdapter; 018import jmri.util.swing.JmriMouseEvent; 019import jmri.util.swing.JmriMouseListener; 020import jmri.util.swing.ResizableImagePanel; 021import jmri.util.com.sun.ToggleOrPressButtonModel; 022import jmri.util.gui.GuiLafPreferencesManager; 023 024import org.jdom2.Element; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028/** 029 * A JButton to activate functions on the decoder. FunctionButtons have a 030 right-click popupMenu menu with several configuration options: 031 <ul> 032 * <li> Set the text 033 * <li> Set the locking state 034 * <li> Set visibility 035 * <li> Set Font 036 * <li> Set function number identity 037 * </ul> 038 * 039 * <hr> 040 * This file is part of JMRI. 041 * <p> 042 * JMRI is free software; you can redistribute it and/or modify it under the 043 * terms of version 2 of the GNU General Public License as published by the Free 044 * Software Foundation. See the "COPYING" file for a copy of this license. 045 * <p> 046 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 047 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 048 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 049 * 050 * @author Glen Oberhauser 051 * @author Bob Jacobsen Copyright 2008 052 * @author Lionel Jeanson 2021 053 */ 054public class FunctionButton extends JToggleButton { 055 056 private final ArrayList<FunctionListener> listeners; 057 private int identity; // F0, F1, etc 058 private boolean isDisplayed = true; 059 private boolean dirty = false; 060 private boolean isImageOK = false; 061 private boolean isSelectedImageOK = false; 062 private String buttonLabel; 063 private JPopupMenu popupMenu; 064 private FunctionButtonPropertyEditor editor ; 065 private String iconPath; 066 private String selectedIconPath; 067 private String dropFolder; 068 private ToggleOrPressButtonModel _model; 069 private Throttle _throttle; 070 private int img_size = DEFAULT_IMG_SIZE; 071 private static final int BUT_HGHT = 24; 072 private static final int BUT_MAX_WDTH = 256; 073 private static final int BUT_MIN_WDTH = 100; 074 075 public static final int DEFAULT_IMG_SIZE = 48; 076 077 public void destroy() { 078 if (editor != null) { 079 editor.destroy(); 080 } 081 _throttle = null; 082 } 083 084 /** 085 * Get Button Height. 086 * @return height. 087 */ 088 public static int getButtonHeight() { 089 return BUT_HGHT; 090 } 091 092 /** 093 * Get the Button Width. 094 * @return width. 095 */ 096 public static int getButtonWidth() { 097 return BUT_MIN_WDTH; 098 } 099 100 /** 101 * Get the Image Button Width. 102 * @return width. 103 */ 104 public int getButtonImageSize() { 105 return img_size; 106 } 107 108 /** 109 * Set the Image Button Hieght and Width. 110 * @param is the image size (sqaure image size = width = height) 111 */ 112 public void setButtonImageSize(int is) { 113 img_size = is; 114 } 115 116 /** 117 * Construct the FunctionButton. 118 * 119 * @param withPopupMenu popup menu on function button available if true 120 */ 121 public FunctionButton(boolean withPopupMenu) { 122 super(); 123 listeners = new ArrayList<>(); 124 initGUI(withPopupMenu); 125 } 126 127 public FunctionButton() { 128 this(true); 129 } 130 131 private void initGUI(boolean withPopupMenu){ 132 _model = new ToggleOrPressButtonModel(this, true); 133 setModel(_model); 134 //Add listener to components that can bring up popupMenu menus. 135 if (withPopupMenu) { 136 addMouseListener(JmriMouseListener.adapt(new PopupListener())); 137 } 138 setFont(new Font("Monospaced", Font.PLAIN, InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize())); 139 setMargin(new Insets(2, 2, 2, 2)); 140 setRolloverEnabled(false); 141 updateLnF(); 142 } 143 144 /** 145 * Set the function number this button will operate. 146 * 147 * @param id An integer, minimum 0. 148 */ 149 public void setIdentity(int id) { 150 this.identity = id; 151 } 152 153 /** 154 * Get the function number this button operates. 155 * 156 * @return An integer, minimum 0. 157 */ 158 public int getIdentity() { 159 return identity; 160 } 161 162 /** 163 * Set the state of the function button. 164 * Does not send update to layout, just updates button status. 165 * <p> 166 * To update AND send to layout use setSelected(boolean). 167 * 168 * @param isOn True if the function should be active. 169 */ 170 public void setState(boolean isOn) { 171 super.setSelected(isOn); 172 _model.updateSelected(isOn); 173 } 174 175 /** 176 * Get the state of the function. 177 * 178 * @return true if the function is active. 179 */ 180 public boolean getState() { 181 return isSelected(); 182 } 183 184 /** 185 * Set the locking state of the button. 186 * <p> 187 * Changes in this parameter are only be sent to the 188 * listeners if the dirty bit is set. 189 * 190 * @param isLockable True if the a clicking and releasing the button changes 191 * the function state. False if the state is changed back 192 * when the button is released 193 */ 194 public void setIsLockable(boolean isLockable) { 195 _model.setLockable(isLockable); 196 if (isDirty()) { 197 for (int i = 0; i < listeners.size(); i++) { 198 listeners.get(i).notifyFunctionLockableChanged(identity, isLockable); 199 } 200 } 201 } 202 203 /** 204 * Get the locking state of the function. 205 * 206 * @return True if the a clicking and releasing the button changes the 207 * function state. False if the state is changed back when 208 * button is released 209 */ 210 public boolean getIsLockable() { 211 return _model.getLockable(); 212 } 213 214 /** 215 * Set the display state of the button. 216 * 217 * @param displayed True if the button exists False if the button has been 218 * removed by the user 219 */ 220 public void setDisplay(boolean displayed) { 221 this.isDisplayed = displayed; 222 } 223 224 /** 225 * Get the display state of the button. 226 * 227 * @return True if the button exists False if the button has been removed by 228 * the user 229 */ 230 public boolean getDisplay() { 231 return isDisplayed; 232 } 233 234 /** 235 * Set Function Button Dirty. 236 * 237 * @param dirty True when button has been modified by user, else false. 238 */ 239 public void setDirty(boolean dirty) { 240 this.dirty = dirty; 241 } 242 243 /** 244 * Get if Button is Dirty. 245 * @return true when function button has been modified by user. 246 */ 247 public boolean isDirty() { 248 return dirty; 249 } 250 251 /** 252 * Get the Button Label. 253 * @return Button Label text. 254 */ 255 public String getButtonLabel() { 256 return buttonLabel; 257 } 258 259 /** 260 * Set the Button Label. 261 * @param label Label Text. 262 */ 263 public void setButtonLabel(String label) { 264 buttonLabel = label; 265 } 266 267 /** 268 * Set Button Text. 269 * {@inheritDoc} 270 */ 271 @Override 272 public void setText(String s) { 273 if (s != null) { 274 buttonLabel = s; 275 if (isImageOK) { 276 setToolTipText(buttonLabel); 277 super.setText(null); 278 } else { 279 super.setText(s); 280 } 281 return; 282 } 283 super.setText(null); 284 if (buttonLabel != null) { 285 setToolTipText(buttonLabel); 286 } 287 } 288 289 /** 290 * Update Button Look and Feel : 291 * Hide/show it if necessary 292 * Decide if it should show the label or an image with text as tooltip. 293 * Button UI updated according to above result. 294 */ 295 public void updateLnF() { 296 setFocusable(false); // for throttle window keyboard controls 297 setVisible(isDisplayed); 298 setBorderPainted(!isImageOK()); 299 setContentAreaFilled(!isImageOK()); 300 if (isImageOK()) { // adjust button for image 301 setText(null); 302 setMinimumSize(new Dimension(img_size, img_size)); 303 setMaximumSize(new Dimension(img_size, img_size)); 304 setPreferredSize(new Dimension(img_size, img_size)); 305 } 306 else { // adjust button for text 307 setText(getButtonLabel()); 308 setMinimumSize(new Dimension(FunctionButton.BUT_MIN_WDTH, FunctionButton.BUT_HGHT)); 309 setMaximumSize(new Dimension(FunctionButton.BUT_MAX_WDTH, FunctionButton.BUT_HGHT)); 310 if (getButtonLabel() != null) { 311 int butWidth = getFontMetrics(getFont()).stringWidth(getButtonLabel()) + 64; // pad out the width a bit 312 butWidth = Math.min(butWidth, FunctionButton.BUT_MAX_WDTH ); 313 butWidth = Math.max(butWidth, FunctionButton.BUT_MIN_WDTH ); 314 setPreferredSize(new Dimension( butWidth, FunctionButton.BUT_HGHT)); 315 } else { 316 setPreferredSize(new Dimension(BUT_MIN_WDTH, BUT_HGHT)); 317 } 318 } 319 } 320 321 /** 322 * Change the state of the function. 323 * Sets internal state, setSelected, and sends to listeners. 324 * <p> 325 * To update this button WITHOUT sending to layout, use setState. 326 * 327 * @param newState true = Is Function on, False = Is Function off. 328 */ 329 @Override 330 public void setSelected(boolean newState){ 331 log.debug("function selected {}", newState); 332 super.setSelected(newState); 333 for (int i = 0; i < listeners.size(); i++) { 334 listeners.get(i).notifyFunctionStateChanged(identity, newState); 335 } 336 } 337 338 /** 339 * Add a listener to this button, probably some sort of keypad panel. 340 * 341 * @param l The FunctionListener that wants notifications via the 342 * FunctionListener.notifyFunctionStateChanged. 343 */ 344 public void addFunctionListener(FunctionListener l) { 345 if (!listeners.contains(l)) { 346 listeners.add(l); 347 } 348 } 349 350 /** 351 * Remove a listener from this button. 352 * 353 * @param l The FunctionListener to be removed 354 */ 355 public void removeFunctionListener(FunctionListener l) { 356 listeners.remove(l); 357 } 358 359 /** 360 * Set the folder where droped images in function button property panel will be stored 361 * 362 * @param df the folder path 363 */ 364 void setDropFolder(String df) { 365 dropFolder = df; 366 } 367 368 /** 369 * A PopupListener to handle mouse clicks and releases. 370 * Handles the popupMenu menu. 371 */ 372 private class PopupListener extends JmriMouseAdapter { 373 374 /** 375 * If the event is the popupMenu trigger, which is dependent on the platform, present the popupMenu menu. 376 * @param e The MouseEvent causing the action. 377 */ 378 @Override 379 public void mouseClicked(JmriMouseEvent e) { 380 checkTrigger(e); 381 } 382 383 /** 384 * If the event is the popupMenu trigger, which is dependent on the platform, present the popupMenu menu. 385 * @param e The MouseEvent causing the action. 386 */ 387 @Override 388 public void mousePressed(JmriMouseEvent e) { 389 checkTrigger( e); 390 } 391 392 /** 393 * If the event is the popupMenu trigger, which is dependent on the platform, present the popupMenu menu. 394 * @param e The MouseEvent causing the action. 395 */ 396 @Override 397 public void mouseReleased(JmriMouseEvent e) { 398 checkTrigger( e); 399 } 400 401 private void checkTrigger( JmriMouseEvent e) { 402 if (e.isPopupTrigger() && e.getComponent().isEnabled() ) { 403 initPopupMenu(); 404 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 405 } 406 } 407 } 408 409 private void initPopupMenu() { 410 if (popupMenu == null) { 411 JMenuItem propertiesItem = new JMenuItem(Bundle.getMessage("MenuItemProperties")); 412 propertiesItem.addActionListener((ActionEvent e) -> { 413 if (editor == null) { 414 editor = new FunctionButtonPropertyEditor(this); 415 } 416 editor.resetProperties(); 417 editor.setLocation(MouseInfo.getPointerInfo().getLocation()); 418 editor.setVisible(true); 419 editor.setDropFolder(dropFolder); 420 }); 421 popupMenu = new JPopupMenu(); 422 popupMenu.add(propertiesItem); 423 } 424 } 425 426 /** 427 * Collect the prefs of this object into XML Element. 428 * <ul> 429 * <li> identity 430 * <li> text 431 * <li> isLockable 432 * </ul> 433 * 434 * @return the XML of this object. 435 */ 436 public Element getXml() { 437 Element me = new Element("FunctionButton"); // NOI18N 438 me.setAttribute("id", String.valueOf(this.getIdentity())); 439 me.setAttribute("text", this.getButtonLabel()); 440 me.setAttribute("isLockable", String.valueOf(this.getIsLockable())); 441 me.setAttribute("isVisible", String.valueOf(this.getDisplay())); 442 if (getFont().getSize() != InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) { 443 me.setAttribute("fontSize", String.valueOf(this.getFont().getSize())); 444 } 445 me.setAttribute("buttonImageSize", String.valueOf(this.getButtonImageSize())); 446 if (this.getIconPath().startsWith(FileUtil.getUserResourcePath())) { 447 me.setAttribute("iconPath", this.getIconPath().substring(FileUtil.getUserResourcePath().length())); 448 } else { 449 me.setAttribute("iconPath", this.getIconPath()); 450 } 451 if (this.getSelectedIconPath().startsWith(FileUtil.getUserResourcePath())) { 452 me.setAttribute("selectedIconPath", this.getSelectedIconPath().substring(FileUtil.getUserResourcePath().length())); 453 } else { 454 me.setAttribute("selectedIconPath", this.getSelectedIconPath()); 455 } 456 return me; 457 } 458 459 /** 460 * Check if File exists. 461 * @param name File name 462 * @return true if exists, else false. 463 */ 464 private boolean checkFile(String name) { 465 File fp = new File(name); 466 return fp.exists(); 467 } 468 469 /** 470 * Set the preferences based on the XML Element. 471 * <ul> 472 * <li> identity 473 * <li> text 474 * <li> isLockable 475 * </ul> 476 * 477 * @param e The Element for this object. 478 */ 479 public void setXml(Element e) { 480 final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class); 481 try { 482 this.setIdentity(e.getAttribute("id").getIntValue()); 483 this.setText(e.getAttribute("text").getValue()); 484 this.setIsLockable(e.getAttribute("isLockable").getBooleanValue()); 485 this.setDisplay(e.getAttribute("isVisible").getBooleanValue()); 486 if (e.getAttribute("fontSize") != null) { 487 this.setFont(new Font("Monospaced", Font.PLAIN, e.getAttribute("fontSize").getIntValue())); 488 } else { 489 this.setFont(new Font("Monospaced", Font.PLAIN, InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize())); 490 } 491 this.setButtonImageSize( (e.getAttribute("buttonImageSize")!=null)?e.getAttribute("buttonImageSize").getIntValue():DEFAULT_IMG_SIZE); 492 if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && (e.getAttribute("iconPath") != null) && (e.getAttribute("iconPath").getValue().length() > 0)) { 493 if (checkFile(FileUtil.getUserResourcePath() + e.getAttribute("iconPath").getValue())) { 494 this.setIconPath(FileUtil.getUserResourcePath() + e.getAttribute("iconPath").getValue()); 495 } else { 496 this.setIconPath(e.getAttribute("iconPath").getValue()); 497 } 498 } else { 499 this.setIconPath(null); 500 } 501 if ((e.getAttribute("selectedIconPath") != null) && (e.getAttribute("selectedIconPath").getValue().length() > 0)) { 502 if (checkFile(FileUtil.getUserResourcePath() + e.getAttribute("selectedIconPath").getValue())) { 503 this.setSelectedIconPath(FileUtil.getUserResourcePath() + e.getAttribute("selectedIconPath").getValue()); 504 } else { 505 this.setSelectedIconPath(e.getAttribute("selectedIconPath").getValue()); 506 } 507 } else { 508 this.setSelectedIconPath(null); 509 } 510 updateLnF(); 511 } catch (org.jdom2.DataConversionException ex) { 512 log.error("DataConverstionException in setXml", ex); 513 } 514 } 515 516 /** 517 * Set the Icon Path, NON selected. 518 * <p> 519 * Checks image and sets isImageOK flag. 520 * @param fnImg icon path. 521 */ 522 public void setIconPath(String fnImg) { 523 iconPath = fnImg; 524 ResizableImagePanel fnImage = new ResizableImagePanel(); 525 fnImage.setBackground(new Color(0, 0, 0, 0)); 526 fnImage.setRespectAspectRatio(true); 527 fnImage.setSize(new Dimension(img_size,img_size)); 528 fnImage.setImagePath(fnImg); 529 if (fnImage.getScaledImage() != null) { 530 setIcon(new ImageIcon(fnImage.getScaledImage())); 531 isImageOK = true; 532 } else { 533 setIcon(null); 534 isImageOK = false; 535 } 536 } 537 538 /** 539 * Get the Icon Path, NON selected. 540 * @return Icon Path, else empty string if null. 541 */ 542 @Nonnull 543 public String getIconPath() { 544 if (iconPath == null) { 545 return ""; 546 } 547 return iconPath; 548 } 549 550 /** 551 * Set the Selected Icon Path. 552 * <p> 553 * Checks image and sets isSelectedImageOK flag. 554 * @param fnImg selected icon path. 555 */ 556 public void setSelectedIconPath(String fnImg) { 557 selectedIconPath = fnImg; 558 ResizableImagePanel fnSelectedImage = new ResizableImagePanel(); 559 fnSelectedImage.setBackground(new Color(0, 0, 0, 0)); 560 fnSelectedImage.setRespectAspectRatio(true); 561 fnSelectedImage.setSize(new Dimension(img_size, img_size)); 562 fnSelectedImage.setImagePath(fnImg); 563 if (fnSelectedImage.getScaledImage() != null) { 564 ImageIcon icon = new ImageIcon(fnSelectedImage.getScaledImage()); 565 setSelectedIcon(icon); 566 setPressedIcon(icon); 567 isSelectedImageOK = true; 568 } else { 569 setSelectedIcon(null); 570 setPressedIcon(null); 571 isSelectedImageOK = false; 572 } 573 } 574 575 /** 576 * Get the Selected Icon Path. 577 * @return selected Icon Path, else empty string if null. 578 */ 579 @Nonnull 580 public String getSelectedIconPath() { 581 if (selectedIconPath == null) { 582 return ""; 583 } 584 return selectedIconPath; 585 } 586 587 /** 588 * Get if isImageOK. 589 * @return true if isImageOK. 590 */ 591 public boolean isImageOK() { 592 return isImageOK; 593 } 594 595 /** 596 * Get if isSelectedImageOK. 597 * @return true if isSelectedImageOK. 598 */ 599 public boolean isSelectedImageOK() { 600 return isSelectedImageOK; 601 } 602 603 /** 604 * Set Throttle. 605 * @param throttle the throttle that this button is associated with. 606 */ 607 protected void setThrottle( Throttle throttle) { 608 _throttle = throttle; 609 } 610 611 /** 612 * Get Throttle for this button. 613 * @return throttle associated with this button. May be null if no throttle currently associated. 614 */ 615 @CheckForNull 616 protected Throttle getThrottle() { 617 return _throttle; 618 } 619 620 private static final Logger log = LoggerFactory.getLogger(FunctionButton.class); 621 622}