/** * 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 . */ 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.HashSet; 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; /** * A View that separates its functions into multiple tabs * * @since 2022-02-19 */ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** * A Set-like view of a JComboBox's items * * @param type of item in list * * @since 2022-02-19 */ private static final class JComboBoxItemSet extends AbstractSet { private final JComboBox comboBox; /** * @param comboBox combo box to get items from * @since 2022-02-19 */ public JComboBoxItemSet(JComboBox comboBox) { this.comboBox = comboBox; } @Override public Iterator 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 dimensionSelector; /** The panel for inputting values in the dimension-based converter */ private final JFormattedTextField valueInput; /** The panel for "From" in the dimension-based converter */ private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ private final SearchBoxList toSearch; /** The output area in the dimension-based converter */ private final JTextArea 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 unitNameList; /** The searchable list of prefix names in the prefix viewer */ private final SearchBoxList prefixNameList; /** The text box for unit data in the unit viewer */ private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; /** * Creates the 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.updateView()); final JLabel arrowLabel = new JLabel("-->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); arrowLabel.setHorizontalAlignment(SwingConstants.CENTER); this.toSearch = new SearchBoxList<>(); inputPanel.add(this.toSearch); } { // panel for submit and output, and also value entry final JPanel outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); outputPanel.setLayout(new BorderLayout()); outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6)); final JLabel valuePrompt = new JLabel("Value to convert: "); outputPanel.add(valuePrompt, BorderLayout.LINE_START); this.valueInput = new 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.presenter.postViewInitialize(); this.frame.pack(); this.frame.setVisible(true); } /** * Creates and returns the settings panel (in its own function to make this * code more organized, as this function is massive!) * * @since 2022-02-19 */ private JPanel createSettingsPanel() { final JPanel settingsPanel = new JPanel(); settingsPanel .setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); // ============ ROUNDING SETTINGS ============ { final JPanel roundingPanel = new JPanel(); settingsPanel.add(roundingPanel); roundingPanel.setBorder(new TitledBorder("Rounding Settings")); roundingPanel.setLayout(new GridBagLayout()); // rounding rule selection final ButtonGroup roundingRuleButtons = new ButtonGroup(); 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 getDimensionNames() { return Collections .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector)); } @Override public String getFromExpression() { return this.fromEntry.getText(); } @Override public Optional getFromSelection() { return this.fromSearch.getSelectedValue(); } @Override public Set getFromUnitNames() { // this should work because the only way I can mutate the item list is // with setFromUnits which only accepts a Set return new HashSet<>(this.fromSearch.getItems()); } @Override public String getInputValue() { return this.valueInput.getText(); } @Override public Optional getSelectedDimensionName() { final String selectedItem = (String) this.dimensionSelector .getSelectedItem(); return Optional.ofNullable(selectedItem); } @Override public String getToExpression() { return this.toEntry.getText(); } @Override public Optional getToSelection() { return this.toSearch.getSelectedValue(); } @Override public Set getToUnitNames() { // this should work because the only way I can mutate the item list is // with setToUnits which only accepts a Set return new HashSet<>(this.toSearch.getItems()); } @Override public void setDimensionNames(Set dimensionNames) { this.dimensionSelector.removeAllItems(); for (final String d : dimensionNames) { this.dimensionSelector.addItem(d); } } @Override public void setFromUnitNames(Set units) { this.fromSearch.setItems(units); } @Override public void setToUnitNames(Set 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(UnitConversionRecord uc) { this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(), uc.outputValueString(), uc.toName())); } @Override public void showUnitConversionOutput(UnitConversionRecord uc) { this.unitOutput.setText(uc.toString()); } }