com.bdaum.zoom.ui.internal.views.ExhibitionView.java Source code

Java tutorial

Introduction

Here is the source code for com.bdaum.zoom.ui.internal.views.ExhibitionView.java

Source

/*
 * This file is part of the ZoRa project: http://www.photozora.org.
 *
 * ZoRa 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 2 of the License, or
 * (at your option) any later version.
 *
 * ZoRa 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 ZoRa; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * (c) 2009 Berthold Daum  
 */

package com.bdaum.zoom.ui.internal.views;

import java.awt.Color;
import java.awt.Font;
import java.awt.event.InputEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.AbstractOperation;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.UndoContext;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressIndicator;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.piccolo2d.PCamera;
import org.piccolo2d.PNode;
import org.piccolo2d.event.PBasicInputEventHandler;
import org.piccolo2d.event.PInputEvent;
import org.piccolo2d.extras.swt.PSWTCanvas;
import org.piccolo2d.extras.swt.PSWTHandle;
import org.piccolo2d.extras.swt.PSWTPath;
import org.piccolo2d.extras.swt.PSWTText;
import org.piccolo2d.extras.util.PLocator;
import org.piccolo2d.util.PBounds;
import org.piccolo2d.util.PDimension;
import org.piccolo2d.util.PPaintContext;

import com.bdaum.aoModeling.runtime.IIdentifiableObject;
import com.bdaum.aoModeling.runtime.IdentifiableObject;
import com.bdaum.zoom.cat.model.Rgb_type;
import com.bdaum.zoom.cat.model.Rgb_typeImpl;
import com.bdaum.zoom.cat.model.asset.Asset;
import com.bdaum.zoom.cat.model.asset.AssetImpl;
import com.bdaum.zoom.cat.model.group.exhibition.Exhibit;
import com.bdaum.zoom.cat.model.group.exhibition.ExhibitImpl;
import com.bdaum.zoom.cat.model.group.exhibition.Exhibition;
import com.bdaum.zoom.cat.model.group.exhibition.ExhibitionImpl;
import com.bdaum.zoom.cat.model.group.exhibition.Wall;
import com.bdaum.zoom.cat.model.group.exhibition.WallImpl;
import com.bdaum.zoom.core.Constants;
import com.bdaum.zoom.core.Core;
import com.bdaum.zoom.core.ISpellCheckingService;
import com.bdaum.zoom.core.db.IDbManager;
import com.bdaum.zoom.core.internal.CoreActivator;
import com.bdaum.zoom.core.internal.IMediaSupport;
import com.bdaum.zoom.core.internal.Utilities;
import com.bdaum.zoom.css.CSSProperties;
import com.bdaum.zoom.css.internal.CssActivator;
import com.bdaum.zoom.net.core.ftp.FtpAccount;
import com.bdaum.zoom.net.core.job.TransferJob;
import com.bdaum.zoom.ui.AssetSelection;
import com.bdaum.zoom.ui.dialogs.AcousticMessageDialog;
import com.bdaum.zoom.ui.internal.HelpContextIds;
import com.bdaum.zoom.ui.internal.Icons;
import com.bdaum.zoom.ui.internal.UiActivator;
import com.bdaum.zoom.ui.internal.UiUtilities;
import com.bdaum.zoom.ui.internal.dialogs.EditWallDialog;
import com.bdaum.zoom.ui.internal.dialogs.ExhibitLayoutDialog;
import com.bdaum.zoom.ui.internal.dialogs.ExhibitionEditDialog;
import com.bdaum.zoom.ui.internal.dialogs.SelectExhibitDialog;
import com.bdaum.zoom.ui.internal.hover.IHoverContext;
import com.bdaum.zoom.ui.internal.operations.ExhibitionPropertiesOperation;
import com.bdaum.zoom.ui.internal.widgets.AbstractHandle;
import com.bdaum.zoom.ui.internal.widgets.GalleryPanEventHandler;
import com.bdaum.zoom.ui.internal.widgets.GreekedPSWTText;
import com.bdaum.zoom.ui.internal.widgets.IAugmentedTextField;
import com.bdaum.zoom.ui.internal.widgets.PPanel;
import com.bdaum.zoom.ui.internal.widgets.PSWTAssetThumbnail;
import com.bdaum.zoom.ui.internal.widgets.TextEventHandler;
import com.bdaum.zoom.ui.internal.widgets.TextField;
import com.bdaum.zoom.vr.internal.ExhibitionJob;

@SuppressWarnings("restriction")
public class ExhibitionView extends AbstractPresentationView implements IHoverContext {

    private static final java.awt.Rectangle RECT1 = new java.awt.Rectangle(0, 0, 1, 1);

    public class LayoutOperation extends AbstractOperation {

        private final ExhibitImpl backup;
        private final ExhibitImpl exhibit;
        private ExhibitImpl redo;

        public LayoutOperation(String title, ExhibitImpl backup, ExhibitImpl exhibit) {
            super(title);
            this.backup = backup;
            this.exhibit = exhibit;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            storeSafelyAndUpdateIndex(null, exhibit, null);
            setInput(exhibition);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            transferValues(redo, exhibit);
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            redo = cloneExhibit(exhibit);
            transferValues(backup, exhibit);
            return execute(monitor, info);
        }

        private void transferValues(ExhibitImpl from, ExhibitImpl to) {
            to.setWidth(from.getWidth());
            to.setHeight(from.getHeight());
            to.setMatWidth(from.getMatWidth());
            to.setMatColor(from.getMatColor());
            to.setFrameWidth(from.getFrameWidth());
            to.setFrameColor(from.getFrameColor());
            to.setHideLabel(from.getHideLabel());
            to.setLabelAlignment(from.getLabelAlignment());
            to.setLabelDistance(from.getLabelDistance());
            to.setLabelIndent(from.getLabelIndent());
        }
    }

    public class MarkSoldOperation extends AbstractOperation {

        private final PExhibit pexhibit;

        public MarkSoldOperation(PExhibit pexhibit) {
            super(pexhibit.exhibit.getSold() ? Messages.getString("ExhibitionView.mark_as_sold") //$NON-NLS-1$
                    : Messages.getString("ExhibitionView.remove_sold_mark")); //$NON-NLS-1$
            this.pexhibit = pexhibit;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            pexhibit.exhibit.setSold(!pexhibit.exhibit.getSold());
            pexhibit.updateSoldMark();
            storeSafelyAndUpdateIndex(null, pexhibit.exhibit, null);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return execute(monitor, info);
        }
    }

    private static final int LEFTINDENT = 30;
    private static final int TOPINDENT = 10;
    private static final int V_WALLMARGINS = 15;
    private static final int gridTolerance = 8;

    public class DeleteWallOperation extends AbstractOperation {

        private PWall pWall;
        private Wall wall;
        private double y;
        private List<ExhibitImpl> deletedExhibits;
        private int index;

        public DeleteWallOperation(PWall pWall) {
            super(Messages.getString("ExhibitionView.delete_wall_undo")); //$NON-NLS-1$
            this.pWall = pWall;
            this.y = pWall.getOffset().getY();
            this.wall = pWall.getWall();
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            index = exhibition.getWall().indexOf(wall);
            if (index < 0)
                return Status.CANCEL_STATUS;
            exhibition.removeWall(wall);
            int size = wall.getExhibit().size();
            List<Object> toBeDeleted = new ArrayList<Object>(size + 1);
            List<String> assetIds = new ArrayList<String>(size);
            deletedExhibits = new ArrayList<ExhibitImpl>(size);
            for (ExhibitImpl obj : Core.getCore().getDbManager().obtainByIds(ExhibitImpl.class,
                    wall.getExhibit())) {
                toBeDeleted.add(obj);
                deletedExhibits.add(obj);
                assetIds.add(obj.getAsset());
            }
            toBeDeleted.add(wall);
            storeSafelyAndUpdateIndex(toBeDeleted, exhibition, assetIds);
            double ydiff = pWall.getHeight() + 15;
            surface.removeChild(pWall);
            pWall.positionSubsequentWalls(-ydiff);
            walls.remove(index);
            setPanAndZoomHandlers();
            updateStatusLine();
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            ExhibitionImpl show = getExhibition();
            if (show != null) {
                List<Wall> allWalls = show.getWall();
                if (index < allWalls.size())
                    allWalls.add(index, wall);
                else
                    allWalls.add(wall);
                double ydiff = pWall.getHeight() + 15;
                surface.addChild(pWall);
                pWall.setOffset(0, y);
                if (index < walls.size())
                    walls.add(index, pWall);
                else
                    walls.add(pWall);
                pWall.positionSubsequentWalls(ydiff);
                setColor(canvas);
                List<Object> tobeStored = new ArrayList<Object>(deletedExhibits);
                List<String> assetIds = new ArrayList<String>(deletedExhibits.size());
                for (ExhibitImpl exhibit : deletedExhibits)
                    assetIds.add(exhibit.getAsset());
                tobeStored.add(wall);
                tobeStored.add(show);
                storeSafelyAndUpdateIndex(null, tobeStored, assetIds);
                updateStatusLine();
            }
            return Status.OK_STATUS;
        }

    }

    public class CreateWallOperation extends AbstractOperation {

        private final Wall wall;
        private final int y;
        private PWall added;

        public CreateWallOperation(Wall wall, int y) {
            super(Messages.getString("ExhibitionView.create_wall_undo")); //$NON-NLS-1$
            this.wall = wall;
            this.y = y;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            ExhibitionImpl show = getExhibition();
            if (show != null) {
                show.addWall(wall);
                added = addWall(wall, y, true, null);
                setColor(canvas);
                List<Object> toBeStored = new ArrayList<Object>(2);
                toBeStored.add(wall);
                toBeStored.add(show);
                storeSafelyAndUpdateIndex(null, toBeStored, null);
                updateStatusLine();
                return Status.OK_STATUS;
            }
            return Status.CANCEL_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            ExhibitionImpl show = getExhibition();
            if (show != null) {
                show.removeWall(wall);
                storeSafelyAndUpdateIndex(wall, exhibition, null);
                Point2D offset = added.getOffset();
                double ydiff = added.getHeight() + 15;
                surface.removeChild(added);
                int index = walls.indexOf(added);
                for (int i = index + 1; i < walls.size(); i++) {
                    PWall w = walls.get(i);
                    if (w.getOffset().getY() > offset.getY())
                        w.offset(0, -ydiff);
                }
                walls.remove(index);
                setPanAndZoomHandlers();
                updateStatusLine();
            }
            return Status.OK_STATUS;
        }
    }

    public class EditWallOperation extends AbstractOperation {

        private final WallImpl wall;
        private final WallImpl backup;
        private WallImpl redo;
        private final PWall pwall;

        public EditWallOperation(WallImpl wall, WallImpl backup, PWall pwall) {
            super(Messages.getString("ExhibitionView.edit_wall_undo")); //$NON-NLS-1$
            this.wall = wall;
            this.backup = backup;
            this.pwall = pwall;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            Core.getCore().getDbManager().safeTransaction(null, wall);
            pwall.setTitle(wall.getLocation());
            pwall.resize();
            setColor(canvas);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            wall.setLocation(redo.getLocation());
            wall.setX(redo.getX());
            wall.setY(redo.getY());
            wall.setWidth(redo.getWidth());
            wall.setHeight(redo.getHeight());
            wall.setColor(redo.getColor());
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            redo = new WallImpl(wall.getLocation(), wall.getX(), wall.getY(), wall.getWidth(), wall.getHeight(),
                    wall.getGX(), wall.getGX(), wall.getGAngle(), wall.getColor());
            wall.setLocation(backup.getLocation());
            wall.setX(backup.getX());
            wall.setY(backup.getY());
            wall.setWidth(backup.getWidth());
            wall.setHeight(backup.getHeight());
            wall.setColor(backup.getColor());
            return execute(monitor, info);
        }
    }

    public static class EditWallTextOperation extends AbstractOperation {

        public static final int CAPTION = 0;
        public static final int DESCRIPTION = 1;
        public static final int CREDITS = 2;
        public static final int DATE = 3;
        private final Wall wall;
        private final String text;
        private String oldtext;
        private final IAugmentedTextField textField;

        public EditWallTextOperation(Wall wall, String text, IAugmentedTextField textField) {
            super(Messages.getString("ExhibitionView.edit_wall_loc_undo")); //$NON-NLS-1$
            this.wall = wall;
            this.text = text;
            this.textField = textField;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            oldtext = wall.getLocation();
            doSetText(text);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            doSetText(text);
            textField.setText(text);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            doSetText(oldtext);
            textField.setText(oldtext);
            return Status.OK_STATUS;
        }

        private void doSetText(String t) {
            wall.setLocation(t);
            Core.getCore().getDbManager().safeTransaction(null, wall);
        }
    }

    public static class EditInfoTextOperation extends AbstractOperation {

        private final Exhibition exhibition;
        private final String text;
        private String oldtext;
        private final IAugmentedTextField textField;

        public EditInfoTextOperation(Exhibition exhibition, String text, IAugmentedTextField textField) {
            super(Messages.getString("ExhibitionView.edit_info_text")); //$NON-NLS-1$
            this.exhibition = exhibition;
            this.text = text;
            this.textField = textField;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            oldtext = exhibition.getInfo();
            doSetText(text);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            doSetText(text);
            textField.setText(text);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            doSetText(oldtext);
            textField.setText(oldtext);
            return Status.OK_STATUS;
        }

        private void doSetText(String t) {
            exhibition.setInfo(t);
            Core.getCore().getDbManager().safeTransaction(null, exhibition);
        }
    }

    public class EditTextOperation extends AbstractOperation {

        public static final int CAPTION = 0;
        public static final int DESCRIPTION = 1;
        public static final int CREDITS = 2;
        public static final int DATE = 3;
        private final ExhibitImpl exhibit;
        private final String text;
        private String oldtext;
        private final IAugmentedTextField textField;
        private final int type;

        public EditTextOperation(ExhibitImpl exhibit, String text, IAugmentedTextField textField, int type) {
            super(Messages.getString("ExhibitionView.edit_exh_text_undo")); //$NON-NLS-1$
            this.exhibit = exhibit;
            this.text = text;
            this.textField = textField;
            this.type = type;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            switch (type) {
            case CAPTION:
                oldtext = exhibit.getTitle();
                break;
            case DESCRIPTION:
                oldtext = exhibit.getDescription();
                break;
            case CREDITS:
                oldtext = exhibit.getCredits();
                break;
            case DATE:
                oldtext = exhibit.getDate();
                break;
            }
            doSetText(text);
            return Status.OK_STATUS;
        }

        private void doSetText(String t) {
            boolean indexed = false;
            boolean changed = false;
            switch (type) {
            case CAPTION:
                changed = exhibit.getTitle() == null || !exhibit.getTitle().equals(t);
                indexed = true;
                exhibit.setTitle(t);
                break;
            case DESCRIPTION:
                changed = exhibit.getDescription() == null || !exhibit.getDescription().equals(t);
                indexed = true;
                exhibit.setDescription(t);
                break;
            case CREDITS:
                changed = exhibit.getCredits() == null || !exhibit.getCredits().equals(t);
                indexed = true;
                exhibit.setCredits(t);
                break;
            case DATE:
                exhibit.setDate(t);
                break;
            }
            if (changed)
                storeSafelyAndUpdateIndex(null, exhibit, indexed ? exhibit.getAsset() : null);
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            doSetText(text);
            textField.setText(text);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            doSetText(oldtext);
            textField.setText(oldtext);
            return Status.OK_STATUS;
        }

    }

    public class EditExhibitOperation extends AbstractOperation {

        private final PExhibit pexhibit;
        private final ExhibitImpl exhibit;
        private ExhibitImpl redo;
        private ExhibitImpl backup;
        private boolean indexChange;

        public EditExhibitOperation(PExhibit pexhibit, ExhibitImpl backup, ExhibitImpl exhibit) {
            super(Messages.getString("ExhibitionView.change_exh_size_pos_undo")); //$NON-NLS-1$
            this.pexhibit = pexhibit;
            this.backup = backup;
            this.exhibit = exhibit;
            indexChange = (backup.getTitle() == null || !backup.getTitle().equals(exhibit.getTitle())
                    || backup.getDescription() == null || !backup.getDescription().equals(exhibit.getDescription())
                    || backup.getCredits() == null || !backup.getCredits().equals(exhibit.getCredits()));
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            pexhibit.setImageWidth(exhibit.getWidth());
            pexhibit.setImageHeight(exhibit.getHeight());
            pexhibit.invalidatePaint();
            pexhibit.relocateAllHandles();
            String id = exhibit.getAsset();
            storeSafelyAndUpdateIndex(null, exhibit, indexChange ? id : null);
            AssetImpl asset = Core.getCore().getDbManager().obtainAsset(id);
            if (asset != null)
                pexhibit.computeResolutionWarning(asset);
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            exhibit.setTitle(redo.getTitle());
            exhibit.setDescription(redo.getDescription());
            exhibit.setCredits(redo.getCredits());
            exhibit.setDate(redo.getDate());
            exhibit.setX(redo.getX());
            exhibit.setY(redo.getY());
            exhibit.setWidth(redo.getWidth());
            exhibit.setHeight(redo.getHeight());
            exhibit.setAsset(redo.getAsset());
            pexhibit.rescale();
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            redo = cloneExhibit(exhibit);
            exhibit.setTitle(backup.getTitle());
            exhibit.setDescription(backup.getDescription());
            exhibit.setCredits(backup.getCredits());
            exhibit.setDate(backup.getDate());
            exhibit.setX(backup.getX());
            exhibit.setY(backup.getY());
            exhibit.setWidth(backup.getWidth());
            exhibit.setHeight(backup.getHeight());
            exhibit.setAsset(backup.getAsset());
            pexhibit.rescale();
            return execute(monitor, info);
        }

    }

    public class MoveExhibitOperation extends AbstractOperation {

        private final PExhibit moved;
        private final Point2D oldOffset;
        private final Point2D newOffset;
        private PWall oldWall;
        private final PWall target;

        public MoveExhibitOperation(PExhibit moved, Point2D oldOffset, PWall target, Point2D newOffset) {
            super(Messages.getString("ExhibitionView.move_exh_undo")); //$NON-NLS-1$
            this.moved = moved;
            this.oldOffset = oldOffset;
            this.target = target;
            this.newOffset = newOffset;
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            oldWall = doMove(newOffset, target, false);
            return (oldWall == null) ? Status.CANCEL_STATUS : Status.OK_STATUS;
        }

        private PWall doMove(Point2D to, PWall toWall, boolean undo) {
            PWall fromPWall = doRemoveExhibit(moved);
            if (fromPWall == null)
                return null;
            Point2D sourceOffset = fromPWall.getOffset();
            Point2D targetOffset = toWall.getOffset();
            double xdif = targetOffset.getX() - sourceOffset.getX();
            double ydif = targetOffset.getY() - sourceOffset.getY();
            double x = to.getX();
            double y = to.getY();
            int index = 0;
            for (PExhibit pExhibit : getSortedExhibits(toWall))
                if (pExhibit.getOffset().getX() < x)
                    index++;
            int globalIndex = index;
            ExhibitImpl exhibit = moved.exhibit;
            Wall targetWall = toWall.getWall();
            for (Wall w : exhibition.getWall()) {
                if (w == targetWall)
                    break;
                globalIndex += w.getExhibit().size();
            }
            if (++globalIndex >= exhibits.size())
                exhibits.add(exhibit);
            else
                exhibits.add(globalIndex, exhibit);
            if (++index >= targetWall.getExhibit().size())
                targetWall.addExhibit(exhibit.getStringId());
            else
                targetWall.getExhibit().add(index, exhibit.getStringId());
            toWall.addChild(moved);
            int newX = (int) (undo ? x : (x - xdif));
            int newY = (int) (undo ? y : (y - ydif));
            if (!undo && exhibition.getShowGrid() && exhibition.getSnapToGrid()) {
                org.eclipse.swt.graphics.Point pnt = UiUtilities.snapToGrid(newX, newY, exhibit.getWidth(),
                        exhibit.getHeight(), targetWall.getHeight(), exhibition.getGridSize(), gridTolerance);
                newX = pnt.x;
                newY = pnt.y;
            }
            moved.setOffset(newX, newY);
            exhibit.setY((targetWall.getHeight() - newY));
            exhibit.setX((newX));
            List<Object> tobeStored = new ArrayList<Object>(3);
            tobeStored.add(exhibit);
            tobeStored.add(targetWall);
            Wall fromWall = fromPWall.getWall();
            if (fromWall != targetWall)
                tobeStored.add(fromWall);
            storeSafelyAndUpdateIndex(null, tobeStored, null);
            updateActions(false);
            return fromPWall;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return (doMove(oldOffset, oldWall, true) == null) ? Status.CANCEL_STATUS : Status.OK_STATUS;
        }
    }

    public class DeleteExhibitOperation extends AbstractOperation {

        private List<PExhibit> deleted;
        private List<PWall> walls;
        private boolean cut;

        public DeleteExhibitOperation(List<PExhibit> deleted, boolean cut) {
            super(Messages.getString("ExhibitionView.delete_exh_undo")); //$NON-NLS-1$
            this.deleted = deleted;
            this.cut = cut;
            walls = new ArrayList<>(deleted.size());
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            if (cut && !deleted.isEmpty())
                clipboard.clear();
            List<IIdentifiableObject> toBeDeleted = new ArrayList<>();
            List<String> toBeIndexed = new ArrayList<>();
            Set<Wall> toBeStored = new HashSet<>();
            List<Exhibit> clipped = null;
            for (Iterator<PExhibit> it = deleted.iterator(); it.hasNext();) {
                PExhibit pexhibit = it.next();
                ExhibitImpl exhibit = pexhibit.exhibit;
                if (cut) {
                    if (clipped == null)
                        clipped = new ArrayList<>(deleted.size());
                    clipped.add(exhibit);
                }
                PWall wallPanel = doRemoveExhibit(pexhibit);
                if (wallPanel == null) {
                    it.remove();
                    continue;
                }
                toBeStored.add(wallPanel.getWall());
                walls.add(wallPanel);
                toBeDeleted.add(exhibit);
                toBeIndexed.add(exhibit.getAsset());
            }
            storeSafelyAndUpdateIndex(toBeDeleted, toBeStored, toBeIndexed);
            if (clipped != null) {
                clipboard.clear();
                clipboard.addAll(clipped);
            }
            updateActions(false);
            updateStatusLine();
            return deleted.isEmpty() ? Status.CANCEL_STATUS : Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            List<IIdentifiableObject> toBeStored = new ArrayList<>();
            List<String> toBeIndexed = new ArrayList<>();
            Set<Wall> wallsBeStored = new HashSet<>();
            for (int i = deleted.size() - 1; i >= 0; i--) {
                PExhibit pExhibit = deleted.get(i);
                ExhibitImpl exhibit = pExhibit.exhibit;
                toBeIndexed.add(exhibit.getAsset());
                toBeStored.add(exhibit);
                PWall wallPanel = walls.get(i);
                WallImpl wall = (WallImpl) wallPanel.getWall();
                wallsBeStored.add(wall);
                double x = pExhibit.getOffset().getX() - wallPanel.getOffset().getX();
                double pos = 0;
                int index = 0;
                List<PExhibit> sortedExhibits = getSortedExhibits(wallPanel);
                for (PExhibit pex : sortedExhibits) {
                    PBounds eBounds = pex.getFullBoundsReference();
                    double p = eBounds.getX();
                    if (p < x) {
                        p += eBounds.getWidth();
                        if (p > pos)
                            pos = p;
                        ++index;
                    }
                }
                int globalIndex = index;
                for (Wall w : exhibition.getWall()) {
                    if (w == wall)
                        break;
                    globalIndex += w.getExhibit().size();
                }
                if (globalIndex >= exhibits.size())
                    exhibits.add(exhibit);
                else
                    exhibits.add(globalIndex, exhibit);
                if (index >= wall.getExhibit().size())
                    wall.addExhibit(exhibit.getStringId());
                else
                    wall.getExhibit().add(index, exhibit.getStringId());
                wallPanel.addChild(pExhibit);
                toBeStored.add(exhibit);
                toBeStored.add(wall);
            }
            toBeStored.addAll(wallsBeStored);
            storeSafelyAndUpdateIndex(null, toBeStored, toBeIndexed);
            updateActions(false);
            updateStatusLine();
            return Status.OK_STATUS;
        }
    }

    public class DropAssetOperation extends AbstractOperation {

        private final AssetSelection selection;
        private final Point2D position;
        private List<PExhibit> added;
        private PWall wallPanel;
        private final boolean replace;
        private DeleteExhibitOperation deleteOp;
        private List<PExhibit> trail = new ArrayList<PExhibit>();
        private double iWidth;

        public DropAssetOperation(AssetSelection selection, Point2D position, boolean replace) {
            super(Messages.getString("ExhibitionView.drop_images_undo")); //$NON-NLS-1$
            this.selection = selection;
            this.position = position;
            this.replace = replace;
            added = new ArrayList<PExhibit>(selection.size());
            RECT1.x = (int) position.getX();
            RECT1.y = (int) position.getY();
            for (PWall w : walls)
                if (w.fullIntersects(RECT1)) {
                    wallPanel = w;
                    break;
                }
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            if (wallPanel != null) {
                ExhibitionImpl show = null;
                if (!selection.isEmpty())
                    show = getExhibition();
                if (show == null)
                    return Status.OK_STATUS;
                WallImpl wall = (WallImpl) wallPanel.getWall();
                double x = position.getX() - wallPanel.getOffset().getX();
                double pos = 0;
                int index = 0;
                PExhibit cand = null;
                for (ListIterator<?> it = wallPanel.getChildrenIterator(); it.hasNext();) {
                    Object next = it.next();
                    if (next instanceof PExhibit) {
                        PExhibit pExhibit = (PExhibit) next;
                        PBounds eBounds = pExhibit.getFullBoundsReference();
                        double p = eBounds.getX();
                        if (p >= x)
                            trail.add(pExhibit);
                        else {
                            p += eBounds.getWidth();
                            if (replace && p >= x)
                                cand = pExhibit;
                            if (p > pos)
                                pos = p;
                            ++index;
                        }
                    }
                }
                double oldPos = pos;
                int globalIndex = index;
                for (Wall w1 : exhibition.getWall()) {
                    if (w1 == wall)
                        break;
                    globalIndex += w1.getExhibit().size();
                }
                Exhibit sibling = null;
                if (cand != null)
                    sibling = cand.exhibit;
                if (globalIndex > 0)
                    sibling = exhibits.get(globalIndex - 1);
                else if (globalIndex < exhibits.size() - 1)
                    sibling = exhibits.get(globalIndex + 1);
                double siblingSize = sibling == null ? Double.NaN
                        : Math.sqrt(sibling.getWidth() * sibling.getWidth()
                                + sibling.getHeight() * sibling.getHeight());
                double resfac = 25.4d / 300d;
                List<Object> toBeStored = new ArrayList<Object>(selection.size() + 1);
                List<String> assetIds = new ArrayList<String>(selection.size());
                int defaultViewingHeight = show.getDefaultViewingHeight();
                int variance = show.getVariance();
                String defaultDescription = exhibition.getDefaultDescription();
                for (Asset asset : selection) {
                    if (!accepts(asset))
                        continue;
                    ++index;
                    ++globalIndex;
                    Date dateCreated = asset.getDateTimeOriginal();
                    if (dateCreated == null)
                        dateCreated = asset.getDateTime();
                    double fac = Double.isNaN(siblingSize) ? resfac
                            : siblingSize / Math.max(320, Math.sqrt(
                                    asset.getHeight() * asset.getHeight() + asset.getWidth() * asset.getWidth()));
                    int h = (int) (asset.getHeight() * fac);
                    int w = (int) (asset.getWidth() * fac);
                    if ((asset.getRotation() + Utilities.orientationDegrees(asset)) % 180 == 90) {
                        int u = h;
                        h = w;
                        w = u;
                    }
                    double r = Math.random();
                    int v = (int) (r * variance + 0.5d) - variance / 2;
                    int ys = defaultViewingHeight + v + h / 2;
                    ys = Math.max(0, Math.min(ys, wall.getHeight()));
                    int xs = (int) (pos + 0.5d);
                    if (variance > 0 && exhibition.getShowGrid() && exhibition.getSnapToGrid()) {
                        org.eclipse.swt.graphics.Point pnt = UiUtilities.snapToGrid(xs, ys, w, h, wall.getHeight(),
                                exhibition.getGridSize(), exhibition.getGridSize());
                        xs = pnt.x;
                        ys = pnt.y;
                    }
                    if (cand != null) {
                        xs = cand.exhibit.getX();
                        ys = cand.exhibit.getY();
                    }
                    String assetId = asset.getStringId();
                    ExhibitImpl exhibit = new ExhibitImpl(UiUtilities.createSlideTitle(asset),
                            defaultDescription == null ? Messages.getString("ExhibitionView.inkjet_print") //$NON-NLS-1$
                                    : defaultDescription,
                            Core.toStringList(asset.getArtist(), " "), //$NON-NLS-1$
                            (dateCreated == null) ? "" //$NON-NLS-1$
                                    : EDF.format(dateCreated),
                            xs, ys, w, h, null, null, null, null, false, null, null, null, null, assetId);
                    if (globalIndex >= exhibits.size())
                        exhibits.add(exhibit);
                    else
                        exhibits.add(globalIndex, exhibit);
                    if (index >= wall.getExhibit().size())
                        wall.addExhibit(exhibit.getStringId());
                    else
                        wall.getExhibit().add(index, exhibit.getStringId());
                    exhibit.setWall_exhibit_parent(wall.getStringId());
                    pos += horizontalGap;
                    if (cand != null) {
                        pos = cand.getFullBoundsReference().getX();
                        deleteOp = new DeleteExhibitOperation(Collections.singletonList(cand), false);
                        deleteOp.execute(monitor, info);
                        cand = null;
                    }
                    PExhibit pExhibit = makeExhibit(wallPanel, (int) pos, exhibit);
                    PBounds eBounds = pExhibit.getFullBoundsReference();
                    pos = eBounds.getX() + eBounds.getWidth();
                    toBeStored.add(exhibit);
                    assetIds.add(assetId);
                    added.add(pExhibit);
                }
                setArtists();
                iWidth = (pos - oldPos);
                for (PExhibit pExhibit : trail) {
                    pExhibit.offset(iWidth, 0);
                    pExhibit.exhibit.setX((int) (pExhibit.exhibit.getX() + iWidth));
                    toBeStored.add(pExhibit.exhibit);
                }
                toBeStored.add(wall);
                storeSafelyAndUpdateIndex(null, toBeStored, assetIds);
            }
            updateActions(false);
            updateStatusLine();
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            added.clear();
            trail.clear();
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            List<IIdentifiableObject> toBeDeleted = new ArrayList<>(selection.size());
            List<IIdentifiableObject> toBeStored = new ArrayList<>(selection.size());
            List<String> assetIds = new ArrayList<String>(selection.size());
            for (PExhibit exhibit : added) {
                doRemoveExhibit(exhibit);
                toBeDeleted.add(exhibit.exhibit);
                assetIds.add(exhibit.getAssetId());
            }
            for (PExhibit pExhibit : trail) {
                pExhibit.offset(-iWidth, 0);
                pExhibit.exhibit.setX((int) (pExhibit.exhibit.getX() - iWidth));
                toBeStored.add(pExhibit.exhibit);
            }
            if (deleteOp != null)
                deleteOp.undo(monitor, info);
            toBeStored.add(wallPanel.getWall());
            storeSafelyAndUpdateIndex(toBeDeleted, toBeStored, assetIds);
            updateActions(false);
            updateStatusLine();
            return Status.OK_STATUS;
        }
    }

    public class PasteExhibitsOperation extends AbstractOperation {

        private final Point2D position;
        private List<PExhibit> added;
        private PWall wallPanel;
        private double iWidth;
        private List<PExhibit> trail = new ArrayList<PExhibit>();

        public PasteExhibitsOperation(Point2D position) {
            super(Messages.getString("ExhibitionView.paste_exhibits")); //$NON-NLS-1$
            this.position = position;
            added = new ArrayList<>(clipboard.size());
            wallPanel = findWall(position);
        }

        @Override
        public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            if (wallPanel != null) {
                ExhibitionImpl show = null;
                if (!clipboard.isEmpty())
                    show = getExhibition();
                if (show == null)
                    return Status.CANCEL_STATUS;
                double pos = 0;
                int index = 0;
                PWall wallPanel = findWall(position);
                if (wallPanel != null) {
                    double x = position.getX() - wallPanel.getOffset().getX();
                    for (PExhibit pExhibit : getSortedExhibits(wallPanel)) {
                        PBounds globalBounds = pExhibit.getGlobalBounds();
                        double p = globalBounds.getX() + globalBounds.getWidth();
                        if (p < x) {
                            PBounds eBounds = pExhibit.getFullBoundsReference();
                            pos = Math.max(pos, eBounds.getX() + eBounds.getWidth());
                            ++index;
                        } else {
                            trail.add(pExhibit);
                        }
                    }
                }
                double oldPos = pos;
                int globalIndex = index;
                Wall wall = wallPanel.getWall();
                for (Wall w1 : exhibition.getWall()) {
                    if (w1 == wall)
                        break;
                    globalIndex += w1.getExhibit().size();
                }
                List<Object> toBeStored = new ArrayList<Object>(clipboard.size() + 1);
                List<String> assetIds = new ArrayList<String>(clipboard.size());
                if (!clipboard.isEmpty())
                    clearSelection();
                for (IIdentifiableObject item : clipboard) {
                    ExhibitImpl exhibit = (ExhibitImpl) item;
                    ExhibitImpl copy = cloneExhibit(exhibit);
                    ++index;
                    ++globalIndex;
                    String assetId = copy.getAsset();
                    if (globalIndex >= exhibits.size())
                        exhibits.add(copy);
                    else
                        exhibits.add(globalIndex, copy);
                    if (index >= wall.getExhibit().size())
                        wall.addExhibit(copy.getStringId());
                    else
                        wall.getExhibit().add(index, copy.getStringId());
                    copy.setWall_exhibit_parent(wall.getStringId());
                    pos += horizontalGap;
                    PExhibit pExhibit = makeExhibit(wallPanel, (int) pos, copy);
                    PBounds eBounds = pExhibit.getFullBoundsReference();
                    pos = eBounds.getX() + eBounds.getWidth();
                    toBeStored.add(exhibit);
                    assetIds.add(assetId);
                    added.add(pExhibit);
                    pExhibit.setSelected(true);
                    lastSelection = recentSelection = pExhibit;
                }
                setArtists();
                iWidth = (pos - oldPos);
                for (PExhibit pExhibit : trail) {
                    pExhibit.offset(iWidth, 0);
                    pExhibit.exhibit.setX((int) (pExhibit.exhibit.getX() + iWidth));
                    toBeStored.add(pExhibit.exhibit);
                }
                toBeStored.add(wall);
                storeSafelyAndUpdateIndex(null, toBeStored, assetIds);
            }
            updateActions(false);
            updateStatusLine();
            return Status.OK_STATUS;
        }

        @Override
        public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            added.clear();
            trail.clear();
            return execute(monitor, info);
        }

        @Override
        public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
            List<IIdentifiableObject> toBeDeleted = new ArrayList<>(added.size());
            List<IIdentifiableObject> toBeStored = new ArrayList<>(added.size());
            List<String> assetIds = new ArrayList<String>(added.size());
            for (PExhibit exhibit : added) {
                doRemoveExhibit(exhibit);
                toBeDeleted.add(exhibit.exhibit);
                assetIds.add(exhibit.getAssetId());
            }
            for (PExhibit pExhibit : trail) {
                pExhibit.offset(-iWidth, 0);
                pExhibit.exhibit.setX((int) (pExhibit.exhibit.getX() - iWidth));
                toBeStored.add(pExhibit.exhibit);
            }
            toBeStored.add(wallPanel.getWall());
            storeSafelyAndUpdateIndex(toBeDeleted, toBeStored, assetIds);
            updateActions(false);
            updateStatusLine();
            return Status.OK_STATUS;
        }
    }

    private static class GridLine extends PSWTPath {
        private static final long serialVersionUID = 8838795470028525193L;
        private static final double MINRENDEREDDISTANCE = 4d;
        private final int lineDist;

        public GridLine(float[] xp, float[] yp, int lineDist) {
            this.lineDist = lineDist;
            setPathToPolyline(xp, yp);
            setStrokeColor(GridColor);
        }

        @Override
        protected void paint(PPaintContext ppc) {
            final double renderedLineDist = lineDist * ppc.getScale();
            if (renderedLineDist > MINRENDEREDDISTANCE)
                super.paint(ppc);
        }
    }

    private static class GuideLine extends PSWTPath {

        private static final long serialVersionUID = 5682799369373994468L;

        public GuideLine(float[] xp, float[] yp) {
            setPathToPolyline(xp, yp);
            setStrokeColor(GuideColor);
        }
    }

    private class PWall extends PPanel {

        private static final long serialVersionUID = 2123557338158980177L;
        private TextField info;
        private PSWTPath pInfoPlate;
        private PSWTPath pDoor;
        private int height;
        private TextField artistsField;

        public PWall(Wall wall) {
            super(wall, wall.getWidth(), wall.getHeight());
            height = wall.getHeight();
            // Grid
            addGrid(this);
            // Viewing height guide
            addGuide(this);
            // Door and info plate
            int sx = exhibition.getStartX();
            int sy = exhibition.getStartY();
            double angle = wall.getGAngle();
            double cos = Math.cos(Math.toRadians(angle));
            double sin = Math.sin(Math.toRadians(angle));
            int wallX1 = wall.getGX();
            int wallY1 = wall.getGY();
            int wallW = wall.getWidth();
            double wallX2 = wallX1 + cos * wallW;
            double wallY2 = wallY1 + sin * wallW;
            boolean door = false;
            if (sx >= Math.min(wallX1, wallX2) - 50 && sx <= Math.max(wallX1, wallX2) + 50
                    && sy >= Math.min(wallY1, wallY2) - 50 && sy <= Math.max(wallY1, wallY2) + 50)
                door = Math.abs(cos) > 0.01d ? Math.abs(wallY1 - sy + (sx - wallX1) * sin / cos) < 50 : true;
            if (door) {
                float doorX1 = (float) (sin * (sy - wallY1) + cos * (sx - wallX1)) - DOORWIDTH / 2;
                float doorX2 = doorX1 + DOORWIDTH;
                int infoPlatePosition = exhibition.getInfoPlatePosition();
                float xinfoShift = 0;
                float infoX1 = doorX2 + INFOGAP;
                if (infoPlatePosition == INFOLEFT) {
                    xinfoShift = INFOWIDTH + INFOGAP;
                    infoX1 = doorX1;
                }
                pDoor = new PSWTPath();
                pDoor.setPathToPolyline(new float[] { 0, 0, DOORWIDTH, DOORWIDTH },
                        new float[] { DOORHEIGHT, 0, 0, DOORHEIGHT });
                pDoor.setOffset(doorX1 + xinfoShift, height - DOORHEIGHT);
                pDoor.setStrokeColor(DoorColor);
                addChild(0, pDoor);
                if (infoPlatePosition != INFONONE) {
                    pInfoPlate = new PSWTPath();
                    pInfoPlate.setPathToPolyline(new float[] { 0, 0, INFOWIDTH, INFOWIDTH, 0 },
                            new float[] { INFOHEIGHT, 0, 0, INFOHEIGHT, INFOHEIGHT });
                    pInfoPlate.setStrokeColor(DoorColor);
                    pInfoPlate.setPickable(true);
                    addChild(pInfoPlate);
                    pInfoPlate.setOffset(infoX1, height - DOORHEIGHT);
                    String fontFamily = "Arial"; //$NON-NLS-1$
                    int fontsize = 22;
                    // caption
                    double ypos = INFOMARGINS;
                    Color penColor = new Color(32, 32, 32);
                    Color background = (Color) pInfoPlate.getPaint();
                    GreekedPSWTText caption = new GreekedPSWTText(exhibition.getName(),
                            new Font(fontFamily, Font.BOLD, fontsize + 6));
                    caption.setPenColor(penColor);
                    caption.setBackgroundColor(background);
                    caption.setOffset(INFOMARGINS / 2, ypos);
                    caption.setTransparent(true);
                    caption.setTextWidth(INFOWIDTH - INFOMARGINS);
                    caption.setAlignment(SWT.CENTER);
                    pInfoPlate.addChild(caption);
                    ypos += caption.getFullBoundsReference().getHeight() + INFOMARGINS;
                    if (!exhibition.getHideCredits()) {
                        artistsField = createTextLine(pInfoPlate, getArtists(), 3, INFOWIDTH - INFOMARGINS,
                                INFOMARGINS / 2, ypos, penColor, background, fontFamily, Font.PLAIN, fontsize,
                                ISpellCheckingService.NOSPELLING,
                                SWT.MULTI | SWT.WRAP | SWT.READ_ONLY | SWT.CENTER);
                        ypos += artistsField.getFullBoundsReference().getHeight() + INFOMARGINS;
                    }
                    // info
                    info = createTextLine(pInfoPlate, exhibition.getInfo(), 3, INFOWIDTH - INFOMARGINS,
                            INFOMARGINS / 2, ypos, penColor, background, fontFamily, Font.PLAIN, fontsize,
                            ISpellCheckingService.DESCRIPTIONOPTIONS, SWT.MULTI | SWT.WRAP | SWT.CENTER);
                    ypos += info.getFullBoundsReference().getHeight();
                }
            }
            // buttons
            deleteButton = createButton(canvas, Icons.largeDelete,
                    Messages.getString("ExhibitionView.delete_wall")); //$NON-NLS-1$
            propButton = createButton(canvas, Icons.largeProperties,
                    Messages.getString("ExhibitionView.wall_properties")); //$NON-NLS-1$
            Rectangle bounds = positionButtons(height);
            // Location
            createTitle(canvas, wall.getLocation(), 11 * bounds.width / 4, height - bounds.height,
                    LOCATIONTITLECOLOR, (Color) getPaint(), 18);
        }

        public void setArtists() {
            if (artistsField != null)
                artistsField.setText(getArtists());
        }

        public void resize() {
            int h = ((Wall) data).getHeight();
            double diff = h - height;
            height = h;
            setPathToRectangle(0, 0, (((Wall) data).getWidth()), height);
            // Grid
            addGrid(this);
            // Viewing height guide
            addGuide(this);
            if (pDoor != null)
                pDoor.setOffset(pDoor.getOffset().getX(), height - DOORHEIGHT);
            if (pInfoPlate != null)
                pInfoPlate.setOffset(pInfoPlate.getOffset().getX(), height - DOORHEIGHT);
            positionButtons(height);
            if (diff != 0) {
                positionImages(0, diff);
                positionSubsequentWalls(diff);
            }
        }

        private void positionSubsequentWalls(double ydiff) {
            for (int i = walls.indexOf(this) + 1; i < walls.size(); i++)
                walls.get(i).offset(0, ydiff);
        }

        private void positionImages(double xdiff, double ydiff) {
            ListIterator<?> it = getChildrenIterator();
            while (it.hasNext()) {
                Object obj = it.next();
                if (obj instanceof PExhibit)
                    ((PExhibit) obj).offset(xdiff, ydiff);
            }
        }

        private Rectangle positionButtons(int h) {
            Rectangle bounds = deleteButton.getImage().getBounds();
            int margins = bounds.width / 4;
            deleteButton.setOffset(margins, h - bounds.height - margins);
            bounds = propButton.getImage().getBounds();
            margins = bounds.width / 4;
            propButton.setOffset(bounds.width + 2 * margins, h - bounds.height - margins);
            return bounds;
        }

        public Wall getWall() {
            return (Wall) data;
        }

        public void processTextEvent(TextField focus) {
            if (focus == info)
                performOperation(new EditInfoTextOperation(exhibition, focus.getText(), focus));
            else if (focus == title)
                performOperation(new EditWallTextOperation(getWall(), focus.getText(), focus));
        }

        @Override
        protected void deletePressed() {
            setPickedNode(PWall.this);
            deleteWallAction.run();
        }

        @Override
        protected void propertiesPressed() {
            textEventHandler.commit();
            editWallLegend(PWall.this);
        }

        /**
         * @return the pInfoPlate
         */
        public PSWTPath getpInfoPlate() {
            return pInfoPlate;
        }
    }

    class PExhibit extends PPresentationItem {

        private static final long serialVersionUID = -8619376689449783925L;

        private final class ExhibitHandle extends AbstractHandle {
            private static final long serialVersionUID = -2915474593495856476L;

            class ExhibitLocator extends PLocator {
                private static final long serialVersionUID = 1228742743568586994L;

                @Override
                public double locateX() {
                    return ExhibitHandle.this.locateX() + size / 4d;
                }

                @Override
                public double locateY() {
                    return ExhibitHandle.this.locateY() - size / 4d;
                }
            }

            private double shiftX;
            private double shiftY;
            private int size;
            private double oldScale;
            private ExhibitImpl backup;

            private ExhibitHandle(int size) {
                super(SWT.CURSOR_SIZENESW, Messages.getString("ExhibitionView.size_tooltip")); //$NON-NLS-1$
                setLocator(new ExhibitLocator());
                this.size = size;
                setPaint(HANDLECOLOR);
                setStrokeColor(null);
            }

            @Override
            public void startHandleDrag(Point2D localPoint, PInputEvent event) {
                backup = cloneExhibit(exhibit);
                shiftX = 0;
                shiftY = 0;
                oldScale = pImage.getScale();
                PExhibit.this.raiseToTop();
            }

            @Override
            public void dragHandle(PDimension localDimension, PInputEvent event) {
                double width = localDimension.getWidth();
                double height = localDimension.getHeight();
                shiftX += width;
                shiftY -= height;
                int w = (int) (backup.getWidth() + shiftX);
                int h = (int) (backup.getHeight() + shiftY);
                if (w >= 0 && h >= 0) {
                    Integer i = exhibit.getMatWidth();
                    int matWidth = (i == null) ? exhibition.getMatWidth() : i;
                    i = exhibit.getFrameWidth();
                    int frameWidth = (i == null) ? exhibition.getFrameWidth() : i;
                    double f = (((double) w / backup.getWidth()) + ((double) h / backup.getHeight())) / 2;
                    int iWidth = (int) (backup.getWidth() * f);
                    exhibit.setWidth(iWidth);
                    int iHeight = (int) (backup.getHeight() * f);
                    exhibit.setHeight(iHeight);
                    pImage.setScale(oldScale * f);
                    if (pOuterFrame != null)
                        pOuterFrame.setPathToRectangle(0f, 0f, iWidth + 2 * (frameWidth + matWidth),
                                iHeight + 2 * (frameWidth + matWidth));
                    if (pMat != null)
                        pMat.setPathToRectangle(frameWidth, frameWidth, iWidth + 2 * matWidth,
                                iHeight + 2 * matWidth);
                    setLabelOffset(pImage.getFullBoundsReference(), exhibit, exhibition);
                }
                invalidatePaint();
                relocateHandle();
                createCue(exhibit);
            }

            private void createCue(ExhibitImpl exhibit) {
                Display display = canvas.getDisplay();
                Image image = new Image(display, 48, 48);
                Rectangle bounds = image.getBounds();
                GC gc = new GC(image);
                gc.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
                gc.fillRectangle(bounds);
                gc.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
                gc.drawRectangle(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1);
                dimFormat.setMaximumFractionDigits(1);
                dimFormat.setMinimumFractionDigits(1);
                TextLayout layout = new TextLayout(display);
                layout.setAlignment(SWT.CENTER);
                layout.setWidth(bounds.width - 6);
                layout.setText(new StringBuilder().append(dimFormat.format(exhibit.getWidth() / 10d)).append(" x ") //$NON-NLS-1$
                        .append(dimFormat.format(exhibit.getHeight() / 10d)).append(" cm").toString()); //$NON-NLS-1$
                Rectangle tbounds = layout.getBounds();
                layout.draw(gc, (bounds.width - tbounds.width) / 2, (bounds.height - tbounds.height) / 2);
                layout.dispose();
                gc.dispose();
                setCue(new Cursor(display, image.getImageData(), 0, 0));
                image.dispose();
            }

            @Override
            public void endHandleDrag(Point2D localPoint, PInputEvent event) {
                setCue(null);
                performOperation(new EditExhibitOperation(PExhibit.this, backup, exhibit));
            }

            public double locateX() {
                return pImage.getFullBoundsReference().getWidth() + pImage.getOffset().getX();
            }

            public double locateY() {
                return pImage.getOffset().getY();
            }
        }

        private ExhibitImpl exhibit;
        private PSWTHandle midPoint;
        private double imageWidth;
        private double imageHeight;
        private PSWTPath imageLabel;
        private TextField description;
        private TextField credits;
        private TextField creationDate;
        private PSWTText resolutionWarning;
        private PPresentationPanel pOuterFrame;
        private PPresentationPanel pMat;
        private final PSWTCanvas canvas;
        private final PWall pwall;
        private PSWTPath soldMark;

        public PExhibit(PSWTCanvas canvas, PWall pwall, final ExhibitImpl exhibit, int pos, int y) {
            this.canvas = canvas;
            this.pwall = pwall;
            this.exhibit = exhibit;
            setPickable(false);
            createContent(canvas, pwall, exhibit, pos, y);
        }

        private void createContent(PSWTCanvas canvas, PWall pwall, final ExhibitImpl exhibit, double pos,
                double y) {
            String fontFamily = "Arial"; //$NON-NLS-1$
            int fontsize = 11;
            if (exhibition.getLabelFontFamily() != null && !exhibition.getLabelFontFamily().isEmpty()) {
                fontFamily = exhibition.getLabelFontFamily();
                fontsize = Math.max(5, exhibition.getLabelFontSize());
            }
            imageWidth = exhibit.getWidth();
            imageHeight = exhibit.getHeight();
            Integer i = exhibit.getMatWidth();
            int matWidth = (i == null) ? exhibition.getMatWidth() : i;
            // Colors
            Rgb_type matColor = exhibit.getMatColor();
            if (matColor == null)
                matColor = exhibition.getMatColor();
            if (matColor == null)
                matColor = new Rgb_typeImpl(255, 255, 252);
            i = exhibit.getFrameWidth();
            int frameWidth = (i == null) ? exhibition.getFrameWidth() : i;
            Rgb_type outerFrameColor = exhibit.getFrameColor();
            if (outerFrameColor == null)
                outerFrameColor = exhibition.getFrameColor();
            if (outerFrameColor == null)
                outerFrameColor = new Rgb_typeImpl(8, 8, 8);
            // Events
            PBasicInputEventHandler inputEventHandler = new PBasicInputEventHandler() {
                @Override
                public void mousePressed(PInputEvent event) {
                    setPickedNode(event.getPickedNode());
                }

                @Override
                public void mouseReleased(PInputEvent event) {
                    if (getPickedNode() == imageLabel && event.getClickCount() == 2)
                        editLayout();
                }
            };
            // Frame and Mat
            if (frameWidth != 0)
                addChild(pOuterFrame = new PPresentationPanel(0, 0,
                        (float) (imageWidth + 2 * (frameWidth + matWidth)),
                        (float) (imageHeight + 2 * (frameWidth + matWidth)),
                        new Color(outerFrameColor.getR(), outerFrameColor.getG(), outerFrameColor.getB()), null,
                        inputEventHandler));
            if (matWidth != 0)
                addChild(pMat = new PPresentationPanel(frameWidth, frameWidth, (float) (imageWidth + 2 * matWidth),
                        (float) (imageHeight + 2 * matWidth),
                        new Color(matColor.getR(), matColor.getG(), matColor.getB()), null, inputEventHandler));
            // Image
            AssetImpl asset = Core.getCore().getDbManager().obtainAsset(exhibit.getAsset());
            pImage = new PSWTAssetThumbnail(canvas, ExhibitionView.this, asset);
            PBounds bounds = pImage.getBoundsReference();
            double width = bounds.getWidth();
            double scale = imageWidth / width;
            addChild(pImage);
            pImage.setPickable(true);
            setBounds(0, 0, imageWidth, imageHeight);
            pImage.scale(scale);
            pImage.setOffset(frameWidth + matWidth, frameWidth + matWidth);
            // Select frame
            pImage.addChild(createSelectionFrame(bounds));
            // Label
            Boolean b = exhibit.getHideLabel();
            boolean hideLabel = b == null ? exhibition.getHideLabel() : b.booleanValue();
            if (!hideLabel) {
                PBounds iBounds = pImage.getFullBoundsReference();
                imageLabel = new PPresentationPanel(0f, 0f, labelWidth, labelHeight, Color.white, Color.LIGHT_GRAY,
                        inputEventHandler);
                imageLabel.setPickable(true);
                addChild(imageLabel);
                double ypos = labelMargins;
                Color penColor = LABELTEXTCOLOR;
                Color background = (Color) pwall.getPaint();
                switch (exhibition.getLabelSequence()) {
                case Constants.EXHLABEL_TIT_CRED_DES:
                    ypos += createCaption(exhibit, fontFamily, fontsize, 0, ypos, penColor, background);
                    ypos += createCredits(exhibit, fontFamily, fontsize, 0, ypos, penColor, background);
                    ypos += createDescription(exhibit, fontFamily, fontsize, labelIndent, ypos, penColor,
                            background);
                    break;
                case Constants.EXHLABEL_CRED_TIT_DES:
                    ypos += createCredits(exhibit, fontFamily, fontsize, 0, ypos, penColor, background);
                    ypos += createCaption(exhibit, fontFamily, fontsize, 0, ypos, penColor, background);
                    ypos += createDescription(exhibit, fontFamily, fontsize, labelIndent, ypos, penColor,
                            background);
                    break;
                default:
                    ypos += createCaption(exhibit, fontFamily, fontsize, 0, ypos, penColor, background);
                    ypos += createDescription(exhibit, fontFamily, fontsize, labelIndent, ypos, penColor,
                            background);
                    ypos += createCredits(exhibit, fontFamily, fontsize, labelIndent, ypos, penColor, background);
                    break;
                }
                // date
                creationDate = createTextLine(imageLabel, exhibit.getDate(), 3, labelWidth - labelMargins * 2,
                        labelMargins + labelIndent, ypos, penColor, background, fontFamily, Font.PLAIN, fontsize,
                        ISpellCheckingService.NOSPELLING, SWT.SINGLE | SWT.WRAP);
                ypos += creationDate.getFullBoundsReference().getHeight();
                // warning
                resolutionWarning = new PSWTText("", new Font(fontFamily, //$NON-NLS-1$
                        Font.ITALIC, Math.max(fontsize - 2, 4)));
                resolutionWarning.setPenColor(Color.RED);
                resolutionWarning.setOffset(labelMargins, ypos);
                resolutionWarning.setPickable(false);
                imageLabel.addChild(resolutionWarning);
                resolutionWarning.setGreekThreshold(1);
                computeResolutionWarning(asset);
                // position and bounds
                setBounds();
                setLabelOffset(iBounds, exhibit, exhibition);
                // sold mark
                updateSoldMark();
            }
            setOffset(pos - frameWidth - matWidth, y - frameWidth - matWidth);
            installHandleEventHandlers(pImage, false, false, this);
            if (pMat != null)
                installHandleEventHandlers(pMat, false, false, this);
            if (pOuterFrame != null)
                installHandleEventHandlers(pOuterFrame, false, false, this);
            if (imageLabel != null) {
                installHandleEventHandlers(imageLabel, true, true, this);
                imageLabel.addInputEventListener(inputEventHandler);
            }
            pImage.addInputEventListener(new PBasicInputEventHandler() {
                @Override
                public void mousePressed(PInputEvent event) {
                    PNode pickedNode = event.getPickedNode();
                    setPickedNode(pickedNode);
                    if (event.isLeftMouseButton() && !event.isShiftDown() && !event.isAltDown()
                            && (pickedNode instanceof PSWTAssetThumbnail))
                        selectExhibit((PExhibit) pickedNode.getParent(), event.getModifiers(),
                                event.getClickCount());
                }
            });

            // size handle
            midPoint = new ExhibitHandle(handleSize);
            addChild(midPoint);
        }

        public void updateSoldMark() {
            if (exhibit.getSold()) {
                soldMark = new PSWTPath();
                soldMark.setPathToEllipse(
                        (int) imageLabel.getBoundsReference().getWidth() - 3 * soldMarkDiameter / 2,
                        soldMarkDiameter / 2, soldMarkDiameter, soldMarkDiameter);
                soldMark.setPaint(Color.RED);
                soldMark.setStrokeColor(null);
                imageLabel.addChild(soldMark);
            } else if (soldMark != null) {
                imageLabel.removeChild(soldMark);
                soldMark = null;
            }
        }

        public void rescale() {
            imageWidth = exhibit.getWidth();
            imageHeight = exhibit.getHeight();
            pImage.setScale(imageWidth / pImage.getBoundsReference().getWidth());
            Integer i = exhibit.getMatWidth();
            int matWidth = (i == null) ? exhibition.getMatWidth() : i;
            i = exhibit.getFrameWidth();
            int frameWidth = (i == null) ? exhibition.getFrameWidth() : i;
            if (pOuterFrame != null)
                pOuterFrame.setPathToRectangle(0f, 0f, (float) (imageWidth + 2 * (frameWidth + matWidth)),
                        (float) (imageHeight + 2 * (frameWidth + matWidth)));
            if (pMat != null)
                pMat.setPathToRectangle(frameWidth, frameWidth, (float) (imageWidth + 2 * matWidth),
                        (float) (imageHeight + 2 * matWidth));
            setLabelOffset(pImage.getFullBoundsReference(), exhibit, exhibition);
        }

        private double createCredits(final ExhibitImpl exhibit, String fontFamily, int fontsize, int indent,
                double ypos, Color penColor, Color background) {
            credits = createTextLine(imageLabel, exhibit.getCredits(), 3, labelWidth - labelMargins * 2,
                    labelMargins + indent, ypos, penColor, background, fontFamily, Font.PLAIN, fontsize,
                    ISpellCheckingService.NOSPELLING, SWT.SINGLE | SWT.WRAP);
            return credits.getFullBoundsReference().getHeight();
        }

        private double createDescription(final ExhibitImpl exhibit, String fontFamily, int fontsize, int indent,
                double ypos, Color penColor, Color background) {
            description = createTextLine(imageLabel, exhibit.getDescription(), 3, labelWidth - labelMargins * 2,
                    labelMargins + indent, ypos, penColor, background, fontFamily, Font.PLAIN, fontsize,
                    ISpellCheckingService.DESCRIPTIONOPTIONS, SWT.MULTI | SWT.WRAP);
            return description.getFullBoundsReference().getHeight();
        }

        private double createCaption(final ExhibitImpl exhibit, String fontFamily, int fontsize, int indent,
                double ypos, Color penColor, Color background) {
            caption = createTextLine(imageLabel, exhibit.getTitle(), 3, labelWidth - labelMargins * 2,
                    labelMargins + indent, ypos, penColor, background, fontFamily, Font.BOLD, fontsize + 1,
                    ISpellCheckingService.TITLEOPTIONS, SWT.SINGLE | SWT.WRAP);
            return caption.getFullBoundsReference().getHeight();
        }

        public void setImageHeight(int height) {
            imageHeight = height;
        }

        public void setImageWidth(int width) {
            imageWidth = width;
        }

        private void computeResolutionWarning(AssetImpl asset) {
            int pixels = asset.getWidth();
            int w = exhibit.getWidth();
            int h = exhibit.getWidth();
            double d = Math.sqrt((double) w * w + (double) h * h);
            double c = Math.sqrt(d) * 20;
            double res = Math.max(180, 100000 / c);
            double dpi = pixels * 25.4d / w;
            resolutionWarning
                    .setText(NLS.bind(Messages.getString("ExhibitionView.image_res_problematic"), (int) dpi)); //$NON-NLS-1$
            resolutionWarning.setVisible(dpi < res);
        }

        private void setLabelOffset(PBounds iBounds, ExhibitImpl exhibit, ExhibitionImpl exhibition) {
            if (imageLabel != null) {
                PBounds lBounds = imageLabel.getBoundsReference();
                Integer i = exhibit.getMatWidth();
                int matWidth = (i == null) ? exhibition.getMatWidth() : i;
                i = exhibit.getFrameWidth();
                int frameWidth = (i == null) ? exhibition.getFrameWidth() : i;
                int tara = matWidth + frameWidth;
                double iWidth = iBounds.getWidth() + 2 * tara;
                double iHeight = iBounds.getHeight() + 2 * tara;
                double lx = 0;
                double ly = 0;
                Integer a = exhibit.getLabelAlignment();
                if (a == null)
                    a = exhibition.getLabelAlignment();
                int alignment = a == null ? Constants.DEFAULTLABELALIGNMENT : a.intValue();
                Integer d = exhibit.getLabelDistance();
                if (d == null)
                    d = exhibition.getLabelDistance();
                int distance = d == null ? Constants.DEFAULTLABELDISTANCE : d.intValue();
                i = exhibit.getLabelIndent();
                if (i == null)
                    i = exhibition.getLabelIndent();
                int indent = i == null ? Constants.DEFAULTLABELINDENT : i.intValue();
                switch (alignment / 9) {
                case 0:
                    lx += iWidth + distance;
                    ly -= indent - align(iHeight, alignment, lBounds.getHeight(), false);
                    break;
                case 1:
                    lx += align(iWidth, alignment, lBounds.getWidth(), true) + indent;
                    ly += iHeight + distance;
                    break;
                case 2:
                    lx -= distance + lBounds.getWidth();
                    ly -= indent - align(iHeight, alignment, lBounds.getHeight(), true);
                    break;
                default:
                    lx += align(iWidth, alignment, lBounds.getWidth(), false) + indent;
                    ly -= distance + lBounds.getHeight();
                    break;
                }
                imageLabel.setOffset(lx, ly);
            }
        }

        private double align(double iWidth, int alignment, double lWidth, boolean reverse) {
            double labelAlign;
            switch (alignment % 3) {
            case 0:
                labelAlign = 0;
                break;
            case 1:
                labelAlign = lWidth / 2;
                break;
            default:
                labelAlign = lWidth;
                break;
            }
            switch (reverse ? 2 - (alignment / 3 % 3) : alignment / 3 % 3) {
            case 0:
                return -labelAlign;
            case 1:
                return iWidth / 2 - labelAlign;
            default:
                return iWidth - labelAlign;
            }
        }

        public void update(Asset asset) {
            double pos = getOffset().getX() + pImage.getOffset().getX();
            double y = getOffset().getY() + pImage.getOffset().getY();
            removeAllChildren();
            pImage.getImage().dispose();
            pImage = null;
            pOuterFrame = null;
            pMat = null;
            createContent(canvas, pwall, exhibit, pos, y);
        }

        void relocateAllHandles() {
            midPoint.relocateHandle();
        }

        public void processTextEvent(TextField focus) {
            int type = -1;
            if (focus == caption)
                type = EditTextOperation.CAPTION;
            else if (focus == description)
                type = EditTextOperation.DESCRIPTION;
            else if (focus == credits)
                type = EditTextOperation.CREDITS;
            else if (focus == creationDate)
                type = EditTextOperation.DATE;
            if (type >= 0)
                performOperation(new EditTextOperation(exhibit, focus.getText(), focus, type));
        }

        public void updateFont(String fontFamily, int fontsize) {
            updateFont(caption, fontFamily, Font.BOLD, fontsize + 1);
            updateFont(description, fontFamily, Font.BOLD, fontsize);
            updateFont(credits, fontFamily, Font.BOLD, fontsize);
            updateFont(creationDate, fontFamily, Font.BOLD, fontsize);
            updateFont(resolutionWarning, fontFamily, Font.BOLD, Math.max(4, fontsize));
            setBounds();
        }

        private void setBounds() {
            if (imageLabel != null) {
                PBounds childrenBounds = imageLabel.getUnionOfChildrenBounds(null);
                childrenBounds.x = 0;
                childrenBounds.y = 0;
                childrenBounds.width += 2 * labelMargins;
                childrenBounds.height += 2 * labelMargins;
                imageLabel.setBounds(childrenBounds);
            }
            PBounds bounds = getUnionOfChildrenBounds(null);
            bounds.x = bounds.y = 0;
            setBounds(bounds);
        }

        private void updateFont(PSWTText field, String fontFamily, int style, int size) {
            field.setFont(new Font(fontFamily, style, size));
        }

        private void updateFont(TextField field, String fontFamily, int style, int size) {
            field.setFont(new Font(fontFamily, style, size));
        }

        public void setTitleColor(Color color) {
            // ignore
        }

        public void setPenColor(Color color) {
            // ignore
        }

        public String getAssetId() {
            return exhibit.getAsset();
        }

    }

    public static final String ID = "com.bdaum.zoom.ui.ExhibitionView"; //$NON-NLS-1$
    private static final String LAST_EXHIBITION = "com.bdaum.zoom.lastExhibition"; //$NON-NLS-1$
    protected static final SimpleDateFormat EDF = new SimpleDateFormat("yyyy"); //$NON-NLS-1$
    public static final String EXHIBITION_PERSPECTIVE = "com.bdaum.zoom.PresentationPerspective"; //$NON-NLS-1$
    private static final float DOORWIDTH = 1000;
    private static final int DOORHEIGHT = 2000;
    private static final int INFOLEFT = 1;
    private static final int INFONONE = -1;
    private static final int INFOMARGINS = 40;
    private static final int INFOWIDTH = 950;
    private static final int INFOGAP = 50;
    private static final int INFOHEIGHT = 1333;
    private static NumberFormat dimFormat = NumberFormat.getNumberInstance();
    private static final int labelWidth = 140;
    private static final int labelHeight = 70;
    private static final int labelMargins = 10;
    private static final int soldMarkDiameter = labelMargins;
    private static final int labelIndent = 10;
    private static final int handleSize = 10;
    protected static final double horizontalGap = 80;

    protected static final Color GridColor = new Color(224, 224, 224);
    protected static final Color DoorColor = new Color(0, 255, 255);
    protected static final Color GuideColor = new Color(64, 192, 192);
    protected static final Color WALLBGCOLOR = new Color(255, 255, 250);
    protected static final Color SELECTIONBGCOLOR = new Color(224, 224, 124);
    protected static final Point DRAGTOLERANCE = new Point(0, 0);
    protected static final Color LOCATIONTITLECOLOR = new Color(128, 128, 128);
    protected static final Color LABELTEXTCOLOR = new Color(92, 92, 92);
    protected static final Color HANDLECOLOR = new Color(0, 224, 0);

    private ExhibitionImpl exhibition;
    private List<PWall> walls = new ArrayList<PWall>(5);
    private List<ExhibitImpl> exhibits = new ArrayList<ExhibitImpl>(50);
    private Action addWallAction;
    private Action layoutAction;
    private Action markSoldAction;
    private Action generateAction;
    private Set<String> offlineImages = null;
    private Action deleteWallAction;
    private Action editWallAction;

    public String getArtists() {
        Set<String> artists = new HashSet<String>();
        for (Exhibit exhibit : exhibits)
            artists.addAll(Core.fromStringList(exhibit.getCredits(), ",")); //$NON-NLS-1$
        String[] artistArray = artists.toArray(new String[artists.size()]);
        Arrays.sort(artistArray);
        return Core.toStringList(artistArray, ", "); //$NON-NLS-1$
    }

    @Override
    public void init(IViewSite site, IMemento memento) throws PartInitException {
        super.init(site, memento);
        if (memento != null) {
            String lastSelection = memento.getString(LAST_EXHIBITION);
            ExhibitionImpl obj = Core.getCore().getDbManager().obtainById(ExhibitionImpl.class, lastSelection);
            if (obj != null)
                setExhibition(obj);
        }
    }

    public void addGrid(PWall pwall) {
        List<PNode> gridLines = new ArrayList<PNode>();
        ListIterator<?> it = pwall.getChildrenIterator();
        while (it.hasNext()) {
            Object obj = it.next();
            if (obj instanceof GridLine)
                gridLines.add((PNode) obj);
        }
        pwall.removeChildren(gridLines);
        Wall wall = pwall.getWall();
        if (exhibition.getShowGrid()) {
            int gridSize = exhibition.getGridSize();
            int width = wall.getWidth();
            int height = wall.getHeight();
            float[] xdim = new float[] { 0f, width };
            float[] ydim = new float[] { 0f, 0f };
            for (float y = height - gridSize; y > 0; y -= gridSize) {
                ydim[0] = ydim[1] = y;
                pwall.addChild(0, new GridLine(xdim, ydim, gridSize));
            }
            ydim[0] = 0;
            ydim[1] = height;
            for (float x = gridSize; x < width; x += gridSize) {
                xdim[0] = xdim[1] = x;
                pwall.addChild(0, new GridLine(xdim, ydim, gridSize));
            }
        }
    }

    public void addGuide(PWall pwall) {
        List<PNode> guideLines = new ArrayList<PNode>();
        ListIterator<?> it = pwall.getChildrenIterator();
        while (it.hasNext()) {
            Object obj = it.next();
            if (obj instanceof GuideLine)
                guideLines.add((PNode) obj);
        }
        pwall.removeChildren(guideLines);
        Wall wall = pwall.getWall();
        int h = wall.getHeight() - wall.getExhibition_wall_parent().getDefaultViewingHeight();
        pwall.addChild(0, new GuideLine(new float[] { 0f, wall.getWidth() }, new float[] { h, h }));
    }

    @Override
    public void saveState(IMemento memento) {
        if (memento != null && exhibition != null)
            memento.putString(LAST_EXHIBITION, exhibition.getStringId());
        super.saveState(memento);
    }

    @Override
    protected String createHoverText(PNode thumbnail) {
        PNode parent = thumbnail.getParent();
        if (parent instanceof PExhibit)
            return UiActivator.getDefault().getHoverManager().getHoverText(
                    "com.bdaum.zoom.ui.hover.exhibition.exhibit", //$NON-NLS-1$
                    ((PExhibit) parent).exhibit, this);
        return ""; //$NON-NLS-1$
    }

    @Override
    protected String createHoverTitle(PNode thumbnail) {
        PNode parent = thumbnail.getParent();
        if (parent instanceof PExhibit)
            return UiActivator.getDefault().getHoverManager()
                    .getHoverTitle("com.bdaum.zoom.ui.hover.exhibition.exhibit", ((PExhibit) parent).exhibit, this); //$NON-NLS-1$
        return ""; //$NON-NLS-1$
    }

    @Override
    public void createPartControl(Composite parent) {
        super.createPartControl(parent);
        canvas.setData(CSSProperties.ID, CSSProperties.EXHIBITION);
        undoContext = new UndoContext() {
            @Override
            public String getLabel() {
                return Messages.getString("ExhibitionView.exh_undo_context"); //$NON-NLS-1$
            }
        };
        setColor(canvas);
        addWheelListener(0.1d, 100d);
        textEventHandler = new TextEventHandler(selectionBackgroundColor);
        PBasicInputEventHandler eventHandler = new PBasicInputEventHandler() {

            private boolean dragged;

            @Override
            public void keyPressed(PInputEvent event) {
                if (textEventHandler.hasFocus())
                    textEventHandler.keyPressed(event);
                else
                    pan(event);
            }

            @Override
            public void mouseMoved(PInputEvent event) {
                currentMousePosition = event.getPosition();
            }

            @Override
            public void mousePressed(PInputEvent event) {
                dragged = false;
                setPickedNode(event.getPickedNode());
            }

            @Override
            public void mouseDragged(PInputEvent event) {
                dragged = true;
                textEventHandler.mouseDragged(event);
            }

            @Override
            public void mouseReleased(PInputEvent event) {
                positionX = null;
                if (!dragged) {
                    PNode picked = getPickedNode();
                    textEventHandler.mouseReleased(event);
                    if (event.getClickCount() == 2) {
                        PCamera camera = event.getCamera();
                        if (oldTransform == null) {
                            oldTransform = camera.getViewTransform();
                            if (picked instanceof PExhibit)
                                camera.scaleViewAboutPoint(15d / camera.getViewScale(), event.getPosition().getX(),
                                        event.getPosition().getY());
                            else {
                                if (picked instanceof PWall) {
                                    PWall pwall = (PWall) picked;
                                    PSWTPath infoPlate = pwall.getpInfoPlate();
                                    if (infoPlate != null) {
                                        Point2D offset = infoPlate.getOffset();
                                        Point2D position = event.getPositionRelativeTo(pwall);
                                        if (position.getX() >= offset.getX()
                                                && position.getX() <= offset.getX() + INFOWIDTH
                                                && position.getY() >= offset.getY()
                                                && position.getY() <= offset.getY() + INFOHEIGHT) {
                                            camera.scaleViewAboutPoint(7.5d / camera.getViewScale(),
                                                    event.getPosition().getX(), event.getPosition().getY());
                                            return;
                                        }
                                    }
                                }
                                if (!(picked instanceof PSWTText))
                                    event.getCamera().setViewTransform(new AffineTransform());
                            }
                        } else {
                            camera.setViewTransform(oldTransform);
                            resetTransform();
                        }
                    } else if (event.isRightMouseButton()) {
                        positionX = event.getPosition();
                        positionRelativeToCamera = event.getPositionRelativeTo(canvas.getCamera());
                    } else if (event.isControlDown()) {
                        if (picked == surface)
                            propertiesAction.run();
                        else if (picked instanceof PWall)
                            editWallAction.run();
                    }
                }
            }
        };
        canvas.getRoot().getDefaultInputManager().setKeyboardFocus(eventHandler);
        canvas.addInputEventListener(eventHandler);
        setPanAndZoomHandlers();
        // Setup
        setupExhibition(getViewSite(), exhibition, 0);
        // Drop-Untersttzung
        addDropListener(canvas);
        // Create the help context id for the viewer's control
        PlatformUI.getWorkbench().getHelpSystem().setHelp(canvas, HelpContextIds.EXHIBITION_VIEW);
        // Hover
        installHoveringController();
        // Actions
        makeActions();
        installListeners();
        hookContextMenu();
        contributeToActionBars();
        if (exhibition != null)
            setInput(exhibition);
        addCatalogListener();
        updateActions(false);
    }

    @Override
    protected void makeActions() {
        super.makeActions();
        propertiesAction = new Action(Messages.getString("ExhibitionView.properties"), //$NON-NLS-1$
                Icons.exhibition.getDescriptor()) {
            @Override
            public void run() {
                ExhibitionImpl show = getExhibition();
                if (show == null)
                    return;
                ExhibitionImpl backup = ExhibitionPropertiesOperation.cloneExhibition(show);
                show = ExhibitionEditDialog.open(getSite().getShell(), null, show, show.getName(), true, null);
                if (show != null) {
                    performOperation(new ExhibitionPropertiesOperation(backup, exhibition = show));
                    setInput(exhibition);
                }
            }
        };
        propertiesAction.setToolTipText(Messages.getString("ExhibitionView.edit_exhibition_properties")); //$NON-NLS-1$
        addWallAction = new Action(Messages.getString("ExhibitionView.add_wall"), Icons.add_obj.getDescriptor()) { //$NON-NLS-1$
            @Override
            public void run() {
                ExhibitionImpl show = getExhibition();
                if (show == null)
                    return;
                int l = show.getWall().size();
                int y = 10;
                for (Wall w : show.getWall())
                    y += w.getHeight() + 15;
                EditWallDialog dialog = new EditWallDialog(getSite().getShell(), null,
                        NLS.bind(Messages.getString("ExhibitionView.wall_n"), l + 1)); //$NON-NLS-1$
                if (dialog.open() == Window.OK)
                    performOperation(new CreateWallOperation(dialog.getResult(), y));
            }
        };
        addWallAction.setToolTipText(Messages.getString("ExhibitionView.add_a_new_wall")); //$NON-NLS-1$
        deleteWallAction = new Action(Messages.getString("ExhibitionView.delete_wall_action"), //$NON-NLS-1$
                Icons.delete.getDescriptor()) {
            @Override
            public void run() {
                PNode node = getPickedNode();
                while (node != null && !(node instanceof PWall))
                    node = node.getParent();
                if (node != null && AcousticMessageDialog.openQuestion(getSite().getShell(),
                        Messages.getString("ExhibitionView.remove_wall"), //$NON-NLS-1$
                        Messages.getString("ExhibitionView.do_you_really_want_to_remove"))) //$NON-NLS-1$
                    performOperation(new DeleteWallOperation((PWall) node));
            }
        };
        deleteWallAction.setToolTipText(Messages.getString("ExhibitionView.delete_wall_tooltip")); //$NON-NLS-1$
        editWallAction = new Action(Messages.getString("ExhibitionView.edit_wall_action"), //$NON-NLS-1$
                Icons.properties_blue.getDescriptor()) {
            @Override
            public void run() {
                if (getPickedNode() instanceof PWall)
                    editWallLegend((PWall) getPickedNode());
            }
        };
        editWallAction.setToolTipText(Messages.getString("ExhibitionView.edit_wall_tooltip")); //$NON-NLS-1$
        generateAction = new Action(Messages.getString("ExhibitionView.generate"), Icons.play.getDescriptor()) { //$NON-NLS-1$
            @Override
            public void run() {
                generate();
            }
        };
        generateAction.setToolTipText(Messages.getString("ExhibitionView.generate_exhibition")); //$NON-NLS-1$

        gotoExhibitAction = new Action(Messages.getString("ExhibitionView.goto_exhibit")) { //$NON-NLS-1$
            @Override
            public void run() {
                List<ExhibitImpl> list = new ArrayList<ExhibitImpl>(300);
                for (PWall w : walls) {
                    ListIterator<?> it = w.getChildrenIterator();
                    while (it.hasNext()) {
                        Object next = it.next();
                        if (next instanceof PExhibit && ((PExhibit) next).exhibit != null)
                            list.add(((PExhibit) next).exhibit);
                    }
                }
                SelectExhibitDialog dialog = new SelectExhibitDialog(getSite().getShell(), list);
                dialog.create();
                org.eclipse.swt.graphics.Point pos = canvas.toDisplay((int) positionRelativeToCamera.getX(),
                        (int) positionRelativeToCamera.getY());
                pos.x += 10;
                pos.y += 10;
                dialog.getShell().setLocation(pos);
                if (dialog.open() == Window.OK) {
                    ExhibitImpl selectedExhibit = dialog.getResult();
                    for (PWall w : walls)
                        for (ListIterator<?> it = w.getChildrenIterator(); it.hasNext();) {
                            Object next = it.next();
                            if (next instanceof PExhibit) {
                                PExhibit pexhibit = (PExhibit) next;
                                if (pexhibit.exhibit == selectedExhibit) {
                                    revealItem(pexhibit);
                                    return;
                                }
                            }
                        }
                }
            }
        };
        gotoExhibitAction.setToolTipText(Messages.getString("ExhibitionView.goto_exhibit_tooltip")); //$NON-NLS-1$

        layoutAction = new Action(Messages.getString("ExhibitionView.layout")) { //$NON-NLS-1$
            @Override
            public void run() {
                PExhibit pExhibit = getPickedExhibit();
                if (pExhibit != null) {
                    ExhibitImpl exhibit = pExhibit.exhibit;
                    ExhibitImpl backup = cloneExhibit(exhibit);
                    ExhibitLayoutDialog dialog = new ExhibitLayoutDialog(getSite().getShell(), exhibition, exhibit);
                    if (dialog.open() == Dialog.OK)
                        performOperation(new LayoutOperation(Messages.getString("ExhibitionView.set_layout"), //$NON-NLS-1$
                                backup, exhibit));
                }
            }
        };
        layoutAction.setToolTipText(Messages.getString("ExhibitionView.layout_tooltip")); //$NON-NLS-1$
        markSoldAction = new Action() {
            @Override
            public void run() {
                PExhibit pExhibit = getPickedExhibit();
                if (pExhibit != null)
                    performOperation(new MarkSoldOperation(pExhibit));
            }
        };
    }

    private PExhibit getPickedExhibit() {
        Object item = getAdapter(IPresentationItem.class);
        if (item instanceof PExhibit)
            return (PExhibit) item;
        return null;
    }

    @Override
    public void updateActions(boolean force) {
        if (addWallAction != null && (isVisible() || force)) {
            super.updateActions(force);
            addWallAction.setEnabled(!dbIsReadonly());
        }
    }

    @Override
    protected void fillLocalToolBar(IToolBarManager manager) {
        super.fillLocalToolBar(manager);
        manager.add(addWallAction);
        manager.add(new Separator());
        manager.add(generateAction);
    }

    @Override
    protected void fillLocalPullDown(IMenuManager manager) {
        manager.add(addWallAction);
        manager.add(synchronizeAction);
        manager.add(new Separator());
        manager.add(generateAction);
        super.fillLocalPullDown(manager);
    }

    @Override
    protected void fillContextMenu(IMenuManager manager) {
        boolean writable = !dbIsReadonly();
        updateActions(true);
        PNode pickedNode = getPickedNode();
        if (pickedNode instanceof PExhibit) {
            addSoldAction(manager, (PExhibit) pickedNode);
            manager.add(layoutAction);
        } else if (pickedNode != null && pickedNode.getParent() instanceof PExhibit) {
            if (writable) {
                manager.add(cutAction);
                manager.add(new Separator());
            }
            addCommonContextActions(manager);
            manager.add(new Separator());
            addSoldAction(manager, (PExhibit) pickedNode.getParent());
            manager.add(layoutAction);
        } else {
            if (pickedNode != null && pickedNode.getParent() instanceof PWall)
                pickedNode = pickedNode.getParent();
            if (pickedNode instanceof PWall) {
                if (writable)
                    manager.add(pasteAction);
                manager.add(new Separator());
                manager.add(editWallAction);
                manager.add(deleteWallAction);
            }
        }
        manager.add(new Separator());
        manager.add(gotoExhibitAction);
        if (gotoLastSelectedAction.isEnabled())
            manager.add(gotoLastSelectedAction);
        super.fillContextMenu(manager);
    }

    private void addSoldAction(IMenuManager manager, PExhibit exhibit) {
        boolean sold = exhibit.exhibit.getSold();
        markSoldAction.setText(sold ? Messages.getString("ExhibitionView.remove_sold_mark") //$NON-NLS-1$
                : Messages.getString("ExhibitionView.mark_as_sold")); //$NON-NLS-1$
        markSoldAction.setToolTipText(sold ? Messages.getString("ExhibitionView.remove_sold_tooltip") //$NON-NLS-1$
                : Messages.getString("ExhibitionView.mark_sold_tooltip")); //$NON-NLS-1$
        manager.add(markSoldAction);
    }

    @Override
    protected void updatePresentation(Collection<? extends Asset> assets) {
        for (Asset asset : assets) {
            String assetId = asset.getStringId();
            lp: for (PNode wall : walls) {
                for (ListIterator<?> it = wall.getChildrenIterator(); it.hasNext();) {
                    Object next = it.next();
                    if (next instanceof PExhibit) {
                        String id = ((PExhibit) next).getAssetId();
                        if (id.equals(assetId)) {
                            ((PExhibit) next).update(asset);
                            break lp;
                        }
                    }
                }
            }
        }
    }

    protected PExhibit makeExhibit(PWall pwall, int pos, ExhibitImpl exhibit) {
        PExhibit pExhibit = new PExhibit(canvas, pwall, exhibit, pos, pwall.getWall().getHeight() - exhibit.getY());
        pwall.addChild(pExhibit);
        return pExhibit;
    }

    @Override
    protected void pastePresentationItems() {
        if (getExhibition() != null)
            performOperation(new PasteExhibitsOperation(positionX));
    }

    @Override
    protected void deletePresentationItem(IPresentationItem picked, boolean cut, boolean multiple) {
        if (getExhibition() != null)
            performOperation(new DeleteExhibitOperation(getSelectedExhibits((PExhibit) picked, multiple), cut));
    }

    protected PWall doRemoveExhibit(PExhibit moved) {
        PNode node = moved.getParent();
        if (node instanceof PWall) {
            PWall pwall = (PWall) node;
            pwall.removeChild(moved);
            ExhibitImpl exhibit = moved.exhibit;
            exhibits.remove(exhibit);
            Wall wall = pwall.getWall();
            wall.removeExhibit(exhibit.getStringId());
            return pwall;
        }
        return null;
    }

    protected void moveExhibit(PExhibit moved, PWall target, Point2D oldOffset, Point2D newOffset) {
        performOperation(new MoveExhibitOperation(moved, oldOffset, target, newOffset));
    }

    protected ExhibitionImpl getExhibition() {
        if (exhibition == null && !dbIsReadonly())
            exhibition = ExhibitionEditDialog.open(getSite().getShell(), null, null,
                    Messages.getString("ExhibitionView.create_exhibition"), false, null); //$NON-NLS-1$
        return exhibition;
    }

    @Override
    protected void setColor(Control control) {
        Color newPaint = UiUtilities.getAwtBackground(control, null);
        selectionBackgroundColor = SELECTIONBGCOLOR;
        CssActivator.getDefault().applyExtendedStyle(control, this);
        surface.setPaint(newPaint);
        for (PWall pwall : walls) {
            setColor(pwall, pwall.getWall());
            pwall.setStrokeColor(selectionBackgroundColor);
        }
    }

    @Override
    public void setSelectionBackgroundColor(org.eclipse.swt.graphics.Color color) {
        if (color != null) {
            RGB rgb = color.getRGB();
            selectionBackgroundColor = new Color(rgb.red, rgb.green, rgb.blue);
        } else
            selectionBackgroundColor = SELECTIONBGCOLOR;

    }

    protected void setColor(PSWTPath path, Wall wall) {
        Rgb_type bgColor = wall.getColor();
        if (bgColor == null)
            path.setPaint(WALLBGCOLOR);
        else
            path.setPaint(new Color(bgColor.getR(), bgColor.getG(), bgColor.getB()));
    }

    @Override
    public void setInput(IdentifiableObject presentation) {
        super.setInput(presentation);
        if (presentation instanceof ExhibitionImpl) {
            ExhibitionImpl exh = (ExhibitionImpl) presentation;
            setExhibition(exh);
            setupExhibition(getViewSite(), exh, 0);
            setPartName(exh.getName());
            int n = 0;
            for (Wall wall : exh.getWall())
                n += wall.getExhibit().size();
            beginTask(n);
            int y = TOPINDENT;
            for (Wall wall : exh.getWall()) {
                addWall(wall, y, false, progressBar);
                y += wall.getHeight() + V_WALLMARGINS;
            }
            setArtists();
            setColor(canvas);
            updateActions(false);
            updateStatusLine();
            endTask();
        } else
            setPartName(Messages.getString("ExhibitionView.exhibition")); //$NON-NLS-1$
    }

    protected void selectExhibit(PExhibit pExhibit, int modifiers, int clicks) {
        if ((modifiers & InputEvent.CTRL_MASK) != 0) {
            pExhibit.setSelected(!pExhibit.isSelected());
            if (pExhibit.isSelected())
                recentSelection = pExhibit;
            else
                updateSelectionMarkers();
        } else if ((modifiers & InputEvent.SHIFT_MASK) != 0) {
            PWall pwall = (PWall) pExhibit.getParent();
            List<PExhibit> exhibits = getSortedExhibits(pwall);
            int index = exhibits.indexOf(pExhibit);
            if (pExhibit.isSelected()) {
                int lastIndex = lastSelection == null ? -1 : exhibits.indexOf(lastSelection);
                if (lastIndex < 0) {
                    int lower = 0;
                    int upper = exhibits.size() - 1;
                    for (int i = index - 1; i >= 0; i--) {
                        if (!exhibits.get(i).isSelected())
                            break;
                        lower = i;
                    }
                    for (int i = index + 1; i < exhibits.size(); i++) {
                        if (!exhibits.get(i).isSelected())
                            break;
                        upper = i;
                    }
                    if (index - lower < upper - index)
                        lastIndex = lower;
                    else
                        lastIndex = upper;
                }
                if (lastIndex < index)
                    for (int i = index - 1; i >= lastIndex; i--)
                        exhibits.get(i).setSelected(false);
                else
                    for (int i = index + 1; i <= lastIndex; i++)
                        exhibits.get(i).setSelected(false);
            } else {
                recentSelection = pExhibit;
                for (int i = index - 1; i >= 0; i--)
                    if (exhibits.get(i).isSelected()) {
                        for (int j = i + 1; j <= index; j++)
                            exhibits.get(j).setSelected(true);
                        lastSelection = pExhibit;
                        return;
                    }
                for (int i = index + 1; i < exhibits.size(); i++)
                    if (exhibits.get(i).isSelected()) {
                        for (int j = i - 1; j >= index; j--)
                            exhibits.get(j).setSelected(true);
                    }
            }
            lastSelection = pExhibit;
        } else {
            clearSelection();
            recentSelection = pExhibit;
            if (clicks == 2) {
                PWall pwall = (PWall) pExhibit.getParent();
                for (ListIterator<?> iterator = pwall.getChildrenIterator(); iterator.hasNext();) {
                    Object next = iterator.next();
                    if (next instanceof PExhibit)
                        ((PExhibit) next).setSelected(true);
                }
            } else {
                pExhibit.setSelected(true);
            }
        }
    }

    private void clearSelection() {
        for (PWall pwall : walls)
            for (ListIterator<?> iterator2 = pwall.getChildrenIterator(); iterator2.hasNext();) {
                Object exhibit = iterator2.next();
                if (exhibit instanceof PExhibit)
                    ((PExhibit) exhibit).setSelected(false);
            }
        lastSelection = recentSelection = null;
    }

    public void updateSelectionMarkers() {
        for (PWall pwall : walls)
            for (ListIterator<?> iterator = pwall.getChildrenIterator(); iterator.hasNext();) {
                Object exhibit = iterator.next();
                if (exhibit instanceof PExhibit && ((PExhibit) exhibit).isSelected())
                    return;
            }
        lastSelection = recentSelection = null;
    }

    protected List<PExhibit> getSelectedExhibits(PExhibit picked, boolean multiple) {
        List<PExhibit> selected = new ArrayList<>();
        if (exhibition != null) {
            boolean hit = false;
            if (multiple)
                for (PWall pwall : walls)
                    for (PExhibit pExhibit : getSortedExhibits(pwall))
                        if (pExhibit.isSelected()) {
                            selected.add(pExhibit);
                            hit |= picked == pExhibit;
                        }
            if (!hit) {
                selected.clear();
                selected.add(picked);
                selectExhibit(picked, 0, 1);
            }
        }
        return selected;
    }

    protected List<PExhibit> getSortedExhibits(PWall pwall) {
        List<PExhibit> exhibits = new ArrayList<>(pwall.getChildrenCount());
        for (ListIterator<?> iterator2 = pwall.getChildrenIterator(); iterator2.hasNext();) {
            Object exhibit = iterator2.next();
            if (exhibit instanceof PExhibit)
                exhibits.add((PExhibit) exhibit);
        }
        Collections.sort(exhibits, new Comparator<PExhibit>() {
            @Override
            public int compare(PExhibit e1, PExhibit e2) {
                double x1 = e1.getBounds().getX();
                double x2 = e2.getBounds().getX();
                return x1 == x2 ? 0 : x1 > x2 ? 1 : -1;
            }
        });
        return exhibits;
    }

    private PWall addWall(Wall wall, double y, boolean reposition, ProgressIndicator bar) {
        PWall pwall = new PWall(wall);
        surface.addChild(pwall);
        walls.add(pwall);
        pwall.setOffset(0, y);
        int pos = 0;
        List<String> ids = new ArrayList<String>(wall.getExhibit().size());
        IDbManager dbManager = Core.getCore().getDbManager();
        for (String id : wall.getExhibit()) {
            ExhibitImpl exhibit = dbManager.obtainById(ExhibitImpl.class, id);
            if (exhibit != null) {
                ids.add(id);
                exhibits.add(exhibit);
                if (reposition)
                    pos += exhibit.getWidth() / 2;
                else
                    pos = exhibit.getX();
                makeExhibit(pwall, pos, exhibit);
                pos += exhibit.getWidth();
            }
        }
        wall.setExhibit(ids);
        if (bar != null)
            bar.worked(1);
        setupExhibition(getViewSite(), exhibition, pos);
        setPanAndZoomHandlers();
        return pwall;
    }

    private void setPanAndZoomHandlers() {
        setPanAndZoomHandlers(-11, -14);
    }

    private void setExhibition(ExhibitionImpl exhibition) {
        this.exhibition = exhibition;
        if (exhibition.getWall().isEmpty()) {
            Wall wall = new WallImpl();
            wall.setLocation(Messages.getString("ExhibitionView.wall_1")); //$NON-NLS-1$
            wall.setWidth(5000);
            wall.setHeight(2500);
            wall.setColor(new Rgb_typeImpl(255, 255, 250));
            exhibition.addWall(wall);
            List<Object> toBeStored = new ArrayList<Object>(2);
            toBeStored.add(wall);
            toBeStored.add(exhibition);
            storeSafelyAndUpdateIndex(null, toBeStored, null);
        }
    }

    private void setupExhibition(IViewSite site, ExhibitionImpl exhibition, int maxwidth) {
        if (exhibition != null) {
            int h = 10;
            int w = 0;
            for (Wall wall : exhibition.getWall()) {
                h += wall.getHeight();
                if (w > 0)
                    h += wall.getHeight() / 2;
                if (wall.getWidth() > w)
                    w = wall.getWidth();
            }
            surface.setBounds(-3000d, -3000d,
                    Math.max(Math.max(maxwidth, w) + 6000d, surface.getBoundsReference().width), h + 6000d);
            surface.setScale(
                    (double) site.getShell().getDisplay().getPrimaryMonitor().getBounds().height / (2 * h));
        }
    }

    @Override
    protected void cleanUp() {
        try {
            for (PWall wall : walls) {
                wall.removeAllChildren();
                PNode parent = wall.getParent();
                if (parent != null)
                    parent.removeChild(wall);
            }
        } catch (SWTException e) {
            // ignore
        }
        exhibits.clear();
        walls.clear();
        super.cleanUp();
    }

    @Override
    protected void dropAssets(ISelection selection, org.eclipse.swt.graphics.Point point, Point2D position,
            boolean replace) {
        performOperation(new DropAssetOperation((AssetSelection) selection, position, replace));
        getSite().getPage().activate(this);
    }

    private PWall findWall(Point2D position) {
        RECT1.x = (int) position.getX();
        RECT1.y = (int) position.getY();
        for (PWall w : walls)
            if (w.fullIntersects(RECT1))
                return w;
        return null;
    }

    private void editWallLegend(PWall pwall) {
        WallImpl w = (WallImpl) pwall.getWall();
        WallImpl backup = new WallImpl(w.getLocation(), w.getX(), w.getY(), w.getWidth(), w.getHeight(), w.getGX(),
                w.getGY(), w.getGAngle(), w.getColor());
        EditWallDialog dialog = new EditWallDialog(getSite().getShell(), w, w.getLocation());
        if (dialog.open() == Window.OK)
            performOperation(new EditWallOperation(w, backup, pwall));
    }

    @Override
    protected PNode[] getWorkArea() {
        return walls.toArray(new PNode[walls.size()]);
    }

    @Override
    protected int getPanDirection() {
        return GalleryPanEventHandler.BOTH;
    }

    @Override
    protected int getHoriztontalMargins() {
        return LEFTINDENT;
    }

    @Override
    public Object getContent() {
        return exhibition;
    }

    @Override
    protected AbstractHandleDragSequenceEventHandler createDragSequenceEventHandler(PNode node, boolean relative,
            boolean restricted, PNode presentationObject) {
        return new AbstractPresentationView.AbstractHandleDragSequenceEventHandler(node, relative, restricted,
                presentationObject) {

            @Override
            protected void moveToFront(PNode object) {
                updateActions(false);
            }

            @Override
            protected void drop(PInputEvent event, PNode pnode) {
                PExhibit pExhibit = (PExhibit) pnode.getParent();
                Point2D canvasPosition = event.getCanvasPosition();
                if (canvasPosition.getY() < 0) {
                    if (!relative)
                        deletePresentationItem(pExhibit, false, false);
                } else if (relative) {
                    Point2D newOffset = pnode.getOffset();
                    moveLabel(pnode, oldOffset, newOffset);
                } else {
                    Point2D newOffset = pExhibit.getOffset();
                    Point2D parentOffset = ((PWall) pExhibit.getParent()).getOffset();
                    Point2D point = new Point2D.Double(newOffset.getX() + parentOffset.getX(),
                            event.getPosition().getY());
                    surface.parentToLocal(point);
                    for (PWall pwall : walls) {
                        Point2D offset = pwall.getOffset();
                        double y = point.getY() - offset.getY();
                        PBounds bounds = pwall.getBoundsReference();
                        if (bounds.getY() <= y && bounds.getY() + bounds.getHeight() >= y) {
                            moveExhibit(pExhibit, pwall, oldOffset, newOffset);
                            return;
                        }
                    }
                    pExhibit.setOffset(oldOffset);
                }
            }

            @Override
            public Point getDragTolerance() {
                return DRAGTOLERANCE;
            }
        };

    }

    protected void moveLabel(PNode pnode, Point2D oldOffset, Point2D newOffset) {
        PExhibit pExhibit = (PExhibit) pnode.getParent();
        PNode pFrame = pExhibit.pOuterFrame;
        if (pFrame == null)
            pFrame = pExhibit.pMat;
        if (pFrame == null)
            pFrame = pExhibit.pImage;
        PBounds iBounds = pFrame.getGlobalBounds();
        double iMidX = iBounds.width / 2 + iBounds.x;
        double iMidY = iBounds.height / 2 + iBounds.y;
        double aspect = iBounds.width / iBounds.height;
        PBounds lBounds = pnode.getGlobalBounds();
        double lMidX = lBounds.width / 2 + lBounds.x;
        double lMidY = lBounds.height / 2 + lBounds.y;
        double degrees;
        if (lMidX == iMidX)
            degrees = lMidY > iMidY ? 0 : 180;
        else {
            double angle = Math.atan((lMidY - iMidY) * aspect / (lMidX - iMidX));
            degrees = 90 - Math.toDegrees(angle);
            if (lMidX < iMidX)
                degrees += 180;
        }
        int segment = (int) ((degrees + 15) / 30) % 12;
        double distance = 0;
        double indent = 0;
        int labPoint = 1;
        switch (segment) {
        case 0:
            labPoint = getClosestHorizontal(iBounds.width / 2 + iBounds.x, iBounds.height + iBounds.y, lBounds.y,
                    lBounds);
            distance = lBounds.y - (iBounds.height + iBounds.y);
            indent = computeIndent(labPoint, iBounds.width / 2 + iBounds.x, lBounds.x, lBounds.width);
            break;
        case 1:
            labPoint = getClosestHorizontal(iBounds.width + iBounds.x, iBounds.height + iBounds.y, lBounds.y,
                    lBounds);
            distance = lBounds.y - (iBounds.height + iBounds.y);
            indent = computeIndent(labPoint, iBounds.width + iBounds.x, lBounds.x, lBounds.width);
            break;
        case 2:
            labPoint = getClosestVertical(iBounds.width + iBounds.x, iBounds.height + iBounds.y, lBounds.x,
                    lBounds);
            distance = lBounds.x - (iBounds.width + iBounds.x);
            indent = -computeIndent(labPoint, iBounds.height + iBounds.y, lBounds.y, lBounds.height);
            break;
        case 3:
            labPoint = getClosestVertical(iBounds.width + iBounds.x, iBounds.height / 2 + iBounds.y, lBounds.x,
                    lBounds);
            distance = lBounds.x - (iBounds.width + iBounds.x);
            indent = -computeIndent(labPoint, iBounds.height / 2 + iBounds.y, lBounds.y, lBounds.height);
            break;
        case 4:
            labPoint = getClosestVertical(iBounds.width + iBounds.x, iBounds.y, lBounds.x, lBounds);
            distance = lBounds.x - (iBounds.width + iBounds.x);
            indent = -computeIndent(labPoint, iBounds.y, lBounds.y, lBounds.height);
            break;
        case 5:
            labPoint = getClosestHorizontal(iBounds.width + iBounds.x, iBounds.y, lBounds.y + lBounds.height,
                    lBounds);
            distance = iBounds.y - (lBounds.height + lBounds.y);
            indent = computeIndent(labPoint, iBounds.width + iBounds.x, lBounds.x, lBounds.width);
            break;
        case 6:
            labPoint = getClosestHorizontal(iBounds.width / 2 + iBounds.x, iBounds.y, lBounds.y + lBounds.height,
                    lBounds);
            distance = iBounds.y - (lBounds.height + lBounds.y);
            indent = computeIndent(labPoint, iBounds.width / 2 + iBounds.x, lBounds.x, lBounds.width);
            break;
        case 7:
            labPoint = getClosestHorizontal(iBounds.x, iBounds.y, lBounds.y + lBounds.height, lBounds);
            distance = iBounds.y - (lBounds.height + lBounds.y);
            indent = computeIndent(labPoint, iBounds.x, lBounds.x, lBounds.width);
            break;
        case 8:
            labPoint = getClosestVertical(iBounds.x, iBounds.y, lBounds.x + lBounds.width, lBounds);
            distance = iBounds.x - (lBounds.width + lBounds.x);
            indent = -computeIndent(labPoint, iBounds.y, lBounds.y, lBounds.height);
            break;
        case 9:
            labPoint = getClosestVertical(iBounds.x, iBounds.height / 2 + iBounds.y, lBounds.x + lBounds.width,
                    lBounds);
            distance = iBounds.x - (lBounds.width + lBounds.x);
            indent = -computeIndent(labPoint, iBounds.height / 2 + iBounds.y, lBounds.y, lBounds.height);
            break;
        case 10:
            labPoint = getClosestVertical(iBounds.x, iBounds.height + iBounds.y, lBounds.x + lBounds.width,
                    lBounds);
            distance = iBounds.x - (lBounds.width + lBounds.x);
            indent = -computeIndent(labPoint, iBounds.height + iBounds.y, lBounds.y, lBounds.height);
            break;
        case 11:
            labPoint = getClosestHorizontal(iBounds.x, iBounds.height + iBounds.y, lBounds.y, lBounds);
            distance = lBounds.y - (iBounds.height + iBounds.y);
            indent = computeIndent(labPoint, iBounds.x, lBounds.x, lBounds.width);
            break;
        }
        int alignment = (16 - segment) % 12 * 3 + labPoint;
        double globalScale = pFrame.getGlobalScale();
        ExhibitImpl exhibit = pExhibit.exhibit;
        ExhibitImpl backup = cloneExhibit(exhibit);
        exhibit.setLabelAlignment(alignment);
        exhibit.setLabelDistance((int) (distance / globalScale + 0.5d));
        exhibit.setLabelIndent((int) (indent / globalScale + 0.5d));
        performOperation(new LayoutOperation(Messages.getString("ExhibitionView.move_label"), backup, exhibit)); //$NON-NLS-1$
    }

    private static double computeIndent(int point, double ix, double lx, double lwidth) {
        switch (point) {
        case 0:
            return lx - ix;
        case 1:
            return lx + lwidth / 2 - ix;
        default:
            return lx + lwidth - ix;
        }
    }

    private static int getClosestVertical(double ix, double iy, double lx, PBounds lBounds) {
        double dx2 = (lx - ix) * (lx - ix);
        double d0 = (lBounds.y - iy) * (lBounds.y - iy) + dx2;
        double d1 = (lBounds.y + lBounds.height / 2 - iy) * (lBounds.y + lBounds.height / 2 - iy) + dx2;
        double d2 = (lBounds.y + lBounds.height - iy) * (lBounds.y + lBounds.height - iy) + dx2;
        double min = Math.min(d0, Math.min(d1, d2));
        return (d1 == min) ? 1 : (d0 == min) ? 0 : 2;
    }

    private static int getClosestHorizontal(double ix, double iy, double ly, PBounds lBounds) {
        double dy2 = (ly - iy) * (ly - iy);
        double d0 = (lBounds.x - ix) * (lBounds.x - ix) + dy2;
        double d1 = (lBounds.x + lBounds.width / 2 - ix) * (lBounds.x + lBounds.width / 2 - ix) + dy2;
        double d2 = (lBounds.x + lBounds.width - ix) * (lBounds.x + lBounds.width - ix) + dy2;
        double min = Math.min(d0, Math.min(d1, d2));
        return (d1 == min) ? 1 : (d0 == min) ? 0 : 2;
    }

    private static ExhibitImpl cloneExhibit(Exhibit exhibit) {
        return new ExhibitImpl(exhibit.getTitle(), exhibit.getDescription(), exhibit.getCredits(),
                exhibit.getDate(), exhibit.getX(), exhibit.getY(), exhibit.getWidth(), exhibit.getHeight(),
                exhibit.getMatWidth(), exhibit.getMatColor(), exhibit.getFrameWidth(), exhibit.getFrameColor(),
                exhibit.getSold(), exhibit.getHideLabel(), exhibit.getLabelAlignment(), exhibit.getLabelDistance(),
                exhibit.getLabelIndent(), exhibit.getAsset());
    }

    private void generate() {
        ExhibitionImpl show = getExhibition();
        if (show == null)
            return;
        if (offlineImages != null && !offlineImages.isEmpty()) {
            String[] volumes = offlineImages.toArray(new String[offlineImages.size()]);
            Arrays.sort(volumes);
            AcousticMessageDialog.openInformation(getSite().getShell(),
                    Messages.getString("ExhibitionView.images_offline"), //$NON-NLS-1$
                    volumes.length > 1 ? NLS.bind(Messages.getString("ExhibitionView.volumes_offline"), //$NON-NLS-1$
                            Core.toStringList(volumes, ", ")) //$NON-NLS-1$
                            : NLS.bind(Messages.getString("ExhibitionView.volume_offline"), //$NON-NLS-1$
                                    volumes[0]));
            return;
        }
        String s = exhibition.getStringId();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (Character.isLetterOrDigit(c))
                sb.append(c);
        }
        String exhibitionId = sb.toString();
        boolean isFtp = show.getIsFtp();
        String outputFolder = (isFtp) ? show.getFtpDir() : show.getOutputFolder();
        while (outputFolder == null || outputFolder.isEmpty()) {
            show = ExhibitionEditDialog.open(getSite().getShell(), null, show, show.getName(), false,
                    Messages.getString("ExhibitionView.please_specify_target")); //$NON-NLS-1$
            if (show == null)
                return;
            isFtp = show.getIsFtp();
            outputFolder = (isFtp) ? show.getFtpDir() : show.getOutputFolder();
        }
        boolean makeDefault = false;
        boolean askForDefault = false;
        final File file = new File(outputFolder);
        if (!isFtp && file.exists() && file.listFiles().length > 0) {
            AcousticMessageDialog dialog;
            File resFile = new File(file, "res"); //$NON-NLS-1$
            File[] rooms = resFile.listFiles(new FileFilter() {
                public boolean accept(File f) {
                    return f.getName().startsWith("room_"); //$NON-NLS-1$
                }
            });
            if (rooms.length > 0) {
                File roomFile = new File(resFile, "room_" //$NON-NLS-1$
                        + exhibitionId);
                if (roomFile.exists()) {
                    askForDefault = rooms.length > 1;
                    dialog = new AcousticMessageDialog(getSite().getShell(),
                            Messages.getString("ExhibitionView.overwriteTitle"), //$NON-NLS-1$
                            null, NLS.bind(Messages.getString("ExhibitionView.exhibition_exists"), file), //$NON-NLS-1$
                            MessageDialog.QUESTION, new String[] { Messages.getString("ExhibitionView.overwrite"), //$NON-NLS-1$
                                    Messages.getString("ExhibitionView.clear_folder"), //$NON-NLS-1$
                                    IDialogConstants.CANCEL_LABEL },
                            0);
                } else {
                    askForDefault = true;
                    dialog = new AcousticMessageDialog(getSite().getShell(),
                            Messages.getString("ExhibitionView.add"), //$NON-NLS-1$
                            null, NLS.bind(Messages.getString("ExhibitionView.other_exhibitions"), file), //$NON-NLS-1$
                            MessageDialog.QUESTION, new String[] { Messages.getString("ExhibitionView.add_room"), //$NON-NLS-1$
                                    Messages.getString("ExhibitionView.clear_folder"), //$NON-NLS-1$
                                    IDialogConstants.CANCEL_LABEL },
                            0);
                }
            } else
                dialog = new AcousticMessageDialog(getSite().getShell(),
                        Messages.getString("ExhibitionView.overwriteTitle"), //$NON-NLS-1$
                        null, NLS.bind(Messages.getString("ExhibitionView.not_empty"), file), //$NON-NLS-1$
                        MessageDialog.QUESTION, new String[] { Messages.getString("ExhibitionView.overwrite"), //$NON-NLS-1$
                                Messages.getString("ExhibitionView.clear_folder"), //$NON-NLS-1$
                                IDialogConstants.CANCEL_LABEL },
                        0);
            int ret = dialog.open();
            switch (ret) {
            case 2:
                return;
            case 1:
                Core.deleteFileOrFolder(file);
                break;
            }
        }
        if (askForDefault)
            makeDefault = AcousticMessageDialog.openQuestion(getSite().getShell(),
                    Messages.getString("ExhibitionView.exhibition_start"), //$NON-NLS-1$
                    Messages.getString("ExhibitionView.start_viewing")); //$NON-NLS-1$
        String page = show.getPageName();
        final boolean ftp = isFtp;
        final File start = new File(file, (page == null || page.isEmpty()) ? "index.html" //$NON-NLS-1$
                : page);
        final ExhibitionJob job = new ExhibitionJob(show, exhibitionId, ExhibitionView.this, makeDefault);
        final FtpAccount account;
        if (ftp) {
            account = FtpAccount.findAccount(outputFolder);
            if (account == null) {
                AcousticMessageDialog.openError(getSite().getShell(),
                        Messages.getString("ExhibitionView.account_not_existing"), //$NON-NLS-1$
                        NLS.bind(Messages.getString("ExhibitionView.account_not_defined"), //$NON-NLS-1$
                                outputFolder));
                return;
            }
        } else
            account = null;
        job.addJobChangeListener(new JobChangeAdapter() {

            @Override
            public void done(IJobChangeEvent event) {
                if (!job.wasAborted()) {
                    if (ftp)
                        new TransferJob(job.getTargetFolder().listFiles(), account, true).schedule(0);
                    else
                        showExhibition(start);
                }
            }

        });
        job.schedule();
    }

    private void showExhibition(final File start) {
        final Display display = getSite().getShell().getDisplay();
        display.asyncExec(() -> {
            if (!display.isDisposed())
                BusyIndicator.showWhile(display, () -> {
                    try {
                        URL url = start.toURI().toURL();
                        IWebBrowser browser = PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser();
                        if (browser != null)
                            browser.openURL(url);
                    } catch (MalformedURLException e1) {
                        // should not happen
                    } catch (PartInitException e2) {
                        // ignore
                    }
                });
        });
    }

    @Override
    protected PBounds getClientAreaReference() {
        PBounds bounds = new PBounds(0, 0, 0, 0);
        for (PWall pwall : walls) {
            PBounds b1 = pwall.getBoundsReference();
            if (b1.getX() < bounds.x)
                bounds.x = b1.getX();
            if (b1.getY() < bounds.y)
                bounds.y = b1.getY();
            if (b1.getWidth() + b1.getX() > bounds.width + bounds.x)
                bounds.width = b1.getWidth() + b1.getX() - bounds.x;
            if (b1.getHeight() + b1.getY() > bounds.height + bounds.y)
                bounds.height = b1.getHeight() + b1.getY() - bounds.y;
        }
        return bounds;
    }

    private void editLayout() {
        textEventHandler.commit();
        layoutAction.run();
    }

    private void setArtists() {
        ListIterator<?> it1 = surface.getChildrenIterator();
        while (it1.hasNext()) {
            Object obj = it1.next();
            if (obj instanceof PWall)
                ((PWall) obj).setArtists();
        }
    }

    public static boolean accepts(Asset asset) {
        if (asset == null)
            return false;
        IMediaSupport mediaSupport = CoreActivator.getDefault().getMediaSupport(asset.getFormat());
        return mediaSupport == null || mediaSupport.testProperty(IMediaSupport.EXHIBITION);
    }

    @Override
    protected void refreshAfterHistoryEvent(IUndoableOperation operation) {
        if (operation instanceof ExhibitionPropertiesOperation)
            setInput(exhibition);
    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getValue(String key, Object object) {
        if (object instanceof Exhibit) {
            if (Constants.HV_TOTAL.equals(key))
                return String.valueOf(exhibits.size());
            String exhibitId = ((Exhibit) object).getStringId();
            if (Constants.HV_WALL.equals(key)) {
                for (int i = 0; i < walls.size(); i++)
                    if (walls.get(i).getWall().getExhibit().contains(exhibitId))
                        return String.valueOf(i + 1);
            } else if (Constants.HV_WALLPOS.equals(key))
                for (int i = 0; i < walls.size(); i++) {
                    int index = walls.get(i).getWall().getExhibit().indexOf(exhibitId);
                    if (index >= 0)
                        return String.valueOf(index + 1);
                }
        }
        return null;
    }

    @Override
    protected String getStatusMessage() {
        Exhibition exh = getExhibition();
        if (exh != null)
            return NLS.bind(walls.size() == 1 ? Messages.getString("ExhibitionView.n_exh_one_wall") //$NON-NLS-1$
                    : Messages.getString("ExhibitionView.n_exh_m_walls"), //$NON-NLS-1$
                    exhibits.size(), walls.size());
        return null;
    }
}