2020import java .awt .Rectangle ;
2121import java .awt .event .ComponentEvent ;
2222import java .awt .event .ComponentListener ;
23+ import java .awt .event .MouseAdapter ;
2324import java .awt .event .MouseEvent ;
24- import java .awt .event .MouseListener ;
2525import java .util .Collection ;
2626import java .util .Collections ;
2727import java .util .HashMap ;
3232import java .util .Optional ;
3333import java .util .Set ;
3434import java .util .TreeMap ;
35- import java .util .function .BiConsumer ;
35+ import java .util .function .Consumer ;
3636import java .util .function .Predicate ;
3737
3838public class MarkableScrollPane extends JScrollPane {
@@ -50,7 +50,7 @@ public class MarkableScrollPane extends JScrollPane {
5050
5151 @ Nullable
5252 private PaintState paintState ;
53- private MouseListener viewMouseListener ;
53+ private MouseAdapter viewMouseAdapter ;
5454
5555 /**
5656 * Constructs a scroll pane with no view,
@@ -123,71 +123,104 @@ public void componentHidden(ComponentEvent e) {
123123 public void setViewportView (Component view ) {
124124 final Component oldView = this .getViewport ().getView ();
125125 if (oldView != null ) {
126- oldView .removeMouseListener (this .viewMouseListener );
126+ oldView .removeMouseListener (this .viewMouseAdapter );
127+ oldView .removeMouseMotionListener (this .viewMouseAdapter );
127128 }
128129
129130 super .setViewportView (view );
130131
131- this .viewMouseListener = new MouseListener () {
132- private void tryMarkerListeners (MouseEvent e , BiConsumer <MouseListener , MouseEvent > eventAction ) {
133- if (MarkableScrollPane .this .paintState != null ) {
134- final Point relativePos = GuiUtil
135- .getRelativePos (MarkableScrollPane .this , e .getXOnScreen (), e .getYOnScreen ());
136- MarkableScrollPane .this .paintState
137- .findSpanContaining (
138- relativePos .x , relativePos .y ,
139- span -> span .getMarker ().mouseListener .isPresent ()
140- )
141- .map (span -> span .getMarker ().mouseListener .orElseThrow ())
142- .ifPresent (listener -> eventAction .accept (listener , e ));
132+ this .viewMouseAdapter = new MouseAdapter () {
133+ static MouseEvent withId (MouseEvent e , int id ) {
134+ return new MouseEvent (
135+ (Component ) e .getSource (), id , e .getWhen (), e .getModifiersEx (),
136+ e .getX (), e .getY (), e .getXOnScreen (), e .getYOnScreen (),
137+ e .getClickCount (), e .isPopupTrigger (), e .getButton ()
138+ );
139+ }
140+
141+ @ Nullable
142+ MarkerListener lastEntered ;
143+
144+ Optional <MarkerListener > findMarkerListener (MouseEvent e ) {
145+ if (MarkableScrollPane .this .paintState == null ) {
146+ return Optional .empty ();
147+ } else {
148+ final Point relativePos =
149+ GuiUtil .getRelativePos (MarkableScrollPane .this , e .getXOnScreen (), e .getYOnScreen ());
150+ return MarkableScrollPane .this .paintState
151+ .findSpanContaining (
152+ relativePos .x , relativePos .y ,
153+ span -> span .getMarker ().listener .isPresent ()
154+ )
155+ .map (span -> span .getMarker ().listener .orElseThrow ());
143156 }
144157 }
145158
146- @ Override
147- public void mouseClicked (MouseEvent e ) {
148- this .tryMarkerListeners (e , MouseListener ::mouseClicked );
159+ void tryMarkerListeners (MouseEvent e , Consumer <MarkerListener > listen ) {
160+ this .findMarkerListener (e ).ifPresent (listen );
149161 }
150162
151163 @ Override
152- public void mousePressed (MouseEvent e ) {
153- this .tryMarkerListeners (e , MouseListener :: mousePressed );
164+ public void mouseClicked (MouseEvent e ) {
165+ this .tryMarkerListeners (e , MarkerListener :: mouseClicked );
154166 }
155167
156168 @ Override
157- public void mouseReleased (MouseEvent e ) {
158- this .tryMarkerListeners ( e , MouseListener :: mouseReleased );
169+ public void mouseExited (MouseEvent e ) {
170+ this .mouseExitedImpl ( );
159171 }
160172
161173 @ Override
162- public void mouseEntered (MouseEvent e ) {
163- this .tryMarkerListeners (e , MouseListener ::mouseEntered );
174+ public void mouseMoved (MouseEvent e ) {
175+ this .tryMarkerListeners (e , MarkerListener ::mouseMoved );
176+
177+ this .findMarkerListener (e ).ifPresentOrElse (
178+ listener -> {
179+ if (listener != this .lastEntered ) {
180+ if (this .lastEntered == null ) {
181+ listener .mouseEntered ();
182+ } else {
183+ listener .mouseTransferred ();
184+ }
185+
186+ this .lastEntered = listener ;
187+ }
188+ },
189+ this ::mouseExitedImpl
190+ );
164191 }
165192
166- @ Override
167- public void mouseExited (MouseEvent e ) {
168- this .tryMarkerListeners (e , MouseListener ::mouseExited );
193+ private void mouseExitedImpl () {
194+ if (this .lastEntered != null ) {
195+ this .lastEntered .mouseExited ();
196+ this .lastEntered = null ;
197+ }
169198 }
170199 };
171200
172201 // add the listener to the view because this doesn't receive clicks within the view
173- view .addMouseListener (this .viewMouseListener );
202+ view .addMouseListener (this .viewMouseAdapter );
203+ view .addMouseMotionListener (this .viewMouseAdapter );
174204 }
175205
176206 /**
177207 * Adds a marker with passed {@code color} at the given {@code pos}.
178208 *
179- * @param pos the vertical center of the marker within the space of this scroll pane's view
180- * @param color the color of the marker
181- * @param priority the priority of the marker; if there are multiple markers at the same position, only up to
182- * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered
183- * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)}
209+ * @param pos the vertical center of the marker within the space of this scroll pane's view
210+ * @param color the color of the marker
211+ * @param priority the priority of the marker; if there are multiple markers at the same position, only up to
212+ * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered
213+ * @param listener a listener for events within the marker; may be null
214+ *
215+ * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)}
184216 */
185- public Object addMarker (int pos , Color color , int priority , @ Nullable MouseListener mouseListener ) {
217+ public Object addMarker (int pos , Color color , int priority , @ Nullable MarkerListener listener ) {
186218 if (pos < 0 ) {
187219 throw new IllegalArgumentException ("pos must not be negative!" );
188220 }
189221
190- final Marker marker = new Marker (color , priority , Optional .ofNullable (mouseListener ));
222+ final Marker marker = new Marker (color , priority , Optional .ofNullable (listener ));
223+
191224 this .markersByPos .put (pos , marker );
192225
193226 if (this .paintState != null ) {
@@ -200,7 +233,7 @@ public Object addMarker(int pos, Color color, int priority, @Nullable MouseListe
200233 /**
201234 * Removes the passed {@code marker} if it belongs to this scroll pane.
202235 *
203- * @param marker an object previously returned by {@link #addMarker(int, Color, int, MouseListener )}
236+ * @param marker an object previously returned by {@link #addMarker(int, Color, int, MarkerListener )}
204237 */
205238 public void removeMarker (Object marker ) {
206239 if (marker instanceof Marker removing ) {
@@ -363,8 +396,7 @@ boolean areaContains(int x, int y) {
363396 }
364397 }
365398
366- private record Marker (Color color , int priority , Optional <MouseListener > mouseListener )
367- implements Comparable <Marker > {
399+ private record Marker (Color color , int priority , Optional <MarkerListener > listener ) implements Comparable <Marker > {
368400 @ Override
369401 public int compareTo (@ Nonnull Marker other ) {
370402 return other .priority - this .priority ;
@@ -422,4 +454,19 @@ void paint(Graphics graphics) {
422454 }
423455 }
424456 }
457+
458+ public interface MarkerListener {
459+ void mouseClicked ();
460+
461+ void mouseExited ();
462+
463+ void mouseEntered ();
464+
465+ /**
466+ * Called when the mouse moves between two adjacent markers.
467+ */
468+ void mouseTransferred ();
469+
470+ void mouseMoved ();
471+ }
425472}
0 commit comments