org.chromium.debug.ui/src/org/chromium/debug/ui/DialogUtils.java
author Eugene Ostroukhov <eugeneo@symbian.org>
Mon, 07 Jun 2010 16:51:19 -0700
changeset 355 8726e95bcbba
permissions -rw-r--r--
Initial commit of updated Chrome Java SDK

// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.ui;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Shell;

/**
 * A small set of utility classes that help programming dialog window logic.
 */
public class DialogUtils {
  /*
   * Part 1. Update graph.
   *
   * All logical elements of dialog and dependencies between them are modeled as DAG.
   * The data flows from vertices that corresponds to various input fields through
   * some transformations to the terminal vertices that are in-dialog helper messages and OK button.
   *
   * A linear data flow (no forks, no merges) is suitable for data transformation and
   * is programmed manually. Forks and merges are hard to dispatch manually and are managed by
   * class Updater.
   *
   * Updater knows about "source" vertices that may have several outgoing edges and about
   * "consumer" vertices that may have several incoming edges. Based on source vertex changes
   * updater updates consumer vertices in topological order.
   */

  /**
   * Represents source vertex for Updater. Technically updater uses this interface only as a flag
   * interface, because the only methods it uses are {@link Object#equals}/Object{@link #hashCode}.
   */
  public interface ValueSource<T> {
    /**
     * Method is not used by updater, for convenience only.
     * @return current value of the vertex
     */
    T getValue();
  }

  /**
   * Represents consumer vertex for Updater. Each instance should be explicitly registered in
   * Updater.
   */
  public interface ValueConsumer {
    /**
     * Called by updater when some linked sources have changed and it's time this vertex
     * got updated. {@link Updater#reportChanged} may be called if some {@line ValueSource}s have
     * changed during this update (but this should not break topological order of the graph).
     */
    void update(Updater updater);
  }

  /**
   * Helps to conduct update for vertices in a value graph.
   * Technically Updater does not see a real graph, because it doesn't support vertices
   * that are simultaneously source and consumer. Programmer helps manage other edges manually by
   * calling {@link #reportChanged} method.
   */
  public static class Updater {
    private final LinkedHashMap<ValueConsumer, Boolean> needsUpdateMap =
        new LinkedHashMap<ValueConsumer, Boolean>();
    private final Map<ValueSource<?>, List<ValueConsumer>> reversedDependenciesMap =
        new HashMap<ValueSource<?>, List<ValueConsumer>>();
    private boolean alreadyUpdating = false;

    public void addConsumer(ValueConsumer value, ValueSource<?> ... dependencies) {
      addConsumer(value, Arrays.asList(dependencies));
    }

    /**
     * Registers a consumer vertex with all its dependencies.
     */
    public void addConsumer(ValueConsumer value, List<? extends ValueSource<?>> dependencies) {
      Boolean res = needsUpdateMap.put(value, Boolean.FALSE);
      if (res != null) {
        throw new IllegalArgumentException("Already added"); //$NON-NLS-1$
      }
      for (ValueSource<?> dep : dependencies) {
        List<ValueConsumer> reversedDeps = reversedDependenciesMap.get(dep);
        if (reversedDeps == null) {
          reversedDeps = new ArrayList<ValueConsumer>(2);
          reversedDependenciesMap.put(dep, reversedDeps);
        }
        reversedDeps.add(value);
      }
    }

    /**
     * Reports about sources that have been changed and plans future update of consumers. This
     * method may be called at any time (it is not thread-safe though).
     */
    public void reportChanged(ValueSource<?> source) {
      List<ValueConsumer> reversedDeps = reversedDependenciesMap.get(source);
      if (reversedDeps != null) {
        for (ValueConsumer consumer : reversedDeps) {
          needsUpdateMap.put(consumer, Boolean.TRUE);
        }
      }
    }

    /**
     * Performs update of all vertices that need it. If some sources are reported changed
     * during the run of this method, their consumers are also updated.
     */
    public void update() {
      if (alreadyUpdating) {
        return;
      }
      alreadyUpdating = true;
      try {
        updateImpl();
      } finally {
        alreadyUpdating = false;
      }
    }

    private void updateImpl() {
      boolean hasChanges = true;
      while (hasChanges) {
        hasChanges = false;
        for (Map.Entry<ValueConsumer, Boolean> en : needsUpdateMap.entrySet()) {
          if (en.getValue() == Boolean.TRUE) {
            en.setValue(Boolean.FALSE);
            ValueConsumer currentValue = en.getKey();
            currentValue.update(this);
          }
        }
      }
    }

    /**
     * Updates all consumer vertices in graph.
     */
    public void updateAll() {
      for (Map.Entry<?, Boolean> en : needsUpdateMap.entrySet()) {
        en.setValue(Boolean.TRUE);
      }
      update();
    }
  }

  /**
   * A basic implementation of object that is both consumer and source. Updater will treat
   * as 2 separate objects.
   */
  public static abstract class ValueProcessor<T> implements ValueConsumer, ValueSource<T> {
    private T currentValue = null;
    public T getValue() {
      return currentValue;
    }
    protected void setCurrentValue(T currentValue) {
      this.currentValue = currentValue;
    }
  }

  /*
   * Part 2. Optional data type etc
   *
   * Since dialog should deal with error user entry, the typical data type is either value or error.
   * This is implemented as Optional interface. Most of data transformations should work only
   * when all inputs are non-error and generate error in return otherwise. This is implemented in
   * ExpressionProcessor.
   */


  /**
   * A primitive approach to "optional" algebraic type. This type is T + Set<Message>.
   */
  public interface Optional<V> {
    V getNormal();
    boolean isNormal();
    Set<? extends Message> errorMessages();
  }

  public static <V> Optional<V> createOptional(final V value) {
    return new Optional<V>() {
      public Set<Message> errorMessages() {
        return Collections.emptySet();
      }
      public V getNormal() {
        return value;
      }
      public boolean isNormal() {
        return true;
      }
    };
  }

  public static <V> Optional<V> createErrorOptional(Message message) {
    return createErrorOptional(Collections.singleton(message));
  }

  public static <V> Optional<V> createErrorOptional(final Set<? extends Message> messages) {
    return new Optional<V>() {
      public Set<? extends Message> errorMessages() {
        return messages;
      }
      public V getNormal() {
        throw new UnsupportedOperationException();
      }
      public boolean isNormal() {
        return false;
      }
    };
  }

  /**
   * A user interface message for dialog window. It has text and priority that helps choosing
   * the most important message it there are many of them.
   */
  public static class Message {
    private final String text;
    private final MessagePriority priority;
    public Message(String text, MessagePriority priority) {
      this.text = text;
      this.priority = priority;
    }
    public String getText() {
      return text;
    }
    public MessagePriority getPriority() {
      return priority;
    }
  }

  /**
   * Priority of a user interface message.
   * Constants are listed from most important to least important.
   */
  public enum MessagePriority {
    BLOCKING_PROBLEM(IMessageProvider.ERROR),
    BLOCKING_INFO(IMessageProvider.NONE),
    WARNING(IMessageProvider.WARNING);

    private final int messageProviderType;
    private MessagePriority(int messageProviderType) {
      this.messageProviderType = messageProviderType;
    }
    public int getMessageProviderType() {
      return messageProviderType;
    }
  }

  /**
   * A base class for the source-consumer pair that accepts several values as a consumer,
   * performs a calculation over them and gives it away the result via source interface.
   * Some sources may be of Optional type. If some of sources have error value the corresponding
   * error value is returned automatically.
   * <p>
   * The implementation should override a single method {@link #calculateNormal}.
   */
  public static abstract class ExpressionProcessor<T> extends ValueProcessor<Optional<T>> {
    private final List<ValueSource<? extends Optional<?>>> optionalSources;
    public ExpressionProcessor(List<ValueSource<? extends Optional<?>>> optionalSources) {
      this.optionalSources = optionalSources;
    }

    protected abstract Optional<T> calculateNormal();

    private Optional<T> calculateNewValue() {
      Set<Message> errors = new LinkedHashSet<Message>(0);
      for (ValueSource<? extends Optional<?>> source : optionalSources) {
        if (!source.getValue().isNormal()) {
          errors.addAll(source.getValue().errorMessages());
        }
      }
      if (errors.isEmpty()) {
        return calculateNormal();
      } else {
        return createErrorOptional(errors);
      }
    }
    public void update(Updater updater) {
      Optional<T> result = calculateNewValue();
      Optional<T> oldValue = getValue();
      setCurrentValue(result);
      if (!result.equals(oldValue)) {
        updater.reportChanged(this);
      }
    }
  }

  /*
   * Part 3. Various utils.
   */

  /**
   * A general-purpose implementation of OK button vertex. It works as a consumer of
   * 1 result value and several warning sources. From its sources it decides whether
   * OK button should be enabled and also provides dialog messages (errors, warnings, infos).
   */
  public static class OkButtonControl implements ValueConsumer {
    private final ValueSource<? extends Optional<?>> resultSource;
    private final List<? extends ValueSource<String>> warningSources;
    private final DialogElements dialogElements;

    public OkButtonControl(ValueSource<? extends Optional<?>> resultSource,
        List<? extends ValueSource<String>> warningSources, DialogElements dialogElements) {
      this.resultSource = resultSource;
      this.warningSources = warningSources;
      this.dialogElements = dialogElements;
    }

    /**
     * Returns a list of dependencies for updater -- a convenience method.
     */
    public List<? extends ValueSource<?>> getDependencies() {
      ArrayList<ValueSource<?>> result = new ArrayList<ValueSource<?>>();
      result.add(resultSource);
      result.addAll(warningSources);
      return result;
    }

    public void update(Updater updater) {
      Optional<?> result = resultSource.getValue();
      List<Message> messages = new ArrayList<Message>();
      for (ValueSource<String> warningSource : warningSources) {
        if (warningSource.getValue() != null) {
          messages.add(new Message(warningSource.getValue(), MessagePriority.WARNING));
        }
      }
      boolean enabled;
      if (result.isNormal()) {
        enabled = true;
      } else {
        enabled = false;
        messages.addAll(result.errorMessages());
      }
      dialogElements.getOkButton().setEnabled(enabled);
      String errorMessage;
      int type;
      if (messages.isEmpty()) {
        errorMessage = null;
        type = IMessageProvider.NONE;
      } else {
        Message visibleMessage = Collections.max(messages, messageComparatorBySeverity);
        errorMessage = visibleMessage.getText();
        type = visibleMessage.getPriority().getMessageProviderType();
      }
      dialogElements.setMessage(errorMessage, type);
    }

    private static final Comparator<Message> messageComparatorBySeverity =
        new Comparator<Message>() {
      public int compare(Message o1, Message o2) {
        int ordinal1 = o1.getPriority().ordinal();
        int ordinal2 = o2.getPriority().ordinal();
        if (ordinal1 < ordinal2) {
          return +1;
        } else if (ordinal1 == ordinal2) {
          return 0;
        } else {
          return -1;
        }
      }
    };
  }

  /**
   * A basic interface to elements of the dialog window from dialog logic part. The user may extend
   * this interface with more elements.
   */
  public interface DialogElements {
    Shell getShell();
    Button getOkButton();
    void setMessage(String message, int type);
  }

  /**
   * A wrapper around Combo that provides logic-level data-oriented access to the control.
   * This is not a simply convenience wrapper, because Combo itself does not keep a real data,
   * but only its string representation.
   */
  public static abstract class ComboWrapper<E> {
    private final Combo combo;
    public ComboWrapper(Combo combo) {
      this.combo = combo;
    }
    public Combo getCombo() {
      return combo;
    }
    public void addSelectionListener(SelectionListener listener) {
      combo.addSelectionListener(listener);
    }
    public abstract E getSelected();
    public abstract void setSelected(E element);
  }
}