1 /*
2  * ToolbarButton.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.core.client.widget;
16 
17 import com.google.gwt.aria.client.Roles;
18 import com.google.gwt.core.client.GWT;
19 import com.google.gwt.dom.client.*;
20 import com.google.gwt.dom.client.Style.Display;
21 import com.google.gwt.event.dom.client.*;
22 import com.google.gwt.event.shared.*;
23 import com.google.gwt.resources.client.ImageResource;
24 import com.google.gwt.uibinder.client.UiBinder;
25 import com.google.gwt.uibinder.client.UiField;
26 import com.google.gwt.user.client.ui.FocusWidget;
27 import com.google.gwt.user.client.ui.Widget;
28 import org.rstudio.core.client.ClassIds;
29 import org.rstudio.core.client.StringUtil;
30 import org.rstudio.core.client.command.ImageResourceProvider;
31 import org.rstudio.core.client.command.SimpleImageResourceProvider;
32 import org.rstudio.core.client.theme.res.ThemeResources;
33 import org.rstudio.core.client.theme.res.ThemeStyles;
34 
35 public class ToolbarButton extends FocusWidget
36 {
37    // button with no visible text
38    public static String NoText = null;
39 
40    // button with no tooltip/accessibility text
41    public static String NoTitle = null;
42 
43    private class SimpleHasHandlers extends HandlerManager implements HasHandlers
44    {
SimpleHasHandlers()45       private SimpleHasHandlers()
46       {
47          super(null);
48       }
49    }
50 
ToolbarButton( String text, String title, ImageResource leftImg, final HandlerManager eventBus, final GwtEvent<? extends T> targetEvent)51    public <T extends EventHandler> ToolbarButton(
52                                       String text,
53                                       String title,
54                                       ImageResource leftImg,
55                                       final HandlerManager eventBus,
56                                       final GwtEvent<? extends T> targetEvent)
57    {
58       this(text, title, leftImg, event -> eventBus.fireEvent(targetEvent));
59    }
60 
ToolbarButton(String text, String title, ImageResourceProvider leftImageProvider, ClickHandler clickHandler)61    public ToolbarButton(String text,
62                         String title,
63                         ImageResourceProvider leftImageProvider,
64                         ClickHandler clickHandler)
65    {
66       this(text, title, leftImageProvider, null, clickHandler);
67    }
68 
ToolbarButton(String text, String title, ImageResource leftImage)69    public ToolbarButton(String text,
70                         String title,
71                         ImageResource leftImage)
72    {
73       this(text, title, new SimpleImageResourceProvider(leftImage), null);
74    }
75 
ToolbarButton(String text, String title, ImageResource leftImage, ClickHandler clickHandler)76    public ToolbarButton(String text,
77                         String title,
78                         ImageResource leftImage,
79                         ClickHandler clickHandler)
80    {
81       this(text, title, new SimpleImageResourceProvider(leftImage), clickHandler);
82    }
83 
ToolbarButton(String text, String title, ImageResource leftImage, ImageResource rightImage, ClickHandler clickHandler)84    public ToolbarButton(String text,
85                         String title,
86                         ImageResource leftImage,
87                         ImageResource rightImage,
88                         ClickHandler clickHandler)
89    {
90       this(text,
91            title,
92            new SimpleImageResourceProvider(leftImage),
93            rightImage,
94            clickHandler);
95    }
96 
ToolbarButton(String text, String title, DecorativeImage leftImage, ImageResource rightImage, ClickHandler clickHandler)97    public ToolbarButton(String text, // visible text
98                         String title, // a11y / tooltip text
99                         DecorativeImage leftImage,
100                         ImageResource rightImage,
101                         ClickHandler clickHandler)
102    {
103       super();
104 
105       setElement(binder.createAndBindUi(this));
106       setClassId(null);
107 
108       this.setStylePrimaryName(styles_.toolbarButton());
109       this.addStyleName(styles_.handCursor());
110 
111       setText(text);
112       setTitle(title);
113       setInfoText(null);
114       leftImageWidget_ = leftImage == null ? new DecorativeImage() : leftImage;
115       leftImageWidget_.setStylePrimaryName(styles_.toolbarButtonLeftImage());
116       leftImageCell_.appendChild(leftImageWidget_.getElement());
117       if (rightImage != null)
118       {
119          rightImageWidget_ = new DecorativeImage(rightImage);
120          rightImageWidget_.setStylePrimaryName(styles_.toolbarButtonRightImage());
121          rightImageCell_.appendChild(rightImageWidget_.getElement());
122       }
123 
124       if (clickHandler != null)
125          addClickHandler(clickHandler);
126 
127       Roles.getPresentationRole().set(wrapper_);
128    }
129 
ToolbarButton(String text, String title, ImageResourceProvider leftImage, ImageResource rightImage, ClickHandler clickHandler)130    public ToolbarButton(String text,
131                         String title,
132                         ImageResourceProvider leftImage,
133                         ImageResource rightImage,
134                         ClickHandler clickHandler)
135    {
136       this(text,
137            title,
138            // extract the supplied left image
139            leftImage != null && leftImage.getImageResource() != null ?
140                new DecorativeImage(leftImage.getImageResource()) :
141                new DecorativeImage(),
142            rightImage,
143            clickHandler);
144 
145       // let the image provider know it has a rendered copy if we made one
146       if (leftImage != null && leftImageWidget_ != null)
147          leftImage.addRenderedImage(leftImageWidget_);
148    }
149 
150    @Override
addClickHandler(ClickHandler clickHandler)151    public HandlerRegistration addClickHandler(ClickHandler clickHandler)
152    {
153       /*
154        * When we directly subscribe to this widget's ClickEvent, sometimes the
155        * click gets ignored (inconsistent repro but it happens enough to be
156        * annoying). Doing it this way fixes it.
157        */
158 
159       hasHandlers_.addHandler(ClickEvent.getType(), clickHandler);
160 
161       final HandlerRegistration mouseDown = addMouseDownHandler(event ->
162       {
163          event.preventDefault();
164          event.stopPropagation();
165 
166          addStyleName(styles_.toolbarButtonPushed());
167          down_ = true;
168       });
169 
170       final HandlerRegistration mouseOut = addMouseOutHandler(event ->
171       {
172          event.preventDefault();
173          event.stopPropagation();
174 
175          removeStyleName(styles_.toolbarButtonPushed());
176          down_ = false;
177       });
178 
179       final HandlerRegistration mouseUp = addMouseUpHandler(event ->
180       {
181          event.preventDefault();
182          event.stopPropagation();
183 
184          if (down_)
185          {
186             down_ = false;
187             removeStyleName(styles_.toolbarButtonPushed());
188 
189             NativeEvent clickEvent = Document.get().createClickEvent(
190                   1,
191                   event.getScreenX(),
192                   event.getScreenY(),
193                   event.getClientX(),
194                   event.getClientY(),
195                   event.getNativeEvent().getCtrlKey(),
196                   event.getNativeEvent().getAltKey(),
197                   event.getNativeEvent().getShiftKey(),
198                   event.getNativeEvent().getMetaKey());
199             DomEvent.fireNativeEvent(clickEvent, hasHandlers_);
200          }
201       });
202 
203       final HandlerRegistration keyPress = addKeyPressHandler(event ->
204       {
205          char charCode = event.getCharCode();
206          if (charCode == KeyCodes.KEY_ENTER || charCode == KeyCodes.KEY_SPACE)
207          {
208             event.preventDefault();
209             event.stopPropagation();
210             click();
211          }
212       });
213 
214       return () ->
215       {
216          mouseDown.removeHandler();
217          mouseOut.removeHandler();
218          mouseUp.removeHandler();
219          keyPress.removeHandler();
220       };
221    }
222 
click()223    public void click()
224    {
225       NativeEvent clickEvent = Document.get().createClickEvent(
226             1,
227             0,
228             0,
229             0,
230             0,
231             false,
232             false,
233             false,
234             false);
235       DomEvent.fireNativeEvent(clickEvent, hasHandlers_);
236    }
237 
getParentToolbar()238    protected Toolbar getParentToolbar()
239    {
240       Widget parent = getParent();
241       while (parent != null)
242       {
243          if (parent instanceof Toolbar)
244             return (Toolbar) parent;
245          parent = parent.getParent();
246       }
247 
248       return null;
249    }
250 
setLeftImage(ImageResource imageResource)251    public void setLeftImage(ImageResource imageResource)
252    {
253       leftImageWidget_.setResource(imageResource);
254    }
255 
setClassId(String name)256    public void setClassId(String name)
257    {
258       if (!StringUtil.isNullOrEmpty(displayClassId_))
259          ClassIds.removeClassId(getElement(), displayClassId_);
260 
261       displayClassId_ = ClassIds.TOOLBAR_BTN + "_" + ClassIds.idSafeString(getTitle());
262       if (!StringUtil.isNullOrEmpty(name))
263          displayClassId_ += "_" + ClassIds.idSafeString(name);
264 
265       ClassIds.assignClassId(getElement(), displayClassId_);
266    }
267 
setText(boolean visible, String text)268    public void setText(boolean visible, String text)
269    {
270       if (visible)
271       {
272          setText(text);
273          Roles.getButtonRole().setAriaLabelProperty(getElement(), "");
274       }
275       else
276       {
277          setText("");
278          Roles.getButtonRole().setAriaLabelProperty(getElement(), text);
279       }
280    }
281 
setText(String label)282    public void setText(String label)
283    {
284       if (!StringUtil.isNullOrEmpty(label))
285       {
286          label_.setInnerText(label);
287          label_.getStyle().setDisplay(Display.BLOCK);
288          removeStyleName(styles_.noLabel());
289       }
290       else
291       {
292          label_.getStyle().setDisplay(Display.NONE);
293          addStyleName(styles_.noLabel());
294       }
295    }
296 
hasLabel()297    public boolean hasLabel()
298    {
299       return !getStyleName().contains(styles_.noLabel());
300    }
301 
setInfoText(String infoText)302    public void setInfoText(String infoText)
303    {
304       if (!StringUtil.isNullOrEmpty(infoText))
305       {
306          infoLabel_.setInnerText(infoText);
307          infoLabel_.getStyle().setDisplay(Display.BLOCK);
308       }
309       else
310       {
311          infoLabel_.getStyle().setDisplay(Display.NONE);
312       }
313    }
314 
getText()315    public String getText()
316    {
317       return StringUtil.notNull(label_.getInnerText());
318    }
319 
setTitle(String title)320    public void setTitle(String title)
321    {
322       super.setTitle(title);
323       if (!StringUtil.isNullOrEmpty(title))
324          Roles.getButtonRole().setAriaLabelProperty(getElement(), title);
325    }
326 
327    // Class name displayed by Help / Diagnostics / Show DOM Elements command. A default value is
328    // set in the constructor, but this can be updated to be more specific.
329    private String displayClassId_;
330 
331    private boolean down_;
332 
333    private final SimpleHasHandlers hasHandlers_ = new SimpleHasHandlers();
334 
335    interface Binder extends UiBinder<Element, ToolbarButton> { }
336 
337    private static final Binder binder = GWT.create(Binder.class);
338 
339    protected static final ThemeStyles styles_ = ThemeResources.INSTANCE.themeStyles();
340 
341    @UiField
342    TableCellElement leftImageCell_;
343    @UiField
344    TableCellElement rightImageCell_;
345    @UiField
346    DivElement label_;
347    @UiField
348    DivElement infoLabel_;
349    @UiField
350    TableElement wrapper_;
351    protected DecorativeImage leftImageWidget_;
352    protected DecorativeImage rightImageWidget_;
353 }
354