001package jmri.jmrit.display.palette; 002 003import java.awt.Color; 004import java.awt.Dimension; 005import java.awt.FontMetrics; 006import java.awt.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.awt.event.ActionListener; 009import java.util.HashMap; 010import java.util.Map.Entry; 011 012import javax.annotation.Nonnull; 013import javax.swing.BorderFactory; 014import javax.swing.Box; 015import javax.swing.BoxLayout; 016import javax.swing.JButton; 017import javax.swing.JLabel; 018import javax.swing.JPanel; 019import javax.swing.JTextField; 020 021import jmri.jmrit.catalog.CatalogPanel; 022import jmri.jmrit.catalog.NamedIcon; 023import jmri.jmrit.display.DisplayFrame; 024import jmri.jmrit.display.PreviewPanel; 025import jmri.jmrit.display.controlPanelEditor.PortalIcon; 026import jmri.util.swing.ImagePanel; 027import jmri.util.swing.JmriJOptionPane; 028 029/** 030 * JPanels for the various item types that can be added to a Panel - e.g. Sensors, 031 * Turnouts, etc. 032 * 033 * Devices such as these have sets of icons to display their various states. 034 * Such sets are called a "family" in the code. These devices then may have sets 035 * of families to provide the user with a choice of the icon set to use for a 036 * particular device. 037 * These sets/families are defined in an xml file stored as xml/defaultPanelIcons.xml 038 * including the icon file paths, to be loaded by an iterator. 039 * The subclass FamilyItemPanel.java and its subclasses handles these devices. 040 * 041 * Other devices, e.g. Backgrounds or Memory, may use only one or no icon to 042 * display. The subclass IconItemPanel.java and its subclasses handles these 043 * devices. 044 * @see jmri.jmrit.display.DisplayFrame for class diagram for the palette package. 045 * 046 * @author Pete Cressman Copyright (c) 2010, 2020 047 * @author Egbert Broerse Copyright 2017, 2021 048 */ 049public abstract class ItemPanel extends JPanel { 050 051 protected DisplayFrame _frame; 052 protected String _itemType; 053 protected boolean _initialized = false; // has init() been run 054 protected boolean _update = false; // editing existing icon, do not allow icon dragging. Set in init() 055 protected boolean _suppressDragging; 056 protected boolean _askOnce = false; 057 protected JTextField _linkName = new JTextField(30); 058 protected PreviewPanel _previewPanel; // contains _iconPanel and optionally _dragIconPanel when used to create a panel object 059 protected HashMap<String, NamedIcon> _currentIconMap; 060 protected ImagePanel _iconPanel; // a panel on _iconFamilyPanel - all icons in family, shown upon [Show Icons] 061 protected JPanel _iconFamilyPanel; // Holds _previewPanel, _familyButtonPanel. 062 protected JPanel _bottomPanel; // contains function buttons for panel 063 protected ActionListener _doneAction; // update done action return 064 protected boolean _wasEmpty; 065 protected JPanel _instructions; 066 067 /* 068 * ****** Default family icon names ******* 069 * 070 * NOTE: Names supplied must be available as properties keys and also match the 071 * element names defined in xml/defaultPanelIcons.xml 072 */ 073 static final String[] TURNOUT = {"TurnoutStateClosed", "TurnoutStateThrown", 074 "BeanStateInconsistent", "BeanStateUnknown"}; 075 static final String[] SENSOR = {"SensorStateActive", "SensorStateInactive", 076 "BeanStateInconsistent", "BeanStateUnknown"}; 077 static final String[] SIGNALHEAD = {"SignalHeadStateRed", "SignalHeadStateYellow", 078 "SignalHeadStateGreen", "SignalHeadStateDark", 079 "SignalHeadStateHeld", "SignalHeadStateLunar", 080 "SignalHeadStateFlashingRed", "SignalHeadStateFlashingYellow", 081 "SignalHeadStateFlashingGreen", "SignalHeadStateFlashingLunar"}; 082 static final String[] LIGHT = {"StateOff", "StateOn", 083 "BeanStateInconsistent", "BeanStateUnknown"}; 084 static final String[] MULTISENSOR = {"SensorStateInactive", "BeanStateInconsistent", 085 "BeanStateUnknown", "first", "second", "third"}; 086 // SIGNALMAST family is empty is signal system 087 static final String[] RPSREPORTER = {"active", "error"}; 088 final static String[] INDICATOR_TRACK = {"ClearTrack", "OccupiedTrack", "PositionTrack", 089 "AllocatedTrack", "DontUseTrack", "ErrorTrack"}; 090 static final String[] PORTAL = {PortalIcon.HIDDEN, PortalIcon.VISIBLE, PortalIcon.PATH, 091 PortalIcon.TO_ARROW, PortalIcon.FROM_ARROW}; 092 093 protected static HashMap<String, String[]> STATE_MAP = new HashMap<>(); 094 static { 095 STATE_MAP.put("Turnout", TURNOUT); 096 STATE_MAP.put("Sensor", SENSOR); 097 STATE_MAP.put("SignalHead", SIGNALHEAD); 098 STATE_MAP.put("Light", LIGHT); 099 STATE_MAP.put("MultiSensor", MULTISENSOR); 100 STATE_MAP.put("RPSReporter", RPSREPORTER); 101 STATE_MAP.put("IndicatorTrack", INDICATOR_TRACK); 102 STATE_MAP.put("IndicatorTO", INDICATOR_TRACK); 103 STATE_MAP.put("Portal", PORTAL); 104 } 105 106 protected static HashMap<String, String> NAME_MAP = new HashMap<>(); 107 static { 108 NAME_MAP.put("Turnout", "BeanNameTurnout"); 109 NAME_MAP.put("Sensor", "BeanNameSensor"); 110 NAME_MAP.put("SignalHead", "BeanNameSignalHead"); 111 NAME_MAP.put("Light", "BeanNameLight"); 112 NAME_MAP.put("SignalMast", "BeanNameSignalMast"); 113 NAME_MAP.put("MultiSensor", "MultiSensor"); 114 NAME_MAP.put("Memory", "BeanNameMemory"); 115 NAME_MAP.put("Reporter", "BeanNameReporter"); 116 NAME_MAP.put("RPSReporter", "RPSreporter"); 117 NAME_MAP.put("IndicatorTrack", "IndicatorTrack"); 118 NAME_MAP.put("IndicatorTO", "IndicatorTO"); 119 NAME_MAP.put("Portal", "BeanNamePortal"); 120 NAME_MAP.put("Icon", "Icon"); 121 NAME_MAP.put("Background", "Background"); 122 NAME_MAP.put("Text", "Text"); 123 NAME_MAP.put("FastClock", "FastClock"); 124 } 125 126 /** 127 * Constructor for all item types. 128 * 129 * @param parentFrame ItemPalette instance 130 * @param type identifier of the ItemPanel type 131 */ 132 public ItemPanel(DisplayFrame parentFrame, @Nonnull String type) { 133 _frame = parentFrame; 134 _itemType = type; 135 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 136 add(Box.createVerticalGlue()); 137 } 138 139 /** 140 * Initialize panel for selecting a new Control Panel item or for updating 141 * an existing item. Adds table if item is a bean. i.e. customizes for the 142 * item type. 143 * Called by enclosing TabbedPanel on change of displayed tab Pane. 144 */ 145 public void init() { 146 if (!_initialized) { 147 _update = false; 148 _suppressDragging = false; 149 initIconFamiliesPanel(); 150 _initialized = true; 151 } 152 } 153 154 @Nonnull 155 protected HashMap<String, NamedIcon> makeNewIconMap(String type) { 156 HashMap<String, NamedIcon> newMap = new HashMap<>(); 157 for (String name : STATE_MAP.get(type)) { 158 NamedIcon icon = new NamedIcon(ItemPalette.RED_X, ItemPalette.RED_X); 159 newMap.put(name, icon); 160 } 161 return newMap; 162 } 163 164 static protected void checkIconMap(String type, HashMap<String, NamedIcon> map) { 165 for (String name : STATE_MAP.get(type)) { 166 if (map.get(name) == null) { 167 NamedIcon icon = new NamedIcon(ItemPalette.RED_X, ItemPalette.RED_X); 168 // store RedX as default icon if icon not set 169 map.put(name, icon); 170 } 171 } 172 } 173 174 protected void previewColorChange() { 175 if (_previewPanel != null) { 176 _previewPanel.setBackgroundSelection(_frame.getPreviewBg()); 177 _previewPanel.invalidate(); 178 } 179 } 180 181 public void closeDialogs() { 182 } 183 184 /** 185 * Make a button panel that can populate an empty ItemPanel 186 * @param update edit icons on a panel 187 * @return the panel 188 */ 189 abstract protected JPanel makeSpecialBottomPanel(boolean update); 190 191 /** 192 * Make a button panel to populate editing an ItemPanel 193 * @return the panel 194 */ 195 abstract protected JPanel makeItemButtonPanel(); 196 197 /** 198 * Add [Update] button to _bottom1Panel. 199 * @param doneAction Action for button 200 * @return button with doneAction Action 201 */ 202 protected JButton makeUpdateButton(ActionListener doneAction) { 203 JButton updateButton = new JButton(Bundle.getMessage("updateButton")); // custom update label 204 updateButton.addActionListener(doneAction); 205 updateButton.setToolTipText(Bundle.getMessage("ToolTipPickFromTable")); 206 return updateButton; 207 } 208 209 210 protected void makeBottomPanel(boolean isEmpty) { 211 if (isEmpty) { 212 _bottomPanel = makeSpecialBottomPanel(_update); 213 } else { 214 _bottomPanel = makeItemButtonPanel(); 215 } 216 if (_doneAction != null) { 217 _bottomPanel.add(makeUpdateButton(_doneAction)); 218 } 219 _bottomPanel.invalidate(); 220 add(_bottomPanel); 221 } 222 223 /** 224 * Initialize or reset an ItemPanel. 225 */ 226 protected void initIconFamiliesPanel() { 227 if (_iconPanel == null) { 228 _iconPanel = new ImagePanel(); 229 _iconPanel.setBorder(BorderFactory.createLineBorder(Color.black)); 230 _iconPanel.setImage(_frame.getPreviewBackground()); 231 _iconPanel.setOpaque(false); 232 } 233 if (_iconFamilyPanel == null) { 234 _iconFamilyPanel = new JPanel(); 235 _iconFamilyPanel.setLayout(new BoxLayout(_iconFamilyPanel, BoxLayout.Y_AXIS)); 236 add(_iconFamilyPanel); 237 } 238 makeFamiliesPanel(); 239 if (log.isDebugEnabled()) { 240 log.debug("initIconFamiliesPanel done for {}, update= {}", _itemType, _update); 241 } 242 } 243 244 protected void makePreviewPanel(boolean hasMaps, ImagePanel dragIconPanel) { 245 if (_previewPanel == null) { 246 if (!_update && !_suppressDragging) { 247 _previewPanel = new PreviewPanel(_frame, _iconPanel, dragIconPanel, true); 248 _instructions = instructions(); 249 _previewPanel.add(_instructions, 0); 250 } else { 251 _previewPanel = new PreviewPanel(_frame, _iconPanel, null, false); 252 _previewPanel.setVisible(false); 253 } 254 _iconFamilyPanel.add(_previewPanel); 255 } 256 _previewPanel.setVisible(true); 257 _previewPanel.invalidate(); 258 } 259 260 /** 261 * Add the current set of icons to a Show Icons pane. Used in several 262 * ways by different ItemPanels. 263 * When dropIcon is true, call may be from an editing dialog and the 264 * caller may allow the icon to dropped upon (replaced) or be the 265 * source of dragging it - (e.g. IconItemPanel). When_showIconsButton 266 * pressed, dropIcon will be false. 267 * 268 * @see #hideIcons() 269 * @param iconMap family maps 270 * @param iconPanel panel to fill with icons 271 * @param dropIcon true for ability to drop new image on icon to change 272 * icon source 273 */ 274 protected void addIconsToPanel(HashMap<String, NamedIcon> iconMap, ImagePanel iconPanel, boolean dropIcon) { 275 if (iconMap == null) { 276 log.debug("_currentIconMap is null for type {}", _itemType); 277 return; 278 } 279 iconPanel.removeAll(); 280 281 GridBagLayout gridbag = new GridBagLayout(); 282 iconPanel.setLayout(gridbag); 283 284 int numCol = 4; 285 GridBagConstraints c = ItemPanel.itemGridBagConstraint(); 286 287 if (iconMap.isEmpty()) { 288 iconPanel.add(Box.createRigidArea(new Dimension(70,70))); 289 } 290 int cnt = 0; 291 for (String key : iconMap.keySet()) { 292 JPanel panel = makeIconDisplayPanel(key, iconMap, dropIcon); 293 294 iconPanel.add(panel, c); 295 if (c.gridx > numCol) { // start next row 296 c.gridy++; 297 c.gridx = 0; 298 } 299 c.gridx++; 300 cnt++; 301 gridbag.setConstraints(panel, c); 302 } 303 if (log.isDebugEnabled()) { 304 log.debug("addIconsToPanel adds {} icons (map size {}) to iconPanel for {}", cnt, iconMap.size(), _itemType); 305 } 306 iconPanel.invalidate(); 307 } 308 309 /** 310 * Utility for above method. Implementation returns a JPanel extension 311 * containing a bordered JLabel extension of icon and labels 312 * 313 * @param key name of icon 314 * @param iconMap containing icon for possible replacement 315 * @param dropIcon JLabel extension may be replaceable or dragable. 316 * @return the JPanel 317 */ 318 abstract protected JPanel makeIconDisplayPanel(String key, HashMap<String, NamedIcon> iconMap, boolean dropIcon); 319 320 /** 321 * Utility used by implementations of above 'makeIconDisplayPanel' method to wrap its panel 322 * @param icon icon held by a JLabel 323 * @param image background image for panel 324 * @param panel holds image and JLable 325 * @param key key of icon in its set - name for the icon can be extracted from it 326 */ 327 protected void wrapIconImage(NamedIcon icon, JLabel image, JPanel panel, String key) { 328 String borderName = ItemPalette.convertText(key); 329 panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 330 panel.setOpaque(false); 331 // I18N use existing NamedBeanBundle keys 332 panel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), borderName)); 333 image.setOpaque(false); 334 image.setToolTipText(icon.getName()); 335 image.setName(key); 336 JPanel iPanel = new JPanel(); 337 iPanel.setOpaque(false); 338 iPanel.add(image); 339 panel.add(iPanel); 340 341 double scale; 342 if (icon.getIconWidth() < 1 || icon.getIconHeight() < 1) { 343 image.setText(Bundle.getMessage("invisibleIcon")); 344 scale = 0; 345 } else { 346 scale = icon.reduceTo(CatalogPanel.ICON_WIDTH, CatalogPanel.ICON_HEIGHT, CatalogPanel.ICON_SCALE); 347 } 348 String scaleText = java.text.MessageFormat.format(Bundle.getMessage("scale"), CatalogPanel.printDbl(scale, 2)); 349 JLabel label = new JLabel(scaleText); 350 JPanel sPanel = new JPanel(); 351 sPanel.setOpaque(false); 352 sPanel.add(label); 353 panel.add(sPanel); 354 355 FontMetrics fm = getFontMetrics(panel.getFont()); 356 int width = fm.stringWidth(borderName) + 5; 357 width = Math.max(Math.max(width, CatalogPanel.ICON_WIDTH), icon.getIconWidth() + 5); 358 int height = panel.getPreferredSize().height; 359 panel.setPreferredSize(new Dimension(width, height)); 360 } 361 362 abstract protected JPanel instructions(); 363 364 /** 365 * Part of the initialization and reseting of an ItemPanel. 366 * Allows divergence for different panel needs. 367 */ 368 abstract protected void makeFamiliesPanel(); 369 370 abstract protected void hideIcons(); 371 372 /** 373 * See if the map is supported by the family map. "Equals" in 374 * this context means that each map is the same size the keys are equal and 375 * the urls for the icons are equal. Note that icons with different urls may 376 * be or appear to be the same. 377 * The item type "SignalHead" allows for unequal sizes but 'mapOne' 378 * must contain 'mapTwo' elements. 379 * 380 * @param mapOne an icon HashMap 381 * @param mapTwo another icon HashMap 382 * @return true if all of signal head entries have matching entries in the 383 * family map. 384 */ 385 protected boolean mapsAreEqual(HashMap<String, NamedIcon> mapOne, HashMap<String, NamedIcon> mapTwo) { 386 if ( !_itemType.equals("SignalHead") && mapOne.size() != mapTwo.size()) { 387 return false; 388 } 389 for (Entry<String, NamedIcon> mapTwoEntry : mapTwo.entrySet()) { 390 NamedIcon mapOneIcon = mapOne.get(mapTwoEntry.getKey()); 391 if (mapOneIcon == null) { 392 return false; 393 } 394 String url = mapOneIcon.getURL(); 395 if (url == null || !url.equals(mapTwoEntry.getValue().getURL())) { 396 return false; 397 } 398 } 399 return true; 400 } 401 402 protected void loadDefaultType() { 403 ItemPalette.loadMissingItemType(_itemType); 404 // Check for duplicate names or duplicate icon sets 405 java.util.ArrayList<String> deletes = new java.util.ArrayList<>(); 406 if (!_itemType.equals("IndicatorTO")) { 407 HashMap<String, HashMap<String, NamedIcon>> families = ItemPalette.getFamilyMaps(_itemType); 408 java.util.Set<String> keys = families.keySet(); 409 String[] key = new String[keys.size()]; 410 key = keys.toArray(key); 411 for (int i=0; i<key.length; i++) { 412 for (int j=i+1; j<key.length; j++) { 413 HashMap<String, NamedIcon> mapK = families.get(key[i]); 414 if (mapsAreEqual(mapK, families.get(key[j]))) { 415 deletes.add(queryWhichToDelete(key[i], key[j])); 416 break; 417 } 418 } 419 } 420 for (String k : deletes) { 421 ItemPalette.removeIconMap(_itemType, k); 422 } 423 if (this instanceof FamilyItemPanel) { 424 ((FamilyItemPanel)this)._family = null; 425 } 426 } else { 427 IndicatorTOItemPanel p = (IndicatorTOItemPanel)this; 428 HashMap<String, HashMap<String, HashMap<String, NamedIcon>>> 429 families = ItemPalette.getLevel4FamilyMaps(_itemType); 430 java.util.Set<String> keys = families.keySet(); 431 String[] key = new String[keys.size()]; 432 key = keys.toArray(key); 433 for (int i=0; i<key.length; i++) { 434 for (int j=i+1; j<key.length; j++) { 435 HashMap<String, HashMap<String, NamedIcon>> mapK = families.get(key[i]); 436 if (p.familiesAreEqual(mapK, families.get(key[j]))) { 437 deletes.add(queryWhichToDelete(key[i], key[j])); 438 break; 439 } 440 } 441 } 442 for (String k : deletes) { 443 ItemPalette.removeLevel4IconMap(_itemType, k, null); 444 } 445 p._family = null; 446 } 447 if (!_initialized) { 448 makeFamiliesPanel(); 449 } else { 450 initIconFamiliesPanel(); 451 hideIcons(); 452 } 453 } 454 455 /** 456 * Ask user to choose from 2 different names for the same icon map. 457 * @param key1 first name found for same map 458 * @param key2 second name found, default to delete 459 * @return the name and map to discard 460 */ 461 private String queryWhichToDelete(String key1, String key2) { 462 int result = JmriJOptionPane.showOptionDialog(this, Bundle.getMessage("DuplicateMap", key1, key2), 463 Bundle.getMessage("QuestionTitle"), JmriJOptionPane.DEFAULT_OPTION, 464 JmriJOptionPane.QUESTION_MESSAGE, null, 465 new Object[] {key1, key2}, key1); 466 if ( result == 0 ) { // position 0 in array, keep key1, return key2 467 return key2; 468 } else if ( result == 1 ) { // position 1 in array, keep key1, return key2 469 return key1; 470 } 471 return key2; 472 } 473 474 /** 475 * Resize frame to allow display/shrink after Icon map is dieplayed. 476 * @param isPalette selector for what to resize, true to resize parent tabbed frame 477 * @param oldDim old panel size 478 * @param frameDim old frame size 479 */ 480 protected void reSizeDisplay(boolean isPalette, Dimension oldDim, Dimension frameDim) { 481 Dimension newDim = getPreferredSize(); 482 Dimension deltaDim = shellDimension(this); 483 if (log.isDebugEnabled()) { 484 // Gather data for additional dimensions needed to display new panel in the total frame 485 Dimension frameDiffDim = new Dimension(frameDim.width - oldDim.width, frameDim.height - oldDim.height); 486 log.debug("resize {} {}. frameDiffDim= ({}, {}) deltaDim= ({}, {}) prefDim= ({}, {}))", 487 (isPalette?"tabPane":"update"), _itemType, 488 frameDiffDim.width, frameDiffDim.height, 489 deltaDim.width, deltaDim.height, newDim.width, newDim.height); 490 } 491 if (isPalette && _initialized) { 492 _frame.reSize(ItemPalette._tabPane, deltaDim, newDim); 493 } else if (_update || _initialized) { 494 _frame.reSize(_frame, deltaDim, newDim); 495 } 496 } 497 498 public Dimension shellDimension(ItemPanel panel) { 499 if (panel instanceof FamilyItemPanel) { 500 return new Dimension(23, 122); 501 } else if (panel instanceof IconItemPanel) { 502 return new Dimension(23, 65); 503 } 504 return new Dimension(23, 48); 505 } 506 507 static public GridBagConstraints itemGridBagConstraint() { 508 GridBagConstraints c = new GridBagConstraints(); 509 c.fill = GridBagConstraints.NONE; 510 c.anchor = GridBagConstraints.CENTER; 511 c.weightx = 1.0; 512 c.weighty = 1.0; 513 c.gridwidth = 1; 514 c.gridheight = 1; 515 c.gridx = 0; 516 c.gridy = 0; 517 return c; 518 } 519 520 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ItemPanel.class); 521 522}