name.schedenig.eclipse.grepconsole.view.items.EditableItemsPanel.java Source code

Java tutorial

Introduction

Here is the source code for name.schedenig.eclipse.grepconsole.view.items.EditableItemsPanel.java

Source

/*******************************************************************************
 * Copyright (c) 2008 - 2014 Marian Schedenig
 * 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:
 *    Marian Schedenig - initial API and implementation
 *******************************************************************************/

package name.schedenig.eclipse.grepconsole.view.items;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import name.schedenig.eclipse.grepconsole.Activator;
import name.schedenig.eclipse.grepconsole.adapters.GrepLineStyleListener;
import name.schedenig.eclipse.grepconsole.i18n.Messages;
import name.schedenig.eclipse.grepconsole.model.AbstractGrepModelElement;
import name.schedenig.eclipse.grepconsole.model.GrepExpressionFolder;
import name.schedenig.eclipse.grepconsole.model.GrepExpressionItem;
import name.schedenig.eclipse.grepconsole.model.GrepExpressionRootFolder;
import name.schedenig.eclipse.grepconsole.model.GrepExpressionsWithSelection;
import name.schedenig.eclipse.grepconsole.model.xml.XmlHandler;
import name.schedenig.eclipse.grepconsole.model.xml.XmlHandlerException;
import name.schedenig.eclipse.grepconsole.util.GridLayoutBuilder;
import name.schedenig.eclipse.grepconsole.view.colors.ColorRegistry;
import name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.IItemTreeListener;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MenuAdapter;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

/**
 * Wraps an items tree panel into a panel with edit controls.
 * 
 * @author msched
 */
public class EditableItemsPanel extends Composite implements IItemTreeListener {
    /** Current position of the sash separating the tree from the preview
     *  panel. */
    private Integer sashPosition;

    /** Line style listener used for the preview. */
    private GrepLineStyleListener grepLineStyleListener;

    /** Color registry used for the preview. */
    private ColorRegistry colorRegistry;

    /** Preview panel text. */
    private String previewText;

    /** GUI variables. */
    private Button btnAddFolder;
    private Button btnAddExpression;
    private Button btnEdit;
    private Button btnRemove;
    private ItemsTreePanel treePanel;
    private Clipboard clipboard;
    private Button btnSaveSelected;
    private Button btnSaveAll;
    private Button btnLoad;
    private StyledText stPreview;
    private Menu treeMenu;
    private MenuItem miCut;
    private MenuItem miCopy;
    private MenuItem miPaste;
    private Composite panelTreeAndButtons;
    private Sash sash;
    private Menu menuPreview;
    private MenuItem miAddExpressionFromPreview;
    private MenuItem miLoadDefault;
    private MenuItem miSaveDefault;
    private Label labelPreview;
    private Font headerFont;

    /**
     * Creates a new instance.
     * 
     * @param parent Parent control.
     * @param style Style flags.
     */
    public EditableItemsPanel(Composite parent, int style) {
        super(parent, style);

        init();
    }

    /**
     * Creates the GUI content.
     */
    private void init() {
        addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                colorRegistry.disposeColors();
            }
        });

        headerFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT);// JFaceResources.getHeaderFont();

        colorRegistry = new ColorRegistry(Activator.getDefault().getColorRegistry());
        clipboard = new Clipboard(getDisplay());

        setLayoutData(new GridData(GridData.FILL_BOTH));

        FormLayout layout = new FormLayout();
        setLayout(layout);

        panelTreeAndButtons = new Composite(this, SWT.NONE);
        new GridLayoutBuilder(panelTreeAndButtons, 2, false).setMargins(0).apply();

        treePanel = new ItemsTreePanel(panelTreeAndButtons, SWT.BORDER);
        treePanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10));
        treePanel.addListener(this);

        hookTreeDragAndDrop();

        Tree tree = treePanel.getTree();

        treeMenu = new Menu(tree);
        treeMenu.addMenuListener(new MenuAdapter() {
            @Override
            public void menuShown(MenuEvent e) {
                refreshMenuItems();
            }
        });

        miCut = new MenuItem(treeMenu, SWT.PUSH);
        miCut.setText(Messages.EditableItemsPanel_cut);

        miCut.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doCopy(true);
            }
        });

        miCopy = new MenuItem(treeMenu, SWT.PUSH);
        miCopy.setText(Messages.EditableItemsPanel_copy);

        miCopy.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doCopy(false);
            }
        });

        miPaste = new MenuItem(treeMenu, SWT.PUSH);
        miPaste.setText(Messages.EditableItemsPanel_paste);

        miPaste.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doPaste();
            }
        });

        tree.setMenu(treeMenu);

        tree.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.character == SWT.DEL) {
                    doRemove();
                    e.doit = false;
                } else if (e.keyCode == SWT.F2) {
                    doEdit();
                    e.doit = false;
                } else if (e.keyCode == 'c' && (e.stateMask & SWT.MOD1) != 0) {
                    doCopy(false);
                    e.doit = false;
                } else if (e.keyCode == 'x' && (e.stateMask & SWT.MOD1) != 0) {
                    doCopy(true);
                    e.doit = false;
                } else if (e.keyCode == 'v' && (e.stateMask & SWT.MOD1) != 0) {
                    doPaste();
                }
            }
        });

        btnAddFolder = new Button(panelTreeAndButtons, SWT.PUSH);
        btnAddFolder.setText(Messages.EditableItemsPanel_add_folder);
        btnAddFolder.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnAddFolder.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doAddFolder();
            }
        });

        btnAddExpression = new Button(panelTreeAndButtons, SWT.PUSH);
        btnAddExpression.setText(Messages.EditableItemsPanel_add_expression);
        btnAddExpression.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnAddExpression.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doAddExpression(null);
            }
        });

        btnEdit = new Button(panelTreeAndButtons, SWT.PUSH);
        btnEdit.setText(Messages.EditableItemsPanel_edit);
        btnEdit.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnEdit.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doEdit();
            }
        });

        btnRemove = new Button(panelTreeAndButtons, SWT.PUSH);
        btnRemove.setText(Messages.EditableItemsPanel_remove);
        btnRemove.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnRemove.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doRemove();
            }
        });

        btnLoad = new Button(panelTreeAndButtons, SWT.PUSH);
        btnLoad.setText(Messages.EditableItemsPanel_load);
        btnLoad.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnLoad.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doLoad();
            }
        });

        btnSaveSelected = new Button(panelTreeAndButtons, SWT.PUSH);
        btnSaveSelected.setText(Messages.EditableItemsPanel_save_selected);
        btnSaveSelected.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnSaveSelected.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doSave(false);
            }
        });

        btnSaveAll = new Button(panelTreeAndButtons, SWT.PUSH);
        btnSaveAll.setText(Messages.EditableItemsPanel_save_all);
        btnSaveAll.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

        btnSaveAll.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doSave(true);
            }
        });

        sash = new Sash(this, SWT.HORIZONTAL | SWT.SMOOTH);

        labelPreview = new Label(this, SWT.NONE);
        labelPreview.setFont(headerFont);
        labelPreview.setText(Messages.EditableItemsPanel_preview);

        grepLineStyleListener = new GrepLineStyleListener(getShell(), null);
        grepLineStyleListener.setColorRegistry(colorRegistry);

        stPreview = new StyledText(this, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
        stPreview.addLineStyleListener(grepLineStyleListener);
        stPreview.setFont(JFaceResources.getTextFont());

        menuPreview = new Menu(stPreview);
        miAddExpressionFromPreview = new MenuItem(menuPreview, SWT.PUSH);
        miAddExpressionFromPreview.setText(Messages.EditableItemsPanel_add_expression);
        miAddExpressionFromPreview.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doAddExpression(stPreview.getSelectionText());
            }
        });

        miLoadDefault = new MenuItem(menuPreview, SWT.PUSH);
        miLoadDefault.setText(Messages.EditableItemsPanel_load_default);
        miLoadDefault.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doLoadDefaultPreview();
            }
        });

        miSaveDefault = new MenuItem(menuPreview, SWT.PUSH);
        miSaveDefault.setText(Messages.EditableItemsPanel_save_default);
        miSaveDefault.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doSaveDefaultPreview();
            }
        });

        new MenuItem(menuPreview, SWT.SEPARATOR);
        new PreviewColorHandler(stPreview, menuPreview);

        menuPreview.addMenuListener(new MenuAdapter() {
            @Override
            public void menuShown(MenuEvent e) {
                Set<AbstractGrepModelElement> selection = treePanel.getSelectedElements();
                boolean hasSelection = selection != null && !selection.isEmpty();

                String text = stPreview.getSelectionText();
                miAddExpressionFromPreview.setEnabled(hasSelection && text != null && text.length() > 0);

                super.menuShown(e);
            }
        });

        stPreview.setMenu(menuPreview);

        int preferredPreviewHeight = ((getDisplay().getDPI().y
                * JFaceResources.getTextFont().getFontData()[0].getHeight()) / 72) * 4;

        FormData fd = new FormData();
        fd.top = new FormAttachment(0, 0);
        fd.bottom = new FormAttachment(sash, 0);
        fd.left = new FormAttachment(0, 0);
        fd.right = new FormAttachment(100, 0);
        panelTreeAndButtons.setLayoutData(fd);

        fd = new FormData();
        fd.bottom = new FormAttachment(100, -sash.computeSize(SWT.DEFAULT, SWT.DEFAULT).y - preferredPreviewHeight);
        fd.left = new FormAttachment(0, 0);
        fd.right = new FormAttachment(100, 0);
        sash.setLayoutData(fd);

        fd = new FormData();
        fd.top = new FormAttachment(sash, 5);
        fd.left = new FormAttachment(0, 0);
        fd.right = new FormAttachment(100, 0);
        //fd.bottom = new FormAttachment(100, 0);
        labelPreview.setLayoutData(fd);

        fd = new FormData();
        fd.top = new FormAttachment(labelPreview, 0);
        fd.left = new FormAttachment(0, 0);
        fd.right = new FormAttachment(100, 0);
        fd.bottom = new FormAttachment(100, 0);
        stPreview.setLayoutData(fd);

        sash.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                Rectangle sashBounds = sash.getBounds();
                Rectangle clientArea = getClientArea();

                setSashPosition(-(clientArea.height - sashBounds.height - e.y));
            }
        });

        addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                int height = getSize().y;
                FormData fd = (FormData) sash.getLayoutData();

                int min = -height + 50;

                if (min > 0) {
                    min = 0;
                }

                if (fd.bottom.offset <= min) {
                    setSashPosition(min);
                }
            }
        });

        if (sashPosition != null) {
            setSashPosition(sashPosition);
        }

        refreshButtons();

        if (previewText == null || previewText.trim().length() == 0) {
            stPreview.setText(Activator.getDefault().getPreviewText());
        } else {
            stPreview.setText(previewText);
        }
    }

    /**
     * Loads the default preview text.
     */
    private void doLoadDefaultPreview() {
        stPreview.setText(Activator.getDefault().getPreviewText());
    }

    /**
     * Saves the default preview text after the user has confirmed via a dialog.
     */
    private void doSaveDefaultPreview() {
        if (MessageDialog.openConfirm(getShell(), Messages.Preview_save_default_preview_title,
                Messages.Preview_save_default_preview_confirm)) {
            Activator.getDefault().setPreviewText(stPreview.getText());
        }
    }

    /**
     * Sets the position of the sash separating the tree from the preview panel.
     * 
     * @param sashPosition New sash position.
     */
    protected void setSashPosition(Integer sashPosition) {
        this.sashPosition = sashPosition;

        if (sash != null && sashPosition != null) {
            FormData fd = (FormData) sash.getLayoutData();
            fd.bottom = new FormAttachment(100, sashPosition);
            layout();
        }
    }

    /**
     * Returns the sash position.
     * 
     * @return Sash position in pixels.
     */
    public Integer getSashPosition() {
        return sashPosition;
    }

    /**
     * Hooks drag and drop listeners to the tree.
     */
    private void hookTreeDragAndDrop() {
        final Tree tree = treePanel.getTree();
        Transfer[] transferTypes = new Transfer[] { TextTransfer.getInstance() };

        DragSource source = new DragSource(tree, DND.DROP_MOVE);
        source.setTransfer(transferTypes);

        source.addDragListener(new DragSourceListener() {
            @Override
            public void dragStart(DragSourceEvent event) {
            }

            @Override
            public void dragSetData(DragSourceEvent event) {
                event.data = buildXml(false, null, true);
            }

            @Override
            public void dragFinished(DragSourceEvent event) {
            }
        });

        DropTarget target = new DropTarget(tree, DND.DROP_MOVE);
        target.setTransfer(transferTypes);

        target.addDropListener(new DropTargetListener() {
            @Override
            public void dropAccept(DropTargetEvent event) {
            }

            private GrepExpressionRootFolder getDropRoot(String xml) {
                XmlHandler handler = new XmlHandler();

                try {
                    return handler.readExpressions(xml);
                } catch (XmlHandlerException ex) {
                    Activator.getDefault().log(IStatus.ERROR, Messages.EditableItemsPanel_paste_failed, ex);
                    return null;
                }
            }

            @Override
            public void drop(DropTargetEvent event) {
                if (event.data == null) {
                    event.detail = DND.DROP_NONE;
                    return;
                }

                event.detail = DND.DROP_NONE;
                GrepExpressionRootFolder dropRoot = getDropRoot((String) event.data);

                if (dropRoot == null) {
                    return;
                }

                Set<GrepExpressionFolder> folders = new HashSet<GrepExpressionFolder>();
                Set<GrepExpressionItem> items = new HashSet<GrepExpressionItem>();

                for (AbstractGrepModelElement element : dropRoot.getChildren()) {
                    if (element instanceof GrepExpressionFolder) {
                        folders.add((GrepExpressionFolder) element);
                    } else {
                        items.add((GrepExpressionItem) element);
                    }
                }

                if (!folders.isEmpty() && !items.isEmpty()) {
                    event.detail = DND.DROP_NONE;
                    return;
                }

                boolean droppingFolders = !folders.isEmpty();
                TreeItem item = (TreeItem) event.item;

                if (item == null) {
                    event.detail = DND.DROP_NONE;
                    return;
                }

                AbstractGrepModelElement targetElement = (AbstractGrepModelElement) item.getData();
                Point pt = tree.getDisplay().map(null, tree, event.x, event.y);
                Rectangle bounds = item.getBounds();

                GrepExpressionFolder root = getRootFolder();
                GrepExpressionFolder targetFolder;
                int offset;
                boolean isFolder = item.getData() instanceof GrepExpressionFolder;

                if (pt.y < bounds.y + (isFolder ? bounds.height / 3 : bounds.height / 2)) {
                    targetFolder = targetElement.getParent();
                    offset = targetFolder.getChildren().indexOf(targetElement);
                } else if (!isFolder || pt.y > bounds.y + 2 * bounds.height / 3) {
                    targetFolder = targetElement.getParent();
                    offset = targetFolder.getChildren().indexOf(targetElement) + 1;
                } else {
                    targetFolder = (GrepExpressionFolder) (isFolder ? targetElement : targetElement.getParent());
                    offset = isFolder ? -1 : targetFolder.getChildren().indexOf(targetElement);
                }

                if (droppingFolders && targetFolder != root) {
                    offset = root.getChildren().indexOf(targetFolder) + 1;
                    targetFolder = root;
                } else if (!droppingFolders && targetFolder == root) {
                    if (offset <= 0 || offset > root.getChildren().size()) {
                        event.detail = DND.DROP_NONE;
                        return;
                    }

                    targetFolder = (GrepExpressionFolder) root.getChildren().get(offset - 1);
                    offset = -1;
                }

                LinkedList<AbstractGrepModelElement> remove = new LinkedList<AbstractGrepModelElement>();
                List<AbstractGrepModelElement> dropElements = new LinkedList<AbstractGrepModelElement>(
                        dropRoot.getChildren());

                for (AbstractGrepModelElement element : dropElements) {
                    remove.add(root.findById(element.getId()));
                    targetFolder.add(element, offset);

                    if (offset >= 0) {
                        offset++;
                    }
                }

                for (AbstractGrepModelElement element : remove) {
                    element.getParent().remove(element);
                }

                treePanel.refresh();
            }

            @Override
            public void dragOver(DropTargetEvent event) {
                TextTransfer textTransfer = TextTransfer.getInstance();

                if (!textTransfer.isSupportedType(event.currentDataType)) {
                    event.operations = DND.DROP_NONE;
                    return;
                }

                event.feedback = DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL;

                if (event.item == null) {
                    return;
                }

                boolean droppingFolders = false;
                Object o = textTransfer.nativeToJava(event.currentDataType);
                String xml = (String) o;

                if (xml != null) {
                    GrepExpressionRootFolder dropRoot = getDropRoot(xml);

                    for (AbstractGrepModelElement element : dropRoot.getChildren()) {
                        if (element instanceof GrepExpressionFolder) {
                            droppingFolders = true;
                            break;
                        }
                    }
                }

                TreeItem item = (TreeItem) event.item;
                Point pt = tree.getDisplay().map(null, tree, event.x, event.y);
                Rectangle bounds = item.getBounds();

                boolean isFolder = item.getData() instanceof GrepExpressionFolder;

                if (pt.y < bounds.y + (isFolder ? bounds.height / 3 : bounds.height / 2)) {
                    event.feedback |= DND.FEEDBACK_INSERT_BEFORE;
                } else if (!isFolder || pt.y > bounds.y + 2 * bounds.height / 3) {
                    event.feedback |= DND.FEEDBACK_INSERT_AFTER;
                } else {
                    if (droppingFolders) {
                        event.feedback = DND.FEEDBACK_NONE;
                    } else {
                        event.feedback |= DND.FEEDBACK_SELECT;
                    }
                }
            }

            @Override
            public void dragOperationChanged(DropTargetEvent event) {
            }

            @Override
            public void dragLeave(DropTargetEvent event) {
            }

            @Override
            public void dragEnter(DropTargetEvent event) {
            }
        });
    }

    /**
     * Sets the model root being edited.
     * 
     * @param expressions Model root.
     */
    public void setExpressions(GrepExpressionsWithSelection expressions) {
        treePanel.setExpressions(expressions);
        refreshLineStyleListener();
        resizeColumns();
    }

    /**
     * Recalculates the tree columns' sizes.
     */
    public void resizeColumns() {
        treePanel.resizeColumns();
    }

    /**
     * Updates the line style listener used for the preview. 
     */
    private void refreshLineStyleListener() {
        LinkedList<GrepExpressionItem> items = new LinkedList<GrepExpressionItem>();
        GrepExpressionsWithSelection expressions = getExpressions();
        GrepExpressionRootFolder root = expressions.getRootFolder();
        addAllEnabledItems(items, root, expressions.getEnablementMap());

        grepLineStyleListener.setItems(items);
        stPreview.redraw();
    }

    /**
     * Recursively collects all enabled items in a list.
     * 
     * @param items List of items. Found items are added to this list.
     * @param element Element to search.
     * @param enablementMap Map of enablement states, or <code>null</code>.
     */
    private void addAllEnabledItems(LinkedList<GrepExpressionItem> items, AbstractGrepModelElement element,
            Map<String, Boolean> enablementMap) {
        Boolean enablement = enablementMap == null ? null : enablementMap.get(element.getId());

        if (enablement == null) {
            enablement = element.isDefaultEnabled();
        }

        if (!(element instanceof GrepExpressionRootFolder) && !enablement) {
            return;
        }

        if (element instanceof GrepExpressionItem) {
            items.add((GrepExpressionItem) element);
        } else if (element instanceof GrepExpressionFolder) {
            for (AbstractGrepModelElement child : ((GrepExpressionFolder) element).getChildren()) {
                addAllEnabledItems(items, child, enablementMap);
            }
        }
    }

    /**
     * Returns the model root being edited.
     * 
     * @return Model root.
     */
    public GrepExpressionsWithSelection getExpressions() {
        return treePanel.getExpressions();
    }

    /**
     * Adds a new folder.
     */
    private void doAddFolder() {
        GrepExpressionFolder folder = new GrepExpressionFolder();

        if (editFolder(folder, true)) {
            getRootFolder().add(folder);
            treePanel.refresh();
        }
    }

    /**
     * Returns the model root folder.
     * 
     * @return Root folder.
     */
    private GrepExpressionFolder getRootFolder() {
        return getExpressions().getRootFolder();
    }

    /**
     * Adds a new expression.
     * 
     * @param text Optional default text. If specified, the text is quoted and
     *       escaped as a regular expression.
     */
    private void doAddExpression(String text) {
        GrepExpressionItem item = new GrepExpressionItem();
        GrepExpressionFolder folder = treePanel.getSelectedFolder();
        folder.add(item);

        if (text != null) {
            item.setGrepExpression("(" + Pattern.quote(text) + ")"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        if (editExpression(item, true)) {
            treePanel.refresh();
            refreshLineStyleListener();
        } else {
            folder.remove(item);
        }
    }

    /**
     * Edits the currently selected model element.
     * 
     * Calls <code>editFolder()</code> or <code>editExpression()</code> depending
     * on the item type. 
     */
    private void doEdit() {
        AbstractGrepModelElement element = treePanel.getSelectedElement();
        boolean refresh;

        if (element instanceof GrepExpressionFolder) {
            refresh = editFolder((GrepExpressionFolder) element, false);
        } else {
            refresh = editExpression((GrepExpressionItem) element, false);
        }

        if (refresh) {
            treePanel.refresh();
            refreshLineStyleListener();
        }
    }

    /**
     * Edits the specified folder.
     * 
     * @param folder Folder.
     */
    public boolean editFolder(GrepExpressionFolder folder, boolean add) {
        FolderDialog dlg = new FolderDialog(getShell(), folder, add);

        return dlg.open() == InputDialog.OK;
    }

    /**
     * Edits the specified item.
     * 
     * @param item Item.
     */
    public boolean editExpression(GrepExpressionItem item, boolean add) {
        ExpressionDialog dlg = new ExpressionDialog(getShell(), add);
        dlg.setPreviewText(stPreview.getText());
        dlg.setItem(item);

        return dlg.open() == ExpressionDialog.OK;
    }

    /**
     * Removes the selected item.
     */
    private void doRemove() {
        Set<AbstractGrepModelElement> elements = treePanel.getSelectedElements();

        if (elements == null) {
            return;
        }

        if (!MessageDialog.openConfirm(getShell(), Messages.EditableItemsPanel_delete_items,
                Messages.EditableItemsPanel_really_delete_items)) {
            return;
        }

        Set<AbstractGrepModelElement> deleted = new HashSet<AbstractGrepModelElement>();

        for (AbstractGrepModelElement element : elements) {
            GrepExpressionFolder parent = element.getParent();

            if (deleted.contains(parent)) {
                continue;
            }

            parent.remove(element);
            deleted.add(element);
        }

        treePanel.refresh();
        // TODO: select next element
    }

    /**
     * Saves an element tree to a file.
     * 
     * @param all Whether to save the entire model tree or only the selected
     *       elements.
     */
    protected void doSave(boolean all) {
        String xml = buildXml(all, null, true);

        FileDialog dlg = new FileDialog(getShell(), SWT.SAVE);
        String fileName = dlg.open();

        if (fileName != null) {
            File file = new File(fileName);
            BufferedWriter writer = null;

            try {
                writer = new BufferedWriter(
                        new OutputStreamWriter(new FileOutputStream(file), Charset.forName("UTF-8"))); //$NON-NLS-1$
                writer.write(xml);
            } catch (IOException ex) {
                Activator.getDefault().log(IStatus.ERROR, Messages.Error_could_not_save_settings, ex);
            } finally {
                if (writer != null) {
                    try {
                        writer.close();
                    } catch (IOException ex) {
                        Activator.getDefault().log(IStatus.WARNING, ex);
                    }
                }
            }
        }
    }

    /**
     * Serialises an element tree to an XML string.
     * 
     * @param all Whether to serialise the entire model tree or only the selected
     *       elements.
     * @param cutElements If provided, this list will be filled with all elements
     *       that should be deleted as part of a cut operation. May be
     *       <code>null</code>.
     * @param identityCopy Whether the XML string should use the original element
     *       IDs or newly generated ones.
     * 
     * @return
     */
    private String buildXml(boolean all, LinkedList<AbstractGrepModelElement> cutElements, boolean identityCopy) {
        Set<AbstractGrepModelElement> allElements;
        GrepExpressionRootFolder copyRoot;

        if (all) {
            copyRoot = getExpressions().getRootFolder();
            allElements = new LinkedHashSet<AbstractGrepModelElement>();
            allElements.addAll(copyRoot.getChildren());
        } else {
            allElements = treePanel.getSelectedElements();
            copyRoot = new GrepExpressionRootFolder();

            for (AbstractGrepModelElement element : allElements) {
                AbstractGrepModelElement parent = element.getParent();
                boolean skip = false;

                while (parent != null) {
                    if (allElements.contains(parent)) {
                        skip = true;
                        break;
                    }

                    parent = parent.getParent();
                }

                if (!skip) {
                    AbstractGrepModelElement newElement = element.copy(identityCopy);
                    copyRoot.add(newElement);

                    if (cutElements != null) {
                        cutElements.add(element);
                    }
                }
            }
        }

        XmlHandler handler = new XmlHandler();
        handler.recalculateStyleSet(copyRoot);

        try {
            return handler.createXmlString(copyRoot);
        } catch (ParserConfigurationException ex) {
            Activator.getDefault().log(IStatus.ERROR, ex);
        } catch (TransformerException ex) {
            Activator.getDefault().log(IStatus.ERROR, ex);
        }

        return null;
    }

    /**
     * Performs a copy or cut operation.
     * 
     * @param cut <code>true</code> for cut, <code>false</code> for copy. 
     */
    private void doCopy(boolean cut) {
        LinkedList<AbstractGrepModelElement> cutElements = cut ? new LinkedList<AbstractGrepModelElement>() : null;
        String xml = buildXml(false, cutElements, cut);
        TextTransfer textTransfer = TextTransfer.getInstance();
        clipboard.setContents(new Object[] { xml }, new Transfer[] { textTransfer });

        if (cut) {
            for (AbstractGrepModelElement element : cutElements) {
                element.getParent().remove(element);
            }

            treePanel.refresh();
        }
    }

    /**
     * Loads an element tree from a file.
     */
    protected void doLoad() {
        FileDialog dlg = new FileDialog(getShell(), SWT.OPEN);
        String fileName = dlg.open();

        if (fileName == null) {
            return;
        }

        File file = new File(fileName);
        BufferedReader reader = null;
        StringBuilder sb = new StringBuilder();

        try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));

            for (;;) {
                String s = reader.readLine();

                if (s == null) {
                    break;
                }

                if (sb.length() > 0) {
                    sb.append("\n"); //$NON-NLS-1$
                }

                sb.append(s);
            }
        } catch (IOException ex) {
            Activator.getDefault().log(IStatus.ERROR, Messages.Error_could_not_load_settings, ex);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException ex) {
                    Activator.getDefault().log(IStatus.WARNING, ex);
                }
            }
        }

        String xml = sb.toString();
        paste(getExpressions().getRootFolder(), xml);
    }

    /**
     * Performs a paste operation.
     */
    private void doPaste() {
        TextTransfer transfer = TextTransfer.getInstance();
        paste(treePanel.getSelectedElement(), (String) clipboard.getContents(transfer));
    }

    /**
     * Pastes the elements serialised in the provided XML string into the
     * specified target element.
     * 
     * @param selectedElement Target element. If this is <code>null</code>, the
     *       root folder is used as the target. If an expression item is specified,
     *       its parent folder is used as the target.
     * @param xml XML string containing a serialised element tree.
     */
    private void paste(AbstractGrepModelElement selectedElement, String xml) {
        GrepExpressionFolder parent = selectedElement == null ? treePanel.getExpressions().getRootFolder()
                : selectedElement instanceof GrepExpressionFolder ? (GrepExpressionFolder) selectedElement
                        : selectedElement.getParent();
        GrepExpressionFolder root = parent.getParent() == null ? parent : parent.getParent();

        XmlHandler handler = new XmlHandler();
        GrepExpressionRootFolder copyRoot;

        try {
            copyRoot = handler.readExpressions(xml);
        } catch (XmlHandlerException ex) {
            Activator.getDefault().log(IStatus.ERROR, Messages.EditableItemsPanel_paste_failed, ex);
            return;
        }

        copyRoot.rewriteDuplicateIds(root.getAllIds(new HashSet<String>()));

        GrepExpressionFolder newFolder = null;

        for (AbstractGrepModelElement element : copyRoot.getChildren()) {
            boolean isItem = element instanceof GrepExpressionItem;

            if (isItem) {
                if (parent == root) {
                    if (newFolder == null) {
                        newFolder = new GrepExpressionFolder();
                        newFolder.setName(Messages.EditableItemsPanel_new_items_folder_title);
                        parent.add(newFolder);
                    }

                    newFolder.add(element.copy(true));
                } else {
                    parent.add(element.copy(true));
                }
            } else {
                root.add(element.copy(true));
            }
        }

        root.getRoot().addMissingStyles();
        treePanel.refresh();
    }

    /**
     * @see name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.
     * IEnabledExpressionsListener#enabledExpressionsChanged()
     */
    @Override
    public void enabledExpressionsChanged() {
        refreshLineStyleListener();
    }

    /**
     * Returns the items tree panel.
     * 
     * @return Panel.
     */
    public ItemsTreePanel getTreePanel() {
        return treePanel;
    }

    /**
     * Refreshes the popup menu.
     */
    private void refreshMenuItems() {
        Set<AbstractGrepModelElement> selection = treePanel.getSelectedElements();
        boolean noSelection = selection == null || selection.isEmpty();

        boolean canPaste = false;
        TextTransfer transfer = TextTransfer.getInstance();
        String xml = (String) clipboard.getContents(transfer);

        if (xml != null) {
            try {
                new XmlHandler().readExpressions(xml);
                canPaste = true;
            } catch (XmlHandlerException ex) {
            }
        }

        miCut.setEnabled(!noSelection);
        miCopy.setEnabled(!noSelection);
        miPaste.setEnabled(canPaste);
    }

    /**
     * Triggers an edit operation when an element is double clicked.
     * 
     * @see name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.IItemTreeListener#elementDoubleClicked(name.schedenig.eclipse.grepconsole.model.AbstractGrepModelElement)
     */
    @Override
    public void elementDoubleClicked(AbstractGrepModelElement element) {
        doEdit();
    }

    /**
     * Refreshes the buttons when the element selection changes.
     * 
     * @see name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.IItemTreeListener#elementSelectionChanged(java.util.Set)
     */
    @Override
    public void elementSelectionChanged(Set<AbstractGrepModelElement> elements) {
        refreshButtons();
    }

    /**
     * Refrehes the buttons according to the currently selected elements. 
     */
    private void refreshButtons() {
        Set<AbstractGrepModelElement> selection = treePanel.getSelectedElements();
        boolean hasSelection = selection != null && !selection.isEmpty();

        btnAddExpression.setEnabled(hasSelection);
        btnEdit.setEnabled(hasSelection);
        btnRemove.setEnabled(hasSelection);
        btnSaveSelected.setEnabled(hasSelection);
    }

    /**
     * Sets the preview text.
     * 
     * @param previewText Preview text.
     */
    public void setPreviewText(String previewText) {
        this.previewText = previewText;

        if (stPreview != null && previewText != null) {
            stPreview.setText(previewText);
        }
    }

    /**
     * Returns the preview text.
     * 
     * @return Preview text
     */
    public String getPreviewText() {
        return previewText;
    }

    /**
     * @see name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.IItemTreeListener#filterExpressionsChanged()
     */
    @Override
    public void filterExpressionsChanged() {
    }

    /**
     * @see name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.IItemTreeListener#statisticsExpressionsChanged()
     */
    @Override
    public void statisticsExpressionsChanged() {
    }

    /**
     * @see name.schedenig.eclipse.grepconsole.view.items.ItemsTreePanel.IItemTreeListener#notificationsExpressionsChanged()
     */
    @Override
    public void notificationsExpressionsChanged() {
    }

    /**
     * Refreshes the tree.
     */
    public void refresh() {
        treePanel.refresh();
    }
}