/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.waveprotocol.wave.client.wavepanel.impl.edit;
import com.google.gwt.dom.client.Element;
import org.waveprotocol.wave.client.common.util.Measurer;
import org.waveprotocol.wave.client.common.util.MeasurerInstance;
import org.waveprotocol.wave.client.common.util.OffsetPosition;
import org.waveprotocol.wave.client.editor.Editor;
import org.waveprotocol.wave.client.editor.EditorUpdateEvent;
import org.waveprotocol.wave.client.editor.EditorUpdateEvent.EditorUpdateListener;
import org.waveprotocol.wave.client.editor.selection.html.NativeSelectionUtil;
import org.waveprotocol.wave.client.scroll.DomScrollPanel;
import org.waveprotocol.wave.client.scroll.Extent;
import org.waveprotocol.wave.client.wavepanel.impl.WavePanelImpl;
import org.waveprotocol.wave.client.wavepanel.view.BlipView;
import org.waveprotocol.wave.client.wavepanel.view.TopConversationView;
import org.waveprotocol.wave.client.wavepanel.view.dom.TopConversationDomImpl;
import org.waveprotocol.wave.client.wavepanel.view.impl.TopConversationViewImpl;
import org.waveprotocol.wave.model.util.IntRange;
/**
* Ensures that the selection is always in the viewport.
* <p>
* This feature only works if the underlying view implementation is DOM-based.
*
* @author hearnden@google.com (David Hearnden)
*/
public final class KeepFocusInView implements EditorUpdateListener, EditSession.Listener {
/**
* Buffer size from the top and bottom of the viewport used as selection
* boundaries.
*/
private static final int PAD_PX = 50;
/** Edit session, whose events activate and deactivate this feature. */
private final EditSession edit;
/** Panel in which this feature is active. */
private final WavePanelImpl waveUi;
/** Measurer used to measure sizes of DOM elements. */
private final Measurer measurer = MeasurerInstance.get();
private Element viewport;
private DomScrollPanel scroller;
KeepFocusInView(EditSession edit, WavePanelImpl waveUi) {
this.edit = edit;
this.waveUi = waveUi;
}
/**
* Installs this feature.
*/
public static void install(EditSession edit, WavePanelImpl panel) {
// It might be feasible to turn this feature off for Firefox, since it has
// native support for keeping the focus in view. However, Firefox only does
// the bare minimum, keeping the focus right at the viewport edges.
new KeepFocusInView(edit, panel).init();
}
public void init() {
edit.addListener(this);
}
public void destroy() {
edit.removeListener(this);
}
@Override
public void onSessionStart(Editor editor, BlipView blipUi) {
viewport = hackExtractScrollElement(waveUi.getContents());
scroller = DomScrollPanel.create(viewport);
editor.addUpdateListener(this);
}
@Override
public void onSessionEnd(Editor editor, BlipView blipUi) {
editor.removeUpdateListener(this);
scroller = null;
viewport = null;
}
private boolean isEditing() {
return viewport != null;
}
@Override
public void onUpdate(EditorUpdateEvent event) {
// Check isEditing to prevent code from running during session teardown.
if (isEditing() && event.selectionCoordsChanged()) {
// First use a non-invasive approach for discovering the focus location.
// This query does not mutate the DOM, so will not force a layout cycle.
IntRange r = NativeSelectionUtil.getFocusBounds();
if (r != null) {
double focusStartInScreen = r.getFirst();
double focusEndInScreen = r.getSecond();
double viewportStartInScreen = measurer.top(null, viewport);
double viewportEndInScreen = measurer.bottom(null, viewport);
if (viewportStartInScreen < focusStartInScreen - PAD_PX
&& focusEndInScreen + PAD_PX < viewportEndInScreen) {
// All ok.
return;
}
}
// The fast path failed. Try a more invasive method. This query does
// mutate the DOM, so the subsequent measurement queries will force
// synchronous layout, which can be slow.
OffsetPosition p = NativeSelectionUtil.slowGetPosition();
if (p != null && p.offsetParent != null) {
Extent viewportInContent = scroller.getViewport();
double focusInViewport = measurer.top(viewport, p.offsetParent) + p.top;
double focusInContent = focusInViewport + viewportInContent.getStart();
if (focusInContent - PAD_PX < viewportInContent.getStart()) {
scroller.moveTo(focusInContent - PAD_PX);
} else if (focusInContent + PAD_PX > viewportInContent.getEnd()) {
scroller.moveTo(focusInContent + PAD_PX - viewportInContent.getSize());
} else {
// All ok.
return;
}
}
// No other options left. Maybe selection doesn't exist?
}
}
/**
* Cracks open a conversation UI object, and rips out the scrollable DOM
* element from it.
*/
//
// This method is a hack because the view interfaces are designed to hide all
// DOM concerns, so that presentation code is independent of a DOM-based
// implementation (e.g., so that it is server-side renderable, runnable in
// tests, runnable on other view implementations, etc). However, since focus
// and selection are essentially DOM-based concerns, this feature does not
// make sense to run in any other environment other than a DOM based one.
//
private static Element hackExtractScrollElement(TopConversationView waveUi) {
@SuppressWarnings("unchecked")
TopConversationViewImpl<TopConversationDomImpl> waveUiImpl =
(TopConversationViewImpl<TopConversationDomImpl>) waveUi;
return waveUiImpl.getIntrinsic().getThreadContainer();
}
}