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); + +}