diff options
author | Sven Gothel <[email protected]> | 2023-12-19 17:40:17 +0100 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2023-12-19 17:40:17 +0100 |
commit | feb3d34be097bcbef5ebc40342b405a832ac581f (patch) | |
tree | 09eb2f8dbb2173d9d90438397819739c3fc3b994 /src/graphui/classes/com | |
parent | eb99bfc27f9f49387cbb08471debcd4d61e4f745 (diff) |
Bug 805: GraphUI: Add Widget 'marker' (a Group), derived by {MediaUI01 -> MediaPlayer} and new RangeSlider
- A widget specifies specific UI semantics including individual controls.
- Being a {@link Group}, implementations provide shape(s) and its instance can be added to the user's scene.
- Due to the specific nature of widgets,
individual controls/listener may be provided with semantic values.
+++
MediaPlayer exposes a RangeSlider for current position (view and control).
Diffstat (limited to 'src/graphui/classes/com')
-rw-r--r-- | src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java (renamed from src/graphui/classes/com/jogamp/graph/ui/widgets/MediaUI01.java) | 272 | ||||
-rw-r--r-- | src/graphui/classes/com/jogamp/graph/ui/widgets/RangeSlider.java | 252 | ||||
-rw-r--r-- | src/graphui/classes/com/jogamp/graph/ui/widgets/Widget.java | 65 |
3 files changed, 476 insertions, 113 deletions
diff --git a/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaUI01.java b/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java index a37d73539..4cef454ea 100644 --- a/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaUI01.java +++ b/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java @@ -50,6 +50,7 @@ import com.jogamp.graph.ui.shapes.Button; import com.jogamp.graph.ui.shapes.Label; import com.jogamp.graph.ui.shapes.MediaButton; import com.jogamp.graph.ui.shapes.Rectangle; +import com.jogamp.graph.ui.widgets.RangeSlider.SliderListener; import com.jogamp.math.Vec2f; import com.jogamp.math.Vec3f; import com.jogamp.math.Vec4f; @@ -68,14 +69,14 @@ import com.jogamp.opengl.util.texture.TextureSequence; import com.jogamp.opengl.util.texture.TextureSequence.TextureFrame; /** - * UI Media player factory, embedding a {@link MediaButton} and its controls within a {@link Group}. - * @see MediaUI01#create(Scene, GLMediaPlayer, int, Uri, int, float, boolean, float, List) + * Media player {@link Widget}, embedding a {@link MediaButton} and its controls. + * @see #MediaPlayer(int, Scene, GLMediaPlayer, Uri, int, float, boolean, float, List) */ -public class MediaUI01 { - public static final int MediaTexUnitMediaPlayer = 1; +public class MediaPlayer extends Widget { + public static final int TexUnit = 1; /** Default texture count, value {@value}, same as {@link GLMediaPlayer#TEXTURE_COUNT_DEFAULT}. */ - public static final int MediaTexCount = GLMediaPlayer.TEXTURE_COUNT_DEFAULT; + public static final int TexCount = GLMediaPlayer.TEXTURE_COUNT_DEFAULT; public static final Vec2f FixedSymSize = new Vec2f(0.0f, 1.0f); public static final Vec2f SymSpacing = new Vec2f(0f, 0.2f); @@ -83,30 +84,11 @@ public class MediaUI01 { public static final float CtrlButtonHeight = 1f; public static final Vec4f CtrlCellCol = new Vec4f(0, 0, 0, 0); - /** Returns the used info font or null if n/a */ - public static Font getInfoFont() { - try { - return FontFactory.get(FontFactory.UBUNTU).getDefault(); - } catch(final IOException ioe) { - ioe.printStackTrace(); - return null; - } - } - /** Returns the used symbols font or null if n/a */ - public static Font getSymbolsFont() { - try { - return FontFactory.get(FontFactory.SYMBOLS).getDefault(); - } catch(final IOException ioe) { - ioe.printStackTrace(); - return null; - } - } - /** - * Returns a {@link Group} containing a {@link MediaButton} and its controls. + * Constructs a {@link MediaPlayer}, i.e. its shapes and controls. + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. * @param scene the used {@link Scene} to query parameter and access rendering loop * @param mPlayer fresh {@link GLMediaPlayer} instance, maybe customized via e.g. {@link GLMediaPlayer#setTextureMinMagFilter(int[])}. - * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. * @param medium {@link Uri} stream source, either a file or network source * @param aid audio-id to start playing, may use {@link GLMediaPlayer#STREAM_ID_AUTO} * @param aratio aspect ratio of the resulting {@link Shape}, usually 16.0f/9.0f or 4.0f/3.0f, which also denotes the width of this shape while using height 1.0. @@ -114,14 +96,16 @@ public class MediaUI01 { * @param zoomSize zoom-size (0..1] for zoom-out control * @param customCtrls optional custom controls, maybe an empty list */ - public static Shape create(final Scene scene, final GLMediaPlayer mPlayer, final int renderModes, - final Uri medium, final int aid, - final float aratio, final boolean letterBox, final float zoomSize, - final List<Shape> customCtrls) + public MediaPlayer(final int renderModes, final Scene scene, final GLMediaPlayer mPlayer, + final Uri medium, final int aid, + final float aratio, final boolean letterBox, final float zoomSize, + final List<Shape> customCtrls) { + super( new BoxLayout(aratio, 1, Alignment.None) ); + final Font fontInfo = getInfoFont(), fontSymbols = getSymbolsFont(); if( null == fontInfo || null == fontSymbols ) { - return new Rectangle(renderModes, aratio, 1, 0.10f); + return; } final float borderSz = 0.01f; final float borderSzS = 0.03f; @@ -137,23 +121,35 @@ public class MediaUI01 { final float ctrlCellWidth = (aratio-2*borderSzS)/ctrlCells; final float ctrlCellHeight = ctrlCellWidth; + final float ctrlSliderHeight = ctrlCellHeight/15f; final Shape[] zoomReplacement = { null }; final Vec3f[] zoomOrigScale = { null }; final Vec3f[] zoomOrigPos = { null }; - final Group container = new Group(new BoxLayout(aratio, 1, Alignment.None)); - container.setName("container"); - container.setBorderColor(borderColor).setBorder(borderSz); - container.setInteractive(true).setFixedARatioResize(true); + this.setName("mp.container"); + this.setBorderColor(borderColor).setBorder(borderSz); + this.setInteractive(true).setFixedARatioResize(true); final MediaButton mButton = new MediaButton(renderModes, aratio, 1, mPlayer); - mButton.setName("mButton").setInteractive(false); + mButton.setName("mp.mButton").setInteractive(false); mButton.setPerp().setPressedColorMod(1f, 1f, 1f, 0.85f); + final RangeSlider ctrlSlider = new RangeSlider(renderModes, aratio, ctrlSliderHeight, 4.0f, 0, 100, 0); + { + final float dy = ( ctrlSlider.getKnobSize() - ctrlSliderHeight ) * 0.5f; + ctrlSlider.setPaddding(new Padding(0, 0, ctrlCellHeight-dy, 0)); + } + ctrlSlider.setName("mp.slider"); + + final Button playButton = new Button(renderModes, fontSymbols, + fontSymbols.getUTF16String("play_arrow"), fontSymbols.getUTF16String("pause"), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); + playButton.setName("mp.play"); + playButton.setSpacing(SymSpacing, FixedSymSize).setPerp().setColor(CtrlCellCol); + // mButton.setBorderColor(borderNormal).setBorder(borderSz); { - mPlayer.setTextureUnit(MediaTexUnitMediaPlayer); + mPlayer.setTextureUnit(TexUnit); mButton.setVerbose(false).addDefaultEventListener().setTextureLetterbox(letterBox); mPlayer.setAudioVolume( 0f ); mPlayer.addEventListener( new GLMediaEventListener() { @@ -166,6 +162,11 @@ public class MediaUI01 { // System.err.println("MediaButton State: "+mp); if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Init) ) { System.err.println(mp.toString()); + ctrlSlider.setMinMax(0, mp.getDuration(), 0); + } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Play) ) { + playButton.setToggle(true); + } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Pause) ) { + playButton.setToggle(false); } if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.EOS) ) { final StreamException err = mp.getStreamException(); @@ -184,8 +185,8 @@ public class MediaUI01 { } } }); - mPlayer.playStream(medium, GLMediaPlayer.STREAM_ID_AUTO, aid, MediaTexCount); - container.addShape(mButton); + mPlayer.playStream(medium, GLMediaPlayer.STREAM_ID_AUTO, aid, TexCount); + this.addShape(mButton); } Group ctrlGroup, infoGroup; @@ -194,7 +195,7 @@ public class MediaUI01 { final Button timeLabel; { muteLabel = new Label(renderModes, fontSymbols, aratio/6f, fontSymbols.getUTF16String("music_off")); // volume_mute, headset_off - muteLabel.setName("muteLabel"); + muteLabel.setName("mp.mute"); { final float sz = aratio/6f; muteLabel.setColor(1, 0, 0, 1); @@ -202,25 +203,25 @@ public class MediaUI01 { muteLabel.setInteractive(false); muteLabel.setVisible( mPlayer.isAudioMuted() ); - container.addShape(muteLabel); + this.addShape(muteLabel); } infoGroup = new Group(new BoxLayout()); - infoGroup.setName("infoGroup").setInteractive(false); - container.addShape( infoGroup.setVisible(false) ); + infoGroup.setName("mp.info").setInteractive(false); + this.addShape( infoGroup.setVisible(false) ); { final float sz = 1/7f; - final Rectangle rect = new Rectangle(renderModes, aratio, sz, sz/2f); - rect.setName("info.blend").setInteractive(false); + final Rectangle rect = new Rectangle(renderModes, aratio, sz, 0); + rect.setName("mp.info.blend").setInteractive(false); rect.setColor(0, 0, 0, alphaBlend); rect.setPaddding(new Padding(0, 0, 1f-sz, 0)); infoGroup.addShape(rect); } { final int lines = 3; - final String text = getInfo(mPlayer, false); + final String text = getInfo(Clock.currentMillis(), mPlayer, false); infoLabel = new Label(renderModes, fontInfo, aratio/40f, text); - infoLabel.setName("infoLabel"); + infoLabel.setName("mp.info.label"); final float szw = aratio/40f; infoLabel.setPaddding(new Padding(0, 0, 1f-szw*lines, szw)); infoLabel.setInteractive(false); @@ -229,8 +230,8 @@ public class MediaUI01 { } { timeLabel = new Button(renderModes, fontInfo, - getMultilineTime(mPlayer), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); - timeLabel.setName("timeLabel"); + getMultilineTime(Clock.currentMillis(), mPlayer), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); + timeLabel.setName("mp.time"); timeLabel.setPerp().setColor(CtrlCellCol); timeLabel.setLabelColor(1, 1, 1); } @@ -240,43 +241,70 @@ public class MediaUI01 { public void display(final GLAutoDrawable drawable) { final GLAnimatorControl anim = drawable.getAnimator(); if( ( timeLabel.isVisible() || infoLabel.isVisible() ) && - mPlayer.getState() != GLMediaPlayer.State.Uninitialized && null != anim ) + mPlayer.getState() == GLMediaPlayer.State.Playing && null != anim ) { final long t1 = anim.getTotalFPSDuration(); - if( t1 - t0 >= 300) { + if( t1 - t0 >= 333) { t0 = t1; - infoLabel.setText(getInfo(mPlayer, false)); - timeLabel.setText(getMultilineTime(mPlayer)); + final int ptsMS = mPlayer.getPTS().get(Clock.currentMillis()); + final int durationMS = mPlayer.getDuration(); + infoLabel.setText(getInfo(ptsMS, durationMS, mPlayer, false)); + timeLabel.setText(getMultilineTime(ptsMS, durationMS)); + ctrlSlider.setValue(ptsMS); } } } } ); + ctrlSlider.onSlider(new SliderListener() { + private void seekPlayer(final int ptsMS) { + final int durationMS = mPlayer.getDuration(); + timeLabel.setText(getMultilineTime(ptsMS, durationMS)); + mPlayer.seek(ptsMS); + } + @Override + public void clicked(final RangeSlider w, final MouseEvent e) { + System.err.println("Clicked "+w.getName()+": "+millisToTimeStr(Math.round(w.getValue()), true)+"ms, "+(w.getValuePct()*100f)+"%"); + seekPlayer( Math.round( w.getValue() ) ); + } + @Override + public void pressed(final RangeSlider w, final MouseEvent e) { + // mPlayer.pause(false); + // seekPlayer( Math.round( w.getValue() ) ); + } + @Override + public void released(final RangeSlider w, final MouseEvent e) { + // seekPlayer( Math.round( w.getValue() ) ); + // mPlayer.resume(); + } + + @Override + public void dragged(final RangeSlider w, final float old_val, final float val, final float old_val_pct, final float val_pct) { + System.err.println("Dragged "+w.getName()+": "+millisToTimeStr(Math.round(val), true)+"ms, "+(val_pct*100f)+"%"); + seekPlayer( Math.round( val ) ); + } + }); + this.addShape( ctrlSlider.setVisible(false) ); - ctrlBlend = new Rectangle(renderModes, aratio, ctrlCellHeight, ctrlCellHeight/2f); + ctrlBlend = new Rectangle(renderModes, aratio, ctrlCellHeight, 0); ctrlBlend.setName("ctrl.blend").setInteractive(false); ctrlBlend.setColor(0, 0, 0, alphaBlend); - ctrlBlend.setPaddding(new Padding(0, 0, 0, 0)); - container.addShape( ctrlBlend.setVisible(false) ); + this.addShape( ctrlBlend.setVisible(false) ); 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)); - container.addShape( ctrlGroup.move(0, 0, ctrlZOffset).setVisible(false) ); + this.addShape( ctrlGroup.move(0, 0, ctrlZOffset).setVisible(false) ); { // 1 - final Button button = new Button(renderModes, fontSymbols, - fontSymbols.getUTF16String("play_arrow"), fontSymbols.getUTF16String("pause"), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); - button.setName("play"); - button.setSpacing(SymSpacing, FixedSymSize).setPerp().setColor(CtrlCellCol); - button.onToggle((final Shape s) -> { + playButton.onToggle((final Shape s) -> { if( s.isToggleOn() ) { mPlayer.resume(); } else { mPlayer.pause(false); } }); - button.setToggle(true); // on == play - ctrlGroup.addShape(button); + playButton.setToggle(true); // on == play + ctrlGroup.addShape(playButton); } { // 2 final Button button = new Button(renderModes, fontSymbols, @@ -384,53 +412,48 @@ public class MediaUI01 { button.onToggle( (final Shape s) -> { if( s.isToggleOn() ) { final AABBox sbox = scene.getBounds(); - final Group parent = container.getParent(); + final Group parent = this.getParent(); if( null != parent ) { zoomReplacement[0] = new Label(renderModes, fontInfo, aratio/40f, "zoomed"); - final boolean r = parent.replaceShape(container, zoomReplacement[0]); + final boolean r = parent.replaceShape(this, zoomReplacement[0]); if( r ) { - System.err.println("Zoom1: "+parent); - final float sxy = sbox.getWidth() * zoomSize / container.getScaledWidth(); - scene.addShape(container); - System.err.println("cont1 "+container); - System.err.println("cbox1 "+container.getBounds()); - System.err.println("sbox1 "+sbox); - System.err.println("sxy1 "+sxy); - container.scale(sxy, sxy, 1f); - container.moveTo(sbox.getLow()).move(sbox.getWidth() * ( 1f - zoomSize )/2.0f, sbox.getHeight() * ( 1f - zoomSize )/2.0f, ctrlZOffset); + // System.err.println("Zoom1: p "+parent); + // System.err.println("Zoom1: t "+this); + final float sxy = sbox.getWidth() * zoomSize / this.getScaledWidth(); + scene.addShape(this); + this.scale(sxy, sxy, 1f); + this.moveTo(sbox.getLow()).move(sbox.getWidth() * ( 1f - zoomSize )/2.0f, sbox.getHeight() * ( 1f - zoomSize )/2.0f, ctrlZOffset); } else { - System.err.println("Zoom1: failed"); + System.err.println("Zoom1: failed "+this); } } else { - zoomOrigScale[0] = container.getScale().copy(); - zoomOrigPos[0] = container.getPosition().copy(); - System.err.println("Zoom2: top"); - final float sxy = sbox.getWidth() * zoomSize / container.getScaledWidth(); - System.err.println("cont2 "+container); - System.err.println("cbox2 "+container.getBounds()); - System.err.println("sbox2 "+sbox); - System.err.println("sxy2 "+sxy); - container.scale(sxy, sxy, 1f); - container.moveTo(sbox.getLow()).move(sbox.getWidth() * ( 1f - zoomSize )/2.0f, sbox.getHeight() * ( 1f - zoomSize )/2.0f, ctrlZOffset); + zoomOrigScale[0] = this.getScale().copy(); + zoomOrigPos[0] = this.getPosition().copy(); + // System.err.println("Zoom2: top"); + // System.err.println("Zoom2: t "+this); + final float sxy = sbox.getWidth() * zoomSize / this.getScaledWidth(); + this.scale(sxy, sxy, 1f); + this.moveTo(sbox.getLow()).move(sbox.getWidth() * ( 1f - zoomSize )/2.0f, sbox.getHeight() * ( 1f - zoomSize )/2.0f, ctrlZOffset); } } else { if( null != zoomReplacement[0] ) { final Group parent = zoomReplacement[0].getParent(); - container.moveTo(0, 0, 0); - parent.replaceShape(zoomReplacement[0], container); + scene.removeShape(this); + this.moveTo(0, 0, 0); + parent.replaceShape(zoomReplacement[0], this); scene.invoke(true, (drawable) -> { final GL2ES2 gl = drawable.getGL().getGL2ES2(); zoomReplacement[0].destroy(gl, scene.getRenderer()); return true; }); zoomReplacement[0] = null; - System.err.println("Reset1: "+parent); + // System.err.println("Reset1: "+parent); } else if( null != zoomOrigScale[0] && null != zoomOrigPos[0] ){ - container.scale(zoomOrigScale[0]); - container.moveTo(zoomOrigPos[0]); + this.scale(zoomOrigScale[0]); + this.moveTo(zoomOrigPos[0]); zoomOrigScale[0] = null; zoomOrigPos[0] = null; - System.err.println("Reset2: top"); + // System.err.println("Reset2: top"); } } }); @@ -441,25 +464,27 @@ public class MediaUI01 { ctrlGroup.addShape(cs); } } - container.setWidgetMode(true); + this.setWidgetMode(true); - container.onActivation( (final Shape s) -> { - if( container.isActive() ) { - container.setBorderColor(borderColorA); + this.onActivation( (final Shape s) -> { + if( this.isActive() ) { + this.setBorderColor(borderColorA); } else { - container.setBorderColor(borderColor); + this.setBorderColor(borderColor); } - if( ctrlGroup.isActive() ) { + if( ctrlGroup.isActive() || ctrlSlider.isActive() ) { + ctrlSlider.setVisible(true); ctrlBlend.setVisible(true); ctrlGroup.setVisible(true); infoGroup.setVisible(true); } else { + ctrlSlider.setVisible(false); ctrlBlend.setVisible(false); ctrlGroup.setVisible(false); infoGroup.setVisible(false); } }); - container.addMouseListener(new Shape.MouseGestureAdapter() { + this.addMouseListener(new Shape.MouseGestureAdapter() { @Override public void mouseReleased(final MouseEvent e) { mButton.setPressedColorMod(1f, 1f, 1f, 1f); @@ -469,8 +494,28 @@ public class MediaUI01 { mButton.setPressedColorMod(1f, 1f, 1f, 0.85f); } } ); - container.forAll((final Shape s) -> { s.setDraggable(false).setResizable(false); return false; }); - return container; + this.forAll((final Shape s) -> { s.setDraggable(false).setResizable(false); return false; }); + ctrlSlider.getKnob().setDraggable(true); + } + + /** Returns the used info font or null if n/a */ + public static Font getInfoFont() { + try { + return FontFactory.get(FontFactory.UBUNTU).getDefault(); + } catch(final IOException ioe) { + ioe.printStackTrace(); + return null; + } + } + + /** Returns the used symbols font or null if n/a */ + public static Font getSymbolsFont() { + try { + return FontFactory.get(FontFactory.SYMBOLS).getDefault(); + } catch(final IOException ioe) { + ioe.printStackTrace(); + return null; + } } public static String millisToTimeStr(final long millis, final boolean addFractions) { @@ -502,7 +547,10 @@ public class MediaUI01 { } } } - public static String getInfo(final GLMediaPlayer mPlayer, final boolean full) { + public static String getInfo(final long currentMillis, final GLMediaPlayer mPlayer, final boolean full) { + return getInfo(mPlayer.getPTS().get(currentMillis), mPlayer.getDuration(), mPlayer, full); + } + public static String getInfo(final int ptsMS, final int durationMS, final GLMediaPlayer mPlayer, final boolean full) { final String name; { final String basename; @@ -521,12 +569,10 @@ public class MediaUI01 { } } final float aspect = (float)mPlayer.getWidth() / (float)mPlayer.getHeight(); - final int pts = mPlayer.getPTS().get(Clock.currentMillis()); - final long duration = mPlayer.getDuration(); - final float pct = (float)pts / (float)duration; + final float pct = (float)ptsMS / (float)durationMS; if( full ) { final String text1 = String.format("%s / %s (%.0f %%), %s (%01.1fx, vol %1.2f), A/R %0.2f, fps %02.1f", - millisToTimeStr(pts, false), millisToTimeStr(duration, false), pct*100, + millisToTimeStr(ptsMS, false), millisToTimeStr(durationMS, false), pct*100, mPlayer.getState().toString().toLowerCase(), mPlayer.getPlaySpeed(), mPlayer.getAudioVolume(), aspect, mPlayer.getFramerate()); final String text2 = String.format("audio: id %d, kbps %d, codec %s", mPlayer.getAID(), mPlayer.getAudioBitrate()/1000, mPlayer.getAudioCodec()); @@ -535,17 +581,17 @@ public class MediaUI01 { return text1+"\n"+text2+"\n"+text3+"\n"+name; } else { final String text1 = String.format("%s / %s (%.0f %%), %s (%01.1fx, vol %1.2f), A/R %.2f", - millisToTimeStr(pts, false), millisToTimeStr(duration, false), pct*100, + millisToTimeStr(ptsMS, false), millisToTimeStr(durationMS, false), pct*100, mPlayer.getState().toString().toLowerCase(), mPlayer.getPlaySpeed(), mPlayer.getAudioVolume(), aspect); return text1+"\n"+name; } } - public static String getMultilineTime(final GLMediaPlayer mPlayer) { - final int pts = mPlayer.getPTS().get(Clock.currentMillis()); - final long duration = mPlayer.getDuration(); - final float pct = (float)pts / (float)duration; + public static String getMultilineTime(final long currentMillis, final GLMediaPlayer mPlayer) { + return getMultilineTime(mPlayer.getPTS().get(currentMillis), mPlayer.getDuration()); + } + public static String getMultilineTime(final int ptsMS, final int durationMS) { + final float pct = (float)ptsMS / (float)durationMS; return String.format("%.0f %%%n%s%n%s", - pct*100, millisToTimeStr(pts, false), millisToTimeStr(duration, false)); + pct*100, millisToTimeStr(ptsMS, false), millisToTimeStr(durationMS, false)); } - } diff --git a/src/graphui/classes/com/jogamp/graph/ui/widgets/RangeSlider.java b/src/graphui/classes/com/jogamp/graph/ui/widgets/RangeSlider.java new file mode 100644 index 000000000..495d81149 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/widgets/RangeSlider.java @@ -0,0 +1,252 @@ +/** + * Copyright 2010-2023 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.widgets; + +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.GLRegion; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.Group; +import com.jogamp.graph.ui.Shape; +import com.jogamp.graph.ui.shapes.BaseButton; +import com.jogamp.graph.ui.shapes.Button; +import com.jogamp.graph.ui.shapes.Rectangle; +import com.jogamp.math.Vec2f; +import com.jogamp.math.Vec3f; +import com.jogamp.math.Vec4f; +import com.jogamp.newt.event.MouseEvent; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.util.texture.TextureSequence; + +/** + * RangeSlider {@link Widget} + * @see #RangeSlider(int, float, float, float, float, float, float) + */ +public final class RangeSlider extends Widget { + /** + * {@link RangeSlider} slider listener + */ + public static interface SliderListener { + /** Slider clicked by user (after completing pressed + released). */ + void clicked(RangeSlider w, final MouseEvent e); + /** Slider pressed down by user. */ + void pressed(RangeSlider w, final MouseEvent e); + /** Slider released down by user. */ + void released(RangeSlider w, final MouseEvent e); + /** + * Slide dragged by user + * @param w the {@link RangeSlider} widget owning the slider + * @param old_val previous absolute value position of the slider + * @param val the absolute value position of the slider + * @param old_val_pct previous percentage value position of the slider + * @param val_pct the percentage value position of the slider + */ + void dragged(RangeSlider w, float old_val, float val, float old_val_pct, float val_pct); + } + + private final boolean horizontal; + private final float knobSz; + private final float width, height; + private final Group barAndKnob; + private final Rectangle bar; + private final BaseButton knob; + private final Vec4f colBar = new Vec4f(0, 0, 1, 1); + private final Vec4f colKnob = new Vec4f(1, 0, 0, 1); + private SliderListener sliderListener = null; + private float min, max; + private float val, val_pct; + + /** + * Constructs a {@link RangeSlider}, i.e. its shapes and controls. + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. + * @param width width of this slider box. A horizontal slider has width >= height. + * @param height height of this slider box. A vertical slider has width < height. + * @param knobScale multiple of slider-bar height for {@link #getKnobSize()} + * @param min minimum value of slider + * @param max maximum value of slider + * @param value current value of slider + */ + public RangeSlider(final int renderModes, final float width, final float height, final float knobScale, + final float min, final float max, final float value) { + this.horizontal = width >= height; + if( horizontal ) { + knobSz = height*knobScale; + this.width = width - knobSz; // half knobSz left and right + this.height = height; + } else { + knobSz = width*knobScale; + this.width = width; + this.height = height - knobSz; // half knobSz bottom and top + } + barAndKnob = new Group(); + barAndKnob.setInteractive(true).setDraggable(false).setToggleable(false); + bar = new Rectangle(renderModes & ~Region.AA_RENDERING_MASK, this.width, this.height, 0); + bar.setToggleable(false); + bar.setColor(colBar); + knob = new BaseButton(renderModes & ~Region.AA_RENDERING_MASK, knobSz*1.01f, knobSz); + knob.setToggleable(false); + knob.setColor(colKnob); + setName(getName()); + barAndKnob.addShape( bar ); + barAndKnob.addShape( knob ); + addShape(barAndKnob); + + setMinMax(min, max, value); + + knob.onMove((final Shape s, final Vec3f origin, final Vec3f dest) -> { + final float old_val = val; + final float old_val_pct = val_pct; + setValuePct( getKnobValuePct( dest.x(), dest.y(), knobSz*0.5f ) ); + // System.err.println("KnobMove "+getName()+": "+origin+" -> "+dest+": "+old_val+" -> "+val+", "+(old_val_pct*100f)+"% -> "+(val_pct*100f)+"%"); + if( null != sliderListener ) { + sliderListener.dragged(this, old_val, val, old_val_pct, val_pct); + } + }); + barAndKnob.addMouseListener(new Shape.MouseGestureAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + final Shape.EventInfo shapeEvent = (Shape.EventInfo) e.getAttachment(); + setValuePct( getKnobValuePct( shapeEvent.objPos.x(), shapeEvent.objPos.y(), 0 ) ); + if( null != sliderListener ) { + sliderListener.clicked(RangeSlider.this, e); + } + } + }); + knob.addMouseListener(new Shape.MouseGestureAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + if( null != sliderListener ) { + sliderListener.clicked(RangeSlider.this, e); + } + } + @Override + public void mousePressed(final MouseEvent e) { + if( null != sliderListener ) { + sliderListener.pressed(RangeSlider.this, e); + } + } + @Override + public void mouseReleased(final MouseEvent e) { + if( null != sliderListener ) { + sliderListener.released(RangeSlider.this, e); + } + } + @Override + public void mouseWheelMoved(final MouseEvent e) { + // Support ? + } + }); + } + + @Override + protected void clearImpl0(final GL2ES2 gl, final RegionRenderer renderer) { + super.clearImpl0(gl, renderer); + sliderListener = null; + } + @Override + protected void destroyImpl0(final GL2ES2 gl, final RegionRenderer renderer) { + super.destroyImpl0(gl, renderer); + sliderListener = null; + } + + public RangeSlider onSlider(final SliderListener l) { + sliderListener = l; + return this; + } + + public Rectangle getBar() { return bar; } + public BaseButton getKnob() { return knob; } + + public final float getWidth() { return width; } + public final float getHeight() { return height; } + public final float getKnobSize() { return knobSz; } + + public float getMin() { return min; } + public float getMax() { return max; } + public float getRange() { return max - min; } + public float getValue() { return val; } + public float getValuePct() { return val_pct; } + + /** + * Sets slider value range and current value + * @param min minimum value of slider + * @param max maximum value of slider + * @param value current value of slider + * @return this instance of chaining + */ + public RangeSlider setMinMax(final float min, final float max, final float value) { + this.min = min; + this.max = max; + this.val = Math.max(min, Math.min(max, value)); + this.val_pct = ( value - min ) / getRange(); + setKnob(); + return this; + } + + public RangeSlider setValuePct(final float v) { + val_pct = v; + val = min + ( val_pct * getRange() ); + setKnob(); + return this; + } + + public RangeSlider setValue(final float v) { + val = v; + val_pct = ( val - min ) / getRange(); + setKnob(); + return this; + } + + /** + * Knob position reflects value on its center and ranges from zero to max. + */ + private Vec2f getKnobPos(final Vec2f posRes, final float val_pct) { + if( horizontal ) { + posRes.setX( val_pct*width - knobSz*0.5f ); + posRes.setY( -( knobSz - height ) * 0.5f ); + } else { + posRes.setX( -( knobSz - width ) * 0.5f ); + posRes.setY( val_pct*height - knobSz*0.5f ); + } + return posRes; + } + private float getKnobValuePct(final float pos_x, final float pos_y, final float adjustment) { + final float v; + if( horizontal ) { + v = ( pos_x + adjustment ) / width; + } else { + v = ( pos_y + adjustment ) / height; + } + return Math.max(0.0f, Math.min(1.0f, v)); + } + + private void setKnob() { + final Vec2f pos = getKnobPos(new Vec2f(), val_pct); + knob.moveTo(pos.x(), pos.y(), Button.DEFAULT_LABEL_ZOFFSET); + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/widgets/Widget.java b/src/graphui/classes/com/jogamp/graph/ui/widgets/Widget.java new file mode 100644 index 000000000..fb547c6c0 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/widgets/Widget.java @@ -0,0 +1,65 @@ +/** + * Copyright 2010-2023 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.widgets; + +import com.jogamp.graph.ui.Group; +import com.jogamp.graph.ui.Shape; +import com.jogamp.graph.ui.Group.Layout; + +/** + * A widget specifies specific UI semantics including individual controls. + * <p> + * Being a {@link Group}, implementations provide shape(s) and its instance can be added to the user's scene. + * </p> + * <p> + * Due to the specific nature of widgets, + * individual controls/listener may be provided with semantic values. + * </p> + */ +public class Widget extends Group { + /** + * Create a Widget group of {@link Shape}s w/o {@link Group.Layout}. + * <p> + * Default is non-interactive, see {@link #setInteractive(boolean)}. + * </p> + */ + public Widget() { + super(null); + } + + /** + * Create a Widget group of {@link Shape}s w/ given {@link Group.Layout}. + * <p> + * Default is non-interactive, see {@link #setInteractive(boolean)}. + * </p> + * @param l optional {@link Layout}, maybe {@code null} + */ + public Widget(final Layout l) { + super(l); + } +} |