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