/* * 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.table; import java.awt.Container; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Iterator; import java.util.WeakHashMap; import javax.swing.DefaultCellEditor; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.collections.Enumerator; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.MuConfigurations; import com.mucommander.conf.MuPreference; import com.mucommander.conf.MuPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.impl.MoveJob; import com.mucommander.text.CustomDateFormat; import com.mucommander.text.Translator; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.MuAction; import com.mucommander.ui.action.impl.MarkNextRowAction; import com.mucommander.ui.action.impl.MarkPreviousRowAction; import com.mucommander.ui.action.impl.MarkSelectedFileAction; import com.mucommander.ui.action.impl.RefreshAction; import com.mucommander.ui.dialog.file.AbstractCopyDialog; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.event.TableSelectionListener; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.TablePopupMenu; import com.mucommander.ui.quicksearch.QuickSearch; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeListener; import com.mucommander.ui.theme.ThemeManager; /** * A heavily modified <code>JTable</code> which displays a folder's contents and allows file mouse and keyboard selection, * marking and navigation. <code>JTable</code> provides the basics for file selection but its behavior has to be * extended to allow file marking. * * @author Maxence Bernard, Nicolas Rinaudo */ public class FileTable extends JTable implements MouseListener, MouseMotionListener, KeyListener, ActivePanelListener, ConfigurationListener, ThemeListener { private static final Logger LOGGER = LoggerFactory.getLogger(FileTable.class); // - Column sizes -------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Minimum width for 'name' column when in automatic column sizing mode */ private final static int RESERVED_NAME_COLUMN_WIDTH = 40; /** Miniumn column width when in automatic column sizing mode */ private final static int MIN_COLUMN_AUTO_WIDTH = 20; // - Containers ---------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Frame containing this file table. */ private MainFrame mainFrame; /** Folder panel containing this frame. */ private FolderPanel folderPanel; // - UI components ------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** TableModel instance used by this JTable to get cells' values */ private FileTableModel tableModel; /** TableCellRender instance used by this JTable to render cells */ private FileTableCellRenderer cellRenderer; /** CellEditor used to edit filenames when clicked */ private FilenameEditor filenameEditor; /** Contains sort-related variables */ private SortInfo sortInfo = new SortInfo(); /** Row currently selected */ private int currentRow; // Used when right button is pressed and mouse is dragged private boolean markOnRightClick; private int lastDraggedRow = -1; // Used by shift+Click private int lastRow; /** Allows to detect repeated key strokes of mark key (space/insert) */ private boolean markKeyRepeated; /** In case of repeated mark keystrokes, true if last row has already been marked/unmarked */ private boolean lastRowMarked; /** Timestamp of last row selection change */ private long selectionChangedTimestamp; /** Timestamp of last double click */ private long lastDoubleClickTimestamp; /** Is automatic columns sizing enabled ? */ private boolean autoSizeColumnsEnabled; /** Instance of the inner class that handles quick search */ private QuickSearch<AbstractFile> quickSearch = new FileTableQuickSearch(); /** TableSelectionListener instances registered to receive selection change events */ private WeakHashMap<TableSelectionListener, ?> tableSelectionListeners = new WeakHashMap<TableSelectionListener, Object>(); /** True when this table is the current or last active table in the MainFrame */ private boolean isActiveTable; /** Timestamp of the last focus gain (in milliseconds) */ private long focusGainedTime; /** Delay in ms after which filename editor can be triggered when current row's filename cell is clicked */ private final static int EDIT_NAME_CLICK_DELAY = 500; /** Timestamp of last double click - workaround for MouseEvent.getClickCount() */ private long doubleClickTime; /** Counts the number of clicks within the double-click interval */ private int doubleClickCounter = 1; /** Interval to wait for the double-click */ private static int DOUBLE_CLICK_INTERVAL = DesktopManager.getMultiClickInterval(); /** Wrapper of presentation adjustments for the file-table */ private FileTableWrapperForDisplay scrollpaneWrapper; /** Table that shows the user to refresh if the location doesn't exist */ private DefaultOverlayable overlayTable; public FileTable(MainFrame mainFrame, FolderPanel folderPanel, FileTableConfiguration conf) { super(new FileTableModel(), new FileTableColumnModel(conf)); tableModel = (FileTableModel)getModel(); tableModel.setSortInfo(sortInfo); ThemeManager.addCurrentThemeListener(this); setAutoResizeMode(AUTO_RESIZE_NEXT_COLUMN); // Stores the mainframe and folderpanel. this.mainFrame = mainFrame; this.folderPanel = folderPanel; // Remove all default action mappings as they conflict with corresponding mu actions InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); inputMap.clear(); inputMap.setParent(null); // Initializes the table. cellRenderer = new FileTableCellRenderer(this); getColumnModel().getColumn(convertColumnIndexToView(Column.NAME.ordinal())).setCellEditor(filenameEditor = new FilenameEditor(new JTextField())); getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); setTableHeader(new FileTableHeader(this)); setShowGrid(false); setIntercellSpacing(new Dimension(0,0)); setRowHeight(); setAutoSizeColumnsEnabled(MuConfigurations.getPreferences().getVariable(MuPreference.AUTO_SIZE_COLUMNS, MuPreferences.DEFAULT_AUTO_SIZE_COLUMNS)); // Initializes event listening. addMouseListener(this); folderPanel.addMouseListener(this); addMouseMotionListener(this); addKeyListener(this); mainFrame.addActivePanelListener(this); MuConfigurations.addPreferencesListener(this); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers // instead of a custom header renderer. if(usesTableHeaderRenderingProperties()) setTableHeaderRenderingProperties(); // Initialize a wrapper of presentation adjustments for the file-table scrollpaneWrapper = new FileTableWrapperForDisplay(this, folderPanel, mainFrame); overlayTable = createOverlayableTable(); addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { overlayTable.repaint(); } }); } private DefaultOverlayable createOverlayableTable() { return new DefaultOverlayable(scrollpaneWrapper) { private static final long serialVersionUID = 1L; { addOverlayComponent(createRefreshNonExistingLocationLabel()); } private JLabel createRefreshNonExistingLocationLabel() { JLabel label = new JLabel("Refresh to reconnect"); label.setIcon(MuAction.getStandardIcon(RefreshAction.class)); return label; } @Override public boolean requestFocusInWindow() { return scrollpaneWrapper.requestFocusInWindow(); } /** * Overridden to ensure that the table is always visible. */ @Override public void setVisible(boolean visible) { if (visible) super.setVisible(true); } }; } public String getFileNameAtRow(int index) { return (index==0 && tableModel.hasParentFolder()) ? ".." : tableModel.getFileAtRow(index).getName(); } /** * Returns the FileTable as a UI component for display purpose. * The UI component is actually a JScrollPane that allows the FileTable to scroll and * responsible to set its viewing properties as needed. * * @return the FileTable as a UI component for display purpose */ public JComponent getAsUIComponent() { return overlayTable; } /** * Under Mac OS X 10.5 (Leopard) and up, sets client properties on this table's JTableHeader to indicate the current * sort criterion/column and sort order (ascending or descending). These properties allow Mac OS X/Java to render * the headers accordingly, instead of having to use a {@link FileTableHeaderRenderer custom header renderer}. * This method has no effect whatsoever on platforms other where {@link #usesTableHeaderRenderingProperties()} * returns <code>false</code>. */ private void setTableHeaderRenderingProperties() { if(usesTableHeaderRenderingProperties()) { JTableHeader tableHeader = getTableHeader(); if(tableHeader==null) return; boolean isActiveTable = isActiveTable(); // Highlights the selected column tableHeader.putClientProperty("JTableHeader.selectedColumn", isActiveTable ? convertColumnIndexToView(sortInfo.getCriterion().ordinal()) :null); // Displays an ascending/descending arrow tableHeader.putClientProperty("JTableHeader.sortDirection", isActiveTable ? sortInfo.getAscendingOrder()?"ascending":"decending" // 'decending' is misspelled but this is OK :null); // Note: if this table is not currently active, properties are cleared to remove the highlighting effect. // However, clearing the properties does not yield the desired behavior as it does not restore the table // header back to normal. This looks like a bug in Apple's implementation. } } /** * Restores selection when focus is gained. * Note: this is not FocusListener implementation method */ private void focusGained() { focusGainedTime = System.currentTimeMillis(); if(isEditing()) { filenameEditor.filenameField.requestFocus(); } else { overlayTable.getOverlayComponents()[0].setEnabled(true); // Repaints the table to reflect the new focused state overlayTable.repaint(); } } /** * Hides selection when focus is lost. * Note: this is not FocusListener implementation method */ private void focusLost() { overlayTable.getOverlayComponents()[0].setEnabled(false); // Repaints the table to reflect the new focused state overlayTable.repaint(); } /** * Returns <code>true</code> if the current platform is capable of indicating the sort criterion and sort order * on the table headers by setting client properties, instead of using a {@link FileTableHeaderRenderer custom header renderer}. * At the moment this method returns <code>true</code> only under Mac OS X 10.5 (and up). * * @return true if the current platform is capable of indicating the sort criterion and sort order on the table * headers by setting client properties. */ static boolean usesTableHeaderRenderingProperties() { return OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher(); } /** * Returns the {@link FolderPanel} that contains this FileTable. * * @return the FolderPanel that contains this FileTable */ public FolderPanel getFolderPanel() { return folderPanel; } /** * Returns <code>true/</code> if this table is the active one in the MainFrame. * Being the active table doesn't necessarily mean that it currently has focus, the focus can be in some other component * of the active {@link FolderPanel}, or nowhere in the MainFrame if the window is not in the foreground. * * <p>Use {@link #hasFocus()} to test if the table currently has focus.</p> * * @return true if this table is the active one in the MainFrame * @see com.mucommander.ui.main.MainFrame#getActiveTable() */ public boolean isActiveTable() { return isActiveTable; } /** * Convenience method that returns this table's model (the one that {@link #getModel()} returns), * as a {@link FileTableModel}, to avoid having to cast it. * * @return this table's model cast as a FileTableModel */ public FileTableModel getFileTableModel() { return tableModel; } /** * Returns a {@link SortInfo} instance that holds information about how this table is currently sorted. * * @return a SortInfo instance that holds information about how this table is currently sorted */ public SortInfo getSortInfo() { return sortInfo; } /** * Returns the {@link QuickSearch} inner class instance used by this FileTable. * * @return the QuickSearch inner class instance used by this FileTable */ public QuickSearch<AbstractFile> getQuickSearch() { return quickSearch; } /** * Returns the file that is currently selected (highlighted), <code>null</code> if the parent folder '..' is * currently selected. * * @return the file that is currently selected (highlighted), null if the parent folder '..' is currently selected */ public synchronized AbstractFile getSelectedFile() { return getSelectedFile(false, false); } /** * Returns the file that is currently selected (highlighted). If the currently selected file is the * parent folder '..', the parent folder is returned only if the corresponding parameter is <code>true</code>. * * @param includeParentFolder if <code>true</code> and parent folder '..' is currently selected, the parent folder * will be returned. * @return the file that is currently selected (highlighted) */ public synchronized AbstractFile getSelectedFile(boolean includeParentFolder) { return getSelectedFile(includeParentFolder, false); } /** * Returns the file that is currently selected (highlighted), wrapped in a {@link com.mucommander.commons.file.CachedFile} * instance if the corresponding parameter is <code>true</code>. If the currently selected file is the * parent folder '..', the parent folder is returned only if the corresponding parameter is <code>true</code>. * * @param includeParentFolder if true and the parent folder '..' is currently selected, the parent folder file * will be returned. If false, null will be returned if the parent folder file is currently selected. * @param returnCachedFile if true, a CachedFile corresponding to the currently selected file will be returned * @return the file that is currently selected (highlighted) */ public synchronized AbstractFile getSelectedFile(boolean includeParentFolder, boolean returnCachedFile) { if(tableModel.getRowCount()==0 || (!includeParentFolder && isParentFolderSelected())) return null; return returnCachedFile?tableModel.getCachedFileAtRow(currentRow):tableModel.getFileAtRow(currentRow); } /** * Returns selected files in a {@link FileSet}. Selected files are either the marked files or the currently selected * file if no file is currently marked. The parent folder '..' is never included in the returned set. * * @return selected files in a FileSet */ public FileSet getSelectedFiles() { FileSet selectedFiles = tableModel.getMarkedFiles(); // if no row is marked, then add selected row if there is one, and if it is not parent folder if(selectedFiles.size()==0) { AbstractFile selectedFile = getSelectedFile(); if(selectedFile!=null) selectedFiles.add(selectedFile); } return selectedFiles; } /** * Returns <code>true</code> if the currently selected row/file is the parent folder '..' . * * @return true if the currently selected row/file is the parent folder '..' */ public boolean isParentFolderSelected() { return currentRow == 0 && tableModel.hasParentFolder(); } /** * Returns <code>true</code> if the given row is the parent folder '..' . * * @param row index of the row to test * @return true if the given row is the parent folder '..' */ public boolean isParentFolder(int row) { return row == 0 && tableModel.hasParentFolder(); } /** * Shorthand for {@link #setCurrentFolder(AbstractFile, AbstractFile[], AbstractFile)} called with no specific file * to select (default selection). * * @param folder the new current folder * @param children children of the specified folder */ public void setCurrentFolder(AbstractFile folder, AbstractFile[] children) { setCurrentFolder(folder, children, null); } /** * Changes the current folder to the specified one and refreshes the table to reflect the folder's contents. * The current file selection is also updated, with the following behavior: * <ul> * <li>If <code>filetoSelect</code> is not <code>null</code>, the specified file becomes the currently selected * file, if it can be found in the new current folder. Previously marked files are cleared.</li> * <li>If it is <code>null</code>: * <ul> * <li>if the current folder is the same as the previous one, the currently selected file and marked files * remain the same, provided they still exist.</li> * <li>if the new current folder is the parent of the previous one, the previous current folder is selected.</li> * <li>in any other case, the first row is selected, whether it be the parent directory ('..') or the first * file of the current folder if it has no parent.</li> * </ul> * </li> * </ul> * * <p> * This method returns only when the folder has actually been changed and the table refreshed.<br> * <b>Important:</b> This method should only be called by {@link FolderPanel} and in any case MUST be synchronized * externally to ensure this method is never called concurrently by different threads. * </p> * * @param folder the new current folder * @param children children of the specified folder * @param fileToSelect the file to select, <code>null</code> for the default selection. */ public void setCurrentFolder(AbstractFile folder, AbstractFile children[], AbstractFile fileToSelect) { overlayTable.setOverlayVisible(!folder.exists()); // Stop quick search in case it was being used before folder change quickSearch.stop(); AbstractFile currentFolder = folderPanel.getCurrentFolder(); // If we're refreshing the current folder, save the current selection and marked files // in order to restore them properly. FileSet markedFiles = null; if(currentFolder != null && folder.equalsCanonical(currentFolder)) { markedFiles = tableModel.getMarkedFiles(); if(fileToSelect==null) fileToSelect = getSelectedFile(); } // If we're navigating to the current folder's parent, we select the current folder. else if(fileToSelect==null) { if(tableModel.hasParentFolder() && folder.equals(tableModel.getParentFolder())) fileToSelect = currentFolder; } // Changes the current folder in the swing thread to make sure that repaints cannot // happen in the middle of the operation - this is used to prevent flickering, badly // refreshed frames and such unpleasant graphical artifacts. Runnable folderChangeThread = new FolderChangeThread(folder, children, markedFiles, fileToSelect); // Wait for the task to complete, so that we return only when the folder has actually been changed and the // table updated to reflect the new folder. // Note: we use a wait/notify scheme rather than calling SwingUtilities#invokeAndWait to avoid deadlocks // due to AWT thread synchronization issues. synchronized(folderChangeThread) { SwingUtilities.invokeLater(folderChangeThread); while(true) { try { // FolderChangeThread will call notify when done folderChangeThread.wait(); break; } catch(InterruptedException e) { // will keep looping } } } } /** * Sets row height based on current cell's font and border, revalidates and repaints this JTable. */ private void setRowHeight() { // JTable.setRowHeight() revalidates and repaints the JTable. // Note that it's important here to use the cell editor's font rather than the cell renderer's: if this method is called // as a result to a font changed event, we do not know which class' fontChanged event will be called first. setRowHeight(2*CellLabel.CELL_BORDER_HEIGHT + Math.max(getFontMetrics(filenameEditor.filenameField.getFont()).getHeight(), (int)FileIcons.getIconDimension().getHeight())); // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called :/ // setRowHeight(Math.max(getFontMetrics(cellRenderer.getCellFont()).getHeight()+cellRenderer.CELL_BORDER_HEIGHT, editorRowHeight)); } /** * Returns <code>true</code> if the auto-columns sizing is currently enabled. * * @return true if the auto-columns sizing is currently enabled */ public boolean isAutoSizeColumnsEnabled() { return this.autoSizeColumnsEnabled; } /** * Enables/disables auto-columns sizing, which automatically resizes columns to fit the table's width. * * @param enabled true to enable auto-columns sizing, false to disable it */ public void setAutoSizeColumnsEnabled(boolean enabled) { this.autoSizeColumnsEnabled = enabled; if(autoSizeColumnsEnabled) { getTableHeader().setResizingAllowed(false); // Will invoke doLayout() resizeAndRepaint(); } else getTableHeader().setResizingAllowed(true); } /** * Returns <code>true</code> if auto columns sizing is currently enabled. * * @return true if auto columns sizing is currently enabled */ public boolean getAutoSizeColumnsEnabled() { return autoSizeColumnsEnabled; } /** * Controls whether folders are displayed first in this FileTable or mixed with regular files. * After calling this method, the table is refreshed to reflect the change. * * @param enabled if true, folders are displayed before regular files. If false, files are mixed with directories. */ public void setFoldersFirst(boolean enabled) { if(sortInfo.getFoldersFirst()!=enabled) { sortInfo.setFoldersFirst(enabled); sortTable(); } } /** * Selects the given file, does nothing if this table does not contain the file. * * @param file the file to select */ public void selectFile(AbstractFile file) { int row = tableModel.getFileRow(file); if(row!=-1) selectRow(row); } /** * Makes the given row the currently selected one. * * @param row index of the row to select */ public void selectRow(int row) { changeSelection(row, 0, false, false); } /** * Equivalent to calling {@link #setRowMarked(int, boolean, boolean)} with <code>repaint</code> enabled. * * @param row index of the row to mark/unmark * @param marked true to mark the row, false to unmark it */ public void setRowMarked(int row, boolean marked) { setRowMarked(row, marked, true); } /** * Sets the given row as marked/unmarked in the table model, repaints the row to reflect the change, * and notifies registered {@link com.mucommander.ui.event.TableSelectionListener} that the files currently marked * on this FileTable have changed. * * <p>This method has no effect if the row corresponds to the parent folder row '..' .</p> * * @param row index of the row to mark/unmark * @param marked true to mark the row, false to unmark it * @param repaint true to repaint the row after it has been marked/unmarked */ public void setRowMarked(int row, boolean marked, boolean repaint) { if(isParentFolder(row)) return; tableModel.setRowMarked(row, marked); if(repaint) repaintRow(row); // Notify registered listeners that currently marked files have changed on this FileTable fireMarkedFilesChangedEvent(); } /** * Equivalent to calling {@link #setFileMarked(AbstractFile, boolean, boolean)} with <code>repaint</code> enabled. * * @param file file to mark/unmark * @param marked true to mark the file, false to unmark it */ public void setFileMarked(AbstractFile file, boolean marked) { setFileMarked(file, marked, true); } /** * Sets the given file as marked/unmarked in the table model, repaints the corresponding row to reflect the change, * and notifies registered {@link com.mucommander.ui.event.TableSelectionListener} that currently marked files * have changed on this FileTable. * * @param file file to mark/unmark * @param marked true to mark the file, false to unmark it * @param repaint true to repaint the file's row after it has been marked/unmarked */ public void setFileMarked(AbstractFile file, boolean marked, boolean repaint) { int row = tableModel.getFileRow(file); if(row!=-1) setRowMarked(row, marked, repaint); } /** * Marks or unmarks the current selected file (current row) and advance current row to the next one, * with the following exceptions: * <ul> * <li>if quick search is active, this method does nothing * <li>if '..' file is selected, file is not marked but current row is still advanced to the next one * <li>if the {@link MarkSelectedFileAction} key event is repeated and the last file has already * been marked/unmarked since the key was last released, the file is not marked in order to avoid * marked/unmarked flaps when the mark key is kept pressed. * </ul> * * @see MarkSelectedFileAction */ public void markSelectedFile() { // Avoids repeated mark/unmark on last row: return if last row has already been marked/unmarked // by repeated mark key strokes if(markKeyRepeated && lastRowMarked) return; // Don't mark '..' file but select next row if(!isParentFolderSelected()) { setRowMarked(currentRow, !tableModel.isRowMarked(currentRow)); } // Changes selected item to the next one if(currentRow!=tableModel.getRowCount()-1) { selectRow(currentRow+1); } else if(!lastRowMarked) { // Need an explicit repaint to repaint the last row since select row is not called repaintRow(currentRow); // Last row has been marked/unmarked, value will be reset by keyReleased() lastRowMarked = true; } // Any further mark key events will be considered as repeated until keyReleased() has been called markKeyRepeated = true; } /** * Marks or unmarks a range of rows, delimited by the provided start row index and end row index (inclusive). * End row index can be lower, greater or equals to the start row. * * @param startRow index of the first row to repaint * @param endRow index of the last row to mark, can be lower, greater or equals to startRow * @param marked if true, the rows will be marked, unmarked otherwise */ public void setRangeMarked(int startRow, int endRow, boolean marked) { tableModel.setRangeMarked(startRow, endRow, marked); repaintRange(startRow, endRow); fireMarkedFilesChangedEvent(); } /** * Repaints the given row. * * @param row the row to repaint */ private void repaintRow(int row) { repaint(0, row*getRowHeight(), getWidth(), rowHeight); } /** * Repaints a range of rows, delimited by the provided start row index and end row index (inclusive). * End row index can be lower, greater or equals to the start row. * * @param startRow index of the first row to repaint * @param endRow index of the last row to repaint, can be lower, greater or equals to startRow */ private void repaintRange(int startRow, int endRow) { int rowHeight = getRowHeight(); repaint(0, Math.min(startRow, endRow)*rowHeight, getWidth(), (Math.abs(startRow-endRow)+1)*rowHeight); } /** * Returns the number of rows that a page down/page up action should jump, based on this FileTable's viewport size. * The returned number doesn't take into account the number of rows available in this FileTable. * * @return the number of rows that a page down/page up action should jump */ public int getPageRowIncrement() { return getScrollableBlockIncrement(getVisibleRect(), SwingConstants.VERTICAL, 1)/getRowHeight() - 1; } /** * Sorts this FileTable by the given sort criterion, order and 'folders first' value. The criterion and ascending * order will be ignored if the corresponding column is not currently visible, but the 'folders first' value will * still be taken into account. * * @param criterion the sort criterion, see {@link com.mucommander.ui.main.table.Column} for possible values * @param ascending true for ascending order, false for descending order * @param foldersFirst if true, folders are displayed before regular files. If false, files are mixed with directories. */ public void sortBy(Column criterion, boolean ascending, boolean foldersFirst) { // If we're not changing the current sort values, abort. if(criterion==sortInfo.getCriterion() && ascending==sortInfo.getAscendingOrder() && foldersFirst==sortInfo.getFoldersFirst()) return; sortInfo.setFoldersFirst(foldersFirst); // Ignore the sort criterion and order if the corresponding column is not visible if(isColumnVisible(criterion)) { sortInfo.setCriterion(criterion); sortInfo.setAscendingOrder(ascending); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if(usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } // Repaint header getTableHeader().repaint(); } // Sorts table while keeping the current file selection sortTable(); } /** * Calls {@link #sortBy(Column, boolean, boolean)} with the sort information contained in the given {@link SortInfo}. * * @param sortInfo the information to use to sort this table. */ public void sortBy(SortInfo sortInfo) { sortBy(sortInfo.getCriterion(), sortInfo.getAscendingOrder(), sortInfo.getFoldersFirst()); } /** * Sorts this FileTable by the given sort criterion and order. The column corresponding to the specified criterion * has to be visible when this method is called. If it isn't, this method won't have any effect. * * @param criterion the sort criterion, see {@link com.mucommander.ui.main.table.Column} for possible values * @param ascending true for ascending order, false for descending order */ public void sortBy(Column criterion, boolean ascending) { sortBy(criterion, ascending, sortInfo.getFoldersFirst()); } /** * Sorts this FileTable by the given sort criterion. If the criterion is already the current one, the sort order * (ascending or descending) will be reversed. * * @param criterion the sort criterion, see {@link com.mucommander.ui.main.table.Column} for possible values */ public void sortBy(Column criterion) { if (criterion==sortInfo.getCriterion()) { reverseSortOrder(); return; } sortBy(criterion, sortInfo.getAscendingOrder()); } /** * Convenience method that returns this table's <code>javax.swing.table.TableColumnModel</code> cast as a * {@link FileTableColumnModel}. * * @return this table's TableColumnModel cast as a FileTableColumnModel */ public FileTableColumnModel getFileTableColumnModel() { return (FileTableColumnModel)getColumnModel(); } @Override public void setColumnModel(TableColumnModel columnModel) { // super.setColumnModel() must be called BEFORE the methods below super.setColumnModel(columnModel); if(filenameEditor != null) columnModel.getColumn(convertColumnIndexToView(Column.NAME.ordinal())).setCellEditor(filenameEditor); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if(usesTableHeaderRenderingProperties()) setTableHeaderRenderingProperties(); } /** * Returns <code>true</code> if the specified column is currently visible. * * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values * @return true if the specified column is currently visible */ public boolean isColumnVisible(Column column) { return getFileTableColumnModel().isColumnVisible(column); } /** * Returns <code>true</code> if the given column can be displayed given the current folder. Certain columns such as * {@link Column#OWNER} and {@link Column#GROUP} can be displayed only if current folder's files are capable * of supplying this information. * Note that the return value does not take into account the column's current enabled state. * * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values * @return true if the given column can be displayed given the current folder */ public boolean isColumnDisplayable(Column column) { // Check this against the children's file implementation whenever possible: certain file implementations may // return different values for the current folder than for its children. For instance, this is the case for file // protocols that have a special file implementation for the root folder (s3 is one). AbstractFile file = getFileTableModel().getFileAt(0); if(file==null) file = folderPanel.getCurrentFolder(); // The Owner and Group columns are displayable only if current folder has this information if(column==Column.OWNER) { return file.canGetOwner(); } else if(column==Column.GROUP) { return file.canGetGroup(); } return true; } /** * Updates the visibility of all columns based on their enabled state, and for conditional columns on the * current folder. */ public void updateColumnsVisibility() { FileTableColumnModel columnModel = getFileTableColumnModel(); for(Column c : Column.values()) columnModel.setColumnVisible(c, columnModel.isColumnEnabled(c) && isColumnDisplayable(c)); } /** * Returns <code>true</code> if the specified column is enabled. * * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values * @return true if the specified column is enabled */ public boolean isColumnEnabled(Column column) { return getFileTableColumnModel().isColumnEnabled(column); } /** * Enables/disables the specified column. Disabling a column will make it invisible. Enabling a column will make it * visible only if the column can be displayed. See {@link #isColumnDisplayable(Column)} for more information about * this. * * <p>If the current sort criterion corresponds to the specified column and this * column is disabled, the sort criterion will be reset to {@link Column#NAME} to prevent the table from being * sorted by an invisible column/criterion.</p> * * @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values * @param enabled true to enable the column, false to disable it. */ public void setColumnEnabled(Column column, boolean enabled) { FileTableColumnModel columnModel = getFileTableColumnModel(); columnModel.setColumnEnabled(column, enabled); // Update the visibility of the column updateColumnsVisibility(); // The column may be the current 'sort by' criterion and may have become invisible. // If that is the case, change the criterion to NAME. if(sortInfo.getCriterion()==column && !columnModel.isColumnVisible(column)) sortBy(Column.NAME); } public int getColumnPosition(Column column) { return getFileTableColumnModel().getColumnPosition(column.ordinal()); } /** * Reverses the current sort order, from ascending to descending or vice-versa. */ public void reverseSortOrder() { boolean newSortOrder = !sortInfo.getAscendingOrder(); sortInfo.setAscendingOrder(newSortOrder); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if(usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } // Repaint header getTableHeader().repaint(); // Sorts table while keeping current file selected sortTable(); } /** * Turns on the filename editor on current row. */ public void editCurrentFilename() { // Forces CommandBar to return to its normal state as modify key release event is never fired to FileTable mainFrame.getCommandBar().setAlternateActionsMode(false); // Temporarily enable editing tableModel.setNameColumnEditable(true); // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called :/ // // Adjust row height to match filename editor's height // setRowHeight(row, (int)filenameEditor.filenameField.getPreferredSize().getHeight()); // Starts editing clicked cell's name column editCellAt(currentRow, convertColumnIndexToView(Column.NAME.ordinal())); // Saves current/editing row in the filename editor and requests focus on the text field filenameEditor.notifyEditingRow(currentRow); // Disable editing tableModel.setNameColumnEditable(false); } /** * Sorts this FileTable and repaints it. Marked files and selected file will remain the same, only * their position will have changed in the newly sorted table. */ private void sortTable() { // Save currently selected file AbstractFile selectedFile = tableModel.getFileAtRow(currentRow); // Sort table, doesn't affect marked files tableModel.sortRows(); // Restore selected file selectFile(selectedFile); // Repaint table repaint(); } //////////////////////////////////// // TableSelectionListener methods // //////////////////////////////////// /** * Adds the given TableSelectionListener to the list of listeners that are registered to receive * notifications when the currently selected file changes. * * @param listener the TableSelectionListener instance to add to the list of registered listeners. */ public void addTableSelectionListener(TableSelectionListener listener) { tableSelectionListeners.put(listener, null); } /** * Removes the given TableSelectionListener from the list of listeners that are registered to receive * notifications when the currently selected file changes. * The listener will not receive any further notification after this method has been called * (or soon after if events are pending). * * @param listener the TableSelectionListener instance to add to the list of registered listeners. */ public void removeTableSelectionListener(TableSelectionListener listener) { tableSelectionListeners.remove(listener); } /** * Notifies all registered listeners that the currently selected file has changed on this FileTable. */ public void fireSelectedFileChangedEvent() { for(TableSelectionListener listener : tableSelectionListeners.keySet()) listener.selectedFileChanged(this); } /** * Notifies all registered listeners that the currently marked files have changed on this FileTable. */ public void fireMarkedFilesChangedEvent() { for(TableSelectionListener listener : tableSelectionListeners.keySet()) listener.markedFilesChanged(this); } // - Layout management --------------------------------------------------------------- // ----------------------------------------------------------------------------------- private void doAutoLayout(boolean respectSize) { Iterator<TableColumn> columns; TableColumn column; TableColumn nameColumn; Column c; int remainingWidth; int columnWidth; int rowCount; FontMetrics fm; String val; int dirStringWidth; int stringWidth; fm = getFontMetrics(FileTableCellRenderer.getCellFont()); dirStringWidth = fm.stringWidth(FileTableModel.DIRECTORY_SIZE_STRING); remainingWidth = getSize().width - RESERVED_NAME_COLUMN_WIDTH; columns = respectSize ? new Enumerator<TableColumn>(getColumnModel().getColumns()) : getFileTableColumnModel().getAllColumns(); nameColumn = null; while(columns.hasNext()) { column = columns.next(); c = Column.valueOf(column.getModelIndex()); if(c == Column.NAME) nameColumn = column; else { if(c == Column.EXTENSION) columnWidth = (int)FileIcons.getIconDimension().getWidth(); else { columnWidth = MIN_COLUMN_AUTO_WIDTH; rowCount = getModel().getRowCount(); for(int rowNum = 0; rowNum < rowCount; rowNum++) { val = (String)getModel().getValueAt(rowNum, column.getModelIndex()); stringWidth = val==null?0 :c==Column.SIZE && val.equals(FileTableModel.DIRECTORY_SIZE_STRING)?dirStringWidth :fm.stringWidth(val); columnWidth = Math.max(columnWidth, stringWidth); } } if(respectSize) columnWidth = Math.min(columnWidth, remainingWidth); columnWidth += 2 * CellLabel.CELL_BORDER_WIDTH; column.setWidth(columnWidth); // Update subtotal remainingWidth -= columnWidth; if(remainingWidth < 0) remainingWidth = 0; } } nameColumn.setWidth(remainingWidth + RESERVED_NAME_COLUMN_WIDTH); } private void doStaticLayout() { int width; TableColumn nameColumn; if((width = getSize().width - getColumnModel().getTotalColumnWidth()) == 0) return; nameColumn = getColumnModel().getColumn(convertColumnIndexToView(Column.NAME.ordinal())); if(nameColumn.getWidth() + width >= RESERVED_NAME_COLUMN_WIDTH) nameColumn.setWidth(nameColumn.getWidth() + width); else nameColumn.setWidth(RESERVED_NAME_COLUMN_WIDTH); } /** * Overrides JTable's doLayout() method to use a custom column layout (if auto-column sizing is enabled). */ @Override public void doLayout() { if(!autoSizeColumnsEnabled) { if(getTableHeader().getResizingColumn() != null) super.doLayout(); else if(!getFileTableColumnModel().wereColumnSizesSet()) doAutoLayout(false); else doStaticLayout(); } // Custom layout else doAutoLayout(true); // Ensures that current row is visible (within current viewport), and if not adjusts viewport to center it Rectangle visibleRect = getVisibleRect(); final Rectangle cellRect = getCellRect(currentRow, 0, false); if(cellRect.y<visibleRect.y || cellRect.y+getRowHeight()>visibleRect.y+visibleRect.height) { if(scrollpaneWrapper!=null) { // At this point JViewport is not yet aware of the new FileTable dimensions, calling setViewPosition // would not work. Instead, SwingUtilities.invokeLater is used to delay the call after all pending // UI events (including JViewport revalidation) have been processed. SwingUtilities.invokeLater(new Runnable() { public void run() { scrollpaneWrapper.getViewport().setViewPosition(new java.awt.Point(0, Math.max(0, cellRect.y-scrollpaneWrapper.getHeight()/2-getRowHeight()/2))); } }); } } } /** * Method overridden to return a custom TableCellRenderer. */ @Override public TableCellRenderer getCellRenderer(int row, int column) { return cellRenderer; } /** * Method overridden to consume keyboard events when quick search is active or when a row is being editing * in order to prevent registered actions from being fired. */ @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent ke, int condition, boolean pressed) { if(quickSearch.isActive() || isEditing()) return true; // if(ActionKeymap.isKeyStrokeRegistered(ks)) // return false; return super.processKeyBinding(ks, ke, condition, pressed); } /** * Overrides the changeSelection method from JTable to track the current selected row (the one that has focus) * and fire a {@link com.mucommander.ui.event.TableSelectionListener#selectedFileChanged(FileTable)} event * to registered listeners. */ @Override public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { // For shift+click lastRow = currentRow; currentRow = rowIndex; super.changeSelection(rowIndex, columnIndex, toggle, extend); // If row changed if(currentRow!=lastRow) { // Update selection changed timestamp selectionChangedTimestamp = System.currentTimeMillis(); // notify registered TableSelectionListener instances that the currently selected file has changed fireSelectedFileChangedEvent(); } // // Don't refresh status bar if up, down, space or insert key is pressed (repeated key strokes). // // Status bar will be refreshed whenever the key is released. // // We need this limit because refreshing status bar takes time. // if(downKeyDown || upKeyDown || spaceKeyDown || insertKeyDown) // return; } @Override public Dimension getPreferredSize() { Container parentComp = getParent(); // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called :/ /* int height; if(isEditing()) height = (tableModel.getRowCount()-1)*getRowHeight() + editorRowHeight; else height = tableModel.getRowCount()*getRowHeight(); return new Dimension(parentComp==null?0:parentComp.getWidth(), height); */ return new Dimension(parentComp==null?0:parentComp.getWidth(), tableModel.getRowCount()*getRowHeight()); } @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } /** * Overridden for debugging purposes. */ @Override public String toString() { return getClass().getName()+"@"+hashCode() +" currentFolder="+folderPanel.getCurrentFolder()+" hasFocus="+hasFocus()+" currentRow="+currentRow; } /////////////////////////// // MouseListener methods // /////////////////////////// public void mouseClicked(MouseEvent e) { // Discard mouse events while in 'no events mode' if (mainFrame.getNoEventsMode()) return; Object source = e.getSource(); // Under Linux with GNOME and KDE, Java does not honour the multi/double-click speed preferences // (see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5076635) and defaults to a 200ms double-click // interval, which for most people is too low. Therefore, we cannot rely on MouseEvent#getClickCount() and // MouseEvent#getMultiClickInterval() to always work properly and have to detect double-clicks using the // proper system multi-click interval returned by DefaultManager#getMultiClickInterval(). if ((System.currentTimeMillis() - doubleClickTime) < DOUBLE_CLICK_INTERVAL && selectionChangedTimestamp < doubleClickTime) { if (doubleClickCounter == 1) { doubleClickCounter = 2; // increase only once e.consume(); // and make sure this event is not sent anywhere else } } else { /* reset the counter for the double click count */ doubleClickTime = System.currentTimeMillis(); doubleClickCounter = 1; } // If one of the table cells was left clicked... if (source == this && DesktopManager.isLeftMouseButton(e)) { // Clicking on the selected row's ... : // - 'name' label triggers the filename editor // - 'date' label triggers the change date dialog // - 'permissions' label triggers the change permissions dialog, only if permissions can be changed // Timestamp check is used to make sure that this mouse click did not trigger current row selection if ((doubleClickCounter == 1) && (System.currentTimeMillis() - selectionChangedTimestamp) > EDIT_NAME_CLICK_DELAY) { int clickX = e.getX(); Point p = new Point(clickX, e.getY()); final int row = rowAtPoint(p); final int viewColumn = columnAtPoint(p); final Column column = Column.valueOf(convertColumnIndexToModel(viewColumn)); // Test if the clicked row is current row, if column is name column, and if current row is not '..' file if (row == currentRow && !isParentFolderSelected() && (column == Column.NAME || column == Column.DATE || column == Column.PERMISSIONS)) { // Test if clicked point is inside the label and abort if not FontMetrics fm = getFontMetrics(FileTableCellRenderer.getCellFont()); int labelWidth = fm.stringWidth((String) tableModel.getValueAt(row, column.ordinal())); int columnX = (int) getTableHeader().getHeaderRect(viewColumn).getX(); if (clickX<columnX+CellLabel.CELL_BORDER_WIDTH || clickX>columnX+labelWidth+CellLabel.CELL_BORDER_WIDTH) return; // The following test ensures that this mouse click is not the one that gave the focus to this table. // Not checking for this would cause a single click on the inactive table's current row to trigger // the filename/date/permission editor if (hasFocus() && System.currentTimeMillis() - focusGainedTime > 100) { // Create a new thread and sleep long enough to ensure that this click was not the first of a double click new Thread() { @Override public void run() { try { sleep(800); } catch (InterruptedException e) {} // Do not execute this block (cancel editing) if: // - a double click was made in the last second // - current row changed // - isEditing() is true which could happen if multiple clicks were made if ((System.currentTimeMillis() - lastDoubleClickTimestamp) > 1000 && row == currentRow) { if (column == Column.NAME) { if(!isEditing()) editCurrentFilename(); } else if(column == Column.DATE) { ActionManager.performAction(com.mucommander.ui.action.impl.ChangeDateAction.Descriptor.ACTION_ID, mainFrame); } else if(column == Column.PERMISSIONS) { if(getSelectedFile().getChangeablePermissions().getIntValue()!=0) ActionManager.performAction(com.mucommander.ui.action.impl.ChangePermissionsAction.Descriptor.ACTION_ID, mainFrame); } } } }.start(); } } } // Double-clicking on a row opens the file/folder else if (doubleClickCounter == 2) { // Note: user can double-click multiple times this.lastDoubleClickTimestamp = System.currentTimeMillis(); ActionManager.performAction(e.isShiftDown() ?com.mucommander.ui.action.impl.OpenNativelyAction.Descriptor.ACTION_ID :com.mucommander.ui.action.impl.OpenAction.Descriptor.ACTION_ID , mainFrame); } } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { // Discard mouse events while in 'no events mode' if(mainFrame.getNoEventsMode()) return; if(e.getSource()!=this) return; // Right-click brings a contextual popup menu if(DesktopManager.isRightMouseButton(e)) { // Find the row that was right-clicked int x = e.getX(); int y = e.getY(); int clickedRow = rowAtPoint(new Point(x,y)); // Does the row correspond to the parent '..' folder ? boolean parentFolderClicked = clickedRow==0 && tableModel.hasParentFolder(); // Select clicked row if it is not selected already if(currentRow!=clickedRow) selectRow(clickedRow); // Request focus on this FileTable is focus is somewhere else if(!hasFocus()) requestFocus(); // Popup menu where the user right-clicked new TablePopupMenu(mainFrame, folderPanel.getCurrentFolder(), parentFolderClicked?null:tableModel.getFileAtRow(clickedRow), parentFolderClicked, tableModel.getMarkedFiles()).show(this, x, y); } // Middle-click on a row marks or unmarks it // Control left-click also works else if (DesktopManager.isMiddleMouseButton(e)) { // Used by mouseDragged lastDraggedRow = rowAtPoint(e.getPoint()); markOnRightClick = !tableModel.isRowMarked(lastDraggedRow); setRowMarked(lastDraggedRow, markOnRightClick); } else if(DesktopManager.isLeftMouseButton(e)) { // Marks a group of rows, from last current row to clicked row (current row) if(e.isShiftDown()) { setRangeMarked(currentRow, lastRow, !tableModel.isRowMarked(currentRow)); } // Marks the clicked row else if (e.isControlDown()) { int rowNum = rowAtPoint(e.getPoint()); setRowMarked(rowNum, !tableModel.isRowMarked(rowNum)); } } } public void mouseReleased(MouseEvent e) { } ///////////////////////////////// // MouseMotionListener methods // ///////////////////////////////// public void mouseDragged(MouseEvent e) { // Discard mouse motion events while in 'no events mode' if(mainFrame.getNoEventsMode()) return; // Marks or unmarks every row that was between the last mouseDragged point // and the current one if (DesktopManager.isMiddleMouseButton(e) && lastDraggedRow!=-1) { int draggedRow = rowAtPoint(e.getPoint()); // Mouse was dragged outside of the FileTable if(draggedRow==-1) return; setRangeMarked(lastDraggedRow, draggedRow, markOnRightClick); lastDraggedRow = draggedRow; } } public void mouseMoved(MouseEvent e) { } ///////////////////////// // KeyListener methods // ///////////////////////// public void keyPressed(KeyEvent e) { } public void keyTyped(KeyEvent e) { } public void keyReleased(KeyEvent e) { // Discard keyReleased events while quick search is active if(quickSearch.isActive()) return; // Test if the event corresponds to the 'Mark/unmark selected file' action keystroke. if(ActionManager.getActionInstance(MarkSelectedFileAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e))) { // Reset variables used to detect repeated key strokes markKeyRepeated = false; lastRowMarked = false; } } ///////////////////////////////// // ActivePanelListener methods // ///////////////////////////////// public void activePanelChanged(FolderPanel folderPanel) { isActiveTable = folderPanel==getFolderPanel(); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers // instead of a custom header renderer. These indicators change when the active table has changed. if(usesTableHeaderRenderingProperties()) setTableHeaderRenderingProperties(); if(isActiveTable) focusGained(); else focusLost(); } /////////////////////////////////// // ConfigurationListener methods // /////////////////////////////////// /** * Listens to certain configuration variables. */ public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); if (var.equals(MuPreferences.DISPLAY_COMPACT_FILE_SIZE)) { FileTableModel.setSizeFormat(event.getBooleanValue()); tableModel.fillCellCache(); resizeAndRepaint(); } else if (var.equals(MuPreferences.DATE_FORMAT) || var.equals(MuPreferences.DATE_SEPARATOR) || var.equals(MuPreferences.TIME_FORMAT)) { // Note: for the update to work properly, CustomDateFormat's configurationChanged() method has to be called // before FileTable's, so that CustomDateFormat gets notified of date format first. // Since listeners are stored by MuConfiguration in a hash map, order is pretty much random. // So CustomDateFormat#updateDateFormat() has to be called before to ensure that is uses the new date format. CustomDateFormat.updateDateFormat(); tableModel.fillCellCache(); resizeAndRepaint(); } // Repaint file icons if their size has changed else if (var.equals(MuPreferences.TABLE_ICON_SCALE)) { // Recalcule row height, revalidate and repaint the table setRowHeight(); } // Repaint file icons if the system file icons policy has changed else if (var.equals(MuPreferences.USE_SYSTEM_FILE_ICONS)) repaint(); } /** * <p>A Custom CellEditor which provides the following functionalities: * <ul> * <li>Filename selection (without extension) when filename starts being edited. * <li>Can be cancelled by pressing ESCAPE * <li>Starts renaming the file when ENTER is pressed * </ul> * * <p>Only once instance per FileTable is created. * * <p><b>Implementation note:</b> stopCellEditing() and cancelCellEditing() should not be overridden to detect * accept/cancel user events as they are totally unrealiable and often not called, for example when clicking * on one of the table's headers (many other cases). */ private class FilenameEditor extends DefaultCellEditor { private JTextField filenameField; /** Row that is currently being edited */ private int editingRow; /** * Creates a new FilenameEditor instance. * * @param textField the text field to use for editing filenames */ public FilenameEditor(JTextField textField) { super(textField); this.filenameField = textField; // Sets the font to the same one that's used for cell rendering (user-defined) filenameField.setFont(FileTableCellRenderer.getCellFont()); textField.addKeyListener( new KeyAdapter() { // Cancel editing when escape key pressed, this is unfortunately not DefaultCellEditor's // default behavior @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if(keyCode == KeyEvent.VK_ESCAPE) cancelCellEditing(); } } ); textField.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { rename(); } }); textField.addFocusListener(new FocusListener() { public void focusLost(FocusEvent e) { cancelCellEditing(); FileTable.this.repaint(); } public void focusGained(FocusEvent e) {} }); } /** * Renames the currently edited name cell, only if the filename has changed. */ private void rename() { String newName = filenameField.getText(); AbstractFile fileToRename = tableModel.getFileAtRow(editingRow); if(!newName.equals(fileToRename.getName())) { AbstractFile current; current = folderPanel.getCurrentFolder(); // Starts moving files ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("move_dialog.moving")); FileSet files = new FileSet(current); files.add(fileToRename); MoveJob renameJob = new MoveJob(progressDialog, mainFrame, files, current, newName, FileCollisionDialog.ASK_ACTION, true); progressDialog.start(renameJob); } } /** * Restores default row height. */ /* public void restore() { // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called. // Add to that the fact that DefaultCellEditor's stopCellEditing() and cancelCellEditing() are not always called, for instance when table header is clicked. // setRowHeight(currentRow, cellRenderer.getFontMetrics(cellRenderer.getCellFont()).getHeight()+cellRenderer.CELL_BORDER_HEIGHT); } */ /** * Notifies this editor that the given row's filename cell is being edited. This method has to be called once * when a row just started being edited. It will save the row number and select the filename without * its extension to make it easier to rename. * * @param row row which is being edited * @see AbstractCopyDialog#selectDestinationFilename(AbstractFile, String, int) */ public void notifyEditingRow(int row) { // The editing row has to be saved as it could change after row editing has been started this.editingRow = row; AbstractFile file = tableModel.getFileAtRow(editingRow); AbstractCopyDialog.selectDestinationFilename(file, file.getName(), 0).feedToPathField(filenameField); // Request focus on text field filenameField.requestFocus(); } } /** * This inner class adds 'quick search' functionality to the FileTable */ private class FileTableQuickSearch extends QuickSearch<AbstractFile> { /** * Creates a new QuickSearch instance, only one instance per FileTable should be created. */ private FileTableQuickSearch() { super(FileTable.this); } @Override protected void searchStarted() { // Repaint the table to add the 'dim' effect on non-matching files scrollpaneWrapper.dimBackground(); } @Override protected void searchStopped() { mainFrame.getStatusBar().updateSelectedFilesInfo(); // Removes the 'dim' effect on non-matching files. scrollpaneWrapper.undimBackground(); } @Override protected int getNumOfItems() { return tableModel.getRowCount(); } @Override protected String getItemString(int index) { return getFileNameAtRow(index); } @Override protected void searchStringBecameEmpty(String searchString) { mainFrame.getStatusBar().setStatusInfo(searchString); // TODO: is needed? } @Override protected void matchFound(int row, String searchString) { // Select best match's row if(row!=currentRow) { selectRow(row); //centerRow(); } // Display the new search string in the status bar // that indicates that the search has yielded a match mainFrame.getStatusBar().setStatusInfo(searchString, IconManager.getIcon(IconManager.STATUS_BAR_ICON_SET, QUICK_SEARCH_OK_ICON), false); } @Override protected void matchNotFound(String searchString) { // No file matching the search string, display the new search string with an icon // that indicates that the search has failed mainFrame.getStatusBar().setStatusInfo(searchString, IconManager.getIcon(IconManager.STATUS_BAR_ICON_SET, QUICK_SEARCH_KO_ICON), false); } /////////////////////////////// // KeyAdapter implementation // /////////////////////////////// @Override public synchronized void keyPressed(KeyEvent e) { // Discard key events while in 'no events mode' if(mainFrame.getNoEventsMode()) return; char keyChar = e.getKeyChar(); // If quick search is not active... if (!isActive()) { // Return (do not start quick search) if the key is not a valid quick search input if(!isValidQuickSearchInput(e)) return; // Return (do not start quick search) if the typed key corresponds to a registered action's accelerator if(ActionKeymap.isKeyStrokeRegistered(KeyStroke.getKeyStrokeForEvent(e))) return; // Start the quick search and continue to process the current key event start(); } // At this point, quick search is active int keyCode = e.getKeyCode(); boolean keyHasModifiers = (e.getModifiersEx()&(KeyEvent.SHIFT_DOWN_MASK|KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.META_DOWN_MASK))!=0; // Backspace removes the last character of the search string if(keyCode==KeyEvent.VK_BACK_SPACE && !keyHasModifiers) { // Search string is empty already if(isSearchStringEmpty()) return; removeLastCharacterFromSearchString(); // Find the row that best matches the new search string and select it findMatch(0, true, true); } // Escape immediately cancels the quick search else if(keyCode==KeyEvent.VK_ESCAPE && !keyHasModifiers) { stop(); } // Up/Down jumps to previous/next match // Shift+Up/Shift+Down marks currently selected file and jumps to previous/next match else if((keyCode==KeyEvent.VK_UP || keyCode==KeyEvent.VK_DOWN) && !keyHasModifiers) { // Find the first row before/after the current row that matches the search string boolean down = keyCode==KeyEvent.VK_DOWN; findMatch(currentRow + (down ? 1 : -1), down, false); } // MarkSelectedFileAction and MarkNextRowAction mark the current row and moves to the next match else if(ActionManager.getActionInstance(MarkSelectedFileAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e)) || ActionManager.getActionInstance(MarkNextRowAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e))) { if(!isParentFolderSelected()) // Don't mark/unmark the '..' file setRowMarked(currentRow, !tableModel.isRowMarked(currentRow)); // Find the first the next row that matches the search string findMatch(currentRow+1, true, false); } // MarkPreviousRowAction marks the current row and moves to the previous match else if(ActionManager.getActionInstance(MarkPreviousRowAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e))) { if(!isParentFolderSelected()) // Don't mark/unmark the '..' file setRowMarked(currentRow, !tableModel.isRowMarked(currentRow)); // Find the first the previous row that matches the search string findMatch(currentRow-1, false, false); } // If no modifier other than Shift is pressed and the typed character is not a control character (space is ok) // and a valid Unicode character, add it to the current search string else if(isValidQuickSearchInput(e)) { appendCharacterToSearchString(keyChar); // Find the row that best matches the new search string and select it findMatch(0, true, true); } else { // Test if the typed key combination corresponds to a registered action. // If that's the case, the quick search is canceled and the action is performed. String muActionId = ActionKeymap.getRegisteredActionIdForKeystroke(KeyStroke.getKeyStrokeForEvent(e)); if(muActionId!=null) { // Consume the key event otherwise it would be fired again on the FileTable // (or any other KeyListener on this FileTable) e.consume(); // Cancel quick search stop(); // Perform the action ActionManager.getActionInstance(muActionId, mainFrame).performAction(); } // Do not update last search string's change timestamp return; } // Update last search string's change timestamp setLastSearchStringChange(System.currentTimeMillis()); } } // End of QuickSearch class // - Theme listening ------------------------------------------------------------- // ------------------------------------------------------------------------------- /** * Not used. */ public void colorChanged(ColorChangedEvent event) {} /** * Receives theme font changes notifications. */ public void fontChanged(FontChangedEvent event) { if(event.getFontId() == Theme.FILE_TABLE_FONT) { // Changes filename editor's font filenameEditor.filenameField.setFont(event.getFont()); // Recalcule row height, revalidate and repaint the table setRowHeight(); } } public FileTableConfiguration getConfiguration() { return getFileTableColumnModel().getConfiguration(); } public int getColumnWidth(Column column) { return getFileTableColumnModel().getColumnFromId(column.ordinal()).getWidth(); } /** * This thread performs the change of current folder. * * @author Nicolas Rinaudo, Maxence Bernard */ private class FolderChangeThread implements Runnable { private AbstractFile folder; private AbstractFile[] children; private FileSet markedFiles; private AbstractFile selectedFile; private FolderChangeThread(AbstractFile folder, AbstractFile[] children, FileSet markedFiles, AbstractFile selectedFile) { this.folder = folder; this.children = children; this.markedFiles = markedFiles; this.selectedFile = selectedFile; } public void run() { try { // Set the new current folder. tableModel.setCurrentFolder(folder, children); // Update the visibility state of conditional columns FileTableColumnModel columnModel = getFileTableColumnModel(); updateColumnsVisibility(); // The column corresponding to the current 'sort by' criterion may have become invisible. // If that is the case, change the criterion to NAME. if(!columnModel.isColumnVisible(sortInfo.getCriterion())) { sortInfo.setCriterion(Column.NAME); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if(usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } } // Sort the new folder using the current sort criteria, ascending/descending order and // 'show folders first' values. tableModel.sortRows(); // Computes the index of the new row selection. int rowToSelect; if(selectedFile!=null) { // Tries to find the index of the file to select. If it cannot be found (the file might not // exist anymore, for example), use the closest possible row. if((rowToSelect = tableModel.getFileRow(selectedFile)) == -1) { int rowCount = tableModel.getRowCount(); rowToSelect = currentRow < rowCount ? currentRow : rowCount - 1; } } // If no file was marked as needing to be selected, selects the first line. else { rowToSelect = 0; } selectRow(currentRow = rowToSelect); fireSelectedFileChangedEvent(); // Restore previously marked files (if any / current folder hasn't changed) if(markedFiles != null) { // Restore previsouly marked files int nbMarkedFiles = markedFiles.size(); int fileRow; for(int i =0; i < nbMarkedFiles; i++) { fileRow = tableModel.getFileRow(markedFiles.elementAt(i)); if(fileRow != -1) tableModel.setRowMarked(fileRow, true); } // Notify registered listeners that currently marked files have changed on this FileTable fireMarkedFilesChangedEvent(); } resizeAndRepaint(); } catch(Throwable e) { // While no such thing should happen, we want to make absolutely sure no exception // is propagated to the AWT event dispatch thread. LOGGER.warn("Caught exception while changing folder, this should not happen!", e); } finally { // Notify #setCurrentFolder that we're done changing the folder. synchronized(this) { notify(); } } } } }