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