From 2eee97c9e64dca79fc6b1614b304b398d25a7f4b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 27 Mar 2021 16:36:39 -0500 Subject: Added automatic building with Gradle --- .../unitConverter/converterGUI/SearchBoxList.java | 307 +++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 src/main/java/org/unitConverter/converterGUI/SearchBoxList.java (limited to 'src/main/java/org/unitConverter/converterGUI/SearchBoxList.java') diff --git a/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java new file mode 100644 index 0000000..10ef589 --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java @@ -0,0 +1,307 @@ +/** + * 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.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 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 customSearchFilter = o -> true; + private final Comparator 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 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 itemsToFilter, + final Comparator defaultOrdering, + final boolean caseSensitive) { + super(new BorderLayout(), true); + this.itemsToFilter = 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 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 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 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 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. + *

+ * Reapplies the search filter, and custom filters. + *

+ * + * @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 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); + } + + /** + * Manually updates the search box's item list. + * + * @since 2020-08-27 + */ + public void updateList() { + this.searchBoxTextChanged(); + } +} -- cgit v1.2.3 From df3651baf72c799339268293fccb75e127bb336c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 22 May 2021 14:51:05 -0500 Subject: Added an option to exclude duplicate units --- .../unitConverter/converterGUI/SearchBoxList.java | 15 ++++++- .../converterGUI/UnitConverterGUI.java | 46 +++++++++++++++++----- .../java/org/unitConverter/unit/UnitDatabase.java | 37 ++++++++++++++++- src/main/resources/unitsfile.txt | 9 ++++- .../org/unitConverter/unit/UnitDatabaseTest.java | 5 ++- 5 files changed, 96 insertions(+), 16 deletions(-) (limited to 'src/main/java/org/unitConverter/converterGUI/SearchBoxList.java') diff --git a/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java index 10ef589..f52d57d 100644 --- a/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java +++ b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java @@ -101,7 +101,7 @@ final class SearchBoxList extends JPanel { final Comparator defaultOrdering, final boolean caseSensitive) { super(new BorderLayout(), true); - this.itemsToFilter = itemsToFilter; + this.itemsToFilter = new ArrayList<>(itemsToFilter); this.defaultOrdering = defaultOrdering; this.caseSensitive = caseSensitive; @@ -296,6 +296,19 @@ final class SearchBoxList extends JPanel { 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 newItems) { + this.itemsToFilter.clear(); + this.itemsToFilter.addAll(newItems); + this.reapplyFilter(); + } + /** * Manually updates the search box's item list. * diff --git a/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java index ee1bcc3..17ec5f9 100644 --- a/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -216,6 +216,9 @@ final class UnitConverterGUI { private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; + // The "include duplicate units" setting + private boolean includeDuplicateUnits = true; + /** * Creates the presenter. * @@ -291,7 +294,7 @@ final class UnitConverterGUI { }; this.unitNames = new ArrayList<>( - this.database.unitMapPrefixless().keySet()); + this.database.unitMapPrefixless(true).keySet()); this.unitNames.sort(null); // sorts it using Comparable this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); @@ -309,10 +312,10 @@ final class UnitConverterGUI { // print out unit counts System.out.printf( "Successfully loaded %d units with %d unit names (%d base units).%n", - new HashSet<>(this.database.unitMapPrefixless().values()).size(), - this.database.unitMapPrefixless().size(), - new HashSet<>(this.database.unitMapPrefixless().values()) - .stream().filter(isFullBase).count()); + this.database.unitMapPrefixless(false).size(), + this.database.unitMapPrefixless(true).size(), + this.database.unitMapPrefixless(false).values().stream() + .filter(isFullBase).count()); } /** @@ -586,6 +589,12 @@ final class UnitConverterGUI { 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); @@ -638,6 +647,8 @@ final class UnitConverterGUI { 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", @@ -646,6 +657,14 @@ final class UnitConverterGUI { } } + public final void setIncludeDuplicateUnits( + boolean includeDuplicateUnits) { + this.includeDuplicateUnits = includeDuplicateUnits; + + this.view.update(); + this.saveSettings(); + } + /** * Enables or disables one-way conversion. * @@ -760,8 +779,9 @@ final class UnitConverterGUI { * @since 2019-04-14 * @since v0.2.0 */ - private final Set unitNameSet() { - return this.database.unitMapPrefixless().keySet(); + public final Set unitNameSet() { + return this.database.unitMapPrefixless(this.includeDuplicateUnits) + .keySet(); } } @@ -1385,9 +1405,11 @@ final class UnitConverterGUI { .setAnchor(GridBagConstraints.LINE_START).build()); final JCheckBox showAllVariations = new JCheckBox( - "Show Symbols in \"Convert Units\""); - showAllVariations.setSelected(true); - showAllVariations.setEnabled(false); + "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()); @@ -1459,6 +1481,10 @@ final class UnitConverterGUI { } 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(); diff --git a/src/main/java/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java index 6322fef..673f119 100644 --- a/src/main/java/org/unitConverter/unit/UnitDatabase.java +++ b/src/main/java/org/unitConverter/unit/UnitDatabase.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -1096,6 +1097,13 @@ public final class UnitDatabase { private static final Pattern NAME_EXPRESSION = Pattern .compile("(\\S+)\\s+(\\S.*)"); + /** + * Like normal string comparisons, but shorter strings are always less than + * longer strings. + */ + private static final Comparator lengthFirstComparator = Comparator + .comparingInt(String::length).thenComparing(Comparator.naturalOrder()); + /** * The exponent operator * @@ -1149,6 +1157,22 @@ public final class UnitDatabase { throw new IllegalArgumentException("Exponents must be numbers."); } + /** + * @return true if entry represents a removable duplicate entry of unitMap. + * @since 2021-05-22 + */ + private static boolean isRemovableDuplicate(Map unitMap, + Entry entry) { + for (final Entry e : unitMap.entrySet()) { + final String name = e.getKey(); + final Unit value = e.getValue(); + if (lengthFirstComparator.compare(entry.getKey(), name) < 0 + && Objects.equals(unitMap.get(entry.getKey()), value)) + return true; + } + return false; + } + /** * The units in this system, excluding prefixes. * @@ -2015,11 +2039,20 @@ public final class UnitDatabase { } /** + * @param includeDuplicates if true, duplicate units will all exist in the + * map; if false, only one of each unit will exist, + * even if the names are different * @return a map mapping unit names to units, ignoring prefixes * @since 2019-04-13 * @since v0.2.0 */ - public Map unitMapPrefixless() { - return Collections.unmodifiableMap(this.prefixlessUnits); + public Map unitMapPrefixless(boolean includeDuplicates) { + if (includeDuplicates) + return Collections.unmodifiableMap(this.prefixlessUnits); + else + return Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(this.prefixlessUnits, + entry -> !isRemovableDuplicate(this.prefixlessUnits, + entry))); } } diff --git a/src/main/resources/unitsfile.txt b/src/main/resources/unitsfile.txt index eafe885..340e8ea 100644 --- a/src/main/resources/unitsfile.txt +++ b/src/main/resources/unitsfile.txt @@ -251,15 +251,22 @@ Calorie kilocalorie Cal Calorie Wh W h -# Extra units to only include in the dimension-based converter +# Extra units to show in the dimension-based converter km km +kilometre km cm cm +centimetre cm mm mm +millimetre mm mg mg +milligram mg mL mL ml ml +millilitre mL kJ kJ +kilojoule kJ MJ MJ +megajoule MJ kWh kWh m/s m / s km/h km / h diff --git a/src/test/java/org/unitConverter/unit/UnitDatabaseTest.java b/src/test/java/org/unitConverter/unit/UnitDatabaseTest.java index f0ba8e5..7f957f3 100644 --- a/src/test/java/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/test/java/org/unitConverter/unit/UnitDatabaseTest.java @@ -134,7 +134,8 @@ class UnitDatabaseTest { @Test public void testPrefixlessUnitMap() { final UnitDatabase database = new UnitDatabase(); - final Map prefixlessUnits = database.unitMapPrefixless(); + final Map prefixlessUnits = database + .unitMapPrefixless(true); database.addUnit("U", U); database.addUnit("V", V); @@ -223,7 +224,7 @@ class UnitDatabaseTest { database.addPrefix("B", B); database.addPrefix("C", C); - final int NUM_UNITS = database.unitMapPrefixless().size(); + final int NUM_UNITS = database.unitMapPrefixless(true).size(); final int NUM_PREFIXES = database.prefixMap().size(); final Iterator nameIterator = database.unitMap().keySet() -- cgit v1.2.3