/** * Copyright (C) 2018 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.unitConverter.converterGUI; import java.awt.BorderLayout; import java.awt.GridLayout; import java.io.File; import java.math.BigDecimal; import java.math.MathContext; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.function.Predicate; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.ListModel; import javax.swing.ListSelectionModel; import org.unitConverter.UnitsDatabase; import org.unitConverter.dimension.StandardDimensions; import org.unitConverter.dimension.UnitDimension; import org.unitConverter.unit.AbstractUnit; import org.unitConverter.unit.NonlinearUnits; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitPrefix; /** * @author Adrien Hopkins * @since 2018-12-27 * @since v0.1.0 */ final class UnitConverterGUI { private static class Presenter { /** The presenter's associated view. */ private final View view; /** The units known by the program. */ private final UnitsDatabase units; /** The names of all of the units */ private final List unitNames; /** The names of all of the units, but filtered */ private final DelegateListModel unitNamesFiltered; /** The names of all of the prefixes */ private final List prefixNames; /** The names of all of the prefixes */ private final DelegateListModel prefixNamesFiltered; private final Comparator prefixNameComparator; private int significantFigures = 6; /** * Creates the presenter. * * @param view * presenter's associated view * @since 2018-12-27 * @since v0.1.0 */ Presenter(final View view) { this.view = view; // load initial units this.units = new UnitsDatabase(); this.units.addUnit("metre", SI.METRE); this.units.addUnit("kilogram", SI.KILOGRAM); this.units.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); this.units.addUnit("second", SI.SECOND); this.units.addUnit("ampere", SI.AMPERE); this.units.addUnit("kelvin", SI.KELVIN); this.units.addUnit("mole", SI.MOLE); this.units.addUnit("candela", SI.CANDELA); this.units.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION)); this.units.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY)); // nonlinear units - must be loaded manually this.units.addUnit("tempCelsius", NonlinearUnits.CELSIUS); this.units.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT); this.units.addAllFromFile(new File("unitsfile.txt")); // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. // otherwise, they are compared by value this.prefixNameComparator = (o1, o2) -> { if (!Presenter.this.units.containsPrefixName(o1)) return -1; else if (!Presenter.this.units.containsPrefixName(o2)) return 1; final UnitPrefix p1 = Presenter.this.units.getPrefix(o1); final UnitPrefix p2 = Presenter.this.units.getPrefix(o2); if (p1.getMultiplier() < p2.getMultiplier()) return -1; else if (p1.getMultiplier() > p2.getMultiplier()) return 1; return o1.compareTo(o2); }; this.unitNames = new ArrayList<>(this.units.prefixlessUnitNameSet()); this.unitNames.sort(null); // sorts it using Comparable this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixlessUnitNameSet())); this.unitNamesFiltered.sort(null); // sorts it using Comparable this.prefixNames = new ArrayList<>(this.units.prefixNameSet()); this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet())); this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator System.out.printf("Successfully loaded %d units (%d base units)", AbstractUnit.getUnitCount(), AbstractUnit.getBaseUnitCount()); } /** * Runs whenever the convert button is pressed. * *

* Reads and parses a unit expression from the from and to boxes, then converts {@code from} to {@code to}. Any * errors are shown in JOptionPanes. *

* * @since 2019-01-26 * @since v0.1.0 */ public final void convert() { final String fromUnitString = this.view.getFromText(); final String toUnitString = this.view.getToText(); // try to parse from final Unit from; try { from = this.units.getUnitFromExpression(fromUnitString); } catch (final IllegalArgumentException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); return; } final double value; // try to parse to final Unit to; try { to = this.units.getUnitFromExpression(toUnitString); } catch (final IllegalArgumentException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); return; } // if I can't convert, leave if (!from.canConvertTo(to)) { this.view.showErrorDialog("Conversion Error", String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); return; } value = to.convertFromBase(from.convertToBase(1)); // round value final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures)); String output = bigValue.toString(); // remove trailing zeroes if (output.contains(".")) { while (output.endsWith("0")) { output = output.substring(0, output.length() - 1); } if (output.endsWith(".")) { output = output.substring(0, output.length() - 1); } } this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString)); } /** * Filters the filtered model for units * * @param filter * filter to use * @since 2019-01-15 * @since v0.1.0 */ private final void filterFilteredPrefixModel(final Predicate filter) { this.prefixNamesFiltered.clear(); for (final String prefixName : this.prefixNames) { if (filter.test(prefixName)) { this.prefixNamesFiltered.add(prefixName); } } } /** * Filters the filtered model for units * * @param filter * filter to use * @since 2019-01-15 * @since v0.1.0 */ private final void filterFilteredUnitModel(final Predicate filter) { this.unitNamesFiltered.clear(); for (final String unitName : this.unitNames) { if (filter.test(unitName)) { this.unitNamesFiltered.add(unitName); } } } /** * @return a list model of all of the unit keys * @since 2019-01-14 * @since v0.1.0 */ public final ListModel keyListModel() { return this.unitNamesFiltered; } /** * Runs whenever the prefix filter is changed. *

* Filters the prefix list then sorts it using a {@code FilterComparator}. *

* * @since 2019-01-15 * @since v0.1.0 */ public final void prefixFilterUpdated() { final String filter = this.view.getPrefixFilterText(); if (filter.equals("")) { this.filterFilteredPrefixModel(t -> true); } else { this.filterFilteredPrefixModel(t -> t.contains(filter)); } this.prefixNamesFiltered.sort(new FilterComparator(filter)); } /** * @return a list model of all of the prefix names * @since 2019-01-15 */ public final ListModel prefixNameListModel() { return this.prefixNamesFiltered; } /** * Runs whenever a prefix is selected in the viewer. *

* Shows its information in the text box to the right. *

* * @since 2019-01-15 * @since v0.1.0 */ public final void prefixSelected() { final int index = this.view.getPrefixListSelection(); if (index == -1) return; else { final String prefixName = this.prefixNamesFiltered.get(index); final UnitPrefix prefix = this.units.getPrefix(prefixName); this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); } } /** * @param significantFigures * new value of significantFigures * @since 2019-01-15 */ public final void setSignificantFigures(final int significantFigures) { this.significantFigures = significantFigures; } /** * Runs whenever the unit filter is changed. *

* Filters the unit list then sorts it using a {@code FilterComparator}. *

* * @since 2019-01-15 * @since v0.1.0 */ public final void unitFilterUpdated() { final String filter = this.view.getUnitFilterText(); if (filter.equals("")) { this.filterFilteredUnitModel(t -> true); } else { this.filterFilteredUnitModel(t -> t.contains(filter)); } this.unitNamesFiltered.sort(new FilterComparator(filter)); } /** * Runs whenever a unit is selected in the viewer. *

* Shows its information in the text box to the right. *

* * @since 2019-01-15 * @since v0.1.0 */ public void unitNameSelected() { final int index = this.view.getUnitListSelection(); if (index == -1) return; else { final String unitName = this.unitNamesFiltered.get(index); final Unit unit = this.units.getUnit(unitName); this.view.setUnitTextBoxText(unit.toString()); } } } private static class View { /** The view's frame. */ private final JFrame frame; /** The view's associated presenter. */ private final Presenter presenter; /** The list of unit names in the unit viewer */ private final JList unitNameList; /** The list of prefix names in the prefix viewer */ private final JList prefixNameList; /** The unit search box in the unit viewer */ private final JTextField unitFilterEntry; /** The text box for unit data in the unit viewer */ private final JTextArea unitTextBox; /** The prefix search box in the prefix viewer */ private final JTextField prefixFilterEntry; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; /** The "From" entry in the conversion panel */ private final JTextField fromEntry; /** The "To" entry in the conversion panel */ private final JTextField toEntry; /** The output area in the conversion panel */ private final JTextArea output; /** * Creates the {@code View}. * * @since 2019-01-14 * @since v0.1.0 */ public View() { this.presenter = new Presenter(this); this.frame = new JFrame("Unit Converter"); this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // create the components this.unitNameList = new JList<>(this.presenter.keyListModel()); this.prefixNameList = new JList<>(this.presenter.prefixNameListModel()); this.unitFilterEntry = new JTextField(); this.unitTextBox = new JTextArea(); this.prefixFilterEntry = new JTextField(); this.prefixTextBox = new JTextArea(); this.fromEntry = new JTextField(); this.toEntry = new JTextField(); this.output = new JTextArea(2, 32); // create more components this.initComponents(); this.frame.pack(); } /** * @return text in "From" box in converter panel * @since 2019-01-15 * @since v0.1.0 */ public String getFromText() { return this.fromEntry.getText(); } /** * @return text in prefix filter * @since 2019-01-15 * @since v0.1.0 */ public String getPrefixFilterText() { return this.prefixFilterEntry.getText(); } /** * @return index of selected prefix * @since 2019-01-15 * @since v0.1.0 */ public int getPrefixListSelection() { return this.prefixNameList.getSelectedIndex(); } /** * @return text in "To" box in converter panel * @since 2019-01-26 * @since v0.1.0 */ public String getToText() { return this.toEntry.getText(); } /** * @return text in unit filter * @see javax.swing.text.JTextComponent#getText() */ public String getUnitFilterText() { return this.unitFilterEntry.getText(); } /** * @return index of selected unit * @since 2019-01-15 * @since v0.1.0 */ public int getUnitListSelection() { return this.unitNameList.getSelectedIndex(); } /** * Starts up the application. * * @since 2018-12-27 * @since v0.1.0 */ public final void init() { this.frame.setVisible(true); } /** * Initializes the view's components. * * @since 2018-12-27 * @since v0.1.0 */ private final void initComponents() { final JPanel masterPanel = new JPanel(); this.frame.add(masterPanel); masterPanel.setLayout(new BorderLayout()); { // pane with all of the tabs final JTabbedPane masterPane = new JTabbedPane(); masterPanel.add(masterPane, BorderLayout.CENTER); { // a panel for unit conversion using a selector final JPanel convertUnitPanel = new JPanel(); masterPane.addTab("Convert Units", convertUnitPanel); convertUnitPanel.setLayout(new BorderLayout()); { // panel for input part final JPanel inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); inputPanel.setLayout(new GridLayout(1, 3)); { // panel for From things final JPanel fromPanel = new JPanel(); inputPanel.add(fromPanel); fromPanel.setLayout(new BorderLayout()); { // search box for from final JTextField fromSearch = new JTextField("Search..."); fromPanel.add(fromSearch, BorderLayout.PAGE_START); } { // list for From units final JList fromList = new JList<>(); fromPanel.add(fromList, BorderLayout.CENTER); } } { // for dimension selector and arrow that represents conversion final JPanel inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); inBetweenPanel.setLayout(new BorderLayout()); { // dimension selector final JComboBox dimensionSelector = new JComboBox<>( new String[] {"Select dimension..."}); inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); } { // the arrow in the middle final JLabel arrowLabel = new JLabel("->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); } } { // panel for To things final JPanel toPanel = new JPanel(); inputPanel.add(toPanel); toPanel.setLayout(new BorderLayout()); { // search box for to final JTextField toSearch = new JTextField("Search..."); toPanel.add(toSearch, BorderLayout.PAGE_START); } { // list for To units final JList toList = new JList<>(); toPanel.add(toList, BorderLayout.CENTER); } } } { // panel for submit and output, and also value entry final JPanel outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); outputPanel.setLayout(new GridLayout(3, 1)); { // unit input final JPanel valueInputPanel = new JPanel(); outputPanel.add(valueInputPanel); valueInputPanel.setLayout(new BorderLayout()); { // prompt final JLabel valuePrompt = new JLabel("Value to convert: "); valueInputPanel.add(valuePrompt, BorderLayout.LINE_START); } { // value to convert final JTextField valueInput = new JFormattedTextField( new DecimalFormat("###############0.################")); valueInputPanel.add(valueInput, BorderLayout.CENTER); } } { // button to convert final JButton convertButton = new JButton("Convert"); outputPanel.add(convertButton); } { // output of conversion final JLabel outputLabel = new JLabel(); outputPanel.add(outputLabel); } } } { // panel for unit conversion using expressions final JPanel convertExpressionPanel = new JPanel(); masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); convertExpressionPanel.setLayout(new GridLayout(5, 1)); { // panel for units to convert from final JPanel fromPanel = new JPanel(); convertExpressionPanel.add(fromPanel); fromPanel.setBorder(BorderFactory.createTitledBorder("From")); fromPanel.setLayout(new GridLayout(1, 1)); { // entry for units fromPanel.add(this.fromEntry); } } { // panel for units to convert to final JPanel toPanel = new JPanel(); convertExpressionPanel.add(toPanel); toPanel.setBorder(BorderFactory.createTitledBorder("To")); toPanel.setLayout(new GridLayout(1, 1)); { // entry for units toPanel.add(this.toEntry); } } { // button to convert final JButton convertButton = new JButton("Convert!"); convertExpressionPanel.add(convertButton); convertButton.addActionListener(e -> this.presenter.convert()); } { // output of conversion final JPanel outputPanel = new JPanel(); convertExpressionPanel.add(outputPanel); outputPanel.setBorder(BorderFactory.createTitledBorder("Output")); outputPanel.setLayout(new GridLayout(1, 1)); { // output outputPanel.add(this.output); this.output.setEditable(false); } } { // panel for specifying precision final JPanel sigDigPanel = new JPanel(); convertExpressionPanel.add(sigDigPanel); sigDigPanel.setBorder(BorderFactory.createTitledBorder("Significant Digits")); { // slider final JSlider sigDigSlider = new JSlider(0, 12); sigDigPanel.add(sigDigSlider); sigDigSlider.setMajorTickSpacing(4); sigDigSlider.setMinorTickSpacing(1); sigDigSlider.setSnapToTicks(true); sigDigSlider.setPaintTicks(true); sigDigSlider.setPaintLabels(true); sigDigSlider.addChangeListener( e -> this.presenter.setSignificantFigures(sigDigSlider.getValue())); } } } { // panel to look up units final JPanel unitLookupPanel = new JPanel(); masterPane.addTab("Unit Viewer", unitLookupPanel); unitLookupPanel.setLayout(new GridLayout()); { // panel for listing and searching final JPanel listPanel = new JPanel(); unitLookupPanel.add(listPanel); listPanel.setLayout(new BorderLayout()); { // unit search box listPanel.add(this.unitFilterEntry, BorderLayout.PAGE_START); this.unitFilterEntry.addCaretListener(e -> this.presenter.unitFilterUpdated()); } { // a list of units listPanel.add(new JScrollPane(this.unitNameList), BorderLayout.CENTER); this.unitNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp this.unitNameList.addListSelectionListener(e -> { this.presenter.unitNameSelected(); }); } } { // the text box for unit's toString unitLookupPanel.add(this.unitTextBox); this.unitTextBox.setEditable(false); this.unitTextBox.setLineWrap(true); } } { // panel to look up prefixes final JPanel prefixLookupPanel = new JPanel(); masterPane.addTab("Prefix Viewer", prefixLookupPanel); prefixLookupPanel.setLayout(new GridLayout(1, 2)); { // panel for listing and seaching final JPanel prefixListPanel = new JPanel(); prefixLookupPanel.add(prefixListPanel); prefixListPanel.setLayout(new BorderLayout()); { // prefix search box prefixListPanel.add(this.prefixFilterEntry, BorderLayout.PAGE_START); this.prefixFilterEntry.addCaretListener(e -> this.presenter.prefixFilterUpdated()); } { // a list of prefixes prefixListPanel.add(new JScrollPane(this.prefixNameList), BorderLayout.CENTER); this.prefixNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp this.prefixNameList.addListSelectionListener(e -> { this.presenter.prefixSelected(); }); } } { // the text box for prefix's toString prefixLookupPanel.add(this.prefixTextBox); this.unitTextBox.setEditable(false); } } } } /** * Sets the text in the output of the conversion panel. * * @param text * text to set * @since 2019-01-15 * @since v0.1.0 */ public void setOutputText(final String text) { this.output.setText(text); } /** * Sets the text of the prefix text box. * * @param text * text to set * @since 2019-01-15 * @since v0.1.0 */ public void setPrefixTextBoxText(final String text) { this.prefixTextBox.setText(text); } /** * Sets the text of the unit text box. * * @param t * text to set * @see javax.swing.text.JTextComponent#setText(java.lang.String) */ public void setUnitTextBoxText(final String t) { this.unitTextBox.setText(t); } /** * Shows an error dialog. * * @param title * title of dialog * @param message * message in dialog * @since 2019-01-14 * @since v0.1.0 */ public void showErrorDialog(final String title, final String message) { JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); } } public static void main(final String[] args) { new View().init(); } }