/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.wui.client.common.lists.pagination; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.roda.core.data.exceptions.NotFoundException; import org.roda.core.data.v2.common.Pair; import org.roda.core.data.v2.index.IndexResult; import org.roda.core.data.v2.index.IsIndexed; import org.roda.core.data.v2.index.facet.Facets; import org.roda.core.data.v2.index.filter.Filter; import org.roda.core.data.v2.index.sort.Sorter; import org.roda.core.data.v2.index.sublist.Sublist; import org.roda.wui.client.browse.BrowserService; import org.roda.wui.client.common.lists.utils.AsyncTableCell; import org.roda.wui.client.common.utils.AsyncCallbackUtils; import org.roda.wui.client.common.utils.HtmlSnippetUtils; import org.roda.wui.common.client.tools.HistoryUtils; import org.roda.wui.common.client.widgets.Toast; import com.github.nmorel.gwtjackson.client.exception.JsonDeserializationException; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.HasClickHandlers; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.storage.client.Storage; import com.google.gwt.storage.client.StorageMap; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionChangeEvent.Handler; import config.i18n.client.ClientMessages; public class ListSelectionUtils { private static final ClientMessages messages = GWT.create(ClientMessages.class); private static final String STORAGE_PREFIX = "ListSelectionState.Clipboard."; private static final Storage storage = Storage.getLocalStorageIfSupported(); private static Map<String, ListSelectionState<?>> clipboard = new HashMap<>(); static { loadClipboardOnStorage(); } private ListSelectionUtils() { super(); } private static <T extends IsIndexed> void loadClipboardOnStorage() { if (storage != null) { StorageMap storageMap = new StorageMap(storage); for (Entry<String, String> entry : storageMap.entrySet()) { if (entry.getKey().startsWith(STORAGE_PREFIX)) { String className = entry.getKey().substring(STORAGE_PREFIX.length()); try { ListSelectionState<T> state = ListSelectionStateMappers.getObject(className, entry.getValue()); clipboard.put(className, state); } catch (JsonDeserializationException e) { // do nothing } } } } } private static <T extends IsIndexed> void saveOnStorage(String className, ListSelectionState<T> state) { if (storage != null) { storage.setItem(STORAGE_PREFIX + className, ListSelectionStateMappers.getJson(className, state)); } } public static <T extends IsIndexed> ListSelectionState<T> create(T selected, Filter filter, Boolean justActive, Facets facets, Sorter sorter, Integer index) { return new ListSelectionState<>(selected, filter, justActive, facets, sorter, index); } @FunctionalInterface public interface ProcessRelativeItem<T> { void process(T object); } private static <T extends IsIndexed> void openRelative(final ListSelectionState<T> state, final int relativeIndex, final AsyncCallback<ListSelectionState<T>> callback, final ProcessRelativeItem<T> processor) { final int newIndex = state.getIndex() + relativeIndex; if (newIndex >= 0) { BrowserService.Util.getInstance().find(state.getSelected().getClass().getName(), state.getFilter(), state.getSorter(), new Sublist(newIndex, 1), state.getFacets(), LocaleInfo.getCurrentLocale().getLocaleName(), state.getJustActive(), new ArrayList<String>(), new AsyncCallback<IndexResult<T>>() { @Override public void onFailure(Throwable caught) { callback.onFailure(caught); } @Override public void onSuccess(IndexResult<T> result) { if (!result.getResults().isEmpty()) { T first = result.getResults().get(0); // if we are jumping to the same file, try the next one if (first.getUUID().equals(state.getSelected().getUUID())) { openRelative(state, relativeIndex < 0 ? relativeIndex - 1 : relativeIndex + 1, callback, processor); } else { processor.process(first); callback.onSuccess(ListSelectionUtils.create(first, state.getFilter(), state.getJustActive(), state.getFacets(), state.getSorter(), newIndex)); } } else { callback.onFailure(new NotFoundException("No items were found")); } } }); } } public static <T extends IsIndexed> void save(final ListSelectionState<T> state) { String className = state.getSelected().getClass().getName(); clipboard.put(className, state); saveOnStorage(className, state); } @SuppressWarnings("unchecked") public static <T extends IsIndexed> ListSelectionState<T> last(Class<T> objectClass) { return (ListSelectionState<T>) clipboard.get(objectClass.getName()); } public static <T extends IsIndexed> boolean hasLast(Class<T> objectClass) { return clipboard.containsKey(objectClass.getName()); } public static <T extends IsIndexed> void jump(final T object, int relativeIndex) { jump(object, relativeIndex, new ProcessRelativeItem<T>() { @Override public void process(T object) { HistoryUtils.resolve(object); } }); } public static <T extends IsIndexed> void jump(final T object, final int relativeIndex, final ProcessRelativeItem<T> processor) { @SuppressWarnings("unchecked") ListSelectionState<T> last = last((Class<T>) object.getClass()); if (last != null) { if (last.getSelected().getUUID().equals(object.getUUID())) { AsyncCallback<ListSelectionState<T>> callback = new AsyncCallback<ListSelectionState<T>>() { @Override public void onFailure(Throwable caught) { if (caught instanceof NotFoundException) { if (relativeIndex > 0) { Toast.showInfo(messages.cannotJumpToNext(), messages.cannotJumpToNextDescription()); } else { Toast.showInfo(messages.cannotJumpToPrevious(), messages.cannotJumpToPreviousDescription()); } } else { AsyncCallbackUtils.defaultFailureTreatment(caught); } } @Override public void onSuccess(ListSelectionState<T> newState) { save(newState); } }; openRelative(last, relativeIndex, callback, processor); } } } public static <T extends IsIndexed> void previous(final T object) { jump(object, -1); } public static <T extends IsIndexed> void next(T object) { jump(object, +1); } public static <T extends IsIndexed> void previous(final T object, ProcessRelativeItem<T> processor) { jump(object, -1, processor); } public static <T extends IsIndexed> void next(T object, ProcessRelativeItem<T> processor) { jump(object, +1, processor); } public static <T extends IsIndexed> void hasPreviousOrNext(final T object, final AsyncCallback<Pair<Boolean, Boolean>> callback) { @SuppressWarnings("unchecked") Class<T> objectClass = (Class<T>) object.getClass(); final ListSelectionState<T> last = last(objectClass); if (last != null) { if (last.getSelected().getUUID().equals(object.getUUID())) { BrowserService.Util.getInstance().count(objectClass.getName(), last.getFilter(), last.getJustActive(), new AsyncCallback<Long>() { @Override public void onFailure(Throwable caught) { callback.onFailure(caught); } @Override public void onSuccess(Long totalCount) { Integer lastIndex = last.getIndex(); Boolean hasPrevious = lastIndex > 0; Boolean hasNext = lastIndex < totalCount - 1; callback.onSuccess(Pair.of(hasPrevious, hasNext)); } }); } else { callback.onSuccess(Pair.of(Boolean.FALSE, Boolean.FALSE)); } } else { callback.onSuccess(Pair.of(Boolean.FALSE, Boolean.FALSE)); } } public static <T extends IsIndexed> void bindBrowseOpener(final AsyncTableCell<T, ?> list) { list.getSelectionModel().addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { ListSelectionState<T> selectionState = list.getListSelectionState(); if (selectionState != null) { save(selectionState); HistoryUtils.resolve(selectionState.getSelected()); } } }); } public static <T extends IsIndexed> void bindLayout(final T object, final HasClickHandlers previousButton, final HasClickHandlers nextButton, final FocusPanel keyboardFocus, final boolean requireControlKeyModifier, final boolean requireShiftKeyModifier, final boolean requireAltKeyModifier, UIObject... extraUiObjectsToHide) { bindLayout(object, previousButton, nextButton, keyboardFocus, requireControlKeyModifier, requireShiftKeyModifier, requireAltKeyModifier, new ProcessRelativeItem<T>() { @Override public void process(T object) { HistoryUtils.resolve(object); } }, extraUiObjectsToHide); } public static <T extends IsIndexed> void bindLayout(final T object, final HasClickHandlers previousButton, final HasClickHandlers nextButton, final FocusPanel keyboardFocus, final boolean requireControlKeyModifier, final boolean requireShiftKeyModifier, final boolean requireAltKeyModifier, final ProcessRelativeItem<T> processor, final UIObject... extraUiObjectsToHide) { StringBuilder b = new StringBuilder(); if (requireControlKeyModifier) { b.append("CTRL + "); } if (requireShiftKeyModifier) { b.append("SHIFT + "); } if (requireAltKeyModifier) { b.append("ALT + "); } // TODO add HTML entities, icons or i18n if (previousButton instanceof UIObject && nextButton instanceof UIObject) { ((UIObject) previousButton).setTitle(b + "LEFT"); ((UIObject) nextButton).setTitle(b + "RIGHT"); } previousButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { previous(object, processor); } }); nextButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { next(object, processor); } }); keyboardFocus.addKeyDownHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { boolean controlModifier = !requireControlKeyModifier || event.isControlKeyDown(); boolean shiftModifier = !requireShiftKeyModifier || event.isShiftKeyDown(); boolean altModifier = !requireAltKeyModifier || event.isAltKeyDown(); if (controlModifier && shiftModifier && altModifier) { NativeEvent ne = event.getNativeEvent(); if (ne.getKeyCode() == KeyCodes.KEY_RIGHT) { ne.preventDefault(); next(object, processor); } else if (ne.getKeyCode() == KeyCodes.KEY_LEFT) { ne.preventDefault(); previous(object, processor); } } } }); updateLayout(object, previousButton, nextButton, extraUiObjectsToHide); } public static <T extends IsIndexed> void updateLayout(final T object, final HasClickHandlers previousButton, final HasClickHandlers nextButton, final UIObject... extraUiObjectsToHide) { hasPreviousOrNext(object, new AsyncCallback<Pair<Boolean, Boolean>>() { @Override public void onFailure(Throwable caught) { AsyncCallbackUtils.defaultFailureTreatment(caught); if (previousButton instanceof UIObject && nextButton instanceof UIObject) { ((UIObject) previousButton).setVisible(false); ((UIObject) nextButton).setVisible(false); } } @Override public void onSuccess(Pair<Boolean, Boolean> result) { Boolean hasPrevious = result.getFirst(); Boolean hasNext = result.getSecond(); // visibility if (previousButton instanceof UIObject && nextButton instanceof UIObject) { ((UIObject) previousButton).setVisible(hasPrevious || hasNext); ((UIObject) nextButton).setVisible(hasPrevious || hasNext); for (UIObject uiObj : extraUiObjectsToHide) { uiObj.setVisible(hasPrevious || hasNext); } if (previousButton instanceof FocusWidget && nextButton instanceof FocusWidget) { ((FocusWidget) previousButton).setEnabled(hasPrevious); ((FocusWidget) nextButton).setEnabled(hasNext); } else { HtmlSnippetUtils.setCssClassDisabled((UIObject) previousButton, !hasPrevious); HtmlSnippetUtils.setCssClassDisabled((UIObject) nextButton, !hasNext); } } } }); } }