From 41dbd9c7e48f714ab6b31e9a23dcdbe52a9ea4b2 Mon Sep 17 00:00:00 2001 From: JJ Allaire Date: Tue, 4 Sep 2012 10:58:30 -0400 Subject: [PATCH] improved keyboard handling for the help pane --- src/cpp/session/modules/SessionHelp.cpp | 16 +- .../org/rstudio/core/client/dom/WindowEx.java | 7 + .../client/events/NativeKeyDownEvent.java | 2 +- .../client/workbench/ui/PaneManager.java | 2 + .../workbench/ui/WorkbenchTabPanel.java | 15 +- .../workbench/views/console/Console.java | 2 + .../client/workbench/views/help/Help.java | 1 + .../client/workbench/views/help/HelpPane.java | 283 +++++++++++++++--- 8 files changed, 276 insertions(+), 52 deletions(-) diff --git a/src/cpp/session/modules/SessionHelp.cpp b/src/cpp/session/modules/SessionHelp.cpp index eb2c7d9158e..1644af4fdc8 100644 --- a/src/cpp/session/modules/SessionHelp.cpp +++ b/src/cpp/session/modules/SessionHelp.cpp @@ -200,12 +200,16 @@ bool handleRShowDocFile(const core::FilePath& filePath) } } -// javascript callbacks to inject into page so that next/prev buttons work -const char * const kJsNavigateCallbacks = +// javascript callbacks to inject into page +const char * const kJsCallbacks = ""; + " window.parent.helpNavigated(document, window);\n" + "if (window.parent.helpKeydown)\n" + " window.onkeydown = function(e) {window.parent.helpKeydown(e);}\n" + "\n"; + + class HelpContentsFilter : public boost::iostreams::aggregate_filter { @@ -239,7 +243,7 @@ class HelpContentsFilter : public boost::iostreams::aggregate_filter "src=\"" + baseUrl + "/"); // append javascript callbacks - std::string js(kJsNavigateCallbacks); + std::string js(kJsCallbacks); std::copy(js.begin(), js.end(), std::back_inserter(dest)); } private: @@ -252,7 +256,7 @@ class CustomHelprContentsFilter { void do_filter(const std::vector& src, std::vector& dest) { - std::string js(kJsNavigateCallbacks); + std::string js(kJsCallbacks); std::copy(src.begin(), src.end(), std::back_inserter(dest)); std::copy(js.begin(), js.end(), std::back_inserter(dest)); } diff --git a/src/gwt/src/org/rstudio/core/client/dom/WindowEx.java b/src/gwt/src/org/rstudio/core/client/dom/WindowEx.java index 63f894588e3..4e56cb40886 100644 --- a/src/gwt/src/org/rstudio/core/client/dom/WindowEx.java +++ b/src/gwt/src/org/rstudio/core/client/dom/WindowEx.java @@ -52,6 +52,13 @@ public final native void forward() /*-{ this.history.forward() ; }-*/; + public final native void removeSelection() /*-{ + selection = this.getSelection(); + if (selection != null) { + selection.removeAllRanges(); + } + }-*/; + public final native boolean find(String term, boolean matchCase, boolean searchUpward, diff --git a/src/gwt/src/org/rstudio/core/client/events/NativeKeyDownEvent.java b/src/gwt/src/org/rstudio/core/client/events/NativeKeyDownEvent.java index 8202f5cc169..121e7d56474 100644 --- a/src/gwt/src/org/rstudio/core/client/events/NativeKeyDownEvent.java +++ b/src/gwt/src/org/rstudio/core/client/events/NativeKeyDownEvent.java @@ -21,7 +21,7 @@ public class NativeKeyDownEvent extends GwtEvent public static final GwtEvent.Type TYPE = new GwtEvent.Type(); - protected NativeKeyDownEvent(NativeEvent event) + public NativeKeyDownEvent(NativeEvent event) { event_ = event; } diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java b/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java index 6b4e26b7040..50e5ebb95f6 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java @@ -24,6 +24,7 @@ import com.google.inject.name.Named; import org.rstudio.core.client.Debug; import org.rstudio.core.client.Triad; +import org.rstudio.core.client.dom.WindowEx; import org.rstudio.core.client.events.WindowStateChangeEvent; import org.rstudio.core.client.layout.DualWindowLayoutPanel; import org.rstudio.core.client.layout.LogicalWindow; @@ -254,6 +255,7 @@ public WorkbenchTab[] getAllTabs() public void activateTab(Tab tab) { + WindowEx.get().focus(); tabToPanel_.get(tab).selectTab(tabToIndex_.get(tab)); } diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/ui/WorkbenchTabPanel.java b/src/gwt/src/org/rstudio/studio/client/workbench/ui/WorkbenchTabPanel.java index eeb55a19e48..5552c96a146 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/ui/WorkbenchTabPanel.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/ui/WorkbenchTabPanel.java @@ -160,8 +160,21 @@ public void onEnsureVisible(EnsureVisibleEvent event) public void selectTab(int tabIndex) { if (tabPanel_.getSelectedIndex() == tabIndex) + { + // if it's already selected then we still want to fire the + // onBeforeSelected and onSelected methods (so that actions + // like auto-focus are always taken) + int selected = getSelectedIndex(); + if (selected != -1) + { + WorkbenchTab tab = tabs_.get(selected); + tab.onBeforeSelected(); + tab.onSelected(); + } + return; - + } + // deal with migrating from n+1 to n tabs, and with -1 values int safeIndex = Math.min(Math.max(0, tabIndex), tabs_.size() - 1); diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/console/Console.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/console/Console.java index cfcbb0cb28b..b4ba3d74669 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/console/Console.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/console/Console.java @@ -17,6 +17,7 @@ import com.google.inject.Inject; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.command.Handler; +import org.rstudio.core.client.dom.WindowEx; import org.rstudio.core.client.layout.DelayFadeInHelper; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.workbench.commands.Commands; @@ -80,6 +81,7 @@ public void onConsolePrompt(ConsolePromptEvent event) @Handler void onActivateConsole() { + WindowEx.get().focus(); view_.bringToFront(); view_.focus(); } diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/help/Help.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/help/Help.java index fed81e7310b..45bbc8fa80a 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/help/Help.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/help/Help.java @@ -50,6 +50,7 @@ public interface Display extends WorkbenchView, void print() ; void popout() ; void refresh() ; + void focus(); LinkMenu getHistory() ; diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/views/help/HelpPane.java b/src/gwt/src/org/rstudio/studio/client/workbench/views/help/HelpPane.java index b83d5905fc0..293c19f7b05 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/views/help/HelpPane.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/views/help/HelpPane.java @@ -24,6 +24,8 @@ import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.logical.shared.ResizeEvent; import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.event.shared.HandlerRegistration; @@ -35,9 +37,12 @@ import org.rstudio.core.client.BrowseCap; import org.rstudio.core.client.StringUtil; +import org.rstudio.core.client.command.KeyboardShortcut; +import org.rstudio.core.client.command.ShortcutManager; import org.rstudio.core.client.dom.ElementEx; import org.rstudio.core.client.dom.IFrameElementEx; import org.rstudio.core.client.dom.WindowEx; +import org.rstudio.core.client.events.NativeKeyDownEvent; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.theme.res.ThemeResources; import org.rstudio.core.client.widget.CanFocus; @@ -126,7 +131,7 @@ protected void onLoad() { initialized_ = true; - initHelpNavigateCallback() ; + initHelpCallbacks() ; Scheduler.get().scheduleDeferred(new ScheduledCommand() { @@ -138,7 +143,7 @@ public void execute() } } - public final native void initHelpNavigateCallback() /*-{ + public final native void initHelpCallbacks() /*-{ function addEventHandler(subject, eventName, handler) { if (subject.addEventListener) { subject.addEventListener(eventName, handler, false); @@ -158,8 +163,45 @@ function addEventHandler(subject, eventName, handler) { $wnd.helpNavigate = function(url) { thiz.@org.rstudio.studio.client.workbench.views.help.HelpPane::showHelp(Ljava/lang/String;)(url); } ; + + $wnd.helpKeydown = function(e) { + thiz.@org.rstudio.studio.client.workbench.views.help.HelpPane::handleKeyDown(Lcom/google/gwt/dom/client/NativeEvent;)(e); + } ; }-*/; + + + // delegate shortcuts which occur while Help has focus + + private void handleKeyDown(NativeEvent e) + { + // determine whether this key-combination means we should focus find + int mod = KeyboardShortcut.getModifierValue(e); + if ((mod == (BrowseCap.hasMetaKey() ? KeyboardShortcut.META + : KeyboardShortcut.CTRL)) && + e.getKeyCode() == 'F') + { + e.preventDefault(); + e.stopPropagation(); + WindowEx.get().focus(); + findTextBox_.focus(); + findTextBox_.selectAll(); + } + + // delegate to the shortcut manager + else + { + NativeKeyDownEvent evt = new NativeKeyDownEvent(e); + ShortcutManager.INSTANCE.onKeyDown(evt); + if (evt.isCanceled()) + { + e.preventDefault(); + e.stopPropagation(); + } + + } + } + private void helpNavigated(Document doc) { NodeList elements = doc.getElementsByTagName("a") ; @@ -271,68 +313,197 @@ public void onClick(ClickEvent event) title_.addClickHandler(clickHandler); image.addClickHandler(clickHandler); - if (BrowseCap.INSTANCE.hasWindowFind()) + if (isFindSupported()) { - final FindTextBox findTextBox = new FindTextBox("Find in Topic"); - findTextBox.setOverrideWidth(90); - toolbar.addLeftWidget(findTextBox); - final SmallButton findButton = new SmallButton("Find"); - findButton.setVisible(false); - findButton.addClickHandler(new ClickHandler() { - + final SmallButton btnNext = new SmallButton(">", true); + btnNext.setTitle("Find next (Enter)"); + btnNext.setVisible(false); + btnNext.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - // prevent two enter keys in rapid succession from - // maximizing or minimizing the help tab - event.stopPropagation(); - event.preventDefault(); - - // do the find - findInTopic(findTextBox.getValue().trim(), findTextBox); - } - + findNext(); + } + }); + + final SmallButton btnPrev = new SmallButton("<", true); + btnPrev.setTitle("Find previous"); + btnPrev.setVisible(false); + btnPrev.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) + { + findPrev(); + } }); - toolbar.addLeftWidget(findButton); - findTextBox.addKeyDownHandler(new KeyDownHandler() { + findTextBox_ = new FindTextBox("Find in Topic"); + findTextBox_.setOverrideWidth(90); + toolbar.addLeftWidget(findTextBox_); + findTextBox_.addKeyUpHandler(new KeyUpHandler() { + @Override - public void onKeyDown(KeyDownEvent event) + public void onKeyUp(KeyUpEvent event) + { + WindowEx contentWindow = getContentWindow(); + if (contentWindow != null) + { + // escape or tab means exit find mode and put focus + // into the main content window + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE || + event.getNativeKeyCode() == KeyCodes.KEY_TAB) + { + event.preventDefault(); + event.stopPropagation(); + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) + clearTerm(); + contentWindow.focus(); + } + else + { + // prevent two enter keys in rapid succession from + // minimizing or maximizing the help pane + if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) + { + event.preventDefault(); + event.stopPropagation(); + } + + // check for term + String term = findTextBox_.getValue().trim(); + + // if there is a term then search for it + if (term.length() > 0) + { + // make buttons visible + setButtonVisibility(true); + + // perform the find (check for incremental) + if (isIncrementalFindSupported()) + { + boolean incremental = + !event.isAnyModifierKeyDown() && + (event.getNativeKeyCode() != KeyCodes.KEY_ENTER); + + performFind(term, true, incremental); + } + else + { + if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) + performFind(term, true, false); + } + } + + // no term means clear term and remove selection + else + { + if (isIncrementalFindSupported()) + { + clearTerm(); + contentWindow.removeSelection(); + } + } + } + } + } + + private void clearTerm() { - // enter key triggers a find - if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) + findTextBox_.setValue(""); + setButtonVisibility(false); + } + + private void setButtonVisibility(final boolean visible) + { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() + { + btnNext.setVisible(visible); + btnPrev.setVisible(visible); + } + }); + } + }); + + findTextBox_.addKeyDownHandler(new KeyDownHandler() { + + @Override + public void onKeyDown(KeyDownEvent event) + { + // we handle these directly so prevent the browser + // from handling them + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE || + event.getNativeKeyCode() == KeyCodes.KEY_TAB || + event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { event.preventDefault(); event.stopPropagation(); - findButton.click(); - findTextBox.focus(); } - else if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) - { - findTextBox.setValue(""); - findButton.setVisible(false); - } - else - { - // other keys trigger visibility chagne of find button - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() - { - findButton.setVisible( - findTextBox.getValue().trim().length() > 0); - - } - }); - } } }); + + if (isIncrementalFindSupported()) + { + btnPrev.getElement().getStyle().setMarginRight(3, Unit.PX); + toolbar.addLeftWidget(btnPrev); + toolbar.addLeftWidget(btnNext); + } + } return toolbar ; } + + private String getTerm() + { + return findTextBox_.getValue().trim(); + } + + private void findNext() + { + String term = getTerm(); + if (term.length() > 0) + performFind(term, true, false); + } + + private void findPrev() + { + String term = getTerm(); + if (term.length() > 0) + performFind(term, false, false); + } + + private void performFind(String term, + boolean forwards, + boolean incremental) + { + WindowEx contentWindow = getContentWindow(); + if (contentWindow == null) + return; + + // if this is an incremental search then reset the selection first + if (incremental) + contentWindow.removeSelection(); + + contentWindow.find(term, false, !forwards, true, false); + } + + private boolean isFindSupported() + { + return BrowseCap.INSTANCE.hasWindowFind(); + } + + // Firefox changes focus during our typeahead search (it must take + // focus when you set the selection into the iframe) which breaks + // typeahead entirely. rather than code around this we simply + // disable it for Firefox + private boolean isIncrementalFindSupported() + { + return isFindSupported() && !BrowseCap.isFirefox(); + } public String getUrl() { @@ -356,6 +527,20 @@ public void showHelp(String url) navigated_ = true; } + @Override + public void bringToFront() + { + super.bringToFront(); + focus(); + } + + @Override + public void onSelected() + { + super.onSelected(); + focus(); + } + private void setLocation(final String url) { // allow subsequent calls to setLocation to override any previous @@ -433,6 +618,14 @@ public void popout() globalDisplay_.openWindow(href, options); } + @Override + public void focus() + { + WindowEx contentWindow = getContentWindow(); + if (contentWindow != null) + contentWindow.focus(); + } + public HandlerRegistration addHelpNavigateHandler(HelpNavigateHandler handler) { return addHandler(handler, HelpNavigateEvent.TYPE) ; @@ -470,11 +663,13 @@ private void findInTopic(String term, CanFocus findInputSource) } } + private final VirtualHistory navStack_ = new VirtualHistory() ; private final ToolbarLinkMenu history_ ; private Label title_ ; private Frame frame_ ; + private FindTextBox findTextBox_; private final Provider searchProvider_ ; private GlobalDisplay globalDisplay_; private final Commands commands_;