diff options
Diffstat (limited to 'gerrit-gwtexpui/src/main/java/com/google/gwtexpui')
63 files changed, 4583 insertions, 0 deletions
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml new file mode 100644 index 0000000000..0e9b0727f5 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml @@ -0,0 +1,20 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <inherits name='com.google.gwt.resources.Resources'/> + <inherits name="com.google.gwtexpui.safehtml.SafeHtml"/> + <inherits name="com.google.gwtexpui.user.User"/> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java new file mode 100644 index 0000000000..68495e8e45 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java @@ -0,0 +1,22 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.clippy.client; + +import com.google.gwt.resources.client.CssResource; + +public interface ClippyCss extends CssResource { + String label(); + String control(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java new file mode 100644 index 0000000000..4c2b8981d4 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java @@ -0,0 +1,25 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.clippy.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ClientBundle; + +public interface ClippyResources extends ClientBundle { + public static final ClippyResources I = GWT.create(ClippyResources.class); + + @Source("clippy.css") + ClippyCss css(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java new file mode 100644 index 0000000000..273318be51 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java @@ -0,0 +1,230 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.clippy.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.http.client.URL; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasText; +import com.google.gwt.user.client.ui.InlineLabel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwtexpui.safehtml.client.SafeHtml; +import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; +import com.google.gwtexpui.user.client.UserAgent; + +/** + * Label which permits the user to easily copy the complete content. + * <p> + * If the Flash plugin is available a "movie" is embedded that provides + * one-click copying of the content onto the system clipboard. The label (if + * visible) can also be clicked, switching from a label to an input box, + * allowing the user to copy the text with a keyboard shortcut. + */ +public class CopyableLabel extends Composite implements HasText { + private static final int SWF_WIDTH = 110; + private static final int SWF_HEIGHT = 14; + private static String swfUrl; + private static boolean flashEnabled = true; + + static { + ClippyResources.I.css().ensureInjected(); + } + + public static boolean isFlashEnabled() { + return flashEnabled; + } + + public static void setFlashEnabled(final boolean on) { + flashEnabled = on; + } + + private static String swfUrl() { + if (swfUrl == null) { + swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf"; + } + return swfUrl; + } + + private final FlowPanel content; + private String text; + private int visibleLen; + private Label textLabel; + private TextBox textBox; + private Element swf; + + /** + * Create a new label + * + * @param str initial content + */ + public CopyableLabel(final String str) { + this(str, true); + } + + /** + * Create a new label + * + * @param str initial content + * @param showLabel if true, the content is shown, if false it is hidden from + * view and only the copy icon is displayed. + */ + public CopyableLabel(final String str, final boolean showLabel) { + content = new FlowPanel(); + initWidget(content); + + text = str; + visibleLen = text.length(); + + if (showLabel) { + textLabel = new InlineLabel(getText()); + textLabel.setStyleName(ClippyResources.I.css().label()); + textLabel.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + showTextBox(); + } + }); + content.add(textLabel); + } + embedMovie(); + } + + /** + * Change the text which is displayed in the clickable label. + * + * @param text the new preview text, should be shorter than the original text + * which would be copied to the clipboard. + */ + public void setPreviewText(final String text) { + if (textLabel != null) { + textLabel.setText(text); + visibleLen = text.length(); + } + } + + private void embedMovie() { + if (flashEnabled && UserAgent.hasFlash) { + final String flashVars = "text=" + URL.encodeQueryString(getText()); + final SafeHtmlBuilder h = new SafeHtmlBuilder(); + + h.openElement("div"); + h.setStyleName(ClippyResources.I.css().control()); + + h.openElement("object"); + h.setWidth(SWF_WIDTH); + h.setHeight(SWF_HEIGHT); + h.setAttribute("classid", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"); + h.paramElement("movie", swfUrl()); + h.paramElement("FlashVars", flashVars); + + h.openElement("embed"); + h.setWidth(SWF_WIDTH); + h.setHeight(SWF_HEIGHT); + h.setAttribute("wmode", "transparent"); + h.setAttribute("type", "application/x-shockwave-flash"); + h.setAttribute("src", swfUrl()); + h.setAttribute("FlashVars", flashVars); + h.closeSelf(); + + h.closeElement("object"); + h.closeElement("div"); + + if (swf != null) { + DOM.removeChild(getElement(), swf); + } + DOM.appendChild(getElement(), swf = SafeHtml.parse(h)); + } + } + + public String getText() { + return text; + } + + public void setText(final String newText) { + text = newText; + visibleLen = newText.length(); + + if (textLabel != null) { + textLabel.setText(getText()); + } + if (textBox != null) { + textBox.setText(getText()); + textBox.selectAll(); + } + embedMovie(); + } + + private void showTextBox() { + if (textBox == null) { + textBox = new TextBox(); + textBox.setText(getText()); + textBox.setVisibleLength(visibleLen); + textBox.addKeyPressHandler(new KeyPressHandler() { + @Override + public void onKeyPress(final KeyPressEvent event) { + if (event.isControlKeyDown() || event.isMetaKeyDown()) { + switch (event.getCharCode()) { + case 'c': + case 'x': + Scheduler.get().scheduleDeferred(new Command() { + public void execute() { + hideTextBox(); + } + }); + break; + } + } + } + }); + textBox.addBlurHandler(new BlurHandler() { + @Override + public void onBlur(final BlurEvent event) { + hideTextBox(); + } + }); + content.insert(textBox, 1); + } + + textLabel.setVisible(false); + textBox.setVisible(true); + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + textBox.selectAll(); + textBox.setFocus(true); + } + }); + } + + private void hideTextBox() { + if (textBox != null) { + textBox.removeFromParent(); + textBox = null; + } + textLabel.setVisible(true); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css new file mode 100644 index 0000000000..b962df304d --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css @@ -0,0 +1,25 @@ +/* Copyright (C) 2009 The Android Open Source Project + * + * 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. + */ + +.label { + vertical-align: top; +} +.control { + margin-left: 5px; + display: inline-block !important; + height: 14px; + width: 14px; + overflow: hidden; +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf Binary files differnew file mode 100644 index 0000000000..e46886cd11 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml new file mode 100644 index 0000000000..b3859873a1 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml @@ -0,0 +1,19 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <define-linker name='cachecss' class='com.google.gwtexpui.css.rebind.CssLinker'/> + <add-linker name='cachecss'/> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java new file mode 100644 index 0000000000..a9a8a2422a --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java @@ -0,0 +1,130 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.css.rebind; + +import com.google.gwt.core.ext.LinkerContext; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.linker.AbstractLinker; +import com.google.gwt.core.ext.linker.Artifact; +import com.google.gwt.core.ext.linker.ArtifactSet; +import com.google.gwt.core.ext.linker.LinkerOrder; +import com.google.gwt.core.ext.linker.PublicResource; +import com.google.gwt.core.ext.linker.impl.StandardLinkerContext; +import com.google.gwt.core.ext.linker.impl.StandardStylesheetReference; +import com.google.gwt.dev.util.Util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; + +@LinkerOrder(LinkerOrder.Order.PRE) +public class CssLinker extends AbstractLinker { + @Override + public String getDescription() { + return "CssLinker"; + } + + @SuppressWarnings("unchecked") + @Override + public ArtifactSet link(final TreeLogger logger, final LinkerContext context, + final ArtifactSet artifacts) throws UnableToCompleteException { + final ArtifactSet returnTo = new ArtifactSet(); + int index = 0; + + final HashMap<String, PublicResource> css = + new HashMap<String, PublicResource>(); + + for (final StandardStylesheetReference ssr : artifacts + .<StandardStylesheetReference> find(StandardStylesheetReference.class)) { + css.put(ssr.getSrc(), null); + } + for (final PublicResource pr : artifacts + .<PublicResource> find(PublicResource.class)) { + if (css.containsKey(pr.getPartialPath())) { + css.put(pr.getPartialPath(), new CssPubRsrc(name(logger, pr), pr)); + } + } + + for (Artifact a : artifacts) { + if (a instanceof PublicResource) { + final PublicResource r = (PublicResource) a; + if (css.containsKey(r.getPartialPath())) { + a = css.get(r.getPartialPath()); + } + } else if (a instanceof StandardStylesheetReference) { + final StandardStylesheetReference r = (StandardStylesheetReference) a; + final PublicResource p = css.get(r.getSrc()); + a = new StandardStylesheetReference(p.getPartialPath(), index); + } + + returnTo.add(a); + index++; + } + return returnTo; + } + + private String name(final TreeLogger logger, final PublicResource r) + throws UnableToCompleteException { + final InputStream in = r.getContents(logger); + final ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + try { + try { + final byte[] buf = new byte[2048]; + int n; + while ((n = in.read(buf)) >= 0) { + tmp.write(buf, 0, n); + } + tmp.close(); + } finally { + in.close(); + } + } catch (IOException e) { + final UnableToCompleteException ute = new UnableToCompleteException(); + ute.initCause(e); + throw ute; + } + + String base = r.getPartialPath(); + final int s = base.lastIndexOf('/'); + if (0 < s) { + base = base.substring(0, s + 1); + } else { + base = ""; + } + return base + Util.computeStrongName(tmp.toByteArray()) + ".cache.css"; + } + + private static class CssPubRsrc extends PublicResource { + private final PublicResource src; + + CssPubRsrc(final String partialPath, final PublicResource r) { + super(StandardLinkerContext.class, partialPath); + src = r; + } + + @Override + public InputStream getContents(final TreeLogger logger) + throws UnableToCompleteException { + return src.getContents(logger); + } + + @Override + public long getLastModified() { + return src.getLastModified(); + } + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml new file mode 100644 index 0000000000..771050f203 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml @@ -0,0 +1,20 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <inherits name='com.google.gwt.resources.Resources'/> + <inherits name='com.google.gwtexpui.user.User'/> + <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java new file mode 100644 index 0000000000..304d56ea39 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java @@ -0,0 +1,40 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.KeyPressEvent; + +public final class CompoundKeyCommand extends KeyCommand { + final KeyCommandSet set; + + public CompoundKeyCommand(int mask, char key, String help, KeyCommandSet s) { + super(mask, key, help); + set = s; + } + + public CompoundKeyCommand(int mask, int key, String help, KeyCommandSet s) { + super(mask, key, help); + set = s; + } + + public KeyCommandSet getSet() { + return set; + } + + @Override + public void onKeyPress(final KeyPressEvent event) { + GlobalKey.temporaryWithTimeout(set); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java new file mode 100644 index 0000000000..d680a72130 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java @@ -0,0 +1,51 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Node; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; + +class DocWidget extends Widget implements HasKeyPressHandlers { + private static DocWidget me; + + static DocWidget get() { + if (me == null) { + me = new DocWidget(); + } + return me; + } + + private DocWidget() { + setElement((Element) docnode()); + onAttach(); + RootPanel.detachOnWindowClose(this); + } + + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + private static Node docnode() { + return Document.get(); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java new file mode 100644 index 0000000000..1eaaa3cfdc --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java @@ -0,0 +1,183 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; + + +public class GlobalKey { + public static final KeyPressHandler STOP_PROPAGATION = new KeyPressHandler() { + @Override + public void onKeyPress(final KeyPressEvent event) { + event.stopPropagation(); + } + }; + + private static State global; + static State active; + private static CloseHandler<PopupPanel> restoreGlobal; + private static Timer restoreTimer; + + static { + KeyResources.I.css().ensureInjected(); + } + + private static void initEvents() { + if (active == null) { + DocWidget.get().addKeyPressHandler(new KeyPressHandler() { + @Override + public void onKeyPress(final KeyPressEvent event) { + final KeyCommandSet s = active.live; + if (s != active.all) { + active.live = active.all; + restoreTimer.cancel(); + } + s.onKeyPress(event); + } + }); + + restoreTimer = new Timer() { + @Override + public void run() { + active.live = active.all; + } + }; + + global = new State(null); + active = global; + } + } + + private static void initDialog() { + if (restoreGlobal == null) { + restoreGlobal = new CloseHandler<PopupPanel>() { + @Override + public void onClose(final CloseEvent<PopupPanel> event) { + active = global; + } + }; + } + } + + static void temporaryWithTimeout(final KeyCommandSet s) { + active.live = s; + restoreTimer.schedule(250); + } + + public static void dialog(final PopupPanel panel) { + initEvents(); + initDialog(); + assert panel.isShowing(); + assert active == global; + active = new State(panel); + active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel)); + panel.addCloseHandler(restoreGlobal); + } + + public static HandlerRegistration addApplication(final Widget widget, + final KeyCommand appKey) { + initEvents(); + final State state = stateFor(widget); + state.add(appKey); + return new HandlerRegistration() { + @Override + public void removeHandler() { + state.remove(appKey); + } + }; + } + + public static HandlerRegistration add(final Widget widget, + final KeyCommandSet cmdSet) { + initEvents(); + final State state = stateFor(widget); + state.add(cmdSet); + return new HandlerRegistration() { + @Override + public void removeHandler() { + state.remove(cmdSet); + } + }; + } + + private static State stateFor(Widget w) { + while (w != null) { + if (w == active.root) { + return active; + } + w = w.getParent(); + } + return global; + } + + public static void filter(final KeyCommandFilter filter) { + active.filter(filter); + if (active != global) { + global.filter(filter); + } + } + + private GlobalKey() { + } + + static class State { + final Widget root; + final KeyCommandSet app; + final KeyCommandSet all; + KeyCommandSet live; + + State(final Widget r) { + root = r; + + app = new KeyCommandSet(KeyConstants.I.applicationSection()); + app.add(ShowHelpCommand.INSTANCE); + + all = new KeyCommandSet(); + all.add(app); + + live = all; + } + + void add(final KeyCommand k) { + app.add(k); + all.add(k); + } + + void remove(final KeyCommand k) { + app.remove(k); + all.remove(k); + } + + void add(final KeyCommandSet s) { + all.add(s); + } + + void remove(final KeyCommandSet s) { + all.remove(s); + } + + void filter(final KeyCommandFilter f) { + all.filter(f); + } + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java new file mode 100644 index 0000000000..0274b9d2f6 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java @@ -0,0 +1,33 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.user.client.ui.PopupPanel; + +/** Hides the given popup panel when invoked. */ +public class HidePopupPanelCommand extends KeyCommand { + private final PopupPanel panel; + + public HidePopupPanelCommand(int mask, int key, PopupPanel panel) { + super(mask, key, KeyConstants.I.closeCurrentDialog()); + this.panel = panel; + } + + @Override + public void onKeyPress(final KeyPressEvent event) { + panel.hide(); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java new file mode 100644 index 0000000000..ba4f62649d --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java @@ -0,0 +1,94 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwtexpui.safehtml.client.SafeHtml; +import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; + + +public abstract class KeyCommand implements KeyPressHandler { + public static final int M_CTRL = 1 << 16; + public static final int M_ALT = 2 << 16; + public static final int M_META = 4 << 16; + + public static boolean same(final KeyCommand a, final KeyCommand b) { + return a.getClass() == b.getClass() && a.helpText.equals(b.helpText); + } + + final int keyMask; + private final String helpText; + + public KeyCommand(final int mask, final int key, final String help) { + this(mask, (char) key, help); + } + + public KeyCommand(final int mask, final char key, final String help) { + assert help != null; + keyMask = mask | key; + helpText = help; + } + + public String getHelpText() { + return helpText; + } + + SafeHtml describeKeyStroke() { + final SafeHtmlBuilder b = new SafeHtmlBuilder(); + + if ((keyMask & M_CTRL) == M_CTRL) { + modifier(b, KeyConstants.I.keyCtrl()); + } + if ((keyMask & M_ALT) == M_ALT) { + modifier(b, KeyConstants.I.keyAlt()); + } + if ((keyMask & M_META) == M_META) { + modifier(b, KeyConstants.I.keyMeta()); + } + + final char c = (char) (keyMask & 0xffff); + switch (c) { + case KeyCodes.KEY_ENTER: + namedKey(b, KeyConstants.I.keyEnter()); + break; + case KeyCodes.KEY_ESCAPE: + namedKey(b, KeyConstants.I.keyEsc()); + break; + default: + b.openSpan(); + b.setStyleName(KeyResources.I.css().helpKey()); + b.append(String.valueOf(c)); + b.closeSpan(); + break; + } + + return b; + } + + private void modifier(final SafeHtmlBuilder b, final String name) { + namedKey(b, name); + b.append(" + "); + } + + private void namedKey(final SafeHtmlBuilder b, final String name) { + b.append('<'); + b.openSpan(); + b.setStyleName(KeyResources.I.css().helpKey()); + b.append(name); + b.closeSpan(); + b.append(">"); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java new file mode 100644 index 0000000000..05f41d4b63 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java @@ -0,0 +1,19 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +public interface KeyCommandFilter { + public boolean include(KeyCommand key); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java new file mode 100644 index 0000000000..4f3205abff --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java @@ -0,0 +1,136 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class KeyCommandSet implements KeyPressHandler { + private final Map<Integer, KeyCommand> map; + private List<KeyCommandSet> sets; + private String name; + + public KeyCommandSet() { + this(""); + } + + public KeyCommandSet(final String setName) { + map = new HashMap<Integer, KeyCommand>(); + name = setName; + } + + public String getName() { + return name; + } + + public void setName(final String setName) { + assert setName != null; + name = setName; + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + public void add(final KeyCommand k) { + assert !map.containsKey(k.keyMask) + : "Key " + k.describeKeyStroke().asString() + + " already registered"; + if (!map.containsKey(k.keyMask)) { + map.put(k.keyMask, k); + } + } + + public void remove(final KeyCommand k) { + assert map.get(k.keyMask) == k; + map.remove(k.keyMask); + } + + public void add(final KeyCommandSet set) { + if (sets == null) { + sets = new ArrayList<KeyCommandSet>(); + } + assert !sets.contains(set); + sets.add(set); + for (final KeyCommand k : set.map.values()) { + add(k); + } + } + + public void remove(final KeyCommandSet set) { + assert sets != null; + assert sets.contains(set); + sets.remove(set); + for (final KeyCommand k : set.map.values()) { + remove(k); + } + } + + public void filter(final KeyCommandFilter filter) { + if (sets != null) { + for (final KeyCommandSet s : sets) { + s.filter(filter); + } + } + for (final Iterator<KeyCommand> i = map.values().iterator(); i.hasNext();) { + final KeyCommand kc = i.next(); + if (!filter.include(kc)) { + i.remove(); + } else if (kc instanceof CompoundKeyCommand) { + ((CompoundKeyCommand) kc).set.filter(filter); + } + } + } + + public Collection<KeyCommand> getKeys() { + return map.values(); + } + + public Collection<KeyCommandSet> getSets() { + return sets != null ? sets : Collections.<KeyCommandSet> emptyList(); + } + + @Override + public void onKeyPress(final KeyPressEvent event) { + final KeyCommand k = map.get(toMask(event)); + if (k != null) { + event.preventDefault(); + event.stopPropagation(); + k.onKeyPress(event); + } + } + + static int toMask(final KeyPressEvent event) { + int mask = event.getCharCode(); + if (event.isAltKeyDown()) { + mask |= KeyCommand.M_ALT; + } + if (event.isControlKeyDown()) { + mask |= KeyCommand.M_CTRL; + } + if (event.isMetaKeyDown()) { + mask |= KeyCommand.M_META; + } + return mask; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java new file mode 100644 index 0000000000..56fb85c71e --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java @@ -0,0 +1,37 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.i18n.client.Constants; + +public interface KeyConstants extends Constants { + public static final KeyConstants I = GWT.create(KeyConstants.class); + + String applicationSection(); + String showHelp(); + String closeCurrentDialog(); + + String keyboardShortcuts(); + String closeButton(); + String orOtherKey(); + String thenOtherKey(); + + String keyCtrl(); + String keyAlt(); + String keyMeta(); + String keyEnter(); + String keyEsc(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties new file mode 100644 index 0000000000..e21daf502e --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties @@ -0,0 +1,14 @@ +applicationSection = Application +showHelp = Open shortcut help +closeCurrentDialog = Close current dialog + +keyboardShortcuts = Keyboard Shortcuts +closeButton = Close +orOtherKey = or +thenOtherKey = then + +keyCtrl = Ctrl +keyAlt = Alt +keyMeta = Meta +keyEnter = Enter +keyEsc = Esc diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java new file mode 100644 index 0000000000..d19018de8f --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java @@ -0,0 +1,29 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.resources.client.CssResource; + +public interface KeyCss extends CssResource { + String helpPopup(); + String helpHeader(); + String helpHeaderGlue(); + String helpTable(); + String helpTableGlue(); + String helpGroup(); + String helpKeyStroke(); + String helpSeparator(); + String helpKey(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java new file mode 100644 index 0000000000..7bd023396d --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java @@ -0,0 +1,228 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.FocusPanel; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HasHorizontalAlignment; +import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; +import com.google.gwtexpui.safehtml.client.SafeHtml; +import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; +import com.google.gwtexpui.user.client.PluginSafePopupPanel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; + + +public class KeyHelpPopup extends PluginSafePopupPanel implements + KeyPressHandler { + private final FocusPanel focus; + + public KeyHelpPopup() { + super(true/* autohide */, true/* modal */); + setStyleName(KeyResources.I.css().helpPopup()); + + final Anchor closer = new Anchor(KeyConstants.I.closeButton()); + closer.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + hide(); + } + }); + + final Grid header = new Grid(1, 3); + header.setStyleName(KeyResources.I.css().helpHeader()); + header.setText(0, 0, KeyConstants.I.keyboardShortcuts()); + header.setWidget(0, 2, closer); + + final CellFormatter fmt = header.getCellFormatter(); + fmt.addStyleName(0, 1, KeyResources.I.css().helpHeaderGlue()); + fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT); + + final Grid lists = new Grid(0, 7); + lists.setStyleName(KeyResources.I.css().helpTable()); + populate(lists); + lists.getCellFormatter().addStyleName(0, 3, + KeyResources.I.css().helpTableGlue()); + + final FlowPanel body = new FlowPanel(); + body.add(header); + DOM.appendChild(body.getElement(), DOM.createElement("hr")); + body.add(lists); + + focus = new FocusPanel(body); + DOM.setStyleAttribute(focus.getElement(), "outline", "0px"); + DOM.setElementAttribute(focus.getElement(), "hideFocus", "true"); + focus.addKeyPressHandler(this); + add(focus); + } + + @Override + public void setVisible(final boolean show) { + super.setVisible(show); + if (show) { + focus.setFocus(true); + } + } + + @Override + public void onKeyPress(final KeyPressEvent event) { + if (KeyCommandSet.toMask(event) == ShowHelpCommand.INSTANCE.keyMask) { + // Block the '?' key from triggering us to show right after + // we just hide ourselves. + // + event.stopPropagation(); + event.preventDefault(); + } + hide(); + } + + private void populate(final Grid lists) { + int end[] = new int[5]; + int column = 0; + for (final KeyCommandSet set : combinedSetsByName()) { + int row = end[column]; + row = formatGroup(lists, row, column, set); + end[column] = row; + if (column == 0) { + column = 4; + } else { + column = 0; + } + } + } + + /** + * @return an ordered collection of KeyCommandSet, combining sets which share + * the same name, so that each set name appears at most once. + */ + private static Collection<KeyCommandSet> combinedSetsByName() { + final LinkedHashMap<String, KeyCommandSet> byName = + new LinkedHashMap<String, KeyCommandSet>(); + for (final KeyCommandSet set : GlobalKey.active.all.getSets()) { + KeyCommandSet v = byName.get(set.getName()); + if (v == null) { + v = new KeyCommandSet(set.getName()); + byName.put(v.getName(), v); + } + v.add(set); + } + return byName.values(); + } + + private int formatGroup(final Grid lists, int row, final int col, + final KeyCommandSet set) { + if (set.isEmpty()) { + return row; + } + + if (lists.getRowCount() < row + 1) { + lists.resizeRows(row + 1); + } + lists.setText(row, col + 2, set.getName()); + lists.getCellFormatter().addStyleName(row, col + 2, + KeyResources.I.css().helpGroup()); + row++; + + return formatKeys(lists, row, col, set, null); + } + + private int formatKeys(final Grid lists, int row, final int col, + final KeyCommandSet set, final SafeHtml prefix) { + final CellFormatter fmt = lists.getCellFormatter(); + final int initialRow = row; + final List<KeyCommand> keys = sort(set); + if (lists.getRowCount() < row + keys.size()) { + lists.resizeRows(row + keys.size()); + } + FORMAT_KEYS: for (int i = 0; i < keys.size(); i++) { + final KeyCommand k = keys.get(i); + + if (k instanceof CompoundKeyCommand) { + final SafeHtmlBuilder b = new SafeHtmlBuilder(); + b.append(k.describeKeyStroke()); + row = formatKeys(lists, row, col, ((CompoundKeyCommand) k).getSet(), b); + continue; + } + + for (int prior = 0; prior < i; prior++) { + if (KeyCommand.same(keys.get(prior), k)) { + final int r = initialRow + prior; + final SafeHtmlBuilder b = new SafeHtmlBuilder(); + b.append(SafeHtml.get(lists, r, col + 0)); + b.append(" "); + b.append(KeyConstants.I.orOtherKey()); + b.append(" "); + if (prefix != null) { + b.append(prefix); + b.append(" "); + b.append(KeyConstants.I.thenOtherKey()); + b.append(" "); + } + b.append(k.describeKeyStroke()); + SafeHtml.set(lists, r, col + 0, b); + continue FORMAT_KEYS; + } + } + + if (prefix != null) { + final SafeHtmlBuilder b = new SafeHtmlBuilder(); + b.append(prefix); + b.append(" "); + b.append(KeyConstants.I.thenOtherKey()); + b.append(" "); + b.append(k.describeKeyStroke()); + SafeHtml.set(lists, row, col + 0, b); + } else { + SafeHtml.set(lists, row, col + 0, k.describeKeyStroke()); + } + lists.setText(row, col + 1, ":"); + lists.setText(row, col + 2, k.getHelpText()); + + fmt.addStyleName(row, col + 0, KeyResources.I.css().helpKeyStroke()); + fmt.addStyleName(row, col + 1, KeyResources.I.css().helpSeparator()); + row++; + } + + return row; + } + + private List<KeyCommand> sort(final KeyCommandSet set) { + final List<KeyCommand> keys = new ArrayList<KeyCommand>(set.getKeys()); + Collections.sort(keys, new Comparator<KeyCommand>() { + @Override + public int compare(KeyCommand arg0, KeyCommand arg1) { + if (arg0.keyMask < arg1.keyMask) { + return -1; + } else if (arg0.keyMask > arg1.keyMask) { + return 1; + } + return 0; + } + }); + return keys; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java new file mode 100644 index 0000000000..a52ca2a5e9 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java @@ -0,0 +1,25 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ClientBundle; + +public interface KeyResources extends ClientBundle { + public static final KeyResources I = GWT.create(KeyResources.class); + + @Source("key.css") + KeyCss css(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java new file mode 100644 index 0000000000..c06d2c427f --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java @@ -0,0 +1,34 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.TextArea; + +public class NpTextArea extends TextArea { + public NpTextArea() { + addKeyPressHandler(GlobalKey.STOP_PROPAGATION); + } + + public NpTextArea(final Element element) { + super(element); + addKeyPressHandler(GlobalKey.STOP_PROPAGATION); + } + + public void setSpellCheck(boolean spell) { + DOM.setElementPropertyBoolean(getElement(), "spellcheck", spell); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java new file mode 100644 index 0000000000..86402e1771 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java @@ -0,0 +1,29 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.TextBox; + +public class NpTextBox extends TextBox { + public NpTextBox() { + addKeyPressHandler(GlobalKey.STOP_PROPAGATION); + } + + public NpTextBox(final Element element) { + super(element); + addKeyPressHandler(GlobalKey.STOP_PROPAGATION); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java new file mode 100644 index 0000000000..50a4a86d60 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java @@ -0,0 +1,61 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.globalkey.client; + +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; + + +public class ShowHelpCommand extends KeyCommand { + public static final ShowHelpCommand INSTANCE = new ShowHelpCommand(); + private static KeyHelpPopup current; + + public ShowHelpCommand() { + super(0, '?', KeyConstants.I.showHelp()); + } + + @Override + public void onKeyPress(final KeyPressEvent event) { + if (current != null) { + // Already open? Close the dialog. + // + current.hide(); + current = null; + return; + } + + final KeyHelpPopup help = new KeyHelpPopup(); + help.addCloseHandler(new CloseHandler<PopupPanel>() { + @Override + public void onClose(final CloseEvent<PopupPanel> event) { + current = null; + } + }); + current = help; + help.setPopupPositionAndShow(new PositionCallback() { + @Override + public void setPosition(final int pWidth, final int pHeight) { + final int left = (Window.getClientWidth() - pWidth) >> 1; + final int wLeft = Window.getScrollLeft(); + final int wTop = Window.getScrollTop(); + help.setPopupPosition(wLeft + left, wTop + 50); + } + }); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css new file mode 100644 index 0000000000..9372e45a6e --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css @@ -0,0 +1,99 @@ +/* Copyright (C) 2009 The Android Open Source Project + * + * 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. + */ + +@external .popupContent; + +.helpPopup { + background: #000000 none repeat scroll 0 50%; + color: #ffffff; + font-family: arial,sans-serif; + font-weight: bold; + overflow: hidden; + text-align: left; + text-shadow: 1px 1px 7px #000000; + width: 92%; + z-index: 1002; + opacity: 0.85; + } + +@if user.agent safari { + .helpPopup { + \-webkit-border-radius: 10px; + } +} +@if user.agent gecko1_8 { + .helpPopup { + \-moz-border-radius: 10px; + } +} + +.helpPopup .popupContent { + margin: 10px; +} + +.helpPopup hr { + width: 100%; +} + +.helpHeader { + width: 100%; +} + +.helpHeader td { + white-space: nowrap; + color: #ffffff; +} + +.helpHeader a, +.helpHeader a:visited, +.helpHeader a:hover { + color: #dddd00; +} + +.helpHeaderGlue { + width: 100%; +} + +.helpTable { + width: 90%; +} +.helpTable td { + vertical-align: top; + white-space: nowrap; +} + +.helpTableGlue { + width: 25px; +} + +.helpGroup { + color: #dddd00; + padding-top: 0.8em; + text-align: left; +} + +.helpKeyStroke { + text-align: right; +} + +.helpSeparator { + width: 0.5em; + text-align: center; + font-weight: bold; +} + +.helpKey { + color: #dddd00; +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml new file mode 100644 index 0000000000..a6978ab1da --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml @@ -0,0 +1,19 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <define-linker name='serverplanned' class='com.google.gwtexpui.linker.rebind.ServerPlannedIFrameLinker'/> + <add-linker name='serverplanned'/> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java new file mode 100644 index 0000000000..6fd8f21191 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java @@ -0,0 +1,68 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.linker.rebind; + +import com.google.gwt.core.ext.LinkerContext; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.linker.AbstractLinker; +import com.google.gwt.core.ext.linker.ArtifactSet; +import com.google.gwt.core.ext.linker.CompilationResult; +import com.google.gwt.core.ext.linker.LinkerOrder; +import com.google.gwt.core.ext.linker.SelectionProperty; +import com.google.gwt.core.ext.linker.StylesheetReference; + +import java.util.Map; +import java.util.SortedMap; + +/** Saves data normally used by the {@code nocache.js} file. */ +@LinkerOrder(LinkerOrder.Order.POST) +public class ServerPlannedIFrameLinker extends AbstractLinker { + @Override + public String getDescription() { + return "ServerPlannedIFrameLinker"; + } + + @SuppressWarnings("unchecked") + @Override + public ArtifactSet link(final TreeLogger logger, final LinkerContext context, + final ArtifactSet artifacts) throws UnableToCompleteException { + ArtifactSet toReturn = new ArtifactSet(artifacts); + + StringBuilder table = new StringBuilder(); + for (StylesheetReference r : artifacts.find(StylesheetReference.class)) { + table.append("css "); + table.append(r.getSrc()); + table.append("\n"); + } + + for (CompilationResult r : artifacts.find(CompilationResult.class)) { + table.append(r.getStrongName() + "\n"); + for (SortedMap<SelectionProperty, String> p : r.getPropertyMap()) { + for (Map.Entry<SelectionProperty, String> e : p.entrySet()) { + table.append(" "); + table.append(e.getKey().getName()); + table.append("="); + table.append(e.getValue()); + table.append('\n'); + } + } + table.append("\n"); + } + + toReturn.add(emitString(logger, table.toString(), "permutations")); + return toReturn; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java new file mode 100644 index 0000000000..89da5292bf --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java @@ -0,0 +1,36 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.linker.server; + +import javax.servlet.http.HttpServletRequest; + +/** A rule that must execute on the client, as we don't know how to compute it. */ +final class ClientSideRule implements Rule { + private final String name; + + ClientSideRule(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String select(HttpServletRequest req) { + return null; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java new file mode 100644 index 0000000000..b319db1458 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java @@ -0,0 +1,160 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.linker.server; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +/** A single permutation of the compiled GWT application. */ +public class Permutation { + private final PermutationSelector selector; + private final String cacheHTML; + private final String[] values; + + Permutation(PermutationSelector sel, String cacheHTML, String[] values) { + this.selector = sel; + this.cacheHTML = cacheHTML; + this.values = values; + } + + boolean matches(String[] r) { + return Arrays.equals(values, r); + } + + /** + * Append GWT bootstrap for this permutation onto the end of the body. + * <p> + * The GWT bootstrap for this particular permutation is appended onto the end + * of the {@code body} element of the passed host page. + * <p> + * To keep the bootstrap code small and simple, not all GWT features are + * actually supported. The {@code gwt:property}, {@code gwt:onPropertyErrorFn} + * and {@code gwt:onLoadErrorFn} meta tags are ignored and not handled. + * <p> + * Load order may differ from the standard GWT {@code nocache.js}. The browser + * is asked to load the iframe immediately, rather than after the body has + * finished loading. + * + * @param dom host page HTML document. + */ + public void inject(Document dom) { + String moduleName = selector.getModuleName(); + String moduleFunc = moduleName; + + StringBuilder s = new StringBuilder(); + s.append("\n"); + s.append("function " + moduleFunc + "(){"); + s.append("var s,l,t"); + s.append(",w=window"); + s.append(",d=document"); + s.append(",n='" + moduleName + "'"); + s.append(",f=d.createElement('iframe')"); + s.append(";"); + + // Callback to execute the module once both s and l are true. + // + s.append("function m(){"); + s.append("if(s&&l){"); + // Base path needs to be absolute. There isn't an easy way to do this + // other than forcing an image to load and then pulling the URL back. + // + s.append("var b,i=d.createElement('img');"); + s.append("i.src=n+'/clear.cache.gif';"); + s.append("b=i.src;"); + s.append("b=b.substring(0,b.lastIndexOf('/')+1);"); + s.append(moduleFunc + "=null;"); // allow us to GC + s.append("f.contentWindow.gwtOnLoad(undefined,n,b);"); + s.append("}"); + s.append("}"); + + // Set s true when the module script has finished loading. The + // exact name here is known to the IFrameLinker and is called by + // the code in the iframe. + // + s.append(moduleFunc + ".onScriptLoad=function(){"); + s.append("s=1;m();"); + s.append("};"); + + // Set l true when the browser has finished processing the iframe + // tag, and everything else on the page. + // + s.append(moduleFunc + ".r=function(){"); + s.append("l=1;m();"); + s.append("};"); + + // Prevents mixed mode security in IE6/7. + s.append("f.src=\"javascript:''\";"); + s.append("f.id=n;"); + s.append("f.style.cssText" + + "='position:absolute;width:0;height:0;border:none';"); + s.append("f.tabIndex=-1;"); + s.append("d.body.appendChild(f);"); + + // The src has to be set after the iframe is attached to the DOM to avoid + // refresh quirks in Safari. We have to use the location.replace trick to + // avoid FF2 refresh quirks. + // + s.append("f.contentWindow.location.replace(n+'/" + cacheHTML + "');"); + + // defer attribute here is to workaround IE running immediately. + // + s.append("d.write('<script defer=\"defer\">" // + + moduleFunc + ".r()</'+'script>');"); + s.append("}"); + s.append(moduleFunc + "();"); + s.append("\n//"); + + final Element html = dom.getDocumentElement(); + final Element head = (Element) html.getElementsByTagName("head").item(0); + final Element body = (Element) html.getElementsByTagName("body").item(0); + + for (String css : selector.getCSS()) { + if (isRelativeURL(css)) { + css = moduleName + '/' + css; + } + + final Element link = dom.createElement("link"); + link.setAttribute("rel", "stylesheet"); + link.setAttribute("href", css); + head.appendChild(link); + } + + final Element script = dom.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("language", "javascript"); + script.appendChild(dom.createComment(s.toString())); + body.appendChild(script); + } + + private static boolean isRelativeURL(String src) { + if (src.startsWith("/")) { + return false; + } + + try { + // If it parses as a URL, assume it is not relative. + // + new URL(src); + return false; + } catch (MalformedURLException e) { + } + + return true; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java new file mode 100644 index 0000000000..d3e5ae35e4 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java @@ -0,0 +1,205 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.linker.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * Selects a permutation based on the HTTP request. + * <p> + * To use this class the application's GWT module must include our linker by + * inheriting our module: + * + * <pre> + * <inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/> + * </pre> + */ +public class PermutationSelector { + private final String moduleName; + private final Map<String, Rule> rulesByName; + private final List<Rule> ruleOrder; + private final List<Permutation> permutations; + private final List<String> css; + + /** + * Create an empty selector for a module. + * <p> + * {@link UserAgentRule} rule is automatically registered. Additional custom + * selector rules may be registered before {@link #init(ServletContext)} is + * called to finish the selector setup. + * + * @param moduleName the name of the module within the context. + */ + public PermutationSelector(final String moduleName) { + this.moduleName = moduleName; + + this.rulesByName = new HashMap<String, Rule>(); + this.ruleOrder = new ArrayList<Rule>(); + this.permutations = new ArrayList<Permutation>(); + this.css = new ArrayList<String>(); + + register(new UserAgentRule()); + } + + private void notInitialized() { + if (!ruleOrder.isEmpty()) { + throw new IllegalStateException("Already initialized"); + } + } + + /** + * Register a property selection rule. + * + * @param r the rule implementation. + */ + public void register(Rule r) { + notInitialized(); + rulesByName.put(r.getName(), r); + } + + /** + * Initialize the selector by reading the module's {@code permutations} file. + * + * @param ctx context to load the module data from. + * @throws ServletException + * @throws IOException + */ + public void init(ServletContext ctx) throws ServletException, IOException { + notInitialized(); + + final String tableName = "/" + moduleName + "/permutations"; + final InputStream in = ctx.getResourceAsStream(tableName); + if (in == null) { + throw new ServletException("No " + tableName + " in context"); + } + try { + BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8")); + for (;;) { + final String strongName = r.readLine(); + if (strongName == null) { + break; + } + + if (strongName.startsWith("css ")) { + css.add(strongName.substring("css ".length())); + continue; + } + + Map<String, String> selections = new LinkedHashMap<String, String>(); + for (;;) { + String permutation = r.readLine(); + if (permutation == null || permutation.isEmpty()) { + break; + } + + int eq = permutation.indexOf('='); + if (eq < 0) { + throw new ServletException(tableName + " has malformed content"); + } + + String k = permutation.substring(0, eq).trim(); + String v = permutation.substring(eq + 1); + + Rule rule = get(k); + if (!ruleOrder.contains(rule)) { + ruleOrder.add(rule); + } + + if (selections.put(k, v) != null) { + throw new ServletException("Table " + tableName + + " has multiple values for " + k + " within permutation " + + strongName); + } + } + + String cacheHtml = strongName + ".cache.html"; + String[] values = new String[ruleOrder.size()]; + for (int i = 0; i < values.length; i++) { + values[i] = selections.get(ruleOrder.get(i).getName()); + } + permutations.add(new Permutation(this, cacheHtml, values)); + } + } finally { + in.close(); + } + } + + private Rule get(final String name) { + Rule r = rulesByName.get(name); + if (r == null) { + r = new ClientSideRule(name); + register(r); + } + return r; + } + + /** @return name of the module (within the application context). */ + public String getModuleName() { + return moduleName; + } + + /** @return all possible permutations */ + public List<Permutation> getPermutations() { + return Collections.unmodifiableList(permutations); + } + + /** + * Select the permutation that best matches the browser request. + * + * @param req current request. + * @return the selected permutation; null if no permutation can be fit to the + * request and the standard {@code nocache.js} loader must be used. + */ + public Permutation select(HttpServletRequest req) { + final String[] values = new String[ruleOrder.size()]; + for (int i = 0; i < values.length; i++) { + final String value = ruleOrder.get(i).select(req); + if (value == null) { + // If the rule returned null it doesn't know how to compute + // the value for this HTTP request. Since we can't do that + // defer to JavaScript by not picking a permutation. + // + return null; + } + values[i] = value; + } + + for (Permutation p : permutations) { + if (p.matches(values)) { + return p; + } + } + + return null; + } + + Collection<String> getCSS() { + return css; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java new file mode 100644 index 0000000000..76b9b5165a --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java @@ -0,0 +1,40 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.linker.server; + +import javax.servlet.http.HttpServletRequest; + +/** A selection rule for a permutation property. */ +public interface Rule { + /** @return the property name, for example {@code "user.agent"}. */ + public String getName(); + + /** + * Compute the value for this property, given the current request. + * <p> + * This rule method must compute the proper permutation value, matching what + * the GWT module XML files use for this property. The rule may use any state + * available in the current servlet request. + * <p> + * If this method returns {@code null} server side selection will be aborted + * and selection for all properties will be handled on the client side by the + * {@code nocache.js} file. + * + * @param req the request + * @return the value for the property; null if the value cannot be determined + * on the server side. + */ + public String select(HttpServletRequest req); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java new file mode 100644 index 0000000000..366b6c57a3 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java @@ -0,0 +1,93 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.linker.server; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static java.util.regex.Pattern.compile; + +import javax.servlet.http.HttpServletRequest; + +/** + * Selects the value for the {@code user.agent} property. + * <p> + * Examines the {@code User-Agent} HTTP request header, and tries to match it to + * known {@code user.agent} values. + * <p> + * Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}. + */ +public class UserAgentRule implements Rule { + private static final Pattern msie = compile(".*msie ([0-9]+)\\.([0-9]+).*"); + private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*"); + + public String getName() { + return "user.agent"; + } + + @Override + public String select(HttpServletRequest req) { + String ua = req.getHeader("User-Agent"); + if (ua == null) { + return null; + } + + ua = ua.toLowerCase(); + + if (ua.indexOf("opera") != -1) { + return "opera"; + + } else if (ua.indexOf("webkit") != -1) { + return "safari"; + + } else if (ua.indexOf("msie") != -1) { + // GWT 2.0 uses document.documentMode here, which we can't do + // on the server side. + + Matcher m = msie.matcher(ua); + if (m.matches() && m.groupCount() == 2) { + int v = makeVersion(m); + if (v >= 10000) { + return "ie10"; + } + if (v >= 9000) { + return "ie9"; + } + if (v >= 8000) { + return "ie8"; + } + if (v >= 6000) { + return "ie6"; + } + } + return null; + + } else if (ua.indexOf("gecko") != -1) { + Matcher m = gecko.matcher(ua); + if (m.matches() && m.groupCount() == 2) { + if (makeVersion(m) >= 1008) { + return "gecko1_8"; + } + } + return "gecko"; + } + + return null; + } + + private int makeVersion(Matcher result) { + return (Integer.parseInt(result.group(1)) * 1000) + + Integer.parseInt(result.group(2)); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml new file mode 100644 index 0000000000..0df89283a3 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml @@ -0,0 +1,19 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <inherits name='com.google.gwt.resources.Resources'/> + <inherits name="com.google.gwt.user.User"/> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java new file mode 100644 index 0000000000..5e13f55b76 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java @@ -0,0 +1,77 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.progress.client; + +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; + +/** + * A simple progress bar with a text label. + * <p> + * The bar is 200 pixels wide and 20 pixels high. To keep the implementation + * simple and lightweight this dimensions are fixed and shouldn't be modified by + * style overrides in client code or CSS. + */ +public class ProgressBar extends Composite { + static { + ProgressResources.I.css().ensureInjected(); + } + + private final String callerText; + private final Label bar; + private final Label msg; + private int value; + + /** Create a bar with no message text. */ + public ProgressBar() { + this(""); + } + + /** Create a bar displaying the specified message. */ + public ProgressBar(final String text) { + if (text == null || text.length() == 0) { + callerText = ""; + } else { + callerText = text + " "; + } + + final FlowPanel body = new FlowPanel(); + body.setStyleName(ProgressResources.I.css().container()); + + msg = new Label(callerText); + msg.setStyleName(ProgressResources.I.css().text()); + body.add(msg); + + bar = new Label(""); + bar.setStyleName(ProgressResources.I.css().bar()); + body.add(bar); + + initWidget(body); + } + + /** @return the current value of the progress meter. */ + public int getValue() { + return value; + } + + /** Update the bar's percent completion. */ + public void setValue(final int pComplete) { + assert 0 <= pComplete && pComplete <= 100; + value = pComplete; + bar.setWidth("" + (2 * pComplete) + "px"); + msg.setText(callerText + pComplete + "%"); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java new file mode 100644 index 0000000000..9de2748f90 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java @@ -0,0 +1,23 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.progress.client; + +import com.google.gwt.resources.client.CssResource; + +public interface ProgressCss extends CssResource { + String container(); + String text(); + String bar(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java new file mode 100644 index 0000000000..0276e9a608 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java @@ -0,0 +1,25 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.progress.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ClientBundle; + +public interface ProgressResources extends ClientBundle { + public static final ProgressResources I = GWT.create(ProgressResources.class); + + @Source("progress.css") + ProgressCss css(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css new file mode 100644 index 0000000000..683396e6a1 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css @@ -0,0 +1,43 @@ +/* Copyright (C) 2009 The Android Open Source Project + * + * 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. + */ + +.container { + position: relative; + border: 1px solid #6B90DA; + height: 20px; + width: 200px; +} + +.text { + position: absolute; + bottom: 0; + left: 0; + z-index: 2; + width: 200px; + padding-bottom: 3px; + text-align: center; + font-weight: bold; + font-style: italic; + font-size: smaller; +} + +.bar { + background: #F0F7F9; + border-right: 1px solid #D0D7D9; + position: absolute; + top: 0; + left: 0; + height: 20px; +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml new file mode 100644 index 0000000000..0df89283a3 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml @@ -0,0 +1,19 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <inherits name='com.google.gwt.resources.Resources'/> + <inherits name="com.google.gwt.user.User"/> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java new file mode 100644 index 0000000000..46d7f51ead --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java @@ -0,0 +1,137 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import java.util.ArrayList; +import java.util.HashMap; + +/** Lightweight map of names/values for element attribute construction. */ +class AttMap { + private static final Tag ANY = new AnyTag(); + private static final HashMap<String, Tag> TAGS; + static { + final Tag src = new SrcTag(); + TAGS = new HashMap<String, Tag>(); + TAGS.put("a", new AnchorTag()); + TAGS.put("form", new FormTag()); + TAGS.put("img", src); + TAGS.put("script", src); + TAGS.put("frame", src); + } + + private final ArrayList<String> names = new ArrayList<String>(); + private final ArrayList<String> values = new ArrayList<String>(); + + private Tag tag = ANY; + private int live; + + void reset(final String tagName) { + tag = TAGS.get(tagName.toLowerCase()); + if (tag == null) { + tag = ANY; + } + live = 0; + } + + void onto(final Buffer raw, final SafeHtmlBuilder esc) { + for (int i = 0; i < live; i++) { + final String v = values.get(i); + if (v.length() > 0) { + raw.append(" "); + raw.append(names.get(i)); + raw.append("=\""); + esc.append(v); + raw.append("\""); + } + } + } + + String get(String name) { + name = name.toLowerCase(); + + for (int i = 0; i < live; i++) { + if (name.equals(names.get(i))) { + return values.get(i); + } + } + return ""; + } + + void set(String name, final String value) { + name = name.toLowerCase(); + tag.assertSafe(name, value); + + for (int i = 0; i < live; i++) { + if (name.equals(names.get(i))) { + values.set(i, value); + return; + } + } + + final int i = live++; + if (names.size() < live) { + names.add(name); + values.add(value); + } else { + names.set(i, name); + values.set(i, value); + } + } + + private static void assertNotJavascriptUrl(final String value) { + if (value.startsWith("#")) { + // common in GWT, and safe, so bypass further checks + + } else if (value.trim().toLowerCase().startsWith("javascript:")) { + // possibly unsafe, we could have random user code here + // we can't tell if its safe or not so we refuse to accept + // + throw new RuntimeException("javascript unsafe in href: " + value); + } + } + + private static interface Tag { + void assertSafe(String name, String value); + } + + private static class AnyTag implements Tag { + public void assertSafe(String name, String value) { + } + } + + private static class AnchorTag implements Tag { + public void assertSafe(String name, String value) { + if ("href".equals(name)) { + assertNotJavascriptUrl(value); + } + } + } + + private static class FormTag implements Tag { + public void assertSafe(String name, String value) { + if ("action".equals(name)) { + assertNotJavascriptUrl(value); + } + } + } + + private static class SrcTag implements Tag { + public void assertSafe(String name, String value) { + if ("src".equals(name)) { + assertNotJavascriptUrl(value); + } + } + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java new file mode 100644 index 0000000000..d79c580623 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java @@ -0,0 +1,33 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +interface Buffer { + void append(boolean v); + + void append(char v); + + void append(int v); + + void append(long v); + + void append(float v); + + void append(double v); + + void append(String v); + + String toString(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java new file mode 100644 index 0000000000..a1801ad038 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java @@ -0,0 +1,56 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +final class BufferDirect implements Buffer { + private final StringBuilder strbuf = new StringBuilder(); + + boolean isEmpty() { + return strbuf.length() == 0; + } + + public void append(final boolean v) { + strbuf.append(v); + } + + public void append(final char v) { + strbuf.append(v); + } + + public void append(final int v) { + strbuf.append(v); + } + + public void append(final long v) { + strbuf.append(v); + } + + public void append(final float v) { + strbuf.append(v); + } + + public void append(final double v) { + strbuf.append(v); + } + + public void append(final String v) { + strbuf.append(v); + } + + @Override + public String toString() { + return strbuf.toString(); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java new file mode 100644 index 0000000000..6b5346d8d2 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java @@ -0,0 +1,56 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +final class BufferSealElement implements Buffer { + private final SafeHtmlBuilder shb; + + BufferSealElement(final SafeHtmlBuilder safeHtmlBuilder) { + shb = safeHtmlBuilder; + } + + public void append(final boolean v) { + shb.sealElement().append(v); + } + + public void append(final char v) { + shb.sealElement().append(v); + } + + public void append(final double v) { + shb.sealElement().append(v); + } + + public void append(final float v) { + shb.sealElement().append(v); + } + + public void append(final int v) { + shb.sealElement().append(v); + } + + public void append(final long v) { + shb.sealElement().append(v); + } + + public void append(final String v) { + shb.sealElement().append(v); + } + + @Override + public String toString() { + return shb.sealElement().toString(); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java new file mode 100644 index 0000000000..f7bc907bf6 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java @@ -0,0 +1,40 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.regexp.shared.RegExp; + +/** A Find/Replace pair used against the {@link SafeHtml} block of text. */ +public interface FindReplace { + /** + * @return regular expression to match substrings with; should be treated as + * immutable. + */ + public RegExp pattern(); + + /** + * Find and replace a single instance of this pattern in an input. + * <p> + * <b>WARNING:</b> No XSS sanitization is done on the return value of this + * method, e.g. this value may be passed directly to + * {@link SafeHtml#replaceAll(String, String)}. Implementations must sanitize output + * appropriately. + * + * @param input input string. + * @return result of regular expression replacement. + * @throws IllegalArgumentException if the input could not be safely sanitized. + */ + public String replace(String input); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java new file mode 100644 index 0000000000..e2c576bce3 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java @@ -0,0 +1,96 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.user.client.ui.SuggestOracle; + +import java.util.ArrayList; + +/** + * A suggestion oracle that tries to highlight the matched text. + * <p> + * Suggestions supplied by the implementation of + * {@link #onRequestSuggestions(Request, Callback)} are modified to wrap all + * occurrences of the {@link SuggestOracle.Request#getQuery()} substring in HTML + * <code><strong></code> tags, so they can be emphasized to the user. + */ +public abstract class HighlightSuggestOracle extends SuggestOracle { + private static String escape(String ds) { + return new SafeHtmlBuilder().append(ds).asString(); + } + + @Override + public final boolean isDisplayStringHTML() { + return true; + } + + @Override + public final void requestSuggestions(final Request request, final Callback cb) { + onRequestSuggestions(request, new Callback() { + public void onSuggestionsReady(final Request request, + final Response response) { + final String qpat = getQueryPattern(request.getQuery()); + final boolean html = isHTML(); + final ArrayList<Suggestion> r = new ArrayList<Suggestion>(); + for (final Suggestion s : response.getSuggestions()) { + r.add(new BoldSuggestion(qpat, s, html)); + } + cb.onSuggestionsReady(request, new Response(r)); + } + }); + } + + protected String getQueryPattern(final String query) { + return "(" + escape(query) + ")"; + } + + /** + * @return true if {@link SuggestOracle.Suggestion#getDisplayString()} returns + * HTML; false if the text must be escaped before evaluating in an + * HTML like context. + */ + protected boolean isHTML() { + return false; + } + + /** Compute the suggestions and return them for display. */ + protected abstract void onRequestSuggestions(Request request, Callback done); + + private static class BoldSuggestion implements Suggestion { + private final Suggestion suggestion; + private final String displayString; + + BoldSuggestion(final String qstr, final Suggestion s, final boolean html) { + suggestion = s; + + String ds = s.getDisplayString(); + if (!html) { + ds = escape(ds); + } + displayString = sgi(ds, qstr, "<strong>$1</strong>"); + } + + private static native String sgi(String inString, String pat, String newHtml) + /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/; + + public String getDisplayString() { + return displayString; + } + + public String getReplacementString() { + return suggestion.getReplacementString(); + } + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java new file mode 100644 index 0000000000..eaa4f23030 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java @@ -0,0 +1,84 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.regexp.shared.RegExp; + +/** + * A Find/Replace pair whose replacement string is a link. + * <p> + * It is safe to pass arbitrary user-provided links to this class. Links are + * sanitized as follows: + * <ul> + * <li>Only http(s) and mailto links are supported; any other scheme results in + * an {@link IllegalArgumentException} from {@link #replace(String)}. + * <li>Special characters in the link after regex replacement are escaped with + * {@link SafeHtmlBuilder}.</li> + * </ul> + */ +public class LinkFindReplace implements FindReplace { + public static boolean hasValidScheme(String link) { + int colon = link.indexOf(':'); + if (colon < 0) { + return true; + } + String scheme = link.substring(0, colon); + return "http".equalsIgnoreCase(scheme) + || "https".equalsIgnoreCase(scheme) + || "mailto".equalsIgnoreCase(scheme); + } + + private RegExp pat; + private String link; + + protected LinkFindReplace() { + } + + /** + * @param regex regular expression pattern to match substrings with. + * @param repl replacement link href. Capture groups within + * <code>regex</code> can be referenced with <code>$<i>n</i></code>. + */ + public LinkFindReplace(String find, String link) { + this.pat = RegExp.compile(find); + this.link = link; + } + + @Override + public RegExp pattern() { + return pat; + } + + @Override + public String replace(String input) { + String href = pat.replace(input, link); + if (!hasValidScheme(href)) { + throw new IllegalArgumentException( + "Invalid scheme (" + toString() + "): " + href); + } + String result = new SafeHtmlBuilder() + .openAnchor() + .setAttribute("href", href) + .append(SafeHtml.asis(input)) + .closeAnchor() + .asString(); + return result; + } + + @Override + public String toString() { + return "find = " + pat.getSource() + ", link = " + link; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java new file mode 100644 index 0000000000..d22fef6e76 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java @@ -0,0 +1,55 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.regexp.shared.RegExp; + +/** + * A Find/Replace pair whose replacement string is arbitrary HTML. + * <p> + * <b>WARNING:</b> This class is not safe used with user-provided patterns. + */ +public class RawFindReplace implements FindReplace { + private RegExp pat; + private String replace; + + protected RawFindReplace() { + } + + /** + * @param regex regular expression pattern to match substrings with. + * @param repl replacement expression. Capture groups within + * <code>regex</code> can be referenced with <code>$<i>n</i></code>. + */ + public RawFindReplace(String find, String replace) { + this.pat = RegExp.compile(find); + this.replace = replace; + } + + @Override + public RegExp pattern() { + return pat; + } + + @Override + public String replace(String input) { + return pat.replace(input, replace); + } + + @Override + public String toString() { + return "find = " + pat.getSource() + ", replace = " + replace; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java new file mode 100644 index 0000000000..0a9f7a2ed9 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java @@ -0,0 +1,302 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HTMLTable; +import com.google.gwt.user.client.ui.HasHTML; +import com.google.gwt.user.client.ui.InlineHTML; +import com.google.gwt.user.client.ui.Widget; + +import java.util.Iterator; +import java.util.List; + +/** Immutable string safely placed as HTML without further escaping. */ +public abstract class SafeHtml { + public static final SafeHtmlResources RESOURCES; + + static { + if (GWT.isClient()) { + RESOURCES = GWT.create(SafeHtmlResources.class); + RESOURCES.css().ensureInjected(); + + } else { + RESOURCES = new SafeHtmlResources() { + @Override + public SafeHtmlCss css() { + return new SafeHtmlCss() { + public String wikiList() { + return "wikiList"; + } + + public String wikiPreFormat() { + return "wikiPreFormat"; + } + + public boolean ensureInjected() { + return false; + } + + public String getName() { + return null; + } + + public String getText() { + return null; + } + }; + } + }; + } + } + + /** @return the existing HTML property of a widget. */ + public static SafeHtml get(final HasHTML t) { + return new SafeHtmlString(t.getHTML()); + } + + /** @return the existing HTML text, wrapped in a safe buffer. */ + public static SafeHtml asis(final String htmlText) { + return new SafeHtmlString(htmlText); + } + + /** Set the HTML property of a widget. */ + public static <T extends HasHTML> T set(final T e, final SafeHtml str) { + e.setHTML(str.asString()); + return e; + } + + /** @return the existing inner HTML of any element. */ + public static SafeHtml get(final Element e) { + return new SafeHtmlString(DOM.getInnerHTML(e)); + } + + /** Set the inner HTML of any element. */ + public static Element set(final Element e, final SafeHtml str) { + DOM.setInnerHTML(e, str.asString()); + return e; + } + + /** @return the existing inner HTML of a table cell. */ + public static SafeHtml get(final HTMLTable t, final int row, final int col) { + return new SafeHtmlString(t.getHTML(row, col)); + } + + /** Set the inner HTML of a table cell. */ + public static <T extends HTMLTable> T set(final T t, final int row, + final int col, final SafeHtml str) { + t.setHTML(row, col, str.asString()); + return t; + } + + /** Parse an HTML block and return the first (typically root) element. */ + public static Element parse(final SafeHtml str) { + return DOM.getFirstChild(set(DOM.createDiv(), str)); + } + + /** Convert bare http:// and https:// URLs into <a href> tags. */ + public SafeHtml linkify() { + final String part = "(?:" + + "[a-zA-Z0-9$_.+!*',%;:@=?#/~-]" + + "|&(?!lt;|gt;)" + + ")"; + return replaceAll( + "(https?://" + + part + "{2,}" + + "(?:[(]" + part + "*" + "[)])*" + + part + "*" + + ")", + "<a href=\"$1\" target=\"_blank\">$1</a>"); + } + + /** + * Apply {@link #linkify()}, and "\n\n" to <p>. + * <p> + * Lines that start with whitespace are assumed to be preformatted, and are + * formatted by the {@link SafeHtmlCss#wikiPreFormat()} CSS class. + */ + public SafeHtml wikify() { + final SafeHtmlBuilder r = new SafeHtmlBuilder(); + for (final String p : linkify().asString().split("\n\n")) { + if (isPreFormat(p)) { + r.openElement("p"); + for (final String line : p.split("\n")) { + r.openSpan(); + r.setStyleName(RESOURCES.css().wikiPreFormat()); + r.append(asis(line)); + r.closeSpan(); + r.br(); + } + r.closeElement("p"); + + } else if (isList(p)) { + wikifyList(r, p); + + } else { + r.openElement("p"); + r.append(asis(p)); + r.closeElement("p"); + } + } + return r.toSafeHtml(); + } + + private void wikifyList(final SafeHtmlBuilder r, final String p) { + boolean in_ul = false; + boolean in_p = false; + for (String line : p.split("\n")) { + if (line.startsWith("-") || line.startsWith("*")) { + if (!in_ul) { + if (in_p) { + in_p = false; + r.closeElement("p"); + } + + in_ul = true; + r.openElement("ul"); + r.setStyleName(RESOURCES.css().wikiList()); + } + line = line.substring(1).trim(); + + } else if (!in_ul) { + if (!in_p) { + in_p = true; + r.openElement("p"); + } else { + r.append(' '); + } + r.append(asis(line)); + continue; + } + + r.openElement("li"); + r.append(asis(line)); + r.closeElement("li"); + } + + if (in_ul) { + r.closeElement("ul"); + } else if (in_p) { + r.closeElement("p"); + } + } + + private static boolean isPreFormat(final String p) { + return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ") + || p.startsWith("\t"); + } + + private static boolean isList(final String p) { + return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ") + || p.startsWith("* "); + } + + /** + * Replace first occurrence of <code>regex</code> with <code>repl</code> . + * <p> + * <b>WARNING:</b> This replacement is being performed against an otherwise + * safe HTML string. The caller must ensure that the replacement does not + * introduce cross-site scripting attack entry points. + * + * @param regex regular expression pattern to match the substring with. + * @param repl replacement expression. Capture groups within + * <code>regex</code> can be referenced with <code>$<i>n</i></code>. + * @return a new string, after the replacement has been made. + */ + public SafeHtml replaceFirst(final String regex, final String repl) { + return new SafeHtmlString(asString().replaceFirst(regex, repl)); + } + + /** + * Replace each occurrence of <code>regex</code> with <code>repl</code> . + * <p> + * <b>WARNING:</b> This replacement is being performed against an otherwise + * safe HTML string. The caller must ensure that the replacement does not + * introduce cross-site scripting attack entry points. + * + * @param regex regular expression pattern to match substrings with. + * @param repl replacement expression. Capture groups within + * <code>regex</code> can be referenced with <code>$<i>n</i></code>. + * @return a new string, after the replacements have been made. + */ + public SafeHtml replaceAll(final String regex, final String repl) { + return new SafeHtmlString(asString().replaceAll(regex, repl)); + } + + /** + * Replace all find/replace pairs in the list in a single pass. + * + * @param findReplaceList find/replace pairs to use. + * @return a new string, after the replacements have been made. + */ + public <T> SafeHtml replaceAll(List<? extends FindReplace> findReplaceList) { + if (findReplaceList == null || findReplaceList.isEmpty()) { + return this; + } + + StringBuilder pat = new StringBuilder(); + Iterator<? extends FindReplace> it = findReplaceList.iterator(); + while (it.hasNext()) { + FindReplace fr = it.next(); + pat.append(fr.pattern().getSource()); + if (it.hasNext()) { + pat.append('|'); + } + } + + StringBuilder result = new StringBuilder(); + RegExp re = RegExp.compile(pat.toString(), "g"); + String orig = asString(); + int index = 0; + MatchResult mat; + while ((mat = re.exec(orig)) != null) { + String g = mat.getGroup(0); + // Re-run each candidate to find which one matched. + for (FindReplace fr : findReplaceList) { + if (fr.pattern().test(g)) { + try { + String repl = fr.replace(g); + result.append(orig.substring(index, mat.getIndex())); + result.append(repl); + } catch (IllegalArgumentException e) { + continue; + } + index = mat.getIndex() + g.length(); + break; + } + } + } + result.append(orig.substring(index, orig.length())); + return asis(result.toString()); + } + + /** @return a GWT block display widget displaying this HTML. */ + public Widget toBlockWidget() { + return new HTML(asString()); + } + + /** @return a GWT inline display widget displaying this HTML. */ + public Widget toInlineWidget() { + return new InlineHTML(asString()); + } + + /** @return a clean HTML string safe for inclusion in any context. */ + public abstract String asString(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java new file mode 100644 index 0000000000..9fe3267e7b --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java @@ -0,0 +1,411 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.core.client.GWT; + +/** + * Safely constructs a {@link SafeHtml}, escaping user provided content. + */ +public class SafeHtmlBuilder extends SafeHtml { + private static final Impl impl; + + static { + if (GWT.isClient()) { + impl = new ClientImpl(); + } else { + impl = new ServerImpl(); + } + } + + private final BufferDirect dBuf; + private Buffer cb; + + private BufferSealElement sBuf; + private AttMap att; + + public SafeHtmlBuilder() { + cb = dBuf = new BufferDirect(); + } + + /** @return true if this builder has not had an append occur yet. */ + public boolean isEmpty() { + return dBuf.isEmpty(); + } + + /** @return true if this builder has content appended into it. */ + public boolean hasContent() { + return !isEmpty(); + } + + public SafeHtmlBuilder append(final boolean in) { + cb.append(in); + return this; + } + + public SafeHtmlBuilder append(final char in) { + switch (in) { + case '&': + cb.append("&"); + break; + + case '>': + cb.append(">"); + break; + + case '<': + cb.append("<"); + break; + + case '"': + cb.append("""); + break; + + case '\'': + cb.append("'"); + break; + + default: + cb.append(in); + break; + } + return this; + } + + public SafeHtmlBuilder append(final int in) { + cb.append(in); + return this; + } + + public SafeHtmlBuilder append(final long in) { + cb.append(in); + return this; + } + + public SafeHtmlBuilder append(final float in) { + cb.append(in); + return this; + } + + public SafeHtmlBuilder append(final double in) { + cb.append(in); + return this; + } + + /** Append already safe HTML as-is, avoiding double escaping. */ + public SafeHtmlBuilder append(final SafeHtml in) { + if (in != null) { + cb.append(in.asString()); + } + return this; + } + + /** Append the string, escaping unsafe characters. */ + public SafeHtmlBuilder append(final String in) { + if (in != null) { + impl.escapeStr(this, in); + } + return this; + } + + /** Append the string, escaping unsafe characters. */ + public SafeHtmlBuilder append(final StringBuilder in) { + if (in != null) { + append(in.toString()); + } + return this; + } + + /** Append the string, escaping unsafe characters. */ + public SafeHtmlBuilder append(final StringBuffer in) { + if (in != null) { + append(in.toString()); + } + return this; + } + + /** Append the result of toString(), escaping unsafe characters. */ + public SafeHtmlBuilder append(final Object in) { + if (in != null) { + append(in.toString()); + } + return this; + } + + /** Append the string, escaping unsafe characters. */ + public SafeHtmlBuilder append(final CharSequence in) { + if (in != null) { + escapeCS(this, in); + } + return this; + } + + /** + * Open an element, appending "<tagName>" to the buffer. + * <p> + * After the element is open the attributes may be manipulated until the next + * <code>append</code>, <code>openElement</code>, <code>closeSelf</code> or + * <code>closeElement</code> call. + * + * @param tagName name of the HTML element to open. + */ + public SafeHtmlBuilder openElement(final String tagName) { + assert isElementName(tagName); + cb.append("<"); + cb.append(tagName); + if (sBuf == null) { + att = new AttMap(); + sBuf = new BufferSealElement(this); + } + att.reset(tagName); + cb = sBuf; + return this; + } + + /** + * Get an attribute of the last opened element. + * + * @param name name of the attribute to read. + * @return the attribute value, as a string. The empty string if the attribute + * has not been assigned a value. The returned string is the raw + * (unescaped) value. + */ + public String getAttribute(final String name) { + assert isAttributeName(name); + assert cb == sBuf; + return att.get(name); + } + + /** + * Set an attribute of the last opened element. + * + * @param name name of the attribute to set. + * @param value value to assign; any existing value is replaced. The value is + * escaped (if necessary) during the assignment. + */ + public SafeHtmlBuilder setAttribute(final String name, final String value) { + assert isAttributeName(name); + assert cb == sBuf; + att.set(name, value != null ? value : ""); + return this; + } + + /** + * Set an attribute of the last opened element. + * + * @param name name of the attribute to set. + * @param value value to assign, any existing value is replaced. + */ + public SafeHtmlBuilder setAttribute(final String name, final int value) { + return setAttribute(name, String.valueOf(value)); + } + + /** + * Append a new value into a whitespace delimited attribute. + * <p> + * If the attribute is not yet assigned, this method sets the attribute. If + * the attribute is already assigned, the new value is appended onto the end, + * after appending a single space to delimit the values. + * + * @param name name of the attribute to append onto. + * @param value additional value to append. + */ + public SafeHtmlBuilder appendAttribute(final String name, String value) { + if (value != null && value.length() > 0) { + final String e = getAttribute(name); + return setAttribute(name, e.length() > 0 ? e + " " + value : value); + } + return this; + } + + /** Set the height attribute of the current element. */ + public SafeHtmlBuilder setHeight(final int height) { + return setAttribute("height", height); + } + + /** Set the width attribute of the current element. */ + public SafeHtmlBuilder setWidth(final int width) { + return setAttribute("width", width); + } + + /** Set the CSS class name for this element. */ + public SafeHtmlBuilder setStyleName(final String style) { + assert isCssName(style); + return setAttribute("class", style); + } + + /** + * Add an additional CSS class name to this element. + *<p> + * If no CSS class name has been specified yet, this method initializes it to + * the single name. + */ + public SafeHtmlBuilder addStyleName(final String style) { + assert isCssName(style); + return appendAttribute("class", style); + } + + private void sealElement0() { + assert cb == sBuf; + cb = dBuf; + att.onto(cb, this); + } + + Buffer sealElement() { + sealElement0(); + cb.append(">"); + return cb; + } + + /** Close the current element with a self closing suffix ("/ >"). */ + public SafeHtmlBuilder closeSelf() { + sealElement0(); + cb.append(" />"); + return this; + } + + /** Append a closing tag for the named element. */ + public SafeHtmlBuilder closeElement(final String name) { + assert isElementName(name); + cb.append("</"); + cb.append(name); + cb.append(">"); + return this; + } + + /** Append "&nbsp;" - a non-breaking space, useful in empty table cells. */ + public SafeHtmlBuilder nbsp() { + cb.append(" "); + return this; + } + + /** Append "<br />" - a line break with no attributes */ + public SafeHtmlBuilder br() { + cb.append("<br />"); + return this; + } + + /** Append "<tr>"; attributes may be set if needed */ + public SafeHtmlBuilder openTr() { + return openElement("tr"); + } + + /** Append "</tr>" */ + public SafeHtmlBuilder closeTr() { + return closeElement("tr"); + } + + /** Append "<td>"; attributes may be set if needed */ + public SafeHtmlBuilder openTd() { + return openElement("td"); + } + + /** Append "</td>" */ + public SafeHtmlBuilder closeTd() { + return closeElement("td"); + } + + /** Append "<div>"; attributes may be set if needed */ + public SafeHtmlBuilder openDiv() { + return openElement("div"); + } + + /** Append "</div>" */ + public SafeHtmlBuilder closeDiv() { + return closeElement("div"); + } + + /** Append "<span>"; attributes may be set if needed */ + public SafeHtmlBuilder openSpan() { + return openElement("span"); + } + + /** Append "</span>" */ + public SafeHtmlBuilder closeSpan() { + return closeElement("span"); + } + + /** Append "<a>"; attributes may be set if needed */ + public SafeHtmlBuilder openAnchor() { + return openElement("a"); + } + + /** Append "</a>" */ + public SafeHtmlBuilder closeAnchor() { + return closeElement("a"); + } + + /** Append "<param name=... value=... />". */ + public SafeHtmlBuilder paramElement(final String name, final String value) { + openElement("param"); + setAttribute("name", name); + setAttribute("value", value); + return closeSelf(); + } + + /** @return an immutable {@link SafeHtml} representation of the buffer. */ + public SafeHtml toSafeHtml() { + return new SafeHtmlString(asString()); + } + + @Override + public String asString() { + return cb.toString(); + } + + private static void escapeCS(final SafeHtmlBuilder b, final CharSequence in) { + for (int i = 0; i < in.length(); i++) { + b.append(in.charAt(i)); + } + } + + private static boolean isElementName(final String name) { + return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$"); + } + + private static boolean isAttributeName(final String name) { + return isElementName(name); + } + + private static boolean isCssName(final String name) { + return isElementName(name); + } + + private static abstract class Impl { + abstract void escapeStr(SafeHtmlBuilder b, String in); + } + + private static class ServerImpl extends Impl { + @Override + void escapeStr(final SafeHtmlBuilder b, final String in) { + SafeHtmlBuilder.escapeCS(b, in); + } + } + + private static class ClientImpl extends Impl { + @Override + void escapeStr(final SafeHtmlBuilder b, final String in) { + b.cb.append(escape(in)); + } + + private static native String escape(String src) + /*-{ return src.replace(/&/g,'&') + .replace(/>/g,'>') + .replace(/</g,'<') + .replace(/"/g,'"') + .replace(/'/g,'''); + }-*/; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java new file mode 100644 index 0000000000..f6836a088d --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java @@ -0,0 +1,22 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.resources.client.CssResource; + +public interface SafeHtmlCss extends CssResource { + String wikiPreFormat(); + String wikiList(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java new file mode 100644 index 0000000000..e3f5724d61 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java @@ -0,0 +1,22 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +import com.google.gwt.resources.client.ClientBundle; + +public interface SafeHtmlResources extends ClientBundle { + @Source("safehtml.css") + SafeHtmlCss css(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java new file mode 100644 index 0000000000..a229421d9b --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java @@ -0,0 +1,28 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.safehtml.client; + +class SafeHtmlString extends SafeHtml { + private final String html; + + SafeHtmlString(final String h) { + html = h; + } + + @Override + public String asString() { + return html; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css new file mode 100644 index 0000000000..fcad92c47a --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css @@ -0,0 +1,23 @@ +/* Copyright (C) 2009 The Android Open Source Project + * + * 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. + */ + +.wikiPreFormat { + white-space: pre; + font-family: 'Lucida Console', 'Lucida Sans Typewriter', Monaco, monospace; + font-size: small; +} + +.wikiList { +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java new file mode 100644 index 0000000000..c4d681f900 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java @@ -0,0 +1,106 @@ +// Copyright (C) 2008 The Android Open Source Project +// +// 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.gwtexpui.server; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Forces GWT resources to cache for a very long time. + * <p> + * GWT compiled JavaScript and ImageBundles can be cached indefinitely by a + * browser and/or an edge proxy, as they never contain user-specific data and + * are named by a unique checksum. If their content is ever modified then the + * URL changes, so user agents would request a different resource. We force + * these resources to have very long expiration times. + * <p> + * To use, add the following block to your <code>web.xml</code>: + * + * <pre> + * <filter> + * <filter-name>CacheControl</filter-name> + * <filter-class>com.google.gwtexpui.server.CacheControlFilter</filter-class> + * </filter> + * <filter-mapping> + * <filter-name>CacheControl</filter-name> + * <url-pattern>/*</url-pattern> + * </filter-mapping> + * </pre> + */ +public class CacheControlFilter implements Filter { + public void init(final FilterConfig config) { + } + + public void destroy() { + } + + public void doFilter(final ServletRequest sreq, final ServletResponse srsp, + final FilterChain chain) throws IOException, ServletException { + final HttpServletRequest req = (HttpServletRequest) sreq; + final HttpServletResponse rsp = (HttpServletResponse) srsp; + final String pathInfo = pathInfo(req); + + if (cacheForever(pathInfo, req)) { + CacheHeaders.setCacheable(req, rsp, 365, TimeUnit.DAYS); + } else if (nocache(pathInfo)) { + CacheHeaders.setNotCacheable(rsp); + } + + chain.doFilter(req, rsp); + } + + private static boolean cacheForever(final String pathInfo, + final HttpServletRequest req) { + if (pathInfo.endsWith(".cache.html")) { + return true; + } else if (pathInfo.endsWith(".cache.gif")) { + return true; + } else if (pathInfo.endsWith(".cache.png")) { + return true; + } else if (pathInfo.endsWith(".cache.css")) { + return true; + } else if (pathInfo.endsWith(".cache.jar")) { + return true; + } else if (pathInfo.endsWith(".cache.swf")) { + return true; + } else if (pathInfo.endsWith(".nocache.js")) { + final String v = req.getParameter("content"); + return v != null && v.length() > 20; + } + return false; + } + + private static boolean nocache(final String pathInfo) { + if (pathInfo.endsWith(".nocache.js")) { + return true; + } + return false; + } + + private static String pathInfo(final HttpServletRequest req) { + final String uri = req.getRequestURI(); + final String ctx = req.getContextPath(); + return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java new file mode 100644 index 0000000000..11409e8ff3 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java @@ -0,0 +1,118 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// 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.gwtexpui.server; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Utilities to manage HTTP caching directives in responses. */ +public class CacheHeaders { + private static final long MAX_CACHE_DURATION = DAYS.toSeconds(365); + + /** + * Do not cache the response, anywhere. + * + * @param res response being returned. + */ + public static void setNotCacheable(HttpServletResponse res) { + String cc = "no-cache, no-store, max-age=0, must-revalidate"; + res.setHeader("Cache-Control", cc); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "Fri, 01 Jan 1990 00:00:00 GMT"); + res.setDateHeader("Date", System.currentTimeMillis()); + } + + /** + * Permit caching the response for up to the age specified. + * <p> + * If the request is on a secure connection (e.g. SSL) private caching is + * used. This allows the user-agent to cache the response, but requests + * intermediate proxies to not cache. This may offer better protection for + * Set-Cookie headers. + * <p> + * If the request is on plaintext (insecure), public caching is used. This may + * allow an intermediate proxy to cache the response, including any Set-Cookie + * header that may have also been included. + * + * @param req current request. + * @param res response being returned. + * @param age how long the response can be cached. + * @param unit time unit for age, usually {@link TimeUnit#SECONDS}. + */ + public static void setCacheable( + HttpServletRequest req, HttpServletResponse res, + long age, TimeUnit unit) { + if (req.isSecure()) { + setCacheablePrivate(res, age, unit); + } else { + setCacheablePublic(res, age, unit); + } + } + + /** + * Allow the response to be cached by proxies and user-agents. + * <p> + * If the response includes a Set-Cookie header the cookie may be cached by a + * proxy and returned to multiple browsers behind the same proxy. This is + * insecure for authenticated connections. + * + * @param res response being returned. + * @param age how long the response can be cached. + * @param unit time unit for age, usually {@link TimeUnit#SECONDS}. + */ + public static void setCacheablePublic(HttpServletResponse res, + long age, TimeUnit unit) { + long now = System.currentTimeMillis(); + long sec = maxAgeSeconds(age, unit); + + res.setDateHeader("Expires", now + SECONDS.toMillis(sec)); + res.setDateHeader("Date", now); + cache(res, "public", age, unit); + } + + /** + * Allow the response to be cached only by the user-agent. + * + * @param res response being returned. + * @param age how long the response can be cached. + * @param unit time unit for age, usually {@link TimeUnit#SECONDS}. + */ + public static void setCacheablePrivate(HttpServletResponse res, + long age, TimeUnit unit) { + long now = System.currentTimeMillis(); + res.setDateHeader("Expires", now); + res.setDateHeader("Date", now); + cache(res, "private", age, unit); + } + + private static void cache(HttpServletResponse res, + String type, long age, TimeUnit unit) { + res.setHeader("Cache-Control", String.format( + "%s, max-age=%d", + type, maxAgeSeconds(age, unit))); + } + + private static long maxAgeSeconds(long age, TimeUnit unit) { + return Math.min(unit.toSeconds(age), MAX_CACHE_DURATION); + } + + private CacheHeaders() { + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml new file mode 100644 index 0000000000..c681d893bf --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml @@ -0,0 +1,27 @@ +<!-- + Copyright (C) 2009 The Android Open Source Project + + 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. +--> +<module> + <inherits name="com.google.gwt.user.User"/> + + <replace-with class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImplAutoHide"> + <when-type-is class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImpl" /> + <any> + <when-property-is name="user.agent" value="safari"/> + <when-property-is name="user.agent" value="gecko"/> + <when-property-is name="user.agent" value="gecko1_8"/> + </any> + </replace-with> +</module> diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java new file mode 100644 index 0000000000..78ea8d607e --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java @@ -0,0 +1,76 @@ +// Copyright (C) 2008 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Window; + +/** A DialogBox that automatically re-centers itself if the window changes */ +public class AutoCenterDialogBox extends PluginSafeDialogBox { + private HandlerRegistration recenter; + + public AutoCenterDialogBox() { + this(false); + } + + public AutoCenterDialogBox(final boolean autoHide) { + this(autoHide, true); + } + + public AutoCenterDialogBox(final boolean autoHide, final boolean modal) { + super(autoHide, modal); + } + + @Override + public void show() { + if (recenter == null) { + recenter = Window.addResizeHandler(new ResizeHandler() { + @Override + public void onResize(final ResizeEvent event) { + final int w = event.getWidth(); + final int h = event.getHeight(); + AutoCenterDialogBox.this.onResize(w, h); + } + }); + } + super.show(); + } + + @Override + protected void onUnload() { + if (recenter != null) { + recenter.removeHandler(); + recenter = null; + } + super.onUnload(); + } + + /** + * Invoked when the outer browser window resizes. + * <p> + * Subclasses may override (but should ensure they still call super.onResize) + * to implement custom logic when a window resize occurs. + * + * @param width new browser window width + * @param height new browser window height + */ + protected void onResize(final int width, final int height) { + if (isAttached()) { + center(); + } + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java new file mode 100644 index 0000000000..c6ab09a151 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java @@ -0,0 +1,65 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.ui.DialogBox; + +/** + * A DialogBox that can appear over Flash movies and Java applets. + * <p> + * Some browsers have issues with placing a <div> (such as that used by + * the DialogBox implementation) over top of native UI such as that used by the + * Flash plugin. Often the native UI leaks over top of the <div>, which is + * not the desired behavior for a dialog box. + * <p> + * This implementation hides the native resources by setting their display + * property to 'none' when the dialog is shown, and restores them back to their + * prior setting when the dialog is hidden. + * */ +public class PluginSafeDialogBox extends DialogBox { + private final PluginSafeDialogBoxImpl impl = + GWT.create(PluginSafeDialogBoxImpl.class); + + public PluginSafeDialogBox() { + this(false); + } + + public PluginSafeDialogBox(final boolean autoHide) { + this(autoHide, true); + } + + public PluginSafeDialogBox(final boolean autoHide, final boolean modal) { + super(autoHide, modal); + } + + @Override + public void setVisible(final boolean show) { + impl.visible(show); + super.setVisible(show); + } + + @Override + public void show() { + impl.visible(true); + super.show(); + } + + @Override + public void hide(final boolean autoClosed) { + impl.visible(false); + super.hide(autoClosed); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java new file mode 100644 index 0000000000..a32fc99fde --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java @@ -0,0 +1,20 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +class PluginSafeDialogBoxImpl { + void visible(final boolean dialogVisible) { + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java new file mode 100644 index 0000000000..e32fe78daa --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java @@ -0,0 +1,86 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.user.client.ui.UIObject; + +import java.util.ArrayList; + +class PluginSafeDialogBoxImplAutoHide extends PluginSafeDialogBoxImpl { + private boolean hidden; + private ArrayList<HiddenElement> hiddenElements = + new ArrayList<HiddenElement>(); + + @Override + void visible(final boolean dialogVisible) { + if (dialogVisible) { + hideAll(); + } else { + showAll(); + } + } + + private void hideAll() { + if (!hidden) { + hideSet(Document.get().getElementsByTagName("object")); + hideSet(Document.get().getElementsByTagName("embed")); + hideSet(Document.get().getElementsByTagName("applet")); + hidden = true; + } + } + + private void hideSet(final NodeList<Element> all) { + for (int i = 0; i < all.getLength(); i++) { + final Element e = all.getItem(i); + if (UIObject.isVisible(e)) { + hiddenElements.add(new HiddenElement(e)); + } + } + } + + private void showAll() { + if (hidden) { + for (final HiddenElement e : hiddenElements) { + e.restore(); + } + hiddenElements.clear(); + hidden = false; + } + } + + private static class HiddenElement { + private final Element element; + private final String visibility; + + HiddenElement(final Element element) { + this.element = element; + this.visibility = getVisibility(element); + setVisibility(element, "hidden"); + } + + void restore() { + setVisibility(element, visibility); + } + + private static native String getVisibility(Element elem) + /*-{ return elem.style.visibility; }-*/; + + private static native void setVisibility(Element elem, String disp) + /*-{ elem.style.visibility = disp; }-*/; + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java new file mode 100644 index 0000000000..7d9c9fc6ac --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java @@ -0,0 +1,65 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.ui.PopupPanel; + +/** + * A PopupPanel that can appear over Flash movies and Java applets. + * <p> + * Some browsers have issues with placing a <div> (such as that used by + * the PopupPanel implementation) over top of native UI such as that used by the + * Flash plugin. Often the native UI leaks over top of the <div>, which is + * not the desired behavior for a dialog box. + * <p> + * This implementation hides the native resources by setting their display + * property to 'none' when the dialog is shown, and restores them back to their + * prior setting when the dialog is hidden. + * */ +public class PluginSafePopupPanel extends PopupPanel { + private final PluginSafeDialogBoxImpl impl = + GWT.create(PluginSafeDialogBoxImpl.class); + + public PluginSafePopupPanel() { + this(false); + } + + public PluginSafePopupPanel(final boolean autoHide) { + this(autoHide, true); + } + + public PluginSafePopupPanel(final boolean autoHide, final boolean modal) { + super(autoHide, modal); + } + + @Override + public void setVisible(final boolean show) { + impl.visible(show); + super.setVisible(show); + } + + @Override + public void show() { + impl.visible(true); + super.show(); + } + + @Override + public void hide(final boolean autoClosed) { + impl.visible(false); + super.hide(autoClosed); + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java new file mode 100644 index 0000000000..02ba9aeac5 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java @@ -0,0 +1,81 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.Window; + +/** + * User agent feature tests we don't create permutations for. + * <p> + * Some features aren't worth creating full permutations in GWT for, as each new + * boolean permutation (only two settings) doubles the compile time required. If + * the setting only affects a couple of lines of JavaScript code, the slightly + * larger cache files for user agents that lack the functionality requested is + * trivial compared to the time developers lose building their application. + */ +public class UserAgent { + /** Does the browser have ShockwaveFlash plugin enabled? */ + public static final boolean hasFlash = hasFlash(); + + private static native boolean hasFlash() + /*-{ + if (navigator.plugins && navigator.plugins.length) { + if (navigator.plugins['Shockwave Flash']) return true; + if (navigator.plugins['Shockwave Flash 2.0']) return true; + + } else if (navigator.mimeTypes && navigator.mimeTypes.length) { + var mimeType = navigator.mimeTypes['application/x-shockwave-flash']; + if (mimeType && mimeType.enabledPlugin) return true; + + } else { + try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7'); return true; } catch (e) {} + try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); return true; } catch (e) {} + try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); return true; } catch (e) {} + } + return false; + }-*/; + + /** + * Test for and disallow running this application in an <iframe>. + * <p> + * If the application is running within an iframe this method requests a + * browser generated redirect to pop the application out of the iframe into + * the top level window, and then aborts execution by throwing an exception. + * This is call should be placed early within the module's onLoad() method, + * before any real UI can be initialized that an attacking site could try to + * snip out and present in a confusing context. + * <p> + * If the break out works, execution will restart automatically in a proper + * top level window, where the script has full control over the display. If + * the break out fails, execution will abort and stop immediately, preventing + * UI widgets from being created, leaving the user with an empty frame. + */ + public static void assertNotInIFrame() { + if (GWT.isScript() && amInsideIFrame()) { + bustOutOfIFrame(Window.Location.getHref()); + throw new RuntimeException(); + } + } + + private static native boolean amInsideIFrame() + /*-{ return top.location != $wnd.location; }-*/; + + private static native void bustOutOfIFrame(String newloc) + /*-{ top.location.href = newloc }-*/; + + private UserAgent() { + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java new file mode 100644 index 0000000000..35ecb12a9d --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java @@ -0,0 +1,57 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Widget; + +/** + * Widget to display within a {@link ViewSite}. + *<p> + * Implementations must override <code>protected void onLoad()</code> and + * arrange for {@link #display()} to be invoked once the DOM within the view is + * consistent for presentation to the user. Typically this means that the + * subclass can start RPCs within <code>onLoad()</code> and then invoke + * <code>display()</code> from within the AsyncCallback's + * <code>onSuccess(Object)</code> method. + */ +public abstract class View extends Composite { + ViewSite<? extends View> site; + + @Override + protected void onUnload() { + site = null; + super.onUnload(); + } + + /** true if this is the current view of its parent view site */ + public final boolean isCurrentView() { + Widget p = getParent(); + while (p != null) { + if (p instanceof ViewSite<?>) { + return ((ViewSite<?>) p).getView() == this; + } + p = p.getParent(); + } + return false; + } + + /** Replace the current view in the parent ViewSite with this view. */ + public final void display() { + if (site != null) { + site.swap(this); + } + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java new file mode 100644 index 0000000000..30b8408ff8 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java @@ -0,0 +1,87 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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.gwtexpui.user.client; + +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.SimplePanel; + +/** + * Hosts a single {@link View}. + * <p> + * View instances are attached inside of an invisible DOM node, permitting their + * <code>onLoad()</code> method to be invoked and to update the DOM prior to the + * elements being made visible in the UI. + * <p> + * Complaint View instances must invoke {@link View#display()} once the DOM is + * ready for presentation. + */ +public class ViewSite<V extends View> extends Composite { + private final FlowPanel main; + private SimplePanel current; + private SimplePanel next; + + public ViewSite() { + main = new FlowPanel(); + initWidget(main); + } + + /** Get the current view; null if there is no view being displayed. */ + @SuppressWarnings("unchecked") + public V getView() { + return current != null ? (V) current.getWidget() : null; + } + + /** + * Set the next view to display. + * <p> + * The view will be attached to the DOM tree within a hidden container, + * permitting its <code>onLoad()</code> method to execute and update the DOM + * without the user seeing the result. + * + * @param view the next view to display. + */ + public void setView(final V view) { + if (next != null) { + main.remove(next); + } + view.site = this; + next = new SimplePanel(); + next.setVisible(false); + main.add(next); + next.add(view); + } + + /** + * Invoked after the view becomes the current view and has been made visible. + * + * @param view the view being displayed. + */ + protected void onShowView(final V view) { + } + + @SuppressWarnings("unchecked") + final void swap(final View v) { + if (next != null && next.getWidget() == v) { + if (current != null) { + main.remove(current); + } + current = next; + next = null; + current.setVisible(true); + onShowView((V) v); + } + } +} |