diff options
author | Adrien Hopkins <ahopk127@my.yorku.ca> | 2022-02-19 16:59:26 -0500 |
---|---|---|
committer | Adrien Hopkins <ahopk127@my.yorku.ca> | 2022-02-19 16:59:26 -0500 |
commit | 540b798e397fb787fd81c8e6e636a2343655a42f (patch) | |
tree | 8370b7f70da0be6284fe648d0bfa85d063ef48b9 /src | |
parent | b179f3720fcd569c07f5fe95ee00d7ccfe12639d (diff) |
Made barebones GUI (TabbedView)
Diffstat (limited to 'src')
-rw-r--r-- | src/main/java/sevenUnits/ProgramInfo.java | 2 | ||||
-rw-r--r-- | src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java | 2 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/DelegateListModel.java | 242 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/FilterComparator.java | 128 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/GridBagBuilder.java | 479 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/MutablePredicate.java | 70 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/Presenter.java | 88 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/SearchBoxList.java | 331 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/TabbedView.java | 605 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/UnitConversionView.java | 10 | ||||
-rw-r--r-- | src/main/java/sevenUnitsGUI/ViewBot.java | 23 | ||||
-rw-r--r-- | src/main/resources/about.txt | 2 | ||||
-rw-r--r-- | src/test/java/sevenUnitsGUI/PresenterTest.java | 12 |
13 files changed, 1968 insertions, 26 deletions
diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index ba6bc7a..0d67824 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -24,7 +24,7 @@ package sevenUnits; */ public final class ProgramInfo { - public static final String VERSION = "0.3.2"; + public static final String VERSION = "0.4.0-dev"; private ProgramInfo() { // this class is only for static variables, you shouldn't be able to diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index bfd5974..e21c25f 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -69,8 +69,8 @@ import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; -import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Metric; +import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; 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/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java new file mode 100644 index 0000000..f34d0c0 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/FilterComparator.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnitsGUI; + +import java.util.Comparator; +import java.util.Objects; + +/** + * A comparator that compares strings using a filter. + * + * @param <T> type of element being compared + * + * @author Adrien Hopkins + * @since 2019-01-15 + * @since v0.1.0 + */ +final class FilterComparator<T> implements Comparator<T> { + /** + * The filter that the comparator is filtered by. + * + * @since 2019-01-15 + * @since v0.1.0 + */ + private final String filter; + /** + * The comparator to use if the arguments are otherwise equal. + * + * @since 2019-01-15 + * @since v0.1.0 + */ + private final Comparator<T> comparator; + /** + * Whether or not the comparison is case-sensitive. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private final boolean caseSensitive; + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter) { + this(filter, null); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter string to filter by + * @param comparator comparator to fall back to if all else fails, null is + * compareTo. + * @throws NullPointerException if filter is null + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter, + final Comparator<T> comparator) { + this(filter, comparator, false); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter string to filter by + * @param comparator comparator to fall back to if all else fails, null is + * compareTo. + * @param caseSensitive whether or not the comparator is case-sensitive + * @throws NullPointerException if filter is null + * @since 2019-04-14 + * @since v0.2.0 + */ + public FilterComparator(final String filter, final Comparator<T> comparator, + final boolean caseSensitive) { + this.filter = Objects.requireNonNull(filter, "filter must not be null."); + this.comparator = comparator; + this.caseSensitive = caseSensitive; + } + + @Override + public int compare(final T arg0, final T arg1) { + // if this is case insensitive, make them lowercase + final String str0, str1; + if (this.caseSensitive) { + str0 = arg0.toString(); + str1 = arg1.toString(); + } else { + str0 = arg0.toString().toLowerCase(); + str1 = arg1.toString().toLowerCase(); + } + + // elements that start with the filter always go first + if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) + return -1; + else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) + return 1; + + // elements that contain the filter but don't start with them go next + if (str0.contains(this.filter) && !str1.contains(this.filter)) + return -1; + else if (!str0.contains(this.filter) && !str1.contains(this.filter)) + return 1; + + // other elements go last + if (this.comparator == null) + return str0.compareTo(str1); + else + return this.comparator.compare(arg0, arg1); + } +} diff --git a/src/main/java/sevenUnitsGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java new file mode 100644 index 0000000..32e94d7 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnitsGUI; + +import java.awt.GridBagConstraints; +import java.awt.Insets; + +/** + * A builder for Java's {@link java.awt.GridBagConstraints} class. + * + * @author Adrien Hopkins + * @since 2018-11-30 + * @since v0.1.0 + */ +final class GridBagBuilder { + /** + * The built {@code GridBagConstraints}'s {@code gridx} property. + * <p> + * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has + * <code>gridx=0</code>. The leading edge of a component's display area is its left edge for a horizontal, + * left-to-right container and its right edge for a horizontal, right-to-left container. The value + * <code>RELATIVE</code> specifies that the component be placed immediately following the component that was added + * to the container just before this component was added. + * <p> + * The default value is <code>RELATIVE</code>. <code>gridx</code> should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridy + * @see java.awt.ComponentOrientation + */ + private final int gridx; + + /** + * The built {@code GridBagConstraints}'s {@code gridy} property. + * <p> + * Specifies the cell at the top of the component's display area, where the topmost cell has <code>gridy=0</code>. + * The value <code>RELATIVE</code> specifies that the component be placed just below the component that was added to + * the container just before this component was added. + * <p> + * The default value is <code>RELATIVE</code>. <code>gridy</code> should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridx + */ + private final int gridy; + + /** + * The built {@code GridBagConstraints}'s {@code gridwidth} property. + * <p> + * Specifies the number of cells in a row for the component's display area. + * <p> + * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridx</code> to the + * last cell in the row. Use <code>RELATIVE</code> to specify that the component's display area will be from + * <code>gridx</code> to the next to the last one in its row. + * <p> + * <code>gridwidth</code> should be non-negative and the default value is 1. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridheight + */ + private final int gridwidth; + + /** + * The built {@code GridBagConstraints}'s {@code gridheight} property. + * <p> + * Specifies the number of cells in a column for the component's display area. + * <p> + * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridy</code> to the + * last cell in the column. Use <code>RELATIVE</code> to specify that the component's display area will be from + * <code>gridy</code> to the next to the last one in its column. + * <p> + * <code>gridheight</code> should be a non-negative value and the default value is 1. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridwidth + */ + private final int gridheight; + + /** + * The built {@code GridBagConstraints}'s {@code weightx} property. + * <p> + * Specifies how to distribute extra horizontal space. + * <p> + * The grid bag layout manager calculates the weight of a column to be the maximum <code>weightx</code> of all the + * components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra + * space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no + * extra space. + * <p> + * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right + * edges. + * <p> + * The default value of this field is <code>0</code>. <code>weightx</code> should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#weighty + */ + private double weightx; + + /** + * The built {@code GridBagConstraints}'s {@code weighty} property. + * <p> + * Specifies how to distribute extra vertical space. + * <p> + * The grid bag layout manager calculates the weight of a row to be the maximum <code>weighty</code> of all the + * components in a row. If the resulting layout is smaller vertically than the area it needs to fill, the extra + * space is distributed to each row in proportion to its weight. A row that has a weight of zero receives no extra + * space. + * <p> + * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom + * edges. + * <p> + * The default value of this field is <code>0</code>. <code>weighty</code> should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#weightx + */ + private double weighty; + + /** + * The built {@code GridBagConstraints}'s {@code anchor} property. + * <p> + * This field is used when the component is smaller than its display area. It determines where, within the display + * area, to place the component. + * <p> + * There are three kinds of possible values: orientation relative, baseline relative and absolute. Orientation + * relative values are interpreted relative to the container's component orientation property, baseline relative + * values are interpreted relative to the baseline and absolute values are not. The absolute values are: + * <code>CENTER</code>, <code>NORTH</code>, <code>NORTHEAST</code>, <code>EAST</code>, <code>SOUTHEAST</code>, + * <code>SOUTH</code>, <code>SOUTHWEST</code>, <code>WEST</code>, and <code>NORTHWEST</code>. The orientation + * relative values are: <code>PAGE_START</code>, <code>PAGE_END</code>, <code>LINE_START</code>, + * <code>LINE_END</code>, <code>FIRST_LINE_START</code>, <code>FIRST_LINE_END</code>, <code>LAST_LINE_START</code> + * and <code>LAST_LINE_END</code>. The baseline relative values are: <code>BASELINE</code>, + * <code>BASELINE_LEADING</code>, <code>BASELINE_TRAILING</code>, <code>ABOVE_BASELINE</code>, + * <code>ABOVE_BASELINE_LEADING</code>, <code>ABOVE_BASELINE_TRAILING</code>, <code>BELOW_BASELINE</code>, + * <code>BELOW_BASELINE_LEADING</code>, and <code>BELOW_BASELINE_TRAILING</code>. The default value is + * <code>CENTER</code>. + * + * @serial + * @see #clone() + * @see java.awt.ComponentOrientation + */ + private int anchor; + + /** + * The built {@code GridBagConstraints}'s {@code fill} property. + * <p> + * This field is used when the component's display area is larger than the component's requested size. It determines + * whether to resize the component, and if so, how. + * <p> + * The following values are valid for <code>fill</code>: + * + * <ul> + * <li><code>NONE</code>: Do not resize the component. + * <li><code>HORIZONTAL</code>: Make the component wide enough to fill its display area horizontally, but do not + * change its height. + * <li><code>VERTICAL</code>: Make the component tall enough to fill its display area vertically, but do not change + * its width. + * <li><code>BOTH</code>: Make the component fill its display area entirely. + * </ul> + * <p> + * The default value is <code>NONE</code>. + * + * @serial + * @see #clone() + */ + private int fill; + + /** + * The built {@code GridBagConstraints}'s {@code insets} property. + * <p> + * This field specifies the external padding of the component, the minimum amount of space between the component and + * the edges of its display area. + * <p> + * The default value is <code>new Insets(0, 0, 0, 0)</code>. + * + * @serial + * @see #clone() + */ + private Insets insets; + + /** + * The built {@code GridBagConstraints}'s {@code ipadx} property. + * <p> + * This field specifies the internal padding of the component, how much space to add to the minimum width of the + * component. The width of the component is at least its minimum width plus <code>ipadx</code> pixels. + * <p> + * The default value is <code>0</code>. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#ipady + */ + private int ipadx; + + /** + * The built {@code GridBagConstraints}'s {@code ipady} property. + * <p> + * This field specifies the internal padding, that is, how much space to add to the minimum height of the component. + * The height of the component is at least its minimum height plus <code>ipady</code> pixels. + * <p> + * The default value is 0. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#ipadx + */ + private int ipady; + + /** + * @param gridx + * x position + * @param gridy + * y position + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder(final int gridx, final int gridy) { + this(gridx, gridy, 1, 1); + } + + /** + * @param gridx + * x position + * @param gridy + * y position + * @param gridwidth + * number of cells occupied horizontally + * @param gridheight + * number of cells occupied vertically + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight) { + this(gridx, gridy, gridwidth, gridheight, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + } + + /** + * @param gridx + * x position + * @param gridy + * y position + * @param gridwidth + * number of cells occupied horizontally + * @param gridheight + * number of cells occupied vertically + * @param weightx + * @param weighty + * @param anchor + * @param fill + * @param insets + * @param ipadx + * @param ipady + * @since 2018-11-30 + * @since v0.1.0 + */ + private GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight, + final double weightx, final double weighty, final int anchor, final int fill, final Insets insets, + final int ipadx, final int ipady) { + super(); + this.gridx = gridx; + this.gridy = gridy; + this.gridwidth = gridwidth; + this.gridheight = gridheight; + this.weightx = weightx; + this.weighty = weighty; + this.anchor = anchor; + this.fill = fill; + this.insets = (Insets) insets.clone(); + this.ipadx = ipadx; + this.ipady = ipady; + } + + /** + * @return {@code GridBagConstraints} created by this builder + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagConstraints build() { + return new GridBagConstraints(this.gridx, this.gridy, this.gridwidth, this.gridheight, this.weightx, + this.weighty, this.anchor, this.fill, this.insets, this.ipadx, this.ipady); + } + + /** + * @return anchor + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getAnchor() { + return this.anchor; + } + + /** + * @return fill + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getFill() { + return this.fill; + } + + /** + * @return gridheight + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridheight() { + return this.gridheight; + } + + /** + * @return gridwidth + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridwidth() { + return this.gridwidth; + } + + /** + * @return gridx + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridx() { + return this.gridx; + } + + /** + * @return gridy + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridy() { + return this.gridy; + } + + /** + * @return insets + * @since 2018-11-30 + * @since v0.1.0 + */ + public Insets getInsets() { + return this.insets; + } + + /** + * @return ipadx + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getIpadx() { + return this.ipadx; + } + + /** + * @return ipady + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getIpady() { + return this.ipady; + } + + /** + * @return weightx + * @since 2018-11-30 + * @since v0.1.0 + */ + public double getWeightx() { + return this.weightx; + } + + /** + * @return weighty + * @since 2018-11-30 + * @since v0.1.0 + */ + public double getWeighty() { + return this.weighty; + } + + /** + * @param anchor + * anchor to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setAnchor(final int anchor) { + this.anchor = anchor; + return this; + } + + /** + * @param fill + * fill to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setFill(final int fill) { + this.fill = fill; + return this; + } + + /** + * @param insets + * insets to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setInsets(final Insets insets) { + this.insets = insets; + return this; + } + + /** + * @param ipadx + * ipadx to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setIpadx(final int ipadx) { + this.ipadx = ipadx; + return this; + } + + /** + * @param ipady + * ipady to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setIpady(final int ipady) { + this.ipady = ipady; + return this; + } + + /** + * @param weightx + * weightx to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setWeightx(final double weightx) { + this.weightx = weightx; + return this; + } + + /** + * @param weighty + * weighty to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setWeighty(final double weighty) { + this.weighty = weighty; + return this; + } +} diff --git a/src/main/java/sevenUnitsGUI/MutablePredicate.java b/src/main/java/sevenUnitsGUI/MutablePredicate.java new file mode 100644 index 0000000..6cb8689 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/MutablePredicate.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnitsGUI; + +import java.util.function.Predicate; + +/** + * A container for a predicate, which can be changed later. + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class MutablePredicate<T> implements Predicate<T> { + /** + * The predicate stored in this {@code MutablePredicate} + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private Predicate<T> predicate; + + /** + * Creates the {@code MutablePredicate}. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public MutablePredicate(final Predicate<T> predicate) { + this.predicate = predicate; + } + + /** + * @return predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final Predicate<T> getPredicate() { + return this.predicate; + } + + /** + * @param predicate + * new value of predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void setPredicate(final Predicate<T> predicate) { + this.predicate = predicate; + } + + @Override + public boolean test(final T t) { + return this.predicate.test(t); + } +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 4373049..07671e4 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -16,12 +16,21 @@ */ package sevenUnitsGUI; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.List; +import java.util.Scanner; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import sevenUnits.ProgramInfo; +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; +import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; /** @@ -32,6 +41,62 @@ import sevenUnits.utils.UncertainDouble; */ public final class Presenter { /** + * @return text in About file + * @since 2022-02-19 + */ + static final String getAboutText() { + return Presenter.getLinesFromResource("/about.txt").stream() + .map(Presenter::withoutComments).collect(Collectors.joining("\n")) + .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); + } + + /** + * 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 {@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); + } + + // ====== SETTINGS ====== + + /** * The view that this presenter communicates with */ private final View view; @@ -41,8 +106,6 @@ public final class Presenter { */ private final UnitDatabase database; - // ====== SETTINGS ====== - /** * The rule used for parsing input numbers. Any number-string inputted into * this program will be parsed using this method. @@ -136,6 +199,8 @@ public final class Presenter { */ public void loadSettings() {} + void prefixSelected() {} + /** * Gets user settings from the view then saves them to the user's settings * file. @@ -143,4 +208,23 @@ public final class Presenter { * @since 2021-12-15 */ public void saveSettings() {} + + /** + * 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 + */ + boolean unitMatchesDimension(String unitName, String dimensionName) { + final Unit unit = this.database.getUnit(unitName); + final ObjectProduct<BaseDimension> dimension = this.database + .getDimension(dimensionName); + return unit.getDimension().equals(dimension); + } + + void unitNameSelected() {} } diff --git a/src/main/java/sevenUnitsGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java new file mode 100644 index 0000000..2b935d0 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java @@ -0,0 +1,331 @@ +/** + * 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.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 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/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java new file mode 100644 index 0000000..e92b661 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -0,0 +1,605 @@ +/** + * 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.KeyEvent; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.AbstractSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; + +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.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.BaseDimension; +import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitPrefix; +import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; + +/** + * A View that separates its functions into multiple tabs + * + * @since 2022-02-19 + */ +final class TabbedView implements ExpressionConversionView, UnitConversionView { + /** + * A List-like view of a JComboBox's items + * + * @param <E> type of item in list + * + * @since 2022-02-19 + */ + private static final class JComboBoxItemSet<E> extends AbstractSet<E> { + private final JComboBox<E> comboBox; + + /** + * @param comboBox combo box to get items from + * @since 2022-02-19 + */ + public JComboBoxItemSet(JComboBox<E> comboBox) { + this.comboBox = comboBox; + } + + @Override + public Iterator<E> iterator() { + return new Iterator<>() { + private int index = 0; + + @Override + public boolean hasNext() { + return this.index < JComboBoxItemSet.this.size(); + } + + @Override + public E next() { + if (this.hasNext()) + return JComboBoxItemSet.this.comboBox.getItemAt(this.index++); + else + throw new NoSuchElementException( + "Iterator has finished iteration"); + } + }; + } + + @Override + public int size() { + return this.comboBox.getItemCount(); + } + + } + + private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + + /** + * Creates a TabbedView. + * + * @param args command line arguments + * @since 2022-02-19 + */ + public static void main(String[] args) { + // This view doesn't need to do anything, the side effects of creating it + // are enough to start the program + @SuppressWarnings("unused") + final View view = new TabbedView(); + } + + /** The Presenter that handles this View */ + final Presenter presenter; + /** The frame that this view lives on */ + final JFrame frame; + /** The tabbed pane that contains all of the components */ + final JTabbedPane masterPane; + + // DIMENSION-BASED CONVERTER + /** The combo box that selects dimensions */ + private final JComboBox<NamedObjectProduct<BaseDimension>> dimensionSelector; + /** The panel for inputting values in the dimension-based converter */ + private final JTextField valueInput; + /** The panel for "From" in the dimension-based converter */ + private final SearchBoxList<Unit> fromSearch; + /** The panel for "To" in the dimension-based converter */ + private final SearchBoxList<Unit> toSearch; + /** The output area in the dimension-based converter */ + private final JTextArea unitOutput; + + // EXPRESSION-BASED CONVERTER + /** The "From" entry in the conversion panel */ + private final JTextField fromEntry; + /** The "To" entry in the conversion panel */ + private final JTextField toEntry; + /** The output area in the conversion panel */ + private final JTextArea expressionOutput; + + // UNIT AND PREFIX VIEWERS + /** The searchable list of unit names in the unit viewer */ + private final SearchBoxList<Unit> unitNameList; + /** The searchable list of prefix names in the prefix viewer */ + private final SearchBoxList<UnitPrefix> 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 view and makes it visible to the user + * + * @since 2022-02-19 + */ + public TabbedView() { + // enable system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException e) { + // oh well, just use default theme + System.err.println("Failed to enable system look-and-feel."); + e.printStackTrace(); + } + + // initialize important components + this.presenter = new Presenter(this); + this.frame = new JFrame("7Units " + ProgramInfo.VERSION); + this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + // master components (those that contain everything else within them) + this.masterPane = new JTabbedPane(); + this.frame.add(this.masterPane); + + // ============ UNIT CONVERSION TAB ============ + final JPanel convertUnitPanel = new JPanel(); + this.masterPane.addTab("Convert Units", convertUnitPanel); + this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); + convertUnitPanel.setLayout(new BorderLayout()); + + { // panel for input part + final JPanel inputPanel = new JPanel(); + convertUnitPanel.add(inputPanel, BorderLayout.CENTER); + inputPanel.setLayout(new GridLayout(1, 3)); + inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6)); + + this.fromSearch = new SearchBoxList<>(); + inputPanel.add(this.fromSearch); + + final JPanel inBetweenPanel = new JPanel(); + inputPanel.add(inBetweenPanel); + inBetweenPanel.setLayout(new BorderLayout()); + + this.dimensionSelector = new JComboBox<>(); + inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START); + this.dimensionSelector + .addItemListener(e -> this.presenter.applyDimensionFilter()); + + 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 JFormattedTextField(NUMBER_FORMATTER); + outputPanel.add(this.valueInput, BorderLayout.CENTER); + + // conversion button + final JButton convertButton = new JButton("Convert"); + outputPanel.add(convertButton, BorderLayout.LINE_END); + convertButton.addActionListener(e -> this.presenter.convertUnits()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + + // conversion output + this.unitOutput = new JTextArea(2, 32); + outputPanel.add(this.unitOutput, BorderLayout.PAGE_END); + this.unitOutput.setEditable(false); + } + + // ============ EXPRESSION CONVERSION TAB ============ + final JPanel convertExpressionPanel = new JPanel(); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); + this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); + convertExpressionPanel.setLayout(new GridLayout(4, 1)); + + // from and to expressions + this.fromEntry = new JTextField(); + convertExpressionPanel.add(this.fromEntry); + this.fromEntry.setBorder(BorderFactory.createTitledBorder("From")); + + this.toEntry = new JTextField(); + convertExpressionPanel.add(this.toEntry); + this.toEntry.setBorder(BorderFactory.createTitledBorder("To")); + + // button to convert + final JButton convertButton = new JButton("Convert"); + convertExpressionPanel.add(convertButton); + + convertButton.addActionListener(e -> this.presenter.convertExpressions()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + + // output of conversion + this.expressionOutput = new JTextArea(2, 32); + convertExpressionPanel.add(this.expressionOutput); + this.expressionOutput + .setBorder(BorderFactory.createTitledBorder("Output")); + this.expressionOutput.setEditable(false); + + // =========== UNIT VIEWER =========== + final JPanel unitLookupPanel = new JPanel(); + this.masterPane.addTab("Unit Viewer", unitLookupPanel); + this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); + unitLookupPanel.setLayout(new GridLayout()); + + this.unitNameList = new SearchBoxList<>(); + unitLookupPanel.add(this.unitNameList); + this.unitNameList.getSearchList() + .addListSelectionListener(e -> this.presenter.unitNameSelected()); + + // the text box for unit's toString + this.unitTextBox = new JTextArea(); + unitLookupPanel.add(this.unitTextBox); + this.unitTextBox.setEditable(false); + this.unitTextBox.setLineWrap(true); + + // ============ PREFIX VIEWER ============= + final JPanel prefixLookupPanel = new JPanel(); + this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); + this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); + prefixLookupPanel.setLayout(new GridLayout(1, 2)); + + this.prefixNameList = new SearchBoxList<>(); + prefixLookupPanel.add(this.prefixNameList); + this.prefixNameList.getSearchList() + .addListSelectionListener(e -> this.presenter.prefixSelected()); + + // the text box for prefix's toString + this.prefixTextBox = new JTextArea(); + prefixLookupPanel.add(this.prefixTextBox); + this.prefixTextBox.setEditable(false); + this.prefixTextBox.setLineWrap(true); + + 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.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(); + + 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()); + } + + // ============ 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.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()); + } + + return settingsPanel; + } + + @Override + public Set<NamedObjectProduct<BaseDimension>> getDimensions() { + return Collections + .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector)); + } + + @Override + public String getFromExpression() { + return this.fromEntry.getText(); + } + + @Override + public Optional<Unit> getFromSelection() { + return this.fromSearch.getSelectedValue(); + } + + @Override + public String getInputValue() { + return this.valueInput.getText(); + } + + @Override + public Optional<? extends ObjectProduct<BaseDimension>> getSelectedDimension() { + // this must work because this function can only return items that are in + // the selector, which are all of type ObjectProduct<BaseDimension> + @SuppressWarnings("unchecked") + final ObjectProduct<BaseDimension> selectedItem = (ObjectProduct<BaseDimension>) this.dimensionSelector + .getSelectedItem(); + return Optional.ofNullable(selectedItem); + } + + @Override + public String getToExpression() { + return this.toEntry.getText(); + } + + @Override + public Optional<Unit> getToSelection() { + return this.toSearch.getSelectedValue(); + } + + @Override + public void setDimensions( + Set<NamedObjectProduct<BaseDimension>> dimensions) { + this.dimensionSelector.removeAllItems(); + for (final NamedObjectProduct<BaseDimension> d : dimensions) { + this.dimensionSelector.addItem(d); + } + } + + @Override + public void setFromUnits(Set<? extends Unit> units) { + this.fromSearch.setItems(units); + } + + @Override + public void setToUnits(Set<? extends Unit> units) { + this.toSearch.setItems(units); + } + + @Override + public void showErrorMessage(String title, String message) { + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); + } + + @Override + public void showExpressionConversionOutput(String fromExpression, + String toExpression, double value) { + this.expressionOutput.setText( + String.format("%s = %s %s", fromExpression, value, toExpression)); + } + + @Override + public void showUnitConversionOutput(String outputString) { + this.unitOutput.setText(outputString); + } + +} diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 97ec30f..5fd5a82 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -16,8 +16,8 @@ */ package sevenUnitsGUI; -import java.util.List; import java.util.Optional; +import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; @@ -35,7 +35,7 @@ public interface UnitConversionView extends View { * @return dimensions available for filtering * @since 2022-01-29 */ - List<NamedObjectProduct<BaseDimension>> getDimensions(); + Set<NamedObjectProduct<BaseDimension>> getDimensions(); /** * @return unit to convert <em>from</em> @@ -68,7 +68,7 @@ public interface UnitConversionView extends View { * @param dimensions dimensions to use * @since 2021-12-15 */ - void setDimensions(List<NamedObjectProduct<BaseDimension>> dimensions); + void setDimensions(Set<NamedObjectProduct<BaseDimension>> dimensions); /** * Sets the available units to convert from. {@link #getFromSelection} is not @@ -78,7 +78,7 @@ public interface UnitConversionView extends View { * @param units units to convert from * @since 2021-12-15 */ - void setFromUnits(List<? extends Unit> units); + void setFromUnits(Set<? extends Unit> units); /** * Sets the available units to convert to. {@link #getToSelection} is not @@ -88,7 +88,7 @@ public interface UnitConversionView extends View { * @param units units to convert to * @since 2021-12-15 */ - void setToUnits(List<? extends Unit> units); + void setToUnits(Set<? extends Unit> units); /** * Shows the output of a unit conversion. diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index cc070e2..0c0d189 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; @@ -38,7 +39,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final Presenter presenter; /** The dimensions available to select from */ - private List<NamedObjectProduct<BaseDimension>> dimensions; + private Set<NamedObjectProduct<BaseDimension>> dimensions; /** The expression in the From field */ private String fromExpression; /** The expression in the To field */ @@ -54,9 +55,9 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** The currently selected dimension */ private Optional<? extends ObjectProduct<BaseDimension>> selectedDimension; /** The units available in the From selection */ - private List<? extends Unit> fromUnits; + private Set<? extends Unit> fromUnits; /** The units available in the To selection */ - private List<? extends Unit> toUnits; + private Set<? extends Unit> toUnits; /** Saved output values of all unit conversions */ private List<String> unitConversionOutputValues; @@ -74,7 +75,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @since 2022-01-29 */ @Override - public List<NamedObjectProduct<BaseDimension>> getDimensions() { + public Set<NamedObjectProduct<BaseDimension>> getDimensions() { return this.dimensions; } @@ -96,8 +97,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in From * @since 2022-01-29 */ - public List<Unit> getFromUnits() { - return Collections.unmodifiableList(this.fromUnits); + public Set<Unit> getFromUnits() { + return Collections.unmodifiableSet(this.fromUnits); } @Override @@ -132,8 +133,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in To * @since 2022-01-29 */ - public List<Unit> getToUnits() { - return Collections.unmodifiableList(this.toUnits); + public Set<Unit> getToUnits() { + return Collections.unmodifiableSet(this.toUnits); } /** @@ -146,7 +147,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void setDimensions( - List<NamedObjectProduct<BaseDimension>> dimensions) { + Set<NamedObjectProduct<BaseDimension>> dimensions) { this.dimensions = Objects.requireNonNull(dimensions, "dimensions may not be null"); } @@ -181,7 +182,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public void setFromUnits(List<? extends Unit> units) { + public void setFromUnits(Set<? extends Unit> units) { this.fromUnits = Objects.requireNonNull(units, "units may not be null"); } @@ -233,7 +234,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public void setToUnits(List<? extends Unit> units) { + public void setToUnits(Set<? extends Unit> units) { this.toUnits = Objects.requireNonNull(units, "units may not be null"); } diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt index f175396..7780db3 100644 --- a/src/main/resources/about.txt +++ b/src/main/resources/about.txt @@ -2,7 +2,7 @@ About 7Units v[VERSION] Copyright Notice: -Unit Converter Copyright (C) 2018-2021 Adrien Hopkins +Unit Converter Copyright (C) 2018-2022 Adrien Hopkins This program comes with ABSOLUTELY NO WARRANTY; for details read the LICENSE file, section 15 diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 675e3ab..3e7c2b5 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -36,9 +37,9 @@ import sevenUnits.utils.NamedObjectProduct; * @since 2022-02-10 */ public final class PresenterTest { - List<Unit> testUnits = List.of(Metric.METRE, Metric.KILOMETRE, + Set<Unit> testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); - List<NamedObjectProduct<BaseDimension>> testDimensions = List.of( + Set<NamedObjectProduct<BaseDimension>> testDimensions = Set.of( Metric.Dimensions.LENGTH.withName(NameSymbol.ofName("Length")), Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); @@ -56,12 +57,13 @@ public final class PresenterTest { viewBot.setFromUnits(this.testUnits); viewBot.setToUnits(this.testUnits); viewBot.setDimensions(this.testDimensions); - viewBot.setSelectedDimension(Optional.of(this.testDimensions.get(0))); + viewBot.setSelectedDimension( + Optional.of(this.testDimensions.iterator().next())); // filter to length units only, then get the filtered sets of units presenter.applyDimensionFilter(); - final List<Unit> fromUnits = viewBot.getFromUnits(); - final List<Unit> toUnits = viewBot.getToUnits(); + final Set<Unit> fromUnits = viewBot.getFromUnits(); + final Set<Unit> toUnits = viewBot.getToUnits(); // test that fromUnits/toUnits is [METRE, KILOMETRE] // HOWEVER I don't care about the order so I'm testing it this way |