net.sourceforge.docfetcher.view.ScopeGroup.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.docfetcher.view.ScopeGroup.java

Source

/*******************************************************************************
 * Copyright (c) 2007, 2008 Tran Nam Quang.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Tran Nam Quang - initial API and implementation
 *******************************************************************************/

package net.sourceforge.docfetcher.view;

import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import net.sourceforge.docfetcher.Const;
import net.sourceforge.docfetcher.DocFetcher;
import net.sourceforge.docfetcher.enumeration.Key;
import net.sourceforge.docfetcher.enumeration.Msg;
import net.sourceforge.docfetcher.enumeration.Pref;
import net.sourceforge.docfetcher.model.Indexable;
import net.sourceforge.docfetcher.model.Job;
import net.sourceforge.docfetcher.model.ResultDocument;
import net.sourceforge.docfetcher.model.RootScope;
import net.sourceforge.docfetcher.model.Scope;
import net.sourceforge.docfetcher.model.ScopeRegistry;
import net.sourceforge.docfetcher.util.Event;
import net.sourceforge.docfetcher.util.UtilFile;
import net.sourceforge.docfetcher.util.UtilGUI;
import net.sourceforge.docfetcher.util.UtilList;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeViewerListener;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreeExpansionEvent;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;

/**
 * @author Tran Nam Quang
 */
public class ScopeGroup extends GroupWrapper {

    public final Event<ResultDocument[]> evtListDocuments = new Event<ResultDocument[]>();

    private CheckboxTreeViewer viewer;
    private ViewerMenuManager viewerMenu;

    public ScopeGroup(Composite parent) {
        super(parent);
        group.setText(Msg.search_scope.value());
        group.setLayout(FillLayoutFactory.getInst().margin(Const.GROUP_MARGIN).create());
        viewer = new CheckboxTreeViewer(group, SWT.BORDER | SWT.MULTI);
        initViewer();
        initDragAndDrop();

        viewerMenu = new ViewerMenuManager(viewer);
        viewerMenu.setRootChecker(new ViewerMenuManager.RootChecker() {
            public boolean isRoot(Object obj) {
                return obj instanceof RootScope;
            }
        });
        viewerMenu.addUnmanagedAction(new CreateIndexAction(), Key.Insert);
        viewerMenu.addSeparator();
        viewerMenu.addRootAction(new UpdateIndexAction(), Key.Update);
        viewerMenu.addRootAction(new RebuildIndexAction(), null);
        viewerMenu.addSeparator();
        viewerMenu.addRootAction(new RemoveIndexAction(), Key.Delete);
        viewerMenu.addUnmanagedAction(new RemoveMissingAction(), null);
        viewerMenu.addSeparator();
        viewerMenu.addUnmanagedAction(new CheckAllAction(true), null);
        viewerMenu.addUnmanagedAction(new CheckAllAction(false), null);
        viewerMenu.addSeparator();
        viewerMenu.addNonEmptyAction(new CheckFlatAction(true), null);
        viewerMenu.addNonEmptyAction(new CheckFlatAction(false), null);
        viewerMenu.addSeparator();
        viewerMenu.addNonEmptyAction(new OpenDirectoryAction(), Key.Enter);
        viewerMenu.addNonEmptyAction(new ListDocumentsAction(), null);
        if (Pref.Bool.AllowRepositoryModification.getValue()) {
            viewerMenu.addSingleElementAction(new CreateSubfolderAction(), Key.ShiftInsert);
            viewerMenu.addSingleElementAction(new RenameFolderAction(), Key.Rename);
            viewerMenu.addNonEmptyAction(new DeleteFolderAction(), Key.ShiftDelete);
            viewerMenu.addSingleElementAction(new PasteIntoFolderAction(), Key.Paste);
        }
        viewerMenu.addSeparator();
        viewerMenu.addRootAction(new ToggleDeleteOnExitAction(), null);
        viewerMenu.setManagedActionsEnabled(false);

        // Update self on add/remove in scope registry
        ScopeRegistry.getInstance().getEvtRegistryChanged().add(new Event.Listener<ScopeRegistry>() {
            public void update(ScopeRegistry scopeReg) {
                Object[] expandedElements = viewer.getExpandedElements();
                setScopes(scopeReg.getEntries());
                viewer.setExpandedElements(expandedElements);
                updateVisibleCheckStates(new Object[0]);
            }
        });
    }

    private void initViewer() {
        // Remove selection when viewer loses focus
        viewer.getTree().addFocusListener(new FocusAdapter() {
            public void focusLost(FocusEvent e) {
                ((Tree) e.widget).deselectAll();

                /*
                 * We have to deactivate the context menu entries manually since
                 * the deselectAll call will not cause a SelectionChangedEvent.
                 */
                viewerMenu.setManagedActionsEnabled(false);
            }
        });

        viewer.setContentProvider(new ITreeContentProvider() {
            public Object[] getElements(Object inputElement) {
                return (RootScope[]) inputElement;
            }

            public Object[] getChildren(Object parentElement) {
                return ((Scope) parentElement).getChildren();
            }

            public boolean hasChildren(Object element) {
                return getChildren(element).length > 0;
            }

            public Object getParent(Object element) {
                return ((Indexable) element).getParent();
            }

            public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            }

            public void dispose() {
            }
        });

        class MyStyledCellLabelProvider extends StyledCellLabelProvider {
            private Styler styler;

            public MyStyledCellLabelProvider() {
                styler = new Styler() {
                    public void applyStyles(TextStyle textStyle) {
                        textStyle.foreground = UtilGUI.getColor(SWT.COLOR_DARK_GRAY);
                    }
                };
            }

            public void update(ViewerCell cell) {
                Indexable scope = (Indexable) cell.getElement();
                File file = scope.getFile();
                String label = file.getName();
                if (label.equals("")) // applies to root directories, i.e. "C:\\" //$NON-NLS-1$
                    label = file.getAbsolutePath();

                // "temp" prefix for RootScopes that will be deleted on exit
                if (scope instanceof RootScope)
                    if (((RootScope) scope).isDeleteOnExit())
                        label = "[X]  " + label; //$NON-NLS-1$

                StyledString styledString = new StyledString(label, null);
                if (scope instanceof RootScope) {
                    String path = file.getAbsolutePath();
                    styledString.append(" --- " + path, styler); //$NON-NLS-1$
                }
                cell.setText(styledString.toString());
                cell.setStyleRanges(styledString.getStyleRanges());
            }

            protected void measure(org.eclipse.swt.widgets.Event event, Object element) {
                super.measure(event, element);
            }
        }
        viewer.setLabelProvider(new MyStyledCellLabelProvider());

        viewer.setSorter(new ViewerSorter() {
            public int compare(Viewer viewer, Object e1, Object e2) {
                String name1 = ((Scope) e1).getFile().getName();
                String name2 = ((Scope) e2).getFile().getName();
                return name1.compareToIgnoreCase(name2);
            }
        });

        /*
         * Because the check states of the viewer items are maintained manually,
         * they have to be kept in sync with tree expansion events.
         */
        viewer.addTreeListener(new ITreeViewerListener() {
            public void treeExpanded(TreeExpansionEvent event) {
                Indexable[] newScopes = ((Scope) event.getElement()).getChildren();
                updateVisibleCheckStates(newScopes);
            }

            public void treeCollapsed(TreeExpansionEvent event) {
                // Do nothing
            }
        });

        /*
         * Check/uncheck children of checked/unchecked item and propagate
         * checkstate changes in viewer back to the model.
         */
        viewer.addCheckStateListener(new ICheckStateListener() {
            public void checkStateChanged(CheckStateChangedEvent event) {
                Event.hold();
                ((Scope) event.getElement()).setCheckedDeep(event.getChecked());
                Event.flush();
                updateVisibleCheckStates(new Object[] { event.getElement() });
            }

        });

        // Doubleclick on item in viewer results in expand/collapse of its children
        viewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                Object[] objects = ((StructuredSelection) viewer.getSelection()).toArray();
                if (objects.length != 1)
                    return;
                viewer.setExpandedState(objects[0], !viewer.getExpandedState(objects[0]));
                updateVisibleCheckStates(new Scope[0]);
            }
        });
    }

    /**
     * Provides drag & drop for this viewer.
     */
    private void initDragAndDrop() {
        int operations = DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT | DND.DROP_LINK;
        Transfer[] types = new Transfer[] { FileTransfer.getInstance(), TextTransfer.getInstance() };

        viewer.addDropSupport(operations, types, new DropTargetAdapter() {

            public void dragEnter(DropTargetEvent event) {
                event.detail = DND.DROP_COPY;
            }

            public void dragOperationChanged(DropTargetEvent event) {
                event.detail = DND.DROP_COPY;
            }

            public void drop(DropTargetEvent event) {
                // Abort if shells other than the main shell and the indexing box are open
                Shell[] shells = Display.getDefault().getShells();
                Shell mainShell = DocFetcher.getInstance().getShell();
                Shell indexingBoxShell = DocFetcher.getInstance().getIndexingDialog().getShell();
                for (Shell shell : shells)
                    if (!shell.equals(mainShell) && !shell.equals(indexingBoxShell))
                        return;

                // Get path strings
                String[] paths = null;
                if (event.data instanceof String)
                    paths = new String[] { (String) event.data };
                else if (event.data instanceof String[])
                    paths = (String[]) event.data;
                else {
                    UtilGUI.showWarningMsg(Msg.invalid_dnd_source.value());
                    return;
                }

                // Abort if drop objects include files or if they're all contained in the ScopeRegistry
                List<RootScope> newScopes = new ArrayList<RootScope>();
                for (String path : paths) {
                    File file = new File(path);
                    if (!file.isDirectory()) {
                        UtilGUI.showWarningMsg(Msg.contains_file.value());
                        return;
                    }
                    newScopes.add(new RootScope(file));
                }
                String msg = ScopeRegistry.getInstance()
                        .checkIntersection(newScopes.toArray(new RootScope[newScopes.size()]));
                if (msg != null) {
                    UtilGUI.showWarningMsg(msg);
                    return;
                }

                // Run indexing
                IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();
                for (RootScope newScope : newScopes)
                    indexingDialog.addJob(new Job(newScope, true, true));
                indexingDialog.open();
            }

        });
    }

    /**
     * Sets the RootScopes to be displayed in this widget. If you want all
     * viewer elements to have the same check state, use
     * <tt>setScopes(boolean, RootScopes[])</tt> instead, because the latter
     * is faster.
     */
    public void setScopes(RootScope... scopes) {
        viewer.setInput(scopes);
        updateVisibleCheckStates(new Object[0]);
    }

    /**
     * Updates the check states of the visible viewer elements and the elements
     * corresponding to the Scopes given in and under <tt>includedScopes</tt>
     * according to the check states in the model. All other elements are left
     * unchecked.
     * <p>
     * The <tt>includedScopes</tt> parameter's purpose is to include Scopes
     * which aren't expanded yet, but will be expanded some time after the call
     * of this method. In particular, TreeEvents (expand or collapse) indicate
     * which elements are <i>going</i> to be expanded.
     */
    private void updateVisibleCheckStates(Object[] includedScopes) {
        List<Scope> checkedItems = new ArrayList<Scope>();
        for (Object includedItem : includedScopes) {
            Scope scope = (Scope) includedItem;
            if (scope.isChecked())
                checkedItems.add(scope);
            addVisibleCheckedChildren(scope, checkedItems);
        }
        for (RootScope rootScope : ScopeRegistry.getInstance().getEntries()) {
            if (rootScope.isChecked())
                checkedItems.add(rootScope);
        }
        Object[] expandedItems = viewer.getVisibleExpandedElements();
        for (Object expandedItem : expandedItems) {
            for (Scope subScope : ((Scope) expandedItem).getChildren()) {
                if (subScope.isChecked())
                    checkedItems.add(subScope);
            }
        }
        viewer.setCheckedElements(checkedItems.toArray());
    }

    /**
     * Update the check states displayed in this widget. This method must be
     * called manually after altering the check states in the model because
     * efficiently detecting these changes is too complicated.
     */
    public void updateCheckStates() {
        updateVisibleCheckStates(new Scope[0]);
    }

    /**
     * Adds all Scopes under the given Scope which are visible and checked
     * according to the model to the given list of Scopes.
     */
    private void addVisibleCheckedChildren(Scope scope, List<Scope> output) {
        if (!viewer.getExpandedState(scope))
            return;
        for (Scope subScope : scope.getChildren()) {
            if (subScope.isChecked())
                output.add(subScope);
            addVisibleCheckedChildren(subScope, output);
        }
    }

    /**
     * Returns the currently selected <tt>RootScope</tt>s. Shows a warning
     * message if some of them don't exist anymore.
     */
    private RootScope[] getExistingRootSelection() {
        // Get selected RootScopes
        StructuredSelection sel = (StructuredSelection) viewer.getSelection();
        List<RootScope> selRootScopes = new ArrayList<RootScope>(sel.size());
        Iterator<?> it = sel.iterator();
        while (it.hasNext()) {
            Object item = it.next();
            if (item instanceof RootScope) // Only enable action for RootScopes (not for other Scopes)
                selRootScopes.add((RootScope) item);
        }

        // Separate existing and missing RootScopes
        if (selRootScopes.size() == 0)
            return new RootScope[0];
        List<RootScope> existing = new ArrayList<RootScope>(selRootScopes.size());
        List<RootScope> missing = new ArrayList<RootScope>(selRootScopes.size());
        for (RootScope scope : selRootScopes) {
            if (scope.getFile().exists())
                existing.add(scope);
            else
                missing.add(scope);
        }

        // Show warning message for missing RootScopes
        if (!missing.isEmpty()) {
            String items = "\n" + UtilList.toString("\n", missing); //$NON-NLS-1$ //$NON-NLS-2$
            UtilGUI.showWarningMsg(Msg.folders_not_found.value() + items);
        }

        return existing.toArray(new RootScope[existing.size()]);
    }

    /**
     * Returns the currently selected <tt>Scope</tt>s (including the
     * <tt>RootScope</tt>s). Shows a warning message if some of
     * them don't exist anymore.
     */
    private Scope[] getExistingSelection() {
        // Get selected Scopes
        Object[] objects = ((StructuredSelection) viewer.getSelection()).toArray();
        Scope[] scopes = new Scope[objects.length];
        System.arraycopy(objects, 0, scopes, 0, objects.length);

        // Separate existing and missing Scopes
        if (scopes.length == 0)
            return new Scope[0];
        List<Scope> existing = new ArrayList<Scope>(scopes.length);
        List<Scope> missing = new ArrayList<Scope>(scopes.length);
        for (Scope scope : scopes) {
            if (scope.getFile().exists())
                existing.add(scope);
            else
                missing.add(scope);
        }

        // Show warning message for missing Scopes
        if (!missing.isEmpty()) {
            String items = "\n" + UtilList.toString("\n", missing); //$NON-NLS-1$ //$NON-NLS-2$
            UtilGUI.showWarningMsg(Msg.folders_not_found.value() + items);
        }

        return existing.toArray(new Scope[existing.size()]);
    }

    /**
     * Performs an index update for all selected <tt>RootScope</tt>s. It
     * checks whether the index folders still exist. If not, an error message is
     * displayed.
     */
    private void updateSelectedIndexes(boolean doRebuild) {
        RootScope[] selRootScopes = getExistingRootSelection();
        if (selRootScopes.length == 0)
            return;
        IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();
        for (RootScope scope : selRootScopes)
            indexingDialog.addJob(new Job(scope, false, doRebuild));
        indexingDialog.open();
    }

    public boolean setFocus() {
        return viewer.getControl().setFocus();
    }

    /**
     * Action to add scopes to the search scope.
     */
    class CreateIndexAction extends Action {
        public CreateIndexAction() {
            setText(Msg.create_index.value());
            setAccelerator(Key.Insert.keyCode);
        }

        public void run() {
            IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();
            if (!indexingDialog.addJobFromDialog())
                indexingDialog.close();
            else
                indexingDialog.open();
        }
    }

    /**
     * Action to remove currently selected scopes.
     */
    class RemoveIndexAction extends Action {
        public RemoveIndexAction() {
            setText(Msg.remove_index.value());
            setAccelerator(Key.Delete.keyCode);
        }

        public void run() {
            // Get RootScope selection
            StructuredSelection sel = (StructuredSelection) viewer.getSelection();
            List<RootScope> selRootScopes = new ArrayList<RootScope>(sel.size());
            Iterator<?> it = sel.iterator();
            while (it.hasNext()) {
                Object item = it.next();
                if (item instanceof RootScope)
                    selRootScopes.add((RootScope) item);
            }

            /*
             * This check is needed because we don't want the message box to pop up
             * when the user hits DELETE without selecting anything.
             */
            if (selRootScopes.size() == 0)
                return;
            int ans = UtilGUI.showConfirmMsg(Msg.remove_sel_indexes.value());
            if (ans == SWT.OK)
                ScopeRegistry.getInstance().remove(selRootScopes.toArray(new RootScope[selRootScopes.size()]));
        }
    }

    /**
     * Action to remove indexes whose folders are missing.
     */
    class RemoveMissingAction extends Action {
        public RemoveMissingAction() {
            setText(Msg.remove_orphaned_indexes.value());
        }

        public void run() {
            ScopeRegistry scopeReg = ScopeRegistry.getInstance();
            RootScope[] rootScopes = scopeReg.getEntries();
            if (rootScopes.length == 0)
                return;

            int ans = UtilGUI.showConfirmMsg(Msg.remove_orphaned_indexes_msg.value());
            if (ans != SWT.OK)
                return;
            for (RootScope rootScope : rootScopes)
                if (!rootScope.getFile().exists())
                    scopeReg.remove(rootScope);
        }
    }

    /**
     * Action to find and index new files in the selected scopes.
     */
    class UpdateIndexAction extends Action {
        public UpdateIndexAction() {
            setText(Msg.update_index.value());
            setAccelerator(Key.Update.keyCode);
        }

        public void run() {
            updateSelectedIndexes(false);
        }
    }

    /**
     * Action to fully reindex the selected scopes.
     */
    class RebuildIndexAction extends Action {
        public RebuildIndexAction() {
            setText(Msg.rebuild_index.value());
        }

        public void run() {
            updateSelectedIndexes(true);
        }
    }

    /**
     * Action to check or uncheck all scopes.
     */
    class CheckAllAction extends Action {

        /**
         * Whether this Action checks or unchecks all Scopes.
         */
        private boolean checked = true;

        public CheckAllAction(boolean checked) {
            setText(checked ? Msg.check_all.value() : Msg.uncheck_all.value());
            this.checked = checked;
        }

        public void run() {
            Event.hold();
            for (RootScope rootScope : ScopeRegistry.getInstance().getEntries())
                rootScope.setCheckedDeep(checked);
            Event.flush();
            updateVisibleCheckStates(new Scope[0]);
        }

    }

    /**
     * Action to check/uncheck the selected scopes only. This class
     * can turn into a Check All or Uncheck All action, depending on the Boolean
     * it is initialized with.
     */
    class CheckFlatAction extends Action {

        /**
         * Whether this Action checks or unchecks selected Scopes.
         */
        private boolean checked = true;

        /**
         * @param select true: CheckFlatAction; false: UncheckFlatAction
         */
        public CheckFlatAction(boolean checked) {
            setText(checked ? Msg.check_toplevel_only.value() : Msg.uncheck_toplevel_only.value());
            this.checked = checked;
        }

        public void run() {
            IStructuredSelection sel = (IStructuredSelection) viewer.getSelection();
            Iterator<?> it = sel.iterator();
            Event.hold();
            while (it.hasNext())
                ((Scope) it.next()).setChecked(checked);
            Event.flush();
            updateVisibleCheckStates(sel.toArray());
        }

    }

    /**
     * An action class that opens the directories corresponding to the Scopes
     * selected in the Scope group.
     */
    class OpenDirectoryAction extends Action {
        public OpenDirectoryAction() {
            setText(Msg.open_folder.value());
            setAccelerator(Key.Enter.getAccelerator());
        }

        public void run() {
            Scope[] scopes = getExistingSelection();
            if (scopes.length == 0)
                return;
            int openLimit = Pref.Int.OpenLimit.getValue();
            if (scopes.length > openLimit) {
                UtilGUI.showInfoMsg(Msg.open_limit.format(openLimit));
                return;
            }
            for (Scope scope : scopes)
                UtilFile.launch(scope.getFile().getAbsolutePath());
        }
    }

    /**
     * An action class that lists all documents under the selected
     * <tt>RootScope</tt>s.
     */
    class ListDocumentsAction extends Action {
        public ListDocumentsAction() {
            setText(Msg.list_docs.value());
        }

        public void run() {
            // Get selected scopes
            final Scope[] scopes = getExistingSelection();
            if (scopes.length == 0)
                return;

            // Check all selected scopes
            Event.hold();
            for (Scope scope : scopes)
                scope.setCheckedDeep(true);
            Event.flush();
            updateVisibleCheckStates(new Scope[0]);

            // Display documents in result panel
            new Thread() {
                public void run() {
                    final ResultDocument[] docs = RootScope.listDocuments(scopes);
                    Display.getDefault().syncExec(new Runnable() {
                        public void run() {
                            evtListDocuments.fireUpdate(docs);
                        }
                    });
                }
            }.start();
        }
    }

    /**
     * Action class for creating a subfolder in the currently selected folder.
     */
    class CreateSubfolderAction extends Action {
        public CreateSubfolderAction() {
            setText(Msg.create_subfolder.value());
            setAccelerator(Key.ShiftInsert.getAccelerator());
        }

        public void run() {
            Scope[] scopes = getExistingSelection();
            if (scopes.length == 0)
                return;
            RootScope rootScope = scopes[0].getRootScope();
            File parentFolder = scopes[0].getFile();

            String suggestedName = UtilFile.suggestNewSubfolderName(parentFolder);
            InputDialog inputDialog = new InputDialog(getShell(), Msg.enter_folder_name.value(),
                    Msg.enter_folder_name_new.value(), suggestedName);
            String newFolderName = inputDialog.open();
            if (newFolderName == null)
                return;
            File newFolder = new File(parentFolder.getAbsolutePath(), newFolderName);
            if (newFolder.exists()) {
                UtilGUI.showInfoMsg(Msg.folder_already_exists.value());
                return;
            }

            // Create folder
            DocFetcher.getInstance().setWatchEnabled(false, rootScope);
            boolean success = newFolder.mkdir();
            DocFetcher.getInstance().setWatchEnabled(Pref.Bool.WatchFS.getValue(), rootScope);

            if (!success) {
                UtilGUI.showErrorMsg(Msg.create_subfolder_failed.value());
            } else {
                // Update indexes, but silently
                IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();
                indexingDialog.addJob(new Job(rootScope, false, false));
            }
        }
    }

    /**
     * Action class for renaming the currently selected folder.
     */
    class RenameFolderAction extends Action {
        public RenameFolderAction() {
            setText(Msg.rename_folder.value());
            setAccelerator(Key.Rename.keyCode);
        }

        public void run() {
            Scope[] scopes = getExistingSelection();
            if (scopes.length == 0)
                return;
            RootScope rootScope = scopes[0].getRootScope();
            File targetFolder = scopes[0].getFile();
            IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();

            // Show confirmation message for root folders
            if (scopes[0] instanceof RootScope) {
                int ans = UtilGUI.showConfirmMsg(Msg.rename_requires_full_rebuild.value());
                if (ans != SWT.OK)
                    return;
            }

            // Get new name from input dialog
            InputDialog inputDialog = new InputDialog(getShell(), Msg.enter_folder_name.value(),
                    Msg.enter_new_foldername.value(), targetFolder.getName());
            String input = inputDialog.open();
            if (input == null)
                return;
            if (targetFolder.getName().equals(input))
                return;

            // Try to rename file
            File parentFile = UtilFile.getParentFile(targetFolder);
            File newFile = new File(parentFile.getAbsolutePath(), input);
            DocFetcher.getInstance().setWatchEnabled(false, rootScope);
            boolean success = targetFolder.renameTo(newFile);
            DocFetcher.getInstance().setWatchEnabled(Pref.Bool.WatchFS.getValue(), rootScope);
            if (!success) {
                UtilGUI.showErrorMsg(Msg.cant_rename_folder.value());
                return;
            }

            // Update or rebuild index
            if (scopes[0] instanceof RootScope) {
                // Rebuild index if a root folder was renamed
                ScopeRegistry.getInstance().remove(rootScope);
                RootScope newRootScope = new RootScope(newFile);
                Job job = new Job(newRootScope, true, true);
                job.setReadyForIndexing(true);
                indexingDialog.addJob(job);
                indexingDialog.open();
            } else {
                // Just update the index for non-root folders
                indexingDialog.addJob(new Job(rootScope, false, false));
                indexingDialog.open();
            }
        }
    }

    /**
     * Action class for removing the currently selected folder.
     */
    class DeleteFolderAction extends Action {
        public DeleteFolderAction() {
            setText(Msg.delete_folder.value());
            setAccelerator(Key.ShiftDelete.getAccelerator());
        }

        public void run() {
            Scope[] scopes = getExistingSelection();
            if (scopes.length == 0)
                return;
            final List<Scope> scopeList = UtilList.toList(scopes);

            // Remove entries from deletion list that are inside other to be deleted items
            final UtilList.Equality<Scope, Scope> parentChildMapper = new UtilList.Equality<Scope, Scope>() {
                public boolean equals(Scope obj1, Scope obj2) {
                    return UtilFile.contains(obj1.getFile(), obj2.getFile());
                }
            };
            UtilList.remove(scopeList, new UtilList.Selector<Scope>() {
                public boolean select(Scope obj) {
                    return UtilList.containsEquality(scopeList, obj, parentChildMapper);
                }
            });

            // Ask user to confirm operation
            String msg = Msg.delete_folder_q.value() + Const.LS + UtilList.toString(Const.LS, scopeList);
            int ans = UtilGUI.showConfirmMsg(msg);
            if (ans != SWT.OK)
                return;

            // Do it
            Set<Job> jobs = new HashSet<Job>(scopeList.size());
            for (Scope scope : scopeList) {
                RootScope rootScope = scope.getRootScope();
                File targetFolder = scope.getFile();
                DocFetcher.getInstance().setWatchEnabled(false, rootScope);
                if (scope instanceof RootScope) {
                    ScopeRegistry.getInstance().remove(rootScope);
                    UtilFile.delete(targetFolder, true);
                } else {
                    UtilFile.delete(targetFolder, true);
                    DocFetcher.getInstance().setWatchEnabled(Pref.Bool.WatchFS.getValue(), rootScope);
                    jobs.add(new Job(rootScope, false, false));
                }
            }
            IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();
            for (Job job : jobs)
                indexingDialog.addJob(job);
        }
    }

    /**
     * An Action class that moves all files from the clipboard into the selected
     * folder.
     */
    class PasteIntoFolderAction extends Action {
        public PasteIntoFolderAction() {
            setText(Msg.paste_into_folder.value());
            setAccelerator('V' | Key.Paste.stateMask);
        }

        public void run() {
            // Get new parent directory to move the files to
            Scope[] scopes = getExistingSelection();
            if (scopes.length == 0)
                return;
            File newParent = scopes[0].getFile();
            RootScope rootScopeToUpdate = scopes[0].getRootScope();

            // Get files from clipboard
            Clipboard cb = DocFetcher.getInstance().getClipboard();
            File[] files = null;
            if (Const.IS_WINDOWS) {
                TransferData[] types = cb.getAvailableTypes();
                boolean enabled = false;
                for (TransferData type : types) {
                    if (FileTransfer.getInstance().isSupportedType(type)) {
                        enabled = true;
                        break;
                    }
                }
                Object data = cb.getContents(FileTransfer.getInstance());
                enabled |= data == null;
                enabled |= !(data instanceof String[]);
                if (!enabled) {
                    UtilGUI.showWarningMsg(Msg.no_files_in_cb.value());
                    return;
                }
                String[] filepaths = (String[]) data;
                files = UtilFile.toFiles(filepaths);
            }
            /*
             * FIXME On Linux, the FileTransfer doesn't work for some unknown
             * reason.
             */
            else if (Const.IS_LINUX) {
                Object data = cb.getContents(TextTransfer.getInstance());
                if (data == null) {
                    UtilGUI.showWarningMsg(Msg.no_files_in_cb.value());
                    return;
                }
                String[] filePaths = ((String) data).trim().split(Const.LS);
                files = UtilFile.toFiles(filePaths);
                for (File file : files)
                    if (!file.exists()) {
                        UtilGUI.showWarningMsg(Msg.no_files_in_cb.value());
                        return;
                    }
            }

            DocFetcher.getInstance().setWatchEnabled(false, rootScopeToUpdate);

            // Copy files
            FileTransferDialog transferDialog = new FileTransferDialog(getShell(), Msg.file_transfer.value());
            transferDialog.open();
            transferDialog.transferFiles(files, newParent);

            DocFetcher.getInstance().setWatchEnabled(Pref.Bool.WatchFS.getValue(), rootScopeToUpdate);

            // Update indexes, but silently
            IndexingDialog indexingDialog = DocFetcher.getInstance().getIndexingDialog();
            indexingDialog.addJob(new Job(rootScopeToUpdate, false, false));
        }
    }

    class ToggleDeleteOnExitAction extends Action {
        ToggleDeleteOnExitAction() {
            setText(Msg.toggle_delete_on_exit.value());
        }

        public void run() {
            RootScope[] selRootScopes = getExistingRootSelection();
            if (selRootScopes.length == 0)
                return;
            for (RootScope rs : selRootScopes) {
                rs.setDeleteOnExit(!rs.isDeleteOnExit());
                viewer.update(rs, null);
            }
        }
    }

}