/*
* Copyright © 2010 Martin Riedel
*
* This file is part of TransFile.
*
* TransFile is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TransFile 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TransFile. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sourceforge.transfile.ui.swing;
import static net.sourceforge.jenerics.Tools.getLoggerForThisMethod;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import javax.swing.AbstractListModel;
import javax.swing.JComboBox;
import javax.swing.MutableComboBoxModel;
import net.sourceforge.transfile.TransFile;
import net.sourceforge.transfile.exceptions.SerializationException;
import net.sourceforge.transfile.exceptions.SerializationFileInUseException;
import net.sourceforge.jenerics.filesystem.FileSystemTools;
/**
* The text bar where the user enters the "PeerURL" of the peer they wish to connect to for file transfer.
*
* PeerURLBar is a lazy-loaded singleton. It must be ensured at all times that there is at most one instance
* of PeerURLBar to avoid persistence / serialization conflicts.
*
* @author Martin Riedel
*
*/
class PeerURLBar extends JComboBox {
/*
* The maximum number of items in the drop-down menu. Requesting to add another item
* when the PeerURLBar already has maxRetainedItems items will cause the oldest present
* item to be deleted. Then, the remaining 4 present items will be shifted one slot towards
* the "older" end of the list, so that the previously second oldest item is now the oldest one.
* Finally, the item whose addition was requested is inserted into the now free spot at the
* "youngest" end of the list.
*
* Typically, entering a (valid) PeerURL and pressing enter causes such a request to add for an item
* to be added, with said item being the PeerURL entered.
*/
public final int maxRetainedItems;
private static final long serialVersionUID = -8782347394069390311L;
/*
* The file the data model will be serialized and saved to to achieve persistence
*/
private final File stateFile;
/*
* A reference to the data model used by the PeerURLBar
*/
private PeerURLBarModel model;
/*
* A Set of all state files already in use by instances of this class
*/
private static final Set<File> usedStateFiles = new HashSet<File>();
/*
* True iff state should be saved to disk when #saveModel is called
*/
private final boolean persistent;
/**
* Constructs a new instance, loading state from the specified file. State will be saved upon
* request ({@link #saveModel}).
*
* @param stateFileName
* <br />File name (not path) of the file load state from and to save state to
* <br />Should not be null
* @param maxRetainedItems
* <br />The number of recent values to remember
* <br />Should be at least 1
* <br />Should not be null
* @throws SerializationFileInUseException if the specified file name is already in use
*
*/
public PeerURLBar(final String stateFileName, final int maxRetainedItems) throws SerializationFileInUseException {
this.stateFile = new File(FileSystemTools.getUserApplicationDirectory(TransFile.USER_APPLICATION_DIRECTORY_NAME), stateFileName);
if (usedStateFiles.contains(this.stateFile))
throw new SerializationFileInUseException(this.stateFile);
this.persistent = true;
this.maxRetainedItems = maxRetainedItems;
usedStateFiles.add(this.stateFile);
setup();
}
/**
* Constructs a new instance without loading state. State will NOT be saved to,
* even if {@link #saveModel} is called.
*
* @param maxRetainedItems
* <br />The number of recent values to remember
* <br />Should be at least 1
* <br />Should not be null
*/
public PeerURLBar(final int maxRetainedItems) {
this.stateFile = null;
this.persistent = false;
this.maxRetainedItems = maxRetainedItems;
setup();
}
/**
* Saves the PeerURLBar's data state to disk. Does nothing if this PeerURLBar instance has been
* created without setting a state file.
*
* @throws SerializationException if serializing or saving the serialized data to disk failed
*/
public void saveModel() throws SerializationException {
if (this.persistent)
this.model.saveHolder();
}
/**
* Checks whether this PeerURLBar instance is persistent (saves state to file upon request)
*
* @return true iff this PeerURLBar instance is persistent (saves state to file upon request)
*/
public boolean isPersistent() {
return this.persistent;
}
/**
* Getter for {@code stateFile}
*
* @return the file serialized versions of PeerURLBar's state are written to
*/
protected final File getStateFile() {
return this.stateFile;
}
private void setup() {
setEditable(true);
addActionListener(new PeerURLBarListener());
this.model = new PeerURLBarModel();
setModel(this.model);
}
/**
* Listens for events in the PeerURLBar.
*
* @author Martin Riedel
*
*/
private class PeerURLBarListener implements ActionListener {
/**
* Constructs a new instance
*
*/
public PeerURLBarListener() {
// do nothing, just allow instantiation
}
/**
* {@inheritDoc}
*/
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("comboBoxEdited"))
addItem(getSelectedItem());
}
}
/**
* Implements the PeerURLBar's data model.
*
* @author Martin Riedel
*
*/
private class PeerURLBarModel extends AbstractListModel implements MutableComboBoxModel {
private static final long serialVersionUID = -7254391160953049844L;
/*
* Holds the items in the PeerURLBar's drop-down menu
*
* Invariants:
* holder.items.get(0) is the youngest (most recently added) item in the list
* holder.items.get(maxRetainedItems - 1) is the oldest item in the list
*/
private ComboBoxItemsHolder holder;
/**
* Constructs a new PeerURLBarModel, attempting to load a previously serialized ItemsHolder
* instance from disk, or, failing that, creating a new ItemsHolder object.
*
*/
public PeerURLBarModel() {
if (PeerURLBar.this.isPersistent()) {
try {
getLoggerForThisMethod().log(Level.FINER, "attempting to load PeerURLBar state from file: " + PeerURLBar.this.getStateFile().getAbsolutePath());
this.holder = ComboBoxItemsHolder.load(PeerURLBar.this.getStateFile());
// maxRetainedItems may have changed since the last time state was saved
removeExcessiveItems();
getLoggerForThisMethod().log(Level.FINE, "successfully loaded PeerURLBar state from file: " + PeerURLBar.this.getStateFile().getAbsolutePath());
} catch (Throwable e) {
getLoggerForThisMethod().log(Level.WARNING, "failed to load PeerURLBar state from file: " + PeerURLBar.this.getStateFile().getAbsolutePath());
this.holder = new ComboBoxItemsHolder(PeerURLBar.this.maxRetainedItems, PeerURLBar.this.getStateFile());
}
} else {
getLoggerForThisMethod().log(Level.FINE, "not loading PeerURLBar state from file, initializing empty model");
this.holder = new ComboBoxItemsHolder(PeerURLBar.this.maxRetainedItems, null);
}
}
/**
* Saves the state of the ComboBoxItemsHolder to disk
*
* @throws SerializationException if serializing or saving the serialized data to disk failed
*/
public void saveHolder() throws SerializationException {
this.holder.save();
}
/**
* Inserts the provided element as the first element. If the model already contains {@code maxRetainedItems} elements,
* the last (oldest) element in the model is removed. Does not set or change the selected item. Ignores attempts to
* add duplicate elements without throwing an exception.
*
* @param e the element to insert
*/
@Override
public void addElement(final Object e) {
if (this.holder.items.contains(e))
return;
this.holder.items.add(0, e);
removeExcessiveItems();
fireContentsChanged(PeerURLBar.this, 0, this.holder.items.size() - 1);
}
/**
* Operation not supported
*
*/
@Override
public void insertElementAt(final Object e, final int ePosition) {
throw new UnsupportedOperationException(this.getClass().getSimpleName() + ".insertElementAt(Object, int)");
}
/**
* Operation not supported
*
*/
@Override
public void removeElement(final Object e) {
throw new UnsupportedOperationException(this.getClass().getSimpleName() + ".removeElement(Object)");
}
/**
* Operation not supported
*
*/
@Override
public void removeElementAt(final int ePosition) {
throw new UnsupportedOperationException(this.getClass().getSimpleName() + ".removeElementAt(int)");
}
/**
* {@inheritDoc}
*/
@Override
public Object getSelectedItem() {
return this.holder.selectedItem;
}
/**
* {@inheritDoc}
*/
@Override
public void setSelectedItem(final Object itemToSelect) {
this.holder.selectedItem = itemToSelect;
int i = this.holder.items.indexOf(itemToSelect);
// if the selected item already exists (user picked it from the drop-down list or re-entered it)
if (i >= 0) {
// make the selected item the "youngest" item (move it to the first position in the list)
this.holder.items.remove(i);
this.holder.items.add(0, itemToSelect);
}
fireContentsChanged(PeerURLBar.this, 0, this.holder.items.size() - 1);
}
/**
* {@inheritDoc}
*/
@Override
public Object getElementAt(final int index) {
return this.holder.items.get(index);
}
/**
* {@inheritDoc}
*/
@Override
public int getSize() {
return this.holder.items.size();
}
/**
* Removes excessive items until the model is holding at most {@code maxRetainedItems} items
*
*/
private void removeExcessiveItems() {
while (this.holder.items.size() > PeerURLBar.this.maxRetainedItems)
this.holder.items.remove(this.holder.items.size() - 1);
}
}
}