From e0c5021a9ba85debf0c0722d78f75a0dbcc8376b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Apr 2019 12:17:14 -0400 Subject: Implemented the dimension-based converter. Also added a search box list, and fixed a bug with dimension exponentiation. --- src/org/unitConverter/UnitsDatabase.java | 2 +- .../converterGUI/DelegateListModel.java | 10 + .../converterGUI/MutablePredicate.java | 60 ++++++ .../unitConverter/converterGUI/SearchBoxList.java | 205 +++++++++++++++++++++ .../converterGUI/UnitConverterGUI.java | 180 ++++++++++++++---- 5 files changed, 419 insertions(+), 38 deletions(-) create mode 100644 src/org/unitConverter/converterGUI/MutablePredicate.java create mode 100644 src/org/unitConverter/converterGUI/SearchBoxList.java (limited to 'src') diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java index 626f145..c3d3131 100755 --- a/src/org/unitConverter/UnitsDatabase.java +++ b/src/org/unitConverter/UnitsDatabase.java @@ -65,7 +65,7 @@ public final class UnitsDatabase { final double exponent = exponentUnit.getConversionFactor(); if (DecimalComparison.equals(exponent % 1, 0)) // then exponentiate - return base.toExponent((int) (exponent % 1 + 0.5)); + return base.toExponent((int) (exponent + 0.5)); else // not an integer throw new UnsupportedOperationException("Decimal exponents are currently not supported."); diff --git a/src/org/unitConverter/converterGUI/DelegateListModel.java b/src/org/unitConverter/converterGUI/DelegateListModel.java index e375126..b80f63d 100755 --- a/src/org/unitConverter/converterGUI/DelegateListModel.java +++ b/src/org/unitConverter/converterGUI/DelegateListModel.java @@ -16,6 +16,7 @@ */ package org.unitConverter.converterGUI; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -49,6 +50,15 @@ final class DelegateListModel extends AbstractListModel implements List */ private final List delegate; + /** + * Creates an empty {@code DelegateListModel}. + * + * @since 2019-04-13 + */ + public DelegateListModel() { + this(new ArrayList<>()); + } + /** * Creates the {@code DelegateListModel}. * diff --git a/src/org/unitConverter/converterGUI/MutablePredicate.java b/src/org/unitConverter/converterGUI/MutablePredicate.java new file mode 100644 index 0000000..157903c --- /dev/null +++ b/src/org/unitConverter/converterGUI/MutablePredicate.java @@ -0,0 +1,60 @@ +/** + * 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 . + */ +package org.unitConverter.converterGUI; + +import java.util.function.Predicate; + +/** + * A container for a predicate, which can be changed later. + * + * @author Adrien Hopkins + * @since 2019-04-13 + */ +final class MutablePredicate implements Predicate { + private Predicate predicate; + + /** + * Creates the {@code MutablePredicate}. + * + * @since 2019-04-13 + */ + public MutablePredicate(final Predicate predicate) { + this.predicate = predicate; + } + + /** + * @return predicate + * @since 2019-04-13 + */ + public final Predicate getPredicate() { + return this.predicate; + } + + /** + * @param predicate + * new value of predicate + * @since 2019-04-13 + */ + public final void setPredicate(final Predicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean test(final T t) { + return this.predicate.test(t); + } +} diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java new file mode 100644 index 0000000..7d3b748 --- /dev/null +++ b/src/org/unitConverter/converterGUI/SearchBoxList.java @@ -0,0 +1,205 @@ +/** + * 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 . + */ +package org.unitConverter.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.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 + */ +final class SearchBoxList extends JPanel { + + /** + * @since 2019-04-13 + */ + private static final long serialVersionUID = 6226930279415983433L; + + /** + * The text to place in an empty search box. + */ + private static final String EMPTY_TEXT = "Search..."; + /** + * The color to use for an empty foreground. + */ + private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); + + // the components + private final Collection itemsToFilter; + private final DelegateListModel listModel; + private final JTextField searchBox; + private final JList 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 searchFilter = o -> true; + + /** + * Creates the {@code SearchBoxList}. + * + * @since 2019-04-13 + */ + public SearchBoxList(final Collection itemsToFilter) { + super(new BorderLayout(), true); + this.itemsToFilter = itemsToFilter; + + // 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 + */ + public void addSearchFilter(final Predicate filter) { + this.searchFilter = this.searchFilter.and(filter); + } + + /** + * Resets the search filter. + * + * @since 2019-04-13 + */ + public void clearSearchFilters() { + this.searchFilter = o -> true; + } + + /** + * @return value selected in item list + * @since 2019-04-13 + */ + public String getSelectedValue() { + return this.searchItems.getSelectedValue(); + } + + /** + * Re-applies the filters. + * + * @since 2019-04-13 + */ + public void reapplyFilter() { + final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText); + + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + if (string.toLowerCase().contains(searchText.toLowerCase())) { + this.listModel.add(string); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.searchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } + + /** + * Runs whenever the search box gains focus. + * + * @param e + * focus event + * @since 2019-04-13 + */ + 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 + */ + private void searchBoxFocusLost(final FocusEvent e) { + this.searchBoxFocused = false; + if (this.searchBoxEmpty) { + this.searchBox.setText(EMPTY_TEXT); + this.searchBox.setForeground(EMPTY_FOREGROUND); + } + } + + 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); + + // initialize list with items that match the filter then sort + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + if (string.toLowerCase().contains(searchText.toLowerCase())) { + this.listModel.add(string); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.searchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } +} diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 4f5ebeb..9314510 100755 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -25,6 +25,7 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import javax.swing.BorderFactory; @@ -116,9 +117,7 @@ final class UnitConverterGUI { this.units.addDimension("LENGTH", StandardDimensions.LENGTH); this.units.addDimension("MASS", StandardDimensions.MASS); this.units.addDimension("TIME", StandardDimensions.TIME); - this.units.addDimension("ELECTRIC_CURRENT", StandardDimensions.ELECTRIC_CURRENT); this.units.addDimension("TEMPERATURE", StandardDimensions.TEMPERATURE); - this.units.addDimension("QUANTITY", StandardDimensions.QUANTITY); this.units.addDimension("LUMINOUS_INTENSITY", StandardDimensions.LUMINOUS_INTENSITY); this.units.loadUnitsFile(new File("unitsfile.txt")); @@ -238,6 +237,52 @@ final class UnitConverterGUI { this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString)); } + /** + * Converts in the dimension-based converter + * + * @since 2019-04-13 + */ + 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.units.getUnit(fromSelection); + final Unit to = this.units.getUnit(toSelection); + + final String input = this.view.getDimensionBasedInput(); + if (input.equals("")) { + this.view.showErrorDialog("Error", "No value to convert entered."); + return; + } + final double beforeValue = Double.parseDouble(input); + final double value = to.convertFromBase(from.convertToBase(beforeValue)); + + // round value + final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures)); + String output = bigValue.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); + } + } + + this.view.setDimensionBasedOutputText( + String.format("%s %s = %s %s", input, fromSelection, output, toSelection)); + } + /** * @return a list of all of the unit dimensions * @since 2019-04-13 @@ -365,6 +410,23 @@ final class UnitConverterGUI { this.unitNamesFiltered.sort(new FilterComparator(filter)); } + /** + * 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 + */ + public boolean unitMatchesDimension(final String unitName, final String dimensionName) { + final Unit unit = this.units.getUnit(unitName); + final UnitDimension dimension = this.units.getDimension(dimensionName); + return unit.getDimension().equals(dimension); + } + /** * Runs whenever a unit is selected in the viewer. *

@@ -385,6 +447,10 @@ final class UnitConverterGUI { this.view.setUnitTextBoxText(unit.toString()); } } + + public final Set unitNameSet() { + return this.units.prefixlessUnitNameSet(); + } } private static class View { @@ -405,6 +471,14 @@ final class UnitConverterGUI { private final JTextField prefixFilterEntry; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; + /** 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 panel for inputting values in the dimension-based converter */ + private final JTextField valueInput; + /** The output area in the dimension-based converter */ + private final JTextArea dimensionBasedOutput; /** The "From" entry in the conversion panel */ private final JTextField fromEntry; /** The "To" entry in the conversion panel */ @@ -430,6 +504,10 @@ final class UnitConverterGUI { this.unitTextBox = new JTextArea(); this.prefixFilterEntry = new JTextField(); this.prefixTextBox = new JTextArea(); + this.fromSearch = new SearchBoxList(this.presenter.unitNameSet()); + this.toSearch = new SearchBoxList(this.presenter.unitNameSet()); + this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################")); + this.dimensionBasedOutput = new JTextArea(2, 32); this.fromEntry = new JTextField(); this.toEntry = new JTextField(); this.output = new JTextArea(2, 32); @@ -440,6 +518,22 @@ final class UnitConverterGUI { this.frame.pack(); } + /** + * @return value in dimension-based converter + * @since 2019-04-13 + */ + public String getDimensionBasedInput() { + return this.valueInput.getText(); + } + + /** + * @return selection in "From" selector in dimension-based converter + * @since 2019-04-13 + */ + public String getFromSelection() { + return this.fromSearch.getSelectedValue(); + } + /** * @return text in "From" box in converter panel * @since 2019-01-15 @@ -467,6 +561,14 @@ final class UnitConverterGUI { return this.prefixNameList.getSelectedIndex(); } + /** + * @return selection in "To" selector in dimension-based converter + * @since 2019-04-13 + */ + public String getToSelection() { + return this.toSearch.getSelectedValue(); + } + /** * @return text in "To" box in converter panel * @since 2019-01-26 @@ -531,22 +633,17 @@ final class UnitConverterGUI { inputPanel.setLayout(new GridLayout(1, 3)); - { // panel for From things - final JPanel fromPanel = new JPanel(); - inputPanel.add(fromPanel); + final JComboBox dimensionSelector = new JComboBox<>( + this.presenter.dimensionNameList().toArray(new String[0])); + dimensionSelector.setSelectedItem("LENGTH"); - fromPanel.setLayout(new BorderLayout()); + // handle dimension filter + final MutablePredicate dimensionFilter = new MutablePredicate<>(s -> true); - { // search box for from - final JTextField fromSearch = new JTextField("Search..."); - fromPanel.add(fromSearch, BorderLayout.PAGE_START); - } + // panel for From things + inputPanel.add(this.fromSearch); - { // list for From units - final JList fromList = new JList<>(); - fromPanel.add(fromList, BorderLayout.CENTER); - } - } + this.fromSearch.addSearchFilter(dimensionFilter); { // for dimension selector and arrow that represents conversion final JPanel inBetweenPanel = new JPanel(); @@ -555,10 +652,6 @@ final class UnitConverterGUI { inBetweenPanel.setLayout(new BorderLayout()); { // dimension selector - final List dimensionNameList = this.presenter.dimensionNameList(); - dimensionNameList.add(0, "Select a dimension..."); - final JComboBox dimensionSelector = new JComboBox<>( - dimensionNameList.toArray(new String[0])); inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); } @@ -568,23 +661,25 @@ final class UnitConverterGUI { } } - { // panel for To things - final JPanel toPanel = new JPanel(); - inputPanel.add(toPanel); + // panel for To things - toPanel.setLayout(new BorderLayout()); + inputPanel.add(this.toSearch); - { // search box for to - final JTextField toSearch = new JTextField("Search..."); - toPanel.add(toSearch, BorderLayout.PAGE_START); - } + this.toSearch.addSearchFilter(dimensionFilter); - { // list for To units - final JList toList = new JList<>(); - toPanel.add(toList, BorderLayout.CENTER); - } - } + // 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 @@ -605,20 +700,20 @@ final class UnitConverterGUI { } { // value to convert - final JTextField valueInput = new JFormattedTextField( - new DecimalFormat("###############0.################")); - valueInputPanel.add(valueInput, BorderLayout.CENTER); + valueInputPanel.add(this.valueInput, BorderLayout.CENTER); } } { // button to convert final JButton convertButton = new JButton("Convert"); outputPanel.add(convertButton); + + convertButton.addActionListener(e -> this.presenter.convertDimensionBased()); } { // output of conversion - final JLabel outputLabel = new JLabel(); - outputPanel.add(outputLabel); + outputPanel.add(this.dimensionBasedOutput); + this.dimensionBasedOutput.setEditable(false); } } } @@ -762,6 +857,17 @@ final class UnitConverterGUI { } } + /** + * Sets the text in the output of the dimension-based converter. + * + * @param text + * text to set + * @since 2019-04-13 + */ + public void setDimensionBasedOutputText(final String text) { + this.dimensionBasedOutput.setText(text); + } + /** * Sets the text in the output of the conversion panel. * -- cgit v1.2.3