96e28efe642159af7a5df034facfb75d8a85045b
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / impl / DiffRequestProcessor.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.diff.impl;
17
18 import com.intellij.codeInsight.hint.HintManager;
19 import com.intellij.codeInsight.hint.HintManagerImpl;
20 import com.intellij.codeInsight.hint.HintUtil;
21 import com.intellij.diff.*;
22 import com.intellij.diff.FrameDiffTool.DiffViewer;
23 import com.intellij.diff.actions.impl.*;
24 import com.intellij.diff.impl.DiffSettingsHolder.DiffSettings;
25 import com.intellij.diff.requests.*;
26 import com.intellij.diff.tools.ErrorDiffTool;
27 import com.intellij.diff.tools.external.ExternalDiffTool;
28 import com.intellij.diff.tools.util.DiffDataKeys;
29 import com.intellij.diff.tools.util.PrevNextDifferenceIterable;
30 import com.intellij.diff.util.DiffUserDataKeys;
31 import com.intellij.diff.util.DiffUserDataKeysEx;
32 import com.intellij.diff.util.DiffUserDataKeysEx.ScrollToPolicy;
33 import com.intellij.diff.util.DiffUtil;
34 import com.intellij.diff.util.LineRange;
35 import com.intellij.ide.DataManager;
36 import com.intellij.ide.impl.DataManagerImpl;
37 import com.intellij.internal.statistic.UsageTrigger;
38 import com.intellij.internal.statistic.beans.ConvertUsagesUtil;
39 import com.intellij.openapi.Disposable;
40 import com.intellij.openapi.actionSystem.*;
41 import com.intellij.openapi.actionSystem.ex.ActionUtil;
42 import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
43 import com.intellij.openapi.application.ApplicationManager;
44 import com.intellij.openapi.diagnostic.Logger;
45 import com.intellij.openapi.editor.Editor;
46 import com.intellij.openapi.editor.LogicalPosition;
47 import com.intellij.openapi.project.DumbAware;
48 import com.intellij.openapi.project.DumbAwareAction;
49 import com.intellij.openapi.project.Project;
50 import com.intellij.openapi.ui.Messages;
51 import com.intellij.openapi.ui.popup.Balloon;
52 import com.intellij.openapi.ui.popup.JBPopupFactory;
53 import com.intellij.openapi.ui.popup.ListPopup;
54 import com.intellij.openapi.util.Disposer;
55 import com.intellij.openapi.util.Key;
56 import com.intellij.openapi.util.UserDataHolder;
57 import com.intellij.openapi.util.UserDataHolderBase;
58 import com.intellij.openapi.wm.IdeFocusManager;
59 import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
60 import com.intellij.ui.HintHint;
61 import com.intellij.ui.JBProgressBar;
62 import com.intellij.ui.LightweightHint;
63 import com.intellij.ui.components.panels.Wrapper;
64 import com.intellij.util.containers.ContainerUtil;
65 import com.intellij.util.ui.JBUI;
66 import com.intellij.util.ui.UIUtil;
67 import org.jetbrains.annotations.CalledInAwt;
68 import org.jetbrains.annotations.NonNls;
69 import org.jetbrains.annotations.NotNull;
70 import org.jetbrains.annotations.Nullable;
71
72 import javax.swing.*;
73 import java.awt.*;
74 import java.util.ArrayList;
75 import java.util.List;
76
77 @SuppressWarnings("InnerClassMayBeStatic")
78 public abstract class DiffRequestProcessor implements Disposable {
79   private static final Logger LOG = Logger.getInstance(DiffRequestProcessor.class);
80
81   private boolean myDisposed;
82
83   @Nullable private final Project myProject;
84   @NotNull private final DiffContext myContext;
85
86   @NotNull private final DiffSettings mySettings;
87   @NotNull private final List<DiffTool> myAvailableTools;
88   @NotNull private final List<DiffTool> myToolOrder;
89
90   @NotNull private final OpenInEditorAction myOpenInEditorAction;
91   @Nullable private DefaultActionGroup myPopupActionGroup;
92
93   @NotNull private final JPanel myPanel;
94   @NotNull private final MyPanel myMainPanel;
95   @NotNull private final Wrapper myContentPanel;
96   @NotNull private final Wrapper myToolbarPanel; // TODO: allow to call 'updateToolbar' from Viewer ?
97   @NotNull private final Wrapper myToolbarStatusPanel;
98   @NotNull private final MyProgressBar myProgressBar;
99
100   @NotNull private DiffRequest myActiveRequest;
101
102   @NotNull private ViewerState myState;
103
104   public DiffRequestProcessor(@Nullable Project project) {
105     this(project, new UserDataHolderBase());
106   }
107
108   public DiffRequestProcessor(@Nullable Project project, @NotNull String place) {
109     this(project, DiffUtil.createUserDataHolder(DiffUserDataKeys.PLACE, place));
110   }
111
112   public DiffRequestProcessor(@Nullable Project project, @NotNull UserDataHolder context) {
113     myProject = project;
114
115     myContext = new MyDiffContext(context);
116     myActiveRequest = new LoadingDiffRequest();
117
118     mySettings = DiffSettingsHolder.getInstance().getSettings(myContext.getUserData(DiffUserDataKeys.PLACE));
119
120     myAvailableTools = DiffManagerEx.getInstance().getDiffTools();
121     myToolOrder = new ArrayList<>(getToolOrderFromSettings(myAvailableTools));
122
123     // UI
124
125     myMainPanel = new MyPanel();
126     myContentPanel = new Wrapper();
127     myToolbarPanel = new Wrapper();
128     myToolbarPanel.setFocusable(true);
129     myToolbarStatusPanel = new Wrapper();
130     myProgressBar = new MyProgressBar();
131
132     myPanel = JBUI.Panels.simplePanel(myMainPanel);
133
134     JPanel statusPanel = JBUI.Panels.simplePanel(myToolbarStatusPanel).addToLeft(myProgressBar);
135     JPanel topPanel = JBUI.Panels.simplePanel(myToolbarPanel).addToRight(statusPanel);
136
137     myMainPanel.add(topPanel, BorderLayout.NORTH);
138     myMainPanel.add(myContentPanel, BorderLayout.CENTER);
139
140     myMainPanel.setFocusTraversalPolicyProvider(true);
141     myMainPanel.setFocusTraversalPolicy(new MyFocusTraversalPolicy());
142
143     JComponent bottomPanel = myContext.getUserData(DiffUserDataKeysEx.BOTTOM_PANEL);
144     if (bottomPanel != null) myMainPanel.add(bottomPanel, BorderLayout.SOUTH);
145     if (bottomPanel instanceof Disposable) Disposer.register(this, (Disposable)bottomPanel);
146
147     myState = EmptyState.INSTANCE;
148     myContentPanel.setContent(DiffUtil.createMessagePanel(((LoadingDiffRequest)myActiveRequest).getMessage()));
149
150     myOpenInEditorAction = new OpenInEditorAction(() -> onAfterNavigate());
151   }
152
153   //
154   // Update
155   //
156
157   @CalledInAwt
158   protected void reloadRequest() {
159     updateRequest(true);
160   }
161
162   @CalledInAwt
163   public void updateRequest() {
164     updateRequest(false);
165   }
166
167   @CalledInAwt
168   public void updateRequest(boolean force) {
169     updateRequest(force, null);
170   }
171
172   @CalledInAwt
173   public abstract void updateRequest(boolean force, @Nullable ScrollToPolicy scrollToChangePolicy);
174
175   @NotNull
176   private FrameDiffTool getFittedTool() {
177     List<FrameDiffTool> tools = filterFittedTools(myToolOrder);
178     return tools.isEmpty() ? ErrorDiffTool.INSTANCE : tools.get(0);
179   }
180
181   @NotNull
182   private List<FrameDiffTool> getAvailableFittedTools() {
183     return filterFittedTools(myAvailableTools);
184   }
185
186   @NotNull
187   private List<FrameDiffTool> filterFittedTools(@NotNull List<DiffTool> tools) {
188     List<FrameDiffTool> result = new ArrayList<>();
189     for (DiffTool tool : tools) {
190       try {
191         if (tool instanceof FrameDiffTool && tool.canShow(myContext, myActiveRequest)) {
192           result.add((FrameDiffTool)tool);
193         }
194       }
195       catch (Throwable e) {
196         LOG.error(e);
197       }
198     }
199
200     return DiffUtil.filterSuppressedTools(result);
201   }
202
203   private void moveToolOnTop(@NotNull DiffTool tool) {
204     myToolOrder.remove(tool);
205
206     FrameDiffTool toolToReplace = getFittedTool();
207
208     int index;
209     for (index = 0; index < myToolOrder.size(); index++) {
210       if (myToolOrder.get(index) == toolToReplace) break;
211     }
212     myToolOrder.add(index, tool);
213
214     updateToolOrderSettings(myToolOrder);
215   }
216
217   @NotNull
218   private ViewerState createState() {
219     FrameDiffTool frameTool = getFittedTool();
220
221     DiffViewer viewer = frameTool.createComponent(myContext, myActiveRequest);
222
223     for (DiffExtension extension : DiffExtension.EP_NAME.getExtensions()) {
224       try {
225         extension.onViewerCreated(viewer, myContext, myActiveRequest);
226       }
227       catch (Throwable e) {
228         LOG.error(e);
229       }
230     }
231
232     DiffViewerWrapper wrapper = myActiveRequest.getUserData(DiffViewerWrapper.KEY);
233     if (wrapper == null) {
234       return new DefaultState(viewer, frameTool);
235     }
236     else {
237       return new WrapperState(viewer, frameTool, wrapper);
238     }
239   }
240
241   //
242   // Abstract
243   //
244
245   @Nullable private ApplyData myQueuedApplyRequest;
246
247   @CalledInAwt
248   protected void applyRequest(@NotNull DiffRequest request, boolean force, @Nullable ScrollToPolicy scrollToChangePolicy) {
249     ApplicationManager.getApplication().assertIsDispatchThread();
250     myIterationState = IterationState.NONE;
251
252     force = force || (myQueuedApplyRequest != null && myQueuedApplyRequest.force);
253     myQueuedApplyRequest = new ApplyData(request, force, scrollToChangePolicy);
254
255     IdeFocusManager.getInstance(myProject).doWhenFocusSettlesDown(() -> {
256       if (myQueuedApplyRequest == null || myDisposed) return;
257       doApplyRequest(myQueuedApplyRequest.request, myQueuedApplyRequest.force, myQueuedApplyRequest.scrollToChangePolicy);
258       myQueuedApplyRequest = null;
259     });
260   }
261
262   @CalledInAwt
263   private void doApplyRequest(@NotNull DiffRequest request, boolean force, @Nullable ScrollToPolicy scrollToChangePolicy) {
264     if (!force && request == myActiveRequest) return;
265
266     request.putUserData(DiffUserDataKeysEx.SCROLL_TO_CHANGE, scrollToChangePolicy);
267
268     boolean hadFocus = isFocused();
269
270     myState.destroy();
271     myToolbarStatusPanel.setContent(null);
272     myToolbarPanel.setContent(null);
273     myContentPanel.setContent(null);
274     ActionUtil.clearActions(myMainPanel);
275
276     myActiveRequest.onAssigned(false);
277     myActiveRequest = request;
278     myActiveRequest.onAssigned(true);
279
280     try {
281       myState = createState();
282       myState.init();
283     }
284     catch (Throwable e) {
285       LOG.error(e);
286       myState = new ErrorState(new ErrorDiffRequest("Error: can't show diff"), getFittedTool());
287       myState.init();
288     }
289
290     if (hadFocus) requestFocusInternal();
291   }
292
293   protected void setWindowTitle(@NotNull String title) {
294   }
295
296   protected void onAfterNavigate() {
297   }
298
299   @CalledInAwt
300   protected void onDispose() {
301   }
302
303   @Nullable
304   public <T> T getContextUserData(@NotNull Key<T> key) {
305     return myContext.getUserData(key);
306   }
307
308   public <T> void putContextUserData(@NotNull Key<T> key, @Nullable T value) {
309     myContext.putUserData(key, value);
310   }
311
312   @NotNull
313   protected List<AnAction> getNavigationActions() {
314     return ContainerUtil.<AnAction>list(
315       new MyPrevDifferenceAction(),
316       new MyNextDifferenceAction(),
317       new MyPrevChangeAction(),
318       new MyNextChangeAction()
319     );
320   }
321
322   //
323   // Misc
324   //
325
326   public boolean isWindowFocused() {
327     Window window = SwingUtilities.getWindowAncestor(myPanel);
328     return window != null && window.isFocused();
329   }
330
331   public boolean isFocused() {
332     return DiffUtil.isFocusedComponent(myProject, myPanel);
333   }
334
335   public void requestFocus() {
336     DiffUtil.requestFocus(myProject, getPreferredFocusedComponent());
337   }
338
339   protected void requestFocusInternal() {
340     JComponent component = getPreferredFocusedComponent();
341     if (component != null) component.requestFocusInWindow();
342   }
343
344   @NotNull
345   protected List<DiffTool> getToolOrderFromSettings(@NotNull List<DiffTool> availableTools) {
346     List<DiffTool> result = new ArrayList<>();
347     List<String> savedOrder = getSettings().getDiffToolsOrder();
348
349     for (final String clazz : savedOrder) {
350       DiffTool tool = ContainerUtil.find(availableTools, t -> t.getClass().getCanonicalName().equals(clazz));
351       if (tool != null) result.add(tool);
352     }
353
354     for (DiffTool tool : availableTools) {
355       if (!result.contains(tool)) result.add(tool);
356     }
357
358     return result;
359   }
360
361   protected void updateToolOrderSettings(@NotNull List<DiffTool> toolOrder) {
362     List<String> savedOrder = new ArrayList<>();
363     for (DiffTool tool : toolOrder) {
364       savedOrder.add(tool.getClass().getCanonicalName());
365     }
366     getSettings().setDiffToolsOrder(savedOrder);
367   }
368
369   @Override
370   public void dispose() {
371     if (myDisposed) return;
372     UIUtil.invokeLaterIfNeeded(() -> {
373       if (myDisposed) return;
374       myDisposed = true;
375
376       onDispose();
377
378       myState.destroy();
379       myToolbarStatusPanel.setContent(null);
380       myToolbarPanel.setContent(null);
381       myContentPanel.setContent(null);
382
383       myActiveRequest.onAssigned(false);
384
385       myState = EmptyState.INSTANCE;
386       myActiveRequest = NoDiffRequest.INSTANCE;
387     });
388   }
389
390   @NotNull
391   protected DefaultActionGroup collectToolbarActions(@Nullable List<AnAction> viewerActions) {
392     DefaultActionGroup group = new DefaultActionGroup();
393
394     List<AnAction> navigationActions = new ArrayList<>();
395     navigationActions.addAll(getNavigationActions());
396     navigationActions.add(myOpenInEditorAction);
397     navigationActions.add(new MyChangeDiffToolAction());
398     DiffUtil.addActionBlock(group,
399                             navigationActions);
400
401     DiffUtil.addActionBlock(group, viewerActions);
402
403     List<AnAction> requestContextActions = myActiveRequest.getUserData(DiffUserDataKeys.CONTEXT_ACTIONS);
404     DiffUtil.addActionBlock(group, requestContextActions);
405
406     List<AnAction> contextActions = myContext.getUserData(DiffUserDataKeys.CONTEXT_ACTIONS);
407     DiffUtil.addActionBlock(group, contextActions);
408
409     DiffUtil.addActionBlock(group,
410                             new ShowInExternalToolAction(),
411                             new ShowOldDiffAction(),
412                             ActionManager.getInstance().getAction(IdeActions.ACTION_CONTEXT_HELP));
413
414     return group;
415   }
416
417   @NotNull
418   protected DefaultActionGroup collectPopupActions(@Nullable List<AnAction> viewerActions) {
419     DefaultActionGroup group = new DefaultActionGroup();
420
421     List<AnAction> selectToolActions = new ArrayList<>();
422     for (DiffTool tool : getAvailableFittedTools()) {
423       if (tool == myState.getActiveTool()) continue;
424       selectToolActions.add(new DiffToolToggleAction(tool));
425     }
426     DiffUtil.addActionBlock(group, selectToolActions);
427
428     DiffUtil.addActionBlock(group, viewerActions);
429
430     return group;
431   }
432
433   protected void buildToolbar(@Nullable List<AnAction> viewerActions) {
434     ActionGroup group = collectToolbarActions(viewerActions);
435     ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.DIFF_TOOLBAR, group, true);
436
437     DataManager.registerDataProvider(toolbar.getComponent(), myMainPanel);
438     toolbar.setTargetComponent(toolbar.getComponent());
439
440     myToolbarPanel.setContent(toolbar.getComponent());
441     for (AnAction action : group.getChildren(null)) {
442       DiffUtil.registerAction(action, myMainPanel);
443     }
444   }
445
446   protected void buildActionPopup(@Nullable List<AnAction> viewerActions) {
447     ShowActionGroupPopupAction action = new ShowActionGroupPopupAction();
448     DiffUtil.registerAction(action, myMainPanel);
449
450     myPopupActionGroup = collectPopupActions(viewerActions);
451   }
452
453   private void setTitle(@Nullable String title) {
454     if (getContextUserData(DiffUserDataKeys.DO_NOT_CHANGE_WINDOW_TITLE) == Boolean.TRUE) return;
455     if (title == null) title = "Diff";
456     setWindowTitle(title);
457   }
458
459   //
460   // Getters
461   //
462
463   @NotNull
464   public JComponent getComponent() {
465     return myPanel;
466   }
467
468   @Nullable
469   public JComponent getPreferredFocusedComponent() {
470     JComponent component = myState.getPreferredFocusedComponent();
471     return component != null ? component : myToolbarPanel.getTargetComponent();
472   }
473
474   @Nullable
475   public Project getProject() {
476     return myProject;
477   }
478
479   @NotNull
480   public DiffContext getContext() {
481     return myContext;
482   }
483
484   @NotNull
485   protected DiffSettings getSettings() {
486     return mySettings;
487   }
488
489   public boolean isDisposed() {
490     return myDisposed;
491   }
492
493   //
494   // Actions
495   //
496
497   private class ShowInExternalToolAction extends DumbAwareAction {
498     public ShowInExternalToolAction() {
499       ActionUtil.copyFrom(this, "Diff.ShowInExternalTool");
500     }
501
502     @Override
503     public void update(AnActionEvent e) {
504       if (!ExternalDiffTool.isEnabled()) {
505         e.getPresentation().setEnabledAndVisible(false);
506         return;
507       }
508       e.getPresentation().setEnabled(ExternalDiffTool.canShow(myActiveRequest));
509       e.getPresentation().setVisible(true);
510     }
511
512     @Override
513     public void actionPerformed(AnActionEvent e) {
514       try {
515         ExternalDiffTool.showRequest(e.getProject(), myActiveRequest);
516       }
517       catch (Throwable ex) {
518         Messages.showErrorDialog(e.getProject(), ex.getMessage(), "Can't Show Diff In External Tool");
519       }
520     }
521   }
522
523   private class MyChangeDiffToolAction extends ComboBoxAction implements DumbAware {
524     public MyChangeDiffToolAction() {
525       // TODO: add icons for diff tools, show only icon in toolbar - to reduce jumping on change ?
526       setEnabledInModalContext(true);
527     }
528
529     @Override
530     public void update(AnActionEvent e) {
531       Presentation presentation = e.getPresentation();
532
533       DiffTool activeTool = myState.getActiveTool();
534       presentation.setText(activeTool.getName());
535
536       if (activeTool == ErrorDiffTool.INSTANCE) {
537         presentation.setEnabledAndVisible(false);
538       }
539
540       for (DiffTool tool : getAvailableFittedTools()) {
541         if (tool != activeTool) {
542           presentation.setEnabledAndVisible(true);
543           return;
544         }
545       }
546
547       presentation.setEnabledAndVisible(false);
548     }
549
550     @NotNull
551     @Override
552     protected DefaultActionGroup createPopupActionGroup(JComponent button) {
553       DefaultActionGroup group = new DefaultActionGroup();
554       for (DiffTool tool : getAvailableFittedTools()) {
555         group.add(new DiffToolToggleAction(tool));
556       }
557
558       return group;
559     }
560   }
561
562   private class DiffToolToggleAction extends AnAction implements DumbAware {
563     @NotNull private final DiffTool myDiffTool;
564
565     private DiffToolToggleAction(@NotNull DiffTool tool) {
566       super(tool.getName());
567       setEnabledInModalContext(true);
568       myDiffTool = tool;
569     }
570
571     @Override
572     public void actionPerformed(@NotNull AnActionEvent e) {
573       if (myState.getActiveTool() == myDiffTool) return;
574
575       UsageTrigger.trigger("diff.DiffSettings.Tool." + ConvertUsagesUtil.ensureProperKey(myDiffTool.getName()));
576       moveToolOnTop(myDiffTool);
577
578       updateRequest(true);
579     }
580   }
581
582   private class ShowActionGroupPopupAction extends DumbAwareAction {
583     public ShowActionGroupPopupAction() {
584       ActionUtil.copyFrom(this, "Diff.ShowSettingsPopup");
585     }
586
587     @Override
588     public void update(AnActionEvent e) {
589       e.getPresentation().setEnabled(myPopupActionGroup != null && myPopupActionGroup.getChildrenCount() > 0);
590     }
591
592     @Override
593     public void actionPerformed(AnActionEvent e) {
594       assert myPopupActionGroup != null;
595       ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup("Diff Actions", myPopupActionGroup, e.getDataContext(),
596                                                                             JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false);
597       popup.showInCenterOf(myPanel);
598     }
599   }
600
601   //
602   // Navigation
603   //
604
605   private enum IterationState {NEXT, PREV, NONE}
606
607   @NotNull private IterationState myIterationState = IterationState.NONE;
608
609   @CalledInAwt
610   protected boolean hasNextChange() {
611     return false;
612   }
613
614   @CalledInAwt
615   protected boolean hasPrevChange() {
616     return false;
617   }
618
619   @CalledInAwt
620   protected void goToNextChange(boolean fromDifferences) {
621   }
622
623   @CalledInAwt
624   protected void goToPrevChange(boolean fromDifferences) {
625   }
626
627   @CalledInAwt
628   protected boolean isNavigationEnabled() {
629     return false;
630   }
631
632   protected class MyNextDifferenceAction extends NextDifferenceAction {
633     @Override
634     public void update(@NotNull AnActionEvent e) {
635       if (!ActionPlaces.DIFF_TOOLBAR.equals(e.getPlace())) {
636         e.getPresentation().setEnabledAndVisible(true);
637         return;
638       }
639
640       PrevNextDifferenceIterable iterable = DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.getData(e.getDataContext());
641       if (iterable != null && iterable.canGoNext()) {
642         e.getPresentation().setEnabled(true);
643         return;
644       }
645
646       if (getSettings().isGoToNextFileOnNextDifference() && isNavigationEnabled() && hasNextChange()) {
647         e.getPresentation().setEnabled(true);
648         return;
649       }
650
651       e.getPresentation().setEnabled(false);
652     }
653
654     @Override
655     public void actionPerformed(@NotNull AnActionEvent e) {
656       PrevNextDifferenceIterable iterable = DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.getData(e.getDataContext());
657       if (iterable != null && iterable.canGoNext()) {
658         iterable.goNext();
659         myIterationState = IterationState.NONE;
660         return;
661       }
662
663       if (!isNavigationEnabled() || !hasNextChange() || !getSettings().isGoToNextFileOnNextDifference()) return;
664
665       if (myIterationState != IterationState.NEXT) {
666         notifyMessage(e, true);
667         myIterationState = IterationState.NEXT;
668         return;
669       }
670
671       goToNextChange(true);
672     }
673   }
674
675   protected class MyPrevDifferenceAction extends PrevDifferenceAction {
676     @Override
677     public void update(@NotNull AnActionEvent e) {
678       if (!ActionPlaces.DIFF_TOOLBAR.equals(e.getPlace())) {
679         e.getPresentation().setEnabledAndVisible(true);
680         return;
681       }
682
683       PrevNextDifferenceIterable iterable = DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.getData(e.getDataContext());
684       if (iterable != null && iterable.canGoPrev()) {
685         e.getPresentation().setEnabled(true);
686         return;
687       }
688
689       if (getSettings().isGoToNextFileOnNextDifference() && isNavigationEnabled() && hasPrevChange()) {
690         e.getPresentation().setEnabled(true);
691         return;
692       }
693
694       e.getPresentation().setEnabled(false);
695     }
696
697     @Override
698     public void actionPerformed(@NotNull AnActionEvent e) {
699       PrevNextDifferenceIterable iterable = DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.getData(e.getDataContext());
700       if (iterable != null && iterable.canGoPrev()) {
701         iterable.goPrev();
702         myIterationState = IterationState.NONE;
703         return;
704       }
705
706       if (!isNavigationEnabled() || !hasPrevChange() || !getSettings().isGoToNextFileOnNextDifference()) return;
707
708       if (myIterationState != IterationState.PREV) {
709         notifyMessage(e, false);
710         myIterationState = IterationState.PREV;
711         return;
712       }
713
714       goToPrevChange(true);
715     }
716   }
717
718   private void notifyMessage(@NotNull AnActionEvent e, boolean next) {
719     Editor editor = e.getData(DiffDataKeys.CURRENT_EDITOR);
720
721     // TODO: provide "change" word in chain UserData - for tests/etc
722     String message = DiffUtil.createNotificationText(next ? "Press again to go to the next file" : "Press again to go to the previous file",
723                                                      "You can disable this feature in " + DiffUtil.getSettingsConfigurablePath());
724
725     final LightweightHint hint = new LightweightHint(HintUtil.createInformationLabel(message));
726     Point point = new Point(myContentPanel.getWidth() / 2, next ? myContentPanel.getHeight() - JBUI.scale(40) : JBUI.scale(40));
727
728     if (editor == null) {
729       final Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
730       final HintHint hintHint = createNotifyHint(myContentPanel, point, next);
731       hint.show(myContentPanel, point.x, point.y, owner instanceof JComponent ? (JComponent)owner : null, hintHint);
732     }
733     else {
734       int x = SwingUtilities.convertPoint(myContentPanel, point, editor.getComponent()).x;
735
736       JComponent header = editor.getHeaderComponent();
737       int shift = editor.getScrollingModel().getVerticalScrollOffset() - (header != null ? header.getHeight() : 0);
738
739       LogicalPosition position;
740       LineRange changeRange = e.getData(DiffDataKeys.CURRENT_CHANGE_RANGE);
741       if (changeRange == null) {
742         position = new LogicalPosition(editor.getCaretModel().getLogicalPosition().line + (next ? 1 : 0), 0);
743       }
744       else {
745         position = new LogicalPosition(next ? changeRange.end : changeRange.start, 0);
746       }
747       int y = editor.logicalPositionToXY(position).y - shift;
748
749       Point editorPoint = new Point(x, y);
750       final HintHint hintHint = createNotifyHint(editor.getComponent(), editorPoint, !next);
751       HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, editorPoint, HintManager.HIDE_BY_ANY_KEY |
752                                                                                   HintManager.HIDE_BY_TEXT_CHANGE |
753                                                                                   HintManager.HIDE_BY_SCROLLING, 0, false, hintHint);
754     }
755   }
756
757   @NotNull
758   private static HintHint createNotifyHint(@NotNull JComponent component, @NotNull Point point, boolean above) {
759     return new HintHint(component, point)
760       .setPreferredPosition(above ? Balloon.Position.above : Balloon.Position.below)
761       .setAwtTooltip(true)
762       .setFont(UIUtil.getLabelFont().deriveFont(Font.BOLD))
763       .setTextBg(HintUtil.INFORMATION_COLOR)
764       .setShowImmediately(true);
765   }
766
767   // Iterate requests
768
769   protected class MyNextChangeAction extends NextChangeAction {
770     @Override
771     public void update(@NotNull AnActionEvent e) {
772       if (!ActionPlaces.DIFF_TOOLBAR.equals(e.getPlace())) {
773         e.getPresentation().setEnabledAndVisible(true);
774         return;
775       }
776
777       if (!isNavigationEnabled()) {
778         e.getPresentation().setEnabledAndVisible(false);
779         return;
780       }
781
782       e.getPresentation().setVisible(true);
783       e.getPresentation().setEnabled(hasNextChange());
784     }
785
786     @Override
787     public void actionPerformed(@NotNull AnActionEvent e) {
788       if (!isNavigationEnabled() || !hasNextChange()) return;
789
790       goToNextChange(false);
791     }
792   }
793
794   protected class MyPrevChangeAction extends PrevChangeAction {
795     @Override
796     public void update(@NotNull AnActionEvent e) {
797       if (!ActionPlaces.DIFF_TOOLBAR.equals(e.getPlace())) {
798         e.getPresentation().setEnabledAndVisible(true);
799         return;
800       }
801
802       if (!isNavigationEnabled()) {
803         e.getPresentation().setEnabledAndVisible(false);
804         return;
805       }
806
807       e.getPresentation().setVisible(true);
808       e.getPresentation().setEnabled(hasPrevChange());
809     }
810
811     @Override
812     public void actionPerformed(@NotNull AnActionEvent e) {
813       if (!isNavigationEnabled() || !hasPrevChange()) return;
814
815       goToPrevChange(false);
816     }
817   }
818
819   //
820   // Helpers
821   //
822
823   private class MyPanel extends JPanel implements DataProvider {
824     public MyPanel() {
825       super(new BorderLayout());
826     }
827
828     @Override
829     public Dimension getPreferredSize() {
830       Dimension windowSize = DiffUtil.getDefaultDiffPanelSize();
831       Dimension size = super.getPreferredSize();
832       return new Dimension(Math.max(windowSize.width, size.width), Math.max(windowSize.height, size.height));
833     }
834
835     @Nullable
836     @Override
837     public Object getData(@NonNls String dataId) {
838       Object data;
839
840       DataProvider contentProvider = DataManagerImpl.getDataProviderEx(myContentPanel.getTargetComponent());
841       if (contentProvider != null) {
842         data = contentProvider.getData(dataId);
843         if (data != null) return data;
844       }
845
846       if (OpenInEditorAction.KEY.is(dataId)) {
847         return myOpenInEditorAction;
848       }
849       else if (DiffDataKeys.DIFF_REQUEST.is(dataId)) {
850         return myActiveRequest;
851       }
852       else if (CommonDataKeys.PROJECT.is(dataId)) {
853         return myProject;
854       }
855       else if (PlatformDataKeys.HELP_ID.is(dataId)) {
856         if (myActiveRequest.getUserData(DiffUserDataKeys.HELP_ID) != null) {
857           return myActiveRequest.getUserData(DiffUserDataKeys.HELP_ID);
858         }
859         else {
860           return "reference.dialogs.diff.file";
861         }
862       }
863       else if (DiffDataKeys.DIFF_CONTEXT.is(dataId)) {
864         return myContext;
865       }
866
867       data = myState.getData(dataId);
868       if (data != null) return data;
869
870       DataProvider requestProvider = myActiveRequest.getUserData(DiffUserDataKeys.DATA_PROVIDER);
871       if (requestProvider != null) {
872         data = requestProvider.getData(dataId);
873         if (data != null) return data;
874       }
875
876       DataProvider contextProvider = myContext.getUserData(DiffUserDataKeys.DATA_PROVIDER);
877       if (contextProvider != null) {
878         data = contextProvider.getData(dataId);
879         if (data != null) return data;
880       }
881       return null;
882     }
883   }
884
885   private static class MyProgressBar extends JBProgressBar {
886     private int myProgressCount = 0;
887
888     public MyProgressBar() {
889       setIndeterminate(true);
890       setVisible(false);
891     }
892
893     public void startProgress() {
894       myProgressCount++;
895       setVisible(true);
896     }
897
898     public void stopProgress() {
899       myProgressCount--;
900       LOG.assertTrue(myProgressCount >= 0);
901       if (myProgressCount == 0) setVisible(false);
902     }
903   }
904
905   private class MyFocusTraversalPolicy extends IdeFocusTraversalPolicy {
906     @Override
907     public final Component getDefaultComponentImpl(final Container focusCycleRoot) {
908       JComponent component = DiffRequestProcessor.this.getPreferredFocusedComponent();
909       if (component == null) return null;
910       return IdeFocusTraversalPolicy.getPreferredFocusedComponent(component, this);
911     }
912   }
913
914   private class MyDiffContext extends DiffContextEx {
915     @NotNull private final UserDataHolder myContext;
916
917     public MyDiffContext(@NotNull UserDataHolder context) {
918       myContext = context;
919     }
920
921     @Override
922     public void reopenDiffRequest() {
923       updateRequest(true);
924     }
925
926     @Override
927     public void reloadDiffRequest() {
928       reloadRequest();
929     }
930
931     @Override
932     public void showProgressBar(boolean enabled) {
933       if (enabled) {
934         myProgressBar.startProgress();
935       }
936       else {
937         myProgressBar.stopProgress();
938       }
939     }
940
941     @Nullable
942     @Override
943     public Project getProject() {
944       return DiffRequestProcessor.this.getProject();
945     }
946
947     @Override
948     public boolean isFocused() {
949       return DiffRequestProcessor.this.isFocused();
950     }
951
952     @Override
953     public boolean isWindowFocused() {
954       return DiffRequestProcessor.this.isWindowFocused();
955     }
956
957     @Override
958     public void requestFocus() {
959       DiffRequestProcessor.this.requestFocusInternal();
960     }
961
962     @Nullable
963     @Override
964     public <T> T getUserData(@NotNull Key<T> key) {
965       return myContext.getUserData(key);
966     }
967
968     @Override
969     public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
970       myContext.putUserData(key, value);
971     }
972   }
973
974   private static class ApplyData {
975     @NotNull private final DiffRequest request;
976     private final boolean force;
977     @Nullable private final ScrollToPolicy scrollToChangePolicy;
978
979     public ApplyData(@NotNull DiffRequest request, boolean force, @Nullable ScrollToPolicy scrollToChangePolicy) {
980       this.request = request;
981       this.force = force;
982       this.scrollToChangePolicy = scrollToChangePolicy;
983     }
984   }
985
986   //
987   // States
988   //
989
990   private interface ViewerState {
991     @CalledInAwt
992     void init();
993
994     @CalledInAwt
995     void destroy();
996
997     @Nullable
998     JComponent getPreferredFocusedComponent();
999
1000     @Nullable
1001     Object getData(@NonNls String dataId);
1002
1003     @NotNull
1004     DiffTool getActiveTool();
1005   }
1006
1007   private static class EmptyState implements ViewerState {
1008     private static final EmptyState INSTANCE = new EmptyState();
1009
1010     @Override
1011     public void init() {
1012     }
1013
1014     @Override
1015     public void destroy() {
1016     }
1017
1018     @Nullable
1019     @Override
1020     public JComponent getPreferredFocusedComponent() {
1021       return null;
1022     }
1023
1024     @Nullable
1025     @Override
1026     public Object getData(@NonNls String dataId) {
1027       return null;
1028     }
1029
1030     @NotNull
1031     @Override
1032     public DiffTool getActiveTool() {
1033       return ErrorDiffTool.INSTANCE;
1034     }
1035   }
1036
1037   private class ErrorState implements ViewerState {
1038     @Nullable private final DiffTool myDiffTool;
1039     @NotNull private final MessageDiffRequest myRequest;
1040
1041     @NotNull private final DiffViewer myViewer;
1042
1043     public ErrorState(@NotNull MessageDiffRequest request) {
1044       this(request, null);
1045     }
1046
1047     public ErrorState(@NotNull MessageDiffRequest request, @Nullable DiffTool diffTool) {
1048       myDiffTool = diffTool;
1049       myRequest = request;
1050
1051       myViewer = ErrorDiffTool.INSTANCE.createComponent(myContext, myRequest);
1052     }
1053
1054     @Override
1055     @CalledInAwt
1056     public void init() {
1057       myContentPanel.setContent(myViewer.getComponent());
1058
1059       FrameDiffTool.ToolbarComponents init = myViewer.init();
1060       buildToolbar(init.toolbarActions);
1061     }
1062
1063     @Override
1064     @CalledInAwt
1065     public void destroy() {
1066       Disposer.dispose(myViewer);
1067     }
1068
1069     @Nullable
1070     @Override
1071     public JComponent getPreferredFocusedComponent() {
1072       return null;
1073     }
1074
1075     @Nullable
1076     @Override
1077     public Object getData(@NonNls String dataId) {
1078       return null;
1079     }
1080
1081     @NotNull
1082     @Override
1083     public DiffTool getActiveTool() {
1084       return myDiffTool != null ? myDiffTool : ErrorDiffTool.INSTANCE;
1085     }
1086   }
1087
1088   private class DefaultState implements ViewerState {
1089     @NotNull private final DiffViewer myViewer;
1090     @NotNull private final FrameDiffTool myTool;
1091
1092     public DefaultState(@NotNull DiffViewer viewer, @NotNull FrameDiffTool tool) {
1093       myViewer = viewer;
1094       myTool = tool;
1095     }
1096
1097     @Override
1098     @CalledInAwt
1099     public void init() {
1100       myContentPanel.setContent(myViewer.getComponent());
1101       setTitle(myActiveRequest.getTitle());
1102
1103       FrameDiffTool.ToolbarComponents toolbarComponents = myViewer.init();
1104
1105       buildToolbar(toolbarComponents.toolbarActions);
1106       buildActionPopup(toolbarComponents.popupActions);
1107
1108       myToolbarStatusPanel.setContent(toolbarComponents.statusPanel);
1109     }
1110
1111     @Override
1112     @CalledInAwt
1113     public void destroy() {
1114       Disposer.dispose(myViewer);
1115     }
1116
1117     @Nullable
1118     @Override
1119     public JComponent getPreferredFocusedComponent() {
1120       return myViewer.getPreferredFocusedComponent();
1121     }
1122
1123     @NotNull
1124     @Override
1125     public DiffTool getActiveTool() {
1126       return myTool;
1127     }
1128
1129     @Nullable
1130     @Override
1131     public Object getData(@NonNls String dataId) {
1132       if (DiffDataKeys.DIFF_VIEWER.is(dataId)) {
1133         return myViewer;
1134       }
1135       return null;
1136     }
1137   }
1138
1139   private class WrapperState implements ViewerState {
1140     @NotNull private final DiffViewer myViewer;
1141     @NotNull private final FrameDiffTool myTool;
1142
1143     @NotNull private DiffViewer myWrapperViewer;
1144
1145     public WrapperState(@NotNull DiffViewer viewer, @NotNull FrameDiffTool tool, @NotNull DiffViewerWrapper wrapper) {
1146       myViewer = viewer;
1147       myTool = tool;
1148       myWrapperViewer = wrapper.createComponent(myContext, myActiveRequest, myViewer);
1149     }
1150
1151     @Override
1152     @CalledInAwt
1153     public void init() {
1154       myContentPanel.setContent(myWrapperViewer.getComponent());
1155       setTitle(myActiveRequest.getTitle());
1156
1157
1158       FrameDiffTool.ToolbarComponents toolbarComponents1 = myViewer.init();
1159       FrameDiffTool.ToolbarComponents toolbarComponents2 = myWrapperViewer.init();
1160
1161       List<AnAction> toolbarActions = new ArrayList<>();
1162       if (toolbarComponents1.toolbarActions != null) toolbarActions.addAll(toolbarComponents1.toolbarActions);
1163       if (toolbarComponents2.toolbarActions != null) {
1164         if (!toolbarActions.isEmpty() && !toolbarComponents2.toolbarActions.isEmpty()) toolbarActions.add(Separator.getInstance());
1165         toolbarActions.addAll(toolbarComponents2.toolbarActions);
1166       }
1167       buildToolbar(toolbarActions);
1168
1169       List<AnAction> popupActions = new ArrayList<>();
1170       if (toolbarComponents1.popupActions != null) popupActions.addAll(toolbarComponents1.popupActions);
1171       if (toolbarComponents2.popupActions != null) {
1172         if (!popupActions.isEmpty() && !toolbarComponents2.popupActions.isEmpty()) popupActions.add(Separator.getInstance());
1173         popupActions.addAll(toolbarComponents2.popupActions);
1174       }
1175       buildActionPopup(popupActions);
1176
1177
1178       myToolbarStatusPanel.setContent(toolbarComponents1.statusPanel); // TODO: combine both panels ?
1179     }
1180
1181     @Override
1182     @CalledInAwt
1183     public void destroy() {
1184       Disposer.dispose(myViewer);
1185       Disposer.dispose(myWrapperViewer);
1186     }
1187
1188     @Nullable
1189     @Override
1190     public JComponent getPreferredFocusedComponent() {
1191       return myWrapperViewer.getPreferredFocusedComponent();
1192     }
1193
1194     @NotNull
1195     @Override
1196     public DiffTool getActiveTool() {
1197       return myTool;
1198     }
1199
1200     @Nullable
1201     @Override
1202     public Object getData(@NonNls String dataId) {
1203       if (DiffDataKeys.WRAPPING_DIFF_VIEWER.is(dataId)) {
1204         return myWrapperViewer;
1205       }
1206       if (DiffDataKeys.DIFF_VIEWER.is(dataId)) {
1207         return myViewer;
1208       }
1209       return null;
1210     }
1211   }
1212 }