001package jmri.jmrit.display;
002
003import java.awt.Component;
004import java.awt.Container;
005import java.awt.Dimension;
006import java.awt.Point;
007import java.awt.event.ActionEvent;
008import java.awt.event.ActionListener;
009import java.util.Objects;
010import java.util.HashSet;
011import java.util.Set;
012
013import javax.annotation.Nonnull;
014import javax.swing.AbstractAction;
015import javax.swing.JCheckBoxMenuItem;
016import javax.swing.JComponent;
017import javax.swing.JFrame;
018import javax.swing.JMenuItem;
019import javax.swing.JPanel;
020import javax.swing.JPopupMenu;
021import javax.swing.JScrollPane;
022import javax.swing.SwingUtilities;
023
024import jmri.InstanceManager;
025
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import jmri.jmrit.display.palette.ItemPanel;
030import jmri.jmrit.display.palette.TextItemPanel;
031import jmri.jmrit.logixng.LogixNG;
032import jmri.jmrit.logixng.LogixNG_Manager;
033import jmri.util.swing.JmriMouseEvent;
034import jmri.util.swing.JmriMouseListener;
035import jmri.util.swing.JmriMouseMotionListener;
036
037/**
038 * <a href="doc-files/Heirarchy.png"><img src="doc-files/Heirarchy.png" alt="UML class diagram for package" height="33%" width="33%"></a>
039 * @author Bob Jacobsen copyright (C) 2009
040 */
041public class PositionableJPanel extends JPanel implements Positionable, JmriMouseListener, JmriMouseMotionListener {
042
043    protected Editor _editor = null;
044
045    private String _id;            // user's Id or null if no Id
046    private final Set<String> _classes = new HashSet<>(); // user's classes
047
048    private ToolTip _tooltip;
049    protected boolean _showTooltip = true;
050    protected boolean _editable = true;
051    protected boolean _positionable = true;
052    protected boolean _viewCoordinates = false;
053    protected boolean _controlling = true;
054    protected boolean _hidden = false;
055    protected boolean _emptyHidden = false;
056    protected int _displayLevel;
057    private double _scale = 1.0;    // scaling factor
058
059    JMenuItem lock = null;
060    JCheckBoxMenuItem showTooltipItem = null;
061
062    private LogixNG _logixNG;
063    private String _logixNG_SystemName;
064
065    public PositionableJPanel(Editor editor) {
066        _editor = editor;
067    }
068
069    /**
070     * Hit testing must use the same scaled bounds as painting in an editor
071     * with a paint scale other than 1.0.
072     *
073     * @param x the x coordinate relative to this panel
074     * @param y the y coordinate relative to this panel
075     * @return true if the point is inside the displayed panel
076     */
077    @Override
078    public boolean contains(int x, int y) {
079        if (_editor == null || _editor.getTargetPanel() == null || getParent() == null) {
080            return super.contains(x, y);
081        }
082        double scale = _editor.getPaintScale();
083        if (scale == 1.0) {
084            return super.contains(x, y);
085        }
086
087        Point origin = SwingUtilities.convertPoint(this, 0, 0, _editor.getTargetPanel());
088        double unscaledX = (x + origin.x) / scale - origin.x;
089        double unscaledY = (y + origin.y) / scale - origin.y;
090        return unscaledX >= 0 && unscaledX < getWidth()
091                && unscaledY >= 0 && unscaledY < getHeight();
092    }
093
094    @Override
095    public Positionable deepClone() {
096        PositionableJPanel pos = new PositionableJPanel(_editor);
097        return finishClone(pos);
098    }
099
100    protected Positionable finishClone(PositionableJPanel pos) {
101        pos.setLocation(getX(), getY());
102        pos._displayLevel = _displayLevel;
103        pos._controlling = _controlling;
104        pos._hidden = _hidden;
105        pos._positionable = _positionable;
106        pos._showTooltip = _showTooltip;
107        pos.setToolTip(getToolTip());
108        pos._editable = _editable;
109        if (getPopupUtility() == null) {
110            pos.setPopupUtility(null);
111        } else {
112            pos.setPopupUtility(getPopupUtility().clone(pos, pos.getTextComponent()));
113        }
114        pos.updateSize();
115        return pos;
116    }
117
118    /** {@inheritDoc} */
119    @Override
120    public void setId(String id) throws Positionable.DuplicateIdException {
121        if (Objects.equals(this._id, id)) return;
122        _editor.positionalIdChange(this, id);
123        this._id = id;
124    }
125
126    /** {@inheritDoc} */
127    @Override
128    public String getId() {
129        return _id;
130    }
131
132    /** {@inheritDoc} */
133    @Override
134    public void addClass(String className) {
135        _editor.positionalAddClass(this, className);
136        _classes.add(className);
137    }
138
139    /** {@inheritDoc} */
140    @Override
141    public void removeClass(String className) {
142        _editor.positionalRemoveClass(this, className);
143        _classes.remove(className);
144    }
145
146    /** {@inheritDoc} */
147    @Override
148    public void removeAllClasses() {
149        for (String className : _classes) {
150            _editor.positionalRemoveClass(this, className);
151        }
152        _classes.clear();
153    }
154
155    /** {@inheritDoc} */
156    @Override
157    public Set<String> getClasses() {
158        return java.util.Collections.unmodifiableSet(_classes);
159    }
160
161    @Override
162    public void setPositionable(boolean enabled) {
163        _positionable = enabled;
164    }
165
166    @Override
167    public boolean isPositionable() {
168        return _positionable;
169    }
170
171    @Override
172    public void setEditable(boolean enabled) {
173        _editable = enabled;
174    }
175
176    @Override
177    public boolean isEditable() {
178        return _editable;
179    }
180
181    @Override
182    public void setViewCoordinates(boolean enabled) {
183        _viewCoordinates = enabled;
184    }
185
186    @Override
187    public boolean getViewCoordinates() {
188        return _viewCoordinates;
189    }
190
191    @Override
192    public void setControlling(boolean enabled) {
193        _controlling = enabled;
194    }
195
196    @Override
197    public boolean isControlling() {
198        return _controlling;
199    }
200
201    @Override
202    public void setHidden(boolean hide) {
203        if (_hidden != hide) {
204            _hidden = hide;
205            showHidden();
206        }
207    }
208
209    @Override
210    public boolean isHidden() {
211        return _hidden;
212    }
213
214    @Override
215    public void showHidden() {
216        if (!_hidden || _editor.isEditable()) {
217            setVisible(true);
218        } else {
219            setVisible(false);
220        }
221    }
222
223    @Override
224    public void setEmptyHidden(boolean hide) {
225        _emptyHidden = hide;
226    }
227
228    @Override
229    public boolean isEmptyHidden() {
230        return _emptyHidden;
231    }
232
233    @Override
234    public void setValueEditDisabled(boolean isDisabled) {
235    }
236
237    @Override
238    public boolean isValueEditDisabled() {
239        return false;
240    }
241
242    public void setLevel(int l) {
243        _displayLevel = l;
244    }
245
246    @Override
247    public void setDisplayLevel(int l) {
248        int oldDisplayLevel = _displayLevel;
249        _displayLevel = l;
250        if (oldDisplayLevel != l) {
251            log.debug("Changing label display level from {} to {}", oldDisplayLevel, _displayLevel);
252            _editor.displayLevelChange(this);
253        }
254    }
255
256    @Override
257    public int getDisplayLevel() {
258        return _displayLevel;
259    }
260
261    @Override
262    public void setShowToolTip(boolean set) {
263        _showTooltip = set;
264    }
265
266    @Override
267    public boolean showToolTip() {
268        return _showTooltip;
269    }
270
271    @Override
272    public void setToolTip(ToolTip tip) {
273        _tooltip = tip;
274    }
275
276    @Override
277    public ToolTip getToolTip() {
278        return _tooltip;
279    }
280
281    @Override
282    public void setScale(double s) {
283        _scale = s;
284    }
285
286    @Override
287    public double getScale() {
288        return _scale;
289    }
290
291    // no subclasses support rotations (yet)
292    @Override
293    public void rotate(int deg) {
294    }
295
296    @Override
297    public int getDegrees() {
298        return 0;
299    }
300
301    @Override
302    public JComponent getTextComponent() {
303        return this;
304    }
305
306    @Override
307    @Nonnull
308    public String getTypeString() {
309        return Bundle.getMessage("PositionableType_PositionableJPanel");
310    }
311
312    @Override
313    public String getNameString() {
314        return getName();
315    }
316
317    @Override
318    public Editor getEditor() {
319        return _editor;
320    }
321
322    @Override
323    public void setEditor(Editor ed) {
324        _editor = ed;
325    }
326
327    public boolean setEditTextItemMenu(JPopupMenu popup) {
328        popup.add(new AbstractAction(Bundle.getMessage("SetTextSizeColor")) {
329            @Override
330            public void actionPerformed(ActionEvent e) {
331                editTextItem();
332            }
333        });
334        return true;
335    }
336
337    TextItemPanel _itemPanel;
338
339    protected void editTextItem() {
340        _paletteFrame = makePaletteFrame(Bundle.getMessage("SetTextSizeColor"));
341        _itemPanel = new TextItemPanel(_paletteFrame, "Text");
342        ActionListener updateAction = (ActionEvent a) -> updateTextItem();
343        _itemPanel.init(updateAction, this);
344        initPaletteFrame(_paletteFrame, _itemPanel);
345    }
346
347    protected void updateTextItem() {
348        PositionablePopupUtil util = _itemPanel.getPositionablePopupUtil();
349        _itemPanel.setAttributes(this);
350        if (_editor._selectionGroup != null) {
351            _editor.setSelectionsAttributes(util, this);
352        } else {
353            _editor.setAttributes(util, this);
354        }
355        finishItemUpdate(_paletteFrame, _itemPanel);
356    }
357
358    public jmri.jmrit.display.DisplayFrame _paletteFrame;
359
360    // ********** Methods for Item Popups in Control Panel editor *******************
361    /**
362     * Create a palette window.
363     *
364     * @param title the name of the palette
365     * @return DisplayFrame for palette item
366     */
367    public DisplayFrame makePaletteFrame(String title) {
368        jmri.jmrit.display.palette.ItemPalette.loadIcons();
369
370        return new DisplayFrame(title, _editor);
371    }
372
373    public void initPaletteFrame(DisplayFrame paletteFrame, @Nonnull ItemPanel itemPanel) {
374        Dimension dim = itemPanel.getPreferredSize();
375        JScrollPane sp = new JScrollPane(itemPanel);
376        dim = new Dimension(dim.width + 25, dim.height + 25);
377        sp.setPreferredSize(dim);
378        paletteFrame.add(sp);
379        paletteFrame.pack();
380        jmri.InstanceManager.getDefault(jmri.util.PlaceWindow.class).nextTo(_editor, this, paletteFrame);
381        paletteFrame.setVisible(true);
382    }
383
384    public void finishItemUpdate(DisplayFrame paletteFrame, @Nonnull ItemPanel itemPanel) {
385        itemPanel.closeDialogs();
386        paletteFrame.dispose();
387        invalidate();
388    }
389
390    // overide where used - e.g. momentary
391    @Override
392    public void doMousePressed(JmriMouseEvent event) {
393    }
394
395    @Override
396    public void doMouseReleased(JmriMouseEvent event) {
397    }
398
399    @Override
400    public void doMouseClicked(JmriMouseEvent event) {
401    }
402
403    @Override
404    public void doMouseDragged(JmriMouseEvent event) {
405    }
406
407    @Override
408    public void doMouseMoved(JmriMouseEvent event) {
409    }
410
411    @Override
412    public void doMouseEntered(JmriMouseEvent event) {
413    }
414
415    @Override
416    public void doMouseExited(JmriMouseEvent event) {
417    }
418
419    @Override
420    public boolean storeItem() {
421        return true;
422    }
423
424    @Override
425    public boolean doViemMenu() {
426        return true;
427    }
428
429    /**
430     * For over-riding in the using classes: add item specific menu choices
431     */
432    @Override
433    public boolean setRotateOrthogonalMenu(JPopupMenu popup) {
434        return false;
435    }
436
437    @Override
438    public boolean setRotateMenu(JPopupMenu popup) {
439        return false;
440    }
441
442    @Override
443    public boolean setScaleMenu(JPopupMenu popup) {
444        return false;
445    }
446
447    @Override
448    public boolean setDisableControlMenu(JPopupMenu popup) {
449        return false;
450    }
451
452    @Override
453    public boolean setTextEditMenu(JPopupMenu popup) {
454        return false;
455    }
456
457    @Override
458    public boolean showPopUp(JPopupMenu popup) {
459        return false;
460    }
461
462    JFrame _iconEditorFrame;
463    IconAdder _iconEditor;
464
465    @Override
466    public boolean setEditIconMenu(JPopupMenu popup) {
467        return false;
468    }
469
470    @Override
471    public boolean setEditItemMenu(JPopupMenu popup) {
472        return setEditIconMenu(popup);
473    }
474
475    protected void makeIconEditorFrame(Container pos, String name, boolean table, IconAdder editor) {
476        if (editor != null) {
477            _iconEditor = editor;
478        } else {
479            _iconEditor = new IconAdder(name);
480        }
481        _iconEditorFrame = _editor.makeAddIconFrame(name, false, table, _iconEditor);
482        _iconEditorFrame.addWindowListener(new java.awt.event.WindowAdapter() {
483            @Override
484            public void windowClosing(java.awt.event.WindowEvent e) {
485                _iconEditorFrame.dispose();
486                _iconEditorFrame = null;
487            }
488        });
489        _iconEditorFrame.setLocationRelativeTo(pos);
490        _iconEditorFrame.toFront();
491        _iconEditorFrame.setVisible(true);
492    }
493
494    void edit() {
495    }
496
497    /*
498     ************** end Positionable methods *********************
499     */
500    /**
501     * Removes this object from display and persistance
502     */
503    @Override
504    public void remove() {
505        _editor.removeFromContents(this);
506        cleanup();
507        // remove from persistance by flagging inactive
508        active = false;
509    }
510
511    /**
512     * To be overridden if any special work needs to be done
513     */
514    void cleanup() {
515    }
516
517    boolean active = true;
518
519    /**
520     * @return true if this object is still displayed, and should be stored;
521     *         false otherwise
522     */
523    public boolean isActive() {
524        return active;
525    }
526
527    private JmriMouseEvent toEditorMouseEvent(JmriMouseEvent event) {
528        Component source = event.getComponent();
529        Point point = SwingUtilities.convertPoint(source, event.getPoint(), _editor.getTargetPanel());
530        double scale = _editor.getPaintScale();
531        return new JmriMouseEvent(this, event.getID(), event.getWhen(), event.getModifiersEx(),
532                (int) Math.round(point.x * scale), (int) Math.round(point.y * scale),
533                event.getClickCount(), event.isPopupTrigger(), event.getButton());
534    }
535
536    @Override
537    public void mousePressed(JmriMouseEvent e) {
538        _editor.mousePressed(toEditorMouseEvent(e));
539    }
540
541    @Override
542    public void mouseReleased(JmriMouseEvent e) {
543        _editor.mouseReleased(toEditorMouseEvent(e));
544    }
545
546    @Override
547    public void mouseClicked(JmriMouseEvent e) {
548        _editor.mouseClicked(toEditorMouseEvent(e));
549    }
550
551    @Override
552    public void mouseExited(JmriMouseEvent e) {
553//     transferFocus();
554        _editor.mouseExited(toEditorMouseEvent(e));
555    }
556
557    @Override
558    public void mouseEntered(JmriMouseEvent e) {
559        _editor.mouseEntered(toEditorMouseEvent(e));
560    }
561
562    @Override
563    public void mouseMoved(JmriMouseEvent e) {
564        _editor.mouseMoved(toEditorMouseEvent(e));
565    }
566
567    @Override
568    public void mouseDragged(JmriMouseEvent e) {
569        _editor.mouseDragged(toEditorMouseEvent(e));
570    }
571
572    /**
573     * ************************************************************
574     */
575    PositionablePopupUtil _popupUtil;
576
577    @Override
578    public void setPopupUtility(PositionablePopupUtil tu) {
579        _popupUtil = tu;
580    }
581
582    @Override
583    public PositionablePopupUtil getPopupUtility() {
584        return _popupUtil;
585    }
586
587    /**
588     * Update the AWT and Swing size information due to change in internal
589     * state, e.g. if one or more of the icons that might be displayed is
590     * changed
591     */
592    @Override
593    public void updateSize() {
594        invalidate();
595        setSize(maxWidth(), maxHeight());
596        if (log.isTraceEnabled()) {
597            // the following fails when run on Jenkins under Xvfb with an NPE in non-JMRI code
598            log.trace("updateSize: {}, text: w={} h={}",
599                    _popupUtil.toString(),
600                    getFontMetrics(_popupUtil.getFont()).stringWidth(_popupUtil.getText()),
601                    getFontMetrics(_popupUtil.getFont()).getHeight());
602        }
603        validate();
604        repaint();
605    }
606
607    @Override
608    public int maxWidth() {
609        int max = 0;
610        if (_popupUtil != null) {
611            if (_popupUtil.getFixedWidth() != 0) {
612                max = _popupUtil.getFixedWidth();
613                max += _popupUtil.getMargin() * 2;
614                if (max < PositionablePopupUtil.MIN_SIZE) {  // don't let item disappear
615                    _popupUtil.setFixedWidth(PositionablePopupUtil.MIN_SIZE);
616                    max = PositionablePopupUtil.MIN_SIZE;
617                }
618            } else {
619                max = getPreferredSize().width;
620                /*
621                 if(_popupUtil._textComponent instanceof javax.swing.JTextField) {
622                 javax.swing.JTextField text = (javax.swing.JTextField)_popupUtil._textComponent;
623                 max = getFontMetrics(text.getFont()).stringWidth(text.getText());
624                 } */
625                max += _popupUtil.getMargin() * 2;
626                if (max < PositionablePopupUtil.MIN_SIZE) {  // don't let item disappear
627                    max = PositionablePopupUtil.MIN_SIZE;
628                }
629            }
630        }
631        log.debug("maxWidth= {} preferred width= {}", max, getPreferredSize().width);
632        return max;
633    }
634
635    @Override
636    public int maxHeight() {
637        int max = 0;
638        if (_popupUtil != null) {
639            if (_popupUtil.getFixedHeight() != 0) {
640                max = _popupUtil.getFixedHeight();
641                max += _popupUtil.getMargin() * 2;
642                if (max < PositionablePopupUtil.MIN_SIZE) {   // don't let item disappear
643                    _popupUtil.setFixedHeight(PositionablePopupUtil.MIN_SIZE);
644                    max = PositionablePopupUtil.MIN_SIZE;
645                }
646            } else {
647                max = getPreferredSize().height;
648                /*
649                 if(_popupUtil._textComponent!=null) {
650                 max = getFontMetrics(_popupUtil._textComponent.getFont()).getHeight();
651                 }  */
652                if (_popupUtil != null) {
653                    max += _popupUtil.getMargin() * 2;
654                }
655                if (max < PositionablePopupUtil.MIN_SIZE) {  // don't let item disappear
656                    max = PositionablePopupUtil.MIN_SIZE;
657                }
658            }
659        }
660        log.debug("maxHeight= {} preferred width= {}", max, getPreferredSize().height);
661        return max;
662    }
663
664    @Override
665    public jmri.NamedBean getNamedBean() {
666        return null;
667    }
668
669    /** {@inheritDoc} */
670    @Override
671    public LogixNG getLogixNG() {
672        return _logixNG;
673    }
674
675    /** {@inheritDoc} */
676    @Override
677    public void setLogixNG(LogixNG logixNG) {
678        this._logixNG = logixNG;
679    }
680
681    /** {@inheritDoc} */
682    @Override
683    public void setLogixNG_SystemName(String systemName) {
684        this._logixNG_SystemName = systemName;
685    }
686
687    /** {@inheritDoc} */
688    @Override
689    public void setupLogixNG() {
690        _logixNG = InstanceManager.getDefault(LogixNG_Manager.class)
691                .getBySystemName(_logixNG_SystemName);
692        if (_logixNG == null) {
693            throw new RuntimeException(String.format(
694                    "LogixNG %s is not found for positional %s in panel %s",
695                    _logixNG_SystemName, getNameString(), getEditor().getName()));
696        }
697        _logixNG.setInlineLogixNG(this);
698    }
699
700    private static final Logger log = LoggerFactory.getLogger(PositionableJPanel.class);
701}