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