diff options
author | Sven Göthel <[email protected]> | 2024-01-07 04:56:28 +0100 |
---|---|---|
committer | Sven Göthel <[email protected]> | 2024-01-07 04:56:28 +0100 |
commit | fa973b03fc1d6af5696cee27e1824c45da3150b4 (patch) | |
tree | 68a2f17d0063c0b867fde5283274414907eaa3b4 /src/graphui/classes/com/jogamp | |
parent | cf0c60eaabd7334c0ae2099a1f999032cddf14dd (diff) |
GraphUI Shape: Enable Tooltip (currently text only) to be displayed after delay w/o mouse-move (1s)
For efficiency, all Tooltip instances is hooked to Scene via Shape as well as its
singleton pop-up HUD tip after delay and no mouse move.
TooltipText is a simple text Button implementation,
but other more fancy HUD tips can be implemented.
Shape adds
- 'public Tooltip setToolTip(final CharSequence text, final Font font, final float scaleY, final Scene scene)'
Demoed within MediaPlayer widget.
Diffstat (limited to 'src/graphui/classes/com/jogamp')
5 files changed, 289 insertions, 6 deletions
diff --git a/src/graphui/classes/com/jogamp/graph/ui/Scene.java b/src/graphui/classes/com/jogamp/graph/ui/Scene.java index 6d3f77971..c71060db4 100644 --- a/src/graphui/classes/com/jogamp/graph/ui/Scene.java +++ b/src/graphui/classes/com/jogamp/graph/ui/Scene.java @@ -36,6 +36,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; import com.jogamp.opengl.FPSCounter; import com.jogamp.opengl.GL; @@ -132,6 +133,8 @@ public final class Scene implements Container, GLEventListener { private static final boolean DEBUG = false; private final List<Shape> shapes = new CopyOnWriteArrayList<Shape>(); + /* pp */ final List<Tooltip> toolTips = new CopyOnWriteArrayList<Tooltip>(); + private final AtomicReference<GraphShape> toolTipHUD = new AtomicReference<GraphShape>(); private boolean doFrustumCulling = false; @@ -500,6 +503,15 @@ public final class Scene implements Container, GLEventListener { displayedOnce = true; syncDisplayedOnce.notifyAll(); } + if( null == toolTipHUD.get() ) { + final GraphShape[] t = { null }; + for(final Tooltip tt : toolTips ) { + if( tt.tick() && forOne(pmv, tt.tool, () -> { t[0] = tt.createTip(pmv); }) ) { + toolTipHUD.set( t[0] ); + break; // done + } + } + } } private void displayGLSelect(final GLAutoDrawable drawable, final Object[] shapes) { @@ -1042,6 +1054,7 @@ public final class Scene implements Container, GLEventListener { private final class SBCGestureListener implements GestureHandler.GestureListener { @Override public void gestureDetected(final GestureEvent gh) { + clearToolTips(); if( null != activeShape ) { // gesture .. delegate to active shape! final InputEvent orig = gh.getTrigger(); @@ -1069,7 +1082,7 @@ public final class Scene implements Container, GLEventListener { * @param glWinX in GL window coordinates, origin bottom-left * @param glWinY in GL window coordinates, origin bottom-left */ - private final boolean dispatchMouseEventPickShape(final MouseEvent e, final int glWinX, final int glWinY) { + private final Shape dispatchMouseEventPickShape(final MouseEvent e, final int glWinX, final int glWinY) { final PMVMatrix4f pmv = new PMVMatrix4f(); final Vec3f objPos = new Vec3f(); final Shape shape = pickShape(pmv, glWinX, glWinY, objPos, (final Shape s) -> { @@ -1077,10 +1090,10 @@ public final class Scene implements Container, GLEventListener { }); if( null != shape ) { setActiveShape(shape); - return true; + return shape; } else { releaseActiveShape(); - return false; + return null; } } /** @@ -1109,6 +1122,7 @@ public final class Scene implements Container, GLEventListener { @Override public void mousePressed(final MouseEvent e) { + clearToolTips(); if( -1 == lId || e.getPointerId(0) == lId ) { lx = e.getX(); ly = e.getY(); @@ -1122,6 +1136,7 @@ public final class Scene implements Container, GLEventListener { @Override public void mouseReleased(final MouseEvent e) { + clearToolTips(); // flip to GL window coordinates, origin bottom-left final int glWinX = e.getX(); final int glWinY = getHeight() - e.getY() - 1; @@ -1137,6 +1152,7 @@ public final class Scene implements Container, GLEventListener { @Override public void mouseClicked(final MouseEvent e) { + clearToolTips(); // flip to GL window coordinates final int glWinX = e.getX(); final int glWinY = getHeight() - e.getY() - 1; @@ -1153,6 +1169,7 @@ public final class Scene implements Container, GLEventListener { @Override public void mouseDragged(final MouseEvent e) { + clearToolTips(); // drag activeShape, if no gesture-activity, only on 1st pointer if( null != activeShape && activeShape.isInteractive() && !pinchToZoomGesture.isWithinGesture() && e.getPointerId(0) == lId ) { lx = e.getX(); @@ -1168,6 +1185,7 @@ public final class Scene implements Container, GLEventListener { @Override public void mouseWheelMoved(final MouseEvent e) { + clearToolTips(); // flip to GL window coordinates final int glWinX = lx; final int glWinY = getHeight() - ly - 1; @@ -1176,6 +1194,7 @@ public final class Scene implements Container, GLEventListener { @Override public void mouseMoved(final MouseEvent e) { + clearToolTips(); if( -1 == lId || e.getPointerId(0) == lId ) { lx = e.getX(); ly = e.getY(); @@ -1183,12 +1202,19 @@ public final class Scene implements Container, GLEventListener { } final int glWinX = lx; final int glWinY = getHeight() - ly - 1; - mouseOver = dispatchMouseEventPickShape(e, glWinX, glWinY); + final Shape s = dispatchMouseEventPickShape(e, glWinX, glWinY); + if( null != s ) { + mouseOver = true; + s.startToolTip(); + } else { + mouseOver = false; + } } @Override public void mouseEntered(final MouseEvent e) { } @Override public void mouseExited(final MouseEvent e) { + clearToolTips(); releaseActiveShape(); clear(); } @@ -1209,6 +1235,19 @@ public final class Scene implements Container, GLEventListener { } } + private void clearToolTips() { + final Shape s = toolTipHUD.getAndSet(null); + if( null != s ) { + invoke(false, (final GLAutoDrawable drawable) -> { + removeShape(drawable.getGL().getGL2ES2(), s); + return true; + }); + } + for(final Tooltip tt : toolTips) { + tt.stop(); + } + } + /** * Return a formatted status string containing avg fps and avg frame duration. * @param glad GLAutoDrawable instance for FPSCounter, its chosen GLCapabilities and its GL's swap-interval diff --git a/src/graphui/classes/com/jogamp/graph/ui/Shape.java b/src/graphui/classes/com/jogamp/graph/ui/Shape.java index 65a989ebd..3e22cba78 100644 --- a/src/graphui/classes/com/jogamp/graph/ui/Shape.java +++ b/src/graphui/classes/com/jogamp/graph/ui/Shape.java @@ -34,7 +34,9 @@ import com.jogamp.nativewindow.NativeWindowException; import com.jogamp.opengl.GL2ES2; import com.jogamp.opengl.GLProfile; import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.graph.curve.Region; import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.font.Font; import com.jogamp.graph.ui.layout.Padding; import com.jogamp.math.FloatUtil; import com.jogamp.math.Matrix4f; @@ -203,6 +205,9 @@ public abstract class Shape { private static final float resize_sxy_min = 1f/200f; // 1/2% - TODO: Maybe customizable? private static final float resize_section = 1f/5f; // resize action in a corner + private static final long toolTipdelayMS = 1000; + private volatile Tooltip tooltip = null; + /** * Create a generic UI {@link Shape} */ @@ -304,6 +309,7 @@ public abstract class Shape { */ public final void clear(final GL2ES2 gl, final RegionRenderer renderer) { synchronized ( dirtySync ) { + stopToolTip(); clearImpl0(gl, renderer); resetState(); } @@ -330,6 +336,7 @@ public abstract class Shape { * @param renderer {@link RegionRenderer} used to release GPU resources */ public final void destroy(final GL2ES2 gl, final RegionRenderer renderer) { + removeToolTip(); destroyImpl0(gl, renderer); resetState(); } @@ -1293,8 +1300,16 @@ public abstract class Shape { if( isActivable() ) { this.zOffset = zOffset; setIO(IO_ACTIVE, v); + final Tooltip tt = tooltip; if( !v ) { releaseInteraction(); + if( null != tt ) { + tt.stop(); + } + } else { + if( null != tt ) { + tt.start(); + } } if( DEBUG ) { System.err.println("XXX "+(v?" Active":"DeActive")+" "+this); @@ -1322,6 +1337,41 @@ public abstract class Shape { return position.z() * getScale().z() + zOffset; } + /** Set's a {@link TooltipText} for this shape using default 1s delay */ + public Tooltip setToolTip(final CharSequence text, final Font font, final float scaleY, final Scene scene) { + final Tooltip oldTT = tooltip; + tooltip = null; + final Tooltip newTT = new TooltipText(text, font, scaleY, this, toolTipdelayMS, scene, Region.VBAA_RENDERING_BIT); + if( null != oldTT ) { + oldTT.stop(); + oldTT.scene.toolTips.remove(oldTT); + } + tooltip = newTT; + newTT.scene.toolTips.add(newTT); + return newTT; + } + public void removeToolTip() { + final Tooltip tt = tooltip; + tooltip = null; + if( null != tt ) { + tt.stop(); + tt.scene.toolTips.remove(tt); + } + } + private void stopToolTip() { + final Tooltip tt = tooltip; + if( null != tt ) { + tt.stop(); + } + } + /* pp */ void startToolTip() { + final Tooltip tt = tooltip; + if( null != tt ) { + tt.start(); + } + } + public Tooltip getTooltip() { return tooltip; } + /** * Set whether this shape is interactive, * i.e. any user interaction like diff --git a/src/graphui/classes/com/jogamp/graph/ui/Tooltip.java b/src/graphui/classes/com/jogamp/graph/ui/Tooltip.java new file mode 100644 index 000000000..6c1603800 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/Tooltip.java @@ -0,0 +1,84 @@ +/** + * Copyright 2023-2024 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import com.jogamp.common.os.Clock; +import com.jogamp.math.util.PMVMatrix4f; + +/** A HUD tooltip for {@link Shape}, see {@link Shape#setToolTip(CharSequence, com.jogamp.graph.font.Font, float, Scene)}. */ +public abstract class Tooltip { + + /** Shape belonging to this tooltip's tool. */ + protected final Shape tool; + protected final long delayMS; + protected final Scene scene; + /** Delay t1, time to show tooltip, i.e. t0 + delayMS */ + protected volatile long delayT1; + + protected Tooltip(final Shape tool, final long delayMS, final Scene scene) { + this.tool = tool; + this.delayMS = delayMS; + this.scene = scene; + this.delayT1 = 0; + } + + /** Stops the timer. */ + public void stop() { + this.delayT1 = 0; + } + + /** Starts the timer. */ + public void start() { + final long t0 = Clock.currentMillis(); + this.delayT1 = t0 + delayMS; + } + + /** + * Send tick to this tooltip + * @return true if timer has been reached to {@link #createTip(PMVMatrix4f)}, otherwise false + */ + public boolean tick() { + if( 0 == delayT1 ) { + return false; + } + final long t1 = Clock.currentMillis(); + if( t1 < delayT1 ) { + return false; + } + this.delayT1 = 0; + return true; + } + + /** + * Create a new HUD tip shape + * @param pmv {@link PMVMatrix4f}, which shall be properly initialized, e.g. via {@link Scene#setupMatrix(PMVMatrix4f)} + * @return newly created HUD tip shape + */ + public abstract GraphShape createTip(final PMVMatrix4f pmv); + +}
\ No newline at end of file diff --git a/src/graphui/classes/com/jogamp/graph/ui/TooltipText.java b/src/graphui/classes/com/jogamp/graph/ui/TooltipText.java new file mode 100644 index 000000000..6c2912bf4 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/TooltipText.java @@ -0,0 +1,99 @@ +/** + * Copyright 2023-2024 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.GLRegion; +import com.jogamp.graph.font.Font; +import com.jogamp.graph.ui.shapes.Button; +import com.jogamp.math.geom.AABBox; +import com.jogamp.math.geom.plane.AffineTransform; +import com.jogamp.math.util.PMVMatrix4f; +import com.jogamp.opengl.GLProfile; + +/** A HUD text {@link Tooltip} for {@link Shape}, see {@link Shape#setToolTip(CharSequence, com.jogamp.graph.font.Font, float, Scene)}. */ +public class TooltipText extends Tooltip { + /** Text of this tooltip */ + final private CharSequence tipText; + /** Font of this tooltip */ + final private Font tipFont; + final private float scaleY; + final private int renderModes; + /** + * Ctor of {@link TooltipText}. + * @param scene the {@link Scene} to be attached to while pressed + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. + */ + /* pp */ TooltipText(final CharSequence tipText, final Font tipFont, final float scaleY, final Shape tool, final long delayMS, final Scene scene, final int renderModes) { + super(tool, delayMS, scene); + this.tipText = tipText; + this.tipFont = tipFont; + this.scaleY = scaleY; + this.renderModes = renderModes; + } + + @Override + public GraphShape createTip(final PMVMatrix4f pmv) { + this.delayT1 = 0; + final float zEps = scene.getZEpsilon(16); + + // Precompute text-box size .. guessing pixelSize + final AffineTransform tempT1 = new AffineTransform(); + final AffineTransform tempT2 = new AffineTransform(); + final AABBox tipBox_em = tipFont.getGlyphBounds(tipText, tempT1, tempT2); + + // final AABBox toolAABox = scene.getBounds(new PMVMatrix4f(), tool); + final AABBox toolAABox = tool.getBounds().transform(pmv.getMv(), new AABBox()); + + final float h = toolAABox.getHeight() * scaleY; + final float w = tipBox_em.getWidth() / tipBox_em.getHeight() * h; + + final AABBox sceneAABox = scene.getBounds(); + final float xpos, ypos; + if( toolAABox.getCenter().x()-w/2 < sceneAABox.getLow().x() ) { + xpos = sceneAABox.getLow().x(); + } else { + xpos = toolAABox.getCenter().x()-w/2; + } + if( toolAABox.getHigh().y() > sceneAABox.getHigh().y() - h ) { + ypos = sceneAABox.getHigh().y() - h; + } else { + ypos = toolAABox.getHigh().y(); + } + final Button ntip = (Button) new Button(renderModes, tipFont, tipText, w, h, zEps) + .setPerp() + .moveTo(xpos, ypos, 100*zEps) + .setColor(1, 1, 0, 0.80f) + // .setBorder(0.05f).setBorderColor(0, 0, 0, 1) + .setInteractive(false); + ntip.setLabelColor(0, 0, 0); + ntip.setSpacing(0.10f, 0.10f); + scene.addShape(ntip); + return ntip; + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java b/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java index 75b07b7c8..0ceb9aaa9 100644 --- a/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java +++ b/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java @@ -1,5 +1,5 @@ /** - * Copyright 2010-2023 JogAmp Community. All rights reserved. + * Copyright 2010-2024 JogAmp Community. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: @@ -41,6 +41,7 @@ import com.jogamp.graph.font.FontFactory; import com.jogamp.graph.ui.Group; import com.jogamp.graph.ui.Scene; import com.jogamp.graph.ui.Shape; +import com.jogamp.graph.ui.TooltipText; import com.jogamp.graph.ui.layout.Alignment; import com.jogamp.graph.ui.layout.BoxLayout; import com.jogamp.graph.ui.layout.Gap; @@ -160,7 +161,8 @@ public class MediaPlayer extends Widget { System.err.println(mp.toString()); for(final GLMediaPlayer.Chapter c : mp.getChapters()) { System.err.println(c); - ctrlSlider.addMark(c.start, new Vec4f(0.9f, 0.9f, 0.9f, 0.5f)); + final Shape mark = ctrlSlider.addMark(c.start, new Vec4f(0.9f, 0.9f, 0.9f, 0.5f)); + mark.setToolTip(c.title+"\n"+PTS.millisToTimeStr(c.start, false), fontInfo, 5, scene); } } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Play) ) { playButton.setToggle(true); @@ -289,6 +291,7 @@ public class MediaPlayer extends Widget { ctrlBlend.setColor(0, 0, 0, alphaBlend); this.addShape( ctrlBlend.setVisible(false) ); + final float toolTipScaleY = 0.6f; ctrlGroup = new Group(new GridLayout(ctrlCellWidth, ctrlCellHeight, Alignment.FillCenter, Gap.None, 1)); ctrlGroup.setName("ctrlGroup").setInteractive(false); ctrlGroup.setPaddding(new Padding(0, borderSzS, 0, borderSzS)); @@ -303,6 +306,7 @@ public class MediaPlayer extends Widget { }); playButton.setToggle(true); // on == play ctrlGroup.addShape(playButton); + playButton.setToolTip("Play", fontInfo, toolTipScaleY, scene); } { // 2 final Button button = new Button(renderModes, fontSymbols, @@ -313,6 +317,7 @@ public class MediaPlayer extends Widget { mPlayer.seek(0); }); ctrlGroup.addShape(button); + button.setToolTip("Back", fontInfo, toolTipScaleY, scene); } { // 3 final Button button = new Button(renderModes, fontSymbols, @@ -323,6 +328,7 @@ public class MediaPlayer extends Widget { mPlayer.setPlaySpeed(mPlayer.getPlaySpeed() - 0.5f); }); ctrlGroup.addShape(button); + button.setToolTip("Fast-Rewind", fontInfo, toolTipScaleY, scene); } { // 4 final Button button = new Button(renderModes, fontSymbols, @@ -333,6 +339,7 @@ public class MediaPlayer extends Widget { mPlayer.setPlaySpeed(mPlayer.getPlaySpeed() + 0.5f); }); ctrlGroup.addShape(button); + button.setToolTip("Fast-Forward", fontInfo, toolTipScaleY, scene); } { // 5 final Button button = new Button(renderModes, fontSymbols, @@ -351,6 +358,7 @@ public class MediaPlayer extends Widget { mPlayer.seek(pts1); } } ); ctrlGroup.addShape(button); + button.setToolTip("Replay 5", fontInfo, toolTipScaleY, scene); } { // 6 final Button button = new Button(renderModes, fontSymbols, @@ -369,6 +377,7 @@ public class MediaPlayer extends Widget { mPlayer.seek(pts1); } } ); ctrlGroup.addShape(button); + button.setToolTip("Forward 5", fontInfo, toolTipScaleY, scene); } { // 7 final Button button = new Button(renderModes, fontSymbols, @@ -392,6 +401,7 @@ public class MediaPlayer extends Widget { } } ); button.setToggle( !mPlayer.isAudioMuted() ); // on == volume ctrlGroup.addShape(button); + button.setToolTip("Volume", fontInfo, toolTipScaleY, scene); } { // 8 ctrlGroup.addShape(timeLabel); @@ -457,6 +467,7 @@ public class MediaPlayer extends Widget { }); button.setToggle( false ); // on == zoom ctrlGroup.addShape(button); + button.setToolTip("Zoom", fontInfo, toolTipScaleY, scene); } for(final Shape cs : customCtrls ) { ctrlGroup.addShape(cs); |