diff --git a/src/studio/kdb/K.java b/src/studio/kdb/K.java index 7d9ea9c..f215f3a 100755 --- a/src/studio/kdb/K.java +++ b/src/studio/kdb/K.java @@ -9,6 +9,7 @@ import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.time.Clock; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Stream; @@ -17,9 +18,10 @@ public class K { private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy.MM.dd"); private final static SimpleDateFormat dateTimeFormatter = new SimpleDateFormat("yyyy.MM.dd'T'HH:mm:ss.SSS"); private final static SimpleDateFormat timestampFormatter = new SimpleDateFormat("yyyy.MM.dd'D'HH:mm:ss."); + private final static long NS_IN_SEC = 1_000_000_000; private final static long SEC_IN_DAY = 24 * 60 * 60; private final static long MS_IN_DAY = 1000 * SEC_IN_DAY; - private final static long NS_IN_DAY = 1000_000_000 * SEC_IN_DAY; + private final static long NS_IN_DAY = NS_IN_SEC * SEC_IN_DAY; private final static long NS_IN_MONTH = (long) (NS_IN_DAY*(365*4+1)/(12*4.0)); static { @@ -1408,6 +1410,65 @@ public static class KTimespan extends KLongBase { public final static KTimespan NULL = new KTimespan(NULL_VALUE); public final static KTimespan ZERO = new KTimespan(0); + private final static Map, Long> NS_IN_TYPES = Map.of( + KTimestamp.class, 1L, + KTimespan.class, 1L, + KDate.class, NS_IN_DAY, + Month.class, NS_IN_MONTH, + Minute.class, 60 * NS_IN_SEC, + Second.class, NS_IN_SEC, + KTime.class, 1_000_000L, + KDatetime.class, NS_IN_DAY + ); + + private final static Map NS_IN_UNITS = Map.of( + ChronoUnit.NANOS, NS_IN_TYPES.get(KTimestamp.class), + ChronoUnit.MILLIS, NS_IN_TYPES.get(KTime.class), + ChronoUnit.SECONDS, NS_IN_TYPES.get(Second.class), + ChronoUnit.MINUTES, NS_IN_TYPES.get(Minute.class), + ChronoUnit.HOURS, 60 * NS_IN_TYPES.get(Minute.class), + ChronoUnit.DAYS, NS_IN_TYPES.get(KDate.class), + ChronoUnit.MONTHS, NS_IN_TYPES.get(Month.class), + ChronoUnit.YEARS, 12 * NS_IN_TYPES.get(Month.class) + ); + + private final static List SUPPORTED_UNiTS = List.of(NS_IN_UNITS.keySet().toArray(new ChronoUnit[0])); + + public final static ChronoUnit[] getSupportedUnits() { + return new ChronoUnit[] { + ChronoUnit.NANOS, ChronoUnit.MILLIS, ChronoUnit.SECONDS, ChronoUnit.MINUTES, + ChronoUnit.HOURS, ChronoUnit.DAYS, ChronoUnit.MONTHS, ChronoUnit.YEARS }; + } + + public static KTimespan duration(double duration, Class unitClass) { + if (! NS_IN_TYPES.containsKey(unitClass)) + throw new IllegalArgumentException(unitClass.toString() + " is not supported"); + + return new KTimespan((long) (duration * NS_IN_TYPES.get(unitClass))); + } + + public double toUnitValue(Class unitClass) { + if (! NS_IN_TYPES.containsKey(unitClass)) + throw new IllegalArgumentException(unitClass.toString() + " is not supported"); + + return value / (double) NS_IN_TYPES.get(unitClass); + } + + public static KTimespan duration(double duration, ChronoUnit unit) { + if (! NS_IN_UNITS.containsKey(unit)) + throw new IllegalArgumentException("Unit " + unit.toString() + " is not supported"); + + return new KTimespan((long) (duration * NS_IN_UNITS.get(unit)) ); + + } + + public double toUnitValue(ChronoUnit unit) { + if (! NS_IN_UNITS.containsKey(unit)) + throw new IllegalArgumentException("Unit " + unit.toString() + " is not supported"); + + return value / (double) NS_IN_UNITS.get(unit); + } + public KTimespan(long x) { super(-16, x); } @@ -1445,6 +1506,7 @@ public StringBuilder format(StringBuilder builder, KFormatContext context) { public Time toTime() { return new Time((value / 1000000)); } + } private static java.text.DecimalFormat i2Formatter = new java.text.DecimalFormat("00"); diff --git a/src/studio/ui/chart/Chart.java b/src/studio/ui/chart/Chart.java index 10ca1b3..b4ecc38 100755 --- a/src/studio/ui/chart/Chart.java +++ b/src/studio/ui/chart/Chart.java @@ -26,12 +26,13 @@ import studio.ui.Util; import studio.utils.WindowsAppUserMode; -import javax.swing.Timer; import javax.swing.*; import java.awt.*; import java.awt.event.*; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; -import java.util.*; +import java.util.Set; public class Chart implements ComponentListener { @@ -62,21 +63,8 @@ public class Chart implements ComponentListener { private final static Set supportedClasses = new HashSet<>(); static { - supportedClasses.addAll(Arrays.asList( - K.KInteger.class, - K.KDouble.class, - K.KFloat.class, - K.KShort.class, - K.KLong.class, - - K.KDate.class, - K.KTime.class, - K.KTimestamp.class, - K.KTimespan.class, - K.KDatetime.class, - K.Month.class, - K.Second.class, - K.Minute.class) ); + supportedClasses.addAll(DurationEditor.VALUE_CLASSES); + supportedClasses.addAll(DurationEditor.TEMPORAL_CLASSES); } private static StandardChartTheme currentTheme = new StandardChartTheme("JFree"); @@ -245,6 +233,14 @@ public void componentHidden(ComponentEvent e) { updateFrameBounds(); } + public Class getDomainClass() { + return table.getColumnElementClass(xIndex); + } + + public Class getRangeClass() { + return table.getColumnElementClass(yIndex); + } + public void refreshPlot() { JFreeChart chart = chartPanel.getChart(); XYPlot plot = chart.getXYPlot(); diff --git a/src/studio/ui/chart/ChartConfigPanel.java b/src/studio/ui/chart/ChartConfigPanel.java index 2297719..03f84d3 100644 --- a/src/studio/ui/chart/ChartConfigPanel.java +++ b/src/studio/ui/chart/ChartConfigPanel.java @@ -94,7 +94,7 @@ public void addLine(Line line) { lines.add(line); listLines.add(title, line.getIcon()); - new LineInfoFrame(line); + new LineInfoFrame(line, chart.getDomainClass(), chart.getRangeClass()); } private void refresh() { diff --git a/src/studio/ui/chart/DurationEditor.java b/src/studio/ui/chart/DurationEditor.java new file mode 100644 index 0000000..dec8a54 --- /dev/null +++ b/src/studio/ui/chart/DurationEditor.java @@ -0,0 +1,118 @@ +package studio.ui.chart; + +import studio.kdb.K; +import studio.ui.chart.event.ValueChangedEvent; +import studio.ui.chart.event.ValueChangedListener; + +import javax.swing.*; +import javax.swing.event.EventListenerList; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.util.List; + + +public class DurationEditor extends JPanel { + + public static final List> VALUE_CLASSES = + List.of( + K.KInteger.class, + K.KDouble.class, + K.KFloat.class, + K.KShort.class, + K.KLong.class + ); + + public static final List> TEMPORAL_CLASSES = + List.of( + K.KDate.class, + K.KTime.class, + K.KTimestamp.class, + K.KTimespan.class, + K.KDatetime.class, + K.Month.class, + K.Second.class, + K.Minute.class + ); + + private final EventListenerList listenerList = new EventListenerList(); + + public static DurationEditor create(Class unitClass) { + if (VALUE_CLASSES.contains(unitClass)) return new DurationEditor(); + if (TEMPORAL_CLASSES.contains(unitClass)) return new TimespanEditor(unitClass); + + throw new UnsupportedOperationException("DurationEditor for class " + unitClass + " is not supported"); + } + + + public void addValueChangedListener(ValueChangedListener listener) { + listenerList.add(ValueChangedListener.class, listener); + } + + public void removeValueChangedListener(ValueChangedListener listener) { + listenerList.remove(ValueChangedListener.class, listener); + } + + protected void notifyValueChanged() { + ValueChangedEvent event = new ValueChangedEvent(this, getValue()); + Object[] listeners = listenerList.getListenerList(); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ValueChangedListener.class) { + ((ValueChangedListener) listeners[i + 1]).valueChanged(event); + } + } + } + + protected final JTextField txtValue = new JTextField(); + + private double value = Double.NaN; + + protected DurationEditor() { + super(new BorderLayout()); + add(txtValue, BorderLayout.CENTER); + + txtValue.addActionListener(this::txtValueChanged); + txtValue.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + txtValueChanged(null); + } + }); + + } + + public double getValue() { + return value; + } + + public void setValue(double value) { + if (value == this.value) return; + this.value = value; + refresh(); + notifyValueChanged(); + } + + protected void refresh() { + txtValue.setText("" + value); + } + + protected void txtValueChanged(double newValue) { + this.value = newValue; + } + + private void txtValueChanged(ActionEvent event) { + boolean error = false; + try { + double v = Double.parseDouble(txtValue.getText()); + txtValueChanged(v); + } catch (NumberFormatException e) { + error = true; + } + refresh(); + if (!error) { + notifyValueChanged(); + } + } + +} diff --git a/src/studio/ui/chart/LineInfoFrame.java b/src/studio/ui/chart/LineInfoFrame.java index e75c41a..e93a79d 100644 --- a/src/studio/ui/chart/LineInfoFrame.java +++ b/src/studio/ui/chart/LineInfoFrame.java @@ -1,5 +1,6 @@ package studio.ui.chart; +import studio.kdb.K; import studio.ui.GroupLayoutSimple; import javax.swing.*; @@ -18,8 +19,8 @@ public class LineInfoFrame extends JFrame { private final JLabel lblX = new JLabel(""); private final JLabel lblY = new JLabel(""); private final JTextField txtTitle = new JTextField(); - private final JTextField txtDX = new JTextField(); - private final JTextField txtDY = new JTextField(); + private final DurationEditor txtDX; + private final DurationEditor txtDY; private final JTextField txtX = new JTextField(); private final JTextField txtY = new JTextField(); @@ -30,8 +31,11 @@ public class LineInfoFrame extends JFrame { private boolean lockDX = true; private boolean lockX = true; - public LineInfoFrame(Line line) { + public LineInfoFrame(Line line, Class xClazz, Class yClazz) { this.line = line; + txtDX = DurationEditor.create(xClazz); + txtDY = DurationEditor.create(yClazz); + line.addChangeListener(e -> refresh()); initComponents(); } @@ -69,6 +73,14 @@ public void focusLost(FocusEvent e) { return textField; } + private DurationEditor withAction(DurationEditor editor, DoubleConsumer action) { + editor.addValueChangedListener(e -> { + action.accept(e.getValue()); + refresh(); + }); + return editor; + } + private void initComponents() { setTitle(getTitle()); setDefaultCloseOperation(DISPOSE_ON_CLOSE); @@ -138,8 +150,8 @@ private void refresh() { if (lockX) y = line.getY(x); else x = line.getX(y); - txtDX.setText("" + dx); - txtDY.setText("" + dy); + txtDX.setValue(dx); + txtDY.setValue(dy); txtX.setText("" + x); txtY.setText("" + y); diff --git a/src/studio/ui/chart/TimespanEditor.java b/src/studio/ui/chart/TimespanEditor.java new file mode 100644 index 0000000..b40f1a4 --- /dev/null +++ b/src/studio/ui/chart/TimespanEditor.java @@ -0,0 +1,81 @@ +package studio.ui.chart; + +import studio.kdb.K; + +import javax.swing.*; +import java.awt.*; +import java.time.temporal.ChronoUnit; + +public class TimespanEditor extends DurationEditor { + + private final Class unitClass; + private K.KTimespan value = K.KTimespan.NULL; + private final JComboBox comboUnit = + new JComboBox<>(K.KTimespan.getSupportedUnits()); + + + public TimespanEditor(Class unitClass) { + this.unitClass = unitClass; + + add(comboUnit, BorderLayout.EAST); + comboUnit.addActionListener(e -> refresh()); + } + + private void unitAutoSelect(K.KTimespan value) { + ChronoUnit selectedUnit = null; + + for (int i=comboUnit.getItemCount()-1; i>=0; i--) { + ChronoUnit unit = comboUnit.getItemAt(i); + double v = value.toUnitValue(unit); + if (Math.abs(v) >= 1.0 || i==0) { + selectedUnit = unit; + break; + } + } + + comboUnit.setSelectedItem(selectedUnit); + } + + public void setValue(double value) { + K.KTimespan newValue = K.KTimespan.duration(value, unitClass); + if (newValue.equals(this.value)) return; + + if (this.value.equals(K.KTimespan.NULL)) { + unitAutoSelect(newValue); + } + + this.value = newValue; + + refresh(); + notifyValueChanged(); + } + + public double getValue() { + return value.toUnitValue(unitClass); + } + + protected void refresh() { + txtValue.setText("" + value.toUnitValue((ChronoUnit) comboUnit.getSelectedItem())); + } + + protected void txtValueChanged(double newValue) { + value = K.KTimespan.duration(newValue, (ChronoUnit) comboUnit.getSelectedItem()); + } + + + + public static void main(String[] args) { + + JFrame f = new JFrame("Test"); + TimespanEditor editor = new TimespanEditor(K.Second.class); + editor.setValue(600); + editor.addValueChangedListener(e -> System.out.printf("New value: %f\n", e.getValue()) ); + + f.setContentPane(editor); + + f.setSize(200,50); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.setVisible(true); + } + +} diff --git a/src/studio/ui/chart/event/ValueChangedEvent.java b/src/studio/ui/chart/event/ValueChangedEvent.java new file mode 100644 index 0000000..0e7fead --- /dev/null +++ b/src/studio/ui/chart/event/ValueChangedEvent.java @@ -0,0 +1,18 @@ +package studio.ui.chart.event; + +import java.util.EventObject; + +public class ValueChangedEvent extends EventObject { + + private double value; + + public ValueChangedEvent(Object source, double value) { + super(source); + this.value = value; + } + + public double getValue() { + return value; + } + +} diff --git a/src/studio/ui/chart/event/ValueChangedListener.java b/src/studio/ui/chart/event/ValueChangedListener.java new file mode 100644 index 0000000..c55875f --- /dev/null +++ b/src/studio/ui/chart/event/ValueChangedListener.java @@ -0,0 +1,8 @@ +package studio.ui.chart.event; + +import java.util.EventListener; + +public interface ValueChangedListener extends EventListener { + + void valueChanged(ValueChangedEvent event); +}