| Author | DaveJarvis <email> |
|---|---|
| Date | 2020-07-19 12:49:40 GMT-0700 |
| Commit | 5e7b42516b23be9cf9c998d9a2312eed4870bfc9 |
| Parent | 3c4c71f |
| Delta | 435 lines added, 351 lines removed, 84-line increase |
|---|
| -/* | ||
| - * Copyright 2020 White Magic Software, Ltd. | ||
| - * | ||
| - * All rights reserved. | ||
| - * | ||
| - * Redistribution and use in source and binary forms, with or without | ||
| - * modification, are permitted provided that the following conditions are met: | ||
| - * | ||
| - * o Redistributions of source code must retain the above copyright | ||
| - * notice, this list of conditions and the following disclaimer. | ||
| - * | ||
| - * o 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| - * "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 THE COPYRIGHT | ||
| - * HOLDER 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. | ||
| - */ | ||
| -package com.whitemagicsoftware.kmcaster; | ||
| - | ||
| -import com.whitemagicsoftware.kmcaster.listeners.FrameDragListener; | ||
| - | ||
| -import javax.swing.*; | ||
| -import java.awt.*; | ||
| -import java.awt.font.TextLayout; | ||
| -import java.awt.geom.RoundRectangle2D; | ||
| -import java.util.HashMap; | ||
| -import java.util.Map; | ||
| - | ||
| -import static com.whitemagicsoftware.kmcaster.HardwareImages.state; | ||
| -import static com.whitemagicsoftware.kmcaster.HardwareSwitch.*; | ||
| -import static java.awt.Font.BOLD; | ||
| -import static java.lang.Boolean.parseBoolean; | ||
| - | ||
| -public class EventFrame extends JFrame { | ||
| - private static final float ARC = 8; | ||
| - private static final Dimension FRAME_DIMENSIONS = new Dimension( 484, 70 ); | ||
| - private static final Color TRANSLUCENT = new Color( .2f, .2f, .2f, 0.5f ); | ||
| - private static final Color TRANSPARENT = new Color( 0, 0, 0, 0 ); | ||
| - private static final Color COLOUR_LABEL = new Color( 33, 33, 33 ); | ||
| - | ||
| - private final HardwareImages mSwitches; | ||
| - private final Map<HardwareSwitch, ImageComponent> mSwitchViews = | ||
| - new HashMap<>(); | ||
| - | ||
| - public EventFrame() { | ||
| - initWindowFrame(); | ||
| - initWindowDragListener(); | ||
| - | ||
| - final var dimensions = new Dimension( getWidth(), getHeight() - 10 ); | ||
| - mSwitches = new HardwareImages( dimensions ); | ||
| - | ||
| - final var mouseImage = mSwitches.get( state( MOUSE_LEFT, false ) ); | ||
| - final var mouseComponent = createImageComponent( mouseImage ); | ||
| - | ||
| - final var shiftUpImage = mSwitches.get( state( KEY_SHIFT, false ) ); | ||
| - final var ctrlUpImage = mSwitches.get( state( KEY_CTRL, false ) ); | ||
| - final var altUpImage = mSwitches.get( state( KEY_ALT, false ) ); | ||
| - final var regularUpImage = mSwitches.get( state( KEY_REGULAR, false ) ); | ||
| - | ||
| - final var shiftComponent = createImageComponent( shiftUpImage ); | ||
| - final var ctrlComponent = createImageComponent( ctrlUpImage ); | ||
| - final var altComponent = createImageComponent( altUpImage ); | ||
| - final var regularComponent = createImageComponent( regularUpImage ); | ||
| - | ||
| - final var panel = new JPanel(); | ||
| - panel.setAlignmentX( CENTER_ALIGNMENT ); | ||
| - panel.setBackground( TRANSLUCENT ); | ||
| - panel.add( mouseComponent ); | ||
| - panel.add( shiftComponent ); | ||
| - panel.add( ctrlComponent ); | ||
| - panel.add( altComponent ); | ||
| - panel.add( regularComponent ); | ||
| - | ||
| - final var content = getContentPane(); | ||
| - final var layout = new BoxLayout( content, BoxLayout.Y_AXIS ); | ||
| - | ||
| - content.setLayout( layout ); | ||
| - content.add( panel ); | ||
| - | ||
| - mSwitchViews.put( KEY_SHIFT, shiftComponent ); | ||
| - mSwitchViews.put( KEY_CTRL, ctrlComponent ); | ||
| - mSwitchViews.put( KEY_ALT, altComponent ); | ||
| - mSwitchViews.put( KEY_REGULAR, regularComponent ); | ||
| - } | ||
| - | ||
| - private void initWindowFrame() { | ||
| - setDefaultCloseOperation( EXIT_ON_CLOSE ); | ||
| - setLocationRelativeTo( null ); | ||
| - setUndecorated( true ); | ||
| - setAlwaysOnTop( true ); | ||
| - setBackground( TRANSPARENT ); | ||
| - setSize( FRAME_DIMENSIONS ); | ||
| - setShape( createShape() ); | ||
| - } | ||
| - | ||
| - private void initWindowDragListener() { | ||
| - final var frameDragListener = new FrameDragListener( this ); | ||
| - addMouseListener( frameDragListener ); | ||
| - addMouseMotionListener( frameDragListener ); | ||
| - } | ||
| - | ||
| - protected void updateSwitchState( final HardwareState keyState ) { | ||
| - final var image = mSwitches.get( keyState ); | ||
| - final var component = mSwitchViews.get( keyState.getHardwareSwitch() ); | ||
| - | ||
| - component.redraw( image ); | ||
| - } | ||
| - | ||
| - protected void updateSwitchLabel( | ||
| - final HardwareState state, final String value ) { | ||
| - if( state.isModifier() ) { | ||
| - final var pressed = parseBoolean( value ); | ||
| - System.out.println( "Modifier pressed: " + pressed ); | ||
| - } | ||
| - else { | ||
| - final var component = mSwitchViews.get( state.getHardwareSwitch() ); | ||
| - component.removeAll(); | ||
| - | ||
| - if( !"false".equals( value ) ) { | ||
| - final var bounds = component.getBounds(); | ||
| - final var label = labelFor( value, bounds ); | ||
| - | ||
| - component.add( label ); | ||
| - component.repaint(); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * @param text The text | ||
| - * @param r The maximum width and height. | ||
| - * @return A label adjusted to the given dimensions. | ||
| - */ | ||
| - private JLabel labelFor( final String text, final Rectangle r ) { | ||
| - final int width = (int) r.getWidth() + 1; | ||
| - final int height = (int) r.getHeight() + 1; | ||
| - | ||
| - final var label = new JLabel( text ); | ||
| - final var font = new Font( "DejaVu Sans", BOLD, 12 ); | ||
| - label.setFont( font ); | ||
| - label.setSize( width, height ); | ||
| - label.setForeground( COLOUR_LABEL ); | ||
| - | ||
| - final var scaledFont = scaleFont( label.getText(), font, r ); | ||
| - label.setFont( scaledFont ); | ||
| - | ||
| - return label; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Adjusts the given {@link Font}/{@link String} size such that it fits | ||
| - * within the bounds of the given {@link Rectangle}. | ||
| - * | ||
| - * @param text The text string to be rendered within the bounds of the | ||
| - * given {@link Rectangle}. | ||
| - * @param font The {@link Font} to use when rendering the text string. | ||
| - * @param dst The bounds for fitting the string. | ||
| - * @return A new {@link Font} instance that is guaranteed to write the given | ||
| - * string within the bounds of the given {@link Rectangle}. | ||
| - */ | ||
| - private Font scaleFont( | ||
| - final String text, final Font font, final Rectangle dst ) { | ||
| - final var g = (Graphics2D) getGraphics(); | ||
| - final var frc = g.getFontRenderContext(); | ||
| - final var dstWidthPx = dst.getWidth(); | ||
| - final var dstHeightPx = dst.getHeight(); | ||
| - | ||
| - var minSizePt = 1f; | ||
| - var maxSizePt = 1000f; | ||
| - var scaledFont = font; | ||
| - float scaledPt = scaledFont.getSize(); | ||
| - | ||
| - while( maxSizePt - minSizePt > 2 ) { | ||
| - final var layout = new TextLayout( text, scaledFont, frc ); | ||
| - final float fontWidthPx = layout.getVisibleAdvance(); | ||
| - final var metrics = scaledFont.getLineMetrics( text, frc ); | ||
| - final float fontHeightPx = metrics.getHeight(); | ||
| - | ||
| - if( (fontWidthPx > dstWidthPx) || (fontHeightPx > dstHeightPx) ) { | ||
| - maxSizePt = scaledPt; | ||
| - } | ||
| - else { | ||
| - minSizePt = scaledPt; | ||
| - } | ||
| - | ||
| - scaledPt = (minSizePt + maxSizePt) / 2; | ||
| - scaledFont = scaledFont.deriveFont( scaledPt ); | ||
| - } | ||
| - | ||
| - return scaledFont; | ||
| - } | ||
| - | ||
| - private ImageComponent createImageComponent( final Image image ) { | ||
| - return new ImageComponent( image ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the shape for the application's window frame. | ||
| - * | ||
| - * @return A rounded rectangle. | ||
| - */ | ||
| - private Shape createShape() { | ||
| - return new RoundRectangle2D.Double( | ||
| - 0, 0, getWidth(), getHeight(), ARC, ARC | ||
| - ); | ||
| - } | ||
| -} | ||
| +/* | ||
| + * Copyright 2020 White Magic Software, Ltd. | ||
| + * | ||
| + * All rights reserved. | ||
| + * | ||
| + * Redistribution and use in source and binary forms, with or without | ||
| + * modification, are permitted provided that the following conditions are met: | ||
| + * | ||
| + * o Redistributions of source code must retain the above copyright | ||
| + * notice, this list of conditions and the following disclaimer. | ||
| + * | ||
| + * o 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| + * "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 THE COPYRIGHT | ||
| + * HOLDER 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. | ||
| + */ | ||
| +package com.whitemagicsoftware.kmcaster; | ||
| + | ||
| +import javax.swing.*; | ||
| +import java.awt.*; | ||
| +import java.awt.font.TextLayout; | ||
| +import java.beans.PropertyChangeEvent; | ||
| +import java.beans.PropertyChangeListener; | ||
| + | ||
| +import static com.whitemagicsoftware.kmcaster.HardwareState.ANY_KEY; | ||
| +import static java.awt.Font.BOLD; | ||
| +import static java.lang.Boolean.parseBoolean; | ||
| + | ||
| +public class EventHandler implements PropertyChangeListener { | ||
| + private static final Font DEFAULT_FONT = new Font( "DejaVu Sans", BOLD, 12 ); | ||
| + private static final Color COLOUR_LABEL = new Color( 33, 33, 33 ); | ||
| + | ||
| + private final HardwareImages mHardwareImages; | ||
| + | ||
| + public EventHandler( final HardwareImages hardwareImages ) { | ||
| + mHardwareImages = hardwareImages; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when a hardware switch has changed state. | ||
| + * | ||
| + * @param e Contains the identifier for the switch, its previous value, | ||
| + * and its new value. | ||
| + */ | ||
| + @Override | ||
| + public void propertyChange( final PropertyChangeEvent e ) { | ||
| + final var switchName = e.getPropertyName(); | ||
| + final var switchValue = e.getNewValue().toString(); | ||
| + | ||
| + // True or false indicates a non-regular key was pressed. | ||
| + final var context = | ||
| + (!"false".equals( switchValue ) && !"true".equals( switchValue )) | ||
| + ? ANY_KEY | ||
| + : switchValue; | ||
| + | ||
| + final var switchState = new HardwareState( switchName, context ); | ||
| + updateSwitchState( switchState ); | ||
| + updateSwitchLabel( switchState, switchValue ); | ||
| + } | ||
| + | ||
| + protected void updateSwitchState( final HardwareState state ) { | ||
| + final var component = mHardwareImages.get( state.getHardwareSwitch() ); | ||
| + | ||
| + component.setState( state ); | ||
| + } | ||
| + | ||
| + protected void updateSwitchLabel( | ||
| + final HardwareState state, final String value ) { | ||
| + if( state.isModifier() ) { | ||
| + //final var pressed = parseBoolean( value ); | ||
| + //System.out.println( "Modifier pressed: " + pressed ); | ||
| + } | ||
| + else { | ||
| + final var component = mHardwareImages.get( state.getHardwareSwitch() ); | ||
| + component.removeAll(); | ||
| + | ||
| + if( !"false".equals( value ) ) { | ||
| + final var bounds = component.getBounds(); | ||
| + final var insets = component.getInsets(); | ||
| + | ||
| + bounds.x += insets.left; | ||
| + bounds.y += insets.top; | ||
| + bounds.width -= insets.right + insets.left; | ||
| + bounds.height -= insets.bottom + insets.top; | ||
| + | ||
| + final var label = createLabel( value, bounds, component.getGraphics() ); | ||
| + | ||
| + label.setLocation( insets.left, insets.top ); | ||
| + | ||
| + component.add( label ); | ||
| + component.repaint(); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a label that can fit within bounds defined by the given | ||
| + * {@link Rectangle} using the given {@link Graphics} context for deriving | ||
| + * the actualized font dimensions (for a particular font point size). | ||
| + * | ||
| + * @param text The label's text. | ||
| + * @param r The label's maximum width and height. | ||
| + * @param graphics The graphics context used to determine font scale (must | ||
| + * be a {@link Graphics2D instance}). | ||
| + * @return A label adjusted to the given dimensions. | ||
| + */ | ||
| + private JLabel createLabel( | ||
| + final String text, final Rectangle r, final Graphics graphics ) { | ||
| + assert text != null; | ||
| + assert r != null; | ||
| + assert graphics != null; | ||
| + | ||
| + final int width = (int) r.getWidth(); | ||
| + final int height = (int) r.getHeight(); | ||
| + | ||
| + final var label = new JLabel( text ); | ||
| + label.setFont( DEFAULT_FONT ); | ||
| + label.setSize( width, height ); | ||
| + label.setForeground( COLOUR_LABEL ); | ||
| + //label.setBorder( BorderFactory.createLineBorder( Color.RED ) ); | ||
| + | ||
| + final var scaledFont = scale( label, r, graphics ); | ||
| + label.setFont( scaledFont ); | ||
| + | ||
| + return label; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Adjusts the given {@link Font}/{@link String} size such that it fits | ||
| + * within the bounds of the given {@link Rectangle}. | ||
| + * | ||
| + * @param label Contains the text and font to scale. | ||
| + * @param dst The bounds for fitting the string. | ||
| + * @param graphics The context for rendering the string. | ||
| + * @return A new {@link Font} instance that is guaranteed to write the given | ||
| + * string within the bounds of the given {@link Rectangle}. | ||
| + */ | ||
| + private Font scale( | ||
| + final JLabel label, final Rectangle dst, final Graphics graphics ) { | ||
| + assert label != null; | ||
| + assert dst != null; | ||
| + assert graphics != null; | ||
| + | ||
| + final var font = label.getFont(); | ||
| + final var text = label.getText(); | ||
| + | ||
| + final var frc = ((Graphics2D) graphics).getFontRenderContext(); | ||
| + | ||
| + final var dstWidthPx = dst.getWidth(); | ||
| + final var dstHeightPx = dst.getHeight(); | ||
| + | ||
| + var minSizePt = 1f; | ||
| + var maxSizePt = 1000f; | ||
| + var scaledFont = font; | ||
| + float scaledPt = scaledFont.getSize(); | ||
| + | ||
| + while( maxSizePt - minSizePt > 2 ) { | ||
| + scaledFont = scaledFont.deriveFont( scaledPt ); | ||
| + | ||
| +// final var layout = new TextLayout( text, scaledFont, frc ); | ||
| +// final float fontWidthPx = layout.getVisibleAdvance(); | ||
| + final var fm = graphics.getFontMetrics(scaledFont); | ||
| + final float fontWidthPx = (float)fm.getStringBounds( text, graphics ).getWidth(); | ||
| + | ||
| + final var metrics = scaledFont.getLineMetrics( text, frc ); | ||
| + final float fontHeightPx = metrics.getHeight(); | ||
| + | ||
| + if( (fontWidthPx > dstWidthPx) || (fontHeightPx > dstHeightPx) ) { | ||
| + maxSizePt = scaledPt; | ||
| + } | ||
| + else { | ||
| + minSizePt = scaledPt; | ||
| + } | ||
| + | ||
| + scaledPt = (minSizePt + maxSizePt) / 2; | ||
| + } | ||
| + | ||
| + return scaledFont; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * Copyright 2020 White Magic Software, Ltd. | ||
| + * | ||
| + * All rights reserved. | ||
| + * | ||
| + * Redistribution and use in source and binary forms, with or without | ||
| + * modification, are permitted provided that the following conditions are met: | ||
| + * | ||
| + * o Redistributions of source code must retain the above copyright | ||
| + * notice, this list of conditions and the following disclaimer. | ||
| + * | ||
| + * o 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| + * "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 THE COPYRIGHT | ||
| + * HOLDER 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. | ||
| + */ | ||
| +package com.whitemagicsoftware.kmcaster; | ||
| + | ||
| +import javax.swing.*; | ||
| +import java.awt.*; | ||
| +import java.util.HashMap; | ||
| +import java.util.Map; | ||
| + | ||
| +/** | ||
| + * Responsible for drawing an image based on a state; the state can be | ||
| + * changed at any time. | ||
| + * | ||
| + * @param <S> The type of state associated with an image. | ||
| + */ | ||
| +public class HardwareComponent<S, I extends Image> extends JComponent { | ||
| + private final static Insets INSET_PROJECTED = | ||
| + new Insets( 3, 7, 6, 7 ); | ||
| + | ||
| + private final Map<S, I> mStateImages = new HashMap<>(); | ||
| + | ||
| + /** | ||
| + * Active state. | ||
| + */ | ||
| + private S mState; | ||
| + | ||
| + /** | ||
| + * Constructs a new {@link HardwareComponent} without an initial state. The | ||
| + * initial state must be set by calling {@link #setState(Object)} before | ||
| + * drawing the image. | ||
| + */ | ||
| + public HardwareComponent() { | ||
| + } | ||
| + | ||
| + /** | ||
| + * Associates a new (or existing) state with the given image. If the | ||
| + * state already exists for the image, the image is updated for that | ||
| + * state. After calling this method, the active state changes to the | ||
| + * given state as a convenience. | ||
| + * | ||
| + * @param state The state to associate with an image. | ||
| + * @param image The image to paint when the given state is selected. | ||
| + */ | ||
| + public void put( final S state, final I image ) { | ||
| + getStateImages().put( state, image ); | ||
| + setState( state ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Dimension getPreferredSize() { | ||
| + // Race-condition guard. | ||
| + final var image = getActiveImage(); | ||
| + | ||
| + return new Dimension( | ||
| + image.getWidth( null ), image.getHeight( null ) | ||
| + ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Insets getInsets() { | ||
| + return INSET_PROJECTED; | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected void paintComponent( final Graphics graphics ) { | ||
| + super.paintComponent( graphics ); | ||
| + | ||
| + final var g = (Graphics2D) graphics.create(); | ||
| + g.drawImage( getActiveImage(), 0, 0, this ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Repaints this component by changing its mutable state. The new state | ||
| + * must have been previously registered by caling {@link #put(Object, Image)}. | ||
| + * | ||
| + * @param state The new state. | ||
| + */ | ||
| + public void setState( final S state ) { | ||
| + assert state != null; | ||
| + mState = state; | ||
| + | ||
| + repaint(); | ||
| + } | ||
| + | ||
| + private Image getActiveImage() { | ||
| + return getStateImages().get( getState() ); | ||
| + } | ||
| + | ||
| + private S getState() { | ||
| + return mState; | ||
| + } | ||
| + | ||
| + private Map<S, I> getStateImages() { | ||
| + return mStateImages; | ||
| + } | ||
| +} | ||
| private final static SvgRasterizer sRasterizer = new SvgRasterizer(); | ||
| - private final Map<HardwareState, Image> mImages = new HashMap<>(); | ||
| + private final Map<HardwareSwitch, HardwareComponent<HardwareState, Image>> | ||
| + mSwitches = new HashMap<>(); | ||
| private final Dimension mDimension; | ||
| final var mouseReleased = mouseImage( "0" ); | ||
| + final var mouseStates = new HardwareComponent<HardwareState, Image>(); | ||
| for( int i = 1; i <= 3; i++ ) { | ||
| final var s = Integer.toString( i ); | ||
| - final var switchName = HardwareSwitch.valueFrom( "button " + s ); | ||
| - mImages.put( state( switchName, true ), mouseImage( s ) ); | ||
| - mImages.put( state( switchName, false ), mouseReleased ); | ||
| + mouseStates.put( state( MOUSE, s ), mouseImage( s ) ); | ||
| } | ||
| - mImages.put( state( MOUSE_LR, true ), mouseImage( "1-3" ) ); | ||
| - mImages.put( state( MOUSE_LR, false ), mouseReleased ); | ||
| + mouseStates.put( state( MOUSE, "1-3" ), mouseImage( "1-3" ) ); | ||
| + mouseStates.put( state( MOUSE, false ), mouseReleased ); | ||
| + mSwitches.put( MOUSE, mouseStates ); | ||
| - mImages.put( state( KEY_ALT, true ), keyDnImage( "medium" ) ); | ||
| - mImages.put( state( KEY_ALT, false ), keyUpImage( "medium" ) ); | ||
| - mImages.put( state( KEY_CTRL, true ), keyDnImage( "medium" ) ); | ||
| - mImages.put( state( KEY_CTRL, false ), keyUpImage( "medium" ) ); | ||
| - mImages.put( state( KEY_SHIFT, true ), keyDnImage( "long" ) ); | ||
| - mImages.put( state( KEY_SHIFT, false ), keyUpImage( "long" ) ); | ||
| - mImages.put( state( KEY_REGULAR, ANY_KEY ), keyDnImage( "short" ) ); | ||
| - mImages.put( state( KEY_REGULAR, false ), keyUpImage( "short" ) ); | ||
| + final var altStates = new HardwareComponent<HardwareState, Image>(); | ||
| + altStates.put( state( KEY_ALT, true ), keyDnImage( "medium" ) ); | ||
| + altStates.put( state( KEY_ALT, false ), keyUpImage( "medium" ) ); | ||
| + mSwitches.put( KEY_ALT, altStates ); | ||
| + | ||
| + final var ctrlStates = new HardwareComponent<HardwareState, Image>(); | ||
| + ctrlStates.put( state( KEY_CTRL, true ), keyDnImage( "medium" ) ); | ||
| + ctrlStates.put( state( KEY_CTRL, false ), keyUpImage( "medium" ) ); | ||
| + mSwitches.put( KEY_CTRL, ctrlStates ); | ||
| + | ||
| + final var shiftStates = new HardwareComponent<HardwareState, Image>(); | ||
| + shiftStates.put( state( KEY_SHIFT, true ), keyDnImage( "long" ) ); | ||
| + shiftStates.put( state( KEY_SHIFT, false ), keyUpImage( "long" ) ); | ||
| + mSwitches.put( KEY_SHIFT, shiftStates ); | ||
| + | ||
| + final var regularStates = new HardwareComponent<HardwareState, Image>(); | ||
| + regularStates.put( state( KEY_REGULAR, ANY_KEY ), keyDnImage( "short" ) ); | ||
| + regularStates.put( state( KEY_REGULAR, false ), keyUpImage( "short" ) ); | ||
| + mSwitches.put( KEY_REGULAR, regularStates ); | ||
| } | ||
| - public Image get( final HardwareState state ) { | ||
| - return mImages.get( state ); | ||
| + public HardwareComponent<HardwareState, Image> get( | ||
| + final HardwareSwitch hwSwitch ) { | ||
| + return mSwitches.get( hwSwitch ); | ||
| } | ||
| - public static HardwareState state( | ||
| + private HardwareState state( | ||
| final HardwareSwitch name, final boolean state ) { | ||
| return state( name, Boolean.toString( state ) ); | ||
| } | ||
| - public static HardwareState state( | ||
| + private HardwareState state( | ||
| final HardwareSwitch name, final String state ) { | ||
| return new HardwareState( name, state ); | ||
| final var diagram = sRasterizer.loadDiagram( resource ); | ||
| final var scale = sRasterizer.calculateScale( diagram, mDimension ); | ||
| - final var rasterized = sRasterizer.rasterize( diagram, mDimension ); | ||
| + final var image = sRasterizer.rasterize( diagram, mDimension ); | ||
| - return rasterized; | ||
| + // TODO: Scale insets. | ||
| + | ||
| + return image; | ||
| } catch( final Exception ex ) { | ||
| rethrow( ex ); | ||
| /** | ||
| * Used for compile-time binding between change listeners input events. | ||
| + * <p> | ||
| + * The element declaration order dictates the on-screen order. | ||
| + * </p> | ||
| */ | ||
| public enum HardwareSwitch { | ||
| - KEY_ALT( "alt", ALT_MASK ), | ||
| - KEY_CTRL( "ctrl", CTRL_MASK ), | ||
| + MOUSE( "mouse" ), | ||
| KEY_SHIFT( "shift", SHIFT_MASK ), | ||
| - KEY_REGULAR( "regular" ), | ||
| - MOUSE_LEFT( "button 1" ), | ||
| - MOUSE_WHEEL( "button 2" ), | ||
| - MOUSE_RIGHT( "button 3" ), | ||
| - MOUSE_LR( "button 1-3" ); | ||
| + KEY_CTRL( "ctrl", CTRL_MASK ), | ||
| + KEY_ALT( "alt", ALT_MASK ), | ||
| + KEY_REGULAR( "regular" ); | ||
| /** | ||
| */ | ||
| public boolean isModifier() { | ||
| - return this == KEY_ALT || this == KEY_CTRL || this == KEY_SHIFT; | ||
| + return mMask != NO_MASK; | ||
| } | ||
| * Returns the switch name. | ||
| * | ||
| - * @return The switch name, nothing more. | ||
| + * @return The switch name, not the enum name. | ||
| */ | ||
| @Override | ||
| -/* | ||
| - * Copyright 2020 White Magic Software, Ltd. | ||
| - * | ||
| - * All rights reserved. | ||
| - * | ||
| - * Redistribution and use in source and binary forms, with or without | ||
| - * modification, are permitted provided that the following conditions are met: | ||
| - * | ||
| - * o Redistributions of source code must retain the above copyright | ||
| - * notice, this list of conditions and the following disclaimer. | ||
| - * | ||
| - * o 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| - * "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 THE COPYRIGHT | ||
| - * HOLDER 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. | ||
| - */ | ||
| -package com.whitemagicsoftware.kmcaster; | ||
| - | ||
| -import javax.swing.*; | ||
| -import java.awt.*; | ||
| - | ||
| -/** | ||
| - * Responsible for drawing an image, which can be changed at any time. | ||
| - */ | ||
| -public class ImageComponent extends JComponent { | ||
| - /** | ||
| - * Mutable image. | ||
| - */ | ||
| - private Image mImage; | ||
| - | ||
| - public ImageComponent( final Image image ) { | ||
| - assert image != null; | ||
| - | ||
| - mImage = image; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public Dimension getPreferredSize() { | ||
| - // Race-condition guard. | ||
| - final var image = mImage; | ||
| - | ||
| - return new Dimension( | ||
| - image.getWidth( null ), image.getHeight( null ) | ||
| - ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - protected void paintComponent( final Graphics graphics ) { | ||
| - super.paintComponent( graphics ); | ||
| - | ||
| - final var g = (Graphics2D) graphics.create(); | ||
| - g.drawImage( mImage, 0, 0, this ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Repaints this component using the given image. This is a mutable | ||
| - * operation that changes the internal {@link Image} instance. | ||
| - * | ||
| - * @param image The new image to use for painting. | ||
| - */ | ||
| - public void redraw( final Image image ) { | ||
| - assert image != null; | ||
| - | ||
| - mImage = image; | ||
| - repaint(); | ||
| - } | ||
| -} | ||
| package com.whitemagicsoftware.kmcaster; | ||
| +import com.whitemagicsoftware.kmcaster.listeners.FrameDragListener; | ||
| import com.whitemagicsoftware.kmcaster.listeners.KeyboardListener; | ||
| import com.whitemagicsoftware.kmcaster.listeners.MouseListener; | ||
| import org.jnativehook.GlobalScreen; | ||
| import org.jnativehook.NativeHookException; | ||
| -import java.beans.PropertyChangeEvent; | ||
| +import javax.swing.*; | ||
| +import java.awt.*; | ||
| +import java.awt.geom.RoundRectangle2D; | ||
| import java.beans.PropertyChangeListener; | ||
| import java.util.logging.Level; | ||
| -import static com.whitemagicsoftware.kmcaster.HardwareState.ANY_KEY; | ||
| import static com.whitemagicsoftware.kmcaster.ui.FontLoader.initFonts; | ||
| import static java.util.logging.Logger.getLogger; | ||
| */ | ||
| @SuppressWarnings("unused") | ||
| -public class KmCaster extends EventFrame implements PropertyChangeListener { | ||
| +public class KmCaster extends JFrame { | ||
| + private static final Dimension FRAME_DIMENSIONS = new Dimension( 484, 70 ); | ||
| + private static final Dimension HARDWARE_DIMENSIONS = new Dimension( | ||
| + (int) FRAME_DIMENSIONS.getWidth(), | ||
| + (int) (FRAME_DIMENSIONS.getHeight() - 10) ); | ||
| + private static final Color TRANSPARENT = new Color( 0, 0, 0, 0 ); | ||
| + private static final Color TRANSLUCENT = new Color( .2f, .2f, .2f, 0.5f ); | ||
| + private static final float ARC = 8; | ||
| + | ||
| /** | ||
| * Fastest typing speed in words per minute. | ||
| */ | ||
| private final static float TYPING_SPEED_WPM = 216f; | ||
| + | ||
| /** | ||
| * Fastest typing speed in words per second. | ||
| */ | ||
| private final static float TYPING_SPEED_WPS = TYPING_SPEED_WPM / 60f; | ||
| + | ||
| /** | ||
| * Fastest typing speed in characters per second. | ||
| */ | ||
| private final static float TYPING_SPEED_CPS = TYPING_SPEED_WPS * 5.1f; | ||
| + | ||
| /** | ||
| * Fastest typing speed in characters per millisecond, which will | ||
| * govern the speed that any pressed key remains visible before showing | ||
| * as released, even if the typist released the key sooner. | ||
| */ | ||
| private final static float TYPING_SPEED_CPMS = TYPING_SPEED_CPS / 1000; | ||
| + | ||
| + private final HardwareImages mHardwareImages = | ||
| + new HardwareImages( HARDWARE_DIMENSIONS ); | ||
| + private final EventHandler mEventHandler = | ||
| + new EventHandler( mHardwareImages ); | ||
| public KmCaster() { | ||
| } | ||
| - /** | ||
| - * Called when a hardware switch has changed state. | ||
| - * | ||
| - * @param e Contains the identifier for the switch, its previous value, | ||
| - * and its new value. | ||
| - */ | ||
| - @Override | ||
| - public void propertyChange( final PropertyChangeEvent e ) { | ||
| - final var switchName = e.getPropertyName(); | ||
| - final var switchValue = e.getNewValue().toString(); | ||
| + private void init() { | ||
| + initWindowFrame(); | ||
| + initWindowContents(); | ||
| + initListeners(); | ||
| + pack(); | ||
| + setVisible( true ); | ||
| + } | ||
| - // True or false indicates a non-regular key was pressed. | ||
| - final var context = | ||
| - (!"false".equals( switchValue ) && !"true".equals( switchValue )) | ||
| - ? ANY_KEY | ||
| - : switchValue; | ||
| + private void initWindowFrame() { | ||
| + setDefaultCloseOperation( EXIT_ON_CLOSE ); | ||
| + setLocationRelativeTo( null ); | ||
| + setUndecorated( true ); | ||
| + setAlwaysOnTop( true ); | ||
| + setBackground( TRANSPARENT ); | ||
| + setSize( FRAME_DIMENSIONS ); | ||
| + setShape( createShape() ); | ||
| + } | ||
| - final var switchState = new HardwareState( e.getPropertyName(), context ); | ||
| - updateSwitchState( switchState ); | ||
| - updateSwitchLabel( switchState, switchValue ); | ||
| + private void initWindowContents() { | ||
| + final var switchPanel = new JPanel(); | ||
| + switchPanel.setAlignmentX( CENTER_ALIGNMENT ); | ||
| + switchPanel.setBackground( TRANSLUCENT ); | ||
| + | ||
| + // Added using the enumerated type definition declaration order. | ||
| + for( final var hwSwitch : HardwareSwitch.values() ) { | ||
| + switchPanel.add( mHardwareImages.get( hwSwitch ) ); | ||
| + } | ||
| + | ||
| + add( switchPanel ); | ||
| } | ||
| private void initListeners() { | ||
| + initWindowDragListener( this ); | ||
| + initMouseListener( getEventHandler() ); | ||
| + initKeyboardListener( getEventHandler() ); | ||
| + } | ||
| + | ||
| + private void initWindowDragListener( final JFrame listener ) { | ||
| + final var frameDragListener = new FrameDragListener( listener ); | ||
| + addMouseListener( frameDragListener ); | ||
| + addMouseMotionListener( frameDragListener ); | ||
| + } | ||
| + | ||
| + private void initMouseListener( final PropertyChangeListener listener ) { | ||
| final MouseListener mouseEventListener = new MouseListener(); | ||
| addNativeMouseListener( mouseEventListener ); | ||
| addNativeMouseMotionListener( mouseEventListener ); | ||
| addNativeMouseWheelListener( mouseEventListener ); | ||
| + } | ||
| + private void initKeyboardListener( final PropertyChangeListener listener ) { | ||
| final KeyboardListener keyboardListener = new KeyboardListener(); | ||
| addNativeKeyListener( keyboardListener ); | ||
| - keyboardListener.addPropertyChangeListener( this ); | ||
| + keyboardListener.addPropertyChangeListener( listener ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the shape for the application's window frame. | ||
| + * | ||
| + * @return A rounded rectangle. | ||
| + */ | ||
| + private Shape createShape() { | ||
| + return new RoundRectangle2D.Double( | ||
| + 0, 0, getWidth(), getHeight(), ARC, ARC | ||
| + ); | ||
| + } | ||
| + | ||
| + private EventHandler getEventHandler() { | ||
| + return mEventHandler; | ||
| } | ||
| final var kc = new KmCaster(); | ||
| - | ||
| - invokeLater( () -> { | ||
| - kc.setVisible( true ); | ||
| - kc.initListeners(); | ||
| - } ); | ||
| + invokeLater( kc::init ); | ||
| } | ||
| } | ||