diff options
author | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-06-28 17:17:32 -0500 |
---|---|---|
committer | Adrien Hopkins <ahopk127@my.yorku.ca> | 2021-06-28 17:17:32 -0500 |
commit | 236b497f516694929b9fe409508a7cce21ca9e6e (patch) | |
tree | 14d2a49900d706070882cfe150e08ec1882cdbc2 /src/main/java/sevenUnits/converterGUI | |
parent | a34d79383061ba53951f3f69a44f142820e82216 (diff) | |
parent | 78af49e0e5b2ab2eaab87e62c33089c5caa834f8 (diff) |
Merge branch 'new-name' into develop
Diffstat (limited to 'src/main/java/sevenUnits/converterGUI')
8 files changed, 2864 insertions, 0 deletions
diff --git a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..835651e --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,95 @@ +/** + * @since 2020-08-26 + */ +package sevenUnits.converterGUI; + +import java.util.List; +import java.util.function.Predicate; + +import sevenUnits.unit.SI; +import sevenUnits.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { + NO_REPETITION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List<UnitPrefix> prefixes) { + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; + } + }; +} diff --git a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java b/src/main/java/sevenUnits/converterGUI/DelegateListModel.java new file mode 100644 index 0000000..dd8cc97 --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/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 sevenUnits.converterGUI; + +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/sevenUnits/converterGUI/FilterComparator.java b/src/main/java/sevenUnits/converterGUI/FilterComparator.java new file mode 100644 index 0000000..edd00e2 --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/FilterComparator.java @@ -0,0 +1,129 @@ +/** + * 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 sevenUnits.converterGUI; + +import java.util.Comparator; +import java.util.Objects; + +/** + * A comparator that compares strings using a filter. + * + * @author Adrien Hopkins + * @since 2019-01-15 + * @since v0.1.0 + */ +final class FilterComparator implements Comparator<String> { + /** + * 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<String> 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<String> 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<String> comparator, final boolean caseSensitive) { + this.filter = Objects.requireNonNull(filter, "filter must not be null."); + this.comparator = comparator; + this.caseSensitive = caseSensitive; + } + + @Override + public int compare(final String arg0, final String arg1) { + // if this is case insensitive, make them lowercase + final String str0, str1; + if (this.caseSensitive) { + str0 = arg0; + str1 = arg1; + } else { + str0 = arg0.toLowerCase(); + str1 = arg1.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(str0, str1); + } +} diff --git a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java b/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java new file mode 100644 index 0000000..0b71d78 --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/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 sevenUnits.converterGUI; + +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/sevenUnits/converterGUI/MutablePredicate.java b/src/main/java/sevenUnits/converterGUI/MutablePredicate.java new file mode 100644 index 0000000..ae6b7a1 --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/MutablePredicate.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.converterGUI; + +import java.util.function.Predicate; + +/** + * A container for a predicate, which can be changed later. + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class MutablePredicate<T> implements Predicate<T> { + /** + * The predicate stored in this {@code MutablePredicate} + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private Predicate<T> predicate; + + /** + * Creates the {@code MutablePredicate}. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public MutablePredicate(final Predicate<T> predicate) { + this.predicate = predicate; + } + + /** + * @return predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final Predicate<T> getPredicate() { + return this.predicate; + } + + /** + * @param predicate + * new value of predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void setPredicate(final Predicate<T> predicate) { + this.predicate = predicate; + } + + @Override + public boolean test(final T t) { + return this.predicate.test(t); + } +} diff --git a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java b/src/main/java/sevenUnits/converterGUI/SearchBoxList.java new file mode 100644 index 0000000..2aa9fce --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/SearchBoxList.java @@ -0,0 +1,320 @@ +/** + * 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 sevenUnits.converterGUI; + +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.Comparator; +import java.util.function.Predicate; + +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; + +/** + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class SearchBoxList extends JPanel { + + /** + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final long serialVersionUID = 6226930279415983433L; + + /** + * The text to place in an empty search box. + * + * @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<String> itemsToFilter; + private final DelegateListModel<String> listModel; + private final JTextField searchBox; + private final JList<String> searchItems; + + private boolean searchBoxEmpty = true; + + // I need to do this because, for some reason, Swing is auto-focusing my + // search box without triggering a focus + // event. + private boolean searchBoxFocused = false; + + private Predicate<String> customSearchFilter = o -> true; + private final Comparator<String> defaultOrdering; + private final boolean caseSensitive; + + /** + * Creates the {@code SearchBoxList}. + * + * @param itemsToFilter items to put in the list + * @since 2019-04-14 + */ + public SearchBoxList(final Collection<String> itemsToFilter) { + this(itemsToFilter, null, false); + } + + /** + * Creates the {@code SearchBoxList}. + * + * @param itemsToFilter items to put in the list + * @param defaultOrdering default ordering of items after filtration + * (null=Comparable) + * @param caseSensitive whether or not the filtration is case-sensitive + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public SearchBoxList(final Collection<String> itemsToFilter, + final Comparator<String> 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<String> 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 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<String> getSearchFilter(final String searchText) { + if (this.caseSensitive) + return string -> string.contains(searchText); + else + return string -> string.toLowerCase() + .contains(searchText.toLowerCase()); + } + + /** + * @return this component's list component + * @since 2019-04-14 + * @since v0.2.0 + */ + public final JList<String> getSearchList() { + return this.searchItems; + } + + /** + * @return index selected in item list + * @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 String getSelectedValue() { + return 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 comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); + final Predicate<String> searchFilter = this.getSearchFilter(searchText); + + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + if (searchFilter.test(string)) { + this.listModel.add(string); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.customSearchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } + + /** + * Runs whenever the search box gains focus. + * + * @param e focus event + * @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 comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); + final Predicate<String> searchFilter = this.getSearchFilter(searchText); + + // initialize list with items that match the filter then sort + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + 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<String> 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/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java new file mode 100644 index 0000000..fb498da --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -0,0 +1,1505 @@ +/** + * Copyright (C) 2018-2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.converterGUI; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.KeyEvent; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFormattedTextField; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.WindowConstants; +import javax.swing.border.TitledBorder; + +import sevenUnits.ProgramInfo; +import sevenUnits.math.ConditionalExistenceCollections; +import sevenUnits.math.ObjectProduct; +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.LinearUnit; +import sevenUnits.unit.LinearUnitValue; +import sevenUnits.unit.NameSymbol; +import sevenUnits.unit.SI; +import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitDatabase; +import sevenUnits.unit.UnitPrefix; +import sevenUnits.unit.UnitValue; + +/** + * @author Adrien Hopkins + * @since 2018-12-27 + * @since v0.1.0 + */ +final class SevenUnitsGUI { + /** + * A tab in the View. + */ + private enum Pane { + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT, + SETTINGS; + } + + private static class Presenter { + /** The default place where settings are stored. */ + private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; + /** The default place where dimensions are stored. */ + private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; + /** The default place where exceptions are stored. */ + private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; + + /** + * Adds default units and dimensions to a database. + * + * @param database database to add to + * @since 2019-04-14 + * @since v0.2.0 + */ + private static void addDefaults(final UnitDatabase database) { + database.addUnit("metre", SI.METRE); + database.addUnit("kilogram", SI.KILOGRAM); + database.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); + database.addUnit("second", SI.SECOND); + database.addUnit("ampere", SI.AMPERE); + database.addUnit("kelvin", SI.KELVIN); + database.addUnit("mole", SI.MOLE); + database.addUnit("candela", SI.CANDELA); + database.addUnit("bit", SI.BIT); + database.addUnit("unit", SI.ONE); + // nonlinear units - must be loaded manually + database.addUnit("tempCelsius", SI.CELSIUS); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); + + // load initial dimensions + database.addDimension("LENGTH", SI.Dimensions.LENGTH); + database.addDimension("MASS", SI.Dimensions.MASS); + database.addDimension("TIME", SI.Dimensions.TIME); + database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); + } + + /** + * Gets the text of a resource file as a set of strings (each one is one + * line of the text). + * + * @param filename filename to get resource from + * @return contents of file + * @since 2021-03-27 + */ + public static final List<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 SevenUnitsGUI.class.getResourceAsStream(filepath); + } + + /** + * @return {@code line} with any comments removed. + * @since 2021-03-13 + */ + private static final String withoutComments(String line) { + final int index = line.indexOf('#'); + return index == -1 ? line : line.substring(index); + } + + /** The presenter's associated view. */ + private final View view; + + /** The units known by the program. */ + private final UnitDatabase database; + + /** The names of all of the units */ + private final List<String> unitNames; + + /** The names of all of the prefixes */ + private final List<String> prefixNames; + + /** The names of all of the dimensions */ + private final List<String> dimensionNames; + + /** Unit names that are ignored by the metric-only/imperial-only filter */ + private final Set<String> metricExceptions; + + private final Comparator<String> prefixNameComparator; + + /** A boolean remembering whether or not one-way conversion is on */ + private boolean oneWay = true; + /** The prefix rule */ + private DefaultPrefixRepetitionRule prefixRule = null; + + // conditions for existence of From and To entries + // used for one-way conversion + private final MutablePredicate<String> fromExistenceCondition = new MutablePredicate<>( + s -> true); + + private final MutablePredicate<String> toExistenceCondition = new MutablePredicate<>( + s -> true); + + /* + * Rounding-related settings. I am using my own system, and not + * MathContext, because MathContext does not support decimal place based + * or scientific rounding, only significant digit based rounding. + */ + private int precision = 6; + + private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; + + // The "include duplicate units" setting + private boolean includeDuplicateUnits = true; + + /** + * Creates the presenter. + * + * @param view presenter's associated view + * @since 2018-12-27 + * @since v0.1.0 + */ + Presenter(final View view) { + this.view = view; + + // load initial units + this.database = new UnitDatabase( + DefaultPrefixRepetitionRule.NO_RESTRICTION); + Presenter.addDefaults(this.database); + + // load units and prefixes + try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { + this.database.loadUnitsFromStream(units); + } catch (final IOException e) { + throw new AssertionError("Loading of unitsfile.txt failed.", e); + } + + // load dimensions + try (final InputStream dimensions = inputStream( + DEFAULT_DIMENSIONS_FILEPATH)) { + this.database.loadDimensionsFromStream(dimensions); + } catch (final IOException e) { + throw new AssertionError("Loading of dimensionfile.txt failed.", e); + } + + // load metric exceptions + try { + this.metricExceptions = new HashSet<>(); + try (InputStream exceptions = inputStream( + DEFAULT_EXCEPTIONS_FILEPATH); + Scanner scanner = new Scanner(exceptions)) { + while (scanner.hasNextLine()) { + final String line = Presenter + .withoutComments(scanner.nextLine()); + if (!line.isBlank()) { + this.metricExceptions.add(line); + } + } + } + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } + + // load settings - requires database to exist + if (Files.exists(this.getSettingsFile())) { + this.loadSettings(); + } + + // a comparator that can be used to compare prefix names + // any name that does not exist is less than a name that does. + // otherwise, they are compared by value + this.prefixNameComparator = (o1, o2) -> { + if (!Presenter.this.database.containsPrefixName(o1)) + return -1; + else if (!Presenter.this.database.containsPrefixName(o2)) + return 1; + + final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); + final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); + + if (p1.getMultiplier() < p2.getMultiplier()) + return -1; + else if (p1.getMultiplier() > p2.getMultiplier()) + return 1; + + return o1.compareTo(o2); + }; + + this.unitNames = new ArrayList<>( + this.database.unitMapPrefixless(true).keySet()); + this.unitNames.sort(null); // sorts it using Comparable + + this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); + this.prefixNames.sort(this.prefixNameComparator); // sorts it using my + // comparator + + this.dimensionNames = new DelegateListModel<>( + new ArrayList<>(this.database.dimensionMap().keySet())); + this.dimensionNames.sort(null); // sorts it using Comparable + + // a Predicate that returns true iff the argument is a full base unit + final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + + // print out unit counts + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", + this.database.unitMapPrefixless(false).size(), + this.database.unitMapPrefixless(true).size(), + this.database.unitMapPrefixless(false).values().stream() + .filter(isFullBase).count()); + } + + /** + * Converts in the dimension-based converter + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void convertDimensionBased() { + final String fromSelection = this.view.getFromSelection(); + if (fromSelection == null) { + this.view.showErrorDialog("Error", + "No unit selected in From field"); + return; + } + final String toSelection = this.view.getToSelection(); + if (toSelection == null) { + this.view.showErrorDialog("Error", "No unit selected in To field"); + return; + } + + final Unit from = this.database.getUnit(fromSelection); + final Unit to = this.database.getUnit(toSelection) + .withName(NameSymbol.ofName(toSelection)); + + final UnitValue beforeValue; + try { + beforeValue = UnitValue.of(from, + this.view.getDimensionConverterInput()); + } catch (final ParseException e) { + this.view.showErrorDialog("Error", + "Error in parsing: " + e.getMessage()); + return; + } + final UnitValue value = beforeValue.convertTo(to); + + final String output = this.getRoundedString(value); + + this.view.setDimensionConverterOutputText( + String.format("%s = %s", beforeValue, output)); + } + + /** + * Runs whenever the convert button is pressed. + * + * <p> + * Reads and parses a unit expression from the from and to boxes, then + * converts {@code from} to {@code to}. Any errors are shown in + * JOptionPanes. + * </p> + * + * @since 2019-01-26 + * @since v0.1.0 + */ + public final void convertExpressions() { + final String fromUnitString = this.view.getFromText(); + final String toUnitString = this.view.getToText(); + + if (fromUnitString.isEmpty()) { + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the From: box."); + return; + } + if (toUnitString.isEmpty()) { + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the To: box."); + return; + } + + final LinearUnitValue from; + final Unit to; + try { + from = this.database.evaluateUnitExpression(fromUnitString); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorDialog("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return; + } + try { + to = this.database.getUnitFromExpression(toUnitString); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorDialog("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); + return; + } + + if (to instanceof LinearUnit) { + // convert to LinearUnitValue + final LinearUnitValue from2; + final LinearUnit to2 = ((LinearUnit) to) + .withName(NameSymbol.ofName(toUnitString)); + final boolean useSlash; + + if (from.canConvertTo(to2)) { + from2 = from; + useSlash = false; + } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { + from2 = LinearUnitValue.ONE.dividedBy(from); + useSlash = true; + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + return; + } + + final LinearUnitValue converted = from2.convertTo(to2); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); + return; + } else { + // convert to UnitValue + final UnitValue from2 = from.asUnitValue(); + if (from2.canConvertTo(to)) { + final UnitValue converted = from2.convertTo(to); + + this.view + .setExpressionConverterOutputText(String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + } + } + } + + /** + * @return a list of all of the unit dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public final List<String> dimensionNameList() { + return this.dimensionNames; + } + + /** + * @return a list of all the entries in the dimension-based converter's + * From box + * @since 2020-08-27 + */ + public final Set<String> fromEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.fromExistenceCondition); + } + + /** + * @return a comparator to compare prefix names + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Comparator<String> getPrefixNameComparator() { + return this.prefixNameComparator; + } + + /** + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final LinearUnitValue value, + boolean showUncertainty) { + switch (this.roundingType) { + case DECIMAL_PLACES: + case SIGNIFICANT_DIGITS: + return this.getRoundedString(value.asUnitValue()); + case SCIENTIFIC: + return value.toString(showUncertainty); + default: + throw new AssertionError("Invalid switch condition."); + } + } + + /** + * Like {@link UnitValue#toString()}, but obeys this unit converter's + * rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final UnitValue value) { + final BigDecimal unrounded = new BigDecimal(value.getValue()); + final BigDecimal rounded; + int precision = this.precision; + + switch (this.roundingType) { + case DECIMAL_PLACES: + rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); + break; + case SCIENTIFIC: + precision = 12; + //$FALL-THROUGH$ + case SIGNIFICANT_DIGITS: + rounded = unrounded + .round(new MathContext(precision, RoundingMode.HALF_EVEN)); + break; + default: + throw new AssertionError("Invalid switch condition."); + } + + String output = rounded.toString(); + + // remove trailing zeroes + if (output.contains(".")) { + while (output.endsWith("0")) { + output = output.substring(0, output.length() - 1); + } + if (output.endsWith(".")) { + output = output.substring(0, output.length() - 1); + } + } + + return output + " " + value.getUnit().getPrimaryName().get(); + } + + /** + * @return The file where settings are stored; + * @since 2020-12-11 + */ + private final Path getSettingsFile() { + return Path.of(DEFAULT_SETTINGS_FILEPATH); + } + + /** + * Loads settings from the settings file. + * + * @since 2021-02-17 + */ + public final void loadSettings() { + try { + // read file line by line + final int lineNum = 0; + for (final String line : Files + .readAllLines(this.getSettingsFile())) { + final int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line " + lineNum); + + final String param = line.substring(0, equalsIndex); + final String value = line.substring(equalsIndex + 1); + + switch (param) { + // set manually to avoid the unnecessary saving of the non-manual + // methods + case "precision": + this.precision = Integer.valueOf(value); + break; + case "rounding_type": + this.roundingType = RoundingType.valueOf(value); + break; + case "prefix_rule": + this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value); + this.database.setPrefixRepetitionRule(this.prefixRule); + break; + case "one_way": + this.oneWay = Boolean.valueOf(value); + if (this.oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName) + .isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + break; + case "include_duplicates": + this.includeDuplicateUnits = Boolean.valueOf(value); + if (this.view.presenter != null) { + this.view.update(); + } + break; + default: + System.err.printf("Warning: unrecognized setting \"%s\".", + param); + break; + } + } + } catch (final IOException e) {} + } + + /** + * @return a set of all prefix names in the database + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Set<String> prefixNameSet() { + return this.database.prefixMap().keySet(); + } + + /** + * Runs whenever a prefix is selected in the viewer. + * <p> + * Shows its information in the text box to the right. + * </p> + * + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void prefixSelected() { + final String prefixName = this.view.getPrefixViewerSelection(); + if (prefixName == null) + return; + else { + final UnitPrefix prefix = this.database.getPrefix(prefixName); + + this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", + prefixName, prefix.getMultiplier())); + } + } + + /** + * Saves the settings to the settings file. + * + * @since 2021-02-17 + */ + public final void saveSettings() { + try (BufferedWriter writer = Files + .newBufferedWriter(this.getSettingsFile())) { + writer.write(String.format("precision=%d\n", this.precision)); + writer.write( + String.format("rounding_type=%s\n", this.roundingType)); + writer.write(String.format("prefix_rule=%s\n", this.prefixRule)); + writer.write(String.format("one_way=%s\n", this.oneWay)); + writer.write(String.format("include_duplicates=%s\n", + this.includeDuplicateUnits)); + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorDialog("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + } + } + + public final void setIncludeDuplicateUnits( + boolean includeDuplicateUnits) { + this.includeDuplicateUnits = includeDuplicateUnits; + + this.view.update(); + this.saveSettings(); + } + + /** + * Enables or disables one-way conversion. + * + * @param oneWay whether one-way conversion should be on (true) or off + * (false) + * @since 2020-08-27 + */ + public final void setOneWay(boolean oneWay) { + this.oneWay = oneWay; + if (oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName).isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + + this.saveSettings(); + } + + /** + * @param precision new value of precision + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void setPrecision(final int precision) { + this.precision = precision; + + this.saveSettings(); + } + + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public void setPrefixRepetitionRule( + Predicate<List<UnitPrefix>> prefixRepetitionRule) { + if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) { + this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule; + } else { + this.prefixRule = null; + } + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + + this.saveSettings(); + } + + /** + * @param roundingType the roundingType to set + * @since 2020-07-16 + */ + public final void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + + this.saveSettings(); + } + + /** + * @return a list of all the entries in the dimension-based converter's To + * box + * @since 2020-08-27 + */ + public final Set<String> toEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.toExistenceCondition); + } + + /** + * Returns true if and only if the unit represented by {@code unitName} + * has the dimension represented by {@code dimensionName}. + * + * @param unitName name of unit to test + * @param dimensionName name of dimension to test + * @return whether unit has dimenision + * @since 2019-04-13 + * @since v0.2.0 + */ + public final boolean unitMatchesDimension(final String unitName, + final String dimensionName) { + final Unit unit = this.database.getUnit(unitName); + final ObjectProduct<BaseDimension> dimension = this.database + .getDimension(dimensionName); + return unit.getDimension().equals(dimension); + } + + /** + * Runs whenever a unit is selected in the viewer. + * <p> + * Shows its information in the text box to the right. + * </p> + * + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void unitNameSelected() { + final String unitName = this.view.getUnitViewerSelection(); + if (unitName == null) + return; + else { + final Unit unit = this.database.getUnit(unitName); + + this.view.setUnitTextBoxText(unit.toString()); + } + } + + /** + * @return a set of all of the unit names + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Set<String> unitNameSet() { + return this.database.unitMapPrefixless(this.includeDuplicateUnits) + .keySet(); + } + } + + /** + * Different types of rounding. + * + * Significant digits: Rounds to a number of digits. i.e. with precision 5, + * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits + * after the decimal point, i.e. with precision 5, 12345.6789 rounds to + * 12345.67890. Scientific: Rounds based on the number of digits and + * operations, following standard scientific rounding. + */ + private static enum RoundingType { + SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; + } + + private static class View { + private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + + /** The view's frame. */ + private final JFrame frame; + /** The view's associated presenter. */ + private final Presenter presenter; + /** The master pane containing all of the tabs. */ + private final JTabbedPane masterPane; + + // DIMENSION-BASED CONVERTER + /** The panel for inputting values in the dimension-based converter */ + private final JTextField valueInput; + /** The panel for "From" in the dimension-based converter */ + private final SearchBoxList fromSearch; + /** The panel for "To" in the dimension-based converter */ + private final SearchBoxList toSearch; + /** The output area in the dimension-based converter */ + private final JTextArea dimensionBasedOutput; + + // EXPRESSION-BASED CONVERTER + /** The "From" entry in the conversion panel */ + private final JTextField fromEntry; + /** The "To" entry in the conversion panel */ + private final JTextField toEntry; + /** The output area in the conversion panel */ + private final JTextArea output; + + // UNIT AND PREFIX VIEWERS + /** The searchable list of unit names in the unit viewer */ + private final SearchBoxList unitNameList; + /** The searchable list of prefix names in the prefix viewer */ + private final SearchBoxList prefixNameList; + /** The text box for unit data in the unit viewer */ + private final JTextArea unitTextBox; + /** The text box for prefix data in the prefix viewer */ + private final JTextArea prefixTextBox; + + /** + * Creates the {@code View}. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + public View() { + this.presenter = new Presenter(this); + this.frame = new JFrame("7Units"); + this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + // enable system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException e) { + // oh well, just use default theme + System.err.println("Failed to enable system look-and-feel."); + e.printStackTrace(); + } + + // create the components + this.masterPane = new JTabbedPane(); + this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); + this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), + this.presenter.getPrefixNameComparator(), true); + this.unitTextBox = new JTextArea(); + this.prefixTextBox = new JTextArea(); + this.fromSearch = new SearchBoxList(this.presenter.fromEntries()); + this.toSearch = new SearchBoxList(this.presenter.toEntries()); + this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); + this.dimensionBasedOutput = new JTextArea(2, 32); + this.fromEntry = new JTextField(); + this.toEntry = new JTextField(); + this.output = new JTextArea(2, 32); + + // create more components + this.initComponents(); + + this.frame.pack(); + } + + /** + * @return the currently selected pane. + * @throws AssertionError if no pane (or an invalid pane) is selected + */ + public Pane getActivePane() { + switch (this.masterPane.getSelectedIndex()) { + case 0: + return Pane.UNIT_CONVERTER; + case 1: + return Pane.EXPRESSION_CONVERTER; + case 2: + return Pane.UNIT_VIEWER; + case 3: + return Pane.PREFIX_VIEWER; + case 4: + return Pane.ABOUT; + case 5: + return Pane.SETTINGS; + default: + throw new AssertionError("No selected pane, or invalid pane."); + } + } + + /** + * @return value in dimension-based converter + * @throws ParseException + * @since 2020-07-07 + */ + public double getDimensionConverterInput() throws ParseException { + final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); + if (value instanceof Double) + return (double) value; + else if (value instanceof Long) + return ((Long) value).longValue(); + else + throw new AssertionError(); + } + + /** + * @return selection in "From" selector in dimension-based converter + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getFromSelection() { + return this.fromSearch.getSelectedValue(); + } + + /** + * @return text in "From" box in converter panel + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getFromText() { + return this.fromEntry.getText(); + } + + /** + * @return index of selected prefix in prefix viewer + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getPrefixViewerSelection() { + return this.prefixNameList.getSelectedValue(); + } + + /** + * @return selection in "To" selector in dimension-based converter + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getToSelection() { + return this.toSearch.getSelectedValue(); + } + + /** + * @return text in "To" box in converter panel + * @since 2019-01-26 + * @since v0.1.0 + */ + public String getToText() { + return this.toEntry.getText(); + } + + /** + * @return index of selected unit in unit viewer + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getUnitViewerSelection() { + return this.unitNameList.getSelectedValue(); + } + + /** + * Starts up the application. + * + * @since 2018-12-27 + * @since v0.1.0 + */ + public final void init() { + this.frame.setVisible(true); + } + + /** + * Initializes the view's components. + * + * @since 2018-12-27 + * @since v0.1.0 + */ + private final void initComponents() { + final JPanel masterPanel = new JPanel(); + this.frame.add(masterPanel); + + masterPanel.setLayout(new BorderLayout()); + + { // pane with all of the tabs + masterPanel.add(this.masterPane, BorderLayout.CENTER); + + // update stuff + this.masterPane.addChangeListener(e -> this.update()); + + { // a panel for unit conversion using a selector + final JPanel convertUnitPanel = new JPanel(); + this.masterPane.addTab("Convert Units", convertUnitPanel); + this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); + + convertUnitPanel.setLayout(new BorderLayout()); + + { // panel for input part + final JPanel inputPanel = new JPanel(); + convertUnitPanel.add(inputPanel, BorderLayout.CENTER); + + inputPanel.setLayout(new GridLayout(1, 3)); + + final JComboBox<String> dimensionSelector = new JComboBox<>( + this.presenter.dimensionNameList() + .toArray(new String[0])); + dimensionSelector.setSelectedItem("LENGTH"); + + // handle dimension filter + final MutablePredicate<String> dimensionFilter = new MutablePredicate<>( + s -> true); + + // panel for From things + inputPanel.add(this.fromSearch); + + this.fromSearch.addSearchFilter(dimensionFilter); + + { // for dimension selector and arrow that represents + // conversion + final JPanel inBetweenPanel = new JPanel(); + inputPanel.add(inBetweenPanel); + + inBetweenPanel.setLayout(new BorderLayout()); + + { // dimension selector + inBetweenPanel.add(dimensionSelector, + BorderLayout.PAGE_START); + } + + { // the arrow in the middle + final JLabel arrowLabel = new JLabel("->"); + inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); + } + } + + // panel for To things + + inputPanel.add(this.toSearch); + + this.toSearch.addSearchFilter(dimensionFilter); + + // code for dimension filter + dimensionSelector.addItemListener(e -> { + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); + this.fromSearch.reapplyFilter(); + this.toSearch.reapplyFilter(); + }); + + // apply the item listener once because I have a default + // selection + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); + this.fromSearch.reapplyFilter(); + this.toSearch.reapplyFilter(); + } + + { // panel for submit and output, and also value entry + final JPanel outputPanel = new JPanel(); + convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); + + outputPanel.setLayout(new GridLayout(3, 1)); + + { // unit input + final JPanel valueInputPanel = new JPanel(); + outputPanel.add(valueInputPanel); + + valueInputPanel.setLayout(new BorderLayout()); + + { // prompt + final JLabel valuePrompt = new JLabel( + "Value to convert: "); + valueInputPanel.add(valuePrompt, + BorderLayout.LINE_START); + } + + { // value to convert + valueInputPanel.add(this.valueInput, + BorderLayout.CENTER); + } + } + + { // button to convert + final JButton convertButton = new JButton("Convert"); + outputPanel.add(convertButton); + + convertButton.addActionListener( + e -> this.presenter.convertDimensionBased()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + } + + { // output of conversion + outputPanel.add(this.dimensionBasedOutput); + this.dimensionBasedOutput.setEditable(false); + } + } + } + + { // panel for unit conversion using expressions + final JPanel convertExpressionPanel = new JPanel(); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); + this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); + + convertExpressionPanel.setLayout(new GridLayout(4, 1)); + + { // panel for units to convert from + final JPanel fromPanel = new JPanel(); + convertExpressionPanel.add(fromPanel); + + fromPanel.setBorder(BorderFactory.createTitledBorder("From")); + fromPanel.setLayout(new GridLayout(1, 1)); + + { // entry for units + fromPanel.add(this.fromEntry); + } + } + + { // panel for units to convert to + final JPanel toPanel = new JPanel(); + convertExpressionPanel.add(toPanel); + + toPanel.setBorder(BorderFactory.createTitledBorder("To")); + toPanel.setLayout(new GridLayout(1, 1)); + + { // entry for units + toPanel.add(this.toEntry); + } + } + + { // button to convert + final JButton convertButton = new JButton("Convert"); + convertExpressionPanel.add(convertButton); + + convertButton.addActionListener( + e -> this.presenter.convertExpressions()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + } + + { // output of conversion + final JPanel outputPanel = new JPanel(); + convertExpressionPanel.add(outputPanel); + + outputPanel + .setBorder(BorderFactory.createTitledBorder("Output")); + outputPanel.setLayout(new GridLayout(1, 1)); + + { // output + outputPanel.add(this.output); + this.output.setEditable(false); + } + } + } + + { // panel to look up units + final JPanel unitLookupPanel = new JPanel(); + this.masterPane.addTab("Unit Viewer", unitLookupPanel); + this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); + + unitLookupPanel.setLayout(new GridLayout()); + + { // search panel + unitLookupPanel.add(this.unitNameList); + + this.unitNameList.getSearchList().addListSelectionListener( + e -> this.presenter.unitNameSelected()); + } + + { // the text box for unit's toString + unitLookupPanel.add(this.unitTextBox); + this.unitTextBox.setEditable(false); + this.unitTextBox.setLineWrap(true); + } + } + + { // panel to look up prefixes + final JPanel prefixLookupPanel = new JPanel(); + this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); + this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); + + prefixLookupPanel.setLayout(new GridLayout(1, 2)); + + { // panel for listing and seaching + prefixLookupPanel.add(this.prefixNameList); + + this.prefixNameList.getSearchList().addListSelectionListener( + e -> this.presenter.prefixSelected()); + } + + { // the text box for prefix's toString + prefixLookupPanel.add(this.prefixTextBox); + this.prefixTextBox.setEditable(false); + this.prefixTextBox.setLineWrap(true); + } + } + + { // Info panel + final JPanel infoPanel = new JPanel(); + this.masterPane.addTab("\uD83D\uDEC8", // info (i) character + new JScrollPane(infoPanel)); + + final JTextArea infoTextArea = new JTextArea(); + infoTextArea.setEditable(false); + infoTextArea.setOpaque(false); + infoPanel.add(infoTextArea); + + // get info text + final String infoText = Presenter + .getLinesFromResource("/about.txt").stream() + .map(Presenter::withoutComments) + .collect(Collectors.joining("\n")) + .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); + infoTextArea.setText(infoText); + } + + { // Settings panel + final JPanel settingsPanel = new JPanel(); + this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); + this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); + + settingsPanel.setLayout( + new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + + { // rounding settings + final JPanel roundingPanel = new JPanel(); + settingsPanel.add(roundingPanel); + roundingPanel + .setBorder(new TitledBorder("Rounding Settings")); + roundingPanel.setLayout(new GridBagLayout()); + + // rounding rule selection + final ButtonGroup roundingRuleButtons = new ButtonGroup(); + + final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrecision = new JRadioButton( + "Fixed Precision"); + if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { + fixedPrecision.setSelected(true); + } + fixedPrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + roundingRuleButtons.add(fixedPrecision); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedDecimals = new JRadioButton( + "Fixed Decimal Places"); + if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { + fixedDecimals.setSelected(true); + } + fixedDecimals.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.DECIMAL_PLACES)); + roundingRuleButtons.add(fixedDecimals); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton relativePrecision = new JRadioButton( + "Scientific Precision"); + if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { + relativePrecision.setSelected(true); + } + relativePrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SCIENTIFIC)); + roundingRuleButtons.add(relativePrecision); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JLabel sliderLabel = new JLabel("Precision:"); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JSlider sigDigSlider = new JSlider(0, 12); + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + + sigDigSlider.setMajorTickSpacing(4); + sigDigSlider.setMinorTickSpacing(1); + sigDigSlider.setSnapToTicks(true); + sigDigSlider.setPaintTicks(true); + sigDigSlider.setPaintLabels(true); + sigDigSlider.setValue(this.presenter.precision); + + sigDigSlider.addChangeListener(e -> this.presenter + .setPrecision(sigDigSlider.getValue())); + } + + { // prefix repetition settings + final JPanel prefixRepetitionPanel = new JPanel(); + settingsPanel.add(prefixRepetitionPanel); + prefixRepetitionPanel.setBorder( + new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setLayout(new GridBagLayout()); + + // prefix rules + final ButtonGroup prefixRuleButtons = new ButtonGroup(); + + final JRadioButton noRepetition = new JRadioButton( + "No Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { + noRepetition.setSelected(true); + } + noRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_REPETITION)); + prefixRuleButtons.add(noRepetition); + prefixRepetitionPanel.add(noRepetition, + new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + + final JRadioButton noRestriction = new JRadioButton( + "No Restriction"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { + noRestriction.setSelected(true); + } + noRestriction.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_RESTRICTION)); + prefixRuleButtons.add(noRestriction); + prefixRepetitionPanel.add(noRestriction, + new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + + final JRadioButton customRepetition = new JRadioButton( + "Complex Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { + customRepetition.setSelected(true); + } + customRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); + prefixRuleButtons.add(customRepetition); + prefixRepetitionPanel.add(customRepetition, + new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + } + + { // search settings + final JPanel searchingPanel = new JPanel(); + settingsPanel.add(searchingPanel); + searchingPanel.setBorder(new TitledBorder("Search Settings")); + searchingPanel.setLayout(new GridBagLayout()); + + // searching rules + final ButtonGroup searchRuleButtons = new ButtonGroup(); + + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); + noPrefixes.setEnabled(false); + searchRuleButtons.add(noPrefixes); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrefixes = new JRadioButton( + "Include Some Prefixes"); + fixedPrefixes.setEnabled(false); + searchRuleButtons.add(fixedPrefixes); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton explicitPrefixes = new JRadioButton( + "Include Explicit Prefixes"); + explicitPrefixes.setEnabled(false); + searchRuleButtons.add(explicitPrefixes); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); + alwaysInclude.setEnabled(false); + searchRuleButtons.add(alwaysInclude); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + + { // miscellaneous settings + final JPanel miscPanel = new JPanel(); + settingsPanel.add(miscPanel); + miscPanel + .setBorder(new TitledBorder("Miscellaneous Settings")); + miscPanel.setLayout(new GridBagLayout()); + + final JCheckBox oneWay = new JCheckBox( + "Convert One Way Only"); + oneWay.setSelected(this.presenter.oneWay); + oneWay.addItemListener( + e -> this.presenter.setOneWay(e.getStateChange() == 1)); + miscPanel.add(oneWay, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JCheckBox showAllVariations = new JCheckBox( + "Show Duplicates in \"Convert Units\""); + showAllVariations + .setSelected(this.presenter.includeDuplicateUnits); + showAllVariations.addItemListener(e -> this.presenter + .setIncludeDuplicateUnits(e.getStateChange() == 1)); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JButton unitFileButton = new JButton( + "Manage Unit Data Files"); + unitFileButton.setEnabled(false); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + } + } + } + + /** + * Sets the text in the output of the dimension-based converter. + * + * @param text text to set + * @since 2019-04-13 + * @since v0.2.0 + */ + public void setDimensionConverterOutputText(final String text) { + this.dimensionBasedOutput.setText(text); + } + + /** + * Sets the text in the output of the conversion panel. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setExpressionConverterOutputText(final String text) { + this.output.setText(text); + } + + /** + * Sets the text of the prefix text box in the prefix viewer. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setPrefixTextBoxText(final String text) { + this.prefixTextBox.setText(text); + } + + /** + * Sets the text of the unit text box in the unit viewer. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setUnitTextBoxText(final String text) { + this.unitTextBox.setText(text); + } + + /** + * Shows an error dialog. + * + * @param title title of dialog + * @param message message in dialog + * @since 2019-01-14 + * @since v0.1.0 + */ + public void showErrorDialog(final String title, final String message) { + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); + } + + public void update() { + this.unitNameList.setItems(this.presenter.unitNameSet()); + this.fromSearch.setItems(this.presenter.fromEntries()); + this.toSearch.setItems(this.presenter.toEntries()); + + switch (this.getActivePane()) { + case UNIT_CONVERTER: + this.fromSearch.updateList(); + this.toSearch.updateList(); + break; + default: + // do nothing, for now + break; + } + } + } + + public static void main(final String[] args) { + new View().init(); + } +} diff --git a/src/main/java/sevenUnits/converterGUI/package-info.java b/src/main/java/sevenUnits/converterGUI/package-info.java new file mode 100644 index 0000000..784664f --- /dev/null +++ b/src/main/java/sevenUnits/converterGUI/package-info.java @@ -0,0 +1,24 @@ +/** + * 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/>. + */ +/** + * The GUI interface of the Unit Converter. + * + * @author Adrien Hopkins + * @since 2019-01-25 + * @since v0.2.0 + */ +package sevenUnits.converterGUI;
\ No newline at end of file |