/** * Copyright (C) 2018-2021 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 sevenUnits.converterGUI; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.nio.file.Files; import java.nio.file.Path; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.Scanner; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.WindowConstants; import javax.swing.border.TitledBorder; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; import sevenUnits.unit.UnitValue; import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.ObjectProduct; /** * @author Adrien Hopkins * @since 2018-12-27 * @since v0.1.0 */ final class SevenUnitsGUI { /** * A tab in the View. */ private enum Pane { UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT, SETTINGS; } private static class Presenter { /** The default place where settings are stored. */ private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; /** The default place where units are stored. */ private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; /** The default place where dimensions are stored. */ private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; /** The default place where exceptions are stored. */ private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; /** * Adds default units and dimensions to a database. * * @param database database to add to * @since 2019-04-14 * @since v0.2.0 */ private static void addDefaults(final UnitDatabase database) { database.addUnit("metre", Metric.METRE); database.addUnit("kilogram", Metric.KILOGRAM); database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000)); database.addUnit("second", Metric.SECOND); database.addUnit("ampere", Metric.AMPERE); database.addUnit("kelvin", Metric.KELVIN); database.addUnit("mole", Metric.MOLE); database.addUnit("candela", Metric.CANDELA); database.addUnit("bit", Metric.BIT); database.addUnit("unit", Metric.ONE); // nonlinear units - must be loaded manually database.addUnit("tempCelsius", Metric.CELSIUS); database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); // load initial dimensions database.addDimension("LENGTH", Metric.Dimensions.LENGTH); database.addDimension("MASS", Metric.Dimensions.MASS); database.addDimension("TIME", Metric.Dimensions.TIME); database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE); } /** * Gets the text of a resource file as a set of strings (each one is one * line of the text). * * @param filename filename to get resource from * @return contents of file * @since 2021-03-27 */ public static final List getLinesFromResource(String filename) { final List lines = new ArrayList<>(); try (InputStream stream = inputStream(filename); Scanner scanner = new Scanner(stream)) { while (scanner.hasNextLine()) { lines.add(scanner.nextLine()); } } catch (final IOException e) { throw new AssertionError( "Error occurred while loading file " + filename, e); } return lines; } /** * Gets an input stream for a resource file. * * @param filepath file to use as resource * @return obtained Path * @since 2021-03-27 */ private static final InputStream inputStream(String filepath) { return SevenUnitsGUI.class.getResourceAsStream(filepath); } /** * @return {@code line} with any comments removed. * @since 2021-03-13 */ private static final String withoutComments(String line) { final int index = line.indexOf('#'); return index == -1 ? line : line.substring(index); } /** The presenter's associated view. */ private final View view; /** The units known by the program. */ private final UnitDatabase database; /** The names of all of the units */ private final List unitNames; /** The names of all of the prefixes */ private final List prefixNames; /** The names of all of the dimensions */ private final List dimensionNames; /** Unit names that are ignored by the metric-only/imperial-only filter */ private final Set metricExceptions; private final Comparator prefixNameComparator; /** A boolean remembering whether or not one-way conversion is on */ private boolean oneWay = true; /** The prefix rule */ private DefaultPrefixRepetitionRule prefixRule = null; // conditions for existence of From and To entries // used for one-way conversion private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( s -> true); private final MutablePredicate toExistenceCondition = new MutablePredicate<>( s -> true); /* * Rounding-related settings. I am using my own system, and not * MathContext, because MathContext does not support decimal place based * or scientific rounding, only significant digit based rounding. */ private int precision = 6; private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; // The "include duplicate units" setting private boolean includeDuplicateUnits = true; /** * 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.database = new UnitDatabase( DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); // load units and prefixes try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { this.database.loadUnitsFromStream(units); } catch (final IOException e) { throw new AssertionError("Loading of unitsfile.txt failed.", e); } // load dimensions try (final InputStream dimensions = inputStream( DEFAULT_DIMENSIONS_FILEPATH)) { this.database.loadDimensionsFromStream(dimensions); } catch (final IOException e) { throw new AssertionError("Loading of dimensionfile.txt failed.", e); } // load metric exceptions try { this.metricExceptions = new HashSet<>(); try (InputStream exceptions = inputStream( DEFAULT_EXCEPTIONS_FILEPATH); Scanner scanner = new Scanner(exceptions)) { while (scanner.hasNextLine()) { final String line = Presenter .withoutComments(scanner.nextLine()); if (!line.isBlank()) { this.metricExceptions.add(line); } } } } catch (final IOException e) { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } // load settings - requires database to exist if (Files.exists(this.getSettingsFile())) { this.loadSettings(); } // 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.database.containsPrefixName(o1)) return -1; else if (!Presenter.this.database.containsPrefixName(o2)) return 1; final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); final UnitPrefix p2 = Presenter.this.database.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.database.unitMapPrefixless(true).keySet()); this.unitNames.sort(null); // sorts it using Comparable this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); this.prefixNames.sort(this.prefixNameComparator); // sorts it using my // comparator this.dimensionNames = new DelegateListModel<>( new ArrayList<>(this.database.dimensionMap().keySet())); this.dimensionNames.sort(null); // sorts it using Comparable // a Predicate that returns true iff the argument is a full base unit final Predicate isFullBase = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); // print out unit counts System.out.printf( "Successfully loaded %d units with %d unit names (%d base units).%n", this.database.unitMapPrefixless(false).size(), this.database.unitMapPrefixless(true).size(), this.database.unitMapPrefixless(false).values().stream() .filter(isFullBase).count()); } /** * Converts in the dimension-based converter * * @since 2019-04-13 * @since v0.2.0 */ public final void convertDimensionBased() { final String fromSelection = this.view.getFromSelection(); if (fromSelection == null) { this.view.showErrorDialog("Error", "No unit selected in From field"); return; } final String toSelection = this.view.getToSelection(); if (toSelection == null) { this.view.showErrorDialog("Error", "No unit selected in To field"); return; } final Unit from = this.database.getUnit(fromSelection); final Unit to = this.database.getUnit(toSelection) .withName(NameSymbol.ofName(toSelection)); final UnitValue beforeValue; try { beforeValue = UnitValue.of(from, this.view.getDimensionConverterInput()); } catch (final ParseException e) { this.view.showErrorDialog("Error", "Error in parsing: " + e.getMessage()); return; } final UnitValue value = beforeValue.convertTo(to); final String output = this.getRoundedString(value); this.view.setDimensionConverterOutputText( String.format("%s = %s", beforeValue, output)); } /** * 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 convertExpressions() { final String fromUnitString = this.view.getFromText(); final String toUnitString = this.view.getToText(); if (fromUnitString.isEmpty()) { this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box."); return; } if (toUnitString.isEmpty()) { this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the To: box."); return; } final LinearUnitValue from; final Unit to; try { from = this.database.evaluateUnitExpression(fromUnitString); } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); return; } try { to = this.database.getUnitFromExpression(toUnitString); } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); return; } if (to instanceof LinearUnit) { // convert to LinearUnitValue final LinearUnitValue from2; final LinearUnit to2 = ((LinearUnit) to) .withName(NameSymbol.ofName(toUnitString)); final boolean useSlash; if (from.canConvertTo(to2)) { from2 = from; useSlash = false; } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { from2 = LinearUnitValue.ONE.dividedBy(from); useSlash = true; } else { // if I can't convert, leave this.view.showErrorDialog("Conversion Error", String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); return; } final LinearUnitValue converted = from2.convertTo(to2); this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + String.format("%s = %s", fromUnitString, this.getRoundedString(converted, false))); return; } else { // convert to UnitValue final UnitValue from2 = from.asUnitValue(); if (from2.canConvertTo(to)) { final UnitValue converted = from2.convertTo(to); this.view .setExpressionConverterOutputText(String.format("%s = %s", fromUnitString, this.getRoundedString(converted))); } else { // if I can't convert, leave this.view.showErrorDialog("Conversion Error", String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); } } } /** * @return a list of all of the unit dimensions * @since 2019-04-13 * @since v0.2.0 */ public final List dimensionNameList() { return this.dimensionNames; } /** * @return a list of all the entries in the dimension-based converter's * From box * @since 2020-08-27 */ public final Set fromEntries() { return ConditionalExistenceCollections.conditionalExistenceSet( this.unitNameSet(), this.fromExistenceCondition); } /** * @return a comparator to compare prefix names * @since 2019-04-14 * @since v0.2.0 */ public final Comparator getPrefixNameComparator() { return this.prefixNameComparator; } /** * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit * converter's rounding settings. * * @since 2020-08-04 */ private final String getRoundedString(final LinearUnitValue value, boolean showUncertainty) { switch (this.roundingType) { case DECIMAL_PLACES: case SIGNIFICANT_DIGITS: return this.getRoundedString(value.asUnitValue()); case SCIENTIFIC: return value.toString(showUncertainty); default: throw new AssertionError("Invalid switch condition."); } } /** * Like {@link UnitValue#toString()}, but obeys this unit converter's * rounding settings. * * @since 2020-08-04 */ private final String getRoundedString(final UnitValue value) { final BigDecimal unrounded = new BigDecimal(value.getValue()); final BigDecimal rounded; int precision = this.precision; switch (this.roundingType) { case DECIMAL_PLACES: rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); break; case SCIENTIFIC: precision = 12; //$FALL-THROUGH$ case SIGNIFICANT_DIGITS: rounded = unrounded .round(new MathContext(precision, RoundingMode.HALF_EVEN)); break; default: throw new AssertionError("Invalid switch condition."); } String output = rounded.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); } } return output + " " + value.getUnit().getPrimaryName().get(); } /** * @return The file where settings are stored; * @since 2020-12-11 */ private final Path getSettingsFile() { return Path.of(DEFAULT_SETTINGS_FILEPATH); } /** * Loads settings from the settings file. * * @since 2021-02-17 */ public final void loadSettings() { try { // read file line by line final int lineNum = 0; for (final String line : Files .readAllLines(this.getSettingsFile())) { final int equalsIndex = line.indexOf('='); if (equalsIndex == -1) throw new IllegalStateException( "Settings file is malformed at line " + lineNum); final String param = line.substring(0, equalsIndex); final String value = line.substring(equalsIndex + 1); switch (param) { // set manually to avoid the unnecessary saving of the non-manual // methods case "precision": this.precision = Integer.valueOf(value); break; case "rounding_type": this.roundingType = RoundingType.valueOf(value); break; case "prefix_rule": this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value); this.database.setPrefixRepetitionRule(this.prefixRule); break; case "one_way": this.oneWay = Boolean.valueOf(value); if (this.oneWay) { this.fromExistenceCondition.setPredicate( unitName -> this.metricExceptions.contains(unitName) || !this.database.getUnit(unitName) .isMetric()); this.toExistenceCondition.setPredicate( unitName -> this.metricExceptions.contains(unitName) || this.database.getUnit(unitName).isMetric()); } else { this.fromExistenceCondition.setPredicate(unitName -> true); this.toExistenceCondition.setPredicate(unitName -> true); } break; case "include_duplicates": this.includeDuplicateUnits = Boolean.valueOf(value); if (this.view.presenter != null) { this.view.update(); } break; default: System.err.printf("Warning: unrecognized setting \"%s\".", param); break; } } } catch (final IOException e) {} } /** * @return a set of all prefix names in the database * @since 2019-04-14 * @since v0.2.0 */ public final Set prefixNameSet() { return this.database.prefixMap().keySet(); } /** * 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 String prefixName = this.view.getPrefixViewerSelection(); if (prefixName == null) return; else { final UnitPrefix prefix = this.database.getPrefix(prefixName); this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); } } /** * Saves the settings to the settings file. * * @since 2021-02-17 */ public final void saveSettings() { try (BufferedWriter writer = Files .newBufferedWriter(this.getSettingsFile())) { writer.write(String.format("precision=%d\n", this.precision)); writer.write( String.format("rounding_type=%s\n", this.roundingType)); writer.write(String.format("prefix_rule=%s\n", this.prefixRule)); writer.write(String.format("one_way=%s\n", this.oneWay)); writer.write(String.format("include_duplicates=%s\n", this.includeDuplicateUnits)); } catch (final IOException e) { e.printStackTrace(); this.view.showErrorDialog("I/O Error", "Error occurred while saving settings: " + e.getLocalizedMessage()); } } public final void setIncludeDuplicateUnits( boolean includeDuplicateUnits) { this.includeDuplicateUnits = includeDuplicateUnits; this.view.update(); this.saveSettings(); } /** * Enables or disables one-way conversion. * * @param oneWay whether one-way conversion should be on (true) or off * (false) * @since 2020-08-27 */ public final void setOneWay(boolean oneWay) { this.oneWay = oneWay; if (oneWay) { this.fromExistenceCondition.setPredicate( unitName -> this.metricExceptions.contains(unitName) || !this.database.getUnit(unitName).isMetric()); this.toExistenceCondition.setPredicate( unitName -> this.metricExceptions.contains(unitName) || this.database.getUnit(unitName).isMetric()); } else { this.fromExistenceCondition.setPredicate(unitName -> true); this.toExistenceCondition.setPredicate(unitName -> true); } this.saveSettings(); } /** * @param precision new value of precision * @since 2019-01-15 * @since v0.1.0 */ public final void setPrecision(final int precision) { this.precision = precision; this.saveSettings(); } /** * @param prefixRepetitionRule the prefixRepetitionRule to set * @since 2020-08-26 */ public void setPrefixRepetitionRule( Predicate> prefixRepetitionRule) { if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) { this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule; } else { this.prefixRule = null; } this.database.setPrefixRepetitionRule(prefixRepetitionRule); this.saveSettings(); } /** * @param roundingType the roundingType to set * @since 2020-07-16 */ public final void setRoundingType(RoundingType roundingType) { this.roundingType = roundingType; this.saveSettings(); } /** * @return a list of all the entries in the dimension-based converter's To * box * @since 2020-08-27 */ public final Set toEntries() { return ConditionalExistenceCollections.conditionalExistenceSet( this.unitNameSet(), this.toExistenceCondition); } /** * Returns true if and only if the unit represented by {@code unitName} * has the dimension represented by {@code dimensionName}. * * @param unitName name of unit to test * @param dimensionName name of dimension to test * @return whether unit has dimenision * @since 2019-04-13 * @since v0.2.0 */ public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { final Unit unit = this.database.getUnit(unitName); final ObjectProduct dimension = this.database .getDimension(dimensionName); return unit.getDimension().equals(dimension); } /** * 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 final void unitNameSelected() { final String unitName = this.view.getUnitViewerSelection(); if (unitName == null) return; else { final Unit unit = this.database.getUnit(unitName); this.view.setUnitTextBoxText(unit.toString()); } } /** * @return a set of all of the unit names * @since 2019-04-14 * @since v0.2.0 */ public final Set unitNameSet() { return this.database.unitMapPrefixless(this.includeDuplicateUnits) .keySet(); } } /** * Different types of rounding. * * Significant digits: Rounds to a number of digits. i.e. with precision 5, * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits * after the decimal point, i.e. with precision 5, 12345.6789 rounds to * 12345.67890. Scientific: Rounds based on the number of digits and * operations, following standard scientific rounding. */ private static enum RoundingType { SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; } private static class View { private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); /** The view's frame. */ private final JFrame frame; /** The view's associated presenter. */ private final Presenter presenter; /** The master pane containing all of the tabs. */ private final JTabbedPane masterPane; // DIMENSION-BASED CONVERTER /** The panel for inputting values in the dimension-based converter */ private final JTextField valueInput; /** The panel for "From" in the dimension-based converter */ private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ private final SearchBoxList toSearch; /** The output area in the dimension-based converter */ private final JTextArea dimensionBasedOutput; // EXPRESSION-BASED CONVERTER /** 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; // UNIT AND PREFIX VIEWERS /** The searchable list of unit names in the unit viewer */ private final SearchBoxList unitNameList; /** The searchable list of prefix names in the prefix viewer */ private final SearchBoxList prefixNameList; /** The text box for unit data in the unit viewer */ private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; /** * Creates the {@code View}. * * @since 2019-01-14 * @since v0.1.0 */ public View() { this.presenter = new Presenter(this); this.frame = new JFrame("7Units"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // enable system look and feel try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) { // oh well, just use default theme System.err.println("Failed to enable system look-and-feel."); e.printStackTrace(); } // create the components this.masterPane = new JTabbedPane(); this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), this.presenter.getPrefixNameComparator(), true); this.unitTextBox = new JTextArea(); this.prefixTextBox = new JTextArea(); this.fromSearch = new SearchBoxList(this.presenter.fromEntries()); this.toSearch = new SearchBoxList(this.presenter.toEntries()); this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); this.dimensionBasedOutput = new JTextArea(2, 32); this.fromEntry = new JTextField(); this.toEntry = new JTextField(); this.output = new JTextArea(2, 32); // create more components this.initComponents(); this.frame.pack(); } /** * @return the currently selected pane. * @throws AssertionError if no pane (or an invalid pane) is selected */ public Pane getActivePane() { switch (this.masterPane.getSelectedIndex()) { case 0: return Pane.UNIT_CONVERTER; case 1: return Pane.EXPRESSION_CONVERTER; case 2: return Pane.UNIT_VIEWER; case 3: return Pane.PREFIX_VIEWER; case 4: return Pane.ABOUT; case 5: return Pane.SETTINGS; default: throw new AssertionError("No selected pane, or invalid pane."); } } /** * @return value in dimension-based converter * @throws ParseException * @since 2020-07-07 */ public double getDimensionConverterInput() throws ParseException { final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); if (value instanceof Double) return (double) value; else if (value instanceof Long) return ((Long) value).longValue(); else throw new AssertionError(); } /** * @return selection in "From" selector in dimension-based converter * @since 2019-04-13 * @since v0.2.0 */ public String getFromSelection() { return this.fromSearch.getSelectedValue(); } /** * @return text in "From" box in converter panel * @since 2019-01-15 * @since v0.1.0 */ public String getFromText() { return this.fromEntry.getText(); } /** * @return index of selected prefix in prefix viewer * @since 2019-01-15 * @since v0.1.0 */ public String getPrefixViewerSelection() { return this.prefixNameList.getSelectedValue(); } /** * @return selection in "To" selector in dimension-based converter * @since 2019-04-13 * @since v0.2.0 */ public String getToSelection() { return this.toSearch.getSelectedValue(); } /** * @return text in "To" box in converter panel * @since 2019-01-26 * @since v0.1.0 */ public String getToText() { return this.toEntry.getText(); } /** * @return index of selected unit in unit viewer * @since 2019-01-15 * @since v0.1.0 */ public String getUnitViewerSelection() { return this.unitNameList.getSelectedValue(); } /** * 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 masterPanel.add(this.masterPane, BorderLayout.CENTER); // update stuff this.masterPane.addChangeListener(e -> this.update()); { // a panel for unit conversion using a selector final JPanel convertUnitPanel = new JPanel(); this.masterPane.addTab("Convert Units", convertUnitPanel); this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); convertUnitPanel.setLayout(new BorderLayout()); { // panel for input part final JPanel inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); inputPanel.setLayout(new GridLayout(1, 3)); final JComboBox dimensionSelector = new JComboBox<>( this.presenter.dimensionNameList() .toArray(new String[0])); dimensionSelector.setSelectedItem("LENGTH"); // handle dimension filter final MutablePredicate dimensionFilter = new MutablePredicate<>( s -> true); // panel for From things inputPanel.add(this.fromSearch); this.fromSearch.addSearchFilter(dimensionFilter); { // for dimension selector and arrow that represents // conversion final JPanel inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); inBetweenPanel.setLayout(new BorderLayout()); { // dimension selector 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 inputPanel.add(this.toSearch); this.toSearch.addSearchFilter(dimensionFilter); // code for dimension filter dimensionSelector.addItemListener(e -> { dimensionFilter.setPredicate(string -> View.this.presenter .unitMatchesDimension(string, (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); }); // apply the item listener once because I have a default // selection dimensionFilter.setPredicate(string -> View.this.presenter .unitMatchesDimension(string, (String) dimensionSelector.getSelectedItem())); this.fromSearch.reapplyFilter(); this.toSearch.reapplyFilter(); } { // 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 valueInputPanel.add(this.valueInput, BorderLayout.CENTER); } } { // button to convert final JButton convertButton = new JButton("Convert"); outputPanel.add(convertButton); convertButton.addActionListener( e -> this.presenter.convertDimensionBased()); convertButton.setMnemonic(KeyEvent.VK_ENTER); } { // output of conversion outputPanel.add(this.dimensionBasedOutput); this.dimensionBasedOutput.setEditable(false); } } } { // panel for unit conversion using expressions final JPanel convertExpressionPanel = new JPanel(); this.masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); convertExpressionPanel.setLayout(new GridLayout(4, 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.convertExpressions()); convertButton.setMnemonic(KeyEvent.VK_ENTER); } { // 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 to look up units final JPanel unitLookupPanel = new JPanel(); this.masterPane.addTab("Unit Viewer", unitLookupPanel); this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); unitLookupPanel.setLayout(new GridLayout()); { // search panel unitLookupPanel.add(this.unitNameList); this.unitNameList.getSearchList().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(); this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); prefixLookupPanel.setLayout(new GridLayout(1, 2)); { // panel for listing and seaching prefixLookupPanel.add(this.prefixNameList); this.prefixNameList.getSearchList().addListSelectionListener( e -> this.presenter.prefixSelected()); } { // the text box for prefix's toString prefixLookupPanel.add(this.prefixTextBox); this.prefixTextBox.setEditable(false); this.prefixTextBox.setLineWrap(true); } } { // Info panel final JPanel infoPanel = new JPanel(); this.masterPane.addTab("\uD83D\uDEC8", // info (i) character new JScrollPane(infoPanel)); final JTextArea infoTextArea = new JTextArea(); infoTextArea.setEditable(false); infoTextArea.setOpaque(false); infoPanel.add(infoTextArea); // get info text final String infoText = Presenter .getLinesFromResource("/about.txt").stream() .map(Presenter::withoutComments) .collect(Collectors.joining("\n")) .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); infoTextArea.setText(infoText); } { // Settings panel final JPanel settingsPanel = new JPanel(); this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); settingsPanel.setLayout( new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); { // rounding settings final JPanel roundingPanel = new JPanel(); settingsPanel.add(roundingPanel); roundingPanel .setBorder(new TitledBorder("Rounding Settings")); roundingPanel.setLayout(new GridBagLayout()); // rounding rule selection final ButtonGroup roundingRuleButtons = new ButtonGroup(); final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton fixedPrecision = new JRadioButton( "Fixed Precision"); if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { fixedPrecision.setSelected(true); } fixedPrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); roundingRuleButtons.add(fixedPrecision); roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton fixedDecimals = new JRadioButton( "Fixed Decimal Places"); if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { fixedDecimals.setSelected(true); } fixedDecimals.addActionListener(e -> this.presenter .setRoundingType(RoundingType.DECIMAL_PLACES)); roundingRuleButtons.add(fixedDecimals); roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton relativePrecision = new JRadioButton( "Scientific Precision"); if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { relativePrecision.setSelected(true); } relativePrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) .setAnchor(GridBagConstraints.LINE_START).build()); final JLabel sliderLabel = new JLabel("Precision:"); roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) .setAnchor(GridBagConstraints.LINE_START).build()); final JSlider sigDigSlider = new JSlider(0, 12); roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) .setAnchor(GridBagConstraints.LINE_START).build()); sigDigSlider.setMajorTickSpacing(4); sigDigSlider.setMinorTickSpacing(1); sigDigSlider.setSnapToTicks(true); sigDigSlider.setPaintTicks(true); sigDigSlider.setPaintLabels(true); sigDigSlider.setValue(this.presenter.precision); sigDigSlider.addChangeListener(e -> this.presenter .setPrecision(sigDigSlider.getValue())); } { // prefix repetition settings final JPanel prefixRepetitionPanel = new JPanel(); settingsPanel.add(prefixRepetitionPanel); prefixRepetitionPanel.setBorder( new TitledBorder("Prefix Repetition Settings")); prefixRepetitionPanel.setLayout(new GridBagLayout()); // prefix rules final ButtonGroup prefixRuleButtons = new ButtonGroup(); final JRadioButton noRepetition = new JRadioButton( "No Repetition"); if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { noRepetition.setSelected(true); } noRepetition.addActionListener( e -> this.presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.NO_REPETITION)); prefixRuleButtons.add(noRepetition); prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START) .build()); final JRadioButton noRestriction = new JRadioButton( "No Restriction"); if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { noRestriction.setSelected(true); } noRestriction.addActionListener( e -> this.presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.NO_RESTRICTION)); prefixRuleButtons.add(noRestriction); prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START) .build()); final JRadioButton customRepetition = new JRadioButton( "Complex Repetition"); if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { customRepetition.setSelected(true); } customRepetition.addActionListener( e -> this.presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); prefixRuleButtons.add(customRepetition); prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START) .build()); } { // search settings final JPanel searchingPanel = new JPanel(); settingsPanel.add(searchingPanel); searchingPanel.setBorder(new TitledBorder("Search Settings")); searchingPanel.setLayout(new GridBagLayout()); // searching rules final ButtonGroup searchRuleButtons = new ButtonGroup(); final JRadioButton noPrefixes = new JRadioButton( "Never Include Prefixed Units"); noPrefixes.setEnabled(false); searchRuleButtons.add(noPrefixes); searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton fixedPrefixes = new JRadioButton( "Include Some Prefixes"); fixedPrefixes.setEnabled(false); searchRuleButtons.add(fixedPrefixes); searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton explicitPrefixes = new JRadioButton( "Include Explicit Prefixes"); explicitPrefixes.setEnabled(false); searchRuleButtons.add(explicitPrefixes); searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); final JRadioButton alwaysInclude = new JRadioButton( "Include All Single Prefixes"); alwaysInclude.setEnabled(false); searchRuleButtons.add(alwaysInclude); searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) .setAnchor(GridBagConstraints.LINE_START).build()); } { // miscellaneous settings final JPanel miscPanel = new JPanel(); settingsPanel.add(miscPanel); miscPanel .setBorder(new TitledBorder("Miscellaneous Settings")); miscPanel.setLayout(new GridBagLayout()); final JCheckBox oneWay = new JCheckBox( "Convert One Way Only"); oneWay.setSelected(this.presenter.oneWay); oneWay.addItemListener( e -> this.presenter.setOneWay(e.getStateChange() == 1)); miscPanel.add(oneWay, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); final JCheckBox showAllVariations = new JCheckBox( "Show Duplicates in \"Convert Units\""); showAllVariations .setSelected(this.presenter.includeDuplicateUnits); showAllVariations.addItemListener(e -> this.presenter .setIncludeDuplicateUnits(e.getStateChange() == 1)); miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); final JButton unitFileButton = new JButton( "Manage Unit Data Files"); unitFileButton.setEnabled(false); miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); } } } } /** * Sets the text in the output of the dimension-based converter. * * @param text text to set * @since 2019-04-13 * @since v0.2.0 */ public void setDimensionConverterOutputText(final String text) { this.dimensionBasedOutput.setText(text); } /** * 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 setExpressionConverterOutputText(final String text) { this.output.setText(text); } /** * Sets the text of the prefix text box in the prefix viewer. * * @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 in the unit viewer. * * @param text text to set * @since 2019-01-15 * @since v0.1.0 */ public void setUnitTextBoxText(final String text) { this.unitTextBox.setText(text); } /** * 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 void update() { this.unitNameList.setItems(this.presenter.unitNameSet()); this.fromSearch.setItems(this.presenter.fromEntries()); this.toSearch.setItems(this.presenter.toEntries()); switch (this.getActivePane()) { case UNIT_CONVERTER: this.fromSearch.updateList(); this.toSearch.updateList(); break; default: // do nothing, for now break; } } } public static void main(final String[] args) { new View().init(); } }