summaryrefslogtreecommitdiff
path: root/src/main/java/sevenUnitsGUI
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/sevenUnitsGUI')
-rw-r--r--src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java95
-rw-r--r--src/main/java/sevenUnitsGUI/DelegateListModel.java242
-rw-r--r--src/main/java/sevenUnitsGUI/ExpressionConversionView.java45
-rw-r--r--src/main/java/sevenUnitsGUI/FilterComparator.java128
-rw-r--r--src/main/java/sevenUnitsGUI/GridBagBuilder.java479
-rw-r--r--src/main/java/sevenUnitsGUI/Main.java34
-rw-r--r--src/main/java/sevenUnitsGUI/MutablePredicate.java70
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java782
-rw-r--r--src/main/java/sevenUnitsGUI/SearchBoxList.java341
-rw-r--r--src/main/java/sevenUnitsGUI/StandardDisplayRules.java246
-rw-r--r--src/main/java/sevenUnitsGUI/TabbedView.java797
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionRecord.java199
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionView.java108
-rw-r--r--src/main/java/sevenUnitsGUI/View.java105
-rw-r--r--src/main/java/sevenUnitsGUI/ViewBot.java507
-rw-r--r--src/main/java/sevenUnitsGUI/package-info.java23
16 files changed, 4201 insertions, 0 deletions
diff --git a/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
new file mode 100644
index 0000000..b56356d
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
@@ -0,0 +1,95 @@
+/**
+ * @since 2020-08-26
+ */
+package sevenUnitsGUI;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.UnitPrefix;
+
+/**
+ * A rule that specifies whether prefix repetition is allowed
+ *
+ * @since 2020-08-26
+ */
+public 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 (!Metric.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 (!Metric.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 (Metric.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 (Metric.YOTTA.equals(prefix) || Metric.YOCTO.equals(prefix)) {
+ part = 0;
+ } else if (Metric.THOUSAND_PREFIXES.contains(prefix)) {
+ part = 1;
+ } else {
+ part = 2;
+ }
+ }
+ return true;
+ }
+ };
+}
diff --git a/src/main/java/sevenUnitsGUI/DelegateListModel.java b/src/main/java/sevenUnitsGUI/DelegateListModel.java
new file mode 100644
index 0000000..5938b59
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/DelegateListModel.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.swing.AbstractListModel;
+
+/**
+ * A list model that delegates to a list.
+ * <p>
+ * It is recommended to use the delegate methods in DelegateListModel instead of the delegated list's methods because
+ * the delegate methods handle updating the list.
+ * </p>
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+final class DelegateListModel<E> extends AbstractListModel<E> implements List<E> {
+ /**
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private static final long serialVersionUID = 8985494428224810045L;
+
+ /**
+ * The list that this model is a delegate to.
+ *
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private final List<E> delegate;
+
+ /**
+ * Creates an empty {@code DelegateListModel}.
+ *
+ * @since 2019-04-13
+ */
+ public DelegateListModel() {
+ this(new ArrayList<>());
+ }
+
+ /**
+ * Creates the {@code DelegateListModel}.
+ *
+ * @param delegate
+ * list to delegate
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public DelegateListModel(final List<E> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean add(final E element) {
+ final int index = this.delegate.size();
+ final boolean success = this.delegate.add(element);
+ this.fireIntervalAdded(this, index, index);
+ return success;
+ }
+
+ @Override
+ public void add(final int index, final E element) {
+ this.delegate.add(index, element);
+ this.fireIntervalAdded(this, index, index);
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends E> c) {
+ boolean changed = false;
+ for (final E e : c) {
+ if (this.add(e)) {
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean addAll(final int index, final Collection<? extends E> c) {
+ for (final E e : c) {
+ this.add(index, e);
+ }
+ return !c.isEmpty(); // Since this is a list, it will always change if c has elements.
+ }
+
+ @Override
+ public void clear() {
+ final int oldSize = this.delegate.size();
+ this.delegate.clear();
+ if (oldSize >= 1) {
+ this.fireIntervalRemoved(this, 0, oldSize - 1);
+ }
+ }
+
+ @Override
+ public boolean contains(final Object elem) {
+ return this.delegate.contains(elem);
+ }
+
+ @Override
+ public boolean containsAll(final Collection<?> c) {
+ for (final Object e : c) {
+ if (!c.contains(e))
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public E get(final int index) {
+ return this.delegate.get(index);
+ }
+
+ @Override
+ public E getElementAt(final int index) {
+ return this.delegate.get(index);
+ }
+
+ @Override
+ public int getSize() {
+ return this.delegate.size();
+ }
+
+ @Override
+ public int indexOf(final Object elem) {
+ return this.delegate.indexOf(elem);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.delegate.isEmpty();
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return this.delegate.iterator();
+ }
+
+ @Override
+ public int lastIndexOf(final Object elem) {
+ return this.delegate.lastIndexOf(elem);
+ }
+
+ @Override
+ public ListIterator<E> listIterator() {
+ return this.delegate.listIterator();
+ }
+
+ @Override
+ public ListIterator<E> listIterator(final int index) {
+ return this.delegate.listIterator(index);
+ }
+
+ @Override
+ public E remove(final int index) {
+ final E returnValue = this.delegate.get(index);
+ this.delegate.remove(index);
+ this.fireIntervalRemoved(this, index, index);
+ return returnValue;
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ final int index = this.delegate.indexOf(o);
+ final boolean returnValue = this.delegate.remove(o);
+ this.fireIntervalRemoved(this, index, index);
+ return returnValue;
+ }
+
+ @Override
+ public boolean removeAll(final Collection<?> c) {
+ boolean changed = false;
+ for (final Object e : c) {
+ if (this.remove(e)) {
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean retainAll(final Collection<?> c) {
+ final int oldSize = this.size();
+ final boolean returnValue = this.delegate.retainAll(c);
+ this.fireIntervalRemoved(this, this.size(), oldSize - 1);
+ return returnValue;
+ }
+
+ @Override
+ public E set(final int index, final E element) {
+ final E returnValue = this.delegate.get(index);
+ this.delegate.set(index, element);
+ this.fireContentsChanged(this, index, index);
+ return returnValue;
+ }
+
+ @Override
+ public int size() {
+ return this.delegate.size();
+ }
+
+ @Override
+ public List<E> subList(final int fromIndex, final int toIndex) {
+ return this.delegate.subList(fromIndex, toIndex);
+ }
+
+ @Override
+ public Object[] toArray() {
+ return this.delegate.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(final T[] a) {
+ return this.delegate.toArray(a);
+ }
+
+ @Override
+ public String toString() {
+ return this.delegate.toString();
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
new file mode 100644
index 0000000..872ca10
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (C) 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+/**
+ * A View that can convert unit expressions
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public interface ExpressionConversionView extends View {
+ /**
+ * @return unit expression to convert <em>from</em>
+ * @since 2021-12-15
+ */
+ String getFromExpression();
+
+ /**
+ * @return unit expression to convert <em>to</em>
+ * @since 2021-12-15
+ */
+ String getToExpression();
+
+ /**
+ * Shows the output of an expression conversion to the user.
+ *
+ * @param uc unit conversion to show
+ * @since 2021-12-15
+ */
+ void showExpressionConversionOutput(UnitConversionRecord uc);
+}
diff --git a/src/main/java/sevenUnitsGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java
new file mode 100644
index 0000000..f34d0c0
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/FilterComparator.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * A comparator that compares strings using a filter.
+ *
+ * @param <T> type of element being compared
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+final class FilterComparator<T> implements Comparator<T> {
+ /**
+ * The filter that the comparator is filtered by.
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ private final String filter;
+ /**
+ * The comparator to use if the arguments are otherwise equal.
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ private final Comparator<T> comparator;
+ /**
+ * Whether or not the comparison is case-sensitive.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private final boolean caseSensitive;
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public FilterComparator(final String filter) {
+ this(filter, null);
+ }
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter string to filter by
+ * @param comparator comparator to fall back to if all else fails, null is
+ * compareTo.
+ * @throws NullPointerException if filter is null
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public FilterComparator(final String filter,
+ final Comparator<T> comparator) {
+ this(filter, comparator, false);
+ }
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter string to filter by
+ * @param comparator comparator to fall back to if all else fails, null is
+ * compareTo.
+ * @param caseSensitive whether or not the comparator is case-sensitive
+ * @throws NullPointerException if filter is null
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public FilterComparator(final String filter, final Comparator<T> comparator,
+ final boolean caseSensitive) {
+ this.filter = Objects.requireNonNull(filter, "filter must not be null.");
+ this.comparator = comparator;
+ this.caseSensitive = caseSensitive;
+ }
+
+ @Override
+ public int compare(final T arg0, final T arg1) {
+ // if this is case insensitive, make them lowercase
+ final String str0, str1;
+ if (this.caseSensitive) {
+ str0 = arg0.toString();
+ str1 = arg1.toString();
+ } else {
+ str0 = arg0.toString().toLowerCase();
+ str1 = arg1.toString().toLowerCase();
+ }
+
+ // elements that start with the filter always go first
+ if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
+ return -1;
+ else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
+ return 1;
+
+ // elements that contain the filter but don't start with them go next
+ if (str0.contains(this.filter) && !str1.contains(this.filter))
+ return -1;
+ else if (!str0.contains(this.filter) && !str1.contains(this.filter))
+ return 1;
+
+ // other elements go last
+ if (this.comparator == null)
+ return str0.compareTo(str1);
+ else
+ return this.comparator.compare(arg0, arg1);
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
new file mode 100644
index 0000000..32e94d7
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
@@ -0,0 +1,479 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+/**
+ * A builder for Java's {@link java.awt.GridBagConstraints} class.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+final class GridBagBuilder {
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridx} property.
+ * <p>
+ * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has
+ * <code>gridx=0</code>. The leading edge of a component's display area is its left edge for a horizontal,
+ * left-to-right container and its right edge for a horizontal, right-to-left container. The value
+ * <code>RELATIVE</code> specifies that the component be placed immediately following the component that was added
+ * to the container just before this component was added.
+ * <p>
+ * The default value is <code>RELATIVE</code>. <code>gridx</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridy
+ * @see java.awt.ComponentOrientation
+ */
+ private final int gridx;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridy} property.
+ * <p>
+ * Specifies the cell at the top of the component's display area, where the topmost cell has <code>gridy=0</code>.
+ * The value <code>RELATIVE</code> specifies that the component be placed just below the component that was added to
+ * the container just before this component was added.
+ * <p>
+ * The default value is <code>RELATIVE</code>. <code>gridy</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridx
+ */
+ private final int gridy;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridwidth} property.
+ * <p>
+ * Specifies the number of cells in a row for the component's display area.
+ * <p>
+ * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridx</code> to the
+ * last cell in the row. Use <code>RELATIVE</code> to specify that the component's display area will be from
+ * <code>gridx</code> to the next to the last one in its row.
+ * <p>
+ * <code>gridwidth</code> should be non-negative and the default value is 1.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridheight
+ */
+ private final int gridwidth;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridheight} property.
+ * <p>
+ * Specifies the number of cells in a column for the component's display area.
+ * <p>
+ * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridy</code> to the
+ * last cell in the column. Use <code>RELATIVE</code> to specify that the component's display area will be from
+ * <code>gridy</code> to the next to the last one in its column.
+ * <p>
+ * <code>gridheight</code> should be a non-negative value and the default value is 1.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridwidth
+ */
+ private final int gridheight;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code weightx} property.
+ * <p>
+ * Specifies how to distribute extra horizontal space.
+ * <p>
+ * The grid bag layout manager calculates the weight of a column to be the maximum <code>weightx</code> of all the
+ * components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra
+ * space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no
+ * extra space.
+ * <p>
+ * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right
+ * edges.
+ * <p>
+ * The default value of this field is <code>0</code>. <code>weightx</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#weighty
+ */
+ private double weightx;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code weighty} property.
+ * <p>
+ * Specifies how to distribute extra vertical space.
+ * <p>
+ * The grid bag layout manager calculates the weight of a row to be the maximum <code>weighty</code> of all the
+ * components in a row. If the resulting layout is smaller vertically than the area it needs to fill, the extra
+ * space is distributed to each row in proportion to its weight. A row that has a weight of zero receives no extra
+ * space.
+ * <p>
+ * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom
+ * edges.
+ * <p>
+ * The default value of this field is <code>0</code>. <code>weighty</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#weightx
+ */
+ private double weighty;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code anchor} property.
+ * <p>
+ * This field is used when the component is smaller than its display area. It determines where, within the display
+ * area, to place the component.
+ * <p>
+ * There are three kinds of possible values: orientation relative, baseline relative and absolute. Orientation
+ * relative values are interpreted relative to the container's component orientation property, baseline relative
+ * values are interpreted relative to the baseline and absolute values are not. The absolute values are:
+ * <code>CENTER</code>, <code>NORTH</code>, <code>NORTHEAST</code>, <code>EAST</code>, <code>SOUTHEAST</code>,
+ * <code>SOUTH</code>, <code>SOUTHWEST</code>, <code>WEST</code>, and <code>NORTHWEST</code>. The orientation
+ * relative values are: <code>PAGE_START</code>, <code>PAGE_END</code>, <code>LINE_START</code>,
+ * <code>LINE_END</code>, <code>FIRST_LINE_START</code>, <code>FIRST_LINE_END</code>, <code>LAST_LINE_START</code>
+ * and <code>LAST_LINE_END</code>. The baseline relative values are: <code>BASELINE</code>,
+ * <code>BASELINE_LEADING</code>, <code>BASELINE_TRAILING</code>, <code>ABOVE_BASELINE</code>,
+ * <code>ABOVE_BASELINE_LEADING</code>, <code>ABOVE_BASELINE_TRAILING</code>, <code>BELOW_BASELINE</code>,
+ * <code>BELOW_BASELINE_LEADING</code>, and <code>BELOW_BASELINE_TRAILING</code>. The default value is
+ * <code>CENTER</code>.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.ComponentOrientation
+ */
+ private int anchor;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code fill} property.
+ * <p>
+ * This field is used when the component's display area is larger than the component's requested size. It determines
+ * whether to resize the component, and if so, how.
+ * <p>
+ * The following values are valid for <code>fill</code>:
+ *
+ * <ul>
+ * <li><code>NONE</code>: Do not resize the component.
+ * <li><code>HORIZONTAL</code>: Make the component wide enough to fill its display area horizontally, but do not
+ * change its height.
+ * <li><code>VERTICAL</code>: Make the component tall enough to fill its display area vertically, but do not change
+ * its width.
+ * <li><code>BOTH</code>: Make the component fill its display area entirely.
+ * </ul>
+ * <p>
+ * The default value is <code>NONE</code>.
+ *
+ * @serial
+ * @see #clone()
+ */
+ private int fill;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code insets} property.
+ * <p>
+ * This field specifies the external padding of the component, the minimum amount of space between the component and
+ * the edges of its display area.
+ * <p>
+ * The default value is <code>new Insets(0, 0, 0, 0)</code>.
+ *
+ * @serial
+ * @see #clone()
+ */
+ private Insets insets;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code ipadx} property.
+ * <p>
+ * This field specifies the internal padding of the component, how much space to add to the minimum width of the
+ * component. The width of the component is at least its minimum width plus <code>ipadx</code> pixels.
+ * <p>
+ * The default value is <code>0</code>.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#ipady
+ */
+ private int ipadx;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code ipady} property.
+ * <p>
+ * This field specifies the internal padding, that is, how much space to add to the minimum height of the component.
+ * The height of the component is at least its minimum height plus <code>ipady</code> pixels.
+ * <p>
+ * The default value is 0.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#ipadx
+ */
+ private int ipady;
+
+ /**
+ * @param gridx
+ * x position
+ * @param gridy
+ * y position
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder(final int gridx, final int gridy) {
+ this(gridx, gridy, 1, 1);
+ }
+
+ /**
+ * @param gridx
+ * x position
+ * @param gridy
+ * y position
+ * @param gridwidth
+ * number of cells occupied horizontally
+ * @param gridheight
+ * number of cells occupied vertically
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight) {
+ this(gridx, gridy, gridwidth, gridheight, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
+ new Insets(0, 0, 0, 0), 0, 0);
+ }
+
+ /**
+ * @param gridx
+ * x position
+ * @param gridy
+ * y position
+ * @param gridwidth
+ * number of cells occupied horizontally
+ * @param gridheight
+ * number of cells occupied vertically
+ * @param weightx
+ * @param weighty
+ * @param anchor
+ * @param fill
+ * @param insets
+ * @param ipadx
+ * @param ipady
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ private GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight,
+ final double weightx, final double weighty, final int anchor, final int fill, final Insets insets,
+ final int ipadx, final int ipady) {
+ super();
+ this.gridx = gridx;
+ this.gridy = gridy;
+ this.gridwidth = gridwidth;
+ this.gridheight = gridheight;
+ this.weightx = weightx;
+ this.weighty = weighty;
+ this.anchor = anchor;
+ this.fill = fill;
+ this.insets = (Insets) insets.clone();
+ this.ipadx = ipadx;
+ this.ipady = ipady;
+ }
+
+ /**
+ * @return {@code GridBagConstraints} created by this builder
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagConstraints build() {
+ return new GridBagConstraints(this.gridx, this.gridy, this.gridwidth, this.gridheight, this.weightx,
+ this.weighty, this.anchor, this.fill, this.insets, this.ipadx, this.ipady);
+ }
+
+ /**
+ * @return anchor
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getAnchor() {
+ return this.anchor;
+ }
+
+ /**
+ * @return fill
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getFill() {
+ return this.fill;
+ }
+
+ /**
+ * @return gridheight
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridheight() {
+ return this.gridheight;
+ }
+
+ /**
+ * @return gridwidth
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridwidth() {
+ return this.gridwidth;
+ }
+
+ /**
+ * @return gridx
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridx() {
+ return this.gridx;
+ }
+
+ /**
+ * @return gridy
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridy() {
+ return this.gridy;
+ }
+
+ /**
+ * @return insets
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public Insets getInsets() {
+ return this.insets;
+ }
+
+ /**
+ * @return ipadx
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getIpadx() {
+ return this.ipadx;
+ }
+
+ /**
+ * @return ipady
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getIpady() {
+ return this.ipady;
+ }
+
+ /**
+ * @return weightx
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public double getWeightx() {
+ return this.weightx;
+ }
+
+ /**
+ * @return weighty
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public double getWeighty() {
+ return this.weighty;
+ }
+
+ /**
+ * @param anchor
+ * anchor to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setAnchor(final int anchor) {
+ this.anchor = anchor;
+ return this;
+ }
+
+ /**
+ * @param fill
+ * fill to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setFill(final int fill) {
+ this.fill = fill;
+ return this;
+ }
+
+ /**
+ * @param insets
+ * insets to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setInsets(final Insets insets) {
+ this.insets = insets;
+ return this;
+ }
+
+ /**
+ * @param ipadx
+ * ipadx to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setIpadx(final int ipadx) {
+ this.ipadx = ipadx;
+ return this;
+ }
+
+ /**
+ * @param ipady
+ * ipady to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setIpady(final int ipady) {
+ this.ipady = ipady;
+ return this;
+ }
+
+ /**
+ * @param weightx
+ * weightx to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setWeightx(final double weightx) {
+ this.weightx = weightx;
+ return this;
+ }
+
+ /**
+ * @param weighty
+ * weighty to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setWeighty(final double weighty) {
+ this.weighty = weighty;
+ return this;
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java
new file mode 100644
index 0000000..b5a896f
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Main.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+/**
+ * The main code for the 7Units GUI
+ *
+ * @since 2022-04-19
+ */
+public final class Main {
+
+ /**
+ * @param args
+ * @since 2022-04-19
+ */
+ public static void main(String[] args) {
+ View.createTabbedView();
+ }
+
+}
diff --git a/src/main/java/sevenUnitsGUI/MutablePredicate.java b/src/main/java/sevenUnitsGUI/MutablePredicate.java
new file mode 100644
index 0000000..6cb8689
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/MutablePredicate.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.function.Predicate;
+
+/**
+ * A container for a predicate, which can be changed later.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+final class MutablePredicate<T> implements Predicate<T> {
+ /**
+ * The predicate stored in this {@code MutablePredicate}
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private Predicate<T> predicate;
+
+ /**
+ * Creates the {@code MutablePredicate}.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public MutablePredicate(final Predicate<T> predicate) {
+ this.predicate = predicate;
+ }
+
+ /**
+ * @return predicate
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final Predicate<T> getPredicate() {
+ return this.predicate;
+ }
+
+ /**
+ * @param predicate
+ * new value of predicate
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final void setPredicate(final Predicate<T> predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean test(final T t) {
+ return this.predicate.test(t);
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
new file mode 100644
index 0000000..4feea44
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -0,0 +1,782 @@
+/**
+ * Copyright (C) 2021-2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.BaseDimension;
+import sevenUnits.unit.BaseUnit;
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.Unit;
+import sevenUnits.unit.UnitDatabase;
+import sevenUnits.unit.UnitPrefix;
+import sevenUnits.unit.UnitType;
+import sevenUnits.unit.UnitValue;
+import sevenUnits.utils.Nameable;
+import sevenUnits.utils.ObjectProduct;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * An object that handles interactions between the view and the backend code
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public final class Presenter {
+ /** The default place where settings are stored. */
+ private static final Path DEFAULT_SETTINGS_FILEPATH = Path
+ .of("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);
+ }
+
+ /**
+ * @return text in About file
+ * @since 2022-02-19
+ */
+ public static final String getAboutText() {
+ return Presenter.getLinesFromResource("/about.txt").stream()
+ .map(Presenter::withoutComments).collect(Collectors.joining("\n"))
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString());
+ }
+
+ /**
+ * 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
+ */
+ private static final List<String> getLinesFromResource(String filename) {
+ final List<String> 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 Presenter.class.getResourceAsStream(filepath);
+ }
+
+ /**
+ * @return true iff a and b have any elements in common
+ * @since 2022-04-19
+ */
+ private static final boolean sharesAnyElements(Set<?> a, Set<?> b) {
+ for (final Object e : a) {
+ if (b.contains(e))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @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(0, index);
+ }
+
+ // ====== SETTINGS ======
+
+ /**
+ * The view that this presenter communicates with
+ */
+ private final View view;
+
+ /**
+ * The database that this presenter communicates with (effectively the model)
+ */
+ final UnitDatabase database;
+
+ /**
+ * The rule used for parsing input numbers. Any number-string inputted into
+ * this program will be parsed using this method. <b>Not implemented yet.</b>
+ */
+ private Function<String, UncertainDouble> numberParsingRule;
+
+ /**
+ * The rule used for displaying the results of unit conversions. The result
+ * of unit conversions will be put into this function, and the resulting
+ * string will be used in the output.
+ */
+ private Function<UncertainDouble, String> numberDisplayRule = StandardDisplayRules
+ .uncertaintyBased();
+
+ /**
+ * A predicate that determines whether or not a certain combination of
+ * prefixes is allowed. If it returns false, a combination of prefixes will
+ * not be allowed. Prefixes are put in the list from right to left.
+ */
+ private Predicate<List<UnitPrefix>> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION;
+
+ /**
+ * The set of units that is considered neither metric nor nonmetric for the
+ * purposes of the metric-imperial one-way conversion. These units are
+ * included in both From and To, even if One Way Conversion is enabled.
+ */
+ private final Set<String> metricExceptions;
+
+ /**
+ * If this is true, views that show units as a list will have metric units
+ * removed from the From unit list and imperial/USC units removed from the To
+ * unit list.
+ */
+ private boolean oneWayConversionEnabled = false;
+
+ /**
+ * If this is false, duplicate units and prefixes will be removed from the
+ * unit view in views that show units as a list to choose from.
+ */
+ private boolean showDuplicates = false;
+
+ /**
+ * Creates a Presenter
+ *
+ * @param view the view that this presenter communicates with
+ * @since 2021-12-15
+ */
+ public Presenter(View view) {
+ this.view = view;
+ this.database = new UnitDatabase();
+ 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);
+ }
+
+ // set default settings temporarily
+ this.loadSettings(DEFAULT_SETTINGS_FILEPATH);
+
+ // a Predicate that returns true iff the argument is a full base unit
+ 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",
+ this.database.unitMapPrefixless(false).size(),
+ this.database.unitMapPrefixless(true).size(),
+ this.database.unitMapPrefixless(false).values().stream()
+ .filter(isFullBase).count());
+ }
+
+ /**
+ * Converts from the view's input expression to its output expression.
+ * Displays an error message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * expression-based conversion (does
+ * not implement
+ * {@link ExpressionConversionView})
+ * @since 2021-12-15
+ */
+ public void convertExpressions() {
+ if (this.view instanceof ExpressionConversionView) {
+ final ExpressionConversionView xcview = (ExpressionConversionView) this.view;
+
+ final String fromExpression = xcview.getFromExpression();
+ final String toExpression = xcview.getToExpression();
+
+ // expressions must not be empty
+ if (fromExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the From: box.");
+ return;
+ }
+ if (toExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the To: box.");
+ return;
+ }
+
+ // evaluate expressions
+ final LinearUnitValue from;
+ final Unit to;
+ try {
+ from = this.database.evaluateUnitExpression(fromExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in From entry: " + e.getMessage());
+ return;
+ }
+ try {
+ to = this.database.getUnitFromExpression(toExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in To entry: " + e.getMessage());
+ return;
+ }
+
+ // convert and show output
+ if (from.getUnit().canConvertTo(to)) {
+ final UncertainDouble uncertainValue;
+
+ // uncertainty is meaningless for non-linear units, so we will have
+ // to erase uncertainty information for them
+ if (to instanceof LinearUnit) {
+ final var toLinear = (LinearUnit) to;
+ uncertainValue = from.convertTo(toLinear).getValue();
+ } else {
+ final double value = from.asUnitValue().convertTo(to).getValue();
+ uncertainValue = UncertainDouble.of(value, 0);
+ }
+
+ final UnitConversionRecord uc = UnitConversionRecord.valueOf(
+ fromExpression, toExpression, "",
+ this.numberDisplayRule.apply(uncertainValue));
+ xcview.showExpressionConversionOutput(uc);
+ } else {
+ this.view.showErrorMessage("Conversion Error",
+ "Cannot convert between \"" + fromExpression + "\" and \""
+ + toExpression + "\".");
+ }
+
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is an ExpressionConversionView");
+ }
+
+ /**
+ * Converts from the view's input unit to its output unit. Displays an error
+ * message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * unit-based conversion (does not
+ * implement
+ * {@link UnitConversionView})
+ * @since 2021-12-15
+ */
+ public void convertUnits() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+
+ final Optional<String> fromUnitOptional = ucview.getFromSelection();
+ final Optional<String> toUnitOptional = ucview.getToSelection();
+ final String inputValueString = ucview.getInputValue();
+
+ // extract values from optionals
+ final String fromUnitString, toUnitString;
+ if (fromUnitOptional.isPresent()) {
+ fromUnitString = fromUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a From unit");
+ return;
+ }
+ if (toUnitOptional.isPresent()) {
+ toUnitString = toUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a To unit");
+ return;
+ }
+
+ // convert strings to data, checking if anything is invalid
+ final Unit fromUnit, toUnit;
+ final UncertainDouble uncertainValue;
+
+ if (this.database.containsUnitName(fromUnitString)) {
+ fromUnit = this.database.getUnit(fromUnitString);
+ } else
+ throw this.viewError("Nonexistent From unit: %s", fromUnitString);
+ if (this.database.containsUnitName(toUnitString)) {
+ toUnit = this.database.getUnit(toUnitString);
+ } else
+ throw this.viewError("Nonexistent To unit: %s", toUnitString);
+ try {
+ uncertainValue = UncertainDouble
+ .fromRoundedString(inputValueString);
+ } catch (final NumberFormatException e) {
+ this.view.showErrorMessage("Value Error",
+ "Invalid value " + inputValueString);
+ return;
+ }
+
+ if (!fromUnit.canConvertTo(toUnit))
+ throw this.viewError("Could not convert between %s and %s",
+ fromUnit, toUnit);
+
+ // convert - we will need to erase uncertainty for non-linear units, so
+ // we need to treat linear and non-linear units differently
+ final String outputValueString;
+ if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) {
+ final LinearUnit fromLinear = (LinearUnit) fromUnit;
+ final LinearUnit toLinear = (LinearUnit) toUnit;
+ final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear,
+ uncertainValue);
+ final LinearUnitValue converted = initialValue.convertTo(toLinear);
+
+ outputValueString = this.numberDisplayRule
+ .apply(converted.getValue());
+ } else {
+ final UnitValue initialValue = UnitValue.of(fromUnit,
+ uncertainValue.value());
+ final UnitValue converted = initialValue.convertTo(toUnit);
+
+ outputValueString = this.numberDisplayRule
+ .apply(UncertainDouble.of(converted.getValue(), 0));
+ }
+
+ ucview.showUnitConversionOutput(
+ UnitConversionRecord.valueOf(fromUnitString, toUnitString,
+ inputValueString, outputValueString));
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is a UnitConversionView.");
+ }
+
+ /**
+ * @return true iff duplicate units are shown in unit lists
+ * @since 2022-03-30
+ */
+ public boolean duplicatesShown() {
+ return this.showDuplicates;
+ }
+
+ /**
+ * Gets a name for this dimension using the database
+ *
+ * @param dimension dimension to name
+ * @return name of dimension
+ * @since 2022-04-16
+ */
+ final String getDimensionName(ObjectProduct<BaseDimension> dimension) {
+ // find this dimension in the database and get its name
+ // if it isn't there, use the dimension's toString instead
+ return this.database.dimensionMap().values().stream()
+ .filter(d -> d.equals(dimension)).findAny().map(Nameable::getName)
+ .orElse(dimension.toString(Nameable::getName));
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert numbers into
+ * strings
+ * @since 2022-04-10
+ */
+ public Function<UncertainDouble, String> getNumberDisplayRule() {
+ return this.numberDisplayRule;
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert strings into
+ * numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private Function<String, UncertainDouble> getNumberParsingRule() {
+ return this.numberParsingRule;
+ }
+
+ /**
+ * @return the rule that determines whether a set of prefixes is valid
+ * @since 2022-04-19
+ */
+ public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
+ return this.prefixRepetitionRule;
+ }
+
+ /**
+ * @return the view associated with this presenter
+ * @since 2022-04-19
+ */
+ public View getView() {
+ return this.view;
+ }
+
+ /**
+ * @return whether or not the provided unit is semi-metric (i.e. an
+ * exception)
+ * @since 2022-04-16
+ */
+ final boolean isSemiMetric(Unit u) {
+ // determine if u is an exception
+ final var primaryName = u.getPrimaryName();
+ final var symbol = u.getSymbol();
+ return primaryName.isPresent()
+ && this.metricExceptions.contains(primaryName.orElseThrow())
+ || symbol.isPresent()
+ && this.metricExceptions.contains(symbol.orElseThrow())
+ || sharesAnyElements(this.metricExceptions, u.getOtherNames());
+ }
+
+ /**
+ * Loads settings from the user's settings file and applies them to the
+ * presenter.
+ *
+ * @param settingsFile file settings should be loaded from
+ * @since 2021-12-15
+ */
+ void loadSettings(Path settingsFile) {
+ try {
+ // read file line by line
+ final int lineNum = 0;
+ for (final String line : Files.readAllLines(settingsFile)) {
+ 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 "number_display_rule":
+ this.numberDisplayRule = StandardDisplayRules
+ .getStandardRule(value);
+ break;
+ case "prefix_rule":
+ this.prefixRepetitionRule = DefaultPrefixRepetitionRule
+ .valueOf(value);
+ this.database.setPrefixRepetitionRule(this.prefixRepetitionRule);
+ break;
+ case "one_way":
+ this.oneWayConversionEnabled = Boolean.valueOf(value);
+ break;
+ case "include_duplicates":
+ this.showDuplicates = Boolean.valueOf(value);
+ break;
+ default:
+ System.err.printf("Warning: unrecognized setting \"%s\".%n",
+ param);
+ break;
+ }
+ }
+ if (this.view.getPresenter() != null) {
+ this.updateView();
+ }
+ } catch (final IOException e) {}
+ }
+
+ /**
+ * @return true iff the One-Way Conversion feature is available (views that
+ * show units as a list will have metric units removed from the From
+ * unit list and imperial/USC units removed from the To unit list)
+ *
+ * @since 2022-03-30
+ */
+ public boolean oneWayConversionEnabled() {
+ return this.oneWayConversionEnabled;
+ }
+
+ /**
+ * Completes creation of the presenter. This part of the initialization
+ * depends on the view's functions, so it cannot be run if the components
+ * they depend on are not created yet.
+ *
+ * @since 2022-02-26
+ */
+ public void postViewInitialize() {
+ // unit conversion specific stuff
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ ucview.setDimensionNames(this.database.dimensionMap().keySet());
+ }
+
+ this.updateView();
+ }
+
+ void prefixSelected() {
+ final Optional<String> selectedPrefixName = this.view
+ .getViewedPrefixName();
+ final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
+ .map(name -> this.database.containsPrefixName(name)
+ ? this.database.getPrefix(name)
+ : null);
+ selectedPrefix
+ .ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(),
+ String.valueOf(prefix.getMultiplier())));
+ }
+
+ /**
+ * Saves the presenter's current settings to its default filepath.
+ *
+ * @since 2022-04-19
+ */
+ public void saveSettings() {
+ this.saveSettings(DEFAULT_SETTINGS_FILEPATH);
+ }
+
+ /**
+ * Saves the presenter's settings to the user settings file.
+ *
+ * @param settingsFile file settings should be saved to
+ * @since 2021-12-15
+ */
+ void saveSettings(Path settingsFile) {
+ try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) {
+ writer.write(String.format("number_display_rule=%s\n",
+ this.numberDisplayRule));
+ writer.write(
+ String.format("prefix_rule=%s\n", this.prefixRepetitionRule));
+ writer.write(
+ String.format("one_way=%s\n", this.oneWayConversionEnabled));
+ writer.write(
+ String.format("include_duplicates=%s\n", this.showDuplicates));
+ } catch (final IOException e) {
+ e.printStackTrace();
+ this.view.showErrorMessage("I/O Error",
+ "Error occurred while saving settings: "
+ + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * @param numberDisplayRule the new rule that will be used by this presenter
+ * to convert numbers into strings
+ * @since 2022-04-10
+ */
+ public void setNumberDisplayRule(
+ Function<UncertainDouble, String> numberDisplayRule) {
+ this.numberDisplayRule = numberDisplayRule;
+ }
+
+ /**
+ * @param numberParsingRule the new rule that will be used by this presenter
+ * to convert strings into numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private void setNumberParsingRule(
+ Function<String, UncertainDouble> numberParsingRule) {
+ this.numberParsingRule = numberParsingRule;
+ }
+
+ /**
+ * @param oneWayConversionEnabled whether not one-way conversion should be
+ * enabled
+ * @since 2022-03-30
+ * @see {@link #isOneWayConversionEnabled}
+ */
+ public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) {
+ this.oneWayConversionEnabled = oneWayConversionEnabled;
+ this.updateView();
+ }
+
+ /**
+ * @param prefixRepetitionRule the rule that determines whether a set of
+ * prefixes is valid
+ * @since 2022-04-19
+ */
+ public void setPrefixRepetitionRule(
+ Predicate<List<UnitPrefix>> prefixRepetitionRule) {
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ this.database.setPrefixRepetitionRule(prefixRepetitionRule);
+ }
+
+ /**
+ * @param showDuplicateUnits whether or not duplicate units should be shown
+ * @since 2022-03-30
+ */
+ public void setShowDuplicates(boolean showDuplicateUnits) {
+ this.showDuplicates = showDuplicateUnits;
+ this.updateView();
+ }
+
+ /**
+ * Shows a unit in the unit viewer
+ *
+ * @param u unit to show
+ * @since 2022-04-16
+ */
+ private final void showUnit(Unit u) {
+ final var nameSymbol = u.getNameSymbol();
+ final boolean isBase = u instanceof BaseUnit
+ || u instanceof LinearUnit && ((LinearUnit) u).isBase();
+ final var definition = isBase ? "(Base unit)" : u.toDefinitionString();
+ final var dimensionString = this.getDimensionName(u.getDimension());
+ final var unitType = UnitType.getType(u, this::isSemiMetric);
+ this.view.showUnit(nameSymbol, definition, dimensionString, unitType);
+ }
+
+ /**
+ * Runs whenever a unit name is selected in the unit viewer. Gets the
+ * description of a unit and displays it.
+ *
+ * @since 2022-04-10
+ */
+ void unitNameSelected() {
+ // get selected unit, if it's there and valid
+ final Optional<String> selectedUnitName = this.view.getViewedUnitName();
+ final Optional<Unit> selectedUnit = selectedUnitName
+ .map(unitName -> this.database.containsUnitName(unitName)
+ ? this.database.getUnit(unitName)
+ : null);
+ selectedUnit.ifPresent(this::showUnit);
+ }
+
+ /**
+ * Updates the view's From and To units, if it has some
+ *
+ * @since 2021-12-15
+ */
+ public void updateView() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var selectedDimensionName = ucview.getSelectedDimensionName();
+
+ // load units & prefixes into viewers
+ this.view.setViewableUnitNames(
+ this.database.unitMapPrefixless(this.showDuplicates).keySet());
+ this.view.setViewablePrefixNames(
+ this.database.prefixMap(this.showDuplicates).keySet());
+
+ // get From and To units
+ var fromUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+ var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+
+ // filter by dimension, if one is selected
+ if (selectedDimensionName.isPresent()) {
+ final var viewDimension = this.database
+ .getDimension(selectedDimensionName.orElseThrow());
+ fromUnits = fromUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ toUnits = toUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ }
+
+ // filter by unit type, if desired
+ if (this.oneWayConversionEnabled) {
+ fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.METRIC);
+ toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.NON_METRIC);
+ }
+
+ // set unit names
+ ucview.setFromUnitNames(
+ fromUnits.map(Map.Entry::getKey).collect(Collectors.toSet()));
+ ucview.setToUnitNames(
+ toUnits.map(Map.Entry::getKey).collect(Collectors.toSet()));
+ }
+ }
+
+ /**
+ * @param message message to add
+ * @param args string formatting arguments for message
+ * @return AssertionError stating that an error has happened in the view's
+ * code
+ * @since 2022-04-09
+ */
+ private AssertionError viewError(String message, Object... args) {
+ return new AssertionError("View Programming Error (from " + this.view
+ + "): " + String.format(message, args));
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java
new file mode 100644
index 0000000..9b41601
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+/**
+ * @param <E> type of element in list
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+final class SearchBoxList<E> 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.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final String EMPTY_TEXT = "Search...";
+
+ /**
+ * The color to use for an empty foreground.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192);
+
+ // the components
+ private final Collection<E> itemsToFilter;
+ private final DelegateListModel<E> listModel;
+ private final JTextField searchBox;
+ private final JList<E> 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
+ // event.
+ private boolean searchBoxFocused = false;
+
+ private Predicate<E> customSearchFilter = o -> true;
+ private final Comparator<E> defaultOrdering;
+ private final boolean caseSensitive;
+
+ /**
+ * Creates an empty SearchBoxList
+ *
+ * @since 2022-02-19
+ */
+ public SearchBoxList() {
+ this(List.of(), null, false);
+ }
+
+ /**
+ * Creates the {@code SearchBoxList}.
+ *
+ * @param itemsToFilter items to put in the list
+ * @since 2019-04-14
+ */
+ public SearchBoxList(final Collection<E> 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
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public SearchBoxList(final Collection<E> itemsToFilter,
+ final Comparator<E> defaultOrdering, final boolean caseSensitive) {
+ super(new BorderLayout(), true);
+ this.itemsToFilter = new ArrayList<>(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.
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void addSearchFilter(final Predicate<E> filter) {
+ this.customSearchFilter = this.customSearchFilter.and(filter);
+ }
+
+ /**
+ * Resets the search filter.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void clearSearchFilters() {
+ this.customSearchFilter = o -> true;
+ }
+
+ /**
+ * @return items available in search list, including items that are hidden by
+ * the search filter
+ * @since 2022-03-30
+ */
+ public Collection<E> getItems() {
+ return Collections.unmodifiableCollection(this.itemsToFilter);
+ }
+
+ /**
+ * @return this component's search box component
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ 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
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private Predicate<E> getSearchFilter(final String searchText) {
+ if (this.caseSensitive)
+ return item -> item.toString().contains(searchText);
+ else
+ return item -> item.toString().toLowerCase()
+ .contains(searchText.toLowerCase());
+ }
+
+ /**
+ * @return this component's list component
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final JList<E> getSearchList() {
+ return this.searchItems;
+ }
+
+ /**
+ * @return index selected in item list, -1 if no selection
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public int getSelectedIndex() {
+ return this.searchItems.getSelectedIndex();
+ }
+
+ /**
+ * @return value selected in item list
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Optional<E> getSelectedValue() {
+ return Optional.ofNullable(this.searchItems.getSelectedValue());
+ }
+
+ /**
+ * Re-applies the filters.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void reapplyFilter() {
+ final String searchText = this.searchBoxEmpty ? ""
+ : this.searchBox.getText();
+ final FilterComparator<E> comparator = new FilterComparator<>(searchText,
+ this.defaultOrdering, this.caseSensitive);
+ final Predicate<E> searchFilter = this.getSearchFilter(searchText);
+
+ this.listModel.clear();
+ this.itemsToFilter.forEach(item -> {
+ if (searchFilter.test(item)) {
+ this.listModel.add(item);
+ }
+ });
+
+ // 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
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private void searchBoxFocusGained(final FocusEvent e) {
+ this.searchBoxFocused = true;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText("");
+ this.searchBox.setForeground(Color.BLACK);
+ }
+ }
+
+ /**
+ * Runs whenever the search box loses focus.
+ *
+ * @param e focus event
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private void searchBoxFocusLost(final FocusEvent e) {
+ this.searchBoxFocused = false;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+ }
+ }
+
+ /**
+ * Runs whenever the text in the search box is changed.
+ * <p>
+ * Reapplies the search filter, and custom filters.
+ * </p>
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private void searchBoxTextChanged() {
+ if (this.searchBoxFocused) {
+ this.searchBoxEmpty = this.searchBox.getText().equals("");
+ }
+ final String searchText = this.searchBoxEmpty ? ""
+ : this.searchBox.getText();
+ final FilterComparator<E> comparator = new FilterComparator<>(searchText,
+ this.defaultOrdering, this.caseSensitive);
+ final Predicate<E> searchFilter = this.getSearchFilter(searchText);
+
+ // initialize list with items that match the filter then sort
+ 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);
+ }
+
+ /**
+ * Resets the search box list's contents to the provided items, removing any
+ * old items
+ *
+ * @param newItems new items to put in list
+ * @since 2021-05-22
+ */
+ public void setItems(Collection<? extends E> newItems) {
+ this.itemsToFilter.clear();
+ this.itemsToFilter.addAll(newItems);
+ this.reapplyFilter();
+ }
+
+ /**
+ * Manually updates the search box's item list.
+ *
+ * @since 2020-08-27
+ */
+ public void updateList() {
+ this.searchBoxTextChanged();
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
new file mode 100644
index 0000000..0c0ba8e
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * A static utility class that can be used to make display rules for the
+ * presenter.
+ *
+ * @since 2022-04-18
+ */
+public final class StandardDisplayRules {
+ /**
+ * A rule that rounds to a fixed number of decimal places.
+ *
+ * @since 2022-04-18
+ */
+ public static final class FixedDecimals
+ implements Function<UncertainDouble, String> {
+ public static final Pattern TO_STRING_PATTERN = Pattern
+ .compile("Round to (\\d+) decimal places");
+ /**
+ * The number of places to round to.
+ */
+ private final int decimalPlaces;
+
+ /**
+ * @param decimalPlaces
+ * @since 2022-04-18
+ */
+ private FixedDecimals(int decimalPlaces) {
+ this.decimalPlaces = decimalPlaces;
+ }
+
+ @Override
+ public String apply(UncertainDouble t) {
+ final var toRound = new BigDecimal(t.value());
+ return toRound.setScale(this.decimalPlaces, RoundingMode.HALF_EVEN)
+ .toPlainString();
+ }
+
+ /**
+ * @return the number of decimal places this rule rounds to
+ * @since 2022-04-18
+ */
+ public int decimalPlaces() {
+ return this.decimalPlaces;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FixedDecimals))
+ return false;
+ final FixedDecimals other = (FixedDecimals) obj;
+ if (this.decimalPlaces != other.decimalPlaces)
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 + this.decimalPlaces;
+ }
+
+ @Override
+ public String toString() {
+ return "Round to " + this.decimalPlaces + " decimal places";
+ }
+ }
+
+ /**
+ * A rule that rounds to a fixed number of significant digits.
+ *
+ * @since 2022-04-18
+ */
+ public static final class FixedPrecision
+ implements Function<UncertainDouble, String> {
+ public static final Pattern TO_STRING_PATTERN = Pattern
+ .compile("Round to (\\d+) significant figures");
+
+ /**
+ * The number of significant figures to round to.
+ */
+ private final MathContext mathContext;
+
+ /**
+ * @param significantFigures
+ * @since 2022-04-18
+ */
+ private FixedPrecision(int significantFigures) {
+ this.mathContext = new MathContext(significantFigures,
+ RoundingMode.HALF_EVEN);
+ }
+
+ @Override
+ public String apply(UncertainDouble t) {
+ final var toRound = new BigDecimal(t.value());
+ return toRound.round(this.mathContext).toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FixedPrecision))
+ return false;
+ final FixedPrecision other = (FixedPrecision) obj;
+ if (this.mathContext == null) {
+ if (other.mathContext != null)
+ return false;
+ } else if (!this.mathContext.equals(other.mathContext))
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 127
+ + (this.mathContext == null ? 0 : this.mathContext.hashCode());
+ }
+
+ /**
+ * @return the number of significant figures this rule rounds to
+ * @since 2022-04-18
+ */
+ public int significantFigures() {
+ return this.mathContext.getPrecision();
+ }
+
+ @Override
+ public String toString() {
+ return "Round to " + this.mathContext.getPrecision()
+ + " significant figures";
+ }
+ }
+
+ /**
+ * A rounding rule that rounds based on UncertainDouble's toString method.
+ * This means the output will have around as many significant figures as the
+ * input.
+ *
+ * @since 2022-04-18
+ */
+ public static final class UncertaintyBased
+ implements Function<UncertainDouble, String> {
+ private UncertaintyBased() {}
+
+ @Override
+ public String apply(UncertainDouble t) {
+ return t.toString(false, RoundingMode.HALF_EVEN);
+ }
+
+ @Override
+ public String toString() {
+ return "Uncertainty-Based Rounding";
+ }
+ }
+
+ /**
+ * For now, I want this to be a singleton. I might want to add a parameter
+ * later, so I won't make it an enum.
+ */
+ private static final UncertaintyBased UNCERTAINTY_BASED_ROUNDING_RULE = new UncertaintyBased();
+
+ /**
+ * @param decimalPlaces decimal places to round to
+ * @return a rounding rule that rounds to fixed number of decimal places
+ * @since 2022-04-18
+ */
+ public static final FixedDecimals fixedDecimals(int decimalPlaces) {
+ return new FixedDecimals(decimalPlaces);
+ }
+
+ /**
+ * @param significantFigures significant figures to round to
+ * @return a rounding rule that rounds to a fixed number of significant
+ * figures
+ * @since 2022-04-18
+ */
+ public static final FixedPrecision fixedPrecision(int significantFigures) {
+ return new FixedPrecision(significantFigures);
+ }
+
+ /**
+ * Gets one of the standard rules from its string representation.
+ *
+ * @param ruleToString string representation of the display rule
+ * @return display rule
+ * @throws IllegalArgumentException if the provided string is not that of a
+ * standard rule.
+ * @since 2021-12-24
+ */
+ public static final Function<UncertainDouble, String> getStandardRule(
+ String ruleToString) {
+ if (UNCERTAINTY_BASED_ROUNDING_RULE.toString().equals(ruleToString))
+ return UNCERTAINTY_BASED_ROUNDING_RULE;
+
+ // test if it is a fixed-places rule
+ final var placesMatch = FixedDecimals.TO_STRING_PATTERN
+ .matcher(ruleToString);
+ if (placesMatch.matches())
+ return new FixedDecimals(Integer.valueOf(placesMatch.group(1)));
+
+ // test if it is a fixed-sig-fig rule
+ final var sigFigMatch = FixedPrecision.TO_STRING_PATTERN
+ .matcher(ruleToString);
+ if (sigFigMatch.matches())
+ return new FixedPrecision(Integer.valueOf(sigFigMatch.group(1)));
+
+ throw new IllegalArgumentException(
+ "Provided string does not match any given rules.");
+ }
+
+ /**
+ * @return an UncertainDouble-based rounding rule
+ * @since 2022-04-18
+ */
+ public static final UncertaintyBased uncertaintyBased() {
+ return UNCERTAINTY_BASED_ROUNDING_RULE;
+ }
+
+ private StandardDisplayRules() {}
+}
diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java
new file mode 100644
index 0000000..be80ccb
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/TabbedView.java
@@ -0,0 +1,797 @@
+/**
+ * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.GridLayout;
+import java.awt.event.ItemEvent;
+import java.awt.event.KeyEvent;
+import java.util.AbstractSet;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.Function;
+
+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.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.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
+import javax.swing.WindowConstants;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * A View that separates its functions into multiple tabs
+ *
+ * @since 2022-02-19
+ */
+final class TabbedView implements ExpressionConversionView, UnitConversionView {
+ /**
+ * A Set-like view of a JComboBox's items
+ *
+ * @param <E> type of item in list
+ *
+ * @since 2022-02-19
+ */
+ private static final class JComboBoxItemSet<E> extends AbstractSet<E> {
+ private final JComboBox<E> comboBox;
+
+ /**
+ * @param comboBox combo box to get items from
+ * @since 2022-02-19
+ */
+ public JComboBoxItemSet(JComboBox<E> comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new Iterator<>() {
+ private int index = 0;
+
+ @Override
+ public boolean hasNext() {
+ return this.index < JComboBoxItemSet.this.size();
+ }
+
+ @Override
+ public E next() {
+ if (this.hasNext())
+ return JComboBoxItemSet.this.comboBox.getItemAt(this.index++);
+ else
+ throw new NoSuchElementException(
+ "Iterator has finished iteration");
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return this.comboBox.getItemCount();
+ }
+
+ }
+
+ /**
+ * The standard types of rounding, corresponding to the options on the
+ * TabbedView's settings panel.
+ *
+ * @since 2022-04-18
+ */
+ private static enum StandardRoundingType {
+ /**
+ * Rounds to a fixed number of significant digits. Precision is used,
+ * representing the number of significant digits to round to.
+ */
+ SIGNIFICANT_DIGITS,
+ /**
+ * Rounds to a fixed number of decimal places. Precision is used,
+ * representing the number of decimal places to round to.
+ */
+ DECIMAL_PLACES,
+ /**
+ * Rounds according to UncertainDouble's toString method. The specified
+ * precision is ignored.
+ */
+ UNCERTAINTY;
+ }
+
+ /**
+ * Creates a TabbedView.
+ *
+ * @param args command line arguments
+ * @since 2022-02-19
+ */
+ public static void main(String[] args) {
+ // This view doesn't need to do anything, the side effects of creating it
+ // are enough to start the program
+ @SuppressWarnings("unused")
+ final View view = new TabbedView();
+ }
+
+ /** The Presenter that handles this View */
+ final Presenter presenter;
+ /** The frame that this view lives on */
+ final JFrame frame;
+ /** The tabbed pane that contains all of the components */
+ final JTabbedPane masterPane;
+
+ // DIMENSION-BASED CONVERTER
+ /** The combo box that selects dimensions */
+ private final JComboBox<String> dimensionSelector;
+ /** 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<String> fromSearch;
+ /** The panel for "To" in the dimension-based converter */
+ private final SearchBoxList<String> toSearch;
+ /** The output area in the dimension-based converter */
+ private final JTextArea unitOutput;
+
+ // 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 expressionOutput;
+
+ // UNIT AND PREFIX VIEWERS
+ /** The searchable list of unit names in the unit viewer */
+ private final SearchBoxList<String> unitNameList;
+ /** The searchable list of prefix names in the prefix viewer */
+ private final SearchBoxList<String> 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;
+
+ // SETTINGS STUFF
+ private StandardRoundingType roundingType;
+ private int precision;
+
+ /**
+ * Creates the view and makes it visible to the user
+ *
+ * @since 2022-02-19
+ */
+ public TabbedView() {
+ // 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();
+ }
+
+ // initialize important components
+ this.presenter = new Presenter(this);
+ this.frame = new JFrame("7Units " + ProgramInfo.VERSION);
+ this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+ // master components (those that contain everything else within them)
+ this.masterPane = new JTabbedPane();
+ this.frame.add(this.masterPane);
+
+ // ============ UNIT CONVERSION TAB ============
+ 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));
+ inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6));
+
+ this.fromSearch = new SearchBoxList<>();
+ inputPanel.add(this.fromSearch);
+
+ final JPanel inBetweenPanel = new JPanel();
+ inputPanel.add(inBetweenPanel);
+ inBetweenPanel.setLayout(new BorderLayout());
+
+ this.dimensionSelector = new JComboBox<>();
+ inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START);
+ this.dimensionSelector
+ .addItemListener(e -> this.presenter.updateView());
+
+ final JLabel arrowLabel = new JLabel("-->");
+ inBetweenPanel.add(arrowLabel, BorderLayout.CENTER);
+ arrowLabel.setHorizontalAlignment(SwingConstants.CENTER);
+
+ this.toSearch = new SearchBoxList<>();
+ inputPanel.add(this.toSearch);
+ }
+
+ { // panel for submit and output, and also value entry
+ final JPanel outputPanel = new JPanel();
+ convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END);
+ outputPanel.setLayout(new BorderLayout());
+ outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6));
+
+ final JLabel valuePrompt = new JLabel("Value to convert: ");
+ outputPanel.add(valuePrompt, BorderLayout.LINE_START);
+
+ this.valueInput = new JTextField();
+ outputPanel.add(this.valueInput, BorderLayout.CENTER);
+
+ // conversion button
+ final JButton convertButton = new JButton("Convert");
+ outputPanel.add(convertButton, BorderLayout.LINE_END);
+ convertButton.addActionListener(e -> this.presenter.convertUnits());
+ convertButton.setMnemonic(KeyEvent.VK_ENTER);
+
+ // conversion output
+ this.unitOutput = new JTextArea(2, 32);
+ outputPanel.add(this.unitOutput, BorderLayout.PAGE_END);
+ this.unitOutput.setEditable(false);
+ }
+
+ // ============ EXPRESSION CONVERSION TAB ============
+ 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));
+
+ // from and to expressions
+ this.fromEntry = new JTextField();
+ convertExpressionPanel.add(this.fromEntry);
+ this.fromEntry.setBorder(BorderFactory.createTitledBorder("From"));
+
+ this.toEntry = new JTextField();
+ convertExpressionPanel.add(this.toEntry);
+ this.toEntry.setBorder(BorderFactory.createTitledBorder("To"));
+
+ // 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
+ this.expressionOutput = new JTextArea(2, 32);
+ convertExpressionPanel.add(this.expressionOutput);
+ this.expressionOutput
+ .setBorder(BorderFactory.createTitledBorder("Output"));
+ this.expressionOutput.setEditable(false);
+
+ // =========== UNIT VIEWER ===========
+ final JPanel unitLookupPanel = new JPanel();
+ this.masterPane.addTab("Unit Viewer", unitLookupPanel);
+ this.masterPane.setMnemonicAt(2, KeyEvent.VK_V);
+ unitLookupPanel.setLayout(new GridLayout());
+
+ this.unitNameList = new SearchBoxList<>();
+ unitLookupPanel.add(this.unitNameList);
+ this.unitNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.unitNameSelected());
+
+ // the text box for unit's toString
+ this.unitTextBox = new JTextArea();
+ unitLookupPanel.add(this.unitTextBox);
+ this.unitTextBox.setEditable(false);
+ this.unitTextBox.setLineWrap(true);
+
+ // ============ PREFIX VIEWER =============
+ final JPanel prefixLookupPanel = new JPanel();
+ this.masterPane.addTab("Prefix Viewer", prefixLookupPanel);
+ this.masterPane.setMnemonicAt(3, KeyEvent.VK_P);
+ prefixLookupPanel.setLayout(new GridLayout(1, 2));
+
+ this.prefixNameList = new SearchBoxList<>();
+ prefixLookupPanel.add(this.prefixNameList);
+ this.prefixNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.prefixSelected());
+
+ // the text box for prefix's toString
+ this.prefixTextBox = new JTextArea();
+ 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);
+ infoTextArea.setText(Presenter.getAboutText());
+
+ // ============ SETTINGS PANEL ============
+ this.masterPane.addTab("\u2699",
+ new JScrollPane(this.createSettingsPanel()));
+ this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
+
+ // ============ FINALIZE CREATION OF VIEW ============
+ this.presenter.postViewInitialize();
+ this.frame.pack();
+ this.frame.setVisible(true);
+ }
+
+ /**
+ * Creates and returns the settings panel (in its own function to make this
+ * code more organized, as this function is massive!)
+ *
+ * @since 2022-02-19
+ */
+ private JPanel createSettingsPanel() {
+ final JPanel settingsPanel = new JPanel();
+
+ 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();
+ this.roundingType = this.getPresenterRoundingType()
+ .orElseThrow(() -> new AssertionError(
+ "Presenter loaded non-standard rounding rule"));
+ this.precision = this.getPresenterPrecision().orElse(6);
+
+ final JLabel roundingRuleLabel = new JLabel("Rounding Rule:");
+ roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // sigDigSlider needs to be first so that the rounding-type buttons can
+ // show and hide it
+ final JLabel sliderLabel = new JLabel("Precision:");
+ sliderLabel.setVisible(
+ this.roundingType != StandardRoundingType.UNCERTAINTY);
+ 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.setVisible(
+ this.roundingType != StandardRoundingType.UNCERTAINTY);
+ sigDigSlider.setValue(this.precision);
+
+ sigDigSlider.addChangeListener(e -> {
+ this.precision = sigDigSlider.getValue();
+ this.updatePresenterRoundingRule();
+ });
+
+ // significant digit rounding
+ final JRadioButton fixedPrecision = new JRadioButton(
+ "Fixed Precision");
+ if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) {
+ fixedPrecision.setSelected(true);
+ }
+ fixedPrecision.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.SIGNIFICANT_DIGITS;
+ sliderLabel.setVisible(true);
+ sigDigSlider.setVisible(true);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(fixedPrecision);
+ roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // decimal place rounding
+ final JRadioButton fixedDecimals = new JRadioButton(
+ "Fixed Decimal Places");
+ if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) {
+ fixedDecimals.setSelected(true);
+ }
+ fixedDecimals.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.DECIMAL_PLACES;
+ sliderLabel.setVisible(true);
+ sigDigSlider.setVisible(true);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(fixedDecimals);
+ roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // scientific rounding
+ final JRadioButton relativePrecision = new JRadioButton(
+ "Uncertainty-Based Rounding");
+ if (this.roundingType == StandardRoundingType.UNCERTAINTY) {
+ relativePrecision.setSelected(true);
+ }
+ relativePrecision.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.UNCERTAINTY;
+ sliderLabel.setVisible(false);
+ sigDigSlider.setVisible(false);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(relativePrecision);
+ roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+
+ // ============ PREFIX REPETITION SETTINGS ============
+ {
+ final JPanel prefixRepetitionPanel = new JPanel();
+ settingsPanel.add(prefixRepetitionPanel);
+ prefixRepetitionPanel
+ .setBorder(new TitledBorder("Prefix Repetition Settings"));
+ prefixRepetitionPanel.setLayout(new GridBagLayout());
+
+ final var prefixRule = this.getPresenterPrefixRule()
+ .orElseThrow(() -> new AssertionError(
+ "Presenter loaded non-standard prefix rule"));
+
+ // prefix rules
+ final ButtonGroup prefixRuleButtons = new ButtonGroup();
+
+ final JRadioButton noRepetition = new JRadioButton("No Repetition");
+ if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
+ noRepetition.setSelected(true);
+ }
+ noRepetition.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_REPETITION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(noRepetition);
+ prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton noRestriction = new JRadioButton("No Restriction");
+ if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
+ noRestriction.setSelected(true);
+ }
+ noRestriction.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_RESTRICTION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(noRestriction);
+ prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton customRepetition = new JRadioButton(
+ "Complex Repetition");
+ if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
+ customRepetition.setSelected(true);
+ }
+ customRepetition.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.COMPLEX_REPETITION);
+ this.presenter.saveSettings();
+ });
+ 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());
+ }
+
+ // ============ OTHER SETTINGS ============
+ {
+ final JPanel miscPanel = new JPanel();
+ settingsPanel.add(miscPanel);
+ miscPanel.setLayout(new GridBagLayout());
+
+ final JCheckBox oneWay = new JCheckBox("Convert One Way Only");
+ oneWay.setSelected(this.presenter.oneWayConversionEnabled());
+ oneWay.addItemListener(e -> {
+ this.presenter.setOneWayConversionEnabled(
+ e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(oneWay, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JCheckBox showAllVariations = new JCheckBox(
+ "Show Duplicate Units & Prefixes");
+ showAllVariations.setSelected(this.presenter.duplicatesShown());
+ showAllVariations.addItemListener(e -> {
+ this.presenter
+ .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ 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());
+ }
+
+ return settingsPanel;
+ }
+
+ @Override
+ public Set<String> getDimensionNames() {
+ return Collections
+ .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector));
+ }
+
+ @Override
+ public String getFromExpression() {
+ return this.fromEntry.getText();
+ }
+
+ @Override
+ public Optional<String> getFromSelection() {
+ return this.fromSearch.getSelectedValue();
+ }
+
+ @Override
+ public Set<String> getFromUnitNames() {
+ // this should work because the only way I can mutate the item list is
+ // with setFromUnits which only accepts a Set
+ return new HashSet<>(this.fromSearch.getItems());
+ }
+
+ @Override
+ public String getInputValue() {
+ return this.valueInput.getText();
+ }
+
+ @Override
+ public Presenter getPresenter() {
+ return this.presenter;
+ }
+
+ /**
+ * @return the precision of the presenter's rounding rule, if that is
+ * meaningful
+ * @since 2022-04-18
+ */
+ private OptionalInt getPresenterPrecision() {
+ final var presenterRule = this.presenter.getNumberDisplayRule();
+ if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ return OptionalInt
+ .of(((StandardDisplayRules.FixedDecimals) presenterRule)
+ .decimalPlaces());
+ else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ return OptionalInt
+ .of(((StandardDisplayRules.FixedPrecision) presenterRule)
+ .significantFigures());
+ else
+ return OptionalInt.empty();
+ }
+
+ /**
+ * @return presenter's prefix repetition rule
+ * @since 2022-04-19
+ */
+ private Optional<DefaultPrefixRepetitionRule> getPresenterPrefixRule() {
+ final var prefixRule = this.presenter.getPrefixRepetitionRule();
+ return prefixRule instanceof DefaultPrefixRepetitionRule
+ ? Optional.of((DefaultPrefixRepetitionRule) prefixRule)
+ : Optional.empty();
+ }
+
+ /**
+ * Determines which rounding type the presenter is currently using, if any.
+ *
+ * @since 2022-04-18
+ */
+ private Optional<StandardRoundingType> getPresenterRoundingType() {
+ final var presenterRule = this.presenter.getNumberDisplayRule();
+ if (Objects.equals(presenterRule,
+ StandardDisplayRules.uncertaintyBased()))
+ return Optional.of(StandardRoundingType.UNCERTAINTY);
+ else if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ return Optional.of(StandardRoundingType.DECIMAL_PLACES);
+ else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ return Optional.of(StandardRoundingType.SIGNIFICANT_DIGITS);
+ else
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<String> getSelectedDimensionName() {
+ final String selectedItem = (String) this.dimensionSelector
+ .getSelectedItem();
+ return Optional.ofNullable(selectedItem);
+ }
+
+ @Override
+ public String getToExpression() {
+ return this.toEntry.getText();
+ }
+
+ @Override
+ public Optional<String> getToSelection() {
+ return this.toSearch.getSelectedValue();
+ }
+
+ @Override
+ public Set<String> getToUnitNames() {
+ // this should work because the only way I can mutate the item list is
+ // with setToUnits which only accepts a Set
+ return new HashSet<>(this.toSearch.getItems());
+ }
+
+ @Override
+ public Optional<String> getViewedPrefixName() {
+ return this.prefixNameList.getSelectedValue();
+ }
+
+ @Override
+ public Optional<String> getViewedUnitName() {
+ return this.unitNameList.getSelectedValue();
+ }
+
+ @Override
+ public void setDimensionNames(Set<String> dimensionNames) {
+ this.dimensionSelector.removeAllItems();
+ for (final String d : dimensionNames) {
+ this.dimensionSelector.addItem(d);
+ }
+ }
+
+ @Override
+ public void setFromUnitNames(Set<String> units) {
+ this.fromSearch.setItems(units);
+ }
+
+ @Override
+ public void setToUnitNames(Set<String> units) {
+ this.toSearch.setItems(units);
+ }
+
+ @Override
+ public void setViewablePrefixNames(Set<String> prefixNames) {
+ this.prefixNameList.setItems(prefixNames);
+ }
+
+ @Override
+ public void setViewableUnitNames(Set<String> unitNames) {
+ this.unitNameList.setItems(unitNames);
+ }
+
+ @Override
+ public void showErrorMessage(String title, String message) {
+ JOptionPane.showMessageDialog(this.frame, message, title,
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ @Override
+ public void showExpressionConversionOutput(UnitConversionRecord uc) {
+ this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(),
+ uc.outputValueString(), uc.toName()));
+ }
+
+ @Override
+ public void showPrefix(NameSymbol name, String multiplierString) {
+ this.prefixTextBox.setText(
+ String.format("%s%nMultiplier: %s", name, multiplierString));
+ }
+
+ @Override
+ public void showUnit(NameSymbol name, String definition,
+ String dimensionName, UnitType type) {
+ this.unitTextBox.setText(
+ String.format("%s%nDefinition: %s%nDimension: %s%nType: %s", name,
+ definition, dimensionName, type));
+ }
+
+ @Override
+ public void showUnitConversionOutput(UnitConversionRecord uc) {
+ this.unitOutput.setText(uc.toString());
+ }
+
+ /**
+ * Sets the presenter's rounding rule to the one specified by the current
+ * settings
+ *
+ * @since 2022-04-18
+ */
+ private void updatePresenterRoundingRule() {
+ final Function<UncertainDouble, String> roundingRule;
+ switch (this.roundingType) {
+ case DECIMAL_PLACES:
+ roundingRule = StandardDisplayRules.fixedDecimals(this.precision);
+ break;
+ case SIGNIFICANT_DIGITS:
+ roundingRule = StandardDisplayRules.fixedPrecision(this.precision);
+ break;
+ case UNCERTAINTY:
+ roundingRule = StandardDisplayRules.uncertaintyBased();
+ break;
+ default:
+ throw new AssertionError();
+ }
+ this.presenter.setNumberDisplayRule(roundingRule);
+ this.presenter.saveSettings();
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
new file mode 100644
index 0000000..f951f44
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.math.RoundingMode;
+
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.UnitValue;
+
+/**
+ * A record of a conversion between units or expressions
+ *
+ * @since 2022-04-09
+ */
+public final class UnitConversionRecord {
+ /**
+ * Gets a {@code UnitConversionRecord} from two linear unit values
+ *
+ * @param input input unit & value
+ * @param output output unit & value
+ * @return unit conversion record
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord fromLinearValues(LinearUnitValue input,
+ LinearUnitValue output) {
+ return UnitConversionRecord.valueOf(input.getUnit().getName(),
+ output.getUnit().getName(),
+ input.getValue().toString(false, RoundingMode.HALF_EVEN),
+ output.getValue().toString(false, RoundingMode.HALF_EVEN));
+ }
+
+ /**
+ * Gets a {@code UnitConversionRecord} from two unit values
+ *
+ * @param input input unit & value
+ * @param output output unit & value
+ * @return unit conversion record
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord fromValues(UnitValue input,
+ UnitValue output) {
+ return UnitConversionRecord.valueOf(input.getUnit().getName(),
+ output.getUnit().getName(), String.valueOf(input.getValue()),
+ String.valueOf(output.getValue()));
+ }
+
+ /**
+ * Gets a {@code UnitConversionRecord}
+ *
+ * @param fromName name of unit or expression that was converted
+ * from
+ * @param toName name of unit or expression that was converted to
+ * @param inputValueString string representing input value
+ * @param outputValueString string representing output value
+ * @return unit conversion record
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord valueOf(String fromName, String toName,
+ String inputValueString, String outputValueString) {
+ return new UnitConversionRecord(fromName, toName, inputValueString,
+ outputValueString);
+ }
+
+ /**
+ * The name of the unit or expression that was converted from
+ */
+ private final String fromName;
+ /**
+ * The name of the unit or expression that was converted to
+ */
+ private final String toName;
+
+ /**
+ * A string representing the input value. It doesn't need to be the same as
+ * the input value's string representation; it could be rounded, for example.
+ */
+ private final String inputValueString;
+ /**
+ * A string representing the input value. It doesn't need to be the same as
+ * the input value's string representation; it could be rounded, for example.
+ */
+ private final String outputValueString;
+
+ /**
+ * @param fromName name of unit or expression that was converted
+ * from
+ * @param toName name of unit or expression that was converted to
+ * @param inputValueString string representing input value
+ * @param outputValueString string representing output value
+ * @since 2022-04-09
+ */
+ private UnitConversionRecord(String fromName, String toName,
+ String inputValueString, String outputValueString) {
+ this.fromName = fromName;
+ this.toName = toName;
+ this.inputValueString = inputValueString;
+ this.outputValueString = outputValueString;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitConversionRecord))
+ return false;
+ final UnitConversionRecord other = (UnitConversionRecord) obj;
+ if (this.fromName == null) {
+ if (other.fromName != null)
+ return false;
+ } else if (!this.fromName.equals(other.fromName))
+ return false;
+ if (this.inputValueString == null) {
+ if (other.inputValueString != null)
+ return false;
+ } else if (!this.inputValueString.equals(other.inputValueString))
+ return false;
+ if (this.outputValueString == null) {
+ if (other.outputValueString != null)
+ return false;
+ } else if (!this.outputValueString.equals(other.outputValueString))
+ return false;
+ if (this.toName == null) {
+ if (other.toName != null)
+ return false;
+ } else if (!this.toName.equals(other.toName))
+ return false;
+ return true;
+ }
+
+ /**
+ * @return name of unit or expression that was converted from
+ * @since 2022-04-09
+ */
+ public String fromName() {
+ return this.fromName;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.fromName == null ? 0 : this.fromName.hashCode());
+ result = prime * result + (this.inputValueString == null ? 0
+ : this.inputValueString.hashCode());
+ result = prime * result + (this.outputValueString == null ? 0
+ : this.outputValueString.hashCode());
+ result = prime * result
+ + (this.toName == null ? 0 : this.toName.hashCode());
+ return result;
+ }
+
+ /**
+ * @return string representing input value
+ * @since 2022-04-09
+ */
+ public String inputValueString() {
+ return this.inputValueString;
+ }
+
+ /**
+ * @return string representing output value
+ * @since 2022-04-09
+ */
+ public String outputValueString() {
+ return this.outputValueString;
+ }
+
+ /**
+ * @return name of unit or expression that was converted to
+ * @since 2022-04-09
+ */
+ public String toName() {
+ return this.toName;
+ }
+
+ @Override
+ public String toString() {
+ final String inputString = this.inputValueString.isBlank() ? this.fromName
+ : this.inputValueString + " " + this.fromName;
+ final String outputString = this.outputValueString.isBlank() ? this.toName
+ : this.outputValueString + " " + this.toName;
+ return inputString + " = " + outputString;
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java
new file mode 100644
index 0000000..6a95aa5
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (C) 2021-2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A View that supports single unit-based conversion
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public interface UnitConversionView extends View {
+ /**
+ * @return dimensions available for filtering
+ * @since 2022-01-29
+ */
+ Set<String> getDimensionNames();
+
+ /**
+ * @return name of unit to convert <em>from</em>
+ * @since 2021-12-15
+ */
+ Optional<String> getFromSelection();
+
+ /**
+ * @return list of names of units available to convert from
+ * @since 2022-03-30
+ */
+ Set<String> getFromUnitNames();
+
+ /**
+ * @return value to convert between the units (specifically, the numeric
+ * string provided by the user)
+ * @since 2021-12-15
+ */
+ String getInputValue();
+
+ /**
+ * @return selected dimension
+ * @since 2021-12-15
+ */
+ Optional<String> getSelectedDimensionName();
+
+ /**
+ * @return name of unit to convert <em>to</em>
+ * @since 2021-12-15
+ */
+ Optional<String> getToSelection();
+
+ /**
+ * @return list of names of units available to convert to
+ * @since 2022-03-30
+ */
+ Set<String> getToUnitNames();
+
+ /**
+ * Sets the available dimensions for filtering.
+ *
+ * @param dimensionNames names of dimensions to use
+ * @since 2021-12-15
+ */
+ void setDimensionNames(Set<String> dimensionNames);
+
+ /**
+ * Sets the available units to convert from. {@link #getFromSelection} is not
+ * required to use one of these units; this method is to be used for views
+ * that allow the user to select units from a list.
+ *
+ * @param unitNames names of units to convert from
+ * @since 2021-12-15
+ */
+ void setFromUnitNames(Set<String> unitNames);
+
+ /**
+ * Sets the available units to convert to. {@link #getToSelection} is not
+ * required to use one of these units; this method is to be used for views
+ * that allow the user to select units from a list.
+ *
+ * @param unitNames names of units to convert to
+ * @since 2021-12-15
+ */
+ void setToUnitNames(Set<String> unitNames);
+
+ /**
+ * Shows the output of a unit conversion.
+ *
+ * @param input input unit & value (obtained from this view)
+ * @param output output unit & value
+ * @since 2021-12-24
+ */
+ void showUnitConversionOutput(UnitConversionRecord uc);
+}
diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java
new file mode 100644
index 0000000..b2d2b94
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/View.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (C) 2021-2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Optional;
+import java.util.Set;
+
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+
+/**
+ * An object that controls user interaction with 7Units
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public interface View {
+ /**
+ * @return a new tabbed view
+ * @since 2022-04-19
+ */
+ static View createTabbedView() {
+ return new TabbedView();
+ }
+
+ /**
+ * @return the presenter associated with this view
+ * @since 2022-04-19
+ */
+ Presenter getPresenter();
+
+ /**
+ * @return name of prefix currently being viewed
+ * @since 2022-04-10
+ */
+ Optional<String> getViewedPrefixName();
+
+ /**
+ * @return name of unit currently being viewed
+ * @since 2022-04-10
+ */
+ Optional<String> getViewedUnitName();
+
+ /**
+ * Sets the list of prefixes that are available to be viewed in a prefix
+ * viewer
+ *
+ * @param prefixNames prefix names to view
+ * @since 2022-04-10
+ */
+ void setViewablePrefixNames(Set<String> prefixNames);
+
+ /**
+ * Sets the list of units that are available to be viewed in a unit viewer
+ *
+ * @param unitNames unit names to view
+ * @since 2022-04-10
+ */
+ void setViewableUnitNames(Set<String> unitNames);
+
+ /**
+ * Shows an error message.
+ *
+ * @param title title of error message; on any view that uses an error
+ * dialog, this should be the title of the error dialog.
+ * @param message error message
+ * @since 2021-12-15
+ */
+ void showErrorMessage(String title, String message);
+
+ /**
+ * Shows information about a prefix to the user.
+ *
+ * @param name name(s) and symbol of prefix
+ * @param multiplierString string representation of prefix multiplier
+ * @since 2022-04-10
+ */
+ void showPrefix(NameSymbol name, String multiplierString);
+
+ /**
+ * Shows information about a unit to the user.
+ *
+ * @param name name(s) and symbol of unit
+ * @param definition unit's definition string
+ * @param dimensionName name of unit's dimension
+ * @param type type of unit (metric/semi-metric/non-metric)
+ * @since 2022-04-10
+ */
+ void showUnit(NameSymbol name, String definition, String dimensionName,
+ UnitType type);
+}
diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java
new file mode 100644
index 0000000..a3ba7a2
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/ViewBot.java
@@ -0,0 +1,507 @@
+/**
+ * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+
+/**
+ * A class that simulates a View (supports both unit and expression conversion)
+ * for testing. Getters and setters work as expected.
+ *
+ * @author Adrien Hopkins
+ * @since 2022-01-29
+ */
+public final class ViewBot
+ implements UnitConversionView, ExpressionConversionView {
+ /**
+ * A record of the parameters given to
+ * {@link View#showPrefix(NameSymbol, String)}, for testing.
+ *
+ * @since 2022-04-16
+ */
+ public static final class PrefixViewingRecord implements Nameable {
+ private final NameSymbol nameSymbol;
+ private final String multiplierString;
+
+ /**
+ * @param nameSymbol
+ * @param multiplierString
+ * @since 2022-04-16
+ */
+ public PrefixViewingRecord(NameSymbol nameSymbol,
+ String multiplierString) {
+ this.nameSymbol = nameSymbol;
+ this.multiplierString = multiplierString;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof PrefixViewingRecord))
+ return false;
+ final PrefixViewingRecord other = (PrefixViewingRecord) obj;
+ return Objects.equals(this.multiplierString, other.multiplierString)
+ && Objects.equals(this.nameSymbol, other.nameSymbol);
+ }
+
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.multiplierString, this.nameSymbol);
+ }
+
+ public String multiplierString() {
+ return this.multiplierString;
+ }
+
+ public NameSymbol nameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("PrefixViewingRecord [nameSymbol=");
+ builder.append(this.nameSymbol);
+ builder.append(", multiplierString=");
+ builder.append(this.multiplierString);
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ /**
+ * A record of the parameters given to
+ * {@link View#showUnit(NameSymbol, String, String, UnitType)}, for testing.
+ *
+ * @since 2022-04-16
+ */
+ public static final class UnitViewingRecord implements Nameable {
+ private final NameSymbol nameSymbol;
+ private final String definition;
+ private final String dimensionName;
+ private final UnitType unitType;
+
+ /**
+ * @since 2022-04-16
+ */
+ public UnitViewingRecord(NameSymbol nameSymbol, String definition,
+ String dimensionName, UnitType unitType) {
+ this.nameSymbol = nameSymbol;
+ this.definition = definition;
+ this.dimensionName = dimensionName;
+ this.unitType = unitType;
+ }
+
+ /**
+ * @return the definition
+ * @since 2022-04-16
+ */
+ public String definition() {
+ return this.definition;
+ }
+
+ /**
+ * @return the dimensionName
+ * @since 2022-04-16
+ */
+ public String dimensionName() {
+ return this.dimensionName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitViewingRecord))
+ return false;
+ final UnitViewingRecord other = (UnitViewingRecord) obj;
+ return Objects.equals(this.definition, other.definition)
+ && Objects.equals(this.dimensionName, other.dimensionName)
+ && Objects.equals(this.nameSymbol, other.nameSymbol)
+ && this.unitType == other.unitType;
+ }
+
+ /**
+ * @return the nameSymbol
+ * @since 2022-04-16
+ */
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.definition, this.dimensionName,
+ this.nameSymbol, this.unitType);
+ }
+
+ public NameSymbol nameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("UnitViewingRecord [nameSymbol=");
+ builder.append(this.nameSymbol);
+ builder.append(", definition=");
+ builder.append(this.definition);
+ builder.append(", dimensionName=");
+ builder.append(this.dimensionName);
+ builder.append(", unitType=");
+ builder.append(this.unitType);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /**
+ * @return the unitType
+ * @since 2022-04-16
+ */
+ public UnitType unitType() {
+ return this.unitType;
+ }
+ }
+
+ /** The presenter that works with this ViewBot */
+ private final Presenter presenter;
+
+ /** The dimensions available to select from */
+ private Set<String> dimensionNames = Set.of();
+ /** The expression in the From field */
+ private String fromExpression = "";
+ /** The expression in the To field */
+ private String toExpression = "";
+ /**
+ * The user-provided string representing the value in {@code fromSelection}
+ */
+ private String inputValue = "";
+ /** The unit selected in the From selection */
+ private Optional<String> fromSelection = Optional.empty();
+ /** The unit selected in the To selection */
+ private Optional<String> toSelection = Optional.empty();
+ /** The currently selected dimension */
+ private Optional<String> selectedDimensionName = Optional.empty();
+ /** The units available in the From selection */
+ private Set<String> fromUnits = Set.of();
+ /** The units available in the To selection */
+ private Set<String> toUnits = Set.of();
+
+ /** The selected unit in the unit viewer */
+ private Optional<String> unitViewerSelection = Optional.empty();
+ /** The selected unit in the prefix viewer */
+ private Optional<String> prefixViewerSelection = Optional.empty();
+
+ /** Saved outputs of all unit conversions */
+ private final List<UnitConversionRecord> unitConversions;
+ /** Saved outputs of all unit expressions */
+ private final List<UnitConversionRecord> expressionConversions;
+ /** Saved outputs of all unit viewings */
+ private final List<UnitViewingRecord> unitViewingRecords;
+ /** Saved outputs of all prefix viewings */
+ private final List<PrefixViewingRecord> prefixViewingRecords;
+
+ /**
+ * Creates a new {@code ViewBot} with a new presenter.
+ *
+ * @since 2022-01-29
+ */
+ public ViewBot() {
+ this.presenter = new Presenter(this);
+
+ this.unitConversions = new ArrayList<>();
+ this.expressionConversions = new ArrayList<>();
+ this.unitViewingRecords = new ArrayList<>();
+ this.prefixViewingRecords = new ArrayList<>();
+ }
+
+ /**
+ * @return list of records of expression conversions done by this bot
+ * @since 2022-04-09
+ */
+ public List<UnitConversionRecord> expressionConversionList() {
+ return Collections.unmodifiableList(this.expressionConversions);
+ }
+
+ /**
+ * @return the available dimensions
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getDimensionNames() {
+ return this.dimensionNames;
+ }
+
+ @Override
+ public String getFromExpression() {
+ return this.fromExpression;
+ }
+
+ @Override
+ public Optional<String> getFromSelection() {
+ return this.fromSelection;
+ }
+
+ /**
+ * @return the units available for selection in From
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getFromUnitNames() {
+ return Collections.unmodifiableSet(this.fromUnits);
+ }
+
+ @Override
+ public String getInputValue() {
+ return this.inputValue;
+ }
+
+ /**
+ * @return the presenter associated with tihs view
+ * @since 2022-01-29
+ */
+ @Override
+ public Presenter getPresenter() {
+ return this.presenter;
+ }
+
+ @Override
+ public Optional<String> getSelectedDimensionName() {
+ return this.selectedDimensionName;
+ }
+
+ @Override
+ public String getToExpression() {
+ return this.toExpression;
+ }
+
+ @Override
+ public Optional<String> getToSelection() {
+ return this.toSelection;
+ }
+
+ /**
+ * @return the units available for selection in To
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getToUnitNames() {
+ return Collections.unmodifiableSet(this.toUnits);
+ }
+
+ @Override
+ public Optional<String> getViewedPrefixName() {
+ return this.prefixViewerSelection;
+ }
+
+ @Override
+ public Optional<String> getViewedUnitName() {
+ return this.unitViewerSelection;
+ }
+
+ /**
+ * @return list of records of this viewBot's prefix views
+ * @since 2022-04-16
+ */
+ public List<PrefixViewingRecord> prefixViewList() {
+ return Collections.unmodifiableList(this.prefixViewingRecords);
+ }
+
+ @Override
+ public void setDimensionNames(Set<String> dimensionNames) {
+ this.dimensionNames = Objects.requireNonNull(dimensionNames,
+ "dimensions may not be null");
+ }
+
+ /**
+ * Sets the From expression (as in {@link #getFromExpression}).
+ *
+ * @param fromExpression the expression to convert from
+ * @throws NullPointerException if {@code fromExpression} is null
+ * @since 2022-01-29
+ */
+ public void setFromExpression(String fromExpression) {
+ this.fromExpression = Objects.requireNonNull(fromExpression,
+ "fromExpression cannot be null.");
+ }
+
+ /**
+ * @param fromSelection the fromSelection to set
+ * @since 2022-01-29
+ */
+ public void setFromSelection(Optional<String> fromSelection) {
+ this.fromSelection = Objects.requireNonNull(fromSelection,
+ "fromSelection cannot be null");
+ }
+
+ /**
+ * @param fromSelection the fromSelection to set
+ * @since 2022-02-10
+ */
+ public void setFromSelection(String fromSelection) {
+ this.setFromSelection(Optional.of(fromSelection));
+ }
+
+ @Override
+ public void setFromUnitNames(Set<String> units) {
+ this.fromUnits = Objects.requireNonNull(units, "units may not be null");
+ }
+
+ /**
+ * @param inputValue the inputValue to set
+ * @since 2022-01-29
+ */
+ public void setInputValue(String inputValue) {
+ this.inputValue = inputValue;
+ }
+
+ /**
+ * @param selectedDimension the selectedDimension to set
+ * @since 2022-01-29
+ */
+ public void setSelectedDimensionName(
+ Optional<String> selectedDimensionName) {
+ this.selectedDimensionName = selectedDimensionName;
+ }
+
+ public void setSelectedDimensionName(String selectedDimensionName) {
+ this.setSelectedDimensionName(Optional.of(selectedDimensionName));
+ }
+
+ /**
+ * Sets the To expression (as in {@link #getToExpression}).
+ *
+ * @param toExpression the expression to convert to
+ * @throws NullPointerException if {@code toExpression} is null
+ * @since 2022-01-29
+ */
+ public void setToExpression(String toExpression) {
+ this.toExpression = Objects.requireNonNull(toExpression,
+ "toExpression cannot be null.");
+ }
+
+ /**
+ * @param toSelection the toSelection to set
+ * @since 2022-01-29
+ */
+ public void setToSelection(Optional<String> toSelection) {
+ this.toSelection = Objects.requireNonNull(toSelection,
+ "toSelection cannot be null.");
+ }
+
+ public void setToSelection(String toSelection) {
+ this.setToSelection(Optional.of(toSelection));
+ }
+
+ @Override
+ public void setToUnitNames(Set<String> units) {
+ this.toUnits = Objects.requireNonNull(units, "units may not be null");
+ }
+
+ @Override
+ public void setViewablePrefixNames(Set<String> prefixNames) {
+ // do nothing, ViewBot supports selecting any prefix
+ }
+
+ @Override
+ public void setViewableUnitNames(Set<String> unitNames) {
+ // do nothing, ViewBot supports selecting any unit
+ }
+
+ public void setViewedPrefixName(Optional<String> viewedPrefixName) {
+ this.prefixViewerSelection = viewedPrefixName;
+ }
+
+ public void setViewedPrefixName(String viewedPrefixName) {
+ this.setViewedPrefixName(Optional.of(viewedPrefixName));
+ }
+
+ public void setViewedUnitName(Optional<String> viewedUnitName) {
+ this.unitViewerSelection = viewedUnitName;
+ }
+
+ public void setViewedUnitName(String viewedUnitName) {
+ this.setViewedUnitName(Optional.of(viewedUnitName));
+ }
+
+ @Override
+ public void showErrorMessage(String title, String message) {
+ System.err.printf("%s: %s%n", title, message);
+ }
+
+ @Override
+ public void showExpressionConversionOutput(UnitConversionRecord uc) {
+ this.expressionConversions.add(uc);
+ System.out.println("Expression Conversion: " + uc);
+ }
+
+ @Override
+ public void showPrefix(NameSymbol name, String multiplierString) {
+ this.prefixViewingRecords
+ .add(new PrefixViewingRecord(name, multiplierString));
+ }
+
+ @Override
+ public void showUnit(NameSymbol name, String definition,
+ String dimensionName, UnitType type) {
+ this.unitViewingRecords
+ .add(new UnitViewingRecord(name, definition, dimensionName, type));
+ }
+
+ @Override
+ public void showUnitConversionOutput(UnitConversionRecord uc) {
+ this.unitConversions.add(uc);
+ System.out.println("Unit Conversion: " + uc);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + String.format("[presenter=%s]", this.presenter);
+ }
+
+ /**
+ * @return list of records of every unit conversion made by this bot
+ * @since 2022-04-09
+ */
+ public List<UnitConversionRecord> unitConversionList() {
+ return Collections.unmodifiableList(this.unitConversions);
+ }
+
+ /**
+ * @return list of records of unit viewings made by this bot
+ * @since 2022-04-16
+ */
+ public List<UnitViewingRecord> unitViewList() {
+ return Collections.unmodifiableList(this.unitViewingRecords);
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/package-info.java b/src/main/java/sevenUnitsGUI/package-info.java
new file mode 100644
index 0000000..cff1ded
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/package-info.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (C) 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 <https://www.gnu.org/licenses/>.
+ */
+/**
+ * The MVP GUI of SevenUnits
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+package sevenUnitsGUI; \ No newline at end of file