| Author | DaveJarvis <email> |
|---|---|
| Date | 2020-07-19 16:16:55 GMT-0700 |
| Commit | b931a58b5fe5646520a53768f3ec2f98bf4301da |
| Parent | a8294b1 |
| package com.whitemagicsoftware.kmcaster; | ||
| -import javax.swing.*; | ||
| +import com.whitemagicsoftware.kmcaster.ui.AutofitLabel; | ||
| + | ||
| import java.awt.*; | ||
| -import java.awt.font.TextLayout; | ||
| import java.beans.PropertyChangeEvent; | ||
| import java.beans.PropertyChangeListener; | ||
| final var switchValue = e.getNewValue().toString(); | ||
| - // True or false indicates a non-regular key was pressed. | ||
| + // True indicates a modifier key was pressed; false indicates a key was | ||
| + // released (doesn't matter what kind of key). | ||
| final var context = | ||
| (!"false".equals( switchValue ) && !"true".equals( switchValue )) | ||
| 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 ); | ||
| + final var label = new AutofitLabel( value, LABEL_FONT, LABEL_COLOUR ); | ||
| 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( LABEL_FONT ); | ||
| - label.setSize( width, height ); | ||
| - label.setForeground( LABEL_COLOUR ); | ||
| - | ||
| - final var scaledFont = scaleFont( 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}. | ||
| - */ | ||
| - public Font scaleFont( | ||
| - final JLabel label, final Rectangle dst, final Graphics graphics ) { | ||
| - assert label != null; | ||
| - assert dst != null; | ||
| - assert graphics != null; | ||
| - final var frc = ((Graphics2D) graphics).getFontRenderContext(); | ||
| - | ||
| - final var dstWidthPx = dst.getWidth(); | ||
| - final var dstHeightPx = dst.getHeight(); | ||
| - | ||
| - final var text = label.getText(); | ||
| - | ||
| - var minSizePt = 1f; | ||
| - var maxSizePt = 1000f; | ||
| - var scaledFont = label.getFont(); | ||
| - float scaledPt = scaledFont.getSize(); | ||
| - | ||
| - while( maxSizePt - minSizePt > 1f ) { | ||
| - scaledFont = scaledFont.deriveFont( scaledPt ); | ||
| - | ||
| - final var layout = new TextLayout( text, scaledFont, frc ); | ||
| - final var fontWidthPx = layout.getVisibleAdvance(); | ||
| - final var metrics = scaledFont.getLineMetrics( text, frc ); | ||
| - final var fontHeightPx = metrics.getHeight(); | ||
| - | ||
| - if( (fontWidthPx > dstWidthPx) || (fontHeightPx > dstHeightPx) ) { | ||
| - maxSizePt = scaledPt; | ||
| - } | ||
| - else { | ||
| - minSizePt = scaledPt; | ||
| } | ||
| - | ||
| - scaledPt = (minSizePt + maxSizePt) / 2; | ||
| } | ||
| - | ||
| - // Round down to guarantee fit. | ||
| - return scaledFont.deriveFont( (float) Math.floor( scaledPt ) ); | ||
| } | ||
| } | ||
| */ | ||
| public class HardwareImages { | ||
| - | ||
| private final static String DIR_IMAGES = "/images"; | ||
| private final static String DIR_IMAGES_KEYBOARD = DIR_IMAGES + "/key"; | ||
| private final static String DIR_IMAGES_MOUSE = DIR_IMAGES + "/mouse"; | ||
| private final static SvgRasterizer sRasterizer = new SvgRasterizer(); | ||
| private final Map<HardwareSwitch, HardwareComponent<HardwareState, Image>> | ||
| mSwitches = new HashMap<>(); | ||
| - private final Dimension mDimension; | ||
| + | ||
| + /** | ||
| + * Images are scaled to these dimensions, maintaining aspect ratio. The | ||
| + * height constrains the width, so as long as the width is sufficiently | ||
| + * large, the application's window will adjust to fit. | ||
| + */ | ||
| + private final Dimension mDimension = new Dimension( 1024, 60 ); | ||
| /** | ||
| * Constructs an enumerated type that represents the different types of | ||
| * images shown when keyboard and mouse events are triggered. | ||
| - * | ||
| - * @param dimension The image will be scaled to the given dimensions, aspect | ||
| - * ratio is maintained. | ||
| */ | ||
| - public HardwareImages( final Dimension dimension ) { | ||
| - assert dimension != null; | ||
| - | ||
| - mDimension = dimension; | ||
| - | ||
| + public HardwareImages() { | ||
| final var mouseReleased = mouseImage( "0" ); | ||
| final var mouseStates = new HardwareComponent<HardwareState, Image>(); | ||
| private Image createImage( final String path ) { | ||
| - assert mDimension != null; | ||
| - | ||
| final var resource = format( "%s.svg", path ); | ||
| @SuppressWarnings("unused") | ||
| 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 final static float TYPING_SPEED_CPMS = TYPING_SPEED_CPS / 1000; | ||
| - private final HardwareImages mHardwareImages = | ||
| - new HardwareImages( HARDWARE_DIMENSIONS ); | ||
| + private final HardwareImages mHardwareImages = new HardwareImages(); | ||
| private final EventHandler mEventHandler = | ||
| new EventHandler( mHardwareImages ); | ||
| setAlwaysOnTop( true ); | ||
| setBackground( TRANSPARENT ); | ||
| - setSize( FRAME_DIMENSIONS ); | ||
| setShape( createShape() ); | ||
| } | ||
| +/* | ||
| + * 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.ui; | ||
| + | ||
| +import javax.swing.*; | ||
| +import java.awt.*; | ||
| +import java.awt.font.FontRenderContext; | ||
| +import java.awt.font.TextLayout; | ||
| + | ||
| +import static java.awt.event.HierarchyEvent.PARENT_CHANGED; | ||
| + | ||
| +/** | ||
| + * Responsible for changing the {@link JLabel}'s font size | ||
| + */ | ||
| +public class AutofitLabel extends JLabel { | ||
| + | ||
| + /** | ||
| + * Constructs an instance of {@link AutofitLabel} that will rescale itself | ||
| + * to the parent {@link Container}, automatically. | ||
| + * | ||
| + * @param text The text to write on the container's graphics context. | ||
| + * @param font The font to use when writing the text. | ||
| + * @param color The colour to use when writing hte text. | ||
| + */ | ||
| + public AutofitLabel( final String text, final Font font, final Color color ) { | ||
| + super( text ); | ||
| + setFont( font ); | ||
| + setForeground( color ); | ||
| + | ||
| + addHierarchyListener( e -> { | ||
| + if( (e.getChangeFlags() & PARENT_CHANGED) != 0 ) { | ||
| + if( getParent() == e.getChangedParent() ) { | ||
| + rescale(); | ||
| + } | ||
| + } | ||
| + } ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Rescales the constructed font to fit within the bounds of the parent | ||
| + * {@link Container}. This must only be called after the parent | ||
| + * {@link Container} has been set, otherwise there will be no | ||
| + * {@link Graphics} context available to compute the maximum {@link Font} | ||
| + * size that will fit the parent's {@link Rectangle} bounds. | ||
| + */ | ||
| + private void rescale() { | ||
| + final var component = getParent(); | ||
| + final var bounds = component.getBounds(); | ||
| + final var insets = component.getInsets(); | ||
| + | ||
| + bounds.x += insets.left; | ||
| + bounds.y += insets.top; | ||
| + bounds.width -= insets.left + insets.right; | ||
| + bounds.height -= insets.top + insets.bottom; | ||
| + | ||
| + setLocation( insets.left, insets.top ); | ||
| + setSize( bounds.width, bounds.height ); | ||
| + setFont( computeScaledFont() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Calculates a new {@link Font} size such that it fits within the bounds | ||
| + * of this label instance. This uses the label's current size, which must | ||
| + * be set prior to calling this method. | ||
| + * | ||
| + * @return A new {@link Font} instance that is guaranteed to write the given | ||
| + * string within the bounds of the given {@link Rectangle}. | ||
| + */ | ||
| + private Font computeScaledFont() { | ||
| + final var frc = getFontRenderContext(); | ||
| + final var text = getText(); | ||
| + | ||
| + final var dstWidthPx = getWidth(); | ||
| + final var dstHeightPx = getHeight(); | ||
| + | ||
| + var minSizePt = 1f; | ||
| + var maxSizePt = 200f; | ||
| + var scaledFont = getFont(); | ||
| + float scaledPt = scaledFont.getSize(); | ||
| + | ||
| + while( maxSizePt - minSizePt > 1f ) { | ||
| + scaledFont = scaledFont.deriveFont( scaledPt ); | ||
| + | ||
| + final var layout = new TextLayout( text, scaledFont, frc ); | ||
| + final var fontWidthPx = layout.getVisibleAdvance(); | ||
| + final var metrics = scaledFont.getLineMetrics( text, frc ); | ||
| + final var fontHeightPx = metrics.getHeight(); | ||
| + | ||
| + if( (fontWidthPx > dstWidthPx) || (fontHeightPx > dstHeightPx) ) { | ||
| + maxSizePt = scaledPt; | ||
| + } | ||
| + else { | ||
| + minSizePt = scaledPt; | ||
| + } | ||
| + | ||
| + scaledPt = (minSizePt + maxSizePt) / 2; | ||
| + } | ||
| + | ||
| + // Round down to guarantee fit. | ||
| + return scaledFont.deriveFont( (float) Math.floor( scaledPt ) ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Gets the {@link FontRenderContext} for the parent {@link Container}'s | ||
| + * {@link Graphics} context, casting it to a {@link Graphics2D} context. | ||
| + * | ||
| + * @return The parent's {@link Graphics2D} context. | ||
| + */ | ||
| + private FontRenderContext getFontRenderContext() { | ||
| + return ((Graphics2D) getGraphics()).getFontRenderContext(); | ||
| + } | ||
| +} | ||
| Delta | 151 lines added, 114 lines removed, 37-line increase |
|---|