info.magnolia.ui.vaadin.layout.LazyThumbnailLayout.java Source code

Java tutorial

Introduction

Here is the source code for info.magnolia.ui.vaadin.layout.LazyThumbnailLayout.java

Source

/**
 * This file Copyright (c) 2012-2015 Magnolia International
 * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the Magnolia
 * Network Agreement and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or MNA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Magnolia Network Agreement (MNA), this file
 * and the accompanying materials are made available under the
 * terms of the MNA which accompanies this distribution, and
 * is available at http://www.magnolia-cms.com/mna.html
 *
 * Any modifications to this file must keep this entire header
 * intact.
 *
 */
package info.magnolia.ui.vaadin.layout;

import info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.connector.ThumbnailLayoutState;
import info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.rpc.ThumbnailLayoutClientRpc;
import info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.rpc.ThumbnailLayoutServerRpc;
import info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.shared.ThumbnailData;
import info.magnolia.ui.vaadin.gwt.shared.Range;
import info.magnolia.ui.vaadin.layout.data.PagingThumbnailContainer;
import info.magnolia.ui.vaadin.layout.data.ThumbnailContainer;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Lists;
import com.vaadin.data.Container;
import com.vaadin.data.Container.Ordered;
import com.vaadin.server.Resource;
import com.vaadin.ui.AbstractComponent;

/**
 * Lazy layout of asset thumbnails.
 */
public class LazyThumbnailLayout extends AbstractComponent
        implements Container.Viewer, Container.ItemSetChangeListener {

    private static Logger log = LoggerFactory.getLogger(LazyThumbnailLayout.class);

    private final List<ThumbnailSelectionListener> selectionListeners = new ArrayList<>();

    private final List<ThumbnailDblClickListener> dblClickListeners = new ArrayList<>();

    private final List<ThumbnailRightClickListener> rightClickListeners = new ArrayList<>();

    private final Set<Object> selectedIds = new HashSet<>();

    private DataProviderKeyMapper mapper = new DataProviderKeyMapper();

    private ThumbnailContainer container;

    private ThumbnailLayoutClientRpc clientRpc;

    private final ThumbnailLayoutServerRpc rpcHandler = new ThumbnailLayoutServerRpc() {

        @Override
        public void loadThumbnails(int startFrom, int length, int cachedFirst, int cachedLast) {
            final Range rangeToLoad = Range.withLength(startFrom, length);
            final Range cachedRange = Range.between(cachedFirst, cachedLast);
            final Range activeRange = cachedRange.isEmpty() ? rangeToLoad : rangeToLoad.combineWith(cachedRange);

            mapper.setActiveRange(activeRange);
            clientRpc.addThumbnails(fetchThumbnails(rangeToLoad), rangeToLoad.getStart());
        }

        @Override
        public void onThumbnailSelected(int index, boolean isMetaKeyPressed, boolean isShiftKeyPressed) {
            handleSelectionAtIndex(index, isMetaKeyPressed || isShiftKeyPressed);
            fireSelectionChange();
        }

        @Override
        public void onThumbnailDoubleClicked(int index) {
            final Object itemId = mapper.itemIdAtIndex(index);
            if (itemId != null) {
                LazyThumbnailLayout.this.onThumbnailDoubleClicked(itemId);
            }
        }

        @Override
        public void onThumbnailRightClicked(int index, int clickX, int clickY) {
            final Object itemId = mapper.itemIdAtIndex(index);
            if (itemId != null) {
                LazyThumbnailLayout.this.onThumbnailRightClicked(itemId, clickX, clickY);
            }
        }

        @Override
        public void updateOffset(int currentThumbnailOffset) {
            getState(false).offset = currentThumbnailOffset;
        }

        @Override
        public void setScaleRatio(float ratio) {
            getState(false).scaleRatio = ratio;
        }

    };

    private void handleSelectionAtIndex(int index, boolean isMultiple) {
        if (isMultiple) {
            getState().selection.toggleMultiSelection(index);
        } else {
            getState().selection.toggleSelection(index);
        }

        updateSelectedIds();
    }

    private void fireSelectionChange() {
        if (selectedIds.size() == 1) {
            this.onThumbnailSelected(selectedIds.iterator().next());
        } else {
            this.onThumbnailsSelected(selectedIds);
        }
    }

    private void updateSelectedIds() {
        selectedIds.clear();
        selectedIds.addAll(Lists.transform(getState().selection.selectedIndices, new Function<Integer, Object>() {
            @Override
            public Object apply(Integer input) {
                return container.getIdByIndex(input.intValue());
            }
        }));
    }

    public LazyThumbnailLayout() {
        setImmediate(true);
        registerRpc(rpcHandler);
        clientRpc = getRpcProxy(ThumbnailLayoutClientRpc.class);
    }

    private void onThumbnailDoubleClicked(Object itemId) {
        for (final ThumbnailDblClickListener listener : dblClickListeners) {
            listener.onThumbnailDblClicked(itemId);
        }
    }

    private void onThumbnailRightClicked(Object itemId, int clickX, int clickY) {
        for (final ThumbnailRightClickListener listener : rightClickListeners) {
            listener.onThumbnailRightClicked(itemId, clickX, clickY);
        }
    }

    private void onThumbnailsSelected(Set<Object> ids) {
        for (final ThumbnailSelectionListener listener : selectionListeners) {
            listener.onThumbnailsSelected(ids);
        }
    }

    /**
     * @deprecated since 5.3.9 - more generic {@link #onThumbnailsSelected(java.util.Set)} should be used instead.
     */
    @Deprecated
    private void onThumbnailSelected(Object itemId) {
        for (final ThumbnailSelectionListener listener : selectionListeners) {
            listener.onThumbnailSelected(itemId);
        }
    }

    private List<ThumbnailData> fetchThumbnails(Range range) {
        final List<ThumbnailData> thumbnails = new ArrayList<>(range.length());
        for (int i = range.getStart(); i < range.getEnd(); ++i) {
            final Object id = mapper.itemIdAtIndex(i);
            final Object resource = container.getThumbnailProperty(id).getValue();

            boolean isRealResource = resource instanceof Resource;
            String thumbnailId = mapper.getKey(id);
            String iconFontId = isRealResource ? null : String.valueOf(resource);
            if (isRealResource) {
                setResource(thumbnailId, (Resource) resource);
            }
            thumbnails.add(new ThumbnailData(thumbnailId, iconFontId, isRealResource));
        }
        return thumbnails;
    }

    private void setThumbnailAmount(int thumbnailAmount) {
        getState().thumbnailAmount = Math.max(thumbnailAmount, 0);
    }

    public void setThumbnailSize(int width, int height) {
        getState().size.height = height;
        getState().size.width = width;
    }

    public int getThumbnailWidth() {
        return getState(false).size.width;
    }

    public int getThumbnailHeight() {
        return getState(false).size.height;
    }

    public void refresh() {
        if (getState(false).thumbnailAmount > 0) {
            getState().resources.clear();
            mapper.clearAll();
        }

        if (container != null) {
            setThumbnailAmount(container.size());

            if (getState().offset > container.size()) {
                getState().offset = 0;
            }
        }

        synchroniseSelection();
        clientRpc.refresh();
    }

    public void addThumbnailSelectionListener(final ThumbnailSelectionListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Selection listener cannot be null!");
        }
        this.selectionListeners.add(listener);
    }

    public void addDoubleClickListener(final ThumbnailDblClickListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Double click listener cannot be null!");
        }
        this.dblClickListeners.add(listener);
    }

    public void addRightClickListener(final ThumbnailRightClickListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Right click listener cannot be null!");
        }
        this.rightClickListeners.add(listener);
    }

    @Override
    public void setContainerDataSource(Container newDataSource) {
        if (!(newDataSource instanceof ThumbnailContainer)) {
            throw new IllegalArgumentException(
                    "Container must implement info.magnolia.ui.vaadin.layout.data.ThumbnailContainer...");
        }

        if (this.container instanceof Container.ItemSetChangeNotifier) {
            ((Container.ItemSetChangeNotifier) this.container).removeItemSetChangeListener(this);
        }

        this.container = (ThumbnailContainer) newDataSource;

        if (this.container instanceof Container.ItemSetChangeNotifier) {
            ((Container.ItemSetChangeNotifier) this.container).addItemSetChangeListener(this);
        }

        refresh();

    }

    @Override
    public Ordered getContainerDataSource() {
        return container;
    }

    @Override
    protected ThumbnailLayoutState getState() {
        return (ThumbnailLayoutState) super.getState();
    }

    @Override
    protected ThumbnailLayoutState getState(boolean markAsDirty) {
        return (ThumbnailLayoutState) super.getState(markAsDirty);
    }

    public void setSelectedItemId(Object selectedItemId) {
        if (selectedItemId == null) {
            this.getState().selection.selectedIndices.clear();
        } else {
            this.getState().selection.toggleSelection(-1);
            this.getState().selection.toggleSelection(container.indexOfId(selectedItemId));
            updateSelectedIds();
        }
    }

    @Override
    public void containerItemSetChange(Container.ItemSetChangeEvent event) {
        refresh();
    }

    @Override
    public void beforeClientResponse(boolean initial) {
        super.beforeClientResponse(initial);

        getState().isFirstUpdate &= initial;
    }

    /**
     * Since the item set changed - the indices in the state might now point to the different items.
     * Since we know which items to select via {@code selectedIds}, we can update the indices in state as well.
     */
    private void synchroniseSelection() {
        final List<Integer> formerSelectedIndices = getState().selection.selectedIndices;
        getState().selection.toggleSelection(-1);

        for (Object id : formerSelectedIndices) {
            if (getContainerDataSource().containsId(id)) {
                handleSelectionAtIndex(container.indexOfId(id), true);
            }
        }

        updateSelectedIds();
        fireSelectionChange();
    }

    /**
     * Maps item ids, indices and client-side keys to each other.
     * Highly inspired by Vaadin analogous class used in Grid component implementation
     * (introduced in Vaadin 7.4).
     */
    private class DataProviderKeyMapper implements Serializable {

        private final BiMap<Integer, Object> indexToItemId = HashBiMap.create();

        private final BiMap<Object, String> itemIdToKey = HashBiMap.create();

        private Range activeRange = Range.withLength(0, 0);

        private long rollingIndex = 0;

        private DataProviderKeyMapper() {
        }

        void setActiveRange(Range newActiveRange) {

            /**
             * First update container's page size if needed - in order to avoid multiple queries to
             * the datasource.
             */
            if (container instanceof PagingThumbnailContainer) {
                ((PagingThumbnailContainer) container).setPageSize(newActiveRange.length());
            }

            final Range[] removed = activeRange.partitionWith(newActiveRange);
            final Range[] added = newActiveRange.partitionWith(activeRange);

            removeActiveThumbnails(removed[0]);
            removeActiveThumbnails(removed[2]);
            addActiveThumbnails(added[0]);
            addActiveThumbnails(added[2]);

            log.debug(
                    "Former active: {}, New Active: {}, idx-id: {}, id-key: {}. Removed: {} and {}, Added: {} and {}",
                    activeRange, newActiveRange, indexToItemId.size(), itemIdToKey.size(), removed[0], removed[2],
                    added[0], added[2]);

            activeRange = newActiveRange;

        }

        private void removeActiveThumbnails(final Range deprecated) {
            for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
                final Object itemId = indexToItemId.get(i);

                itemIdToKey.remove(itemId);
                indexToItemId.remove(i);
            }
        }

        private void addActiveThumbnails(Range added) {
            if (added.isEmpty()) {
                return;
            }

            List<?> newItemIds = container.getItemIds(added.getStart(), added.length());
            Integer index = added.getStart();
            for (Object itemId : newItemIds) {
                if (!indexToItemId.containsKey(index)) {
                    if (!itemIdToKey.containsKey(itemId)) {
                        itemIdToKey.put(itemId, nextKey());
                    }

                    indexToItemId.forcePut(index, itemId);
                }
                index++;
            }
        }

        private String nextKey() {
            return String.valueOf(rollingIndex++);
        }

        String getKey(Object itemId) {
            String key = itemIdToKey.get(itemId);
            if (key == null) {
                key = nextKey();
                itemIdToKey.put(itemId, key);
            }
            return key;
        }

        public Object getItemId(String key) throws IllegalStateException {
            Object itemId = itemIdToKey.inverse().get(key);
            if (itemId != null) {
                return itemId;
            } else {
                throw new IllegalStateException("No item id for key " + key + " found.");
            }
        }

        public Collection<Object> getItemIds(Collection<String> keys) throws IllegalStateException {
            if (keys == null) {
                throw new IllegalArgumentException("keys may not be null");
            }

            final List<Object> itemIds = new ArrayList<>(keys.size());
            for (String key : keys) {
                itemIds.add(getItemId(key));
            }
            return itemIds;
        }

        Object itemIdAtIndex(int index) {
            return indexToItemId.get(index);
        }

        int indexOf(Object itemId) {
            return indexToItemId.inverse().get(itemId);
        }

        public void clearAll() {
            indexToItemId.clear();
            itemIdToKey.clear();
            rollingIndex = 0;
            activeRange = Range.withLength(0, 0);
        }
    }

    /**
     * Listener interface for thumbnail selection.
     */
    public interface ThumbnailSelectionListener {

        /**
         * @deprecated since 5.3.9 - more generic {@link #onThumbnailsSelected(java.util.Set)} should be used.
         */
        @Deprecated
        void onThumbnailSelected(Object itemId);

        void onThumbnailsSelected(Set<Object> ids);

    }

    /**
     * Listener for thumbnail double clicks.
     */
    public interface ThumbnailDblClickListener {
        void onThumbnailDblClicked(Object itemId);
    }

    /**
     * Listener for thumbnail right clicks.
     */
    public interface ThumbnailRightClickListener {
        void onThumbnailRightClicked(Object itemId, int clickX, int clickY);
    }
}