/* * ***************************************************************************** * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * **************************************************************************** */ package io.github.dsheirer.gui.control; import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.InvalidFrequencyException; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; import java.awt.Color; import java.awt.EventQueue; import java.awt.Font; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import net.miginfocom.swing.MigLayout; import org.apache.commons.math3.util.FastMath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.UIManager; /** * Custom frequency controller for displaying or editing frequency values in MHz. */ public class JFrequencyControl extends JPanel implements ISourceEventProcessor { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class); private List mProcessors = new ArrayList<>(); private Color mHighlightColor = Color.YELLOW; private long mFrequency; private HashMap mDigits = new HashMap(); /** * Constructor */ public JFrequencyControl() { init(); } /** * Initializes the control/layout. */ private void init() { setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", "")); Font font = new Font(Font.MONOSPACED, Font.BOLD, 30); for(int x = 9; x >= 0; x--) { Digit digit = null; try { digit = new Digit(x); } catch(ParseException e) { mLog.error("JFrequencyControl - parse exception constructing a digit - " + e); } if(digit != null) { mDigits.put(x, digit); add(digit); digit.setFont(font); if(x == 6) { add(new JLabel(". ")); } } } revalidate(); } /** * Enables or disables the control. * @param enabled true if this component should be enabled, false otherwise */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); for(Digit digit : mDigits.values()) { digit.setEnabled(enabled); } } /** * Receives a frequency change event invoked by another control. We don't * rebroadcast this event, just set the control to indicate the new frequency. */ @Override public void process(SourceEvent event) { switch(event.getEvent()) { case NOTIFICATION_FREQUENCY_CHANGE: EventQueue.invokeLater(() -> setFrequency(event.getValue().longValue(), false)); break; } } /** * Loads the frequency into the display and optionally fires a change event * to all registered listeners */ public void setFrequency(long frequency, boolean fireChangeEvent) { mFrequency = frequency; for(Digit digit : mDigits.values()) { digit.setFrequency(frequency, fireChangeEvent); } } /** * Current frequency value for the control. * @return frequency */ public long getFrequency() { return mFrequency; } /** * Updates the frequency value according to the manually changed digit value. */ private void updateFrequency() { long frequency = 0; for(Digit digit : mDigits.values()) { frequency += digit.getFrequency(); } mFrequency = frequency; } /** * Fires a source changed event * @throws SourceException of there's an error */ private void fireSourceChanged() throws SourceException { updateFrequency(); Iterator it = mProcessors.iterator(); SourceEvent event = SourceEvent.frequencyRequest(mFrequency); while(it.hasNext()) { it.next().process(event); } } /** * Adds the source event listener * @param processor */ public void addListener(ISourceEventProcessor processor) { if(!mProcessors.contains(processor)) { mProcessors.add(processor); } } /** * Remove all source event processors */ public void clearListeners() { mProcessors.clear(); } /** * Removes the source event processor * @param processor */ public void removeListener(ISourceEventProcessor processor) { mProcessors.remove(processor); } /** * Custom single digit value editor. */ public class Digit extends JTextField { private static final long serialVersionUID = 1L; private int mPower = 0; private long mValue = 0; /** * Constructor * @param position of the digit where a position of 1 is the ones unit of MHz and a position of -1 is the tenths * unit of MHz. * @throws ParseException if there is an error */ private Digit(int position) throws ParseException { super("0"); setColumns(1); mPower = position; setToolTipText(getTooltip(mPower)); Listener listener = new Listener(); this.addKeyListener(listener); this.addMouseListener(listener); this.addMouseWheelListener(listener); } /** * Generates a custom tooltip for the current hover-over digit. * @param power of the digit in units of 10 * @return custom tooptip. */ private static String getTooltip(int power) { StringBuilder sb = new StringBuilder(); sb.append("Center frequency "); switch(power) { case 0: sb.append("1 Hertz"); break; case 1: sb.append("10 Hertz"); break; case 2: sb.append("100 Hertz"); break; case 3: sb.append("1 Kilohertz"); break; case 4: sb.append("10 Kilohertz"); break; case 5: sb.append("100 Kilohertz"); break; case 6: sb.append("1 Megahertz"); break; case 7: sb.append("10 Megahertz"); break; case 8: sb.append("100 Megahertz"); break; case 9: sb.append("1 Gigahertz"); break; } sb.append(" units. Type a number, use up/down arrows, or click the upper/lower digit control to adjust"); return sb.toString(); } /** * Sets this digit to the value of the column in frequency that corresponds * to the power (ie column) set for this digit. Optionally, fires a * value change event to all listeners. */ public void setFrequency(long frequency, boolean fireChangeEvent) { //Strip the digits higher than this one long lower = frequency % (long)(FastMath.pow(10, mPower + 1)); //Set the value to int value of dividing by 10 to this power long value = (long)(lower / (long)(FastMath.pow(10, mPower))); set(value, fireChangeEvent); } /** * Frequency value. * @return frequency value. */ public long getFrequency() { return mValue * (long)FastMath.pow(10, mPower); } /** * Increments the unit value and fires a change event */ public void increment() { increment(true); } /** * Increments the unit value and fires a change event * @param fireChangeEvent true to fire an event */ public void increment(boolean fireChangeEvent) { if(isEnabled()) { set(mValue + 1, fireChangeEvent); } } /** * Decrements the unit value and fires a change event. */ public void decrement() { decrement(true); } /** * Decrements the unit value and optionally fires a change event. * @param fireChangeEvent set to true to fire an event */ public void decrement(boolean fireChangeEvent) { if(isEnabled()) { set(mValue - 1, fireChangeEvent); } } /** * Convenience wrapper to change amount and fire change event */ private void set(long amount) { set(amount, true); } /** * Changes the value and optionally fires change event to listeners */ private void set(long amount, boolean fireChangeEvent) { long previous = mValue; boolean higherDigitIncremented = false; boolean higherDigitDecremented = false; mValue = amount; while(mValue < 0) { mValue += 10; Digit nextHigherDigit = mDigits.get(mPower + 1); if(nextHigherDigit != null) { nextHigherDigit.decrement(false); higherDigitDecremented = true; } } while(mValue > 9) { mValue -= 10; Digit nextHigherDigit = mDigits.get(mPower + 1); if(nextHigherDigit != null) { nextHigherDigit.increment(false); higherDigitIncremented = true; } } if(fireChangeEvent) { try { fireSourceChanged(); } catch(SourceException se) { mValue = previous; if(higherDigitIncremented) { Digit nextHigherDigit = mDigits.get(mPower + 1); if(nextHigherDigit != null) { nextHigherDigit.decrement(false); } } if(higherDigitDecremented) { Digit nextHigherDigit = mDigits.get(mPower + 1); if(nextHigherDigit != null) { nextHigherDigit.increment(false); } } if(se instanceof InvalidFrequencyException ife) { JOptionPane.showMessageDialog(this, ife.getMessage() + " for this tuner."); } } } setText(String.valueOf(mValue)); repaint(); } /** * Multiple Listener interface implementation */ private class Listener implements KeyListener, MouseListener, MouseWheelListener { @Override public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); switch(key) { case KeyEvent.VK_0: case KeyEvent.VK_NUMPAD0: set(0); Digit.this.transferFocus(); break; case KeyEvent.VK_1: case KeyEvent.VK_NUMPAD1: set(1); Digit.this.transferFocus(); break; case KeyEvent.VK_2: case KeyEvent.VK_NUMPAD2: set(2); Digit.this.transferFocus(); break; case KeyEvent.VK_3: case KeyEvent.VK_NUMPAD3: set(3); Digit.this.transferFocus(); break; case KeyEvent.VK_4: case KeyEvent.VK_NUMPAD4: set(4); Digit.this.transferFocus(); break; case KeyEvent.VK_5: case KeyEvent.VK_NUMPAD5: set(5); Digit.this.transferFocus(); break; case KeyEvent.VK_6: case KeyEvent.VK_NUMPAD6: set(6); Digit.this.transferFocus(); break; case KeyEvent.VK_7: case KeyEvent.VK_NUMPAD7: set(7); Digit.this.transferFocus(); break; case KeyEvent.VK_8: case KeyEvent.VK_NUMPAD8: set(8); Digit.this.transferFocus(); break; case KeyEvent.VK_9: case KeyEvent.VK_NUMPAD9: set(9); Digit.this.transferFocus(); break; case KeyEvent.VK_LEFT: Digit.this.transferFocusBackward(); break; case KeyEvent.VK_RIGHT: Digit.this.transferFocus(); break; case KeyEvent.VK_UP: increment(); break; case KeyEvent.VK_DOWN: decrement(); break; case KeyEvent.VK_TAB: break; default: set(mValue); break; } repaint(); } @Override public void keyPressed(KeyEvent e) {} @Override public void keyTyped(KeyEvent e) {} @Override public void mouseClicked(MouseEvent e) { switch(e.getButton()) { case MouseEvent.BUTTON1: increment(); break; case MouseEvent.BUTTON2: break; case MouseEvent.BUTTON3: decrement(); break; } } public void mousePressed(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) { Color bg = UIManager.getColor("TextField.selectionBackground"); Color fg = UIManager.getColor("TextField.selectionForeground"); Digit.this.setBackground(bg != null ? bg : Color.YELLOW); Digit.this.setForeground(fg != null ? fg : Color.BLACK); repaint(); } @Override public void mouseExited(MouseEvent e) { Color bg = UIManager.getColor("TextField.background"); Color fg = UIManager.getColor("TextField.foreground"); Digit.this.setBackground(bg != null ? bg : Color.WHITE); Digit.this.setForeground(fg != null ? fg : Color.BLACK); repaint(); } @Override public void mouseWheelMoved(MouseWheelEvent e) { set(mValue - e.getWheelRotation()); } } } }