All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.rinde.rinsim.ui.View Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2011-2018 Rinde R.S. van Lon
 *
 * 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 com.github.rinde.rinsim.ui;

import static com.google.common.base.Preconditions.checkArgument;

import java.awt.im.InputContext;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Shell;

import com.github.rinde.rinsim.core.Simulator;
import com.github.rinde.rinsim.core.model.CompositeModelBuilder;
import com.github.rinde.rinsim.core.model.DependencyProvider;
import com.github.rinde.rinsim.core.model.Model.AbstractModel;
import com.github.rinde.rinsim.core.model.ModelBuilder;
import com.github.rinde.rinsim.core.model.ModelBuilder.AbstractModelBuilder;
import com.github.rinde.rinsim.core.model.UserInterface;
import com.github.rinde.rinsim.core.model.time.Clock;
import com.github.rinde.rinsim.core.model.time.ClockController;
import com.github.rinde.rinsim.core.model.time.TickListener;
import com.github.rinde.rinsim.core.model.time.TimeLapse;
import com.github.rinde.rinsim.event.Event;
import com.github.rinde.rinsim.event.Listener;
import com.github.rinde.rinsim.ui.renderers.Renderer;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

/**
 * The view class is the main GUI class. For creating a view, see
 * {@link #builder()}.
 * @author Rinde van Lon ([email protected])
 * @author Bartosz Michalik
 * @since 2.0
 */
public final class View
    extends AbstractModel
    implements TickListener, UserInterface, MainView {

  final Builder builder;
  final ClockController clockController;
  final Shell shell;
  final Display display;
  final Set listeners;

  View(Builder b, ClockController cc) {
    builder = b;
    clockController = cc;

    listeners = new LinkedHashSet<>();
    Display.setAppName("RinSim");
    final Display d = builder.display().isPresent()
      ? builder.display().get()
      : Display.getCurrent();
    final boolean isDisplayOwner = d == null;
    display = isDisplayOwner ? new Display() : Display.getCurrent();

    int shellArgs = SWT.TITLE | SWT.CLOSE;
    if (!builder.viewOptions().contains(ViewOption.DISALLOW_RESIZE)) {
      shellArgs = shellArgs | SWT.RESIZE;
    }

    shell = new Shell(display, shellArgs);

    if (builder.monitor().isPresent()) {
      final Monitor m = builder.monitor().get();
      shell.setLocation(m.getBounds().x, m.getBounds().y);
    }

    shell.setText("RinSim - " + builder.title());
    if (builder.viewOptions().contains(ViewOption.FULL_SCREEN)) {
      shell.setFullScreen(true);
      shell.setMaximized(true);
    } else {
      shell.setSize(builder.screenSize());
    }

    shell.addListener(SWT.Close, new org.eclipse.swt.widgets.Listener() {
      @Override
      public void handleEvent(@Nullable org.eclipse.swt.widgets.Event event) {
        clockController.stop();
        while (clockController.isTicking()) {
          // wait until clock actually stops (it finishes its
          // current tick first).
        }
        if (isDisplayOwner && !display.isDisposed()) {
          display.dispose();
        } else if (!isDisplayOwner && !shell.isDisposed()) {
          shell.dispose();
        }
      }
    });
    clockController.getEventAPI().addListener(new Listener() {
      @Override
      public void handleEvent(Event e) {
        if (builder.viewOptions().contains(ViewOption.AUTO_CLOSE)) {
          close();
        }
      }
    }, Clock.ClockEventType.STOPPED);
  }

  void close() {
    if (!shell.isDisposed()) {
      display.asyncExec(new Runnable() {
        @Override
        public void run() {
          shell.close();
        }
      });
    }
    if (builder.callback().isPresent()) {
      builder.callback().get()
        .handleEvent(new Event(Clock.ClockEventType.STOPPED, null));
    }
  }

  @Override
  public void show() {
    shell.open();
    for (final Listener l : listeners) {
      l.handleEvent(new Event(EventType.SHOW, this));
    }
    if (!builder.viewOptions().contains(ViewOption.ASYNC)) {
      while (!shell.isDisposed()) {
        if (!display.readAndDispatch()) {
          display.sleep();
        }
      }
      if (shell.isDisposed()) {
        clockController.stop();
      }
    }

  }

  @Override
  public void tick(TimeLapse time) {}

  @Override
  public void afterTick(TimeLapse time) {
    if (builder.stopTime() > 0 && time.getTime() >= builder.stopTime()) {
      clockController.stop();
    }
  }

  @Override
  public boolean register(Void element) {
    return false;
  }

  @Override
  public boolean unregister(Void element) {
    return false;
  }

  @Override
  public void addListener(Listener l) {
    listeners.add(l);
  }

  @Override
  public  U get(Class clazz) {
    if (clazz == Shell.class) {
      return clazz.cast(shell);
    }
    if (clazz == Device.class || clazz == Display.class) {
      return clazz.cast(shell.getDisplay());
    }
    if (clazz == MainView.class) {
      return clazz.cast(this);
    }
    throw new IllegalArgumentException("Unknown type: " + clazz);
  }

  /**
   * Creates a {@link View.Builder}. The returned builder allows to configure
   * the visualization.
   * @return The {@link View.Builder}.
   */
  @CheckReturnValue
  public static Builder builder() {
    return Builder.create();
  }

  enum ViewOption {
    AUTO_PLAY, AUTO_CLOSE, DISALLOW_RESIZE, FULL_SCREEN, ASYNC;
  }

  /**
   * A builder that creates a visualization for {@link Simulator} instances.
   * @author Rinde van Lon
   */
  @AutoValue
  public abstract static class Builder extends AbstractModelBuilder
      implements CompositeModelBuilder {
    /**
     * The default window size: 800x600.
     */
    public static final Point DEFAULT_WINDOW_SIZE = new Point(800, 600);
    private static final long serialVersionUID = -955386603340399937L;

    Builder() {
      setDependencies(ClockController.class);
      setProvidingTypes(Shell.class, Device.class, Display.class,
        MainView.class);
    }

    abstract ImmutableSet> renderers();

    abstract ImmutableSet viewOptions();

    abstract ImmutableMap accelerators();

    abstract int speedUp();

    abstract long stopTime();

    abstract String title();

    abstract Point screenSize();

    abstract Optional callback();

    abstract Optional monitor();

    abstract Optional display();

    /**
     * Adds the specified builder of a {@link Renderer}.
     * @param builder The builder to add.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder with(ModelBuilder builder) {
      return create(ImmutableSet
        .>builder()
        .addAll(renderers())
        .add(builder)
        .build(),
        viewOptions(), accelerators(), speedUp(), stopTime(), title(),
        screenSize(), callback(), monitor(), display());
    }

    /**
     * When auto play is enabled the {@link Simulator} will be started
     * directly when {@link #show()} is called. Default: disabled.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withAutoPlay() {
      return create(
        renderers(),
        Sets.immutableEnumSet(ViewOption.AUTO_PLAY,
          viewOptions().toArray(new ViewOption[] {})),
        accelerators(), speedUp(), stopTime(), title(), screenSize(),
        callback(), monitor(), display());
    }

    /**
     * When auto close is enabled the view will be closed as soon as the
     * {@link Simulator} is stopped. This is useful for creating automated GUIs.
     * Default: disabled.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withAutoClose() {
      return create(
        renderers(),
        Sets.immutableEnumSet(ViewOption.AUTO_CLOSE,
          viewOptions().toArray(new ViewOption[] {})),
        accelerators(), speedUp(), stopTime(), title(), screenSize(),
        callback(), monitor(), display());
    }

    /**
     * Stops the simulator at the specified time.
     * @param simulationTime The time to stop, must be positive.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withSimulatorEndTime(long simulationTime) {
      checkArgument(simulationTime > 0);
      return create(renderers(), viewOptions(), accelerators(), speedUp(),
        simulationTime,
        title(), screenSize(), callback(), monitor(), display());
    }

    /**
     * Speed up defines the simulation time between two respective GUI draw
     * operations. Default: 1.
     * @param speed The speed to use.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withSpeedUp(int speed) {
      return create(renderers(), viewOptions(), accelerators(), speed,
        stopTime(), title(), screenSize(), callback(), monitor(), display());
    }

    /**
     * Should be used in case there is already an SWT application running that
     * was launched from the same VM as the current GUI that is created.
     * @param d The existing {@link Display} to use as display for the view.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withDisplay(Display d) {
      return create(renderers(), viewOptions(), accelerators(), speedUp(),
        stopTime(), title(), screenSize(), callback(), monitor(),
        Optional.of(d));
    }

    /**
     * Changes the title appendix of the view. The default title is RinSim -
     * Simulator, the title appendix is everything after the dash.
     * @param titleAppendix The new appendix to use.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withTitleAppendix(String titleAppendix) {
      return create(renderers(), viewOptions(), accelerators(), speedUp(),
        stopTime(), titleAppendix, screenSize(), callback(), monitor(),
        display());
    }

    /**
     * Don't allow the user to resize the application window. Default:
     * allowed.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withNoResizing() {
      return create(
        renderers(),
        Sets.immutableEnumSet(ViewOption.DISALLOW_RESIZE,
          viewOptions().toArray(new ViewOption[] {})),
        accelerators(), speedUp(), stopTime(), title(), screenSize(),
        callback(), monitor(), display());
    }

    /**
     * This takes precedence over any calls to {@link #withResolution(int, int)}
     * .
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withFullScreen() {
      return create(
        renderers(),
        Sets.immutableEnumSet(ViewOption.FULL_SCREEN,
          viewOptions().toArray(new ViewOption[] {})),
        accelerators(), speedUp(), stopTime(), title(), screenSize(),
        callback(), monitor(), display());
    }

    /**
     * Change the resolution of the window. Default resolution:
     * {@link #DEFAULT_WINDOW_SIZE}.
     * @param width The new width to use.
     * @param height The new height to use.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withResolution(int width, int height) {
      checkArgument(width > 0 && height > 0,
        "Only positive dimensions are allowed, input: %s x %s.", width,
        height);
      return create(renderers(), viewOptions(), accelerators(), speedUp(),
        stopTime(), title(), new Point(width, height), callback(), monitor(),
        display());
    }

    /**
     * Specify on which monitor the application should be positioned. If this
     * method is not called SWT decides where the screen is positioned, usually
     * on the primary monitor.
     * @param m The monitor.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withMonitor(Monitor m) {
      return create(renderers(), viewOptions(), accelerators(), speedUp(),
        stopTime(), title(), screenSize(), callback(), Optional.of(m),
        display());
    }

    /**
     * Allows to change the accelerators (aka shortcuts) of the menu items. Each
     * accelerator is set to its respective menu item via
     * {@link org.eclipse.swt.widgets.MenuItem#setAccelerator(int)}. By default
     * the accelerators are set to {@link MenuItems#QWERTY_ACCELERATORS} unless
     * a keyboard for the French language is detected (probably using an AZERTY
     * layout), then {@link MenuItems#AZERTY_ACCELERATORS} is used.
     * @param acc The accelerators to set.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withAccelerators(Map acc) {
      return create(renderers(), viewOptions(), ImmutableMap.copyOf(acc),
        speedUp(), stopTime(), title(), screenSize(), callback(), monitor(),
        display());
    }

    /**
     * Sets the view into asynchronous mode. This means that the call to
     * {@link #show()} is non-blocking and will return immediately. By default
     * the {@link #show()} is synchronous.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withAsync() {
      return create(
        renderers(),
        Sets.immutableEnumSet(ViewOption.ASYNC,
          viewOptions().toArray(new ViewOption[] {})),
        accelerators(), speedUp(), stopTime(), title(), screenSize(),
        callback(), monitor(), display());
    }

    /**
     * Allows to register a {@link Listener} for stop events of the simulator.
     * @param l The listener to register, overwrites previous listeners if any.
     * @return A new builder instance.
     */
    @CheckReturnValue
    public Builder withCallback(Listener l) {
      return create(renderers(), viewOptions(), accelerators(), speedUp(),
        stopTime(), title(), screenSize(), Optional.of(l), monitor(),
        display());
    }

    @CheckReturnValue
    @Override
    public View build(DependencyProvider dependencyProvider) {
      checkArgument(!renderers().isEmpty(),
        "At least one renderer needs to be defined.");

      final ClockController cc = dependencyProvider.get(ClockController.class);
      return new View(this, cc);
    }

    @Override
    public ImmutableSet> getChildren() {
      return ImmutableSet.>builder()
        .add(SimulationViewer.builder(this))
        .addAll(renderers())
        .build();
    }

    static Builder create() {
      final ImmutableMap accelerators;
      @Nullable
      final Locale loc = InputContext.getInstance().getLocale();
      if (loc != null
        && loc.getLanguage().equals(Locale.FRENCH.getLanguage())) {
        accelerators = MenuItems.AZERTY_ACCELERATORS;
      } else {
        accelerators = MenuItems.QWERTY_ACCELERATORS;
      }

      return create(ImmutableSet.>of(),
        ImmutableSet.of(), accelerators, 1, -1L, "Simulator",
        DEFAULT_WINDOW_SIZE,
        Optional.absent(),
        Optional.absent(),
        Optional.absent());
    }

    static Builder create(
        ImmutableSet> renderers,
        ImmutableSet viewOptions,
        ImmutableMap accelerators,
        int speedUp,
        long stopTime,
        String title,
        Point screenSize,
        Optional callback,
        Optional monitor,
        Optional display) {
      return new AutoValue_View_Builder(renderers, viewOptions, accelerators,
        speedUp, stopTime, title, screenSize, callback, monitor, display);
    }
  }
}