Ticket #3441: middle_click_patch.patch

File middle_click_patch.patch, 18.9 KB (added by xeen, 3 years ago)
  • src/org/openstreetmap/josm/gui/MapStatus.java

     
    66 
    77import java.awt.AWTEvent; 
    88import java.awt.Color; 
     9import java.awt.Component; 
    910import java.awt.Cursor; 
    1011import java.awt.Dimension; 
    1112import java.awt.EventQueue; 
    1213import java.awt.Font; 
    1314import java.awt.GridBagLayout; 
    1415import java.awt.Point; 
     16import java.awt.SystemColor; 
    1517import java.awt.Toolkit; 
    1618import java.awt.event.AWTEventListener; 
    1719import java.awt.event.ComponentEvent; 
     
    2224import java.awt.event.MouseEvent; 
    2325import java.awt.event.MouseListener; 
    2426import java.awt.event.MouseMotionListener; 
    25 import java.lang.reflect.InvocationTargetException; 
     27import java.util.ArrayList; 
    2628import java.util.Collection; 
    2729import java.util.ConcurrentModificationException; 
     30import java.util.List; 
    2831import java.util.Map.Entry; 
    2932 
    3033import javax.swing.BorderFactory; 
     
    112115     */ 
    113116    private final class Collector implements Runnable { 
    114117        /** 
    115          * The last object displayed in status line. 
     118         * the mouse position of the previous iteration. This is used to show 
     119         * the popup until the cursor is moved. 
    116120         */ 
    117         Collection<OsmPrimitive> osmStatus; 
     121        private Point oldMousePos; 
    118122        /** 
    119          * The old modifiers that was pressed the last time this collector ran. 
     123         * Contains the labels that are currently shown in the information 
     124         * popup 
    120125         */ 
    121         private int oldModifiers; 
     126        private List<JLabel> popupLabels = null; 
    122127        /** 
    123128         * The popup displayed to show additional information 
    124129         */ 
     
    143148                } 
    144149                if (parent != Main.map) 
    145150                    return; // exit, if new parent. 
    146                 if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0 || ms.mousePos == null) { 
    147                     continue; // freeze display when holding down ctrl 
     151 
     152                // Do nothing, if required data is missing 
     153                if(ms.mousePos == null || mv.center == null) { 
     154                    continue; 
    148155                } 
    149156 
    150                 if (mv.center == null) { 
     157                // Freeze display when holding down CTRL 
     158                if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { 
     159                    // update the information popup's labels though, because 
     160                    // the selection might have changed from the outside 
     161                    popupUpdateLabels(); 
    151162                    continue; 
    152163                } 
    153164 
     
    156167                // access to the data need to be restarted, if the main thread modifies 
    157168                // the data. 
    158169                try { 
    159                     OsmPrimitive osmNearest = null; 
    160170                    // Set the text label in the bottom status bar 
    161                     osmNearest = mv.getNearest(ms.mousePos); 
    162                     if (osmNearest != null) { 
    163                         nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance())); 
    164                     } else { 
    165                         nameText.setText(tr("(no object)")); 
    166                     } 
     171                    statusBarElementUpdate(ms); 
     172 
     173                    // The popup != null check is required because a left-click 
     174                    // produces several events as well, which would make this 
     175                    // variable true. Of course we only want the popup to show 
     176                    // if the middle mouse button has been pressed in the first 
     177                    // place 
     178                    boolean isAtOldPosition = (oldMousePos != null 
     179                            && oldMousePos.equals(ms.mousePos) 
     180                            && popup != null); 
     181                    boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0; 
     182 
    167183 
    168184                    // Popup Information 
    169                     if ((ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0 ) { 
     185                    // display them if the middle mouse button is pressed and 
     186                    // keep them until the mouse is moved 
     187                    if (middleMouseDown || isAtOldPosition) 
     188                    { 
    170189                        Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos); 
    171190 
    172191                        if (osms == null) { 
    173192                            continue; 
    174193                        } 
    175                         if (osms.equals(osmStatus) && ms.modifiers == oldModifiers) { 
    176                             continue; 
    177                         } 
    178194 
    179                         if (popup != null) { 
    180                             try { 
    181                                 EventQueue.invokeAndWait(new Runnable() { 
    182                                     public void run() { 
    183                                         popup.hide(); 
    184                                     } 
    185                                 }); 
    186                             } catch (InterruptedException e) { 
    187                             } catch (InvocationTargetException e) { 
    188                                 throw new RuntimeException(e); 
    189                             } 
     195                        JPanel c = new JPanel(new GridBagLayout()); 
     196                        c.setBorder(BorderFactory.createRaisedBevelBorder()); 
     197                        final JLabel lbl = new JLabel( 
     198                                "<html>"+tr("Middle click again, to cycle through.<br>"+ 
     199                                "Hold CTRL to select something from this list.<hr>")+"</html>", 
     200                                null, 
     201                                JLabel.HORIZONTAL 
     202                        ); 
     203                        lbl.setHorizontalAlignment(JLabel.LEFT); 
     204                        c.add(lbl, GBC.eol().insets(2, 0, 2, 0)); 
     205 
     206                        // Only cycle if the mouse has not been moved and the 
     207                        // middle mouse button has been pressed at least twice 
     208                        // (the reason for this is the popup != null check for 
     209                        // isAtOldPosition, see above. This is a nice side 
     210                        // effect though, because it does not change selection 
     211                        // of the first middle click) 
     212                        if(isAtOldPosition && middleMouseDown) { 
     213                            popupCycleSelection(osms); 
    190214                        } 
    191215 
    192                         JPanel c = new JPanel(new GridBagLayout()); 
     216                        // These labels may need to be updated from the outside 
     217                        // so collect them 
     218                        List<JLabel> lbls = new ArrayList<JLabel>(); 
    193219                        for (final OsmPrimitive osm : osms) { 
    194                             final StringBuilder text = new StringBuilder(); 
    195                             String name = osm.getDisplayName(DefaultNameFormatter.getInstance()); 
    196                             if (osm.getId() == 0 || osm.isModified()) { 
    197                                 name = "<i><b>"+ osm.getDisplayName(DefaultNameFormatter.getInstance())+"*</b></i>"; 
    198                             } 
    199                             text.append(name); 
    200                             if (osm.getId() != 0) { 
    201                                 text.append("<br>id="+osm.getId()); 
    202                             } 
    203                             for (Entry<String, String> e : osm.entrySet()) { 
    204                                 text.append("<br>"+e.getKey()+"="+e.getValue()); 
    205                             } 
    206                             final JLabel l = new JLabel( 
    207                                     "<html>"+text.toString()+"</html>", 
    208                                     ImageProvider.get(OsmPrimitiveType.from(osm)), 
    209                                     JLabel.HORIZONTAL 
    210                             ); 
    211                             l.setFont(l.getFont().deriveFont(Font.PLAIN)); 
    212                             l.setVerticalTextPosition(JLabel.TOP); 
    213                             l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 
    214                             l.addMouseListener(new MouseAdapter(){ 
    215                                 @Override public void mouseEntered(MouseEvent e) { 
    216                                     l.setText("<html><u color='blue'>"+text.toString()+"</u></html>"); 
    217                                 } 
    218                                 @Override public void mouseExited(MouseEvent e) { 
    219                                     l.setText("<html>"+text.toString()+"</html>"); 
    220                                 } 
    221                                 @Override public void mouseClicked(MouseEvent e) { 
    222                                     Main.main.getCurrentDataSet().setSelected(osm); 
    223                                     mv.repaint(); 
    224                                 } 
    225                             }); 
    226                             c.add(l, GBC.eol()); 
     220                            JLabel l = popupBuildPrimitiveLabels(osm); 
     221                            lbls.add(l); 
     222                            c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2)); 
    227223                        } 
    228224 
    229                         Point p = mv.getLocationOnScreen(); 
    230                         popup = PopupFactory.getSharedInstance().getPopup(mv, c, p.x+ms.mousePos.x+16, p.y+ms.mousePos.y+16); 
    231                         final Popup staticPopup = popup; 
    232                         EventQueue.invokeLater(new Runnable(){ 
    233                             public void run() { 
    234                                 staticPopup.show(); 
    235                             } 
    236                         }); 
    237                     } else if (popup != null) { 
    238                         final Popup staticPopup = popup; 
    239                         popup = null; 
    240                         EventQueue.invokeLater(new Runnable(){ 
    241                             public void run() { 
    242                                 staticPopup.hide(); 
    243                             } 
    244                         }); 
     225                        popupShowPopup(popupCreatePopup(c, ms), lbls); 
     226                    } else { 
     227                        popupHidePopup(); 
    245228                    } 
     229 
     230                    oldMousePos = ms.mousePos; 
    246231                } catch (ConcurrentModificationException x) { 
     232                    //x.printStackTrace(); 
    247233                } catch (NullPointerException x) { 
     234                    //x.printStackTrace(); 
    248235                } 
    249236            } 
    250237        } 
     238 
     239        /** 
     240         * Creates a popup for the given content next to the cursor. Tries to 
     241         * keep the popup on screen. 
     242         * @param content 
     243         * @param ms 
     244         * @return popup 
     245         */ 
     246        private final Popup popupCreatePopup(Component content, MouseState ms) { 
     247            Point p = mv.getLocationOnScreen(); 
     248            Dimension scrn = Toolkit.getDefaultToolkit().getScreenSize(); 
     249            Dimension dim = content.getPreferredSize(); 
     250 
     251            int xPos = p.x + ms.mousePos.x + 16; 
     252            // Display the popup to the left of the cursor if it would be cut 
     253            // off on its right 
     254            if(xPos + dim.width > scrn.width) { 
     255                xPos = p.x + ms.mousePos.x - 4 - dim.width; 
     256            } 
     257            int yPos = p.y + ms.mousePos.y + 16; 
     258            // Move the popup up if it would be cut off at its bottom 
     259            if(yPos + dim.height > scrn.height - 5) { 
     260                yPos = scrn.height - dim.height - 5; 
     261            } 
     262 
     263            PopupFactory pf = PopupFactory.getSharedInstance(); 
     264            return pf.getPopup(mv, content, xPos, yPos); 
     265        } 
     266 
     267        /** 
     268         * Calls this to update the element that is shown in the statusbar 
     269         * @param ms 
     270         */ 
     271        private final void statusBarElementUpdate(MouseState ms) { 
     272            final OsmPrimitive osmNearest = mv.getNearest(ms.mousePos); 
     273            if (osmNearest != null) { 
     274                nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance())); 
     275            } else { 
     276                nameText.setText(tr("(no object)")); 
     277            } 
     278        } 
     279 
     280        /** 
     281         * Call this with a set of primitives to cycle through them. Method 
     282         * will automatically select the next item and update the map 
     283         * @param osms 
     284         */ 
     285        private final void popupCycleSelection(Collection<OsmPrimitive> osms) { 
     286            // Find some items that are required for cycling through 
     287            OsmPrimitive firstItem = null; 
     288            OsmPrimitive firstSelected = null; 
     289            OsmPrimitive nextSelected = null; 
     290            for (final OsmPrimitive osm : osms) { 
     291                if(firstItem == null) { 
     292                    firstItem = osm; 
     293                } 
     294                if(firstSelected != null && nextSelected == null) { 
     295                    nextSelected = osm; 
     296                } 
     297                if(firstSelected == null && osm.isSelected()) { 
     298                    firstSelected = osm; 
     299                } 
     300            } 
     301 
     302            // This will cycle through the available items. 
     303            if(firstSelected == null) { 
     304                firstItem.setSelected(true); 
     305            } else { 
     306                firstSelected.setSelected(false); 
     307                if(nextSelected != null) { 
     308                    nextSelected.setSelected(true); 
     309                } 
     310            } 
     311            mv.repaint(); 
     312        } 
     313 
     314        /** 
     315         * Tries to hide the given popup 
     316         * @param popup 
     317         */ 
     318        private final void popupHidePopup() { 
     319            popupLabels = null; 
     320            if(popup == null) 
     321                return; 
     322            final Popup staticPopup = popup; 
     323            popup = null; 
     324            EventQueue.invokeLater(new Runnable(){ 
     325                public void run() { staticPopup.hide(); }}); 
     326        } 
     327 
     328        /** 
     329         * Tries to show the given popup, can be hidden using popupHideOldPopup 
     330         * If an old popup exists, it will be automatically hidden 
     331         * @param popup 
     332         */ 
     333        private final void popupShowPopup(Popup newPopup, List<JLabel> lbls) { 
     334            final Popup staticPopup = newPopup; 
     335            if(this.popup != null) { 
     336                // If an old popup exists, remove it when the new popup has been 
     337                // drawn to keep flickering to a minimum 
     338                final Popup staticOldPopup = this.popup; 
     339                EventQueue.invokeLater(new Runnable(){ 
     340                    public void run() { 
     341                        staticPopup.show(); 
     342                        staticOldPopup.hide(); 
     343                    } 
     344                }); 
     345            } else { 
     346                // There is no old popup 
     347                EventQueue.invokeLater(new Runnable(){ 
     348                    public void run() { staticPopup.show(); }}); 
     349            } 
     350            this.popupLabels = lbls; 
     351            this.popup = newPopup; 
     352        } 
     353 
     354        /** 
     355         * This method should be called if the selection may have changed from 
     356         * outside of this class. This is the case when CTRL is pressed and the 
     357         * user clicks on the map instead of the popup. 
     358         */ 
     359        private final void popupUpdateLabels() { 
     360            if(this.popup == null || this.popupLabels == null) 
     361                return; 
     362            for(JLabel l : this.popupLabels) { 
     363                l.validate(); 
     364            } 
     365        } 
     366 
     367        /** 
     368         * Sets the colors for the given label depending on the selected status of 
     369         * the given OsmPrimitive 
     370         *  
     371         * @param lbl The label to color 
     372         * @param osm The primitive to derive the colors from 
     373         */ 
     374        private final void popupSetLabelColors(JLabel lbl, OsmPrimitive osm) { 
     375            if(osm.isSelected()) { 
     376                lbl.setBackground(SystemColor.textHighlight); 
     377                lbl.setForeground(SystemColor.textHighlightText); 
     378            } else { 
     379                lbl.setBackground(SystemColor.control); 
     380                lbl.setForeground(SystemColor.controlText); 
     381            } 
     382        } 
     383 
     384        /** 
     385         * Builds the labels with all necessary listeners for the info popup for the 
     386         * given OsmPrimitive 
     387         * @param osm  The primitive to create the label for 
     388         * @return 
     389         */ 
     390        private final JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) { 
     391            final StringBuilder text = new StringBuilder(); 
     392            String name = osm.getDisplayName(DefaultNameFormatter.getInstance()); 
     393            if (osm.getId() == 0 || osm.isModified()) { 
     394                name = "<i><b>"+ osm.getDisplayName(DefaultNameFormatter.getInstance())+"*</b></i>"; 
     395            } 
     396            text.append(name); 
     397            if (osm.getId() != 0) { 
     398                text.append("<br>id="+osm.getId()); 
     399            } 
     400            for (Entry<String, String> e1 : osm.entrySet()) { 
     401                text.append("<br>"+e1.getKey()+"="+e1.getValue()); 
     402            } 
     403 
     404            final JLabel l = new JLabel( 
     405                    "<html>"+text.toString()+"</html>", 
     406                    ImageProvider.get(OsmPrimitiveType.from(osm)), 
     407                    JLabel.HORIZONTAL 
     408            ) { 
     409                // This is necessary so the label updates its colors when the 
     410                // selection is changed from the outside 
     411                @Override public void validate() { 
     412                    super.validate(); 
     413                    popupSetLabelColors(this, osm); 
     414                } 
     415            }; 
     416            l.setOpaque(true); 
     417            popupSetLabelColors(l, osm); 
     418            l.setFont(l.getFont().deriveFont(Font.PLAIN)); 
     419            l.setVerticalTextPosition(JLabel.TOP); 
     420            l.setHorizontalAlignment(JLabel.LEFT); 
     421            l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 
     422            l.addMouseListener(new MouseAdapter(){ 
     423                @Override public void mouseEntered(MouseEvent e) { 
     424                    l.setBackground(SystemColor.info); 
     425                    l.setForeground(SystemColor.infoText); 
     426                } 
     427                @Override public void mouseExited(MouseEvent e) { 
     428                    popupSetLabelColors(l, osm); 
     429                } 
     430                @Override public void mouseClicked(MouseEvent e) { 
     431                    // Let the user toggle the selection 
     432                    osm.setSelected(!osm.isSelected()); 
     433                    mv.repaint(); 
     434                    l.validate(); 
     435                } 
     436            }); 
     437            // Sometimes the mouseEntered event is not catched, thus the label 
     438            // will not be highlighted, making it confusing. The MotionListener 
     439            // can correct this defect. 
     440            l.addMouseMotionListener(new MouseMotionListener() { 
     441                public void mouseMoved(MouseEvent e) { 
     442                    l.setBackground(SystemColor.info); 
     443                    l.setForeground(SystemColor.infoText); 
     444                } 
     445                public void mouseDragged(MouseEvent e) { 
     446                    l.setBackground(SystemColor.info); 
     447                    l.setForeground(SystemColor.infoText); 
     448                } 
     449            }); 
     450            return l; 
     451        } 
    251452    } 
    252453 
    253454    /**