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