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

Java tutorial

Introduction

Here is the source code for com.bdaum.zoom.ui.internal.views.SlideShowPlayer.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.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.HelpEvent;
import org.eclipse.swt.events.HelpListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolTip;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;

import com.bdaum.zoom.batch.internal.BatchActivator;
import com.bdaum.zoom.batch.internal.ExifTool;
import com.bdaum.zoom.batch.internal.IFileWatcher;
import com.bdaum.zoom.batch.internal.LoaderListener;
import com.bdaum.zoom.cat.model.asset.Asset;
import com.bdaum.zoom.cat.model.asset.AssetImpl;
import com.bdaum.zoom.cat.model.group.Group;
import com.bdaum.zoom.cat.model.group.SmartCollectionImpl;
import com.bdaum.zoom.cat.model.group.exhibition.ExhibitImpl;
import com.bdaum.zoom.cat.model.group.slideShow.Slide;
import com.bdaum.zoom.cat.model.group.slideShow.SlideImpl;
import com.bdaum.zoom.cat.model.group.slideShow.SlideShowImpl;
import com.bdaum.zoom.cat.model.group.webGallery.WebExhibitImpl;
import com.bdaum.zoom.core.Constants;
import com.bdaum.zoom.core.Core;
import com.bdaum.zoom.core.IVolumeManager;
import com.bdaum.zoom.core.QueryField;
import com.bdaum.zoom.core.Ticketbox;
import com.bdaum.zoom.core.db.IDbManager;
import com.bdaum.zoom.core.internal.CoreActivator;
import com.bdaum.zoom.css.CSSProperties;
import com.bdaum.zoom.css.internal.CssActivator;
import com.bdaum.zoom.image.IFocalLengthProvider;
import com.bdaum.zoom.image.ImageConstants;
import com.bdaum.zoom.image.ImageUtilities;
import com.bdaum.zoom.image.ZImage;
import com.bdaum.zoom.image.recipe.Recipe;
import com.bdaum.zoom.job.CustomJob;
import com.bdaum.zoom.job.OperationJob;
import com.bdaum.zoom.operations.CompoundOperation;
import com.bdaum.zoom.operations.internal.AddAlbumOperation;
import com.bdaum.zoom.operations.internal.DeleteOperation;
import com.bdaum.zoom.operations.internal.RateOperation;
import com.bdaum.zoom.operations.internal.RotateOperation;
import com.bdaum.zoom.operations.internal.VoiceNoteOperation;
import com.bdaum.zoom.program.IRawConverter;
import com.bdaum.zoom.ui.AssetSelection;
import com.bdaum.zoom.ui.Ui;
import com.bdaum.zoom.ui.dialogs.AcousticMessageDialog;
import com.bdaum.zoom.ui.internal.AbstractKiosk;
import com.bdaum.zoom.ui.internal.Icons;
import com.bdaum.zoom.ui.internal.UiActivator;
import com.bdaum.zoom.ui.internal.UiConstants;
import com.bdaum.zoom.ui.internal.dialogs.AlbumSelectionDialog;
import com.bdaum.zoom.ui.internal.dialogs.RatingDialog;
import com.bdaum.zoom.ui.internal.dialogs.SelectSlideDialog;
import com.bdaum.zoom.ui.internal.dialogs.SlideShowSaveDialog;
import com.bdaum.zoom.ui.internal.dialogs.VoiceNoteDialog;
import com.bdaum.zoom.ui.internal.hover.HoverInfo;
import com.bdaum.zoom.ui.internal.hover.HoveringController;
import com.bdaum.zoom.ui.internal.hover.IGalleryHover;
import com.bdaum.zoom.ui.internal.hover.IHoverInfo;
import com.bdaum.zoom.ui.internal.widgets.FadingShell;
import com.bdaum.zoom.ui.preferences.PreferenceConstants;

@SuppressWarnings("restriction")
public class SlideShowPlayer extends AbstractKiosk implements MouseListener, KeyListener {

    static final int TICK = 17;
    static final int LONGTICK = 60;
    static final int NTICKS = (LONGTICK + TICK / 2) / TICK;
    public static final long STARTUPDELAY = 3000L;

    public class PreparationJob extends Job {

        private final int position;
        private final Rectangle bounds;

        public PreparationJob(int position, Rectangle bounds) {
            super(Messages.getString("SlideShowPlayer.slideshow_preparation")); //$NON-NLS-1$
            this.position = position;
            this.bounds = bounds;
            setPriority(Job.INTERACTIVE);
        }

        @Override
        public boolean belongsTo(Object family) {
            return Constants.SLIDESHOW == family;
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            IDbManager dbManager = Core.getCore().getDbManager();
            if (slides == null)
                slides = new ArrayList<SlideImpl>(dbManager.obtainByIds(SlideImpl.class, slideshow.getEntry()));
            int plannedDuration = 0;
            SlideRequest lastSectionRequest = null;
            IVolumeManager volumeManager = Core.getCore().getVolumeManager();
            SlideRequest previousRequest = null;
            for (SlideImpl slide : slides) {
                if (monitor.isCanceled())
                    return Status.CANCEL_STATUS;
                String assetId = slide.getAsset();
                if (assetId == null) {
                    ++total;
                    int d = slide.getDelay() + slide.getDuration();
                    duration += d;
                    plannedDuration += d;
                    if (plannedDuration > position && firstSlide < 0) {
                        firstSlide = requestList.size();
                        positionFirstSlide = position + d - plannedDuration;
                    }
                    SlideRequest request = new SlideRequest(requestList.size(), slide, null, 0, -1, 0);
                    lastSectionRequest = request;
                    requestList.add(request);
                    if (previousRequest != null)
                        previousRequest.setOverlap(Math.min(0, slide.getFadeIn() - slide.getDelay()));
                    previousRequest = request;
                } else {
                    AssetImpl asset = dbManager.obtainAsset(assetId);
                    if (asset != null) {
                        ++total;
                        URI uri = volumeManager.findExistingFile(asset, false);
                        int d = slide.getDelay() + slide.getDuration();
                        plannedDuration += d;
                        if (uri != null) {
                            duration += d;
                            if (plannedDuration > position && firstSlide < 0) {
                                firstSlide = requestList.size();
                                positionFirstSlide = position + d - plannedDuration;
                            }
                            SlideRequest request = new SlideRequest(requestList.size(), slide, uri,
                                    asset.getRotation(), asset.getRating(),
                                    slideshow.getFromPreview() ? asset.getPreviewSize() : 0);
                            request.addAsset(asset);
                            requestList.add(request);
                            if (lastSectionRequest != null)
                                lastSectionRequest.addAsset(asset);
                            if (previousRequest != null)
                                previousRequest.setOverlap(Math.min(0, slide.getFadeIn() - slide.getDelay()));
                            previousRequest = request;
                        } else
                            ++offline;
                    }
                }
            }
            if (monitor.isCanceled())
                return Status.CANCEL_STATUS;
            if (firstSlide >= 0) {
                new ImageProviderJob(bottomShell, bounds, advanced, cms).schedule();
                requests.add(requestList.get(firstSlide));
            }
            return Status.OK_STATUS;
        }

    }

    public class ApplyChangesJob extends CustomJob {

        private List<SlideRequest> reqList;

        public ApplyChangesJob(List<SlideRequest> requestList) {
            super(Messages.getString("SlideShowPlayer.applying_changes")); //$NON-NLS-1$
            this.reqList = requestList;
            setPriority(Job.LONG);
        }

        @Override
        public boolean belongsTo(Object family) {
            return Constants.OPERATIONJOBFAMILY == family || Constants.CRITICAL == family;
        }

        @Override
        protected IStatus runJob(IProgressMonitor monitor) {
            List<Asset> deleteFromCat = new ArrayList<Asset>();
            List<Asset> deleteFromDisk = new ArrayList<Asset>();
            final List<SlideImpl> removeFromShow = new ArrayList<SlideImpl>();
            Map<Integer, List<Asset>> rated = new HashMap<Integer, List<Asset>>();
            Map<Integer, List<Asset>> rotated = new HashMap<Integer, List<Asset>>();
            Map<SmartCollectionImpl, List<Asset>> addedToAlbum = new HashMap<SmartCollectionImpl, List<Asset>>();
            Map<Asset, VoiceNoteOperation> voice = new HashMap<Asset, VoiceNoteOperation>();
            final IDbManager dbManager = Core.getCore().getDbManager();
            final AssetSelection assetSelection = new AssetSelection(reqList.size());
            for (SlideRequest request : reqList) {
                Asset asset = request.getAsset();
                if (asset != null) {
                    if (request.isSelected())
                        assetSelection.add(asset);
                    if (request.isDeleted()) {
                        if (request.getDeleteFromDisk())
                            deleteFromDisk.add(asset);
                        else
                            deleteFromCat.add(asset);
                        for (List<Asset> ratedList : rated.values())
                            ratedList.remove(asset);
                        for (List<Asset> rotatedList : rotated.values())
                            rotatedList.remove(asset);
                        for (List<Asset> albumList : addedToAlbum.values())
                            albumList.remove(asset);
                        voice.remove(asset);
                    } else {
                        int rating = request.getRating();
                        if (rating != RatingDialog.ABORT && rating != asset.getRating()) {
                            deleteFromDisk.remove(asset);
                            deleteFromCat.remove(asset);
                            for (List<Asset> ratedList : rated.values())
                                ratedList.remove(asset);
                            List<Asset> ratedList = rated.get(rating);
                            if (ratedList == null) {
                                ratedList = new ArrayList<Asset>();
                                rated.put(rating, ratedList);
                            }
                            ratedList.add(asset);
                        }
                        int rot = (request.getRotation() - asset.getRotation()) % 360;
                        if (rot != 0) {
                            deleteFromDisk.remove(asset);
                            deleteFromCat.remove(asset);
                            for (List<Asset> rotatedList : rotated.values())
                                rotatedList.remove(asset);
                            List<Asset> rotatedList = rotated.get(rot);
                            if (rotatedList == null) {
                                rotatedList = new ArrayList<Asset>();
                                rotated.put(rot, rotatedList);
                            }
                            rotatedList.add(asset);
                        }
                        SmartCollectionImpl[] albums = request.getAlbums();
                        if (albums != null && albums.length > 0) {
                            deleteFromDisk.remove(asset);
                            deleteFromCat.remove(asset);
                            for (List<Asset> albumList : addedToAlbum.values())
                                albumList.remove(asset);
                            for (SmartCollectionImpl album : albums) {
                                List<Asset> albumList = addedToAlbum.get(album);
                                if (albumList == null)
                                    addedToAlbum.put(album, albumList = new ArrayList<Asset>());
                                albumList.add(asset);
                            }
                        }
                        if (request.annotationChanged())
                            voice.put(asset, new VoiceNoteOperation(asset, request.getSourceURI(),
                                    request.getTargetURI(), request.getNoteText(), request.getSvg()));
                    }
                    if (request.isRemoveFromShow())
                        removeFromShow.add(request.getSlide());
                }
            }
            if (!assetSelection.isEmpty()) {
                final Shell shell = parentWindow.getShell();
                if (!shell.isDisposed())
                    shell.getDisplay().asyncExec(() -> {
                        if (!shell.isDisposed())
                            Ui.getUi().getNavigationHistory(parentWindow).postSelection(assetSelection);
                    });
            }
            if (!removeFromShow.isEmpty() && !slideshow.getAdhoc()) {
                SlideshowView editor = null;
                IWorkbenchWindow ww = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
                if (ww != null) {
                    IWorkbenchPage page = ww.getActivePage();
                    if (page != null)
                        editor = (SlideshowView) page.findView(SlideshowView.ID);
                }
                if (editor != null && editor.getContent() != slideshow)
                    editor = null;
                if (editor == null) {
                    dbManager.safeTransaction(() -> {
                        for (SlideImpl slideImpl : removeFromShow) {
                            dbManager.delete(slideImpl);
                            slideshow.removeEntry(slideImpl.getStringId());
                        }
                        dbManager.store(slideshow);
                    });
                } else {
                    final SlideshowView view = editor;
                    if (!display.isDisposed())
                        display.asyncExec(() -> {
                            if (!display.isDisposed())
                                view.removeSlides(removeFromShow);
                        });
                }
                Core.getCore().fireAssetsModified(null, null);
            }
            CompoundOperation compoundOperation = new CompoundOperation(
                    Messages.getString("SlideShowPlayer.modifications_during_slideshow")); //$NON-NLS-1$
            if (!deleteFromCat.isEmpty())
                compoundOperation.addOperation(new DeleteOperation(deleteFromCat, false, null, null, null,
                        UiActivator.getDefault().createImportConfiguration(SlideShowPlayer.this)));
            if (!deleteFromDisk.isEmpty())
                compoundOperation.addOperation(new DeleteOperation(deleteFromDisk, true, null, null, null,
                        UiActivator.getDefault().createImportConfiguration(SlideShowPlayer.this)));
            for (Map.Entry<Integer, List<Asset>> entry : rated.entrySet())
                compoundOperation.addOperation(new RateOperation(entry.getValue(), entry.getKey()));
            for (Map.Entry<Integer, List<Asset>> entry : rotated.entrySet())
                compoundOperation.addOperation(new RotateOperation(entry.getValue(), entry.getKey()));
            for (Map.Entry<SmartCollectionImpl, List<Asset>> entry : addedToAlbum.entrySet())
                compoundOperation.addOperation(new AddAlbumOperation(entry.getKey(), entry.getValue(), null));
            for (VoiceNoteOperation op : voice.values())
                compoundOperation.addOperation(op);
            if (!compoundOperation.isEmpty())
                OperationJob.executeOperation(compoundOperation, SlideShowPlayer.this);
            return Status.OK_STATUS;
        }
    }

    public class ImageProviderJob extends Job implements LoaderListener {

        private MultiStatus status;
        private Shell shell;
        private boolean adv;
        private Rectangle bounds;
        private SlideRequest request;
        private int cms1;

        public ImageProviderJob(Shell shell, Rectangle bounds, boolean advanced, int cms) {
            super(Messages.getString("SlideShowPlayer.image_provider")); //$NON-NLS-1$
            this.shell = shell;
            this.bounds = bounds;
            this.adv = advanced;
            this.cms1 = cms;
            setPriority(Job.LONG);
            setSystem(true);
        }

        @Override
        public boolean belongsTo(Object family) {
            return Constants.SLIDESHOW == family;
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            String opId = java.util.UUID.randomUUID().toString();
            CoreActivator activator = CoreActivator.getDefault();
            IFileWatcher fileWatcher = activator.getFileWatchManager();
            status = new MultiStatus(UiActivator.PLUGIN_ID, 0,
                    Messages.getString("SlideShowPlayer.image_loading_report"), null); //$NON-NLS-1$
            try (Ticketbox box = new Ticketbox()) {
                while (!monitor.isCanceled()) {
                    try {
                        request = requests.take();
                        if (shell.isDisposed() || request == FINALREQUEST || monitor.isCanceled())
                            break;
                        int zoom = request.getSlide().getZoom();
                        float zp = zoom == 0 ? 1f : (1f + zoom / 100f) * 0.631f;
                        int preferredWidth = (int) (bounds.width * zp);
                        int preferredHeight = (int) (bounds.height * zp);
                        ZImage image = null;
                        URI uri = request.getUri();
                        if (uri == null)
                            image = createSectionBreak(shell, request);
                        else {
                            File file = null;
                            File tempFile = null;
                            if (Constants.FILESCHEME.equals(uri.getScheme())) {
                                file = new File(uri);
                                if (slideshow.getVoiceNotes()) {
                                    URI voiceURI = Core.getCore().getVolumeManager()
                                            .findVoiceFile(request.getAsset());
                                    if (voiceURI != null)
                                        try {
                                            URL url = voiceURI.toURL();
                                            request.setVoiceUrl(url);
                                            request.setVoiceLength((int) ((UiActivator.getDefault()
                                                    .getSoundfileLengthInMicroseconds(url) + 999L) / 1000L));
                                        } catch (MalformedURLException e) {
                                            // should not happen
                                        }
                                }
                            } else
                                try {
                                    shell.getDisplay().syncExec(() -> {
                                        if (!text.getText().isEmpty()) {
                                            text.setText(text.getText()
                                                    + Messages.getString("SlideShowPlayer.downloading")); //$NON-NLS-1$
                                            bottomCanvas.redraw();
                                        }
                                    });
                                    tempFile = file = box.download(uri);
                                } catch (IOException e) {
                                    status.add(new Status(IStatus.ERROR, UiActivator.PLUGIN_ID,
                                            NLS.bind(Messages.getString("SlideShowPlayer.download_failed"), //$NON-NLS-1$
                                                    uri),
                                            e));
                                }
                            if (file != null) {
                                Recipe recipe = null;
                                boolean isRawOrDng = ImageConstants.isRaw(file.getName(), true);
                                if (isRawOrDng) {
                                    IRawConverter currentRawConverter = BatchActivator.getDefault()
                                            .getCurrentRawConverter(false);
                                    if (currentRawConverter != null)
                                        recipe = currentRawConverter.getRecipe(file.toURI().toString(), false,
                                                new IFocalLengthProvider() {
                                                    public double get35mm() {
                                                        return request.getAsset().getFocalLengthIn35MmFilm();
                                                    }
                                                }, null, null);
                                }
                                if (monitor.isCanceled())
                                    break;
                                if (recipe == null || recipe == Recipe.NULL) {
                                    int previewSize = request.getPreviewSize();
                                    if (previewSize < 0 || previewSize >= Math.max(bounds.width, bounds.height)) {
                                        try {
                                            image = getExifTool(file).getPreviewImage(false);
                                            if (image != null) {
                                                if (shell.isDisposed())
                                                    break;
                                                ExifTool.fixOrientation(image, 0, request.getRotation());
                                                Rectangle nbounds = image.getBounds();
                                                image.setScaling(nbounds.width, nbounds.height, true, 0, null);
                                                image.develop(monitor, display, ZImage.UNCROPPED, -1, -1,
                                                        ZImage.SWTIMAGE);
                                            }
                                        } catch (Exception e) {
                                            // ignore and use full size image
                                        }
                                        if (monitor.isCanceled())
                                            break;
                                    }
                                }
                                if (image == null) {
                                    if (monitor.isCanceled())
                                        break;
                                    if (recipe != null && recipe != Recipe.NULL)
                                        shell.getDisplay().syncExec(() -> {
                                            if (!text.getText().isEmpty()) {
                                                text.setText(text.getText()
                                                        + Messages.getString("SlideShowPlayer.developing")); //$NON-NLS-1$
                                                bottomCanvas.redraw();
                                            }
                                        });
                                    try {
                                        image = CoreActivator.getDefault().getHighresImageLoader().loadImage(null,
                                                status, file, request.getRotation(),
                                                request.getAsset().getFocalLengthIn35MmFilm(),
                                                new Rectangle(0, 0, preferredWidth, preferredHeight), 1d, 1d, adv,
                                                cms1, null, null, recipe, fileWatcher, opId, this);
                                    } catch (Exception e) {
                                        // ignore
                                    }
                                    if (monitor.isCanceled())
                                        break;
                                }
                                if (tempFile != null)
                                    tempFile.delete();
                            }
                        }
                        if (image != null)
                            image.develop(monitor, display, ZImage.CROPPED, preferredWidth, preferredHeight,
                                    ZImage.SWTIMAGE);
                        else
                            image = createIntermediatePlate(shell, request,
                                    Messages.getString("SlideShowPlayer.image_unavail"), //$NON-NLS-1$
                                    Messages.getString("SlideShowPlayer.highres_image_not_loaded"), //$NON-NLS-1$
                                    Constants.SLIDE_THUMBNAILS_BOTTOM);
                        request.setImage(image);
                        if (monitor.isCanceled())
                            break;
                        answers.add(request);
                        request = null;
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            } finally {
                fileWatcher.stopIgnoring(opId);
            }
            return status;
        }

        private ExifTool getExifTool(File file) {
            if (exifTool == null)
                exifTool = new ExifTool(file, false);
            else
                exifTool.reset(file);
            return exifTool;
        }

        public boolean progress(int tot, int worked) {
            return (request != null) ? request.isAborted() : false;
        }

        private ZImage createSectionBreak(final Shell shl, SlideRequest req) {
            final SlideImpl slide = req.getSlide();
            return createIntermediatePlate(shl, req, slide.getCaption() != null ? slide.getCaption() : "", //$NON-NLS-1$
                    slide.getDescription() != null ? slide.getDescription() : "", slide.getLayout()); //$NON-NLS-1$
        }

        public ZImage createIntermediatePlate(final Shell shl, SlideRequest req, final String caption,
                final String description, final int slayout) {
            final int w = bounds.width;
            final int h = bounds.height;
            final Display displ = shl.getDisplay();
            Image image = new Image(displ, w, h);
            final GC gc = new GC(image);
            displ.syncExec(() -> {
                gc.setBackground(shl.getBackground());
                gc.fillRectangle(0, 0, w, h);
                gc.setLineWidth(3);
                gc.setForeground(shl.getForeground());
                gc.drawRectangle(0, 0, w - 1, h - 1);
                TextLayout layout = new TextLayout(displ);
                StringBuilder sb = new StringBuilder();
                sb.append(caption);
                int boldLength = sb.length();
                if (!description.isEmpty()) {
                    if (sb.length() > 0)
                        sb.append("\n\n"); //$NON-NLS-1$
                    sb.append(description);
                }
                int tw, th, tx, ty;
                switch (slayout) {
                case Constants.SLIDE_THUMBNAILS_LEFT:
                    tw = (int) (w * (0.5d - MARGINWIDTH));
                    th = (int) (h * (1d - 2 * MARGINHEIGHT));
                    tx = w / 2;
                    ty = (int) (h * MARGINHEIGHT);
                    break;
                case Constants.SLIDE_THUMBNAILS_RIGHT:
                    tw = (int) (w * (0.5d - MARGINWIDTH));
                    th = (int) (h * (1d - 2 * MARGINHEIGHT));
                    tx = (int) (w * MARGINWIDTH);
                    ty = (int) (h * MARGINHEIGHT);
                    break;
                case Constants.SLIDE_THUMBNAILS_TOP:
                    tw = (int) (w * (1d - 2 * MARGINWIDTH));
                    th = (int) (h * (0.5d - MARGINHEIGHT));
                    tx = (int) (w * MARGINWIDTH);
                    ty = h / 2;
                    break;
                case Constants.SLIDE_THUMBNAILS_BOTTOM:
                    tw = (int) (w * (1d - 2 * MARGINWIDTH));
                    th = (int) (h * (0.5d - MARGINHEIGHT));
                    tx = (int) (w * MARGINWIDTH);
                    ty = (int) (h * MARGINHEIGHT);
                    break;
                default:
                    tw = (int) (w * (1d - 2 * MARGINWIDTH));
                    th = (int) (h * (1d - 2 * MARGINHEIGHT));
                    tx = (int) (w * MARGINWIDTH);
                    ty = (int) (h * MARGINHEIGHT);
                    break;
                }
                layout.setText(sb.toString());
                layout.setFont(JFaceResources.getFont(UiConstants.VIEWERTITLEFONT));
                layout.setStyle(new TextStyle(JFaceResources.getFont(UiConstants.VIEWERBANNERFONT), null, null), 0,
                        boldLength);
                layout.setWidth(tw);
                Rectangle b = layout.getBounds();
                float f = Math.max(0.1f, (float) Math.sqrt((float) b.height / th));
                layout.setWidth((int) (tw * f));
                b = layout.getBounds();
                f = 1f / Math.max((float) b.height / th, (float) b.width / tw);
                Transform t = new Transform(displ);
                t.translate(tx, ty);
                t.scale(f, f);
                gc.setTransform(t);
                layout.draw(gc, 0, 0);
                layout.dispose();
                gc.setTransform(null);
                t.dispose();
            });
            if (req != null) {
                AssetImpl[] assets = req.getNextAssets();
                int l = assets.length;
                if (l > 0) {
                    switch (slayout) {
                    case Constants.SLIDE_THUMBNAILS_LEFT:
                        int cols = Math.min(3, (int) Math.sqrt(l));
                        int rows = (l + cols - 1) / cols;
                        int maxw = (int) ((w * (0.5d - MARGINWIDTH)) / cols);
                        int maxh = (int) ((h * (1d - 2 * MARGINHEIGHT)) / rows);
                        int xoff = (int) (w * MARGINWIDTH);
                        int yoff = (int) (h * MARGINHEIGHT);
                        for (int i = 0; i < assets.length; i++)
                            drawThumbnail(gc, assets[i], maxw, maxh, (i % cols) * maxw + xoff,
                                    (i / cols) * maxh + yoff);
                        break;
                    case Constants.SLIDE_THUMBNAILS_RIGHT:
                        cols = Math.min(3, (int) Math.sqrt(l));
                        rows = (l + cols - 1) / cols;
                        maxw = (int) ((w * (0.5d - MARGINWIDTH)) / cols);
                        maxh = (int) ((h * (1d - 2 * MARGINHEIGHT)) / rows);
                        xoff = w / 2;
                        yoff = (int) (h * MARGINHEIGHT);
                        for (int i = 0; i < assets.length; i++)
                            drawThumbnail(gc, assets[i], maxw, maxh, (i % cols) * maxw + xoff,
                                    (i / cols) * maxh + yoff);
                        break;
                    case Constants.SLIDE_THUMBNAILS_TOP:
                        rows = Math.min(2, (int) Math.sqrt(l));
                        cols = (l + rows - 1) / rows;
                        maxw = (int) ((w * (1d - 2 * MARGINWIDTH)) / cols);
                        maxh = (int) ((h * (0.5d - MARGINHEIGHT)) / rows);
                        xoff = (int) (w * MARGINWIDTH);
                        yoff = (int) (h * MARGINHEIGHT);
                        for (int i = 0; i < assets.length; i++)
                            drawThumbnail(gc, assets[i], maxw, maxh, (i % cols) * maxw + xoff,
                                    (i / cols) * maxh + yoff);
                        break;
                    case Constants.SLIDE_THUMBNAILS_BOTTOM:
                        rows = Math.min(2, (int) Math.sqrt(l));
                        cols = (l + rows - 1) / rows;
                        maxw = (int) ((w * (1d - 2 * MARGINWIDTH)) / cols);
                        maxh = (int) ((h * (0.5d - MARGINHEIGHT)) / rows);
                        xoff = (int) (w * MARGINWIDTH);
                        yoff = h / 2;
                        for (int i = 0; i < assets.length; i++)
                            drawThumbnail(gc, assets[i], maxw, maxh, (i % cols) * maxw + xoff,
                                    (i / cols) * maxh + yoff);
                        break;
                    }
                }
            }
            gc.dispose();
            return new ZImage(image, null);
        }

        private void drawThumbnail(final GC gc, final AssetImpl asset, final int maxw, final int maxh, final int x,
                final int y) {
            display.syncExec(() -> {
                Image thImage = ImageUtilities.loadThumbnail(display, asset.getJpegThumbnail(),
                        Ui.getUi().getDisplayCMS(), SWT.IMAGE_JPEG, true);
                Rectangle thBounds = thImage.getBounds();
                double f = Math.min(maxw * (1d - THUMBNAILSPACE) / thBounds.width,
                        maxh * (1d - THUMBNAILSPACE) / thBounds.height);
                gc.drawImage(thImage, 0, 0, thBounds.width, thBounds.height, x, y, (int) (thBounds.width * f),
                        (int) (thBounds.height * f));
                thImage.dispose();
            });
        }
    }

    public interface FadingListener {
        void fadeinStarted();

        void fadeinEnded();

        void fadeoutStarted();

        void fadeoutEnded();
    }

    public SmartCollectionImpl[] selectedAlbums;

    public class SingleSlideJob extends Job implements HelpListener {

        public class SlideControl implements IHoverSubject {

            public class ControlHover implements IGalleryHover {

                public IHoverInfo getHoverInfo(IHoverSubject viewer, MouseEvent event) {
                    Object ob = viewer.findObject(event);
                    if (ob instanceof Integer) {
                        String msg = null;
                        switch ((Integer) ob) {
                        case C_BACK:
                            msg = Messages.getString("SlideShowPlayer.one_step_back"); //$NON-NLS-1$
                            break;
                        case C_PLAY:
                            msg = Messages.getString("SlideShowPlayer.continue"); //$NON-NLS-1$
                            break;
                        case C_STOP:
                            msg = Messages.getString("SlideShowPlayer.stop"); //$NON-NLS-1$
                            break;
                        case C_FORWARD:
                            msg = Messages.getString("SlideShowPlayer.one_step_forward"); //$NON-NLS-1$
                            break;
                        case C_JUMP:
                            msg = Messages.getString("SlideShowPlayer.jump_to_slide"); //$NON-NLS-1$
                            break;
                        case C_ROTCW:
                            msg = Messages.getString("SlideShowPlayer.rotate_cw"); //$NON-NLS-1$
                            break;
                        case C_ROTCCW:
                            msg = Messages.getString("SlideShowPlayer.rotate_ccw"); //$NON-NLS-1$
                            break;
                        case C_DELETE:
                            msg = Messages.getString("SlideShowPlayer.delete"); //$NON-NLS-1$
                            break;
                        case C_RATE:
                            msg = Messages.getString("SlideShowPlayer.rate_remove"); //$NON-NLS-1$
                            break;
                        case C_ALBUM:
                            msg = Messages.getString("SlideShowPlayer.add_to_album"); //$NON-NLS-1$
                            break;
                        case C_SELECTION:
                            msg = request.isSelected() ? Messages.getString("SlideShowPlayer.remove_selection") //$NON-NLS-1$
                                    : Messages.getString("SlideShowPlayer.add_selection"); //$NON-NLS-1$
                            break;
                        case C_VOICE:
                            msg = Messages.getString("SlideShowPlayer.attach_remove_voice_note"); //$NON-NLS-1$
                            break;
                        }
                        if (msg != null)
                            return new HoverInfo(msg);
                    }
                    return null;
                }
            }

            private FadingShell controlShell;
            private HoveringController controller;
            private double factor;
            private Image scaledPanel;

            public SlideControl(FadingShell parentShell) {
                Image controlPanel = (request.getImage() == null ? Icons.slideControl_dark
                        : canBeSelected(request) ? Icons.slideControl : Icons.slideControl_nonselect).getImage();
                Rectangle pBounds = parentShell.getBounds();
                final Rectangle iBounds = controlPanel.getBounds();
                factor = (double) pBounds.width / iBounds.width * 6 / 14;
                final int w = (int) (iBounds.width * factor);
                buttonWidth = w / 12;
                final int h = (int) (iBounds.height * factor);
                GC gc = new GC(scaledPanel = new Image(displ, w, h));
                gc.drawImage(controlPanel, 0, 0, iBounds.width, iBounds.height, 0, 0, w, h);
                gc.dispose();
                final Shell shell = new Shell(parentShell.getShell(), SWT.TOOL);
                controlShell = new FadingShell(shell, true, Constants.SLIDE_TRANSITION_FADE);
                shell.setText(slide.getCaption());
                shell.setBackgroundImage(scaledPanel);
                shell.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseDown(MouseEvent e) {
                        if (controlShell != null)
                            controlShell.setAlpha(225);
                        int button = ((int) (e.x / factor + 0.5d)) / 80;
                        int seqno = request.getSeqno();
                        switch (button) {
                        case C_BACK:
                            if (seqno > 0)
                                startButtonTimer(BACK, slideControl.getLocation());
                            break;
                        case C_FORWARD:
                            if (seqno < requestList.size() - 1) {
                                Point p = slideControl.getLocation();
                                p.x += 3 * buttonWidth;
                                startButtonTimer(FORWARD, p);
                            }
                            break;
                        case C_JUMP:
                            Point p = slideControl.getLocation();
                            p.x += button * buttonWidth;
                            gotoSlide(p, request, RESTART);
                            break;
                        }
                    }

                    @Override
                    public void mouseUp(MouseEvent e) {
                        stopButtonTimer();
                        if (selectSlideDialog == null) {
                            int button = ((int) (e.x / factor - 1.5d)) / 80;
                            switch (button) {
                            case C_BACK:
                                request.setOperation((request.getSeqno() > 0) ? BACK : NOOP);
                                break;
                            case C_PLAY:
                                request.setPaused(false);
                                request.setOperation(CONTINUE);
                                closeControl();
                                break;
                            case C_STOP:
                                int cret = showClosePrompt(bottomShell);
                                if (cret != CLOSE_CANCEL) {
                                    setPaused(false);
                                    request.setOperation(cret == CLOSE_CLOSE ? ABORT : IGNORE);
                                    closeControl();
                                }
                                break;
                            case C_FORWARD:
                                request.setOperation(
                                        (request.getSeqno() < requestList.size() - 1) ? FORWARD : NOOP);
                                break;
                            case C_ROTCW:
                                rotateClockwise(90);
                                break;
                            case C_ROTCCW:
                                rotateClockwise(270);
                                break;
                            case C_DELETE:
                                if (request.getImage() != null)
                                    processDelete(bottomShell, controlShell.getShell().getBounds());
                                break;
                            case C_RATE:
                                if (request.getImage() != null)
                                    rateSlide(controlShell.getShell(), e, RatingDialog.ABORT, false);
                                break;
                            case C_ALBUM:
                                if (request.getImage() != null) {
                                    Shell sh = controlShell.getShell();
                                    addToAlbums(sh, sh.toDisplay(e.x, e.y));
                                }
                                break;
                            case C_SELECTION:
                                if (request.getImage() != null)
                                    addToSelection(request, !request.isSelected());
                                break;
                            case C_VOICE:
                                request.setPaused(true);
                                recordVoiceNote(button * buttonWidth, request);
                                showControl();
                                break;
                            }
                            startTimer();
                        }
                    }
                });
                controlShell.getShell().addKeyListener(new KeyAdapter() {
                    @Override
                    public void keyReleased(KeyEvent e) {
                        switch (e.character) {
                        case ' ':
                            request.setPaused(false);
                            request.setOperation(CONTINUE);
                            closeControl();
                            break;
                        case SWT.TAB:
                            setPaused(true);
                            int cret = showClosePrompt(bottomShell);
                            if (cret != CLOSE_CANCEL) {
                                setPaused(false);
                                request.setOperation(cret == CLOSE_CLOSE ? ABORT : IGNORE);
                                closeControl();
                            } else
                                setPaused(false);
                            break;
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                            if (request.getImage() != null)
                                rateSlide(controlShell.getShell(), null, e.character - '0', true);
                            break;
                        case 'a':
                            setPaused(true);
                            Point p = slideControl.getLocation();
                            p.x += 4 * buttonWidth;
                            addToAlbums(controlShell.getShell(), p);
                            setPaused(false);
                            break;
                        case 'r':
                            rotateClockwise(270);
                            break;
                        case 'R':
                            rotateClockwise(90);
                            break;
                        case 's':
                            if (request.getImage() != null)
                                addToSelection(request, true);
                            break;
                        case 'S':
                            if (request.getImage() != null)
                                addToSelection(request, false);
                            break;
                        case 'v':
                        case 'V':
                            request.setPaused(true);
                            recordVoiceNote(C_VOICE * buttonWidth, request);
                            showControl();
                            break;
                        default:
                            switch (e.keyCode) {
                            case SWT.ESC:
                                setPaused(true);
                                cret = showClosePrompt(bottomShell);
                                if (cret != CLOSE_CANCEL) {
                                    setPaused(false);
                                    request.setOperation(cret == CLOSE_CLOSE ? ABORT : IGNORE);
                                    closeControl();
                                } else
                                    setPaused(false);
                                break;
                            case SWT.ARROW_LEFT:
                                if ((e.stateMask & SWT.CONTROL) == SWT.CONTROL) {
                                    rotateClockwise(270);
                                    break;
                                }
                                if (request.getSeqno() > 0) {
                                    request.setPaused(false);
                                    request.setOperation(BACK);
                                    closeControl();
                                    request.setPaused(true);
                                }
                                break;
                            case SWT.ARROW_RIGHT:
                                if ((e.stateMask & SWT.CONTROL) == SWT.CONTROL) {
                                    rotateClockwise(90);
                                    break;
                                }
                                if (request.getSeqno() < requestList.size() - 1) {
                                    request.setPaused(false);
                                    request.setOperation(FORWARD);
                                    closeControl();
                                    request.setPaused(true);
                                }
                                break;
                            case SWT.ARROW_UP:
                                setPaused(true);
                                p = slideControl.getLocation();
                                p.x += 4 * buttonWidth;
                                gotoSlide(p, request, RESTART);
                                setPaused(false);
                                break;
                            case SWT.DEL:
                                if ((e.stateMask & (SWT.CONTROL | SWT.ALT)) != 0 && request.getImage() != null)
                                    processDelete(bottomShell, controlShell.getShell().getBounds());
                                break;
                            case SWT.F2:
                                if (request.getImage() != null) {
                                    Asset asset = request.getAsset();
                                    if (asset != null)
                                        metadataRequested(asset);
                                }
                            }
                            break;
                        }
                    }
                });
                controlShell.getShell().setBounds(pBounds.x + (pBounds.width - w) / 2,
                        pBounds.y + pBounds.height - h - 50, w, h);
                controlShell.setAlpha(0);
                controlShell.open();
                controlShell.forceActive();
                controlShell.forceFocus();
                controlShell.setFocus();
                controller = new HoveringController(this);
                controller.install();
                for (int i = 1; i <= 10; i++) {
                    if (i > 1)
                        sleepTick(TICK);
                    controlShell.setAlpha(i * 15);
                }
            }

            public void dispose() {
                if (scaledPanel != null)
                    scaledPanel.dispose();
                if (controller != null)
                    controller.uninstall();
                if (controlShell != null && !controlShell.isDisposed()) {
                    int alpha = controlShell.getAlpha();
                    while (alpha > 15) {
                        controlShell.setAlpha(alpha -= 15);
                        sleepTick(TICK);
                    }
                    controlShell.close();
                    controlShell = null;
                }
            }

            public boolean isDisposed() {
                return controlShell.isDisposed();
            }

            public Shell getShell() {
                return controlShell.getShell();
            }

            public Point getLocation() {
                return controlShell.getLocation();
            }

            public Control getControl() {
                return getShell();
            }

            public String getTooltip(int mx, int my) {
                return null;
            }

            public Object findObject(MouseEvent event) {
                startTimer();
                return ((int) (event.x / factor - 1.5d)) / 80;
            }

            public ImageRegion[] findAllRegions(MouseEvent event) {
                return null;
            }

            public IGalleryHover getGalleryHover(MouseEvent event) {
                return new ControlHover();
            }

            @Override
            public Control[] getControls() {
                return null;
            }

        }

        private static final int CONTROLTIMEOUT = 3000;
        private int startFrom;
        private Shell parent;
        private SlideImpl slide;
        private double transitionValue;
        private boolean next = false;
        private int mouseMovements = 0;
        private Canvas imageCanvas;
        private FadingShell fadingShell;
        private int startNext;
        private FadingShell titleShell;
        private int titleTime;
        private Display displ;
        private SlideRequest request;
        private boolean pauseAt;
        private SlideRequest finishedRequest;
        private int buttonWidth;
        private SelectSlideDialog selectSlideDialog;
        private int effect;
        private FadingListener listener;
        private ScheduledFuture<?> titleTask;
        private ScheduledFuture<?> buttonTask;
        private ScheduledFuture<?> panelTask;
        private SlideControl slideControl;
        private Point pnt = new Point(0, 0);
        private Rectangle mbounds;
        private boolean inhibitTimer;
        private float zoom = 1f;
        private int ticks;

        public SingleSlideJob(SlideRequest request, int startFrom, int startNext, Shell parent, Rectangle mbounds,
                boolean pauseAt, FadingListener listener) {
            super(request.getSlide().getCaption());
            setPriority(Job.INTERACTIVE);
            setSystem(true);
            this.mbounds = mbounds;
            this.request = request;
            this.slide = request.getSlide();
            this.startFrom = startFrom;
            this.startNext = startNext;
            this.parent = parent;
            this.pauseAt = pauseAt;
            this.listener = listener;
            displ = parent.getDisplay();
        }

        @Override
        public boolean belongsTo(Object family) {
            return Constants.SLIDESHOW == family;
        }

        @Override
        protected IStatus run(final IProgressMonitor monitor) {
            finishedRequest = null;
            displ.syncExec(() -> {
                final Shell shell = new Shell(parent, SWT.NO_TRIM);
                effect = slide.getEffect();
                if (effect == Constants.SLIDE_TRANSITION_RANDOM)
                    effect = (int) (Math.random() * Constants.SLIDE_TRANSITION_N);
                fadingShell = new FadingShell(shell, true, effect);
                shell.setText(slide.getCaption());
                shell.setLayout(new FillLayout());
                imageCanvas = new Canvas(shell, SWT.NO_BACKGROUND | SWT.DOUBLE_BUFFERED);
                imageCanvas.setCursor(transparentCursor);
                imageCanvas.addPaintListener(new PaintListener() {
                    public void paintControl(PaintEvent e) {
                        ZImage image = request.getImage();
                        Rectangle sbounds = imageCanvas.getClientArea();
                        GC gc = e.gc;
                        gc.setBackground(shell.getBackground());
                        gc.fillRectangle(sbounds);
                        if (image != null && !image.isDisposed()) {
                            Rectangle ibounds = image.getBounds();
                            if (slide.getZoom() == 0)
                                image.draw(gc, (sbounds.width - ibounds.width) / 2,
                                        (sbounds.height - ibounds.height) / 2, ZImage.CROPPED, sbounds.width,
                                        sbounds.height);
                            else {
                                double f = Math.min((double) sbounds.width / ibounds.width,
                                        (double) sbounds.height / ibounds.height);
                                double iwidth = ibounds.width * f;
                                double iheight = ibounds.height * f;
                                double w = ibounds.width * zoom;
                                double h = ibounds.height * zoom;
                                double offx = Math.max(0, Math.min(ibounds.width - w,
                                        ibounds.width * (slide.getZoomX() + 100) / 200 - w / 2));
                                double offy = Math.max(0, Math.min(ibounds.height - h,
                                        ibounds.height * (slide.getZoomY() + 100) / 200 - h / 2));
                                image.draw(gc, (int) (offx + 0.5d), (int) (offy + 0.5d), (int) (w + 0.5d),
                                        (int) (h + 0.5d), (int) ((sbounds.width - iwidth) / 2 + 0.5d),
                                        (int) ((sbounds.height - iheight) / 2 + 0.5d), (int) (iwidth + 0.5d),
                                        (int) (iheight + 0.5d), ZImage.CROPPED, sbounds.width, sbounds.height,
                                        true);
                            }
                            if (request.isRemoveFromShow()) {
                                String txt = Messages.getString("SlideShowPlayer.deleted"); //$NON-NLS-1$
                                gc.setFont(JFaceResources.getFont(UiConstants.VIEWERBANNERFONT));
                                int x = gc.textExtent(txt).x;
                                gc.setForeground(displ.getSystemColor(SWT.COLOR_WHITE));
                                gc.drawText(txt, sbounds.width - x - 5, 5, true);
                                gc.setForeground(displ.getSystemColor(SWT.COLOR_RED));
                                gc.drawText(txt, sbounds.width - x - 4, 4, true);
                            }
                        }
                    }
                });
                imageCanvas.addMouseMoveListener(new MouseMoveListener() {
                    public void mouseMove(MouseEvent e) {
                        if (panelTask == null && !pauseAt && !ignoreMouseMovements)
                            mouseMovements++;
                    }
                });
                KeyAdapter keyListener = new KeyAdapter() {
                    @Override
                    public void keyReleased(KeyEvent e) {
                        switch (e.character) {
                        case SWT.TAB:
                            setPaused(true);
                            int ret = showClosePrompt(bottomShell);
                            if (ret != CLOSE_CANCEL)
                                request.setOperation(ret == CLOSE_CLOSE ? ABORT : IGNORE);
                            else
                                setPaused(false);
                            break;
                        case ' ':
                            if (request.isPaused())
                                request.setOperation(CONTINUE);
                            request.setPaused(!request.isPaused());
                            closeControl();
                            if (request.isPaused())
                                showMessage(Messages.getString("SlideShowPlayer.pause"), 1500, SWT.CENTER, //$NON-NLS-1$
                                        UiConstants.VIEWERBANNERFONT, 5);
                            break;
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                            if (request.getImage() != null) {
                                request.setPaused(true);
                                ZImage image = request.getImage();
                                if (image != null)
                                    rateSlide(shell, null, e.character - '0', true);
                                closeControl();
                                request.setPaused(false);
                            }
                            break;
                        case 'a':
                            setPaused(true);
                            addToAlbums(shell, null);
                            setPaused(false);
                            break;
                        case 'r':
                            rotateClockwise(270);
                            break;
                        case 'R':
                            rotateClockwise(90);
                            break;
                        case 's':
                            if (request.getImage() != null)
                                addToSelection(request, true);
                            break;
                        case 'S':
                            if (request.getImage() != null)
                                addToSelection(request, false);
                            break;
                        default:
                            switch (e.keyCode) {
                            case SWT.ESC:
                                setPaused(true);
                                int cret = showClosePrompt(bottomShell);
                                if (cret != CLOSE_CANCEL)
                                    request.setOperation(cret == CLOSE_CLOSE ? ABORT : IGNORE);
                                else
                                    setPaused(false);
                                break;
                            case SWT.ARROW_LEFT:
                                if ((e.stateMask & SWT.CONTROL) == SWT.CONTROL) {
                                    rotateClockwise(270);
                                    break;
                                }
                                if (request.getSeqno() > 0) {
                                    request.setOperation(BACK);
                                    closeControl();
                                    request.setPaused(true);
                                }
                                break;
                            case SWT.ARROW_RIGHT:
                                if ((e.stateMask & SWT.CONTROL) == SWT.CONTROL) {
                                    rotateClockwise(90);
                                    break;
                                }
                                if (request.getSeqno() < requestList.size() - 1) {
                                    request.setOperation(FORWARD);
                                    closeControl();
                                    request.setPaused(true);
                                }
                                break;
                            case SWT.ARROW_UP:
                                setPaused(true);
                                gotoSlide(null, request, RESTART);
                                setPaused(false);
                                break;
                            case SWT.DEL:
                                if ((e.stateMask & (SWT.CONTROL | SWT.ALT)) != 0 && request.getImage() != null) {
                                    request.setPaused(true);
                                    processDelete(shell, null);
                                    closeControl();
                                    request.setPaused(false);
                                }
                                break;
                            case SWT.F2:
                                if (request.getImage() != null) {
                                    Asset asset = request.getAsset();
                                    if (asset != null) {
                                        request.setPaused(true);
                                        metadataRequested(asset);
                                        request.setPaused(false);
                                    }
                                }
                            }
                            break;
                        }
                    }
                };
                imageCanvas.addKeyListener(keyListener);
                imageCanvas.addHelpListener(SingleSlideJob.this);
                shell.setFullScreen(true);
                shell.setBackground(parent.getBackground());
                shell.setForeground(parent.getForeground());
                shell.setBounds(mbounds);
                fadingShell.layout();
                fadingShell.open();
                fadingShell.forceActive();
                fadingShell.forceFocus();
                imageCanvas.setFocus();
            });
            try {
                int delay = slide.getDelay();
                int fadein = slide.getFadeIn();
                if (delay > fadein && !pauseAt) {
                    int diff = delay - fadein;
                    if (startFrom < diff) {
                        final int delaySteps = computeSteps(diff - startFrom);
                        delLoop: for (int i = 0; i < delaySteps; i++) {
                            sleepTick(TICK);
                            if (i % NTICKS == 0) {
                                switch (checkStatus(monitor)) {
                                case ABORT:
                                case IGNORE:
                                    return Status.CANCEL_STATUS;
                                case FORWARD:
                                    startFrom -= ((delaySteps - i) * TICK + duration + fadein);
                                    break delLoop;
                                }
                            }
                        }
                        startFrom = 0;
                    } else
                        startFrom -= diff;
                }
                Runnable runnable = new Runnable() {
                    public void run() {
                        Rectangle bounds = fadingShell.getShell().getParent().getBounds();
                        switch (effect) {
                        case Constants.SLIDE_TRANSITION_MOVE_LEFT:
                            double d = 1 - transitionValue;
                            fadingShell.setLocation((int) (bounds.x + bounds.width * d + 0.5d), bounds.y);
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_RIGHT:
                            d = transitionValue - 1;
                            fadingShell.setLocation((int) (bounds.x + bounds.width * d + 0.5d), bounds.y);
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_UP:
                            d = 1 - transitionValue;
                            fadingShell.setLocation(bounds.x, (int) (bounds.y + bounds.height * d + 0.5d));
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_DOWN:
                            d = transitionValue - 1;
                            fadingShell.setLocation(bounds.x, (int) (bounds.y + bounds.height * d + 0.5d));
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_TOPLEFT:
                            d = 1 - transitionValue;
                            fadingShell.setLocation((int) (bounds.x + bounds.width * d + 0.5d),
                                    (int) (bounds.y + bounds.height * d + 0.5d));
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_TOPRIGHT:
                            double dx = transitionValue - 1;
                            double dy = 1 - transitionValue;
                            fadingShell.setLocation((int) (bounds.x + bounds.width * dx + 0.5d),
                                    (int) (bounds.y + bounds.height * dy + 0.5d));
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_BOTTOMLEFT:
                            dx = 1 - transitionValue;
                            dy = transitionValue - 1;
                            fadingShell.setLocation((int) (bounds.x + bounds.width * dx + 0.5d),
                                    (int) (bounds.y + bounds.height * dy + 0.5d));
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        case Constants.SLIDE_TRANSITION_MOVE_BOTTOMRIGHT:
                            d = transitionValue - 1;
                            fadingShell.setLocation((int) (bounds.x + bounds.width * d + 0.5d),
                                    (int) (bounds.y + bounds.height * d + 0.5d));
                            fadingShell.setAlpha(transitionValue > 0 ? 255 : 0);
                            break;
                        default:
                            fadingShell.setAlpha((int) (255 * transitionValue + 0.5d));
                            break;
                        }
                    }
                };
                if (listener != null)
                    listener.fadeinStarted();
                if (startFrom < fadein) {
                    ignoreMouseMovements = true;
                    try {
                        final int insteps = computeSteps(fadein);
                        inLoop: for (int i = 0; i < insteps; i++) {
                            if (startFrom <= 0) {
                                if (fadingShell == null || fadingShell.isDisposed())
                                    break;
                                transitionValue = (double) (i + 1) / insteps;
                                if (pauseAt && i >= insteps - 1) {
                                    pauseAt = false;
                                    request.setPaused(true);
                                }
                                if (!displ.isDisposed())
                                    displ.asyncExec(runnable);
                                sleepTick(TICK);
                                if (i % NTICKS == 0) {
                                    switch (checkStatus(monitor)) {
                                    case ABORT:
                                    case IGNORE:
                                        return Status.CANCEL_STATUS;
                                    case FORWARD:
                                        startFrom -= ((insteps - i) * TICK + duration);
                                        break inLoop;
                                    }
                                }
                            } else
                                startFrom -= TICK;
                        }
                    } finally {
                        ignoreMouseMovements = false;
                    }
                } else {
                    startFrom -= fadein;
                    transitionValue = 1d;
                    if (!displ.isDisposed())
                        displ.asyncExec(runnable);
                    if (pauseAt) {
                        pauseAt = false;
                        request.setPaused(true);
                    }
                }
                if (listener != null)
                    listener.fadeinEnded();
                int dur = Math.max(slide.getDuration(), request.getVoiceLength());
                if (startFrom < dur) {
                    URL voiceUrl = request.getVoiceUri();
                    if (voiceUrl != null)
                        UiActivator.getDefault().playSoundfile(voiceUrl, null);
                    final int dursteps = computeSteps(dur);
                    if (slide.getAsset() != null) {
                        final int titleDur = Math.min(slideshow.getTitleDisplay(), dur) - startFrom;
                        if (titleDur > 0)
                            if (!fadingShell.isDisposed()) {
                                displ.syncExec(() -> {
                                    if (!fadingShell.isDisposed()) {
                                        final String title;
                                        switch (slideshow.getTitleContent()) {
                                        case Constants.SLIDE_TITLE_SNO:
                                            title = NLS.bind("{2} ({0}/{1})", //$NON-NLS-1$
                                                    new Object[] { slide.getSequenceNo(),
                                                            slideshow.getEntry().size(), slide.getCaption() });
                                            break;
                                        case Constants.SLIDE_SNOONLY:
                                            title = NLS.bind("{0}/{1}", slide.getSequenceNo(), slideshow //$NON-NLS-1$
                                                    .getEntry().size());
                                            break;
                                        case Constants.SLIDE_TITLE_NE:
                                            String caption = slide.getCaption();
                                            Asset asset = request.getAsset();
                                            if (asset != null) {
                                                String fileName = Core.getFileName(asset.getUri(), true);
                                                if (caption.equals(fileName)) {
                                                    title = null;
                                                    break;
                                                }
                                            }
                                            title = caption;
                                            break;
                                        default:
                                            title = slide.getCaption();
                                            break;
                                        }
                                        if (title != null)
                                            showMessage(title, titleDur, SWT.END, UiConstants.VIEWERTITLEFONT, 2);
                                    }
                                });
                            }
                    }
                    long t1 = System.currentTimeMillis();
                    ticks = 1;
                    int sleep = TICK;
                    durLoop: for (int i = 0; i < dursteps; i += ticks) {
                        if (startFrom <= 0)
                            sleepTick(sleep);
                        if (i % NTICKS == 0) {
                            switch (checkStatus(monitor)) {
                            case ABORT:
                            case IGNORE:
                                return Status.CANCEL_STATUS;
                            case CONTINUE:
                            case FORWARD:
                                startFrom -= ((dursteps - i) * TICK);
                                break durLoop;
                            }
                        }
                        if (slide.getZoom() > 0) {
                            zoom = 1 - ((float) i / dursteps) * slide.getZoom() / 200;
                            displ.syncExec(() -> {
                                if (!fadingShell.isDisposed())
                                    imageCanvas.redraw();
                            });
                            long t2 = System.currentTimeMillis();
                            int interval = (int) (t2 - t1);
                            t1 = t2;
                            ticks = (interval + TICK - 1) / TICK;
                            if (ticks < 1)
                                ticks = 1;
                            sleep = Math.max(2, TICK * ticks - interval);
                        }
                        startFrom -= TICK;
                        if (i * TICK >= startNext && !next) {
                            finishedRequest = request;
                            next = true;
                        }
                    }
                } else
                    startFrom -= dur;
                if (!next) {
                    finishedRequest = request;
                    next = true;
                }
                if (finishedRequest != null) {
                    done.add(finishedRequest);
                    finishedRequest = null;
                }
                if (listener != null)
                    listener.fadeoutStarted();
                int fadeout = slide.getFadeOut();
                if (startFrom < fadeout) {
                    if (effect == Constants.SLIDE_TRANSITION_FADE) {
                        final int outsteps = computeSteps(fadeout);
                        for (int i = outsteps - 1; i >= 0; i--) {
                            if (startFrom <= 0) {
                                if (fadingShell.isDisposed())
                                    break;
                                transitionValue = (double) i / outsteps;
                                if (!displ.isDisposed())
                                    displ.asyncExec(runnable);
                                sleepTick(TICK);
                                if (i % NTICKS == 0) {
                                    int ret = checkStatus(monitor);
                                    if (ret == ABORT || ret == IGNORE)
                                        return Status.CANCEL_STATUS;
                                }
                            } else
                                startFrom -= TICK;
                        }
                    } else {
                        try {
                            Thread.sleep(fadeout);
                        } catch (InterruptedException e) {
                            // do nothing
                        }
                        startFrom -= fadeout;
                    }
                } else
                    startFrom -= fadeout;
                if (listener != null)
                    listener.fadeoutEnded();
            } finally {
                displ.syncExec(() -> {
                    fadingShell.setAlpha(0);
                    fadingShell.close();
                    if (request.operation != RESTART)
                        request.dispose();
                });
                if (finishedRequest != null) {
                    done.add(finishedRequest);
                    finishedRequest = null;
                }
            }
            return Status.OK_STATUS;
        }

        protected void showMessage(String msg, final int titleDur, int valign, String font, int margins) {
            Rectangle pBounds = fadingShell.getBounds();
            Shell shell = new Shell(fadingShell.getShell(), SWT.NO_TRIM);
            titleShell = new FadingShell(shell, true, Constants.SLIDE_TRANSITION_FADE);
            shell.setText(msg);
            shell.setLayout(new FillLayout());
            final Canvas titleCanvas = new Canvas(shell, SWT.NO_BACKGROUND | SWT.DOUBLE_BUFFERED);
            GC gc = new GC(titleCanvas);
            gc.setFont(JFaceResources.getFont(font));
            Point tx = gc.textExtent(msg);
            gc.dispose();
            titleCanvas.addPaintListener(new PaintListener() {
                public void paintControl(PaintEvent e) {
                    e.gc.setFont(JFaceResources.getFont(font));
                    e.gc.drawText(msg, margins, margins);
                }
            });
            titleCanvas.addHelpListener(SingleSlideJob.this);
            int y = pBounds.y;
            switch (valign) {
            case SWT.BEGINNING:
                y += margins;
                break;
            case SWT.CENTER:
                y += (pBounds.height - tx.y) / 2 - margins;
                break;
            default:
                y += pBounds.height - tx.y - margins;
                break;
            }
            shell.setBounds(pBounds.x + (pBounds.width - tx.x) / 2 - margins, y, tx.x + 2 * margins,
                    tx.y + 2 * margins);
            titleShell.layout();
            titleShell.setAlpha(0);
            titleShell.open();
            titleShell.forceActive();
            titleCanvas.redraw();
            imageCanvas.setFocus();
            titleTime = 0;
            final int fade = Math.min(500, titleDur / 4);
            final int dur = Math.max(fade * 2 + 100, titleDur);
            titleTask = UiActivator.getScheduledExecutorService().scheduleAtFixedRate(() -> {
                titleTime += TICK;
                if (titleShell != null && !titleShell.isDisposed()) {
                    if (titleTime >= dur)
                        displ.syncExec(() -> {
                            titleTask.cancel(true);
                            titleShell.close();
                        });
                    else if (titleTime <= fade)
                        displ.syncExec(() -> {
                            titleShell.setAlpha(
                                    Math.min(255, titleShell.getAlpha() + (255 * TICK + fade - 1) / fade));
                        });
                    else if (titleTime > dur - fade)
                        displ.syncExec(() -> {
                            titleShell.setAlpha(
                                    Math.max(0, titleShell.getAlpha() - ((255 * TICK + fade - 1) / fade)));
                        });
                }
            }, 0L, TICK, TimeUnit.MILLISECONDS);
        }

        private int checkStatus(IProgressMonitor monitor) {
            int result = doCheckStatus(monitor);
            if (result != NOOP)
                return result;
            while (request.isPaused()) {
                sleepTick(LONGTICK);
                result = doCheckStatus(monitor);
                if (result != NOOP)
                    return result;
            }
            return NOOP;
        }

        private int doCheckStatus(IProgressMonitor monitor) {
            if (request.getOperation() != NOOP || monitor.isCanceled()) {
                if (!next)
                    finishedRequest = request;
                switch (request.getOperation()) {
                case FORWARD:
                    return FORWARD;
                case CONTINUE:
                    return CONTINUE;
                default:
                    return ABORT;
                }
            }
            if (mouseMovements > 3) {
                request.setPaused(true);
                showControl();
            }
            mouseMovements = 0;
            return NOOP;
        }

        private void showControl() {
            if (!fadingShell.isDisposed())
                displ.syncExec(() -> {
                    doShowControl();
                });
        }

        private void doShowControl() {
            if (!fadingShell.isDisposed()) {
                imageCanvas.setCursor(null);
                slideControl = new SlideControl(fadingShell);
                startTimer();
            }
        }

        private boolean canBeSelected(SlideRequest request) {
            return (request.getImage() == null || request.getSlide().getAsset() == null) ? false
                    : !request.isSelected();
        }

        protected void restart(int incr) {
            restartAt(request.getSeqno() + incr);
        }

        protected void restartAt(int pos) {
            request.setOperation((pos >= 0 && pos < requestList.size()) ? pos : NOOP);
        }

        private void startTimer() {
            stopTimer();
            if (!inhibitTimer)
                panelTask = UiActivator.getScheduledExecutorService().schedule(() -> {
                    if (!displ.isDisposed())
                        displ.syncExec(() -> closeControl());
                    if (!displ.isDisposed())
                        displ.syncExec(() -> {
                            if (imageCanvas != null && !imageCanvas.isDisposed()) {
                                imageCanvas.setCursor(transparentCursor);
                                imageCanvas.setFocus();
                            }
                        });
                    panelTask = null;
                }, CONTROLTIMEOUT, TimeUnit.MILLISECONDS);
        }

        private void startButtonTimer(final int operation, final Point pos) {
            stopTimer();
            stopButtonTimer();
            buttonTask = UiActivator.getScheduledExecutorService().schedule(() -> {
                if (!displ.isDisposed())
                    displ.syncExec(() -> gotoSlide(pos, request, operation));
            }, 700L, TimeUnit.MILLISECONDS);
        }

        private void gotoSlide(Point pos, SlideRequest current, int operation) {
            inhibitTimer = true;
            try {
                stopTimer();
                List<SlideImpl> slideList = new ArrayList<SlideImpl>(requestList.size());
                switch (operation) {
                case BACK:
                    for (int i = requestList.size() - 1; i >= 0; i--) {
                        SlideRequest r = requestList.get(i);
                        if (r == current)
                            break;
                        slideList.add(r.getSlide());
                    }
                    break;
                case FORWARD:
                    boolean found = false;
                    for (SlideRequest r : requestList) {
                        if (found)
                            slideList.add(r.getSlide());
                        if (r == current)
                            found = true;
                    }
                    break;
                default:
                    for (SlideRequest r : requestList)
                        slideList.add(r.getSlide());
                }
                Shell sh = slideControl != null && !slideControl.isDisposed() ? slideControl.getShell()
                        : new Shell(parent, SWT.NO_TRIM);
                selectSlideDialog = new SelectSlideDialog(sh, slideList, false);
                selectSlideDialog.create();
                if (pos != null) {
                    pos.y -= selectSlideDialog.getShell().getSize().y;
                    selectSlideDialog.getShell().setLocation(pos);
                }
                if (operation != BACK && operation != FORWARD)
                    selectSlideDialog.setSelection(current.getSlide());
                int ret = selectSlideDialog.open();
                startTimer();
                if (ret == Window.OK) {
                    SlideImpl selectedSlide = selectSlideDialog.getResult();
                    for (SlideRequest r : requestList) {
                        if (r.getSlide() == selectedSlide) {
                            restartAt(r.getSeqno());
                            break;
                        }
                    }
                }
                selectSlideDialog = null;
            } finally {
                inhibitTimer = false;
            }
        }

        protected void closeControl() {
            if (slideControl != null) {
                slideControl.dispose();
                slideControl = null;
            }
            mouseMovements = 0;
        }

        protected void stopTimer() {
            if (panelTask != null) {
                panelTask.cancel(true);
                panelTask = null;
            }
        }

        protected void stopButtonTimer() {
            if (buttonTask != null) {
                buttonTask.cancel(true);
                buttonTask = null;
            }
        }

        public void helpRequested(HelpEvent e) {
            ToolTip toolTip = new ToolTip(displ.getActiveShell(), SWT.BALLOON);
            toolTip.setAutoHide(true);
            pnt.x = pnt.y = 0;
            toolTip.setLocation(pnt);
            toolTip.setText(Messages.getString("SlideShowPlayer.Slideshow")); //$NON-NLS-1$
            toolTip.setMessage(getKeyboardHelp());
            toolTip.setVisible(true);
        }

        private void metadataRequested(Asset asset) {
            ToolTip toolTip = new ToolTip(displ.getActiveShell(), SWT.BALLOON);
            toolTip.setAutoHide(true);
            Rectangle bounds = displ.getPrimaryMonitor().getBounds();
            pnt.x = bounds.width / 2;
            pnt.y = bounds.height / 2;
            toolTip.setLocation(pnt);
            toolTip.setText(NLS.bind(Messages.getString("SlideShowPlayer.metadata"), asset.getName())); //$NON-NLS-1$
            toolTip.setMessage(new HoverInfo(asset, (ImageRegion[]) null).getText());
            toolTip.setVisible(true);
        }

        private void rateSlide(final Control control, MouseEvent event, int rating, boolean autoClose) {
            stopTimer();
            Shell shell = control instanceof Shell ? (Shell) control : control.getShell();
            RatingDialog ratingDialog = new RatingDialog(shell,
                    rating != RatingDialog.ABORT ? rating
                            : request.isRemoveFromShow() ? RatingDialog.DELETE : request.getRating(),
                    1d, false, false);
            if (autoClose)
                ratingDialog.setTimeout(3000);
            ratingDialog.create();
            if (event != null)
                ratingDialog.getShell().setLocation(control.toDisplay(event.x, event.y));
            rating = ratingDialog.open();
            switch (rating) {
            case RatingDialog.DELETE:
                if (request.isDeleted()) {
                    request.setDeleted(false);
                    request.setDeleteFromDisk(false);
                    request.setRemoveFromShow(false);
                } else
                    processDelete(bottomShell, shell.getBounds());
                break;
            case RatingDialog.ABORT:
                break;
            default:
                request.setDeleted(false);
                request.setDeleteFromDisk(false);
                request.setRemoveFromShow(false);
                request.setRating(rating);
                break;
            }
        }

        protected void recordVoiceNote(int xoff, SlideRequest request) {
            if (request != null) {
                Asset asset = request.getAsset();
                if (asset != null && asset.getFileState() != IVolumeManager.PEER) {
                    UiActivator.getDefault().stopAudio();
                    VoiceNoteDialog dialog = new VoiceNoteDialog(fadingShell.getShell(), asset);
                    dialog.create();
                    Point p = slideControl.getLocation();
                    dialog.getShell().setLocation(p.x + xoff, p.y - dialog.getShell().getSize().y);
                    if (dialog.open() == VoiceNoteDialog.OK)
                        request.setVoiceNote(dialog.getSourceUri(), dialog.getTargetUri(), dialog.getNoteText(),
                                dialog.getSvg());
                }
            }
        }

        private void addToAlbums(final Control control, Point pos) {
            stopTimer();
            Shell shell = control instanceof Shell ? (Shell) control : control.getShell();
            IDbManager dbManager = Core.getCore().getDbManager();
            String assetId = request.getSlide().getAsset();
            String[] albums = null;
            if (assetId != null) {
                AssetImpl asset = dbManager.obtainAsset(assetId);
                if (asset != null)
                    albums = asset.getAlbum();
            }
            AlbumSelectionDialog addAlbumDialog = new AlbumSelectionDialog(shell, true,
                    (albums != null) ? Arrays.asList(albums) : null, selectedAlbums);
            addAlbumDialog.create();
            if (pos != null) {
                pos.y -= addAlbumDialog.getShell().getSize().y;
                addAlbumDialog.getShell().setLocation(pos);
            }
            if (addAlbumDialog.open() == AlbumSelectionDialog.OK) {
                Collection<SmartCollectionImpl> checkedAlbums = addAlbumDialog.getResult();
                request.setAlbums(
                        selectedAlbums = checkedAlbums.toArray(new SmartCollectionImpl[checkedAlbums.size()]));
            }
        }

        private void addToSelection(SlideRequest request, boolean select) {
            if (request.getImage() != null && request.getSlide().getAsset() != null) {
                request.setSelected(select);
                closeControl();
                request.setPaused(true);
                doShowControl();
            }
        }

        private void processDelete(Shell shell, Rectangle bounds) {
            SlideImpl thisslide = request.getSlide();
            String assetId = thisslide.getAsset();
            IDbManager dbManager = Core.getCore().getDbManager();
            List<SlideImpl> otherSlides = dbManager.obtainObjects(SlideImpl.class, "asset", assetId, //$NON-NLS-1$
                    QueryField.EQUALS);
            List<ExhibitImpl> exhibits = dbManager.obtainObjects(ExhibitImpl.class, "asset", assetId, //$NON-NLS-1$
                    QueryField.EQUALS);
            List<WebExhibitImpl> webexhibits = dbManager.obtainObjects(WebExhibitImpl.class, "asset", assetId, //$NON-NLS-1$
                    QueryField.EQUALS);
            if (!(otherSlides.isEmpty() || otherSlides.size() == 1 && otherSlides.contains(thisslide))
                    || !exhibits.isEmpty() || !webexhibits.isEmpty()) {
                AcousticMessageDialog dialog = new AcousticMessageDialog(shell,
                        Messages.getString("SlideShowPlayer.delete_image"), //$NON-NLS-1$
                        null, // accept
                        Messages.getString("SlideShowPlayer.also_used_in_other_presentations"), //$NON-NLS-1$
                        AcousticMessageDialog.QUESTION,
                        new String[] { IDialogConstants.OK_LABEL, IDialogConstants.CANCEL_LABEL }, 0);
                if (bounds != null)
                    dialog.setY(bounds.y);
                dialog.create();
                dialog.setReturnCode(-1);
                if (dialog.open() == 0) {
                    request.setDeleted(false);
                    request.setRemoveFromShow(true);
                    request.setDeleteFromDisk(false);
                }
            } else {
                AcousticMessageDialog dialog = new AcousticMessageDialog(shell,
                        Messages.getString("SlideShowPlayer.delete_image"), //$NON-NLS-1$
                        null, Messages.getString("SlideShowPlayer.this_will_delete_from_cat_and_slideshow"), //$NON-NLS-1$
                        MessageDialog.QUESTION,
                        adhoc ? new String[] { Messages.getString("SlideShowPlayer.only_in_catalog"), //$NON-NLS-1$
                                Messages.getString("SlideShowPlayer.delete_on_disc_too"), //$NON-NLS-1$
                                IDialogConstants.CANCEL_LABEL }
                                : new String[] { Messages.getString("SlideShowPlayer.only_in_catalog"), //$NON-NLS-1$
                                        Messages.getString("SlideShowPlayer.delete_on_disc_too"), //$NON-NLS-1$
                                        Messages.getString("SlideShowPlayer.remove_only_from_slideshow"), //$NON-NLS-1$
                                        IDialogConstants.CANCEL_LABEL },
                        0);
                if (bounds != null)
                    dialog.setY(bounds.y);
                dialog.create();
                dialog.setReturnCode(-1);
                switch (dialog.open()) {
                case 0:
                    request.setDeleted(true);
                    request.setRemoveFromShow(true);
                    request.setDeleteFromDisk(false);
                    break;
                case 1:
                    request.setDeleted(true);
                    request.setRemoveFromShow(true);
                    request.setDeleteFromDisk(true);
                    break;
                case 2:
                    if (!adhoc) {
                        request.setDeleted(false);
                        request.setRemoveFromShow(true);
                        request.setDeleteFromDisk(false);
                    }
                    break;
                }
            }
        }

        private void rotateClockwise(int angle) {
            ZImage image = request.getImage();
            if (image != null) {
                image.setRotation(image.getRotation() + angle, 1f, 1f);
                int rotation = request.getRotation();
                rotation += angle;
                if (rotation >= 360)
                    rotation -= 360;
                request.setRotation(rotation);
                restart(0);
            }
        }

        public void setPaused(boolean paused) {
            if (request != null)
                request.setPaused(paused);
            mouseMovements = 0;
        }

    }

    public class MainLoopJob extends Job {

        private int currentSlide;
        private int startFrom;
        private Shell shell;
        private long timeout;
        private SlideRequest request;

        public MainLoopJob(int firstSlide, int positionFirstSlide, Shell shell, Rectangle mbounds) {
            super(Messages.getString("SlideShowPlayer.slideshow_main_loop")); //$NON-NLS-1$
            timeout = UiActivator.getDefault().getPreferenceStore().getInt(PreferenceConstants.INACTIVITYINTERVAL)
                    * 60000L;
            setPriority(Job.INTERACTIVE);
            setSystem(true);
            this.currentSlide = firstSlide;
            this.startFrom = positionFirstSlide;
            this.shell = shell;
        }

        @Override
        public boolean belongsTo(Object family) {
            return Constants.SLIDESHOW == family;
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            for (int i = 0; i < 300; i++) {
                if (requestList != null && System.currentTimeMillis() - startTime >= STARTUPDELAY)
                    break;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // ignore
                }
            }
            SlideRequest answer = null;
            try {
                boolean pauseAt = false;
                if (currentSlide < requestList.size()) {
                    request = requestList.get(currentSlide++);
                    show: while (!checkStatus(request, monitor)) {
                        SlideImpl slide = null;
                        singleSlideJob = null;
                        answer = null;
                        try {
                            answer = answers.poll(180, TimeUnit.SECONDS);
                        } catch (InterruptedException e) {
                            // should never happen
                        }
                        if (checkStatus(request, monitor))
                            break;
                        int d = 0;
                        if (answer != null && !shell.isDisposed()) {
                            shell.getDisplay().asyncExec(() -> {
                                if (!shell.isDisposed()) {
                                    text.setText(""); //$NON-NLS-1$
                                    bottomCanvas.redraw();
                                }
                            });
                            slide = answer.getSlide();
                            ZImage image = answer.getImage();
                            if (image != null) {
                                d = slide.getDelay() + slide.getDuration() + slide.getFadeOut();
                                int startNext = slide.getDuration() - answer.getOverlap();
                                singleSlideJob = new SingleSlideJob(answer, startFrom, startNext, shell, mbounds,
                                        pauseAt, new FadingListener() {
                                            public void fadeoutStarted() {
                                                // do nothing
                                            }

                                            public void fadeoutEnded() {
                                                // do nothing
                                            }

                                            public void fadeinStarted() {
                                                // do nothing
                                            }

                                            public void fadeinEnded() {
                                                preloadNextSlide();
                                            }
                                        });
                                singleSlideJob.schedule();
                            }
                        }
                        pauseAt = false;
                        if (checkStatus(answer, monitor))
                            break;
                        startFrom = 0;
                        if (singleSlideJob != null) {
                            single: while (true) {
                                if (checkStatus(answer, monitor))
                                    break show;
                                try {
                                    SlideRequest receipt = done.poll(d + timeout, TimeUnit.MILLISECONDS);
                                    int operation = ABORT;
                                    if (receipt != null) {
                                        operation = receipt.getOperation();
                                        receipt.setOperation(NOOP);
                                    }
                                    switch (operation) {
                                    case RESTART:
                                        if (receipt != null)
                                            receipt.setPaused(false);
                                        sleepTick(TICK);
                                        if (slide != null && receipt != null)
                                            new SingleSlideJob(receipt, slide.getDelay(),
                                                    slide.getDuration() - receipt.getOverlap(), shell, mbounds,
                                                    true, null).schedule();
                                        break;
                                    case FORWARD:
                                        if (receipt != null)
                                            receipt.setPaused(false);
                                        pauseAt = true;
                                        break single;
                                    case BACK:
                                        request = jumpToSlide(request, receipt,
                                                receipt == null ? 0 : receipt.getSeqno() - 1,
                                                Messages.getString("SlideShowPlayer.loading_previous_slide")); //$NON-NLS-1$
                                        pauseAt = true;
                                        continue show;
                                    case NOOP:
                                    case CONTINUE:
                                        break single;
                                    case ABORT:
                                    case IGNORE:
                                        request.setAborted(true);
                                        break show;
                                    default:
                                        request = jumpToSlide(request, receipt, operation,
                                                Messages.getString("SlideShowPlayer.loading_selected_slide")); //$NON-NLS-1$
                                        pauseAt = true;
                                        continue show;
                                    }
                                } catch (InterruptedException e1) {
                                    break;
                                }
                            }
                        }
                        if (checkStatus(request, monitor))
                            break;
                        if (currentSlide > requestList.size()) {
                            if (slide != null && !shell.isDisposed()) {
                                shell.getDisplay().asyncExec(() -> {
                                    if (!shell.isDisposed()) {
                                        text.setText(Messages.getString("SlideShowPlayer.end_of_show")); //$NON-NLS-1$
                                        CssActivator.getDefault().setColors(bottomShell);
                                        bottomCanvas.redraw();
                                    }
                                });
                                for (int i = computeSteps(slide.getFadeOut() + 500) - 1; i >= 0; i--) {
                                    sleepTick(TICK);
                                    if (checkStatus(request, monitor))
                                        break;
                                }
                            }
                            break;
                        }
                    }
                }
                if (!ignoreChanges)
                    new ApplyChangesJob(requestList).schedule();
                if (adhoc && !display.isDisposed())
                    display.asyncExec(() -> {
                        if (!display.isDisposed())
                            promptForSave(requestList);
                    });
                return Status.OK_STATUS;
            } catch (Exception e) {
                return new Status(IStatus.ERROR, UiActivator.PLUGIN_ID,
                        Messages.getString("SlideShowPlayer.internal_error"), e); //$NON-NLS-1$
            } finally {
                if (exifTool != null) {
                    exifTool.close();
                    exifTool = null;
                }
                if (answer != null)
                    answer.dispose();
                if (!shell.isDisposed())
                    shell.getDisplay().asyncExec(() -> {
                        if (!shell.isDisposed())
                            close();
                    });
            }
        }

        private void preloadNextSlide() {
            if (currentSlide < requestList.size()) {
                request = requestList.get(currentSlide);
                requests.add(request);
            }
            ++currentSlide;
        }

        private SlideRequest jumpToSlide(SlideRequest req, SlideRequest receipt, int pos, final String message)
                throws InterruptedException {
            // waste currently converted image
            if (currentSlide <= requestList.size()) {
                req.setAborted(true);
                answers.poll(180, TimeUnit.SECONDS);
                req.setAborted(false);
            }
            // jump to slide and ask for another
            if (!shell.isDisposed())
                shell.getDisplay().asyncExec(() -> {
                    if (!text.isDisposed()) {
                        text.setText(message);
                        bottomCanvas.redraw();
                    }
                });
            currentSlide = pos;
            req = requestList.get(currentSlide++);
            requests.add(req);
            // continue
            if (receipt != null)
                receipt.setPaused(false);
            return req;
        }

        private boolean checkStatus(SlideRequest req, IProgressMonitor monitor) {
            if (req != null) {
                int op = req.getOperation();
                if (op == ABORT || op == IGNORE || monitor.isCanceled())
                    return true;
                while (req.isPaused())
                    sleepTick(TICK);
            }
            return false;
        }
    }

    private static class SlideRequest {
        private SlideImpl slide;
        private URI uri;
        private int rotation;
        private int rating = -1;
        private boolean deleted;
        private int overlap;
        private ZImage image;
        private int operation = NOOP;
        private boolean paused = false;
        private boolean aborted;
        private boolean deleteFromDisk;
        private boolean removeFromShow;
        private int seqno;
        private int previewSize;
        private List<AssetImpl> assets = new ArrayList<AssetImpl>();
        private SmartCollectionImpl[] albums;
        private boolean selected;
        private URL voiceUrl;
        private int voiceLength;
        private String sourceURI;
        private String targetURI;
        private String noteText;
        private String svg;
        private boolean annotationChanged;

        public SlideRequest(int seqno, SlideImpl slide, URI uri, int rotation, int rating, int previewSize) {
            this.seqno = seqno;
            this.slide = slide;
            this.uri = uri;
            this.rotation = rotation;
            this.rating = rating;
            this.previewSize = previewSize;
        }

        public void setVoiceNote(String sourceURI, String targetURI, String noteText, String svg) {
            this.sourceURI = sourceURI;
            this.targetURI = targetURI;
            this.noteText = noteText;
            this.svg = svg;
            annotationChanged = true;
        }

        public void setVoiceLength(int voiceLength) {
            this.voiceLength = voiceLength;
        }

        public void setVoiceUrl(URL voiceUrl) {
            this.voiceUrl = voiceUrl;
        }

        public void dispose() {
            if (image != null) {
                image.dispose();
                image = null;
            }
        }

        public AssetImpl[] getNextAssets() {
            return assets.toArray(new AssetImpl[assets.size()]);
        }

        public boolean getDeleteFromDisk() {
            return deleteFromDisk;
        }

        public boolean isAborted() {
            return aborted;
        }

        public void setRotation(int rotation) {
            this.rotation = rotation;
        }

        public int getOverlap() {
            return overlap;
        }

        public int getRotation() {
            return rotation;
        }

        public SlideImpl getSlide() {
            return slide;
        }

        public URI getUri() {
            return uri;
        }

        public void setOverlap(int overlap) {
            this.overlap = overlap;
        }

        public ZImage getImage() {
            return image;
        }

        public void setImage(ZImage image) {
            this.image = image;
        }

        public int getRating() {
            return rating;
        }

        public void setRating(int rating) {
            this.rating = rating;
        }

        public boolean isDeleted() {
            return deleted;
        }

        public void setDeleted(boolean deleted) {
            this.deleted = deleted;
        }

        public int getOperation() {
            return operation;
        }

        public void setOperation(int operation) {
            this.operation = operation;
        }

        public boolean isPaused() {
            return paused;
        }

        public void setPaused(boolean paused) {
            this.paused = paused;
        }

        public void setAborted(boolean aborted) {
            this.aborted = aborted;
        }

        public void setDeleteFromDisk(boolean deleteFromDisk) {
            this.deleteFromDisk = deleteFromDisk;
        }

        public int getSeqno() {
            return seqno;
        }

        public int getPreviewSize() {
            return previewSize;
        }

        public void addAsset(AssetImpl asset) {
            assets.add(asset);
        }

        public void setRemoveFromShow(boolean removeFromShow) {
            this.removeFromShow = removeFromShow;
        }

        public boolean isRemoveFromShow() {
            return removeFromShow;
        }

        public SmartCollectionImpl[] getAlbums() {
            return albums;
        }

        public void setAlbums(SmartCollectionImpl[] albums) {
            this.albums = albums;
        }

        public boolean isSelected() {
            return selected;
        }

        public void setSelected(boolean selected) {
            this.selected = selected;
        }

        public Asset getAsset() {
            return assets.isEmpty() ? null : assets.get(0);
        }

        public URL getVoiceUri() {
            return voiceUrl;
        }

        public int getVoiceLength() {
            return voiceLength;
        }

        public String getSourceURI() {
            return sourceURI;
        }

        public String getTargetURI() {
            return targetURI;
        }

        public String getNoteText() {
            return noteText;
        }

        public String getSvg() {
            return svg;
        }

        public boolean annotationChanged() {
            return annotationChanged;
        }
    }

    // Status
    private static final int NOOP = Integer.MIN_VALUE;
    private static final int BACK = -1;
    private static final int RESTART = -2;
    private static final int FORWARD = -3;
    private static final int CONTINUE = -4;
    private static final int ABORT = Integer.MAX_VALUE;
    private static final int IGNORE = Integer.MAX_VALUE - 1;
    // Commands
    private static final int C_BACK = 0;
    private static final int C_PLAY = 1;
    private static final int C_STOP = 2;
    private static final int C_FORWARD = 3;
    private static final int C_JUMP = 4;
    private static final int C_ROTCW = 5;
    private static final int C_ROTCCW = 6;
    private static final int C_DELETE = 7;
    private static final int C_RATE = 8;
    private static final int C_ALBUM = 9;
    private static final int C_SELECTION = 10;
    private static final int C_VOICE = 11;

    private static final double MARGINHEIGHT = 0.1d;
    private static final double MARGINWIDTH = 0.1d;
    private static final double THUMBNAILSPACE = 0.3d;
    private static final SlideRequest FINALREQUEST = new SlideRequest(-1, null, null, 0, -1, 0);
    private static final int CLOSE_CLOSE = 0;
    private static final int CLOSE_IGNORE = 1;
    private static final int CLOSE_CANCEL = 2;

    BlockingQueue<SlideRequest> requests;
    BlockingQueue<SlideRequest> answers;
    BlockingQueue<SlideRequest> done;
    private SlideShowImpl slideshow;
    private Shell bottomShell;
    private Canvas bottomCanvas;
    private TextLayout text;
    private List<SlideImpl> slides;
    private boolean advanced;
    private List<SlideRequest> requestList;
    private int cms;
    private int total;
    private int offline;
    private int duration;
    private int firstSlide;
    private int positionFirstSlide;
    protected boolean adhoc;
    private PreparationJob prepJob;
    protected SingleSlideJob singleSlideJob;
    private long startTime;
    private ExifTool exifTool;
    public Job transferJob;
    public boolean ignoreMouseMovements;
    public boolean ignoreChanges;

    // State

    public void init(IWorkbenchWindow parentWindow, SlideShowImpl slideshow, List<SlideImpl> slides,
            boolean adhoc) {
        super.init(parentWindow, PRIMARY);
        this.adhoc = adhoc;
        this.slideshow = slideshow;
        this.slides = slides;
        IPreferencesService preferencesService = Platform.getPreferencesService();
        advanced = preferencesService.getBoolean(UiActivator.PLUGIN_ID, PreferenceConstants.ADVANCEDGRAPHICS, false,
                null);
        cms = preferencesService.getInt(UiActivator.PLUGIN_ID, PreferenceConstants.COLORPROFILE,
                ImageConstants.SRGB, null);
    }

    public void promptForSave(List<SlideRequest> reqList) {
        final Set<String> deleted = new HashSet<String>();
        for (SlideRequest request : reqList)
            if (request.isRemoveFromShow())
                deleted.add(request.getSlide().getStringId());
        if (slides.size() > deleted.size()) {
            ToolTip tooltip = new ToolTip(parentWindow.getShell(), SWT.BALLOON);
            tooltip.setText(Messages.getString("SlideShowPlayer.save_show")); //$NON-NLS-1$
            tooltip.setMessage(Messages.getString("SlideShowPlayer.save_show_msg")); //$NON-NLS-1$
            tooltip.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    doPromptForSave(deleted);
                }
            });
            tooltip.setVisible(true);
        }
    }

    private void doPromptForSave(final Set<String> deleted) {
        SlideShowSaveDialog dialog = new SlideShowSaveDialog(parentWindow.getShell());
        if (dialog.open() == Window.OK) {
            final String name = dialog.getName();
            final Group group = dialog.getGroup();
            boolean open = dialog.getOpen();
            final IDbManager db = Core.getCore().getDbManager();
            db.safeTransaction(() -> {
                for (Slide slide : slides)
                    if (deleted.contains(slide.toString()))
                        slideshow.removeEntry(slide.toString());
                    else
                        db.store(slide);
                slideshow.setName(name);
                slideshow.setAdhoc(false);
                group.addSlideshow(slideshow.toString());
                slideshow.setGroup_slideshow_parent(group.toString());
                db.store(slideshow);
                db.store(group);
            });
            if (open) {
                IWorkbenchWindow ww = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
                if (ww != null) {
                    IWorkbenchPage page = ww.getActivePage();
                    if (page != null) {
                        BasicView.openPerspective(SlideshowView.SLIDES_PERSPECTIVE);
                        try {
                            IViewPart view = page.showView(SlideshowView.ID);
                            if (view instanceof SlideshowView)
                                ((SlideshowView) view).setInput(slideshow);
                        } catch (PartInitException e) {
                            // should never happen
                        }
                    }
                }
            }
            Core.getCore().fireStructureModified();
        }
    }

    public String getKeyboardHelp() {
        return Messages.getString("SlideShowPlayer.move_mouse_for_panel"); //$NON-NLS-1$
    }

    public void create() {
        super.create();
        bottomShell = createKioskShell(Messages.getString("SlideShowPlayer.slideshow")); //$NON-NLS-1$
        bottomCanvas = new Canvas(bottomShell, SWT.DOUBLE_BUFFERED);
        bottomCanvas.addKeyListener(this);
        bottomCanvas.addMouseListener(this);
        text = new TextLayout(display);
        text.setAlignment(SWT.CENTER);
        text.setFont(JFaceResources.getFont(UiConstants.VIEWERFONT));
        createTransparentCursor();
        bottomCanvas.setCursor(transparentCursor);
        bottomCanvas.addPaintListener(new PaintListener() {
            public void paintControl(PaintEvent e) {
                Rectangle sbounds = bottomCanvas.getClientArea();
                GC gc = e.gc;
                gc.setBackground(bottomShell.getBackground());
                gc.fillRectangle(sbounds);
                gc.setForeground(bottomShell.getForeground());
                text.setWidth(sbounds.width / 2);
                Rectangle tbounds = text.getBounds();
                text.draw(gc, (sbounds.width - tbounds.width) / 2, (sbounds.height - tbounds.height) / 2);
            }
        });
        bottomShell.setData(CSSProperties.ID, CSSProperties.SLIDESHOW);
        CssActivator.getDefault().setColors(bottomShell);
        text.setText(NLS.bind(slideshow.getAdhoc() ? Messages.getString("SlideShowPlayer.preparing") //$NON-NLS-1$
                : Messages.getString("SlideShowPlayer.preparing_slideshow"), //$NON-NLS-1$
                slideshow.getName()));
        bottomShell.layout();
    }

    public void prepare(final int position) {
        Job.getJobManager().cancel(Constants.SLIDESHOW);
        requests = new LinkedBlockingQueue<SlideRequest>();
        answers = new LinkedBlockingQueue<SlideRequest>();
        done = new LinkedBlockingQueue<SlideRequest>();
        requestList = new ArrayList<SlideRequest>(slides.size());
        total = 0;
        offline = 0;
        duration = 0;
        firstSlide = -1;
        positionFirstSlide = 0;
        prepJob = new PreparationJob(position, mbounds);
        prepJob.addJobChangeListener(new JobChangeAdapter() {
            @Override
            public void done(IJobChangeEvent event) {
                try {
                    if (!display.isDisposed())
                        display.asyncExec(() -> {
                            if (!display.isDisposed() && event.getResult().isOK())
                                showInfo(position, slideshow.getName());
                        });
                } finally {
                    UiActivator.getDefault().setSlideShowRunning(false);
                }
            }
        });
        prepJob.schedule();
    }

    private static String formatPeriod(int dur) {
        int hours = dur / 3600000;
        int minutes = (dur - hours * 3600000) / 60000;
        int seconds = (dur - hours * 3600000 - minutes * 60000) / 1000;
        StringBuilder sb = new StringBuilder();
        if (hours != 0)
            sb.append(NLS.bind(Messages.getString("SlideShowPlayer.n_hours"), hours)); //$NON-NLS-1$
        if (minutes != 0 || hours != 0) {
            if (sb.length() > 0)
                sb.append(", "); //$NON-NLS-1$
            sb.append(NLS.bind(Messages.getString("SlideShowPlayer.n_minutes"), minutes)); //$NON-NLS-1$
        }
        if (sb.length() > 0)
            sb.append(", "); //$NON-NLS-1$
        sb.append(NLS.bind(Messages.getString("SlideShowPlayer.n_seconds"), seconds)); //$NON-NLS-1$
        return sb.toString();
    }

    public void open(final int position) {
        UiActivator.getDefault().setSlideShowRunning(true);
        try {
            if (bottomShell == null) {
                create();
                prepare(position);
            }
            bottomShell.open();
            if (!display.isDisposed())
                display.asyncExec(() -> {
                    if (!display.isDisposed())
                        guiLoop();
                });
        } catch (Exception e) {
            UiActivator.getDefault().setSlideShowRunning(false);
        }
    }

    private void guiLoop() {
        startTime = System.currentTimeMillis();
        bottomCanvas.setCursor(display.getSystemCursor(SWT.CURSOR_WAIT));
        while (prepJob.getState() == Job.RUNNING && !bottomShell.isDisposed()) {
            while (bottomShell != null && !bottomShell.isDisposed())
                if (!display.readAndDispatch())
                    break;
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                break;
            }
        }
        if (firstSlide >= 0)
            new MainLoopJob(firstSlide, positionFirstSlide, bottomShell, mbounds).schedule();
        while (bottomShell != null && !bottomShell.isDisposed())
            if (!display.readAndDispatch())
                display.sleep();
    }

    private void showInfo(int position, String name) {
        StringBuilder sb = new StringBuilder(512);
        sb.append(name);
        String description = slideshow.getDescription();
        if (description != null && !description.isEmpty())
            sb.append("\n\n").append(description); //$NON-NLS-1$
        sb.append("\n\n").append(NLS.bind(Messages.getString("SlideShowPlayer.n_slides"), total)); //$NON-NLS-1$ //$NON-NLS-2$
        if (firstSlide < 0)
            sb.append('\n').append(Messages.getString("SlideShowPlayer.all_images_are_offline")); //$NON-NLS-1$
        else {
            if (offline > 0) {
                sb.append('\n');
                if (offline == 1)
                    sb.append(Messages.getString("SlideShowPlayer.one_image_is_offline")); //$NON-NLS-1$
                else
                    sb.append(NLS.bind(Messages.getString("SlideShowPlayer.n_images_are_offline"), //$NON-NLS-1$
                            offline));
            }
            sb.append('\n').append(NLS.bind(Messages.getString("SlideShowPlayer.total_duration"), //$NON-NLS-1$
                    formatPeriod(duration)));
            if (position > 0)
                sb.append('\n')
                        .append(NLS.bind(Messages.getString("SlideShowPlayer.duration_from_current_position"), //$NON-NLS-1$
                                formatPeriod(duration - position)));
        }
        sb.append('\n').append(Messages.getString("SlideShowPlayer.press_esc_to_cancel")); //$NON-NLS-1$
        if (firstSlide >= 0)
            sb.append('\n').append(Messages.getString("SlideShowPlayer.move_mouse_to_obtain_control_panel")); //$NON-NLS-1$
        text.setText(sb.toString());
        text.setStyle(new TextStyle(JFaceResources.getFont(UiConstants.VIEWERTITLEFONT), null, null), 0,
                name.length());
        bottomCanvas.redraw();
    }

    public boolean close() {
        requests.add(FINALREQUEST);
        if (transferJob != null)
            Job.getJobManager().cancel(transferJob);
        Job.getJobManager().cancel(Constants.SLIDESHOW);
        if (text != null)
            text.dispose();
        if (!bottomShell.isDisposed())
            bottomShell.close();
        return super.close();
    }

    public void mouseDoubleClick(MouseEvent e) {
        closeWithPrompt();
    }

    public void mouseDown(MouseEvent e) {
        // do nothing
    }

    public void mouseUp(MouseEvent e) {
        // do nothing
    }

    public void keyPressed(KeyEvent e) {
        // do nothing
    }

    public void keyReleased(KeyEvent e) {
        if (e.character == SWT.TAB || e.keyCode == SWT.ESC)
            closeWithPrompt();
    }

    private void closeWithPrompt() {
        if (singleSlideJob != null)
            singleSlideJob.setPaused(true);
        int ret = showClosePrompt(bottomShell);
        if (ret != CLOSE_CANCEL) {
            if (ret == CLOSE_IGNORE)
                ignoreChanges = true;
            close();
        }
    }

    private int showClosePrompt(Shell shell) {
        boolean changes = false;
        for (SlideRequest request : requestList) {
            if (request.isSelected() || request.isDeleted() || request.isRemoveFromShow()) {
                changes = true;
                break;
            }
            Asset asset = request.getAsset();
            if (asset != null) {
                int rating = request.getRating();
                if (rating != RatingDialog.ABORT && rating != asset.getRating()) {
                    changes = true;
                    break;
                }
                if ((request.getRotation() - asset.getRotation()) % 360 != 0) {
                    changes = true;
                    break;
                }
            }
            SmartCollectionImpl[] albums = request.getAlbums();
            if (albums != null && albums.length > 0) {
                changes = true;
                break;
            }
            if (request.annotationChanged()) {
                changes = true;
                break;
            }
        }
        AcousticMessageDialog dialog = new AcousticMessageDialog(shell, Messages.getString("SlideShowPlayer.close"), //$NON-NLS-1$
                null, Messages.getString("SlideShowPlayer.really_close"), //$NON-NLS-1$
                MessageDialog.QUESTION, changes ? new String[] { Messages.getString("SlideShowPlayer.close_close"), //$NON-NLS-1$
                        Messages.getString("SlideShowPlayer.close_ignore"), //$NON-NLS-1$
                        Messages.getString("SlideShowPlayer.close_cancel") } //$NON-NLS-1$
                        : new String[] { Messages.getString("SlideShowPlayer.close_close"), //$NON-NLS-1$
                                Messages.getString("SlideShowPlayer.close_cancel") }, //$NON-NLS-1$
                CLOSE_CLOSE);
        dialog.create();
        int ret = dialog.open();
        return ret == (changes ? 2 : 1) ? CLOSE_CANCEL : ret;
    }

    int computeSteps(int dur) {
        return (dur + TICK - 1) / TICK;
    }

    @Override
    public boolean isDisposed() {
        return bottomShell.isDisposed();
    }

}