diff options
author | Adrien Hopkins <ahopk127@my.yorku.ca> | 2022-07-17 16:26:49 -0500 |
---|---|---|
committer | Adrien Hopkins <ahopk127@my.yorku.ca> | 2022-07-17 16:26:49 -0500 |
commit | a5f088ae43c285bc3708303bdcc99bd8936477d2 (patch) | |
tree | 8d3ac45478468fe772618aa6d44c4879152738b5 /src/main/java/sevenUnitsGUI | |
parent | cc79db65bc347c50267d0a719278ef1d90cf6b1a (diff) | |
parent | b76c06eb393c7c6d9a3ece66efec1fd20311b7e8 (diff) |
Merge branch 'release-0.4.0' into stable
Diffstat (limited to 'src/main/java/sevenUnitsGUI')
-rw-r--r-- | src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java | 95 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/DelegateListModel.java | 242 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/ExpressionConversionView.java | 49 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/FilterComparator.java | 136 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/GridBagBuilder.java | 479 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/Main.java | 38 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/PrefixSearchRule.java | 171 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/Presenter.java | 851 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/SearchBoxList.java | 341 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/StandardDisplayRules.java | 254 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/TabbedView.java | 831 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/UnitConversionRecord.java | 207 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/UnitConversionView.java | 120 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/View.java | 115 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/ViewBot.java | 508 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/package-info.java | 23 |
16 files changed, 4460 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..5c39788 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java @@ -0,0 +1,49 @@ +/** + * 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 v0.4.0 + * @since 2021-12-15 + */ +public interface ExpressionConversionView extends View { + /** + * @return unit expression to convert <em>from</em> + * @since v0.4.0 + * @since 2021-12-15 + */ + String getFromExpression(); + + /** + * @return unit expression to convert <em>to</em> + * @since v0.4.0 + * @since 2021-12-15 + */ + String getToExpression(); + + /** + * Shows the output of an expression conversion to the user. + * + * @param uc unit conversion to show + * @since v0.4.0 + * @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..c0a67e8 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/FilterComparator.java @@ -0,0 +1,136 @@ +/** + * 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) { + Objects.requireNonNull(filter, "filter must not be null."); + this.filter = caseSensitive ? filter : filter.toLowerCase(); + this.comparator = comparator; + this.caseSensitive = caseSensitive; + } + + /** + * Compares two objects according to whether or not they match a filter. + * Objects whose string representation starts with the filter's text go + * first, then those that contain it but don't start with it, then those that + * don't contain it. Objects in the same order here are sorted by their + * string representation's compareTo or the provided comparator. + */ + @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..998b373 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/Main.java @@ -0,0 +1,38 @@ +/** + * 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 v0.4.0 + * @since 2022-04-19 + */ +public final class Main { + + /** + * The main method that starts 7Units + * + * @param args commandline arguments + * @since v0.4.0 + * @since 2022-04-19 + */ + public static void main(String[] args) { + View.createTabbedView(); + } + +} diff --git a/src/main/java/sevenUnitsGUI/PrefixSearchRule.java b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java new file mode 100644 index 0000000..a5034c9 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/PrefixSearchRule.java @@ -0,0 +1,171 @@ +/** + * 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.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import sevenUnits.unit.LinearUnit; +import sevenUnits.unit.Metric; +import sevenUnits.unit.UnitPrefix; + +/** + * A search rule that applies a certain set of prefixes to a unit. It always + * includes the original unit in the output map. + * + * @since v0.4.0 + * @since 2022-07-06 + */ +public final class PrefixSearchRule implements + Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> { + /** + * A rule that does not add any prefixed versions of units. + * + * @since v0.4.0 + */ + public static final PrefixSearchRule NO_PREFIXES = getUniversalRule( + Set.of()); + + /** + * A rule that gives every unit a common set of prefixes. + * + * @since v0.4.0 + */ + public static final PrefixSearchRule COMMON_PREFIXES = getCoherentOnlyRule( + Set.of(Metric.MILLI, Metric.KILO)); + + /** + * A rule that gives every unit all metric prefixes. + * + * @since v0.4.0 + */ + public static final PrefixSearchRule ALL_METRIC_PREFIXES = getCoherentOnlyRule( + Metric.ALL_PREFIXES); + + /** + * Gets a rule that applies the provided prefixes to coherent units only (as + * defined by {@link LinearUnit#isCoherent}), except the kilogram + * (specifically, units named "kilogram"). + * + * @param prefixes prefixes to apply + * @return prefix rule + * @since v0.4.0 + * @since 2022-07-06 + */ + public static final PrefixSearchRule getCoherentOnlyRule( + Set<UnitPrefix> prefixes) { + return new PrefixSearchRule(prefixes, + u -> u.isCoherent() && !u.getName().equals("kilogram")); + } + + /** + * Gets a rule that applies the provided prefixes to all units. + * + * @param prefixes prefixes to apply + * @return prefix rule + * @since v0.4.0 + * @since 2022-07-06 + */ + public static final PrefixSearchRule getUniversalRule( + Set<UnitPrefix> prefixes) { + return new PrefixSearchRule(prefixes, u -> true); + } + + /** + * The set of prefixes that will be applied to the unit. + */ + private final Set<UnitPrefix> prefixes; + + /** + * Determines which units are given prefixes. + */ + private final Predicate<LinearUnit> prefixableUnitRule; + + /** + * @param prefixes prefixes to add to units + * @param prefixableUnitRule function that determines which units get + * prefixes + * @since v0.4.0 + * @since 2022-07-06 + */ + public PrefixSearchRule(Set<UnitPrefix> prefixes, + Predicate<LinearUnit> prefixableUnitRule) { + this.prefixes = Collections.unmodifiableSet(new HashSet<>(prefixes)); + this.prefixableUnitRule = prefixableUnitRule; + } + + @Override + public Map<String, LinearUnit> apply(Entry<String, LinearUnit> t) { + final Map<String, LinearUnit> outputUnits = new HashMap<>(); + final String originalName = t.getKey(); + final LinearUnit originalUnit = t.getValue(); + outputUnits.put(originalName, originalUnit); + if (this.prefixableUnitRule.test(originalUnit)) { + for (final UnitPrefix prefix : this.prefixes) { + outputUnits.put(prefix.getName() + originalName, + originalUnit.withPrefix(prefix)); + } + } + return Collections.unmodifiableMap(outputUnits); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof PrefixSearchRule)) + return false; + final PrefixSearchRule other = (PrefixSearchRule) obj; + return Objects.equals(this.prefixableUnitRule, other.prefixableUnitRule) + && Objects.equals(this.prefixes, other.prefixes); + } + + /** + * @return rule that determines which units get prefixes + * @since v0.4.0 + * @since 2022-07-09 + */ + public Predicate<LinearUnit> getPrefixableUnitRule() { + return this.prefixableUnitRule; + } + + /** + * @return the prefixes that are applied by this rule + * @since v0.4.0 + * @since 2022-07-06 + */ + public Set<UnitPrefix> getPrefixes() { + return this.prefixes; + } + + @Override + public int hashCode() { + return Objects.hash(this.prefixableUnitRule, this.prefixes); + } + + @Override + public String toString() { + return "Apply the following prefixes: " + this.prefixes; + } +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java new file mode 100644 index 0000000..abdd1f6 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -0,0 +1,851 @@ +/** + * 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 java.util.stream.Stream; + +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; + + /** + * A rule that accepts a prefixless name-unit pair and returns a map mapping + * names to prefixed versions of that unit (including the unit itself) that + * should be searchable. + */ + private Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule = PrefixSearchRule.NO_PREFIXES; + + /** + * 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()); + } + + /** + * Applies a search rule to an entry in a name-unit map. + * + * @param e entry + * @return stream of entries, ready for flat-mapping + * @since 2022-07-06 + */ + private final Stream<Map.Entry<String, Unit>> applySearchRule( + Map.Entry<String, Unit> e) { + final Unit u = e.getValue(); + if (u instanceof LinearUnit) { + final String name = e.getKey(); + final Map.Entry<String, LinearUnit> linearEntry = Map.entry(name, + (LinearUnit) u); + return this.searchRule.apply(linearEntry).entrySet().stream().map( + entry -> Map.entry(entry.getKey(), (Unit) entry.getValue())); + } else + return Stream.of(e); + } + + /** + * 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 rule that determines which units are prefixed + * @since 2022-07-08 + */ + public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getSearchRule() { + return this.searchRule; + } + + /** + * @return a search rule that shows all single prefixes + * @since 2022-07-08 + */ + public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getUniversalSearchRule() { + return PrefixSearchRule.getCoherentOnlyRule( + new HashSet<>(this.database.prefixMap(true).values())); + } + + /** + * @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; + case "search_prefix_rule": + if (PrefixSearchRule.NO_PREFIXES.toString().equals(value)) { + this.searchRule = PrefixSearchRule.NO_PREFIXES; + } else if (PrefixSearchRule.COMMON_PREFIXES.toString() + .equals(value)) { + this.searchRule = PrefixSearchRule.COMMON_PREFIXES; + } else { + this.searchRule = this.getUniversalSearchRule(); + } + 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)); + writer.write( + String.format("search_prefix_rule=%s\n", this.searchRule)); + } 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 searchRule A rule that accepts a prefixless name-unit pair and + * returns a map mapping names to prefixed versions of that + * unit (including the unit itself) that should be + * searchable. + * @since 2022-07-08 + */ + public void setSearchRule( + Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) { + this.searchRule = searchRule; + } + + /** + * @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.flatMap(this::applySearchRule) + .map(Map.Entry::getKey).collect(Collectors.toSet())); + ucview.setToUnitNames(toUnits.flatMap(this::applySearchRule) + .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..cc69d31 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -0,0 +1,254 @@ +/** + * 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 v0.4.0 + * @since 2022-04-18 + */ +public final class StandardDisplayRules { + /** + * A rule that rounds to a fixed number of decimal places. + * + * @since v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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..6181eae --- /dev/null +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -0,0 +1,831 @@ +/** + * 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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 */ + final JComboBox<String> dimensionSelector; + /** The panel for inputting values in the dimension-based converter */ + final JTextField valueInput; + /** The panel for "From" in the dimension-based converter */ + final SearchBoxList<String> fromSearch; + /** The panel for "To" in the dimension-based converter */ + final SearchBoxList<String> toSearch; + /** The button used for conversion */ + final JButton convertUnitButton; + /** The output area in the dimension-based converter */ + final JTextArea unitOutput; + + // EXPRESSION-BASED CONVERTER + /** The "From" entry in the conversion panel */ + final JTextField fromEntry; + /** The "To" entry in the conversion panel */ + final JTextField toEntry; + /** The button used for conversion */ + final JButton convertExpressionButton; + /** The output area in the conversion panel */ + 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 v0.4.0 + * @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 + this.convertUnitButton = new JButton("Convert"); + outputPanel.add(this.convertUnitButton, BorderLayout.LINE_END); + this.convertUnitButton + .addActionListener(e -> this.presenter.convertUnits()); + this.convertUnitButton.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 + this.convertExpressionButton = new JButton("Convert"); + convertExpressionPanel.add(this.convertExpressionButton); + + this.convertExpressionButton + .addActionListener(e -> this.presenter.convertExpressions()); + this.convertExpressionButton.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 var searchRule = this.presenter.getSearchRule(); + + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); + noPrefixes.addActionListener(e -> { + this.presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES); + this.presenter.updateView(); + this.presenter.saveSettings(); + }); + searchRuleButtons.add(noPrefixes); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton commonPrefixes = new JRadioButton( + "Include Common Prefixes"); + commonPrefixes.addActionListener(e -> { + this.presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES); + this.presenter.updateView(); + this.presenter.saveSettings(); + }); + searchRuleButtons.add(commonPrefixes); + searchingPanel.add(commonPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); + alwaysInclude.addActionListener(e -> { + this.presenter + .setSearchRule(this.presenter.getUniversalSearchRule()); + this.presenter.updateView(); + this.presenter.saveSettings(); + }); + searchRuleButtons.add(alwaysInclude); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + + if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) { + noPrefixes.setSelected(true); + } else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) { + commonPrefixes.setSelected(true); + } else { + alwaysInclude.setSelected(true); + this.presenter + .setSearchRule(this.presenter.getUniversalSearchRule()); + this.presenter.saveSettings(); + } + } + + // ============ 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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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..fa64ee9 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -0,0 +1,207 @@ +/** + * 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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @since 2022-04-09 + */ + public String inputValueString() { + return this.inputValueString; + } + + /** + * @return string representing output value + * @since v0.4.0 + * @since 2022-04-09 + */ + public String outputValueString() { + return this.outputValueString; + } + + /** + * @return name of unit or expression that was converted to + * @since v0.4.0 + * @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..0d07823 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -0,0 +1,120 @@ +/** + * 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 v0.4.0 + * @since 2021-12-15 + */ +public interface UnitConversionView extends View { + /** + * @return dimensions available for filtering + * @since v0.4.0 + * @since 2022-01-29 + */ + Set<String> getDimensionNames(); + + /** + * @return name of unit to convert <em>from</em> + * @since v0.4.0 + * @since 2021-12-15 + */ + Optional<String> getFromSelection(); + + /** + * @return list of names of units available to convert from + * @since v0.4.0 + * @since 2022-03-30 + */ + Set<String> getFromUnitNames(); + + /** + * @return value to convert between the units (specifically, the numeric + * string provided by the user) + * @since v0.4.0 + * @since 2021-12-15 + */ + String getInputValue(); + + /** + * @return selected dimension + * @since v0.4.0 + * @since 2021-12-15 + */ + Optional<String> getSelectedDimensionName(); + + /** + * @return name of unit to convert <em>to</em> + * @since v0.4.0 + * @since 2021-12-15 + */ + Optional<String> getToSelection(); + + /** + * @return list of names of units available to convert to + * @since v0.4.0 + * @since 2022-03-30 + */ + Set<String> getToUnitNames(); + + /** + * Sets the available dimensions for filtering. + * + * @param dimensionNames names of dimensions to use + * @since v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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..bb810ec --- /dev/null +++ b/src/main/java/sevenUnitsGUI/View.java @@ -0,0 +1,115 @@ +/** + * 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 v0.4.0 + * @since 2021-12-15 + */ +public interface View { + /** + * @return a new tabbed view + * @since v0.4.0 + * @since 2022-04-19 + */ + static View createTabbedView() { + return new TabbedView(); + } + + /** + * @return the presenter associated with this view + * @since v0.4.0 + * @since 2022-04-19 + */ + Presenter getPresenter(); + + /** + * @return name of prefix currently being viewed + * @since v0.4.0 + * @since 2022-04-10 + */ + Optional<String> getViewedPrefixName(); + + /** + * @return name of unit currently being viewed + * @since v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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 v0.4.0 + * @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..e7304c4 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -0,0 +1,508 @@ +/** + * 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 v0.4.0 + * @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 |