void printInfo(Line badLine, String msg) {
    Line line = getFirstLine();
    PrintStream stream = System.out;

    int i = 0;
    while (line != null) {

      int indent = line.getIndent();

      stream.println(
          CollectionUtils.repeat('.', line.getIndent())
              + line.toString()
              + " indent:"
              + indent
              + CollectionUtils.repeat(' ', 20)
              + line.getCachedNumberValue()
              + "  ("
              + i
              + ")");

      if (line == badLine) {
        stream.println("\n\n\n");
        stream = System.err;
        stream.println(msg);
        stream.println(">>>>>>>>>>>>>>>>>>>>>>>>> DIED ON LINE ABOVE <<<<<<<<<<<<<<<<<<\n\n");
      }

      line = line.next();
      i++;
    }
  }
  /** Miscellaneous tests that depend on the previous state for convenience */
  public void testMiscellaneous() {
    PainterRegistryImpl r = new PainterRegistryImpl("a", "b", new AnnotationPainter(null));

    assertTrue(r.getPaintFunctions().isEmpty());

    // Test we get back the function we registered
    r.registerPaintFunction(CollectionUtils.newStringSet("a"), p1);
    assertSame(p1, r.getPaintFunctions().iterator().next());

    PainterRegistryImpl r2 = (PainterRegistryImpl) r.createExtension();

    // Test the extension contains the function
    assertSame(p1, r2.getPaintFunctions().iterator().next());
    assertEquals(r.debugGetVersion(), r2.debugGetKnownParentVersion());

    // Test propagation occurs
    r.registerPaintFunction(CollectionUtils.newStringSet("b"), p2);
    assertFalse(r.debugGetVersion() == r2.debugGetKnownParentVersion());
    assertTrue(r2.getPaintFunctions().contains(p2));
    assertEquals(r.debugGetVersion(), r2.debugGetKnownParentVersion());

    PainterRegistryImpl r3 = (PainterRegistryImpl) r2.createExtension();
    // ensure cache is filled
    r3.getPaintFunctions();

    // Test propagation occurs two levels deep after caching
    r.registerPaintFunction(CollectionUtils.newStringSet("c"), p3);
    assertFalse(r.debugGetVersion() == r2.debugGetKnownParentVersion());
    assertTrue(r2.getPaintFunctions().contains(p3));
    assertEquals(r.debugGetVersion(), r2.debugGetKnownParentVersion());

    assertFalse(r2.debugGetVersion() == r3.debugGetKnownParentVersion());
    assertTrue(r3.getPaintFunctions().contains(p3));
    assertEquals(r2.debugGetVersion(), r3.debugGetKnownParentVersion());
  }
  public void testPropagatesDeeply() {
    PainterRegistryImpl r1 = new PainterRegistryImpl("a", "b", new AnnotationPainter(null));
    PainterRegistryImpl r2 = (PainterRegistryImpl) r1.createExtension();
    PainterRegistryImpl r3 = (PainterRegistryImpl) r2.createExtension();

    r1.registerBoundaryFunction(CollectionUtils.newStringSet("a"), b1);
    assertTrue(r3.getBoundaryFunctions().contains(b1));

    r1.registerPaintFunction(CollectionUtils.newStringSet("b"), p1);
    assertTrue(r3.getPaintFunctions().contains(p1));
  }
  public void testInitialState() {
    PainterRegistryImpl r = new PainterRegistryImpl("a", "b", new AnnotationPainter(null));

    PainterRegistryImpl r2 = (PainterRegistryImpl) r.createExtension();
    r2.registerPaintFunction(CollectionUtils.newStringSet("b"), p2);
    assertTrue(r2.getPaintFunctions().contains(p2));
    assertTrue(r2.getKeys().contains("b"));

    r2.registerPaintFunction(CollectionUtils.newStringSet("d"), p4);
    assertTrue(r2.getPaintFunctions().contains(p4));
    assertTrue(r2.getKeys().contains("d"));
    r2.registerBoundaryFunction(CollectionUtils.newStringSet("e"), b1);
    assertTrue(r2.getBoundaryFunctions().contains(b1));
    assertTrue(r2.getKeys().contains("e"));
  }
Ejemplo n.º 5
0
  public static void initRootRegistries() {
    if (rootRegistriesInitialised) {
      return;
    }
    rootRegistriesInitialised = true;

    // TODO(danilatos/patcoleman): Fix up this kludge
    Editor.TAB_TARGETS.addAll(
        CollectionUtils.newStringSet(Caption.TAGNAME, "profile-field", "text-setting"));

    ImeExtractor.register(ROOT_HANDLER_REGISTRY);
    ContentDocElement.register(ROOT_HANDLER_REGISTRY, ContentDocElement.DEFAULT_TAGNAME);
    Paragraph.register(ROOT_HANDLER_REGISTRY);
    LineRendering.registerLines(ROOT_HANDLER_REGISTRY);

    Caption.register(ROOT_HANDLER_REGISTRY);
    ChunkyElementHandler.register("br", ROOT_HANDLER_REGISTRY);
    AnnotationPaint.register(ROOT_HANDLER_REGISTRY);
    ImgDoodad.register(ROOT_HANDLER_REGISTRY);

    FormDoodads.register(ROOT_HANDLER_REGISTRY);

    // after registries, set selection information:
    ValidSelectionStrategy.registerTagForSelections(
        LineContainers.PARAGRAPH_FULL_TAGNAME, false, Skip.NONE);
    ValidSelectionStrategy.registerTagForSelections(
        AnnotationPaint.SPREAD_FULL_TAGNAME, false, Skip.SHALLOW);
    ValidSelectionStrategy.registerTagForSelections(LineContainers.LINE_TAGNAME, true, Skip.DEEP);
  }
  /** A mock flushing operation sink. */
  private static final class MockFlushingOperationSink
      implements FlushingOperationSink<WaveletOperation> {

    private static enum Method {
      CONSUME,
      FLUSH
    }

    private final Queue<Object[]> expectations = CollectionUtils.newLinkedList();
    private Runnable resumeCommand;

    /**
     * Expects a call to consume an op. Optionally performs an action when the consume() call is
     * made.
     */
    void expectConsume(WaveletOperation op, Runnable action) {
      expectations.add(new Object[] {Method.CONSUME, op, action});
    }

    /**
     * Expects a call to flush and operation with a resume command. If {@code} succeed is true then
     * the call will return true, else it will return false (and the caller will expect the resume
     * command to be later invoked).
     */
    void expectFlush(WaveletOperation operation, boolean succeed) {
      expectations.add(new Object[] {Method.FLUSH, operation, succeed});
    }

    void checkExpectationsSatisfied() {
      assertTrue(expectations.isEmpty());
    }

    /** @return the last command passed to flush() */
    Runnable getLastResumeCommand() {
      return resumeCommand;
    }

    @Override
    public void consume(WaveletOperation op) {
      Object[] expected = expectations.remove();
      assertEquals(expected[0], Method.CONSUME);
      assertSame(expected[1], op);
      if (expected[2] != null) {
        ((Runnable) expected[2]).run();
      }
    }

    @Override
    public boolean flush(WaveletOperation operation, Runnable resume) {
      Object[] expected = expectations.remove();
      assertEquals(expected[0], Method.FLUSH);
      assertSame(expected[1], operation);
      resumeCommand = resume;
      return (Boolean) expected[2];
    }
  }
 /**
  * Parses an element.
  *
  * @param parser tokenizer
  * @param parentElement the parent element to attach the parsed node to
  * @return a new element.
  */
 private E parseElement(SafeXmlPullParser parser, D doc, E parentElement) {
   E element =
       doc.createElement(
           parser.getTagName(),
           CollectionUtils.newJavaMap(parser.getAttributes()),
           parentElement,
           null);
   parseChildren(parser, doc, element);
   return element;
 }
Ejemplo n.º 8
0
  /** Opens a mux, binding its operation channels with operation-supporting wavelets. */
  public static void openAndBind(
      WaveletOperationalizer operationalizer,
      WaveViewImpl<OpBasedWavelet> wave,
      WaveDocuments<? extends CcDocument> docRegistry,
      OperationChannelMultiplexer mux,
      IdFilter filter,
      Command whenOpened) {
    StaticChannelBinder staticBinder = new StaticChannelBinder(operationalizer, docRegistry);
    LiveChannelBinder liveBinder =
        new LiveChannelBinder(staticBinder, operationalizer, wave, mux, whenOpened);

    final Collection<KnownWavelet> remoteWavelets = CollectionUtils.createQueue();
    final Collection<ObservableWaveletData> localWavelets = CollectionUtils.createQueue();
    for (ObservableWaveletData wavelet : operationalizer.getWavelets()) {
      // Version 0 wavelets must be wavelets that the client has created in this
      // session. They are not to be included in the known-wavelet collection,
      // because the server does not know about them.
      if (wavelet.getVersion() > 0) {
        remoteWavelets.add(
            new KnownWavelet(wavelet, wavelet.getHashedVersion(), Accessibility.READ_WRITE));
      } else {
        localWavelets.add(wavelet);
      }
    }

    // Start listening to wave events and channel events.
    wave.addListener(liveBinder);
    // This binder only starts getting events once open() has been called, since
    // that is what sets this binder as a mux listener. Since wavelet-to-channel
    // binding occurs through event callbacks, this listener setting must occur
    // before trying to bind localWavelets.
    mux.open(liveBinder, filter, remoteWavelets);
    for (ObservableWaveletData local : localWavelets) {
      mux.createOperationChannel(local.getWaveletId(), local.getCreator());
    }
  }
  private static void addDocumentSnapshotToWavelet(DocumentSnapshot snapshot, WaveletData container)
      throws InvalidParticipantAddress {
    DocOp op = CoreWaveletOperationSerializer.deserialize(snapshot.getDocumentOperation());
    DocInitialization docInit = DocOpUtil.asInitialization(op);

    Collection<ParticipantId> contributors = CollectionUtils.newArrayList();
    for (String p : snapshot.getContributorList()) {
      contributors.add(getParticipantId(p));
    }
    container.createDocument(
        snapshot.getDocumentId(),
        getParticipantId(snapshot.getAuthor()),
        contributors,
        docInit,
        snapshot.getLastModifiedTime(),
        snapshot.getLastModifiedVersion());
  }
  /**
   * @param xmlString
   * @return parsed string
   */
  public D parse(String xmlString) {
    SafeXmlPullParser parser;
    try {
      parser = XmlParserFactory.buffered(xmlString);
    } catch (XmlParseException e) {
      throw new RuntimeException("Cannot parse xml: " + xmlString, e);
    }
    // TODO(ohler): This can be an infinite loop.  Fix that.
    while (parser.getCurrentType() != ItemType.START_ELEMENT) {
      parser.next();
    }

    D document =
        factory.create(parser.getTagName(), CollectionUtils.newJavaMap(parser.getAttributes()));
    parseChildren(parser, document, document.getDocumentElement());

    return document;
  }
Ejemplo n.º 11
0
/**
 * Instances of this class encapsulate the event to signal mapping logic for a specific environment
 * (os/browser).
 *
 * <p>Contains as much of the signal event logic as possible in a POJO testable manner.
 *
 * @author [email protected] (Daniel Danilatos)
 */
public final class SignalKeyLogic {

  /** For webkit + IE I think also all browsers on windows? */
  public static final int IME_CODE = 229;

  private static final String DELETE_KEY_IDENTIFIER = "U+007F";

  // TODO(danilatos): Use int map
  private static final Set<Integer> NAVIGATION_KEYS = new HashSet<Integer>();
  private static final StringMap<Integer> NAVIGATION_KEY_IDENTIFIERS =
      CollectionUtils.createStringMap();

  static {
    NAVIGATION_KEY_IDENTIFIERS.put("Left", KeyCodes.KEY_LEFT);
    NAVIGATION_KEY_IDENTIFIERS.put("Right", KeyCodes.KEY_RIGHT);
    NAVIGATION_KEY_IDENTIFIERS.put("Up", KeyCodes.KEY_UP);
    NAVIGATION_KEY_IDENTIFIERS.put("Down", KeyCodes.KEY_DOWN);
    NAVIGATION_KEY_IDENTIFIERS.put("PageUp", KeyCodes.KEY_PAGEUP);
    NAVIGATION_KEY_IDENTIFIERS.put("PageDown", KeyCodes.KEY_PAGEDOWN);
    NAVIGATION_KEY_IDENTIFIERS.put("Home", KeyCodes.KEY_HOME);
    NAVIGATION_KEY_IDENTIFIERS.put("End", KeyCodes.KEY_END);

    NAVIGATION_KEY_IDENTIFIERS.each(
        new ProcV<Integer>() {
          public void apply(String key, Integer keyCode) {
            NAVIGATION_KEYS.add(keyCode);
          }
        });
  }

  public enum UserAgentType {
    WEBKIT,
    GECKO,
    IE
  }

  public enum OperatingSystem {
    WINDOWS,
    MAC,
    LINUX
  }

  @VisibleForTesting
  public static class Result {
    @VisibleForTesting public int keyCode;
    // Sentinal by default for testing purposes
    @VisibleForTesting public KeySignalType type = KeySignalType.SENTINAL;
  }

  private final UserAgentType userAgent;
  private final boolean commandIsCtrl;

  // Hack, get rid of this
  final boolean commandComboDoesntGiveKeypress;

  /**
   * @param userAgent
   * @param os Operating system
   */
  public SignalKeyLogic(
      UserAgentType userAgent, OperatingSystem os, boolean commandComboDoesntGiveKeypress) {
    this.userAgent = userAgent;
    this.commandComboDoesntGiveKeypress = commandComboDoesntGiveKeypress;
    commandIsCtrl = os != OperatingSystem.MAC;
  }

  public boolean commandIsCtrl() {
    return commandIsCtrl;
  }

  public void computeKeySignalType(
      Result result,
      String typeName,
      int keyCode,
      int which,
      String keyIdentifier,
      boolean metaKey,
      boolean ctrlKey,
      boolean altKey,
      boolean shiftKey) {

    boolean ret = true;

    int typeInt;
    if ("keydown".equals(typeName)) {
      typeInt = Event.ONKEYDOWN;
    } else if ("keypress".equals(typeName)) {
      typeInt = Event.ONKEYPRESS;
    } else if ("keyup".equals(typeName)) {
      result.type = null;
      return;
    } else {
      throw new AssertionError("Non-key-event passed to computeKeySignalType");
    }

    KeySignalType type;

    int computedKeyCode = which != 0 ? which : keyCode;

    if (computedKeyCode == 10) {
      computedKeyCode = KeyCodes.KEY_ENTER;
    }

    // For non-firefox browsers, we only get keydown events for IME, no keypress
    boolean isIME = computedKeyCode == IME_CODE;

    boolean commandKey = commandIsCtrl ? ctrlKey : metaKey;

    // Some trace logging very useful to debug
    EditorStaticDeps.logger
        .trace()
        .log(
            "KEY SIGNAL IN PROCESS identifier = "
                + keyIdentifier
                + " code = "
                + computedKeyCode
                + " type = "
                + (typeInt == Event.ONKEYDOWN ? "KeyDown" : "KeyPress")
                + (ctrlKey ? " CTRL" : "")
                + (shiftKey ? " SHIFT" : "")
                + (altKey ? " ALT" : ""));

    switch (userAgent) {
      case WEBKIT:
        // This is a bit tricky because there are significant differences
        // between safari 3.0 and safari 3.1...

        // We could probably actually almost use the same code that we use for IE
        // for safari 3.1, because with 3.1 the webkit folks made a big shift to
        // get the events to be in line with IE for compatibility. 3.0 events
        // are a lot more similar to FF, but different enough to need special
        // handling. However, it seems that using more advanced features like
        // keyIdentifier for safaris is probably better and more future-proof,
        // as well as being compatible between the two, so for now we're not
        // using IE logic for safari 3.1

        // Weird special large keycode numbers for safari 3.0, where it gives
        // us keypress events (though they happen after the dom is changed,
        // for some things like delete. So not too useful). The number
        // 63200 is known as the cutoff mark.
        if (typeInt == Event.ONKEYDOWN && computedKeyCode > 63200) {
          result.type = null;
          return;
        } else if (typeInt == Event.ONKEYPRESS) {
          // Skip keypress for tab and escape, because they are the only non-input keys
          // that don't have keycodes above 63200. This is to prevent them from being treated
          // as INPUT in the || = keypress below. See (X) below
          if (computedKeyCode == KeyCodes.KEY_ESCAPE || computedKeyCode == KeyCodes.KEY_TAB) {
            result.type = null;
            return;
          }
        }

        // boolean isPossiblyCtrlInput = typeInt == Event.ONKEYDOWN && ret.getCtrlKey();
        boolean isActuallyCtrlInput = false;

        boolean startsWithUPlus = keyIdentifier != null && keyIdentifier.startsWith("U+");

        // Need to use identifier for the delete key because the keycode conflicts
        // with the keycode for the full stop.
        if (isIME) {
          // If is IME, override the logic below - we get keyIdentifiers for IME events,
          // but those are basically useless as the event is basically still an IME input
          // event (e.g. keyIdentifier might say "Up", but it's certainly not navigation,
          // it's just the user selecting from the IME dialog).
          type = KeySignalType.INPUT;
        } else if ((DELETE_KEY_IDENTIFIER.equals(keyIdentifier) && typeInt == Event.ONKEYDOWN)
            || computedKeyCode == KeyCodes.KEY_BACKSPACE) {
          // WAVE-407 Avoid missing the '.' char (KEYPRESS + CODE 46)
          // ensuring it's a KEYDOWN event with a DELETE_KEY_IDENTIFIER

          type = KeySignalType.DELETE;
        } else if (NAVIGATION_KEY_IDENTIFIERS.containsKey(keyIdentifier)
            && typeInt == Event.ONKEYDOWN) {
          // WAVE-407 Avoid missing chars with NAVIGATION_KEY_IDENTIFIERS but
          // represeting a SHIFT + key char (! " · ...). Navigation events come
          // with KEYDOWN, not with KEYPRESS

          type = KeySignalType.NAVIGATION;
          // Escape, backspace and context-menu-key (U+0010) are, to my knowledge,
          // the only non-navigation keys that
          // have a "U+..." keyIdentifier, so we handle them explicitly.
          // (Backspace was handled earlier).
        } else if (computedKeyCode == KeyCodes.KEY_ESCAPE || "U+0010".equals(keyIdentifier)) {
          type = KeySignalType.NOEFFECT;
        } else if (computedKeyCode < 63200
            && // if it's not a safari 3.0 non-input key (See (X) above)
            (typeInt == Event.ONKEYPRESS
                || // if it's a regular keypress
                startsWithUPlus
                || computedKeyCode == KeyCodes.KEY_ENTER)) {
          type = KeySignalType.INPUT;
          isActuallyCtrlInput = ctrlKey || (commandComboDoesntGiveKeypress && commandKey);
        } else {
          type = KeySignalType.NOEFFECT;
        }

        // Maybe nullify it with the same logic as IE, EXCEPT for the special
        // Ctrl Input webkit behaviour, and IME for windows
        if (isActuallyCtrlInput) {
          if (computedKeyCode == KeyCodes.KEY_ENTER) {
            ret = typeInt == Event.ONKEYDOWN;
          }
          // HACK(danilatos): Don't actually nullify isActuallyCtrlInput for key press.
          // We get that for AltGr combos on non-mac computers.
        } else if (isIME || keyCode == KeyCodes.KEY_TAB) {
          ret = typeInt == Event.ONKEYDOWN;
        } else {
          ret = maybeNullWebkitIE(ret, typeInt, type);
        }
        if (!ret) {
          result.type = null;
          return;
        }
        break;
      case GECKO:
        boolean hasKeyCodeButNotWhich = keyCode != 0 && which == 0;

        // Firefox is easy for deciding signal events, because it issues a keypress for
        // whenever we would want a signal. So we can basically ignore all keydown events.
        // It also, on all OSes, does any default action AFTER the keypress (even for
        // things like Ctrl/Meta+C, etc). So keypress is perfect for us.
        // Ctrl+Space is an exception, where we don't get a keypress
        // Firefox also gives us keypress events even for Windows IME input
        if (ctrlKey && !altKey && !shiftKey && computedKeyCode == ' ') {
          if (typeInt != Event.ONKEYDOWN) {
            result.type = null;
            return;
          }
        } else if (typeInt == Event.ONKEYDOWN) {
          result.type = null;
          return;
        }

        // Backspace fails the !hasKeyCodeButNotWhich test, so check it explicitly first
        if (computedKeyCode == KeyCodes.KEY_BACKSPACE) {
          type = KeySignalType.DELETE;
          // This 'keyCode' but not 'which' works very nicely for catching normal typing input keys,
          // the only 'exceptions' I've seen so far are bksp & enter which have both
        } else if (!hasKeyCodeButNotWhich
            || computedKeyCode == KeyCodes.KEY_ENTER
            || computedKeyCode == KeyCodes.KEY_TAB) {
          type = KeySignalType.INPUT;
        } else if (computedKeyCode == KeyCodes.KEY_DELETE) {
          type = KeySignalType.DELETE;
        } else if (NAVIGATION_KEYS.contains(computedKeyCode)) {
          type = KeySignalType.NAVIGATION;
        } else {
          type = KeySignalType.NOEFFECT;
        }

        break;
      case IE:

        // Unfortunately IE gives us the least information, so there are no nifty tricks.
        // So we pretty much need to use some educated guessing based on key codes.
        // Experimentation page to the rescue.

        boolean isKeydownForInputKey = isInputKeyCodeIE(computedKeyCode);

        // IE has some strange behaviour with modifiers and whether or not there will
        // be a keypress. Ctrl kills the keypress, unless shift is also held.
        // Meta doesn't kill it. Alt always kills the keypress, overriding other rules.
        boolean hasModifiersThatResultInNoKeyPress = altKey || (ctrlKey && !shiftKey);

        if (typeInt == Event.ONKEYDOWN) {
          if (isKeydownForInputKey) {
            type = KeySignalType.INPUT;
          } else if (computedKeyCode == KeyCodes.KEY_BACKSPACE
              || computedKeyCode == KeyCodes.KEY_DELETE) {
            type = KeySignalType.DELETE;
          } else if (NAVIGATION_KEYS.contains(computedKeyCode)) {
            type = KeySignalType.NAVIGATION;
          } else {
            type = KeySignalType.NOEFFECT;
          }
        } else {
          // Escape is the only non-input thing that has a keypress event
          if (computedKeyCode == KeyCodes.KEY_ESCAPE) {
            result.type = null;
            return;
          }
          assert typeInt == Event.ONKEYPRESS;
          // I think the guessCommandFromModifiers() check here isn't needed,
          // but i feel safer putting it in.
          type = KeySignalType.INPUT;
        }

        if (hasModifiersThatResultInNoKeyPress || isIME || computedKeyCode == KeyCodes.KEY_TAB) {
          ret = typeInt == Event.ONKEYDOWN ? ret : false;
        } else {
          ret = maybeNullWebkitIE(ret, typeInt, type);
        }
        if (!ret) {
          result.type = null;
          return;
        }
        break;
      default:
        throw new UnsupportedOperationException("Unhandled user agent");
    }

    if (ret) {
      result.type = type;
      result.keyCode = computedKeyCode;
    } else {
      result.type = null;
      return;
    }
  }

  private static final boolean isInputKeyCodeIE(int keyCode) {
    /*
    DATA
    ----
    For KEYDOWN:

    "Input"
    48-57 (numbers)
    65-90 (a-z)
    96-111 (Numpad digits & other keys, with numlock off. with numlock on, they
      behave like their corresponding keys on the rest of the keyboard)
    186-192 219-222 (random non-alphanumeric next to letters on RHS + backtick)
    229 Code that the input has passed to an IME

    Non-"input"
    < 48 ('0')
    91-93 (Left & Right Win keys, ContextMenu key)
    112-123 (F1-F12)
    144-5 (NUMLOCK,SCROLL LOCK)

    For KEYPRESS: only "input" things get this event! yay! not even backspace!
    Well, one exception: ESCAPE
    */
    // boundaries in keycode ranges where the keycode for a keydown is for an input
    // key. at "ON" it is, starting from the number going up, and the opposite for "OFF".
    final int A_ON = 48;
    final int B_OFF = 91;
    final int C_ON = 96;
    final int D_OFF = 112;
    final int E_ON = 186;

    return (keyCode == 9 || keyCode == 32 || keyCode == 13)
        || // And tab, enter & spacebar, of course!
        (keyCode >= A_ON && keyCode < B_OFF)
        || (keyCode >= C_ON && keyCode < D_OFF)
        || (keyCode >= E_ON);
  }

  /** Common logic between Webkit and IE for deciding whether we want the keydown or the keypress */
  private static boolean maybeNullWebkitIE(boolean ret, int typeInt, KeySignalType type) {
    // Use keydown as the signal for everything except input.
    // This is because the mutation always happens after the keypress for
    // input (this is especially important for chrome,
    // which interleaves deferred commands between keydown and keypress).
    //
    // For everything else, keypress is redundant with keydown, and also, the resulting default
    // dom mutation (if any) often happens after the keydown but before the keypress in webkit.
    // Also, if the 'Command' key is held for chrome/safari etc, we want to get the keydown
    // event, NOT the keypress event, for everything because of things like ctrl+c etc.
    // where sometimes it'll happen just after the keydown, or sometimes we just won't
    // get a keypress at all
    if (typeInt == (type == KeySignalType.INPUT ? Event.ONKEYDOWN : Event.ONKEYPRESS)) {
      return false;
    }

    return ret;
  }
}
Ejemplo n.º 12
0
/**
 * Binds operation channels from a {@link OperationChannelMultiplexer mux} with the output sinks of
 * wavelets, and keeps binding matching channel/wavelet pairs while live.
 *
 * @author [email protected] (David Hearnden)
 */
public final class LiveChannelBinder
    implements WaveViewListener, OperationChannelMultiplexer.Listener {

  private final StaticChannelBinder binder;
  private final WaveletOperationalizer operationalizer;
  private final WaveViewImpl<OpBasedWavelet> wave;
  private final OperationChannelMultiplexer mux;
  private final Command whenOpened;

  /**
   * Operation channels waiting to be bound. This map is populated from {@link
   * #onOperationChannelCreated(OperationChannel, ObservableWaveletData, Accessibility)}, and
   * depleted by {@link #connect(String)}.
   */
  private final StringMap<OperationChannel> channels = CollectionUtils.createStringMap();

  //
  // The binding flow is not completely trivial, because it has to work with
  // two directions of control flow:
  //
  // Client-created wavelets:
  // 1. wavelet shows up in the model
  // 2. this binder tells mux to create op channel,
  // 3. its operation channel shows up, then
  // 4. wavelet and op channel are bound together.
  //
  // Server-created wavelets:
  // 1. op channel shows up in the mux,
  // 2. this binder builds wavelet and puts it in the model,
  // 3. wavelet shows up in the model, then
  // 4. wavelet and op channel are bound together.
  //
  // Also, the initial set of operation channels when opening a wave with known
  // wavelet states is just like the server-created wavelet flow, except without
  // step 2.
  //

  private LiveChannelBinder(
      StaticChannelBinder binder,
      WaveletOperationalizer operationalizer,
      WaveViewImpl<OpBasedWavelet> wave,
      OperationChannelMultiplexer mux,
      Command whenOpened) {
    this.binder = binder;
    this.operationalizer = operationalizer;
    this.wave = wave;
    this.mux = mux;
    this.whenOpened = whenOpened;
  }

  /** Opens a mux, binding its operation channels with operation-supporting wavelets. */
  public static void openAndBind(
      WaveletOperationalizer operationalizer,
      WaveViewImpl<OpBasedWavelet> wave,
      WaveDocuments<? extends CcDocument> docRegistry,
      OperationChannelMultiplexer mux,
      IdFilter filter,
      Command whenOpened) {
    StaticChannelBinder staticBinder = new StaticChannelBinder(operationalizer, docRegistry);
    LiveChannelBinder liveBinder =
        new LiveChannelBinder(staticBinder, operationalizer, wave, mux, whenOpened);

    final Collection<KnownWavelet> remoteWavelets = CollectionUtils.createQueue();
    final Collection<ObservableWaveletData> localWavelets = CollectionUtils.createQueue();
    for (ObservableWaveletData wavelet : operationalizer.getWavelets()) {
      // Version 0 wavelets must be wavelets that the client has created in this
      // session. They are not to be included in the known-wavelet collection,
      // because the server does not know about them.
      if (wavelet.getVersion() > 0) {
        remoteWavelets.add(
            new KnownWavelet(wavelet, wavelet.getHashedVersion(), Accessibility.READ_WRITE));
      } else {
        localWavelets.add(wavelet);
      }
    }

    // Start listening to wave events and channel events.
    wave.addListener(liveBinder);
    // This binder only starts getting events once open() has been called, since
    // that is what sets this binder as a mux listener. Since wavelet-to-channel
    // binding occurs through event callbacks, this listener setting must occur
    // before trying to bind localWavelets.
    mux.open(liveBinder, filter, remoteWavelets);
    for (ObservableWaveletData local : localWavelets) {
      mux.createOperationChannel(local.getWaveletId(), local.getCreator());
    }
  }

  @Override
  public void onFailed(CorruptionDetail detail) {
    throw new RuntimeException(detail);
  }

  @Override
  public void onOpenFinished() {
    if (whenOpened != null) {
      whenOpened.execute();
    }
  }

  //
  // Wavelet and Channel lifecycle events:
  //

  @Override
  public void onWaveletAdded(ObservableWavelet wavelet) {
    String id = ModernIdSerialiser.INSTANCE.serialiseWaveletId(wavelet.getId());
    if (channels.containsKey(id)) {
      connect(id);
    } else {
      // This will trigger the onOperationChannelCreated callback below.
      mux.createOperationChannel(wavelet.getId(), wavelet.getCreatorId());
    }
  }

  @Override
  public void onOperationChannelCreated(
      OperationChannel channel, ObservableWaveletData snapshot, Accessibility accessibility) {
    WaveletId wid = snapshot.getWaveletId();
    String id = ModernIdSerialiser.INSTANCE.serialiseWaveletId(wid);

    Preconditions.checkState(!channels.containsKey(id));
    channels.put(id, channel);

    if (wave.getWavelet(wid) != null) {
      connect(id);
    } else {
      // This will trigger the onWaveletAdded callback above.
      wave.addWavelet(operationalizer.operationalize(snapshot));
    }
  }

  @Override
  public void onWaveletRemoved(ObservableWavelet wavelet) {
    // TODO
  }

  @Override
  public void onOperationChannelRemoved(OperationChannel channel, WaveletId waveletId) {
    // TODO
  }

  private void connect(String id) {
    binder.bind(id, removeAndReturn(channels, id));
  }

  // Something that should have been on StringMap from the beginning.
  private static <V> V removeAndReturn(StringMap<V> map, String key) {
    V value = map.get(key);
    map.remove(key);
    return value;
  }
}
/**
 * De-multiplexes object channels a client is listening to.
 *
 * <p>Packets arrive with two keys, 'id' to identify the object, and 'm' containing the message
 * payload.
 *
 * @author [email protected] (Daniel Danilatos)
 */
public class GaeChannelDemuxer {
  // There should only ever be one global instance, or strange things will happen.
  private GaeChannelDemuxer() {}

  private static final GaeChannelDemuxer INSTANCE = new GaeChannelDemuxer();

  public static GaeChannelDemuxer get() {
    return INSTANCE;
  }

  /** Channel that listens for messages to a specific object */
  public interface GaeChannel {
    void onMessage(JsoView data);
  }

  private final Log log = Logs.create("demuxer");
  private final StringMap<GaeChannel> channels = CollectionUtils.createStringMap();
  private String currentToken = null;

  @SuppressWarnings("unused") // used by native code
  private JavaScriptObject socket;

  public void registerChannel(String objectId, GaeChannel channel) {
    Preconditions.checkState(
        !channels.containsKey(objectId), "Channel handler already registered for " + objectId);

    channels.put(objectId, channel);
  }

  public void deregisterChannel(String objectId) {
    Preconditions.checkState(
        channels.containsKey(objectId), "Channel handler not registered for %s", objectId);
    channels.remove(objectId);
  }

  public void connect(String token) {
    if (!Preconditions.checkNotNull(token, "Null token").equals(currentToken)) {
      log.log(Level.INFO, "Connecting with token ", token);
      currentToken = token;
      connectNative(token);
    } else {
      log.log(Level.DEBUG, "Already using same token, ignoring ", token);
    }
  }

  private native void connectNative(String token) /*-{
    var me = this;

    var socket = [email protected]::socket;
    if (socket != null) {
      socket.close();
    }

    channel = new $wnd.goog.appengine.Channel(token);

    socket = channel.open();
    [email protected]::socket = socket;

    socket.onopen = $entry(function() {
      me.
        @com.google.walkaround.wave.client.GaeChannelDemuxer::onOpened()
        ();
    });
    socket.onmessage = $entry(function(msg) {
      me.
        @com.google.walkaround.wave.client.GaeChannelDemuxer::onMessage(Ljava/lang/String;)
        (msg.data);
    });
    socket.onerror = $entry(function(err) {
      me.
        @com.google.walkaround.wave.client.GaeChannelDemuxer::onError(ILjava/lang/String;)
        (err.code, err.description);
    });
    socket.onclose = $entry(function() {
      me.
        @com.google.walkaround.wave.client.GaeChannelDemuxer::onClose()
        ();
    });
  }-*/;

  @SuppressWarnings("unused") // called by native code
  private void onOpened() {
    log.log(Level.DEBUG, "onOpened ");
  }

  @SuppressWarnings("unused") // called by native code
  private void onMessage(String data) {
    log.log(Level.DEBUG, "onMessage data=", data);
    if (data == null) {
      log.log(Level.WARNING, "Null data on channel");
      return;
    }
    try {
      JsoView jso = JsUtil.eval(data);
      if (!jso.containsKey("id") || !jso.containsKey("m")) {
        throw new MessageException("Missing fields");
      }
      String id = jso.getString("id");
      JsoView m = jso.getJsoView("m");
      GaeChannel channel = channels.get(id);
      if (channel == null) {
        log.log(Level.WARNING, "No channel registered for object with id ", id);
        return;
      }
      channel.onMessage(m);
    } catch (MessageException e) {
      log.log(Level.WARNING, "Bad data on channel ", data, " ", e);
    }
  }

  @SuppressWarnings("unused") // called by native code
  private void onError(int httpCode, String description) {
    log.log(Level.WARNING, "onError code=", httpCode, " description=", description);
  }

  @SuppressWarnings("unused") // called by native code
  private void onClose() {
    log.log(Level.DEBUG, "onClose ");
  }
}
Ejemplo n.º 14
0
/**
 * Presents a search model into a search view.
 *
 * <p>This class invokes rendering, and controls the lifecycle of digest views. It also handles all
 * UI gesture events sourced from views in the search panel.
 *
 * @author [email protected] (David Hearnden)
 */
public final class SearchPresenter
    implements Search.Listener, SearchPanelView.Listener, SearchView.Listener, ProfileListener {

  /** Handles wave actions. */
  public interface WaveActionHandler {
    /** Handles the wave creation action. */
    void onCreateWave();

    /** Handles a wave selection action. */
    void onWaveSelected(WaveId id);
  }

  private static final SearchPresenterMessages messages = GWT.create(SearchPresenterMessages.class);

  /** How often to repeat the search query. */
  private static final int POLLING_INTERVAL_MS = 15000; // 15s

  private static final String DEFAULT_SEARCH = "in:inbox";
  private static final int DEFAULT_PAGE_SIZE = 20;

  // External references
  private final TimerService scheduler;
  private final Search search;
  private final SearchPanelView searchUi;
  private final WaveActionHandler actionHandler;

  // Internal state
  private final IdentityMap<DigestView, Digest> digestUis = CollectionUtils.createIdentityMap();
  private final IncrementalTask searchUpdater =
      new IncrementalTask() {
        @Override
        public boolean execute() {
          doSearch();
          return true;
        }
      };

  private final Task renderer =
      new Task() {
        @Override
        public void execute() {
          if (search.getState() == State.READY) {
            render();
          } else {
            // Try again later.
            scheduler.schedule(this);
          }
        }
      };

  /** Current search query. */
  private String queryText = DEFAULT_SEARCH;
  /** Number of results to query for. */
  private int querySize = DEFAULT_PAGE_SIZE;
  /** Current selected digest. */
  private DigestView selected;

  /** The dispatcher of profiles events. */
  SourcesEvents<ProfileListener> profiles;

  private boolean isRenderingInProgress = false;
  private final SearchPresenterResources.Css css;

  SearchPresenter(
      SearchPresenterResources.Css css,
      TimerService scheduler,
      Search search,
      SearchPanelView searchUi,
      WaveActionHandler actionHandler,
      SourcesEvents<ProfileListener> profiles) {
    this.css = css;
    this.search = search;
    this.searchUi = searchUi;
    this.scheduler = scheduler;
    this.actionHandler = actionHandler;
    this.profiles = profiles;
  }

  /**
   * Creates a search presenter.
   *
   * @param model model to present
   * @param view view to render into
   * @param actionHandler handler for actions
   * @param profileEventsDispatcher the dispatcher of profile events.
   */
  public static SearchPresenter create(
      Search model,
      SearchPanelView view,
      WaveActionHandler actionHandler,
      SourcesEvents<ProfileListener> profileEventsDispatcher) {
    SearchPresenterResources.Css css = SearchPresenterResources.Loader.res.css();
    SearchPresenter presenter =
        new SearchPresenter(
            css,
            SchedulerInstance.getHighPriorityTimer(),
            model,
            view,
            actionHandler,
            profileEventsDispatcher);
    presenter.init();
    return presenter;
  }

  /** Performs initial presentation, and attaches listeners to live objects. */
  private void init() {
    initToolbarMenu();
    initSearchBox();
    render();
    search.addListener(this);
    profiles.addListener(this);
    searchUi.init(this);
    searchUi.getSearch().init(this);

    // Fire a polling search.
    scheduler.scheduleRepeating(searchUpdater, 0, POLLING_INTERVAL_MS);

    StateManagerInstance.get()
        .onStateChanged(
            false,
            new StateChangedHandler() {
              @Override
              public void onStateChanged(StateChangedEvent event) {
                StateAbstractDTO state = event.getState();
                if (state instanceof StateContentDTO) {
                  StateContentDTO ctn = (StateContentDTO) state;
                  if (ctn.isWave() && ctn.isParticipant()) {
                    // This will work only if the wave is in the current search
                    // GwtWaverefEncoder.decodeWaveRefFromPath(DEFAULT_SEARCH)
                    // final String waveUri =
                    // GwtWaverefEncoder.encodeToUriPathSegment(WaveRef.of(digest.getWaveId()));
                    selectWaveUri(ctn.getWaveRef().replaceFirst("\\/~\\/conv\\+root", ""));
                  }
                }
              }
            });
  }

  /** Releases resources and detaches listeners. */
  public void destroy() {
    scheduler.cancel(searchUpdater);
    scheduler.cancel(renderer);
    searchUi.getSearch().reset();
    searchUi.reset();
    search.removeListener(this);
    profiles.removeListener(this);
  }

  /** Adds custom buttons to the toolbar. */
  private void initToolbarMenu() {
    GroupingToolbar.View toolbarUi = searchUi.getToolbar();
    ToolbarView group = toolbarUi.addGroup();
    new ToolbarButtonViewBuilder()
        .setText(messages.newWave())
        .applyTo(
            group.addClickButton(),
            new ToolbarClickButton.Listener() {
              @Override
              public void onClicked() {
                actionHandler.onCreateWave();

                // HACK(hearnden): To mimic live search, fire a search poll
                // reasonably soon (500ms) after creating a wave. This will be unnecessary
                // with a real live search implementation. The delay is to give
                // enough time for the wave state to propagate to the server.
                int delay = 500;
                scheduler.scheduleRepeating(searchUpdater, delay, POLLING_INTERVAL_MS);
              }
            });
    // Fake group with empty button - to force the separator be displayed.
    group = toolbarUi.addGroup();
    new ToolbarButtonViewBuilder().setText("").applyTo(group.addClickButton(), null);
  }

  /** Initializes the search box. */
  private void initSearchBox() {
    searchUi.getSearch().setQuery(queryText);
  }

  /** Executes the current search. */
  private void doSearch() {
    search.find(queryText, querySize);
  }

  /** Renders the current state of the search result into the panel. */
  private void render() {
    renderTitle();
    renderDigests();
    renderShowMore();
  }

  /** Renders the paging information into the title bar. */
  private void renderTitle() {
    int resultEnd = querySize;
    String totalStr;
    if (search.getTotal() != Search.UNKNOWN_SIZE) {
      resultEnd = Math.min(resultEnd, search.getTotal());
      totalStr = messages.of(search.getTotal());
    } else {
      totalStr = messages.ofUnknown();
    }
    searchUi.setTitleText(queryText + " (0-" + resultEnd + " " + totalStr + ")");
  }

  private void renderDigests() {
    isRenderingInProgress = true;
    // Preserve selection on re-rendering.
    WaveId toSelect = selected != null ? digestUis.get(selected).getWaveId() : null;
    searchUi.clearDigests();
    digestUis.clear();
    setSelected(null);
    for (int i = 0, size = search.getMinimumTotal(); i < size; i++) {
      Digest digest = search.getDigest(i);
      if (digest == null) {
        continue;
      }
      DigestView digestUi = searchUi.insertBefore(null, digest);
      digestUis.put(digestUi, digest);
      if (digest.getWaveId().equals(toSelect)) {
        setSelected(digestUi);
      }
    }
    isRenderingInProgress = false;
  }

  private void renderShowMore() {
    searchUi.setShowMoreVisible(
        search.getTotal() == Search.UNKNOWN_SIZE || querySize < search.getTotal());
  }

  //
  // UI gesture events.
  //

  private void setSelected(DigestView digestUi) {
    if (digestUi != selected) {
      if (selected != null) {
        selected.deselect();
      }
      if (digestUi != null) {
        // Log.info("SearchPresenter: Set selected");
        digestUi.select();
      }
      selected = digestUi;
    }
  }

  /** Invokes the wave-select action on the currently selected digest. */
  private void openSelected() {
    actionHandler.onWaveSelected(digestUis.get(selected).getWaveId());
  }

  @Override
  public void onClicked(DigestView digestUi) {
    setSelected(digestUi);
    openSelected();
  }

  @Override
  public void onQueryEntered() {
    queryText = searchUi.getSearch().getQuery();
    querySize = DEFAULT_PAGE_SIZE;
    searchUi.setTitleText(messages.searching());
    doSearch();
  }

  @Override
  public void onShowMoreClicked() {
    querySize += DEFAULT_PAGE_SIZE;
    doSearch();
  }

  //
  // Search events. For now, dumbly re-render the whole list.
  //

  @Override
  public void onStateChanged() {
    //
    // If the state switches to searching, then do nothing. A manual title-bar
    // update is performed in onQueryEntered(), and the title-bar should not be
    // updated when a polling search fires.
    //
    // If the state switches to ready, then just update the title. Do not
    // necessarily re-render, since that is only necessary if a change occurred,
    // which would have fired one of the other methods below.
    //
    if (search.getState() == State.READY) {
      renderTitle();
    }
  }

  @Override
  public void onDigestAdded(int index, Digest digest) {
    renderLater();
  }

  @Override
  public void onDigestRemoved(int index, Digest digest) {
    renderLater();
  }

  /**
   * Find the DigestView that contains a certain digest
   *
   * @param digest the digest the DigestView should contain.
   * @return the DigestView containing the digest. {@null} if the digest is not found.
   */
  private DigestView findDigestView(Digest digest) {
    DigestView digestUi = searchUi.getFirst();
    while (digestUi != null) {
      if (digestUis.get(digestUi).equals(digest)) {
        return digestUi;
      }
      digestUi = searchUi.getNext(digestUi);
    }
    return null;
  }

  private void selectWaveUri(String waveId) {
    // Log.info("Trying to select waveId " + waveId);
    DigestView digestUi = searchUi.getFirst();
    while (digestUi != null) {
      Digest digest = digestUis.get(digestUi);
      WaveId iteraWaveId = digest.getWaveId();
      // Log.info("Comparing to waveId " + iteraWaveId);
      if (iteraWaveId.equals(WaveId.deserialise(waveId))) {
        setSelected(digestUi);
        return;
      }
      digestUi = searchUi.getNext(digestUi);
    }
    setSelected(null);
  }

  /**
   * Insert a digest before amongst the currently shown digests
   *
   * @param insertRef the DigestView to insert the new digest before. The new digest is inserted
   *     last if insertRef is {@null}.
   * @param digest the digest to insert.
   * @return the newly inserted DigestView.
   */
  private DigestView insertDigest(DigestView insertRef, Digest digest) {
    DigestView newDigestUi = null;
    if (insertRef != null) {
      newDigestUi = searchUi.insertBefore(insertRef, digest);
      digestUis.put(newDigestUi, digest);
    } else {
      insertRef = searchUi.getLast();
      newDigestUi = searchUi.insertAfter(insertRef, digest);
      digestUis.put(newDigestUi, digest);
    }
    return newDigestUi;
  }

  @Override
  public void onDigestReady(int index, Digest digest) {
    if (isRenderingInProgress) {
      return;
    }

    setSelected(null);
    DigestView digestToRemove = findDigestView(digest);
    if (digestToRemove == null) {
      return;
    }
    DigestView insertRef = searchUi.getNext(digestToRemove);
    digestToRemove.remove();
    DigestView newDigestUi = insertDigest(insertRef, digest);
    setSelected(newDigestUi);
  }

  @Override
  public void onTotalChanged(int total) {
    renderLater();
  }

  private void renderLater() {
    if (!scheduler.isScheduled(renderer)) {
      scheduler.schedule(renderer);
    }
  }

  @Override
  public void onProfileUpdated(Profile profile) {
    // NOTE: Search panel will be re-rendered once for every profile that comes
    // back to the client. If this causes an efficiency problem then have the
    // SearchPanelRenderer to be the profile listener, rather than
    // SearchPresenter, and make it stateful. Have it remember which digests
    // have used which profiles in their renderings.
    renderLater();
  }
}
 protected static ReadableStringSet strs(String... strings) {
   return CollectionUtils.newStringSet(strings);
 }