#5 Playlist Editor With Radio Reference Support (#760)

* 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:
Denny Sheirer
2020-04-26 11:53:43 +00:00
committed by GitHub
parent 39f6d6c831
commit cc52ae5bd7
290 changed files with 31251 additions and 15306 deletions

View 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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}

View 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
});
}
}

View File

@@ -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;
}
}
}