1 /*
2  * DataImportFileChooser.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 
16 package org.rstudio.studio.client.workbench.views.environment.dataimport;
17 
18 import com.google.gwt.aria.client.Roles;
19 import org.rstudio.core.client.StringUtil;
20 import org.rstudio.core.client.files.FileSystemItem;
21 import org.rstudio.core.client.widget.CanSetControlId;
22 import org.rstudio.core.client.widget.Operation;
23 import org.rstudio.core.client.widget.ProgressIndicator;
24 import org.rstudio.core.client.widget.ProgressOperationWithInput;
25 import org.rstudio.core.client.widget.ThemedButton;
26 import org.rstudio.studio.client.RStudioGinjector;
27 import org.rstudio.studio.client.workbench.WorkbenchContext;
28 
29 import com.google.gwt.core.client.GWT;
30 import com.google.gwt.dom.client.Style.Unit;
31 import com.google.gwt.uibinder.client.UiBinder;
32 import com.google.gwt.uibinder.client.UiField;
33 import com.google.gwt.user.client.Timer;
34 import com.google.gwt.user.client.ui.Composite;
35 import com.google.gwt.user.client.ui.TextBox;
36 import com.google.gwt.user.client.ui.Widget;
37 import com.google.inject.Inject;
38 
39 public class DataImportFileChooser extends Composite
40                                    implements CanSetControlId
41 {
DataImportFileChooser(Operation updateOperation, boolean growTextbox)42    public DataImportFileChooser(Operation updateOperation, boolean growTextbox)
43    {
44       RStudioGinjector.INSTANCE.injectMembers(this);
45 
46       initWidget(uiBinder.createAndBindUi(this));
47 
48       updateOperation_ = updateOperation;
49 
50       if (growTextbox)
51       {
52          locationTextBox_.getElement().getStyle().setHeight(22, Unit.PX);
53          locationTextBox_.getElement().getStyle().setMarginTop(0, Unit.PX);
54       }
55 
56       locationTextBox_.addValueChangeHandler(stringValueChangeEvent ->
57       {
58       });
59 
60       actionButton_.addClickHandler(event ->
61       {
62          if (updateMode_)
63          {
64             updateOperation_.execute();
65          }
66          else
67          {
68             FileSystemItem fileSystemItemPath = FileSystemItem.createFile(getText());
69             if (getText() == "") {
70                fileSystemItemPath = workbenchContext_.getDefaultFileDialogDir();
71             }
72 
73             RStudioGinjector.INSTANCE.getFileDialogs().openFile(
74                   "Choose File",
75                   RStudioGinjector.INSTANCE.getRemoteFileSystemContext(),
76                   fileSystemItemPath,
77                   new ProgressOperationWithInput<FileSystemItem>()
78                   {
79                      public void execute(FileSystemItem input,
80                                          ProgressIndicator indicator)
81                      {
82                         if (input == null)
83                            return;
84 
85                         locationTextBox_.setText(input.getPath());
86                         preventModeChange();
87 
88                         indicator.onCompleted();
89 
90                         updateOperation_.execute();
91                      }
92                   });
93          }
94       });
95 
96       checkForTextBoxChange();
97    }
98 
99    @Inject
initialize(WorkbenchContext workbenchContext)100    private void initialize(WorkbenchContext workbenchContext)
101    {
102       workbenchContext_ = workbenchContext;
103    }
104 
setEnabled(boolean enabled)105    public void setEnabled(boolean enabled)
106    {
107       locationTextBox_.setEnabled(enabled);
108       actionButton_.setEnabled(enabled);
109    }
110 
getText()111    public String getText()
112    {
113       return locationTextBox_.getText();
114    }
115 
116    @Override
onDetach()117    public void onDetach()
118    {
119       checkTextBoxInterval_ = 0;
120    }
121 
setFocus()122    public void setFocus()
123    {
124       locationTextBox_.setFocus(true);
125    }
126 
127    @UiField
128    TextBox locationTextBox_;
129 
130    @UiField
131    ThemedButton actionButton_;
132 
checkForTextBoxChange()133    private void checkForTextBoxChange()
134    {
135       if (checkTextBoxInterval_ == 0)
136          return;
137 
138       // Check continuously for changes in the textbox to reliably detect changes even when OS pastes text
139       new Timer()
140       {
141          @Override
142          public void run()
143          {
144             if (lastTextBoxValue_ != null && locationTextBox_.getText() != lastTextBoxValue_)
145             {
146                switchToUpdateMode(!locationTextBox_.getText().isEmpty());
147             }
148 
149             lastTextBoxValue_ = locationTextBox_.getText();
150             checkForTextBoxChange();
151          }
152       }.schedule(checkTextBoxInterval_);
153    }
154 
preventModeChange()155    private void preventModeChange()
156    {
157       lastTextBoxValue_ = locationTextBox_.getText();
158    }
159 
switchToUpdateMode(Boolean updateMode)160    public void switchToUpdateMode(Boolean updateMode)
161    {
162       if (updateMode_ != updateMode)
163       {
164          updateMode_ = updateMode;
165          if (updateMode)
166          {
167             actionButton_.setText(updateModeCaption_);
168          }
169          else
170          {
171             actionButton_.setText(browseModeCaption_ + "...");
172          }
173          updateButtonAriaLabel();
174       }
175    }
176 
177    /**
178     * @param suffix aria-label for the button to provide additional context to
179     *               screen reader users; applied as a suffix to the visible
180     *               button text, e.g. "Browse..." becomes "Browse for File/URL..."
181     */
setAriaLabelSuffix(String suffix)182    public void setAriaLabelSuffix(String suffix)
183    {
184       ariaLabelSuffix_ = suffix;
185       updateButtonAriaLabel();
186    }
187 
updateButtonAriaLabel()188    public void updateButtonAriaLabel()
189    {
190       if (StringUtil.isNullOrEmpty(ariaLabelSuffix_))
191       {
192          Roles.getButtonRole().setAriaLabelProperty(actionButton_.getElement(), "");
193          return;
194       }
195 
196       final String prefix = updateMode_ ? updateModeCaption_ : browseModeCaption_ + " for";
197       final String finalSuffix = updateMode_ ? "" : "...";
198       Roles.getButtonRole().setAriaLabelProperty(actionButton_.getElement(),
199          prefix + " " + ariaLabelSuffix_ + finalSuffix);
200    }
201 
202    @Override
setElementId(String id)203    public void setElementId(String id)
204    {
205       locationTextBox_.getElement().setId(id);
206    }
207 
208    private static final String browseModeCaption_ = "Browse";
209    private static final String updateModeCaption_ = "Update";
210    private boolean updateMode_ = false;
211    private String lastTextBoxValue_;
212    private int checkTextBoxInterval_ = 250;
213    private final Operation updateOperation_;
214    private String ariaLabelSuffix_;
215 
216    private static DataImportFileChooserUiBinder uiBinder = GWT.create(DataImportFileChooserUiBinder.class);
217    interface DataImportFileChooserUiBinder extends UiBinder<Widget, DataImportFileChooser> {}
218 
219    private WorkbenchContext workbenchContext_;
220 }
221