All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.shredzone.jshred.swing.JHistoryTextField Maven / Gradle / Ivy

/**
 * jshred - Shred's Toolbox
 *
 * Copyright (C) 2009 Richard "Shred" Körber
 *   http://jshred.shredzone.org
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License / GNU Lesser
 * General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * 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.
 *
 */
package net.shredzone.jshred.swing;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JTextField;

/**
 * This is some kind of {@link JTextField} with a {@link JComboBox}. Text can be entered
 * freely into the text field. The class keeps a history of recently entered texts, which
 * can be selected in the {@link JComboBox}. The history is optionally stored as
 * {@link Preferences} if a unique name is given to the constructor.
 *
 * @author Richard "Shred" Körber
 * @since R6
 */
public class JHistoryTextField extends JComponent {
    private static final long serialVersionUID = 3688784791113577272L;

    private JComboBox jCombo;
    private int histSize;
    private boolean autoSelect = true;
    private String nodeName = null;
    private final ListenerManager listeners = new ListenerManager();

    /**
     * Creates a new {@link JHistoryTextField}. The history will only be stored during the
     * lifetime of this object.
     */
    public JHistoryTextField() {
        this(null);
    }

    /**
     * Creates a new {@link JHistoryTextField}. The history will be persistently stored
     * using {@link Preferences}. You must provide a unique name for the history to be
     * stored. It is strongly recommended to use the Java notation for unique names, as
     * used in package names (i.e. your domain name in reverse notation, following a
     * unique string of your choice).
     *
     * @param name
     *            Name which is used to persist the history.
     */
    public JHistoryTextField(String name) {
        nodeName = name;

        // --- Create the GUI ---
        setLayout(new BorderLayout());
        jCombo = new JComboBox();
        jCombo.setEditable(true);
        add(jCombo, BorderLayout.CENTER);

        // --- Set the default history size ---
        setHistorySize(10); // Default is 10 entries

        // --- Recall the history ---
        if (nodeName != null) {
            try {
                recallHistory(nodeName);
            } catch (BackingStoreException e) {
                // We will lose the history now. Anyhow we shouldn't bother
                // the user because he won't be able to do anything about it
                // anyways. So silently ignore the exception.
            }
            jCombo.setSelectedIndex(-1);
        }

        // --- Add an ActionListener ---
        jCombo.getEditor().addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String newtext = jCombo.getEditor().getItem().toString();

                // --- Remember the entry ---
                addHistory(newtext);

                // --- Mark all the text ---
                if (autoSelect) jCombo.getEditor().selectAll();

                // --- Notify everyone about a new text ---
                final ActionEvent e2 = new ActionEvent(JHistoryTextField.this,
                                ActionEvent.ACTION_PERFORMED, "textChanged", e.getWhen(),
                                e.getModifiers());
                fireActionEvent(e2);
            }
        });
    }

    /**
     * Sets the maximum history size. If a new entry would exceed the limit, the least
     * recently used entry will be removed from the history.
     *
     * @param size
     *            New history size
     */
    public void setHistorySize(int size) {
        if (histSize < 0) throw new IllegalArgumentException("size must be positive");

        // --- Remove entries to reach the size ---
        synchronized (jCombo) {
            histSize = size;
            if (histSize > jCombo.getItemCount()) {
                for (int ix = jCombo.getItemCount() - 1; ix >= histSize; ix--) {
                    jCombo.removeItemAt(ix);
                }
            }
        }

        // --- Set Maximum ---
        // Take care that the entire history is visible
        // within a reasonable range.
        jCombo.setMaximumRowCount(Math.min(size, 20));
    }

    /**
     * Gets the current history size. Default is 10.
     *
     * @return Current history size
     */
    public int getHistorySize() {
        return histSize;
    }

    /**
     * Sets the auto selection mode. If enabled, the entire text field will be marked
     * after the user pressed return, so a new text entered will automatically replace the
     * old one. This mode is turned on by default.
     *
     * @param as
     *            Auto selection mode.
     */
    public void setAutoSelection(boolean as) {
        autoSelect = as;
    }

    /**
     * Checks if the auto selection mode is enabled.
     *
     * @return Auto selection mode state.
     */
    public boolean isAutoSelection() {
        return autoSelect;
    }

    /**
     * Sets the text to be shown in this component. The text will also be added to the
     * history.
     *
     * @param text
     *            Text to be set.
     */
    public void setText(String text) {
        synchronized (jCombo) {
            jCombo.setSelectedItem(text);
        }
    }

    /**
     * Gets the text that is currently shown in this component.
     *
     * @return Current text.
     */
    public String getText() {
        synchronized (jCombo) {
            Object item = jCombo.getSelectedItem();
            return (item != null ? item.toString() : "");
        }
    }

    /**
     * Adds a text to the top of the history. If the text was already within the history,
     * it is moved to the top. After that the text will be shown in the component. The
     * current history will be persisted if a name was given to the constructor.
     *
     * @param text
     *            Text to be added to the history.
     */
    protected void addHistory(String text) {
        if (text == null || text.equals("")) return;

        synchronized (jCombo) {
            // --- First look if the entry already exists ---
            int cnt = jCombo.getItemCount();
            for (int ix = 0; ix < cnt; ix++) {
                if (jCombo.getItemAt(ix).equals(text)) {
                    // --- YES: move it to the top ---
                    jCombo.removeItemAt(ix);
                    break;
                }
            }

            // --- Add the new item to the top ---
            jCombo.insertItemAt(text, 0);
            jCombo.setSelectedIndex(0);

            // --- Keep within maximum ---
            while (jCombo.getItemCount() > histSize) {
                jCombo.removeItemAt(jCombo.getItemCount() - 1);
            }

            // --- Store history ---
            if (nodeName != null) {
                try {
                    storeHistory(nodeName);
                } catch (BackingStoreException e) {
                    // We will lose the history now. Anyhow we shouldn't bother
                    // the user because he won't be able to do anything about it
                    // anyhow. So silently ignore the exception.
                }
            }
        }
    }

    /**
     * Gets a {@link List} of the current history. The list is unmodifiable and sorted
     * from the most recent to the least recent history entry.
     *
     * @return {@link List} of all history entries
     */
    public List getHistory() {
        synchronized (jCombo) {
            int cnt = jCombo.getItemCount();
            List lResult = new ArrayList(cnt);
            for (int ix = 0; ix < cnt; ix++) {
                lResult.add(jCombo.getItemAt(ix).toString());
            }
            return Collections.unmodifiableList(lResult);
        }
    }

    /**
     * Gets the current number of history entries.
     *
     * @return Current number of history entries.
     */
    public int getCurrentSize() {
        synchronized (jCombo) {
            return jCombo.getItemCount();
        }
    }

    /**
     * Clears the entire history.
     */
    public void clearHistory() {
        synchronized (jCombo) {
            jCombo.removeAllItems();
        }
    }

    @Override
    public void setEnabled(boolean enabled) {
        jCombo.setEnabled(enabled);
        super.setEnabled(enabled);
    }

    /**
     * Stores the current history to the preferences, using the given name.
     *
     * @param name
     *            Name to file the history to.
     * @throws BackingStoreException
     *             if it was not possible to store the history.
     */
    protected void storeHistory(String name) throws BackingStoreException {
        Preferences prefs = Preferences.userNodeForPackage(this.getClass());
        prefs = prefs.node(name);

        synchronized (jCombo) {
            // --- Clear all old prefs ---
            prefs.clear();

            // --- Write the current history ---
            int cnt = jCombo.getItemCount();
            for (int ix = 0; ix < cnt; ix++) {
                prefs.put(String.valueOf(ix), jCombo.getItemAt(ix).toString());
            }
        }
    }

    /**
     * Recalls the history from preferences, using the given name. The current history
     * will be replaced.
     *
     * @param name
     *            Name the history was filed to.
     * @throws BackingStoreException
     *             if it was not possible to recall the history.
     */
    protected void recallHistory(String name) throws BackingStoreException {
        Preferences prefs = Preferences.userNodeForPackage(this.getClass());
        prefs = prefs.node(name);

        synchronized (jCombo) {
            // --- Clear history ---
            clearHistory();

            // --- Recall the current history ---
            String[] keys = prefs.keys();
            Arrays.sort(keys, new HTFComparator());
            int max = Math.min(keys.length, histSize);
            for (int ix = 0; ix < max; ix++) {
                jCombo.addItem(prefs.get(keys[ix], ""));
            }
        }
    }

    /**
     * Adds an {@link ActionListener}. It will be invoked if a new text has been entered.
     * If it was already added, nothing will happen.
     *
     * @param l
     *            ActionListener to be added.
     */
    public void addActionListener(ActionListener l) {
        listeners.addListener(l);
    }

    /**
     * Removes an ActionListener. If it was not added, nothing will happen.
     *
     * @param l
     *            ActionListener to be removed.
     */
    public void removeActionListener(ActionListener l) {
        listeners.removeListener(l);
    }

    /**
     * Informs all registered ActionListeners about a new ActionEvent.
     *
     * @param e
     *            The ActionEvent to be broadcasted.
     */
    protected void fireActionEvent(ActionEvent e) {
        for (ActionListener l : listeners.getListeners()) {
            l.actionPerformed(e);
        }
    }

    /**
     * This comparator compares two integer values of a string representation.
     */
    private static class HTFComparator implements Comparator {
        @Override
        public int compare(String o1, String o2) {
            int i1 = Integer.parseInt(o1);
            int i2 = Integer.parseInt(o2);
            return i1 - i2;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy