/*
* 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.main;
import java.awt.AWTKeyStroke;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.dnd.DropTarget;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.auth.CredentialsMapping;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.FileURL;
import com.mucommander.core.FolderChangeMonitor;
import com.mucommander.core.LocalLocationHistory;
import com.mucommander.core.LocationChanger;
import com.mucommander.core.LocationChanger.ChangeFolderThread;
import com.mucommander.ui.action.ActionKeymap;
import com.mucommander.ui.action.ActionManager;
import com.mucommander.ui.action.impl.FocusNextAction;
import com.mucommander.ui.action.impl.FocusPreviousAction;
import com.mucommander.ui.dnd.FileDragSourceListener;
import com.mucommander.ui.dnd.FileDropTargetListener;
import com.mucommander.ui.event.LocationManager;
import com.mucommander.ui.main.quicklist.BookmarksQL;
import com.mucommander.ui.main.quicklist.ParentFoldersQL;
import com.mucommander.ui.main.quicklist.RecentExecutedFilesQL;
import com.mucommander.ui.main.quicklist.RecentLocationsQL;
import com.mucommander.ui.main.quicklist.RootFoldersQL;
import com.mucommander.ui.main.quicklist.TabsQL;
import com.mucommander.ui.main.table.FileTable;
import com.mucommander.ui.main.table.FileTableConfiguration;
import com.mucommander.ui.main.tabs.ConfFileTableTab;
import com.mucommander.ui.main.tabs.FileTableTab;
import com.mucommander.ui.main.tabs.FileTableTabs;
import com.mucommander.ui.main.tree.FoldersTreePanel;
import com.mucommander.ui.quicklist.QuickList;
import com.mucommander.ui.quicklist.QuickListContainer;
import com.mucommander.ui.tabs.ActiveTabListener;
import com.mucommander.utils.Callback;
/**
* Folder pane that contains the table that displays the contents of the current directory and allows navigation, the
* drive button, and the location field.
*
* @author Maxence Bernard, Arik Hadas
*/
public class FolderPanel extends JPanel implements FocusListener, QuickListContainer, ActiveTabListener {
private static final Logger LOGGER = LoggerFactory.getLogger(FolderPanel.class);
/** The following constants are used to identify the left and right folder panels */
public enum FolderPanelType { LEFT, RIGHT }
private MainFrame mainFrame;
private LocationManager locationManager = new LocationManager(this);
/* We're NOT using JComboBox anymore because of its strange behavior:
it calls actionPerformed() each time an item is highlighted with the arrow (UP/DOWN) keys,
so there is no way to tell if it's the final selection (ENTER) or not.
*/
private DrivePopupButton driveButton;
private LocationTextField locationTextField;
private FileTable fileTable;
private FileTableTabs tabs;
private FoldersTreePanel foldersTreePanel;
private JSplitPane treeSplitPane;
private FileDragSourceListener fileDragSourceListener;
private LocationChanger locationChanger;
/** Is directory tree visible */
private boolean treeVisible = false;
/** Saved width of a directory tree (when it's not visible) */
private int oldTreeWidth = 150;
/** Array of all the existing pop ups for this panel's FileTable **/
private QuickList[] fileTablePopups;
/* TODO branch private boolean branchView; */
/**
* Constructor
*
* @param mainFrame - the MainFrame that contains this panel
* @param initialFolders - the initial folders displayed at this panel's tabs
* @param conf - configuration for this panel's file table
*/
FolderPanel(MainFrame mainFrame, ConfFileTableTab[] initialTabs, int indexOfSelectedTab, FileTableConfiguration conf) {
super(new BorderLayout());
LOGGER.trace(" initialTabs:");
for (FileTableTab tab:initialTabs)
LOGGER.trace("\t"+(tab.getLocation() != null ? tab.getLocation().toString() : null));
this.mainFrame = mainFrame;
// No decoration for this panel
setBorder(null);
JPanel locationPanel = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.HORIZONTAL;
c.gridy = 0;
// Create and add drive button
this.driveButton = new DrivePopupButton(this);
c.weightx = 0;
c.gridx = 0;
locationPanel.add(driveButton, c);
// Create location text field
this.locationTextField = new LocationTextField(this);
// Give location field all the remaining space until the PoupupsButton
c.weightx = 1;
c.gridx = 1;
// Add some space between drive button and location combo box (none by default)
c.insets = new Insets(0, 4, 0, 0);
locationPanel.add(locationTextField, c);
add(locationPanel, BorderLayout.NORTH);
// Initialize quick lists
fileTablePopups = new QuickList[]{
new ParentFoldersQL(this),
new RecentLocationsQL(this),
new RecentExecutedFilesQL(this),
new BookmarksQL(this),
new RootFoldersQL(this),
new TabsQL(this)};
// Create the FileTable
fileTable = new FileTable(mainFrame, this, conf);
locationChanger = new LocationChanger(mainFrame, this, locationManager);
// Create the Tabs (Must be called after the fileTable was created and current folder was set)
tabs = new FileTableTabs(mainFrame, this, initialTabs);
// Select the tab that was previously selected on last run
tabs.selectTab(indexOfSelectedTab);
tabs.addActiveTabListener(this);
// create folders tree on a JSplitPane
foldersTreePanel = new FoldersTreePanel(this);
foldersTreePanel.setVisible(false);
treeSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, foldersTreePanel, tabs);
treeSplitPane.setDividerSize(0);
treeSplitPane.setDividerLocation(0);
// Remove default border
treeSplitPane.setBorder(null);
add(treeSplitPane, BorderLayout.CENTER);
// Disable Ctrl+Tab and Shift+Ctrl+Tab focus traversal keys
disableCtrlFocusTraversalKeys(locationTextField);
disableCtrlFocusTraversalKeys(foldersTreePanel.getTree());
disableCtrlFocusTraversalKeys(fileTable);
disableCtrlFocusTraversalKeys(tabs);
registerCycleThruFolderPanelAction(locationTextField);
registerCycleThruFolderPanelAction(foldersTreePanel.getTree());
// No need to register cycle actions for FileTable, they already are
// Listen to focus event in order to notify MainFrame of changes of the current active panel/table
fileTable.addFocusListener(this);
locationTextField.addFocusListener(this);
tabs.addFocusListener(this);
// Drag and Drop support
// Enable drag support on the FileTable
this.fileDragSourceListener = new FileDragSourceListener(this);
fileDragSourceListener.enableDrag(fileTable);
// Allow the location field to change the current directory when a file/folder is dropped on it
FileDropTargetListener dropTargetListener = new FileDropTargetListener(this, true);
locationTextField.setDropTarget(new DropTarget(locationTextField, dropTargetListener));
driveButton.setDropTarget(new DropTarget(driveButton, dropTargetListener));
}
/**
* Removes the Control+Tab and Shift+Control+Tab focus traversal keys from the given component so that those
* shortcuts can be used for other purposes.
*
* @param component the component for which to remove the Control+Tab and Shift+Control+Tab focus traversal keys
*/
private void disableCtrlFocusTraversalKeys(Component component) {
// Remove Ctrl+Tab from forward focus traversal keys
HashSet<AWTKeyStroke> keyStrokeSet = new HashSet<AWTKeyStroke>();
keyStrokeSet.add(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, 0));
component.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, keyStrokeSet);
// Remove Shift+Ctrl+Tab from backward focus traversal keys
keyStrokeSet = new HashSet<AWTKeyStroke>();
keyStrokeSet.add(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, java.awt.event.InputEvent.SHIFT_DOWN_MASK));
component.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, keyStrokeSet);
}
/**
* Registers the {@link FocusNextAction} and {@link FocusPreviousAction} actions onto the given component's
* input map.
*
* @param component the component for which to register the cycle actions
*/
private void registerCycleThruFolderPanelAction(JComponent component) {
ActionKeymap.registerActionAccelerators(
ActionManager.getActionInstance(FocusNextAction.Descriptor.ACTION_ID, mainFrame),
component,
JComponent.WHEN_FOCUSED);
ActionKeymap.registerActionAccelerators(
ActionManager.getActionInstance(FocusPreviousAction.Descriptor.ACTION_ID, mainFrame),
component,
JComponent.WHEN_FOCUSED);
}
public FileDragSourceListener getFileDragSourceListener() {
return this.fileDragSourceListener;
}
/**
* Returns the MainFrame that contains this panel.
*
* @return the MainFrame that contains this panel
*/
public MainFrame getMainFrame() {
return this.mainFrame;
}
/**
* Returns the FileTable contained by this panel.
*
* @return the FileTable contained by this panel
*/
public FileTable getFileTable() {
return this.fileTable;
}
/**
* Returns the FileTable tabs contained by this panel.
*
* @return the FileTable tabs contained by this panel
*/
public FileTableTabs getTabs() {
return this.tabs;
}
/**
* Returns the LocationTextField contained by this panel.
*
* @return the LocationTextField contained by this panel
*/
public LocationTextField getLocationTextField() {
return locationTextField;
}
/**
* Returns the DrivePopupButton contained by this panel.
*
* @return the DrivePopupButton contained by this panel
*/
public DrivePopupButton getDriveButton() {
return driveButton;
}
/**
* Returns the visited folders history, wrapped in a FolderHistory object.
*
* @return the visited folders history, wrapped in a FolderHistory object
*/
public LocalLocationHistory getFolderHistory() {
return getTabs().getCurrentTab().getLocationHistory();
}
/**
* Returns the LocationManager instance that notifies registered listeners of location changes
* that occur in this FolderPanel.
*
* @return the LocationManager instance that notifies registered listeners of location changes that occur in
* this FolderPanel
*/
public LocationManager getLocationManager() {
return locationManager;
}
/**
* Allows the user to easily change the current folder and type a new one: requests focus
* on the location field and selects the folder string.
*/
public void changeCurrentLocation() {
locationTextField.selectAll();
locationTextField.requestFocus();
}
/**
* Returns the FolderChangeMonitor which monitors changes in the current folder and automatically refreshes it.
*
* @return the FolderChangeMonitor which monitors changes in the current folder and automatically refreshes it
*/
public FolderChangeMonitor getFolderChangeMonitor() {
return locationManager.getFolderChangeMonitor();
}
public void setProgressValue(int value) {
locationTextField.setProgressValue(value);
}
public void tryChangeCurrentFolderInternal(FileURL folderURL, Callback callback) {
locationChanger.tryChangeCurrentFolderInternal(folderURL, callback);
}
public ChangeFolderThread tryChangeCurrentFolder(AbstractFile folder) {
return locationChanger.tryChangeCurrentFolder(folder, false);
}
public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL, AbstractFile selectThisFileAfter, boolean findWorkableFolder) {
return locationChanger.tryChangeCurrentFolder(FileFactory.getFile(folderURL), selectThisFileAfter, findWorkableFolder, false);
}
public ChangeFolderThread tryChangeCurrentFolder(AbstractFile folder, AbstractFile selectThisFileAfter, boolean findWorkableFolder) {
return locationChanger.tryChangeCurrentFolder(folder, selectThisFileAfter, findWorkableFolder, false);
}
public ChangeFolderThread tryChangeCurrentFolder(String folderPath) {
return locationChanger.tryChangeCurrentFolder(folderPath);
}
public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL) {
return locationChanger.tryChangeCurrentFolder(folderURL);
}
public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL, CredentialsMapping credentialsMapping) {
return locationChanger.tryChangeCurrentFolder(folderURL, credentialsMapping, false);
}
public ChangeFolderThread tryRefreshCurrentFolder() {
return locationChanger.tryRefreshCurrentFolder();
}
public ChangeFolderThread tryRefreshCurrentFolder(AbstractFile selectThisFileAfter) {
return locationChanger.tryRefreshCurrentFolder(selectThisFileAfter);
}
public ChangeFolderThread getChangeFolderThread() {
return locationChanger.getChangeFolderThread();
}
public long getLastFolderChangeTime() {
return locationChanger.getLastFolderChangeTime();
}
/**
* Returns the folder that is currently being displayed by this panel.
*
* @return the folder that is currently being displayed by this panel
*/
public AbstractFile getCurrentFolder() {
return locationManager.getCurrentFolder();
}
/**
* This method updates the UI with the given folder
* If the currently selected tab is locked and the given flag "changeLockedTab" is off,
* then a new tab is opened with the given folder,
* otherwise, the currently presented folder is replaces with the given folder
*
* @param folder - the folder to be set
* @param children - the children of the given folder
* @param fileToSelect - the file that would be selected after changing the folder
* @param changeLockedTab - flag that indicates whether to change the presented folder in
* the currently selected tab although it's locked (used when switching tabs)
*/
public void setCurrentFolder(AbstractFile folder, AbstractFile children[], AbstractFile fileToSelect, boolean changeLockedTab) {
// Change the current folder in the table and select the given file if not null
if(fileToSelect == null)
fileTable.setCurrentFolder(folder, children);
else
fileTable.setCurrentFolder(folder, children, fileToSelect);
}
/**
* Shows the pop up which is located the given index in fileTablePopups.
*
* @param index - index of the FileTablePopup in fileTablePopups.
*/
public void showQuickList(int index) {
fileTablePopups[index].show();
}
/**
* Returns true if a directory tree is visible.
*/
public boolean isTreeVisible() {
return treeVisible;
}
/**
* Returns width of a folders tree.
* @return a width of a folders tree
*/
public int getTreeWidth() {
if (!treeVisible) {
return oldTreeWidth;
} else {
return treeSplitPane.getDividerLocation();
}
}
/**
* Sets a width of a folders tree.
* @param width new width
*/
public void setTreeWidth(int width) {
if (!treeVisible) {
oldTreeWidth = width;
} else {
treeSplitPane.setDividerLocation(width);
treeSplitPane.doLayout();
}
}
/**
* Returns a panel with a folders tree.
* @return a panel with a folders tree
*/
public FoldersTreePanel getFoldersTreePanel() {
return foldersTreePanel;
}
/**
* Enables/disables a directory tree visibility. Invoked by {@link com.mucommander.ui.action.impl.ToggleTreeAction}.
*/
public void setTreeVisible(boolean treeVisible) {
if (this.treeVisible != treeVisible) {
this.treeVisible = treeVisible;
if (!treeVisible) {
// save width of a tree panel
oldTreeWidth = treeSplitPane.getDividerLocation();
}
foldersTreePanel.setVisible(treeVisible);
// hide completely divider if a tree isn't visible
treeSplitPane.setDividerLocation(treeVisible ? oldTreeWidth : 0);
treeSplitPane.setDividerSize(treeVisible ? 5 : 0);
foldersTreePanel.requestFocus();
}
}
////////////////////////
// Overridden methods //
////////////////////////
/**
* Overridden for debugging purposes.
*/
@Override
public String toString() {
return getClass().getName()+"@"+hashCode() +" currentFolder="+getCurrentFolder()+" hasFocus="+hasFocus();
}
///////////////////////////
// FocusListener methods //
///////////////////////////
public void focusGained(FocusEvent e) {
// Notify MainFrame that we are in control now! (our table/location field is active)
mainFrame.setActiveTable(fileTable);
}
public void focusLost(FocusEvent e) {
fileTable.getQuickSearch().stop();
}
////////////////////////////////
// QuickListContainer methods //
////////////////////////////////
public Point calcQuickListPosition(Dimension dim) {
return new Point(
Math.max((getWidth() - (int)dim.getWidth()) / 2, 0),
getLocationTextField().getHeight() + Math.max((getHeight() - (int)dim.getHeight()) / 3, 0)
);
}
public Component containerComponent() {
return this;
}
public Component nextFocusableComponent() {
return fileTable;
}
///////////////////////////////
// ActiveTabListener methods //
///////////////////////////////
public void activeTabChanged() {
boolean isCurrentTabLocked = tabs.getCurrentTab().isLocked();
locationTextField.setEnabled(!isCurrentTabLocked);
driveButton.setEnabled(!isCurrentTabLocked);
}
}