mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
* WIP - Adds support for aliasing P25 Phase 2 tones.Playlist editor - adds editor support for alias actions * Playlist editor implementation and integration into application is complete. Adds JavaFX window monitoring to persist window size/location across application runs. Co-authored-by: Denny <denny@denny-desktop>
This commit is contained in:
139
src/main/java/io/github/dsheirer/gui/control/HexFormatter.java
Normal file
139
src/main/java/io/github/dsheirer/gui/control/HexFormatter.java
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Text formatter for integer values displayed and edited as hexadecimal values that constrains values to
|
||||
* specified minimum and maximum valid values.
|
||||
*/
|
||||
public class HexFormatter extends TextFormatter<Integer>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(HexFormatter.class);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param minimum allowed value
|
||||
* @param maximum allowed value
|
||||
*/
|
||||
public HexFormatter(int minimum, int maximum)
|
||||
{
|
||||
super(new HexIntegerStringConverter(), null, new HexFilter(minimum, maximum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted text change filter that only allows hexadecimal characters where the converted decimal value
|
||||
* is also constrained within minimum and maximum valid values.
|
||||
*/
|
||||
public static class HexFilter implements UnaryOperator<TextFormatter.Change>
|
||||
{
|
||||
private String HEXADECIMAL_REGEX = "[0-9A-Fa-f].*";
|
||||
private int mMinimum;
|
||||
private int mMaximum;
|
||||
|
||||
public HexFilter(int minimum, int maximum)
|
||||
{
|
||||
mMinimum = minimum;
|
||||
mMaximum = maximum;
|
||||
}
|
||||
|
||||
private boolean isValid(Integer value)
|
||||
{
|
||||
return value != null && mMinimum <= value && value <= mMaximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change)
|
||||
{
|
||||
//Only validate if the user added text to the control. Otherwise, allow it to go through
|
||||
if(change.getText() != null)
|
||||
{
|
||||
String updatedText = change.getControlNewText();
|
||||
|
||||
if(updatedText == null || updatedText.isEmpty())
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
if(!updatedText.matches(HEXADECIMAL_REGEX) ||
|
||||
!isValid(HexIntegerStringConverter.getValue(change.getControlNewText())))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Force all hex letters to uppercase
|
||||
if(change.getText().matches("[a-f].*"))
|
||||
{
|
||||
change.setText(change.getText().toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hexadecimal integer string converter. Obtain value as an integer but user edits hexadecimal values.
|
||||
*/
|
||||
public static class HexIntegerStringConverter extends StringConverter<Integer>
|
||||
{
|
||||
@Override
|
||||
public String toString(Integer value)
|
||||
{
|
||||
// If the specified value is null, return a zero-length String
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return (Integer.toHexString(value.intValue()).toUpperCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer fromString(String value)
|
||||
{
|
||||
return getValue(value);
|
||||
}
|
||||
|
||||
public static Integer getValue(String value)
|
||||
{
|
||||
// If the specified value is null or zero-length, return null
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
if (value.length() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Integer.valueOf(value, 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasItemEditor;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Text formatter for integer values that constrains values to specified minimum and maximum valid values.
|
||||
*/
|
||||
public class IntegerFormatter extends TextFormatter<Integer>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AliasItemEditor.class);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param minimum allowed value
|
||||
* @param maximum allowed value
|
||||
*/
|
||||
public IntegerFormatter(int minimum, int maximum)
|
||||
{
|
||||
super(new IntegerStringConverter(), null, new IntegerFilter(minimum, maximum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted text change filter that only allows hexadecimal characters where the converted decimal value
|
||||
* is also constrained within minimum and maximum valid values.
|
||||
*/
|
||||
public static class IntegerFilter implements UnaryOperator<Change>
|
||||
{
|
||||
private String DECIMAL_REGEX = "[0-9].*";
|
||||
private int mMinimum;
|
||||
private int mMaximum;
|
||||
|
||||
public IntegerFilter(int minimum, int maximum)
|
||||
{
|
||||
mMinimum = minimum;
|
||||
mMaximum = maximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the value argument is parsable as an integer, or is empty or null.
|
||||
*/
|
||||
private boolean isValid(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int parsed = Integer.parseInt(value);
|
||||
return mMinimum <= parsed && parsed <= mMaximum;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change)
|
||||
{
|
||||
//Only validate if the user added text to the control. Otherwise, allow it to go through
|
||||
if(change.getText() != null)
|
||||
{
|
||||
String updatedText = change.getControlNewText();
|
||||
|
||||
if(updatedText == null || updatedText.isEmpty())
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
if(!updatedText.matches(DECIMAL_REGEX) || !isValid(updatedText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Text field control for integer values.
|
||||
*/
|
||||
public class IntegerTextField extends TextField
|
||||
{
|
||||
private static final String REGEX = "[1-9][0-9]*";
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public IntegerTextField()
|
||||
{
|
||||
UnaryOperator<TextFormatter.Change> filter = change ->
|
||||
{
|
||||
String newText = change.getControlNewText();
|
||||
|
||||
if(newText.isEmpty() || newText.matches(REGEX))
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
setTextFormatter(new TextFormatter<Integer>(new IntegerStringConverter(), 0, filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Current integer value of the control, or null if the value is not parsable as an integer
|
||||
*/
|
||||
public Integer get()
|
||||
{
|
||||
String rawText = getText();
|
||||
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(rawText);
|
||||
}
|
||||
catch(Exception pe)
|
||||
{
|
||||
//Do nothing ... we couldn't parse the value
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the integer value in the control
|
||||
*/
|
||||
public void set(int value)
|
||||
{
|
||||
setText(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
230
src/main/java/io/github/dsheirer/gui/control/LtrFormatter.java
Normal file
230
src/main/java/io/github/dsheirer/gui/control/LtrFormatter.java
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Text formatter for integer values displayed and edited as LTR formatted values that constrains values to
|
||||
* specified minimum and maximum valid values.
|
||||
*/
|
||||
public class LtrFormatter extends TextFormatter<Integer>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(LtrFormatter.class);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param minimum allowed value
|
||||
* @param maximum allowed value
|
||||
*/
|
||||
public LtrFormatter(int minimum, int maximum)
|
||||
{
|
||||
super(new LtrIntegerStringConverter(), null, new LtrFilter(minimum, maximum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted text change filter that only allows formatted characters where the converted decimal value
|
||||
* is also constrained within minimum and maximum valid values.
|
||||
*/
|
||||
public static class LtrFilter implements UnaryOperator<Change>
|
||||
{
|
||||
private int mMinimum;
|
||||
private int mMaximum;
|
||||
|
||||
public LtrFilter(int minimum, int maximum)
|
||||
{
|
||||
mMinimum = minimum;
|
||||
mMaximum = maximum;
|
||||
}
|
||||
|
||||
private boolean isValid(Integer value)
|
||||
{
|
||||
return value != null && mMinimum <= value && value <= mMaximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change)
|
||||
{
|
||||
String updatedText = change.getControlNewText();
|
||||
|
||||
if(updatedText == null || updatedText.isEmpty())
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
if(LtrIntegerStringConverter.isValid(updatedText))
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LTR format integer string converter. Obtain value as an integer but user edits formatted values.
|
||||
*/
|
||||
public static class LtrIntegerStringConverter extends StringConverter<Integer>
|
||||
{
|
||||
private static final Pattern LTR_PATTERN = Pattern.compile("([01]?)(?:-(\\d{0,2})(?:-(\\d{0,3}))?)?");
|
||||
|
||||
private static final int AREA_MASK = 0x2000;
|
||||
private static final int LCN_MASK = 0x1F00;
|
||||
private static final int TALKGROUP_MASK = 0xFF;
|
||||
|
||||
@Override
|
||||
public String toString(Integer value)
|
||||
{
|
||||
// If the specified value is null, return a zero-length String
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
int area = (value & AREA_MASK) >> 13;
|
||||
int lcn = (value & LCN_MASK) >> 8;
|
||||
int talkgroup = (value & TALKGROUP_MASK);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(area);
|
||||
sb.append("-");
|
||||
sb.append(String.format("%02d", lcn));
|
||||
sb.append("-");
|
||||
sb.append(String.format("%03d", talkgroup));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer fromString(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher m = LTR_PATTERN.matcher(value);
|
||||
|
||||
if(m.matches())
|
||||
{
|
||||
int a = 0;
|
||||
int b = 0;
|
||||
int c = 0;
|
||||
|
||||
try
|
||||
{
|
||||
a = Integer.parseInt(m.group(1));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
b = Integer.parseInt(m.group(2));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
c = Integer.parseInt(m.group(3));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
|
||||
int talkgroup = (a << 13) + (b << 8) + c;
|
||||
|
||||
return talkgroup;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isValid(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Matcher m = LTR_PATTERN.matcher(value);
|
||||
|
||||
if(m.matches())
|
||||
{
|
||||
//We don't have to check the first digit - it will only match a 0 or 1
|
||||
String part2 = m.group(2);
|
||||
|
||||
if(part2 != null && !part2.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
int value2 = Integer.parseInt(part2);
|
||||
|
||||
if(value2 < 0 || value2 > 31)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
String part3 = m.group(3);
|
||||
|
||||
if(part3 != null && !part3.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
int value3 = Integer.parseInt(part3);
|
||||
|
||||
if(value3 < 0 || value3 > 255)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
//if part 2 and/or part 3 is null or empty then valid = true
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
public class MaxLengthUnaryOperator implements UnaryOperator<TextFormatter.Change>
|
||||
{
|
||||
private int mMaxLength;
|
||||
|
||||
public MaxLengthUnaryOperator(int maxLength)
|
||||
{
|
||||
mMaxLength = maxLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextFormatter.Change apply(TextFormatter.Change change)
|
||||
{
|
||||
if(change.isContentChange() && change.getControlNewText().length() > mMaxLength)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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 <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.DefaultCellEditor;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFormattedTextField;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.text.DefaultFormatterFactory;
|
||||
import javax.swing.text.NumberFormatter;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Multiple frequency editor control for editing an ordered list of frequencies. Frequency values are persisted as
|
||||
* long values in hertz, but presented to the user as double values in megahertz.
|
||||
*
|
||||
*/
|
||||
public class MultipleFrequencyEditor extends JPanel
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MultipleFrequencyEditor.class);
|
||||
private static final Long DEFAULT_FREQUENCY = 0l;
|
||||
private static final Icon ICON_ADD = IconFontSwing.buildIcon(FontAwesome.PLUS, 10);
|
||||
private static final Icon ICON_DOWN = IconFontSwing.buildIcon(FontAwesome.ARROW_DOWN, 10);
|
||||
private static final Icon ICON_REMOVE = IconFontSwing.buildIcon(FontAwesome.MINUS, 10);
|
||||
private static final Icon ICON_UP = IconFontSwing.buildIcon(FontAwesome.ARROW_UP, 10);
|
||||
private DecimalFormat mDecimalFormat = new DecimalFormat("0.000000");
|
||||
private FrequencyModel mFrequencyModel = new FrequencyModel();
|
||||
private JTable mFrequencyTable;
|
||||
private JButton mAddButton;
|
||||
private JButton mRemoveButton;
|
||||
private JButton mMoveUpButton;
|
||||
private JButton mMoveDownButton;
|
||||
|
||||
/**
|
||||
* Constructs the editor
|
||||
* @param minimum allowable frequency value
|
||||
* @param maximum allowable frequency value
|
||||
*/
|
||||
public MultipleFrequencyEditor(double minimum, double maximum)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[]1[grow,fill]1[]", "[]1[]1[top,grow,fill]"));
|
||||
|
||||
|
||||
mMoveUpButton = new JButton();
|
||||
mMoveUpButton.setIcon(ICON_UP);
|
||||
mMoveUpButton.setEnabled(false);
|
||||
mMoveUpButton.addActionListener(e -> {
|
||||
int selectedRow = mFrequencyTable.getSelectedRow();
|
||||
int index = mFrequencyTable.convertRowIndexToModel(selectedRow);
|
||||
mFrequencyModel.moveUp(index);
|
||||
mFrequencyTable.getSelectionModel().setLeadSelectionIndex(index - 1);
|
||||
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(selectedRow, 0, true)));
|
||||
|
||||
updateButtons();
|
||||
});
|
||||
add(mMoveUpButton);
|
||||
|
||||
mFrequencyTable = new JTable(mFrequencyModel);
|
||||
//testing ...
|
||||
mFrequencyTable.setPreferredScrollableViewportSize(new Dimension(100, 65));
|
||||
mFrequencyTable.setFillsViewportHeight(true);
|
||||
|
||||
mFrequencyTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
mFrequencyTable.getSelectionModel().addListSelectionListener(new SelectionListener());
|
||||
mFrequencyTable.setDefaultEditor(Long.class, new FrequencyEditor(minimum, maximum));
|
||||
mFrequencyTable.setDefaultRenderer(Long.class, new FrequencyRenderer());
|
||||
JScrollPane listScrollPane = new JScrollPane(mFrequencyTable);
|
||||
add(listScrollPane, "span 1 3");
|
||||
|
||||
mAddButton = new JButton();
|
||||
mAddButton.setIcon(ICON_ADD);
|
||||
mAddButton.addActionListener(e -> {
|
||||
int selectedRow = mFrequencyTable.getSelectedRow();
|
||||
if(selectedRow >= 0)
|
||||
{
|
||||
int index = mFrequencyTable.convertRowIndexToModel(selectedRow);
|
||||
int nextIndex = index + 1;
|
||||
mFrequencyModel.add(DEFAULT_FREQUENCY, nextIndex);
|
||||
mFrequencyTable.requestFocus();
|
||||
mFrequencyTable.setRowSelectionInterval(nextIndex, nextIndex);
|
||||
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(nextIndex, 0, true)));
|
||||
mFrequencyTable.editCellAt(nextIndex, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
mFrequencyModel.add(DEFAULT_FREQUENCY);
|
||||
int lastIndex = mFrequencyModel.getRowCount() - 1;
|
||||
mFrequencyTable.setRowSelectionInterval(lastIndex, lastIndex);
|
||||
mFrequencyTable.editCellAt(lastIndex, 0);
|
||||
}
|
||||
|
||||
mFrequencyTable.setSurrendersFocusOnKeystroke(true);
|
||||
mFrequencyTable.getEditorComponent().requestFocus();
|
||||
|
||||
updateButtons();
|
||||
});
|
||||
add(mAddButton, "wrap");
|
||||
|
||||
mMoveDownButton = new JButton();
|
||||
mMoveDownButton.setIcon(ICON_DOWN);
|
||||
mMoveDownButton.setEnabled(false);
|
||||
mMoveDownButton.addActionListener(e -> {
|
||||
int selectedRow = mFrequencyTable.getSelectedRow();
|
||||
int index = mFrequencyTable.convertRowIndexToModel(selectedRow);
|
||||
mFrequencyModel.moveDown(index);
|
||||
mFrequencyTable.getSelectionModel().setLeadSelectionIndex(index + 1);
|
||||
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(selectedRow, 0, true)));
|
||||
|
||||
updateButtons();
|
||||
});
|
||||
add(mMoveDownButton);
|
||||
|
||||
mRemoveButton = new JButton();
|
||||
mRemoveButton.setIcon(ICON_REMOVE);
|
||||
mRemoveButton.setEnabled(false);
|
||||
mRemoveButton.addActionListener(e -> {
|
||||
int selectedRow = mFrequencyTable.getSelectedRow();
|
||||
int selectedIndex = mFrequencyTable.convertRowIndexToModel(selectedRow);
|
||||
mFrequencyModel.remove(selectedIndex);
|
||||
|
||||
if(selectedIndex < mFrequencyModel.getRowCount())
|
||||
{
|
||||
int reselectedRow = mFrequencyTable.convertRowIndexToView(selectedIndex);
|
||||
mFrequencyTable.setRowSelectionInterval(reselectedRow, reselectedRow);
|
||||
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(reselectedRow, 0, true)));
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
});
|
||||
add(mRemoveButton);
|
||||
}
|
||||
|
||||
public FrequencyModel getFrequencyModel()
|
||||
{
|
||||
return mFrequencyModel;
|
||||
}
|
||||
|
||||
private void updateButtons()
|
||||
{
|
||||
int index = mFrequencyTable.getSelectedRow();
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
mMoveUpButton.setEnabled(index > 0);
|
||||
mMoveDownButton.setEnabled(index < mFrequencyModel.getRowCount() - 1);
|
||||
mRemoveButton.setEnabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
mRemoveButton.setEnabled(false);
|
||||
mMoveUpButton.setEnabled(false);
|
||||
mMoveDownButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the frequency value as MHz
|
||||
*/
|
||||
private String format(long frequency)
|
||||
{
|
||||
return mDecimalFormat.format(frequency / 1e6d);
|
||||
}
|
||||
|
||||
public class FrequencyModel extends AbstractTableModel
|
||||
{
|
||||
private List<Long> mFrequencies = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column)
|
||||
{
|
||||
return "Frequencies (MHz)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount()
|
||||
{
|
||||
return mFrequencies.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex)
|
||||
{
|
||||
return mFrequencies.get(rowIndex);
|
||||
}
|
||||
|
||||
public void setValueAt(int index, Long value)
|
||||
{
|
||||
mFrequencies.set(index, value);
|
||||
fireTableRowsUpdated(index, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex)
|
||||
{
|
||||
return Long.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a frequency to the model
|
||||
*/
|
||||
public void add(long frequency, int index)
|
||||
{
|
||||
mFrequencies.add(index, frequency);
|
||||
fireTableRowsInserted(index, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a frequency to the model
|
||||
*/
|
||||
public void add(long frequency)
|
||||
{
|
||||
mFrequencies.add(frequency);
|
||||
fireTableRowsInserted(mFrequencies.size() - 1, mFrequencies.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an indexed value from the model
|
||||
*/
|
||||
public Long remove(int index)
|
||||
{
|
||||
Long value = mFrequencies.remove(index);
|
||||
fireTableRowsDeleted(index, index);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all frequencies from the model
|
||||
*/
|
||||
public void clear()
|
||||
{
|
||||
mFrequencies.clear();
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the frequency up in the list
|
||||
*/
|
||||
public void moveUp(int index)
|
||||
{
|
||||
if(index > 0 && index < mFrequencies.size())
|
||||
{
|
||||
Long valueA = mFrequencies.get(index);
|
||||
Long valueB = mFrequencies.get(index - 1);
|
||||
mFrequencies.set(index, valueB);
|
||||
mFrequencies.set(index - 1, valueA);
|
||||
|
||||
fireTableRowsUpdated(index - 1, index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the frequency down in the list
|
||||
*/
|
||||
public void moveDown(int index)
|
||||
{
|
||||
if(index >= 0 && index + 1 < mFrequencies.size())
|
||||
{
|
||||
Long valueA = mFrequencies.get(index);
|
||||
Long valueB = mFrequencies.get(index + 1);
|
||||
mFrequencies.set(index, valueB);
|
||||
mFrequencies.set(index + 1, valueA);
|
||||
|
||||
fireTableRowsUpdated(index, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequencies for the model
|
||||
*/
|
||||
public void setFrequencies(List<Long> frequencies)
|
||||
{
|
||||
mFrequencies.clear();
|
||||
mFrequencies.addAll(frequencies);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of long frequency values contained in the model
|
||||
*/
|
||||
public List<Long> getFrequencies()
|
||||
{
|
||||
List<Long> frequencies = new ArrayList<>(mFrequencies);
|
||||
return frequencies;
|
||||
}
|
||||
}
|
||||
|
||||
public class SelectionListener implements ListSelectionListener
|
||||
{
|
||||
@Override
|
||||
public void valueChanged(ListSelectionEvent event)
|
||||
{
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
public class FrequencyEditor extends DefaultCellEditor
|
||||
{
|
||||
private JFormattedTextField mTextField;
|
||||
private NumberFormat mNumberFormat;
|
||||
private Double mMinimum;
|
||||
private Double mMaximum;
|
||||
|
||||
public FrequencyEditor(double minValue, double maxValue)
|
||||
{
|
||||
super(new JFormattedTextField());
|
||||
mTextField = (JFormattedTextField)getComponent();
|
||||
mMinimum = minValue;
|
||||
mMaximum = maxValue;
|
||||
|
||||
mNumberFormat = NumberFormat.getNumberInstance();
|
||||
mNumberFormat.setMinimumFractionDigits(1);
|
||||
mNumberFormat.setMaximumFractionDigits(6);
|
||||
mNumberFormat.setGroupingUsed(false);
|
||||
NumberFormatter numberFormatter = new NumberFormatter(mNumberFormat);
|
||||
numberFormatter.setFormat(mNumberFormat);
|
||||
numberFormatter.setMinimum(mMinimum);
|
||||
numberFormatter.setMaximum(mMaximum);
|
||||
numberFormatter.setAllowsInvalid(false);
|
||||
|
||||
mTextField.setFormatterFactory(new DefaultFormatterFactory(numberFormatter));
|
||||
mTextField.setValue(mMinimum);
|
||||
mTextField.setHorizontalAlignment(JTextField.TRAILING);
|
||||
mTextField.setFocusLostBehavior(JFormattedTextField.PERSIST);
|
||||
mTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check");
|
||||
mTextField.getActionMap().put("check", new AbstractAction()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event)
|
||||
{
|
||||
//This should always be true since we use formatter.setAllowsInvalid(false) above.
|
||||
if(mTextField.isEditValid())
|
||||
{
|
||||
int selectedRow = mFrequencyTable.getSelectedRow();
|
||||
int selectedIndex = mFrequencyTable.convertRowIndexToModel(selectedRow);
|
||||
|
||||
try
|
||||
{
|
||||
Double frequencyMHz = Double.parseDouble(mTextField.getText());
|
||||
|
||||
long frequency = (long)(frequencyMHz * 1e6d);
|
||||
mFrequencyModel.setValueAt(selectedIndex, frequency);
|
||||
}
|
||||
catch(Exception err)
|
||||
{
|
||||
mLog.error("Error parsing frequency: " + mTextField.getText(), err);
|
||||
}
|
||||
|
||||
fireEditingStopped();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Override to invoke setValue on the formatted text field.
|
||||
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
|
||||
{
|
||||
JFormattedTextField ftf = (JFormattedTextField)super.getTableCellEditorComponent(table, value, isSelected, row, column);
|
||||
|
||||
if(value instanceof Long)
|
||||
{
|
||||
ftf.setValue(((Long)value).doubleValue() / 1e6d);
|
||||
}
|
||||
else
|
||||
{
|
||||
ftf.setValue(value);
|
||||
}
|
||||
|
||||
ftf.requestFocus();
|
||||
ftf.selectAll();
|
||||
ftf.setCaretPosition(0);
|
||||
return ftf;
|
||||
}
|
||||
|
||||
//Override to ensure that the value remains a Double.
|
||||
public Object getCellEditorValue()
|
||||
{
|
||||
JFormattedTextField ftf = (JFormattedTextField)getComponent();
|
||||
Object o = ftf.getValue();
|
||||
if(o instanceof Double)
|
||||
{
|
||||
return o;
|
||||
}
|
||||
else if(o instanceof Number)
|
||||
{
|
||||
return ((Number)o).doubleValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
return Double.parseDouble(o.toString());
|
||||
}
|
||||
catch(Exception exc)
|
||||
{
|
||||
return 0.0d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Table cell renderer to display long hertz values as double megahertz values
|
||||
*/
|
||||
public class FrequencyRenderer extends DefaultTableCellRenderer
|
||||
{
|
||||
|
||||
public FrequencyRenderer()
|
||||
{
|
||||
super.setHorizontalAlignment(JLabel.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
|
||||
{
|
||||
JLabel cell = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
if(value instanceof Long)
|
||||
{
|
||||
cell.setText(format((long)value));
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.setText("??");
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
EventQueue.invokeLater(() -> {
|
||||
final JFrame frame = new JFrame();
|
||||
frame.setSize(new Dimension(400, 400));
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frame.add(new MultipleFrequencyEditor(1.0, 6000.0));
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.control;
|
||||
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Text formatter for talkgroups that use prefix and ident format that constrains values to specified minimum and
|
||||
* maximum valid values.
|
||||
*/
|
||||
public class PrefixIdentFormatter extends TextFormatter<Integer>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(PrefixIdentFormatter.class);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param minimum allowed value
|
||||
* @param maximum allowed value
|
||||
*/
|
||||
public PrefixIdentFormatter(int minimum, int maximum)
|
||||
{
|
||||
super(new PrefixIdentIntegerStringConverter(), null, new PrefixIdentFilter(minimum, maximum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted text change filter that only allows formatted characters where the converted decimal value
|
||||
* is also constrained within minimum and maximum valid values.
|
||||
*/
|
||||
public static class PrefixIdentFilter implements UnaryOperator<Change>
|
||||
{
|
||||
private int mMinimum;
|
||||
private int mMaximum;
|
||||
|
||||
public PrefixIdentFilter(int minimum, int maximum)
|
||||
{
|
||||
mMinimum = minimum;
|
||||
mMaximum = maximum;
|
||||
}
|
||||
|
||||
private boolean isValid(Integer value)
|
||||
{
|
||||
return value != null && mMinimum <= value && value <= mMaximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change)
|
||||
{
|
||||
String updatedText = change.getControlNewText();
|
||||
|
||||
if(updatedText == null || updatedText.isEmpty())
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
if(PrefixIdentIntegerStringConverter.isValid(updatedText))
|
||||
{
|
||||
return change;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix-Ident integer string converter. Obtain value as an integer but user edits formatted values.
|
||||
*/
|
||||
public static class PrefixIdentIntegerStringConverter extends StringConverter<Integer>
|
||||
{
|
||||
private static final Pattern PREFIX_IDENT_PATTERN = Pattern.compile("(\\d{0,3})(?:-(\\d{0,4}))?");
|
||||
|
||||
private static final int PREFIX_MASK = 0xFE000;
|
||||
private static final int IDENT_MASK = 0x1FFF;
|
||||
|
||||
@Override
|
||||
public String toString(Integer value)
|
||||
{
|
||||
// If the specified value is null, return a zero-length String
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
int prefix = (value & PREFIX_MASK) >> 13;
|
||||
int ident = (value & IDENT_MASK);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(String.format("%03d", prefix));
|
||||
sb.append("-");
|
||||
sb.append(String.format("%04d", ident));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer fromString(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher m = PREFIX_IDENT_PATTERN.matcher(value);
|
||||
|
||||
if(m.matches())
|
||||
{
|
||||
int a = 0;
|
||||
int b = 0;
|
||||
|
||||
try
|
||||
{
|
||||
a = Integer.parseInt(m.group(1));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
b = Integer.parseInt(m.group(2));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
|
||||
int talkgroup = (a << 13) + b;
|
||||
|
||||
return talkgroup;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isValid(String value)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Matcher m = PREFIX_IDENT_PATTERN.matcher(value);
|
||||
|
||||
if(m.matches())
|
||||
{
|
||||
//We don't have to check the first digit - it will only match a 0 or 1
|
||||
String rawPrefix = m.group(1);
|
||||
|
||||
if(rawPrefix != null && !rawPrefix.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
int prefix = Integer.parseInt(rawPrefix);
|
||||
|
||||
if(prefix < 0 || prefix > 127)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
String rawIdent = m.group(2);
|
||||
|
||||
if(rawIdent != null && !rawIdent.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
int ident = Integer.parseInt(rawIdent);
|
||||
|
||||
if(ident < 0 || ident > 8191)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
//if prefix and/or ident is null or empty then valid = true
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user