/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander 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.
*
* muCommander 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.autocomplete;
import com.mucommander.ui.autocomplete.completers.Completer;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* AutoCompletionType defines the behaviour of the auto-completion, such as:
* - The key(s) that will open the popup window
* - When should the documentListener be attached to the text component
* - etc..
* This abstract class contains the common things to all CompleterType implementations.
*
* @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion
*/
public abstract class CompletionType {
private Completer completer;
protected AutocompleterTextComponent autocompletedtextComp;
protected DocumentListener documentListener;
protected JList list = new JList();
protected JPopupMenu popup = new JPopupMenu();
protected ShowingThread showingThread;
// Constants:
protected final int VISIBLE_ROW_COUNT = 10;
protected final int POPUP_DELAY_AT_TEXT_INSERTION = 1500;
protected final int POPUP_DELAY_AT_TEXT_DELETION = 500;
protected final int POPUP_DELAY_AFTER_ACCEPTING_LIST_ITEM = 1500;
/**
* ShowingThread is an abstract class for threads that show auto-completion popup
* window after a given delay.
* Each implementation of ShowingThread should implements an abstract function
* "showPopup" that contains the popup opening.
*
* @author Arik Hadas
*/
protected abstract class ShowingThread extends Thread {
protected boolean isStopped;
protected int delayTime;
public ShowingThread(int delayTime) {
isStopped = false;
this.delayTime = delayTime;
}
@Override
public void run() {
// Hide the auto-completion popup window.
hideAutocompletionPopup();
if (!autocompletedtextComp.isShowing() || !autocompletedtextComp.isEnabled())
return;
// Sleep for delayTime milieconds.
delay(delayTime);
// If this thread should stop, finish its execution.
if (isStopped)
return;
// Show auto-completion popup window.
showAutocompletionPopup();
}
/**
* Stop this thread execution.
*/
public void done() {
isStopped = true;
}
/**
* Cause this thread sleep for the given time (in miliseconds).
*/
protected void delay(int miliseconds) {
if (miliseconds > 0) {
try {
Thread.sleep(miliseconds);
} catch (InterruptedException e1) { }
}
}
abstract void showAutocompletionPopup();
}
public CompletionType(AutocompleterTextComponent comp, Completer completer) {
autocompletedtextComp = comp;
this.completer = completer;
// JScrollPane scroll = new JScrollPane(list);
// Disable horizontal scrolling because they would sometimes appear under Mac OS X even though there was
// plenty of horizontal space to display the list.
JScrollPane scroll = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setBorder(null);
list.setFocusable( false );
popup.setFocusable( false );
scroll.getVerticalScrollBar().setFocusable( false );
scroll.getHorizontalScrollBar().setFocusable( false );
popup.setBorder(BorderFactory.createLineBorder(Color.black));
popup.add(scroll);
createDocumentListener();
addMouseListenerToList();
list.setRequestFocusEnabled(false);
autocompletedtextComp.addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) { }
public void focusLost(FocusEvent e) {
hideAutocompletionPopup();
}
});
}
// abstract methods:
/**
* Start a new thread that implement ShowingThread with the given delay.
*/
protected abstract void startNewShowingThread(int delay);
/**
* Hide the auto-completion popup window.
*/
protected abstract void hideAutocompletionPopup();
/**
* update auto-completion popup's list model depending on the data in text component.
*
* @param list - Auto-completion popup's list.
* @return true if the list was updated successfully, false otherwise.
*/
protected boolean updateListData(JList list) {
return completer.updateListData(list, autocompletedtextComp);
}
/**
* user has selected some item from the auto-completion popup list,
* update text component accordingly.
*
* @param selected - The selected item from the auto-completion popup's list.
*/
protected void updateTextComponent(String selected) {
completer.updateTextComponent(selected, autocompletedtextComp);
}
/**
* createNewShowingThread shows the auto-completion popup window after
* a non-blocking delay.
*
* @param delay - The requested delay (in miliseconds) until the popup appear.
*/
protected void createNewShowingThread(int delay) {
// stop current showing thread (if exist)
if (showingThread != null)
showingThread.done();
// start new showing thread
startNewShowingThread(delay);
}
/**
* The function handles the case when an item of the auto-copmpletion popup was selected.
* it ask the text component to be updated according to the selected item and create
* a new thread that will open auto-completion popup windos after delay of
* POPUP_DELAY_AFTER_ACCEPTING_LIST_ITEM seconds.
*/
protected void acceptListItem(String selected) {
updateTextComponent(selected);
createNewShowingThread(POPUP_DELAY_AFTER_ACCEPTING_LIST_ITEM);
}
private void addMouseListenerToList() {
list.addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
// If there was double click on item of the popup's list,
// select it, and update the text component.
if (e.getClickCount() == 2) {
int index = list.locationToIndex(e.getPoint());
list.setSelectedIndex(index);
acceptListItem((String) list.getSelectedValue());
}
}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void mousePressed(MouseEvent e) {}
});
}
private void createDocumentListener() {
documentListener = new DocumentListener(){
public void insertUpdate(DocumentEvent e){
// If text was inserted to the text component and carent is at the end of
// the text, then start a showingThread to open auto-completion popup.
if (autocompletedtextComp.isCarentAtEndOfTextAtInsertion())
createNewShowingThread(popup.isVisible() ? 0 : POPUP_DELAY_AT_TEXT_INSERTION);
}
public void removeUpdate(DocumentEvent e){
// If text was deleted from the text component and carent is at the end of
// the text, then start a showingThread to open auto-completion popup.
if (autocompletedtextComp.isCarentAtEndOfTextAtRemoval())
createNewShowingThread(popup.isVisible() ? 0 : POPUP_DELAY_AT_TEXT_DELETION);
}
public void changedUpdate(DocumentEvent e){ }
};
}
/**
* Returns true if the auto-completion popup window is visible.
*/
protected boolean isPopupListShowing() {
return popup.isShowing();
}
/**
* Returns true if there is a selected item at the auto-completion popup window.
*/
protected boolean isItemSelectedAtPopupList() {
return popup.isShowing() && list.getSelectedIndex() >= 0;
}
/**
* Selects the first item in the list.
*/
protected void selectFirstValue() {
list.setSelectedIndex(0);
list.ensureIndexIsVisible(0);
}
/**
* Selects the last item in the list.
*/
protected void selectLastValue() {
int lastIndex = list.getModel().getSize() - 1;
list.setSelectedIndex(lastIndex);
list.ensureIndexIsVisible(lastIndex);
}
/**
* Selects the item at (VISIBLE_ROW_COUNT - 1) places after the currently
* selected item in the list.
*/
protected void selectNextPage() {
int si = list.getSelectedIndex();
int nextIndex = 0;
if (si >= 0)
nextIndex = Math.min(si + VISIBLE_ROW_COUNT - 1, list.getModel().getSize() - 1);
list.setSelectedIndex(nextIndex);
list.ensureIndexIsVisible(nextIndex);
}
/**
* Selects the item at (VISIBLE_ROW_COUNT - 1) places before the currently
* selected item in the list.
*/
protected void selectPreviousPage() {
int si = list.getSelectedIndex();
if(si > 0){
int nextIndex = Math.max(si - (VISIBLE_ROW_COUNT - 1), 0);
list.setSelectedIndex(nextIndex);
list.ensureIndexIsVisible(nextIndex);
}
}
/**
* Selects the next item in the list. It won't change the selection if the
* currently selected item is already the last item.
*/
protected void selectNextPossibleValue(){
int si = list.getSelectedIndex();
if(si < list.getModel().getSize() - 1){
int nextIndex = si + 1;
list.setSelectedIndex(nextIndex);
list.ensureIndexIsVisible(nextIndex);
}
}
/**
* Selects the previous item in the list. It won't change the selection if the
* currently selected item is already the first item.
*/
protected void selectPreviousPossibleValue(){
int si = list.getSelectedIndex();
if(si > 0){
int nextIndex = si - 1;
list.setSelectedIndex(nextIndex);
list.ensureIndexIsVisible(nextIndex);
}
}
}