// Copyright 2012 Google Inc. All Rights Reserved. // // 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.google.collide.client.editor; import com.google.collide.client.common.Constants; import com.google.collide.json.shared.JsonArray; import com.google.collide.json.shared.JsonStringMap; import com.google.collide.shared.document.LineInfo; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.ListenerManager; import com.google.collide.shared.util.ListenerRegistrar; import com.google.collide.shared.util.ListenerRegistrar.Remover; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.user.client.Timer; import org.waveprotocol.wave.client.common.util.SignalEvent; import org.waveprotocol.wave.client.common.util.UserAgent; import elemental.events.Event; /** * Manages mouse hover events, optionally when a key modifier combination is * pressed. * * <p>This class fires mouse hover events asynchronously, with the delay of * {@link Constants#MOUSE_HOVER_DELAY} milliseconds. The reason is that it is * quite expensive to calculate LineInfo and column number from a mouse move * event's {x,y} pair. */ public class MouseHoverManager { public enum KeyModifier { NONE(0), SHIFT(KeyCodes.KEY_SHIFT), CTRL(KeyCodes.KEY_CTRL), ALT(KeyCodes.KEY_ALT), META(91); /** * @return {@link #META} key modifier for Mac OS, or {@link #CTRL} otherwise */ public static KeyModifier ctrlOrMeta() { return UserAgent.isMac() ? KeyModifier.META : KeyModifier.CTRL; } private final int keyCode; private KeyModifier(int keyCode) { this.keyCode = keyCode; } public int getKeyCode() { return keyCode; } } public interface MouseHoverListener { void onMouseHover(int x, int y, LineInfo lineInfo, int column); } private final Editor editor; private final JsonStringMap<ListenerManager<MouseHoverListener>> listenerManagers = JsonCollections.createMap(); /** * Current key combination that we will dispatch the mouse hover events for. * If {@code null}, no mouse hover events should be dispatched just yet. */ private KeyModifier lastKeyModifier = KeyModifier.NONE; private ListenerRegistrar.Remover keyPressListenerRemover; private ListenerRegistrar.Remover mouseMoveListenerRemover; private ListenerRegistrar.Remover mouseOutListenerRemover; private ListenerRegistrar.Remover nativeKeyUpListenerRemover; private final Editor.NativeKeyUpListener keyUpListener = new Editor.NativeKeyUpListener() { @Override public boolean onNativeKeyUp(Event event) { /* * Consider any key-up event releases the key modifier combination, to * avoid tricky stale states. */ releaseLastKeyModifier(); // Do not interfere with the editor input. return false; } }; private final Editor.KeyListener keyPressListener = new Editor.KeyListener() { @Override public boolean onKeyPress(SignalEvent signal) { KeyModifier newKeyModifier = null; JsonArray<String> modifierKeys = listenerManagers.getKeys(); for (int i = 0, n = modifierKeys.size(); i < n; ++i) { KeyModifier keyModifier = KeyModifier.valueOf(modifierKeys.get(i)); if (keyModifier.getKeyCode() == signal.getKeyCode()) { newKeyModifier = keyModifier; break; } } if (lastKeyModifier != newKeyModifier) { lastKeyModifier = newKeyModifier; updateEditorListeners(); } // Do not interfere with the editor input. return false; } }; private class MouseListenersImpl extends Timer implements Buffer.MouseMoveListener, Buffer.MouseOutListener { private int x; private int y; @Override public void run() { handleOnMouseMove(x, y); } @Override public void onMouseMove(int x, int y) { this.x = x; this.y = y; schedule(Constants.MOUSE_HOVER_DELAY); } @Override public void onMouseOut() { // We are no longer hovering the editor's buffer. cancel(); // We can not track the keyboard outside the buffer, just reset the state. releaseLastKeyModifier(); } } private final MouseListenersImpl mouseListener = new MouseListenersImpl(); MouseHoverManager(Editor editor) { this.editor = editor; } public Remover addMouseHoverListener(MouseHoverListener listener) { return addMouseHoverListener(KeyModifier.NONE, listener); } public Remover addMouseHoverListener( final KeyModifier keyModifier, final MouseHoverListener listener) { String key = keyModifier.toString(); ListenerManager<MouseHoverListener> manager = listenerManagers.get(key); if (manager == null) { manager = ListenerManager.create(); listenerManagers.put(key, manager); } final Remover listenerRemover = manager.add(listener); updateEditorListeners(); return new Remover() { @Override public void remove() { removeMouseHoverListener(listenerRemover, keyModifier, listener); } }; } private void removeMouseHoverListener( Remover listenerRemover, KeyModifier keyModifier, MouseHoverListener listener) { String key = keyModifier.toString(); ListenerManager<MouseHoverListener> manager = listenerManagers.get(key); if (manager == null) { return; } listenerRemover.remove(); if (manager.getCount() == 0) { listenerManagers.remove(key); } updateEditorListeners(); } private void releaseLastKeyModifier() { if (lastKeyModifier != KeyModifier.NONE) { lastKeyModifier = KeyModifier.NONE; updateEditorListeners(); } } private void updateEditorListeners() { if (listenerManagers.isEmpty()) { removeAllEditorListeners(); return; } // Attach the performance-critical mouse move listener only if we really need it. if (lastKeyModifier == null || listenerManagers.get(lastKeyModifier.toString()) == null) { if (mouseMoveListenerRemover != null) { mouseMoveListenerRemover.remove(); mouseMoveListenerRemover = null; } } else { if (mouseMoveListenerRemover == null) { mouseMoveListenerRemover = editor.getBuffer().getMouseMoveListenerRegistrar().add(mouseListener); } } if (keyPressListenerRemover == null) { keyPressListenerRemover = editor.getKeyListenerRegistrar().add(keyPressListener); } if (nativeKeyUpListenerRemover == null) { nativeKeyUpListenerRemover = editor.getNativeKeyUpListenerRegistrar().add(keyUpListener); } /* * We should always listen to these events, since we want to release the * last key modifier upon receiving it. */ if (mouseOutListenerRemover == null) { mouseOutListenerRemover = editor.getBuffer().getMouseOutListenerRegistrar().add(mouseListener); } } private void removeAllEditorListeners() { if (keyPressListenerRemover != null) { keyPressListenerRemover.remove(); keyPressListenerRemover = null; } if (mouseMoveListenerRemover != null) { mouseMoveListenerRemover.remove(); mouseMoveListenerRemover = null; } if (mouseOutListenerRemover != null) { mouseOutListenerRemover.remove(); mouseOutListenerRemover = null; } if (nativeKeyUpListenerRemover != null) { nativeKeyUpListenerRemover.remove(); nativeKeyUpListenerRemover = null; } } private void handleOnMouseMove(final int x, final int y) { if (lastKeyModifier == null || editor.getDocument() == null) { return; } String key = lastKeyModifier.toString(); ListenerManager<MouseHoverListener> manager = listenerManagers.get(key); if (manager == null) { return; } int lineNumber = editor.getBuffer().convertYToLineNumber(y, true); final LineInfo lineInfo = editor.getDocument().getLineFinder().findLine(lineNumber); final int column = editor.getBuffer().convertXToRoundedVisibleColumn(x, lineInfo.line()); manager.dispatch(new ListenerManager.Dispatcher<MouseHoverListener>() { @Override public void dispatch(MouseHoverListener listener) { listener.onMouseHover(x, y, lineInfo, column); } }); } }