Changeset 18574 in josm for trunk/src/org/openstreetmap


Ignore:
Timestamp:
2022-10-17T16:22:45+02:00 (2 years ago)
Author:
taylor.smock
Message:

Fix #22378: Preview object properties on mouse hover (patch by Woazboat, modified)

Show tags and relation memberships of an object in the properties dialog when
moving the mouser pointer over it (similar to iD).

Can be enabled/disabled via settings (enabled by default)

Location:
trunk/src/org/openstreetmap/josm/gui
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java

    r18494 r18574  
    99import java.awt.event.HierarchyEvent;
    1010import java.awt.event.HierarchyListener;
     11import java.awt.event.MouseAdapter;
     12import java.awt.event.MouseEvent;
    1113import java.awt.geom.AffineTransform;
    1214import java.awt.geom.Point2D;
     
    2123import java.util.Map;
    2224import java.util.Map.Entry;
     25import java.util.Objects;
    2326import java.util.Set;
    2427import java.util.Stack;
     
    4144import org.openstreetmap.josm.data.osm.BBox;
    4245import org.openstreetmap.josm.data.osm.DataSet;
     46import org.openstreetmap.josm.data.osm.IPrimitive;
    4347import org.openstreetmap.josm.data.osm.Node;
    4448import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    5357import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
    5458import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     59import org.openstreetmap.josm.gui.PrimitiveHoverListener.PrimitiveHoverEvent;
    5560import org.openstreetmap.josm.gui.help.Helpful;
    5661import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
     
    6469import org.openstreetmap.josm.tools.Logging;
    6570import org.openstreetmap.josm.tools.Utils;
     71import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
    6672
    6773/**
     
    150156    }
    151157
     158    private final CopyOnWriteArrayList<PrimitiveHoverListener> primitiveHoverListeners = new CopyOnWriteArrayList<>();
     159    private IPrimitive previousHoveredPrimitive;
     160    private final PrimitiveHoverMouseListener primitiveHoverMouseListenerHelper = new PrimitiveHoverMouseListener();
     161
     162    /**
     163     * Removes a primitive hover listener
     164     *
     165     * @param listener the listener. Ignored if null or already absent.
     166     * @since 18574
     167     */
     168    public void removePrimitiveHoverListener(PrimitiveHoverListener listener) {
     169        primitiveHoverListeners.remove(listener);
     170    }
     171
     172    /**
     173     * Adds a primitive hover listener
     174     *
     175     * @param listener the listener. Ignored if null or already registered.
     176     * @since 18574
     177     */
     178    public void addPrimitiveHoverListener(PrimitiveHoverListener listener) {
     179        if (listener != null) {
     180            primitiveHoverListeners.addIfAbsent(listener);
     181        }
     182    }
     183
     184    /**
     185     * Send a {@link PrimitiveHoverEvent} to registered {@link PrimitiveHoverListener}s
     186     * @param e primitive hover event information
     187     * @since 18574
     188     */
     189    protected void firePrimitiveHovered(PrimitiveHoverEvent e) {
     190        GuiHelper.runInEDT(() -> {
     191            for (PrimitiveHoverListener l : primitiveHoverListeners) {
     192                try {
     193                    l.primitiveHovered(e);
     194                } catch (RuntimeException ex) {
     195                    Logging.logWithStackTrace(Logging.LEVEL_ERROR, "Error in primitive hover listener", ex);
     196                    BugReportExceptionHandler.handleException(ex);
     197                }
     198            }
     199        });
     200    }
     201
     202    private void updateHoveredPrimitive(IPrimitive hovered, MouseEvent e) {
     203        if (!Objects.equals(hovered, previousHoveredPrimitive)) {
     204            firePrimitiveHovered(new PrimitiveHoverEvent(hovered, previousHoveredPrimitive, e));
     205            previousHoveredPrimitive = hovered;
     206        }
     207    }
     208
    152209    // The only events that may move/resize this map view are window movements or changes to the map view size.
    153210    // We can clean this up more by only recalculating the state on repaint.
     
    199256        addHierarchyListener(hierarchyListener);
    200257        addComponentListener(componentListener);
     258        addPrimitiveHoverMouseListeners();
    201259        super.addNotify();
    202260    }
     
    206264        removeHierarchyListener(hierarchyListener);
    207265        removeComponentListener(componentListener);
     266        removePrimitiveHoverMouseListeners();
    208267        super.removeNotify();
     268    }
     269
     270    private void addPrimitiveHoverMouseListeners() {
     271        addMouseMotionListener(primitiveHoverMouseListenerHelper);
     272        addMouseListener(primitiveHoverMouseListenerHelper);
     273    }
     274
     275    private void removePrimitiveHoverMouseListeners() {
     276        removeMouseMotionListener(primitiveHoverMouseListenerHelper);
     277        removeMouseListener(primitiveHoverMouseListenerHelper);
    209278    }
    210279
     
    17161785        )/512;
    17171786    }
     1787
     1788    /**
     1789     * Listener for mouse movement events. Used to detect when primitives are being hovered over with the mouse pointer
     1790     * so that registered {@link PrimitiveHoverListener}s can be notified.
     1791     */
     1792    private class PrimitiveHoverMouseListener extends MouseAdapter {
     1793        @Override
     1794        public void mouseMoved(MouseEvent e) {
     1795            OsmPrimitive hovered = getNearestNodeOrWay(e.getPoint(), isSelectablePredicate, true);
     1796            updateHoveredPrimitive(hovered, e);
     1797        }
     1798
     1799        @Override
     1800        public void mouseExited(MouseEvent e) {
     1801            updateHoveredPrimitive(null, e);
     1802        }
     1803    }
    17181804}
  • trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java

    r18332 r18574  
    1313import java.awt.event.MouseAdapter;
    1414import java.awt.event.MouseEvent;
     15import java.beans.PropertyChangeEvent;
     16import java.beans.PropertyChangeListener;
    1517import java.util.ArrayList;
    1618import java.util.Arrays;
     
    6971import org.openstreetmap.josm.data.osm.KeyValueVisitor;
    7072import org.openstreetmap.josm.data.osm.Node;
    71 import org.openstreetmap.josm.data.osm.OsmData;
    7273import org.openstreetmap.josm.data.osm.OsmDataManager;
    7374import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    8485import org.openstreetmap.josm.data.osm.search.SearchCompiler;
    8586import org.openstreetmap.josm.data.osm.search.SearchSetting;
     87import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeEvent;
     88import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
    8689import org.openstreetmap.josm.data.preferences.BooleanProperty;
     90import org.openstreetmap.josm.data.preferences.CachingProperty;
    8791import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
    8892import org.openstreetmap.josm.gui.ExtendedDialog;
    8993import org.openstreetmap.josm.gui.MainApplication;
    9094import org.openstreetmap.josm.gui.PopupMenuHandler;
     95import org.openstreetmap.josm.gui.PrimitiveHoverListener;
    9196import org.openstreetmap.josm.gui.SideButton;
    9297import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
     
    97102import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
    98103import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
     104import org.openstreetmap.josm.gui.layer.Layer;
    99105import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    100106import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
     
    141147 */
    142148public class PropertiesDialog extends ToggleDialog
    143 implements DataSelectionListener, ActiveLayerChangeListener, DataSetListenerAdapter.Listener, TaggingPresetListener {
     149implements DataSelectionListener, ActiveLayerChangeListener, PropertyChangeListener,
     150        DataSetListenerAdapter.Listener, TaggingPresetListener, PrimitiveHoverListener {
    144151    private final BooleanProperty PROP_DISPLAY_DISCARDABLE_KEYS = new BooleanProperty("display.discardable-keys", false);
    145152
     
    249256
    250257    /**
     258     * Show tags and relation memberships of objects in the properties dialog when hovering over them with the mouse pointer
     259     * @since 18574
     260     */
     261    public static final BooleanProperty PROP_PREVIEW_ON_HOVER = new BooleanProperty("propertiesdialog.preview-on-hover", true);
     262    private final HoverPreviewPropListener hoverPreviewPropListener = new HoverPreviewPropListener();
     263
     264    /**
     265     * Always show information for selected objects when something is selected instead of the hovered object
     266     * @since 18574
     267     */
     268    public static final CachingProperty<Boolean> PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION =
     269        new BooleanProperty("propertiesdialog.preview-on-hover.always-show-selected", true).cached();
     270
     271    /**
    251272     * Create a new PropertiesDialog
    252273     */
     
    305326
    306327        TaggingPresets.addListener(this);
     328
     329        PROP_PREVIEW_ON_HOVER.addListener(hoverPreviewPropListener);
    307330    }
    308331
     
    587610        SelectionEventManager.getInstance().addSelectionListenerForEdt(this);
    588611        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
     612        if (PROP_PREVIEW_ON_HOVER.get())
     613            MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
    589614        for (JosmAction action : josmActions) {
    590615            MainApplication.registerActionShortcut(action);
     
    598623        SelectionEventManager.getInstance().removeSelectionListener(this);
    599624        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
     625        MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
    600626        for (JosmAction action : josmActions) {
    601627            MainApplication.unregisterActionShortcut(action);
     
    618644        super.destroy();
    619645        TaggingPresets.removeListener(this);
     646        PROP_PREVIEW_ON_HOVER.removeListener(hoverPreviewPropListener);
    620647        Container parent = pluginHook.getParent();
    621648        if (parent != null) {
     
    636663        // Ignore parameter as we do not want to operate always on real selection here, especially in draw mode
    637664        Collection<? extends IPrimitive> newSel = OsmDataManager.getInstance().getInProgressISelection();
    638         int newSelSize = newSel.size();
     665
     666        // Temporarily disable listening to primitive mouse hover events while we have a selection as that takes priority
     667        if (PROP_PREVIEW_ON_HOVER.get() && PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get()) {
     668            if (newSel.isEmpty()) {
     669                MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
     670            } else {
     671                MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     672            }
     673        }
     674
     675        updateUi(newSel);
     676    }
     677
     678    @Override
     679    public void primitiveHovered(PrimitiveHoverEvent e) {
     680        Collection<? extends IPrimitive> selection = OsmDataManager.getInstance().getInProgressISelection();
     681        if (PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get() && !selection.isEmpty())
     682            return;
     683
     684        if (e.getHoveredPrimitive() != null) {
     685            updateUi(e.getHoveredPrimitive());
     686        } else {
     687            updateUi(selection);
     688        }
     689    }
     690
     691    private void autoresizeTagTable() {
     692        if (PROP_AUTORESIZE_TAGS_TABLE.get()) {
     693            // resize table's columns to fit content
     694            TableHelper.computeColumnsWidth(tagTable);
     695        }
     696    }
     697
     698    private void updateUi(IPrimitive primitive) {
     699        updateUi(primitive == null ? Collections.emptyList() :
     700                                     Collections.singletonList(primitive));
     701    }
     702
     703    private void updateUi(Collection<? extends IPrimitive> primitives) {
    639704        IRelation<?> selectedRelation = null;
    640705        String selectedTag = editHelper.getChangedKey(); // select last added or last edited key by default
     
    645710            selectedRelation = (IRelation<?>) membershipData.getValueAt(membershipTable.getSelectedRow(), 0);
    646711        }
     712
     713        updateTagTableData(primitives);
     714        updateMembershipTableData(primitives);
     715
     716        updateMembershipTableVisibility();
     717        updateActionsEnabledState();
     718        updateTagTableVisibility(primitives);
     719
     720        setupTaginfoNationalActions(primitives);
     721        autoresizeTagTable();
     722
     723        int selectedIndex;
     724        if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
     725            tagTable.changeSelection(selectedIndex, 0, false, false);
     726        } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
     727            membershipTable.changeSelection(selectedIndex, 0, false, false);
     728        } else if (tagData.getRowCount() > 0) {
     729            tagTable.changeSelection(0, 0, false, false);
     730        } else if (membershipData.getRowCount() > 0) {
     731            membershipTable.changeSelection(0, 0, false, false);
     732        }
     733
     734        updateTitle(primitives);
     735    }
     736
     737    private void updateTagTableData(Collection<? extends IPrimitive> primitives) {
     738        int newSelSize = primitives.size();
    647739
    648740        // re-load tag data
     
    654746        valueCount.clear();
    655747        Set<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
    656         for (IPrimitive osm : newSel) {
     748        for (IPrimitive osm : primitives) {
    657749            types.add(TaggingPresetType.forPrimitive(osm));
    658750            osm.visitKeys((p, key, value) -> {
     
    680772        }
    681773
     774        presets.updatePresets(types, tags, presetHandler);
     775    }
     776
     777    private void updateMembershipTableData(Collection<? extends IPrimitive> primitives) {
    682778        membershipData.setRowCount(0);
    683779
    684780        Map<IRelation<?>, MemberInfo> roles = new HashMap<>();
    685         for (IPrimitive primitive: newSel) {
    686             for (IPrimitive ref: primitive.getReferrers(true)) {
     781        for (IPrimitive primitive : primitives) {
     782            for (IPrimitive ref : primitive.getReferrers(true)) {
    687783                if (ref instanceof IRelation && !ref.isIncomplete() && !ref.isDeleted()) {
    688784                    IRelation<?> r = (IRelation<?>) ref;
    689                     MemberInfo mi = roles.computeIfAbsent(r, ignore -> new MemberInfo(newSel));
     785                    MemberInfo mi = roles.computeIfAbsent(r, ignore -> new MemberInfo(primitives));
    690786                    int i = 1;
    691787                    for (IRelationMember<?> m : r.getMembers()) {
     
    708804            membershipData.addRow(new Object[]{r, roles.get(r)});
    709805        }
    710 
    711         presets.updatePresets(types, tags, presetHandler);
    712 
     806    }
     807
     808    private void updateMembershipTableVisibility() {
    713809        membershipTable.getTableHeader().setVisible(membershipData.getRowCount() > 0);
    714810        membershipTable.setVisible(membershipData.getRowCount() > 0);
    715 
    716         OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
    717         boolean isReadOnly = ds != null && ds.isLocked();
    718         boolean hasSelection = !newSel.isEmpty();
     811    }
     812
     813    private void updateTagTableVisibility(Collection<? extends IPrimitive> primitives) {
     814        boolean hasSelection = !primitives.isEmpty();
    719815        boolean hasTags = hasSelection && tagData.getRowCount() > 0;
    720         boolean hasMemberships = hasSelection && membershipData.getRowCount() > 0;
    721         addAction.setEnabled(!isReadOnly && hasSelection);
    722         editAction.setEnabled(!isReadOnly && (hasTags || hasMemberships));
    723         deleteAction.setEnabled(!isReadOnly && (hasTags || hasMemberships));
     816
    724817        tagTable.setVisible(hasTags);
    725818        tagTable.getTableHeader().setVisible(hasTags);
     
    727820        selectSth.setVisible(!hasSelection);
    728821        pluginHook.setVisible(hasSelection);
    729 
    730         setupTaginfoNationalActions(newSel);
    731         autoresizeTagTable();
    732 
    733         int selectedIndex;
    734         if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
    735             tagTable.changeSelection(selectedIndex, 0, false, false);
    736         } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
    737             membershipTable.changeSelection(selectedIndex, 0, false, false);
    738         } else if (hasTags) {
    739             tagTable.changeSelection(0, 0, false, false);
    740         } else if (hasMemberships) {
    741             membershipTable.changeSelection(0, 0, false, false);
    742         }
    743 
     822    }
     823
     824    private void updateActionsEnabledState() {
     825        addAction.updateEnabledState();
     826        editAction.updateEnabledState();
     827        deleteAction.updateEnabledState();
     828    }
     829
     830    private void updateTitle(Collection<? extends IPrimitive> primitives) {
     831        int newSelSize = primitives.size();
    744832        if (tagData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
    745833            if (newSelSize > 1) {
     
    752840        } else {
    753841            setTitle(tr("Tags/Memberships"));
    754         }
    755     }
    756 
    757     private void autoresizeTagTable() {
    758         if (PROP_AUTORESIZE_TAGS_TABLE.get()) {
    759             // resize table's columns to fit content
    760             TableHelper.computeColumnsWidth(tagTable);
    761842        }
    762843    }
     
    804885        // it is time to save history of tags
    805886        updateSelection();
     887
     888        // Listen for active layer visibility change to enable/disable hover preview
     889        // Remove previous listener first (order matters if we are somehow getting a layer change event
     890        // switching from one layer to the same layer)
     891        Layer prevLayer = e.getPreviousDataLayer();
     892        if (prevLayer != null) {
     893            prevLayer.removePropertyChangeListener(this);
     894        }
     895
     896        Layer newLayer = e.getSource().getActiveDataLayer();
     897        if (newLayer != null) {
     898            newLayer.addPropertyChangeListener(this);
     899            if (newLayer.isVisible()) {
     900                MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
     901            } else {
     902                MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     903            }
     904        }
     905    }
     906
     907    @Override
     908    public void propertyChange(PropertyChangeEvent e) {
     909        if (Layer.VISIBLE_PROP.equals(e.getPropertyName())) {
     910            boolean isVisible = (boolean) e.getNewValue();
     911
     912            // Disable hover preview when primitives are invisible
     913            if (isVisible) {
     914                MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
     915            } else {
     916                MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     917            }
     918        }
    806919    }
    807920
     
    12521365            }
    12531366        }
     1367
     1368        @Override
     1369        protected final void updateEnabledState() {
     1370            DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
     1371            setEnabled(ds != null && !ds.isLocked() &&
     1372                    !Utils.isEmpty(OsmDataManager.getInstance().getInProgressSelection()));
     1373        }
    12541374    }
    12551375
     
    14051525        }
    14061526    }
     1527
     1528    private class HoverPreviewPropListener implements ValueChangeListener<Boolean> {
     1529        @Override
     1530        public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
     1531            if (e.getProperty().get() && isDialogShowing()) {
     1532                MainApplication.getMap().mapView.addPrimitiveHoverListener(PropertiesDialog.this);
     1533            } else if (!e.getProperty().get()) {
     1534                MainApplication.getMap().mapView.removePrimitiveHoverListener(PropertiesDialog.this);
     1535            }
     1536        }
     1537    }
    14071538}
  • trunk/src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java

    r17841 r18574  
    3333import org.openstreetmap.josm.gui.NavigatableComponent;
    3434import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
     35import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog;
    3536
    3637import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
     
    9293    private final JCheckBox showLocalizedName = new JCheckBox(tr("Show localized name in selection lists"));
    9394    private final JCheckBox modeless = new JCheckBox(tr("Modeless working (Potlatch style)"));
     95    private final JCheckBox previewPropsOnHover = new JCheckBox(tr("Preview object properties on mouse hover"));
     96    private final JCheckBox previewPrioritizeSelection = new JCheckBox(tr("Prefer showing information for selected objects"));
    9497    private final JCheckBox dynamicButtons = new JCheckBox(tr("Dynamic buttons in side menus"));
    9598    private final JCheckBox isoDates = new JCheckBox(tr("Display ISO dates"));
     
    174177        panel.add(showLocalizedName, GBC.eop().insets(20, 0, 0, 0));
    175178        panel.add(modeless, GBC.eop().insets(20, 0, 0, 0));
     179
     180        previewPropsOnHover.setToolTipText(
     181                tr("Show tags and relation memberships of objects in the properties dialog when hovering over them with the mouse pointer"));
     182        previewPropsOnHover.setSelected(PropertiesDialog.PROP_PREVIEW_ON_HOVER.get());
     183        panel.add(previewPropsOnHover, GBC.eop().insets(20, 0, 0, 0));
     184
     185        previewPrioritizeSelection.setToolTipText(
     186            tr("Always show information for selected objects when something is selected instead of the hovered object"));
     187        previewPrioritizeSelection.setSelected(PropertiesDialog.PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get());
     188        panel.add(previewPrioritizeSelection, GBC.eop().insets(40, 0, 0, 0));
     189        previewPropsOnHover.addActionListener(l -> previewPrioritizeSelection.setEnabled(previewPropsOnHover.isSelected()));
    176190
    177191        dynamicButtons.setToolTipText(tr("Display buttons in right side menus only when mouse is inside the element"));
     
    239253        Config.getPref().putBoolean("osm-primitives.localize-name", showLocalizedName.isSelected());
    240254        MapFrame.MODELESS.put(modeless.isSelected());
     255        PropertiesDialog.PROP_PREVIEW_ON_HOVER.put(previewPropsOnHover.isSelected());
     256        PropertiesDialog.PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.put(previewPrioritizeSelection.isSelected());
    241257        Config.getPref().putBoolean(ToggleDialog.PROP_DYNAMIC_BUTTONS.getKey(), dynamicButtons.isSelected());
    242258        Config.getPref().putBoolean(DateUtils.PROP_ISO_DATES.getKey(), isoDates.isSelected());
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java

    r18321 r18574  
    468468     */
    469469    public boolean isShowable() {
    470         return data.stream().anyMatch(i -> !(i instanceof Optional || i instanceof Space || i instanceof Key));
     470        // Not using streams makes this method effectively allocation free and uses ~40% fewer CPU cycles.
     471        for (TaggingPresetItem i : data) {
     472            if (!(i instanceof Optional || i instanceof Space || i instanceof Key)) {
     473                return true;
     474            }
     475        }
     476        return false;
    471477    }
    472478
Note: See TracChangeset for help on using the changeset viewer.