1 /*
2  * SVNReviewPanel.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.studio.client.workbench.views.vcs.svn.dialog;
16 
17 import com.google.gwt.core.client.GWT;
18 import com.google.gwt.core.client.Scheduler;
19 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
20 import com.google.gwt.dom.client.Document;
21 import com.google.gwt.dom.client.NativeEvent;
22 import com.google.gwt.dom.client.Style.Unit;
23 import com.google.gwt.event.dom.client.*;
24 import com.google.gwt.event.logical.shared.ValueChangeEvent;
25 import com.google.gwt.event.logical.shared.ValueChangeHandler;
26 import com.google.gwt.event.shared.GwtEvent;
27 import com.google.gwt.event.shared.HandlerManager;
28 import com.google.gwt.event.shared.HandlerRegistration;
29 import com.google.gwt.resources.client.ClientBundle;
30 import com.google.gwt.resources.client.ImageResource;
31 import com.google.gwt.resources.client.ImageResource.ImageOptions;
32 import com.google.gwt.resources.client.ImageResource.RepeatStyle;
33 import com.google.gwt.uibinder.client.UiBinder;
34 import com.google.gwt.uibinder.client.UiField;
35 import com.google.gwt.user.client.Command;
36 import com.google.gwt.user.client.Event;
37 import com.google.gwt.user.client.Event.NativePreviewEvent;
38 import com.google.gwt.user.client.Event.NativePreviewHandler;
39 import com.google.gwt.user.client.ui.*;
40 import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
41 import com.google.inject.Inject;
42 import org.rstudio.core.client.WidgetHandlerRegistration;
43 import org.rstudio.core.client.command.KeyboardShortcut;
44 import org.rstudio.core.client.resources.ImageResource2x;
45 import org.rstudio.core.client.widget.FormLabel;
46 import org.rstudio.core.client.widget.LeftRightToggleButton;
47 import org.rstudio.core.client.widget.Toolbar;
48 import org.rstudio.core.client.widget.ToolbarButton;
49 import org.rstudio.core.client.widget.ToolbarPopupMenu;
50 import org.rstudio.studio.client.common.vcs.GitServerOperations.PatchMode;
51 import org.rstudio.studio.client.common.vcs.StatusAndPath;
52 import org.rstudio.studio.client.workbench.commands.Commands;
53 import org.rstudio.studio.client.workbench.views.vcs.common.ChangelistTable;
54 import org.rstudio.studio.client.workbench.views.vcs.common.diff.ChunkOrLine;
55 import org.rstudio.studio.client.workbench.views.vcs.common.diff.LineTablePresenter;
56 import org.rstudio.studio.client.workbench.views.vcs.common.diff.LineTableView;
57 import org.rstudio.studio.client.workbench.views.vcs.dialog.SharedStyles;
58 import org.rstudio.studio.client.workbench.views.vcs.dialog.SizeWarningWidget;
59 import org.rstudio.studio.client.workbench.views.vcs.svn.SVNChangelistTablePresenter;
60 import org.rstudio.studio.client.workbench.views.vcs.svn.dialog.SVNReviewPresenter.Display;
61 
62 import java.util.ArrayList;
63 
64 public class SVNReviewPanel extends ResizeComposite implements Display
65 {
66    interface Resources extends ClientBundle
67    {
68       @Source("SVNReviewPanel.css")
styles()69       Styles styles();
70 
71       @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
72       @Source("../../dialog/images/toolbarTile.png")
toolbarTile()73       ImageResource toolbarTile();
74 
75       @Source("../../dialog/images/stageAllFiles_2x.png")
stageAllFiles2x()76       ImageResource stageAllFiles2x();
77 
78       @Source("../../dialog/images/discard_2x.png")
discard2x()79       ImageResource discard2x();
80 
81       @Source("../../dialog/images/stage_2x.png")
stage2x()82       ImageResource stage2x();
83 
84       @Source("../../dialog/images/splitterTileV.png")
85       @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
splitterTileV()86       ImageResource splitterTileV();
87 
88       @Source("../../dialog/images/splitterTileH.png")
89       @ImageOptions(repeatStyle = RepeatStyle.Vertical)
splitterTileH()90       ImageResource splitterTileH();
91 
92       @Source("../../dialog/images/blankFileIcon_2x.png")
blankFileIcon2x()93       ImageResource blankFileIcon2x();
94    }
95 
96    interface Styles extends SharedStyles
97    {
contextLabel()98       String contextLabel();
diffToolbar()99       String diffToolbar();
diffViewOptions()100       String diffViewOptions();
diffContextLines()101       String diffContextLines();
102    }
103 
104    @SuppressWarnings("unused")
105    private static class ClickCommand implements HasClickHandlers, Command
106    {
107       @Override
execute()108       public void execute()
109       {
110          ClickEvent.fireNativeEvent(
111                Document.get()
112                      .createClickEvent(0,
113                                        0,
114                                        0,
115                                        0,
116                                        0,
117                                        false,
118                                        false,
119                                        false,
120                                        false),
121                this);
122       }
123 
124       @Override
addClickHandler(ClickHandler handler)125       public HandlerRegistration addClickHandler(ClickHandler handler)
126       {
127          return handlerManager_.addHandler(ClickEvent.getType(), handler);
128       }
129 
130       @Override
fireEvent(GwtEvent<?> event)131       public void fireEvent(GwtEvent<?> event)
132       {
133          handlerManager_.fireEvent(event);
134       }
135 
136       private final HandlerManager handlerManager_ = new HandlerManager(this);
137    }
138 
139    private static class ListBoxAdapter implements HasValue<Integer>
140    {
ListBoxAdapter(ListBox listBox)141       private ListBoxAdapter(ListBox listBox)
142       {
143          listBox_ = listBox;
144          listBox_.addChangeHandler(new ChangeHandler()
145          {
146             @Override
147             public void onChange(ChangeEvent event)
148             {
149                ValueChangeEvent.fire(ListBoxAdapter.this, getValue());
150             }
151          });
152       }
153 
154       @Override
getValue()155       public Integer getValue()
156       {
157          return Integer.parseInt(
158                listBox_.getValue(listBox_.getSelectedIndex()));
159       }
160 
161       @Override
setValue(Integer value)162       public void setValue(Integer value)
163       {
164          setValue(value, true);
165       }
166 
167       @Override
setValue(Integer value, boolean fireEvents)168       public void setValue(Integer value, boolean fireEvents)
169       {
170          String valueStr = value.toString();
171          for (int i = 0; i < listBox_.getItemCount(); i++)
172          {
173             if (listBox_.getValue(i) == valueStr)
174             {
175                listBox_.setSelectedIndex(i);
176                break;
177             }
178          }
179       }
180 
181       @Override
addValueChangeHandler(ValueChangeHandler<Integer> handler)182       public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler)
183       {
184          return handlers_.addHandler(ValueChangeEvent.getType(), handler);
185       }
186 
187       @Override
fireEvent(GwtEvent<?> event)188       public void fireEvent(GwtEvent<?> event)
189       {
190          handlers_.fireEvent(event);
191       }
192 
193       private final ListBox listBox_;
194       private final HandlerManager handlers_ = new HandlerManager(this);
195    }
196 
197    interface Binder extends UiBinder<Widget, SVNReviewPanel>
198    {
199    }
200 
201    @Inject
SVNReviewPanel(SVNChangelistTablePresenter changelist, LineTableView diffPane, Commands commands)202    public SVNReviewPanel(SVNChangelistTablePresenter changelist,
203                          LineTableView diffPane,
204                          Commands commands)
205    {
206       commands_ = commands;
207       splitPanel_ = new SplitLayoutPanel(4);
208 
209       changelist_ = changelist.getView();
210       lines_ = diffPane;
211       lines_.getElement().setTabIndex(-1);
212       lines_.hideStageCommands();
213 
214       overrideSizeWarning_ = new SizeWarningWidget("diff");
215 
216       topToolbar_ = new Toolbar("SVN Review");
217       diffToolbar_ = new Toolbar("SVN Diff");
218 
219       changelist.setSelectFirstItemByDefault(true);
220 
221       Widget widget = GWT.<Binder>create(Binder.class).createAndBindUi(this);
222       initWidget(widget);
223 
224       topToolbar_.addStyleName(RES.styles().toolbar());
225       topToolbar_.getWrapper().addStyleName(RES.styles().toolbarInnerWrapper());
226 
227       switchViewButton_ = new LeftRightToggleButton("Changes", "History", true);
228       switchViewButton_.getElement().getStyle().setMarginRight(8, Unit.PX);
229       topToolbar_.addLeftWidget(switchViewButton_);
230 
231       topToolbar_.addLeftSeparator();
232 
233       topToolbar_.addLeftWidget(new ToolbarButton(
234             "Refresh", ToolbarButton.NoTitle, commands.vcsRefresh().getImageResource(),
235             new ClickHandler() {
236                @Override
237                public void onClick(ClickEvent event)
238                {
239                   changelist_.showProgress();
240                   commands_.vcsRefresh().execute();
241                }
242             }));
243 
244       topToolbar_.addLeftSeparator();
245 
246       topToolbar_.addLeftWidget(commands.vcsAddFiles().createToolbarButton());
247       topToolbar_.addLeftWidget(commands.vcsRemoveFiles().createToolbarButton());
248       topToolbar_.addLeftSeparator();
249       topToolbar_.addLeftWidget(commands.vcsRevert().createToolbarButton());
250       topToolbar_.addLeftWidget(commands.vcsIgnore().createToolbarButton());
251       topToolbar_.addLeftSeparator();
252       topToolbar_.addLeftWidget(commands.vcsResolve().createToolbarButton());
253       topToolbar_.addLeftSeparator();
254       topToolbar_.addLeftWidget(commands.vcsCommit().createToolbarButton());
255 
256 
257       commands.vcsPull().setButtonLabel("Update");
258       commands.vcsPull().setMenuLabel("Update");
259       topToolbar_.addRightWidget(commands.vcsPull().createToolbarButton());
260 
261       diffToolbar_.addStyleName(RES.styles().toolbar());
262       diffToolbar_.addStyleName(RES.styles().diffToolbar());
263       diffToolbar_.getWrapper().addStyleName(RES.styles().diffToolbarInnerWrapper());
264 
265       diffToolbar_.addLeftSeparator();
266       discardAllButton_ = diffToolbar_.addLeftWidget(new ToolbarButton(
267             "Discard All", ToolbarButton.NoTitle,  new ImageResource2x(RES.discard2x())));
268 
269       contextLines_.addStyleName(RES.styles().diffContextLines());
270       lblContext_.setFor(contextLines_);
271       listBoxAdapter_ = new ListBoxAdapter(contextLines_);
272 
273       new WidgetHandlerRegistration(this)
274       {
275          @Override
276          protected HandlerRegistration doRegister()
277          {
278             return Event.addNativePreviewHandler(new NativePreviewHandler()
279             {
280                @Override
281                public void onPreviewNativeEvent(NativePreviewEvent event)
282                {
283                   NativeEvent nativeEvent = event.getNativeEvent();
284                   if (event.getTypeInt() == Event.ONKEYDOWN
285                       && KeyboardShortcut.getModifierValue(nativeEvent) == KeyboardShortcut.CTRL)
286                   {
287                      switch (nativeEvent.getKeyCode())
288                      {
289                         case KeyCodes.KEY_DOWN:
290                            nativeEvent.preventDefault();
291                            scrollBy(diffScroll_, getLineScroll(diffScroll_), 0);
292                            break;
293                         case KeyCodes.KEY_UP:
294                            nativeEvent.preventDefault();
295                            scrollBy(diffScroll_,
296                                     -getLineScroll(diffScroll_),
297                                     0);
298                            break;
299                         case KeyCodes.KEY_PAGEDOWN:
300                            nativeEvent.preventDefault();
301                            scrollBy(diffScroll_, getPageScroll(diffScroll_), 0);
302                            break;
303                         case KeyCodes.KEY_PAGEUP:
304                            nativeEvent.preventDefault();
305                            scrollBy(diffScroll_,
306                                     -getPageScroll(diffScroll_),
307                                     0);
308                            break;
309                      }
310                   }
311                }
312             });
313          }
314       };
315    }
316 
317    private void scrollBy(ScrollPanel scrollPanel, int vscroll, int hscroll)
318    {
319       if (vscroll != 0)
320       {
321          scrollPanel.setVerticalScrollPosition(
322                Math.max(0, scrollPanel.getVerticalScrollPosition() + vscroll));
323       }
324 
325       if (hscroll != 0)
326       {
327          scrollPanel.setHorizontalScrollPosition(
328                Math.max(0, scrollPanel.getHorizontalScrollPosition() + hscroll));
329       }
330    }
331 
332    private int getLineScroll(ScrollPanel panel)
333    {
334       return 30;
335    }
336 
337    private int getPageScroll(ScrollPanel panel)
338    {
339       // Return slightly less than the client height (so there's overlap between
340       // one screen and the next) but never less than the line scroll height.
341       return Math.max(
342             getLineScroll(panel),
343             panel.getElement().getClientHeight() - getLineScroll(panel));
344    }
345 
346    @Override
347    public HasClickHandlers getSwitchViewButton()
348    {
349       return switchViewButton_;
350    }
351 
352    @Override
353    public HasClickHandlers getDiscardAllButton()
354    {
355       return discardAllButton_;
356    }
357 
358 
359    @Override
360    public void setData(ArrayList<ChunkOrLine> lines)
361    {
362       int vscroll = diffScroll_.getVerticalScrollPosition();
363       int hscroll = diffScroll_.getHorizontalScrollPosition();
364 
365       getLineTableDisplay().setData(lines, PatchMode.Working);
366 
367       diffScroll_.setVerticalScrollPosition(vscroll);
368       diffScroll_.setHorizontalScrollPosition(hscroll);
369    }
370 
371    @Override
372    public ArrayList<String> getSelectedPaths()
373    {
374       return changelist_.getSelectedPaths();
375    }
376 
377    @Override
378    public ArrayList<StatusAndPath> getSelectedItems()
379    {
380       return changelist_.getSelectedItems();
381    }
382 
383    @Override
384    public void setSelectedStatusAndPaths(ArrayList<StatusAndPath> selectedPaths)
385    {
386       changelist_.setSelectedStatusAndPaths(selectedPaths);
387    }
388 
389    @Override
390    public LineTablePresenter.Display getLineTableDisplay()
391    {
392       return lines_;
393    }
394 
395    @Override
396    public ChangelistTable getChangelistTable()
397    {
398       return changelist_;
399    }
400 
401    @Override
402    public HasValue<Integer> getContextLines()
403    {
404       return listBoxAdapter_;
405    }
406 
407    @Override
408    public HasClickHandlers getOverrideSizeWarningButton()
409    {
410       return overrideSizeWarning_;
411    }
412 
413    @Override
414    public void showSizeWarning(long sizeInBytes)
415    {
416       overrideSizeWarning_.setSize(sizeInBytes);
417       diffScroll_.setWidget(overrideSizeWarning_);
418    }
419 
420    @Override
421    public void hideSizeWarning()
422    {
423       diffScroll_.setWidget(lines_);
424    }
425 
426    @Override
427    public void showContextMenu(final int clientX,
428                                final int clientY)
429    {
430       final ToolbarPopupMenu menu = new ToolbarPopupMenu();
431 
432       menu.addItem(commands_.vcsAddFiles().createMenuItem(false));
433       menu.addItem(commands_.vcsRemoveFiles().createMenuItem(false));
434       menu.addSeparator();
435       menu.addItem(commands_.vcsRevert().createMenuItem(false));
436       menu.addItem(commands_.vcsIgnore().createMenuItem(false));
437       menu.addSeparator();
438       menu.addItem(commands_.vcsResolve().createMenuItem(false));
439       menu.addSeparator();
440       menu.addItem(commands_.vcsOpen().createMenuItem(false));
441 
442       menu.setPopupPositionAndShow(new PositionCallback() {
443          @Override
444          public void setPosition(int offsetWidth, int offsetHeight)
445          {
446             menu.setPopupPosition(clientX, clientY);
447          }
448       });
449    }
450 
451    @Override
452    public void onShow()
453    {
454       Scheduler.get().scheduleDeferred(new ScheduledCommand()
455       {
456          @Override
457          public void execute()
458          {
459             changelist_.focus();
460          }
461       });
462    }
463 
464    @UiField(provided = true)
465    SplitLayoutPanel splitPanel_;
466    @UiField(provided = true)
467    ChangelistTable changelist_;
468    @UiField(provided = true)
469    LineTableView lines_;
470    @UiField
471    FormLabel lblContext_;
472    @UiField
473    ListBox contextLines_;
474    @UiField(provided = true)
475    Toolbar topToolbar_;
476    @UiField(provided = true)
477    Toolbar diffToolbar_;
478    @UiField
479    ScrollPanel diffScroll_;
480 
481    private final Commands commands_;
482 
483    private ListBoxAdapter listBoxAdapter_;
484 
485    private ToolbarButton discardAllButton_;
486 
487    private LeftRightToggleButton switchViewButton_;
488 
489    private SizeWarningWidget overrideSizeWarning_;
490 
491    private static final Resources RES = GWT.create(Resources.class);
492    static {
493       RES.styles().ensureInjected();
494    }
495 }