diff options
author | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-03-13 16:21:49 -0500 |
---|---|---|
committer | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-03-13 16:21:49 -0500 |
commit | fe4135a68cfed92ef336eec663e9c42c2c97dcbc (patch) | |
tree | 2fcf583265be3c575086f3ce3183a3268c997538 /src/org/unitConverter/converterGUI | |
parent | 761ba0b6627df8bc9f6ab41c94f349c84d378609 (diff) | |
parent | 184b7cc697ffc2dcbd49cfb3d0fd7b14bdac8803 (diff) |
Merge branch 'feature-settings-tab' into develop
Diffstat (limited to 'src/org/unitConverter/converterGUI')
3 files changed, 974 insertions, 305 deletions
diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..bdc3a2e --- /dev/null +++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,95 @@ +/** + * @since 2020-08-26 + */ +package org.unitConverter.converterGUI; + +import java.util.List; +import java.util.function.Predicate; + +import org.unitConverter.unit.SI; +import org.unitConverter.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { + NO_REPETITION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; + } + }; +} diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java index 1995466..10ef589 100644 --- a/src/org/unitConverter/converterGUI/SearchBoxList.java +++ b/src/org/unitConverter/converterGUI/SearchBoxList.java @@ -36,13 +36,13 @@ import javax.swing.JTextField; * @since v0.2.0 */ final class SearchBoxList extends JPanel { - + /** * @since 2019-04-13 * @since v0.2.0 */ private static final long serialVersionUID = 6226930279415983433L; - + /** * The text to place in an empty search box. * @@ -50,7 +50,7 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ private static final String EMPTY_TEXT = "Search..."; - + /** * The color to use for an empty foreground. * @@ -58,94 +58,92 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); - + // the components private final Collection<String> itemsToFilter; private final DelegateListModel<String> listModel; private final JTextField searchBox; private final JList<String> searchItems; - + private boolean searchBoxEmpty = true; - - // I need to do this because, for some reason, Swing is auto-focusing my search box without triggering a focus + + // I need to do this because, for some reason, Swing is auto-focusing my + // search box without triggering a focus // event. private boolean searchBoxFocused = false; - + private Predicate<String> customSearchFilter = o -> true; private final Comparator<String> defaultOrdering; private final boolean caseSensitive; - + /** * Creates the {@code SearchBoxList}. * - * @param itemsToFilter - * items to put in the list + * @param itemsToFilter items to put in the list * @since 2019-04-14 */ public SearchBoxList(final Collection<String> itemsToFilter) { this(itemsToFilter, null, false); } - + /** * Creates the {@code SearchBoxList}. * - * @param itemsToFilter - * items to put in the list - * @param defaultOrdering - * default ordering of items after filtration (null=Comparable) - * @param caseSensitive - * whether or not the filtration is case-sensitive + * @param itemsToFilter items to put in the list + * @param defaultOrdering default ordering of items after filtration + * (null=Comparable) + * @param caseSensitive whether or not the filtration is case-sensitive * * @since 2019-04-13 * @since v0.2.0 */ - public SearchBoxList(final Collection<String> itemsToFilter, final Comparator<String> defaultOrdering, + public SearchBoxList(final Collection<String> itemsToFilter, + final Comparator<String> defaultOrdering, final boolean caseSensitive) { super(new BorderLayout(), true); this.itemsToFilter = itemsToFilter; this.defaultOrdering = defaultOrdering; this.caseSensitive = caseSensitive; - + // create the components this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); this.searchItems = new JList<>(this.listModel); - + this.searchBox = new JTextField(EMPTY_TEXT); this.searchBox.setForeground(EMPTY_FOREGROUND); - + // add them to the panel this.add(this.searchBox, BorderLayout.PAGE_START); this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); - + // set up the search box this.searchBox.addFocusListener(new FocusListener() { @Override public void focusGained(final FocusEvent e) { SearchBoxList.this.searchBoxFocusGained(e); } - + @Override public void focusLost(final FocusEvent e) { SearchBoxList.this.searchBoxFocusLost(e); } }); - + this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); this.searchBoxEmpty = true; } - + /** * Adds an additional filter for searching. * - * @param filter - * filter to add. + * @param filter filter to add. * @since 2019-04-13 * @since v0.2.0 */ public void addSearchFilter(final Predicate<String> filter) { this.customSearchFilter = this.customSearchFilter.and(filter); } - + /** * Resets the search filter. * @@ -155,7 +153,7 @@ final class SearchBoxList extends JPanel { public void clearSearchFilters() { this.customSearchFilter = o -> true; } - + /** * @return this component's search box component * @since 2019-04-14 @@ -164,11 +162,11 @@ final class SearchBoxList extends JPanel { public final JTextField getSearchBox() { return this.searchBox; } - + /** - * @param searchText - * text to search for - * @return a filter that filters out that text, based on this list's case sensitive setting + * @param searchText text to search for + * @return a filter that filters out that text, based on this list's case + * sensitive setting * @since 2019-04-14 * @since v0.2.0 */ @@ -176,9 +174,10 @@ final class SearchBoxList extends JPanel { if (this.caseSensitive) return string -> string.contains(searchText); else - return string -> string.toLowerCase().contains(searchText.toLowerCase()); + return string -> string.toLowerCase() + .contains(searchText.toLowerCase()); } - + /** * @return this component's list component * @since 2019-04-14 @@ -187,7 +186,7 @@ final class SearchBoxList extends JPanel { public final JList<String> getSearchList() { return this.searchItems; } - + /** * @return index selected in item list * @since 2019-04-14 @@ -196,7 +195,7 @@ final class SearchBoxList extends JPanel { public int getSelectedIndex() { return this.searchItems.getSelectedIndex(); } - + /** * @return value selected in item list * @since 2019-04-13 @@ -205,7 +204,7 @@ final class SearchBoxList extends JPanel { public String getSelectedValue() { return this.searchItems.getSelectedValue(); } - + /** * Re-applies the filters. * @@ -213,29 +212,30 @@ final class SearchBoxList extends JPanel { * @since v0.2.0 */ public void reapplyFilter() { - final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); final Predicate<String> searchFilter = this.getSearchFilter(searchText); - + this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (searchFilter.test(string)) { this.listModel.add(string); } }); - + // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); - + // sorts the remaining items this.listModel.sort(comparator); } - + /** * Runs whenever the search box gains focus. * - * @param e - * focus event + * @param e focus event * @since 2019-04-13 * @since v0.2.0 */ @@ -246,12 +246,11 @@ final class SearchBoxList extends JPanel { this.searchBox.setForeground(Color.BLACK); } } - + /** * Runs whenever the search box loses focus. * - * @param e - * focus event + * @param e focus event * @since 2019-04-13 * @since v0.2.0 */ @@ -262,7 +261,7 @@ final class SearchBoxList extends JPanel { this.searchBox.setForeground(EMPTY_FOREGROUND); } } - + /** * Runs whenever the text in the search box is changed. * <p> @@ -276,10 +275,12 @@ final class SearchBoxList extends JPanel { if (this.searchBoxFocused) { this.searchBoxEmpty = this.searchBox.getText().equals(""); } - final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); final Predicate<String> searchFilter = this.getSearchFilter(searchText); - + // initialize list with items that match the filter then sort this.listModel.clear(); this.itemsToFilter.forEach(string -> { @@ -287,11 +288,20 @@ final class SearchBoxList extends JPanel { this.listModel.add(string); } }); - + // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); - + // sorts the remaining items this.listModel.sort(comparator); } + + /** + * Manually updates the search box's item list. + * + * @since 2020-08-27 + */ + public void updateList() { + this.searchBoxTextChanged(); + } } diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 0230728..6ddc4a0 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * 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 @@ -17,41 +17,63 @@ package org.unitConverter.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.File; +import java.io.BufferedWriter; +import java.io.IOException; 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.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 org.unitConverter.math.ConditionalExistenceCollections; import org.unitConverter.math.ObjectProduct; import org.unitConverter.unit.BaseDimension; import org.unitConverter.unit.BritishImperial; import org.unitConverter.unit.LinearUnit; +import org.unitConverter.unit.LinearUnitValue; +import org.unitConverter.unit.NameSymbol; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitDatabase; import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.unit.UnitValue; /** * @author Adrien Hopkins @@ -63,10 +85,22 @@ final class UnitConverterGUI { * A tab in the View. */ private enum Pane { - UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER; + 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 Path DEFAULT_UNITS_FILE = Path.of("unitsfile.txt"); + /** The default place where dimensions are stored. */ + private static final Path DEFAULT_DIMENSION_FILE = Path + .of("dimensionfile.txt"); + /** The default place where exceptions are stored. */ + private static final Path DEFAULT_EXCEPTIONS_FILE = Path + .of("metric_exceptions.txt"); + /** * Adds default units and dimensions to a database. * @@ -88,33 +122,65 @@ final class UnitConverterGUI { // nonlinear units - must be loaded manually database.addUnit("tempCelsius", SI.CELSIUS); database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); - + // load initial dimensions database.addDimension("LENGTH", SI.Dimensions.LENGTH); database.addDimension("MASS", SI.Dimensions.MASS); database.addDimension("TIME", SI.Dimensions.TIME); database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } - + + /** + * @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<String> unitNames; - + /** The names of all of the prefixes */ private final List<String> prefixNames; - + /** The names of all of the dimensions */ private final List<String> dimensionNames; - + + /** Unit names that are ignored by the metric-only/imperial-only filter */ + private final Set<String> metricExceptions; + private final Comparator<String> prefixNameComparator; - - private int significantFigures = 6; - + + /** 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<String> fromExistenceCondition = new MutablePredicate<>( + s -> true); + + private final MutablePredicate<String> 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; + /** * Creates the presenter. * @@ -124,14 +190,28 @@ final class UnitConverterGUI { */ Presenter(final View view) { this.view = view; - + // load initial units - this.database = new UnitDatabase(); + this.database = new UnitDatabase( + DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); - - this.database.loadUnitsFile(new File("unitsfile.txt")); - this.database.loadDimensionFile(new File("dimensionfile.txt")); - + + this.database.loadUnitsFile(DEFAULT_UNITS_FILE); + this.database.loadDimensionFile(DEFAULT_DIMENSION_FILE); + + // load metric exceptions + try { + this.metricExceptions = Files.readAllLines(DEFAULT_EXCEPTIONS_FILE) + .stream().map(Presenter::withoutComments) + .filter(s -> !s.isBlank()).collect(Collectors.toSet()); + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } + + // load settings - requires database to exist + 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 @@ -140,37 +220,43 @@ final class UnitConverterGUI { 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().keySet()); + + this.unitNames = new ArrayList<>( + this.database.unitMapPrefixless().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.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<Unit> isFullBase = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); - + final Predicate<Unit> 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", + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", new HashSet<>(this.database.unitMapPrefixless().values()).size(), this.database.unitMapPrefixless().size(), - new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count()); + new HashSet<>(this.database.unitMapPrefixless().values()) + .stream().filter(isFullBase).count()); } - + /** * Converts in the dimension-based converter * @@ -180,7 +266,8 @@ final class UnitConverterGUI { public final void convertDimensionBased() { final String fromSelection = this.view.getFromSelection(); if (fromSelection == null) { - this.view.showErrorDialog("Error", "No unit selected in From field"); + this.view.showErrorDialog("Error", + "No unit selected in From field"); return; } final String toSelection = this.view.getToSelection(); @@ -188,30 +275,35 @@ final class UnitConverterGUI { 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); - - final String input = this.view.getDimensionConverterInput(); - if (input.equals("")) { - this.view.showErrorDialog("Error", "No value to convert entered."); + 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 double beforeValue = Double.parseDouble(input); - final double value = from.convertTo(to, beforeValue); - + final UnitValue value = beforeValue.convertTo(to); + final String output = this.getRoundedString(value); - + this.view.setDimensionConverterOutputText( - String.format("%s %s = %s %s", input, fromSelection, output, toSelection)); + String.format("%s = %s", beforeValue, output)); } - + /** * Runs whenever the convert button is pressed. * * <p> - * 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. + * 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. * </p> * * @since 2019-01-26 @@ -220,64 +312,79 @@ final class UnitConverterGUI { 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."); + 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."); + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the To: box."); return; } - - // try to parse from - final Unit from; + + final LinearUnitValue from; + final Unit to; try { - from = this.database.getUnitFromExpression(fromUnitString); - } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); + 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; } - - final double value; - // try to parse to - final Unit to; try { - if (this.database.containsUnitName(toUnitString)) { - // if it's a unit, convert to that - to = this.database.getUnit(toUnitString); + 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 { - to = this.database.getUnitFromExpression(toUnitString); + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + return; } - } catch (final IllegalArgumentException e) { - this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); + + final LinearUnitValue converted = from2.convertTo(to2); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); return; - } - - if (from.canConvertTo(to)) { - value = from.convertTo(to, 1); - - // round value - final String output = this.getRoundedString(value); - - this.view.setExpressionConverterOutputText( - String.format("%s = %s %s", fromUnitString, output, toUnitString)); - } else if (from instanceof LinearUnit && SI.ONE.dividedBy((LinearUnit) from).canConvertTo(to)) { - // reciprocal conversion (like seconds to hertz) - value = SI.ONE.dividedBy((LinearUnit) from).convertTo(to, 1); - - // round value - final String output = this.getRoundedString(value); - - this.view.setExpressionConverterOutputText( - String.format("1 / %s = %s %s", fromUnitString, output, toUnitString)); } else { - // if I can't convert, leave - this.view.showErrorDialog("Conversion Error", - String.format("Cannot convert between %s and %s", fromUnitString, toUnitString)); + // 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 @@ -286,7 +393,17 @@ final class UnitConverterGUI { public final List<String> 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<String> fromEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.fromExistenceCondition); + } + /** * @return a comparator to compare prefix names * @since 2019-04-14 @@ -295,19 +412,54 @@ final class UnitConverterGUI { public final Comparator<String> getPrefixNameComparator() { return this.prefixNameComparator; } - + /** - * @param value value to round - * @return string of that value rounded to {@code significantDigits} significant - * digits. - * @since 2019-04-14 - * @since v0.2.0 + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. + * + * @since 2020-08-04 */ - private final String getRoundedString(final double value) { - // round value - final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures)); - String output = bigValue.toString(); - + 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")) { @@ -317,10 +469,74 @@ final class UnitConverterGUI { output = output.substring(0, output.length() - 1); } } - - return output; + + 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; + 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 @@ -329,7 +545,7 @@ final class UnitConverterGUI { public final Set<String> prefixNameSet() { return this.database.prefixMap().keySet(); } - + /** * Runs whenever a prefix is selected in the viewer. * <p> @@ -345,23 +561,107 @@ final class UnitConverterGUI { return; else { final UnitPrefix prefix = this.database.getPrefix(prefixName); - - this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier())); + + 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)); + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorDialog("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + } + } + + /** + * 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 significantFigures new value of significantFigures + * @param precision new value of precision * @since 2019-01-15 * @since v0.1.0 */ - public final void setSignificantFigures(final int significantFigures) { - this.significantFigures = significantFigures; + public final void setPrecision(final int precision) { + this.precision = precision; + + this.saveSettings(); } - + /** - * Returns true if and only if the unit represented by {@code unitName} has the - * dimension represented by {@code dimensionName}. + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public void setPrefixRepetitionRule( + Predicate<List<UnitPrefix>> 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<String> 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 @@ -369,12 +669,14 @@ final class UnitConverterGUI { * @since 2019-04-13 * @since v0.2.0 */ - public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { + public final boolean unitMatchesDimension(final String unitName, + final String dimensionName) { final Unit unit = this.database.getUnit(unitName); - final ObjectProduct<BaseDimension> dimension = this.database.getDimension(dimensionName); + final ObjectProduct<BaseDimension> dimension = this.database + .getDimension(dimensionName); return unit.getDimension().equals(dimension); } - + /** * Runs whenever a unit is selected in the viewer. * <p> @@ -390,29 +692,44 @@ final class UnitConverterGUI { 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<String> unitNameSet() { + private final Set<String> unitNameSet() { return this.database.unitMapPrefixless().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; @@ -422,7 +739,7 @@ final class UnitConverterGUI { 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; @@ -430,7 +747,7 @@ final class UnitConverterGUI { 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; @@ -440,7 +757,7 @@ final class UnitConverterGUI { private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; - + /** * Creates the {@code View}. * @@ -451,28 +768,38 @@ final class UnitConverterGUI { this.presenter = new Presenter(this); this.frame = new JFrame("Unit Converter"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - this.masterPane = new JTabbedPane(); - + + // 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.unitNameSet()); - this.toSearch = new SearchBoxList(this.presenter.unitNameSet()); - this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################")); + 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 @@ -487,20 +814,30 @@ final class UnitConverterGUI { 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 - * @since 2019-04-13 - * @since v0.2.0 + * @throws ParseException + * @since 2020-07-07 */ - public String getDimensionConverterInput() { - return this.valueInput.getText(); + 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 @@ -509,7 +846,7 @@ final class UnitConverterGUI { public String getFromSelection() { return this.fromSearch.getSelectedValue(); } - + /** * @return text in "From" box in converter panel * @since 2019-01-15 @@ -518,7 +855,7 @@ final class UnitConverterGUI { public String getFromText() { return this.fromEntry.getText(); } - + /** * @return index of selected prefix in prefix viewer * @since 2019-01-15 @@ -527,7 +864,7 @@ final class UnitConverterGUI { public String getPrefixViewerSelection() { return this.prefixNameList.getSelectedValue(); } - + /** * @return selection in "To" selector in dimension-based converter * @since 2019-04-13 @@ -536,7 +873,7 @@ final class UnitConverterGUI { public String getToSelection() { return this.toSearch.getSelectedValue(); } - + /** * @return text in "To" box in converter panel * @since 2019-01-26 @@ -545,7 +882,7 @@ final class UnitConverterGUI { public String getToText() { return this.toEntry.getText(); } - + /** * @return index of selected unit in unit viewer * @since 2019-01-15 @@ -554,7 +891,7 @@ final class UnitConverterGUI { public String getUnitViewerSelection() { return this.unitNameList.getSelectedValue(); } - + /** * Starts up the application. * @@ -564,7 +901,7 @@ final class UnitConverterGUI { public final void init() { this.frame.setVisible(true); } - + /** * Initializes the view's components. * @@ -574,229 +911,443 @@ final class UnitConverterGUI { 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<String> dimensionSelector = new JComboBox<>( - this.presenter.dimensionNameList().toArray(new String[0])); + this.presenter.dimensionNameList() + .toArray(new String[0])); dimensionSelector.setSelectedItem("LENGTH"); - + // handle dimension filter - final MutablePredicate<String> dimensionFilter = new MutablePredicate<>(s -> true); - + final MutablePredicate<String> 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 + + { // 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); + 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())); + 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())); + + // 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); + final JLabel valuePrompt = new JLabel( + "Value to convert: "); + valueInputPanel.add(valuePrompt, + BorderLayout.LINE_START); } - + { // value to convert - valueInputPanel.add(this.valueInput, BorderLayout.CENTER); + 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.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.addTab("Convert Unit Expressions", + convertExpressionPanel); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); - - convertExpressionPanel.setLayout(new GridLayout(5, 1)); - + + 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.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 + .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(); 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()); + + 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()); + + 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; + try { + final Path aboutFile = Path.of("src", "about.txt"); + infoText = Files.readAllLines(aboutFile).stream() + .map(Presenter::withoutComments) + .collect(Collectors.joining("\n")); + } catch (final IOException e) { + throw new AssertionError("I/O exception loading about.txt"); + } + 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 Symbols in \"Convert Units\""); + showAllVariations.setSelected(true); + showAllVariations.setEnabled(false); + 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. * @@ -807,7 +1358,7 @@ final class UnitConverterGUI { public void setDimensionConverterOutputText(final String text) { this.dimensionBasedOutput.setText(text); } - + /** * Sets the text in the output of the conversion panel. * @@ -818,7 +1369,7 @@ final class UnitConverterGUI { public void setExpressionConverterOutputText(final String text) { this.output.setText(text); } - + /** * Sets the text of the prefix text box in the prefix viewer. * @@ -829,7 +1380,7 @@ final class UnitConverterGUI { public void setPrefixTextBoxText(final String text) { this.prefixTextBox.setText(text); } - + /** * Sets the text of the unit text box in the unit viewer. * @@ -840,7 +1391,7 @@ final class UnitConverterGUI { public void setUnitTextBoxText(final String text) { this.unitTextBox.setText(text); } - + /** * Shows an error dialog. * @@ -850,10 +1401,23 @@ final class UnitConverterGUI { * @since v0.1.0 */ public void showErrorDialog(final String title, final String message) { - JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); + } + + public void update() { + 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(); } |