diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LazyImageSeriesFrame.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LazyImageSeriesFrame.java
index a0d622ad3f917bb0a49566c179f54a68163e38b2..b38e37482dd46ddb7bce2605d67902631ddf1bd7 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LazyImageSeriesFrame.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LazyImageSeriesFrame.java
@@ -16,6 +16,7 @@
 
 package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import com.extjs.gxt.ui.client.widget.Label;
@@ -59,7 +60,10 @@ public class LazyImageSeriesFrame extends LayoutContainer
 
     private boolean needsImageDownload;
 
-    private ImagesDownloadListener imagesDownloadListener;
+    private boolean imagesDownloaded;
+
+    private List<ImagesDownloadListener> imagesDownloadListeners =
+            new ArrayList<ImagesDownloadListener>();
 
     private LogicalImageClickHandler imageClickHandler;
 
@@ -84,6 +88,11 @@ public class LazyImageSeriesFrame extends LayoutContainer
         return needsImageDownload;
     }
 
+    public boolean areImagesDownloaded()
+    {
+        return imagesDownloaded;
+    }
+
     public synchronized void downloadImagesFromServer()
     {
         if (false == needsImageDownload)
@@ -131,9 +140,16 @@ public class LazyImageSeriesFrame extends LayoutContainer
                 public void imageLoaded(FitImageLoadEvent event)
                 {
                     tilesDownloaded++;
-                    if (tilesDownloaded >= totalTilesToDownload && imagesDownloadListener != null)
+                    if (tilesDownloaded >= totalTilesToDownload)
                     {
-                        imagesDownloadListener.imagesDownloaded(LazyImageSeriesFrame.this);
+                        imagesDownloaded = true;
+                        if (imagesDownloadListeners != null)
+                        {
+                            for (ImagesDownloadListener listener : imagesDownloadListeners)
+                            {
+                                listener.imagesDownloaded(LazyImageSeriesFrame.this);
+                            }
+                        }
                     }
                 }
             };
@@ -203,9 +219,9 @@ public class LazyImageSeriesFrame extends LayoutContainer
         container.add(dummy);
     }
 
-    public void setImagesDownloadListener(ImagesDownloadListener imagesDownloadListener)
+    public void addImagesDownloadListener(ImagesDownloadListener imagesDownloadListener)
     {
-        this.imagesDownloadListener = imagesDownloadListener;
+        this.imagesDownloadListeners.add(imagesDownloadListener);
     }
 
     public void setImageClickHandler(LogicalImageClickHandler imageClickHandler)
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesGrid.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesGrid.java
index d232bfa0d3a69f1e47c3a2b307ea8f0cc9a086d7..8dc957acdb661421a2b2b098ed5df259ab922628 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesGrid.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesGrid.java
@@ -32,6 +32,7 @@ import com.extjs.gxt.ui.client.widget.Label;
 import com.extjs.gxt.ui.client.widget.LayoutContainer;
 import com.extjs.gxt.ui.client.widget.Slider;
 import com.extjs.gxt.ui.client.widget.layout.HBoxLayout;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Widget;
 
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils;
@@ -106,7 +107,7 @@ class LogicalImageSeriesGrid extends LayoutContainer
                     currentFrameIndex =
                             (timeSlider.getValue() - 1) * numberOfDepthLevels
                                     + (depthSlider.getValue() - 1);
-                    imageDownloader.frameSelectionChanged(oldIndex, currentFrameIndex);
+                    imageDownloader.frameSelectionChanged(oldIndex, currentFrameIndex, null);
                     int timeSliderValue = timeSlider.getValue();
                     int depthSliderValue = depthSlider.getValue();
                     setSliderLabels(model, timeSliderLabel, timeSliderValue, depthSliderLabel,
@@ -141,25 +142,21 @@ class LogicalImageSeriesGrid extends LayoutContainer
         final List<ImageSeriesPoint> sortedPoints = model.getSortedPoints();
         final LayoutContainer mainContainer = createMainContainer(imageDownloader);
 
-        Listener<SliderEvent> listener = new Listener<SliderEvent>()
-            {
-                public void handleEvent(SliderEvent e)
-                {
-                    int oldValue = e.getOldValue();
-                    int newValue = e.getNewValue();
-                    imageDownloader.frameSelectionChanged(oldValue - 1, newValue - 1);
-                    removeFirstItem(mainContainer);
-                    mainContainer.insert(createSeriesPointLabel(sortedPoints, newValue), 0);
-                    mainContainer.layout();
-                }
-
-            };
-        final Slider slider = createSlider(model.getSortedPoints().size());
-        slider.addListener(Events.Change, listener);
+        MovieButtonsWithSlider buttonsWithSlider =
+                new MovieButtonsWithSlider(model.getSortedPoints().size())
+                    {
+                        protected void loadFrame(int frame, AsyncCallback<Void> callback)
+                        {
+                            imageDownloader.frameSelectionChanged(frame, callback);
+                            removeFirstItem(mainContainer);
+                            mainContainer
+                                    .insert(createSeriesPointLabel(sortedPoints, frame + 1), 0);
+                            mainContainer.layout();
+                        }
+                    };
 
         mainContainer.add(createSeriesPointLabel(sortedPoints, 1));
-        mainContainer.add(slider);
-
+        mainContainer.add(buttonsWithSlider);
         return mainContainer;
     }
 
@@ -536,7 +533,9 @@ class LogicalImageSeriesGrid extends LayoutContainer
 
         private boolean keepDownloading;
 
-        private int selectedFrameIndex = -1;
+        private int selectedFrameIndex = 0;
+
+        private int shownFrameIndex = 0;
 
         private ImagesDownloadListener imageDownloadListener;
 
@@ -558,7 +557,7 @@ class LogicalImageSeriesGrid extends LayoutContainer
 
             if (!frames.isEmpty())
             {
-                frames.get(0).setImagesDownloadListener(imageDownloadListener);
+                frames.get(0).addImagesDownloadListener(imageDownloadListener);
             }
 
             for (int i = 0; i < numFrames; i++)
@@ -578,22 +577,61 @@ class LogicalImageSeriesGrid extends LayoutContainer
             keepDownloading = false;
         }
 
-        public void frameSelectionChanged(int oldSelectionIndex, int newSelectionIndex)
+        public void frameSelectionChanged(int newSelectionIndex, AsyncCallback<Void> callback)
+        {
+            frameSelectionChanged(selectedFrameIndex, newSelectionIndex, callback);
+        }
+
+        public void frameSelectionChanged(final int oldSelectionIndex, final int newSelectionIndex,
+                final AsyncCallback<Void> callback)
         {
-            if (oldSelectionIndex >= 0)
+            // do nothing when the requested frame is already selected
+            if (newSelectionIndex == selectedFrameIndex)
             {
-                frames.get(oldSelectionIndex).hide();
+                if (callback != null)
+                {
+                    callback.onSuccess(null);
+                }
+                return;
             }
 
+            final LazyImageSeriesFrame newSelectedFrame = frames.get(newSelectionIndex);
+
             selectedFrameIndex = newSelectionIndex;
-            LazyImageSeriesFrame selectedFrame = frames.get(selectedFrameIndex);
-            selectedFrame.show();
+
+            if (callback != null)
+            {
+                ImagesDownloadListener listener = new ImagesDownloadListener()
+                    {
+                        public void imagesDownloaded(LazyImageSeriesFrame frame)
+                        {
+                            // do not display the frame if selection changed during loading
+                            if (newSelectionIndex == selectedFrameIndex)
+                            {
+                                LazyImageSeriesFrame shownFrame = frames.get(shownFrameIndex);
+                                shownFrameIndex = newSelectionIndex;
+                                shownFrame.hide();
+                                newSelectedFrame.show();
+                            }
+                            callback.onSuccess(null);
+                        }
+                    };
+
+                if (newSelectedFrame.areImagesDownloaded())
+                {
+                    listener.imagesDownloaded(newSelectedFrame);
+                } else
+                {
+                    newSelectedFrame.addImagesDownloadListener(listener);
+                }
+            }
 
             if (false == fullDownloadStarted)
             {
                 fullDownloadStarted = true;
                 scheduleDownloadNextChunkOfImages();
             }
+
         }
 
         private void scheduleDownloadNextChunkOfImages()
@@ -650,7 +688,7 @@ class LogicalImageSeriesGrid extends LayoutContainer
 
             for (LazyImageSeriesFrame frame : framesToDownload)
             {
-                frame.setImagesDownloadListener(downloadListener);
+                frame.addImagesDownloadListener(downloadListener);
                 frame.downloadImagesFromServer();
             }
         }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MovieButtons.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MovieButtons.java
new file mode 100644
index 0000000000000000000000000000000000000000..835159b63e008df61c3b6ee61f6496b5ab56bd7e
--- /dev/null
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MovieButtons.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
+
+import com.extjs.gxt.ui.client.event.ButtonEvent;
+import com.extjs.gxt.ui.client.event.SelectionListener;
+import com.extjs.gxt.ui.client.widget.button.Button;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Panel;
+
+/**
+ * @author pkupczyk
+ */
+public abstract class MovieButtons extends Composite
+{
+
+    private int currentFrame;
+
+    private int numberOfFrames;
+
+    private MovieButtonsView view;
+
+    private MovieButtonsState state;
+
+    public MovieButtons(int numberOfFrames)
+    {
+        this.currentFrame = 0;
+        this.numberOfFrames = numberOfFrames;
+        initView();
+        initState();
+    }
+
+    private void initView()
+    {
+        view = new MovieButtonsView();
+
+        view.addPlayListener(new SelectionListener<ButtonEvent>()
+            {
+                public void componentSelected(ButtonEvent event)
+                {
+                    handlePlay();
+                }
+            });
+        view.addStopListener(new SelectionListener<ButtonEvent>()
+            {
+                public void componentSelected(ButtonEvent event)
+                {
+                    handleStop();
+                }
+            });
+        view.addPreviousListener(new SelectionListener<ButtonEvent>()
+            {
+                public void componentSelected(ButtonEvent event)
+                {
+                    handlePrevious();
+                }
+            });
+        view.addNextListener(new SelectionListener<ButtonEvent>()
+            {
+                public void componentSelected(ButtonEvent event)
+                {
+                    handleNext();
+                }
+            });
+
+        initWidget(view);
+    }
+
+    private void initState()
+    {
+        setState(new MovieButtonsStoppedState());
+    }
+
+    private MovieButtonsState getState()
+    {
+        return state;
+    }
+
+    private void setState(MovieButtonsState newState)
+    {
+        state = newState;
+        state.init();
+    }
+
+    public int getFrame()
+    {
+        return state.handleGetFrame();
+    }
+
+    public void setFrame(int frame)
+    {
+        state.handleSetFrame(frame);
+    }
+
+    private boolean isFirstFrame()
+    {
+        return getFrame() == 0;
+    }
+
+    private boolean isLastFrame()
+    {
+        return getFrame() == (numberOfFrames - 1);
+    }
+
+    protected abstract void loadFrame(int frame, AsyncCallback<Void> callback);
+
+    private void loadFrame(int frame)
+    {
+        loadFrame(frame, new AsyncCallback<Void>()
+            {
+                public void onSuccess(Void result)
+                {
+                }
+
+                public void onFailure(Throwable caught)
+                {
+                }
+            });
+    }
+
+    private void handlePlay()
+    {
+        state.handlePlay();
+    }
+
+    private void handleStop()
+    {
+        state.handleStop();
+    }
+
+    private void handlePrevious()
+    {
+        state.handlePrevious();
+    }
+
+    private void handleNext()
+    {
+        state.handleNext();
+    }
+
+    private class MovieButtonsView extends Composite
+    {
+        private Button playButton;
+
+        private Button stopButton;
+
+        private Button previousButton;
+
+        private Button nextButton;
+
+        public MovieButtonsView()
+        {
+            playButton = new Button("Play");
+            stopButton = new Button("Stop");
+            previousButton = new Button("<<");
+            nextButton = new Button(">>");
+
+            Panel panel = new HorizontalPanel();
+            panel.setStyleName("movieButtons");
+            panel.add(playButton);
+            panel.add(stopButton);
+            panel.add(previousButton);
+            panel.add(nextButton);
+
+            initWidget(panel);
+        }
+
+        public void addPlayListener(SelectionListener<ButtonEvent> listener)
+        {
+            playButton.addSelectionListener(listener);
+        }
+
+        public void addStopListener(SelectionListener<ButtonEvent> listener)
+        {
+            stopButton.addSelectionListener(listener);
+        }
+
+        public void addPreviousListener(SelectionListener<ButtonEvent> listener)
+        {
+            previousButton.addSelectionListener(listener);
+        }
+
+        public void addNextListener(SelectionListener<ButtonEvent> listener)
+        {
+            nextButton.addSelectionListener(listener);
+        }
+
+        public void setPlayEnabled(boolean enabled)
+        {
+            playButton.setEnabled(enabled);
+        }
+
+        public void setStopEnabled(boolean enabled)
+        {
+            stopButton.setEnabled(enabled);
+        }
+
+        public void setPreviousEnabled(boolean enabled)
+        {
+            previousButton.setEnabled(enabled);
+        }
+
+        public void setNextEnabled(boolean enabled)
+        {
+            nextButton.setEnabled(enabled);
+        }
+
+    }
+
+    private interface MovieButtonsState
+    {
+        public void init();
+
+        public void handlePlay();
+
+        public void handleStop();
+
+        public void handlePrevious();
+
+        public void handleNext();
+
+        public int handleGetFrame();
+
+        public void handleSetFrame(int frame);
+    }
+
+    private class MovieButtonsStoppedState implements MovieButtonsState
+    {
+
+        public void init()
+        {
+            view.setPlayEnabled(true);
+            view.setStopEnabled(false);
+            view.setPreviousEnabled(!isFirstFrame());
+            view.setNextEnabled(!isLastFrame());
+        }
+
+        public void handlePlay()
+        {
+            if (isLastFrame())
+            {
+                setFrame(0);
+            }
+            setState(new MovieButtonsPlayingState());
+        }
+
+        public void handleStop()
+        {
+            // do nothing
+        }
+
+        public void handlePrevious()
+        {
+            if (!isFirstFrame())
+            {
+                setFrame(getFrame() - 1);
+            }
+        }
+
+        public void handleNext()
+        {
+            if (!isLastFrame())
+            {
+                setFrame(getFrame() + 1);
+            }
+        }
+
+        public int handleGetFrame()
+        {
+            return currentFrame;
+        }
+
+        public void handleSetFrame(int frame)
+        {
+            currentFrame = frame;
+            view.setPreviousEnabled(!isFirstFrame());
+            view.setNextEnabled(!isLastFrame());
+            loadFrame(frame);
+        }
+    }
+
+    private class MovieButtonsPlayingState implements MovieButtonsState
+    {
+
+        private static final int PREFFERED_DELAY_BETWEEN_FRAMES_IN_MILLIS = 100;
+
+        public void init()
+        {
+            view.setPlayEnabled(false);
+            view.setStopEnabled(true);
+            view.setPreviousEnabled(true);
+            view.setNextEnabled(true);
+            loadNextFrame(1);
+        }
+
+        public void handlePlay()
+        {
+            // do nothing
+        }
+
+        public void handleStop()
+        {
+            setState(new MovieButtonsStoppedState());
+        }
+
+        public void handlePrevious()
+        {
+            handleStop();
+        }
+
+        public void handleNext()
+        {
+            handleStop();
+        }
+
+        public int handleGetFrame()
+        {
+            return currentFrame;
+        }
+
+        public void handleSetFrame(int frame)
+        {
+            currentFrame = frame;
+        }
+
+        private void loadNextFrame(int delay)
+        {
+            Timer timer = new Timer()
+                {
+                    public void run()
+                    {
+                        final long startTime = System.currentTimeMillis();
+
+                        if (isLastFrame())
+                        {
+                            handleStop();
+                            setFrame(0);
+                        } else
+                        {
+                            setFrame(getFrame() + 1);
+
+                            loadFrame(getFrame(), new AsyncCallback<Void>()
+                                {
+
+                                    public void onSuccess(Void result)
+                                    {
+                                        if (MovieButtonsPlayingState.this == getState())
+                                        {
+                                            int currentDelay =
+                                                    (int) (System.currentTimeMillis() - startTime);
+
+                                            if (currentDelay < PREFFERED_DELAY_BETWEEN_FRAMES_IN_MILLIS)
+                                            {
+                                                loadNextFrame(PREFFERED_DELAY_BETWEEN_FRAMES_IN_MILLIS
+                                                        - currentDelay);
+                                            } else
+                                            {
+                                                loadNextFrame(1);
+                                            }
+                                        }
+                                    }
+
+                                    public void onFailure(Throwable caught)
+                                    {
+                                        onSuccess(null);
+                                    }
+
+                                });
+                        }
+                    }
+                };
+            timer.schedule(delay);
+        }
+    }
+
+}
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MovieButtonsWithSlider.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MovieButtonsWithSlider.java
new file mode 100644
index 0000000000000000000000000000000000000000..a844f8d6af8d5d05507727284d533458755099d7
--- /dev/null
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/MovieButtonsWithSlider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
+
+import com.extjs.gxt.ui.client.event.Events;
+import com.extjs.gxt.ui.client.event.Listener;
+import com.extjs.gxt.ui.client.event.SliderEvent;
+import com.extjs.gxt.ui.client.widget.Slider;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+/**
+ * @author pkupczyk
+ */
+public abstract class MovieButtonsWithSlider extends Composite
+{
+
+    private MovieButtons buttons;
+
+    private Slider slider;
+
+    public MovieButtonsWithSlider(int numberOfFrames)
+    {
+        buttons = new MovieButtons(numberOfFrames)
+            {
+                protected void loadFrame(int frame, AsyncCallback<Void> callback)
+                {
+                    MovieButtonsWithSlider.this.loadFrame(frame, callback);
+                    slider.setValue(frame + 1, true);
+                }
+            };
+
+        slider = new Slider();
+        // we do not want the slider to be long when there are just few points
+        slider.setWidth(Math.min(230, Math.max(100, numberOfFrames * 10)));
+        slider.setIncrement(1);
+        slider.setMinValue(1);
+        slider.setMaxValue(numberOfFrames);
+        slider.setClickToChange(true);
+        slider.setUseTip(false);
+        slider.addListener(Events.Change, new Listener<SliderEvent>()
+            {
+                public void handleEvent(SliderEvent be)
+                {
+                    buttons.setFrame(be.getNewValue() - 1);
+                    MovieButtonsWithSlider.this.loadFrame(be.getNewValue() - 1,
+                            new AsyncCallback<Void>()
+                                {
+                                    public void onSuccess(Void result)
+                                    {
+                                    }
+
+                                    public void onFailure(Throwable caught)
+                                    {
+                                    }
+                                });
+                };
+            });
+
+        Panel panel = new VerticalPanel();
+        panel.add(slider);
+        panel.add(buttons);
+        initWidget(panel);
+    }
+
+    protected abstract void loadFrame(int frame, AsyncCallback<Void> callback);
+
+}