8d716da36cf8f6521b9b585aaa41c18acb76414e
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / patch / ApplyPatchDifferentiatedDialog.java
1 /*
2  * Copyright 2000-2009 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.openapi.vcs.changes.patch;
17
18 import com.intellij.ide.util.PropertiesComponent;
19 import com.intellij.openapi.actionSystem.*;
20 import com.intellij.openapi.diff.impl.patch.*;
21 import com.intellij.openapi.fileChooser.FileChooser;
22 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
23 import com.intellij.openapi.fileChooser.FileChooserDialog;
24 import com.intellij.openapi.fileChooser.FileChooserFactory;
25 import com.intellij.openapi.fileTypes.FileTypes;
26 import com.intellij.openapi.fileTypes.StdFileTypes;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.ui.DialogWrapper;
29 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
30 import com.intellij.openapi.ui.popup.JBPopupFactory;
31 import com.intellij.openapi.ui.popup.PopupStep;
32 import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
33 import com.intellij.openapi.util.IconLoader;
34 import com.intellij.openapi.util.Pair;
35 import com.intellij.openapi.util.text.StringUtil;
36 import com.intellij.openapi.vcs.ObjectsConvertor;
37 import com.intellij.openapi.vcs.VcsBundle;
38 import com.intellij.openapi.vcs.ZipperUpdater;
39 import com.intellij.openapi.vcs.changes.Change;
40 import com.intellij.openapi.vcs.changes.ChangeListManager;
41 import com.intellij.openapi.vcs.changes.LocalChangeList;
42 import com.intellij.openapi.vcs.changes.actions.DiffRequestPresentable;
43 import com.intellij.openapi.vcs.changes.actions.ShowDiffAction;
44 import com.intellij.openapi.vcs.changes.actions.ShowDiffUIContext;
45 import com.intellij.openapi.vcs.changes.ui.*;
46 import com.intellij.openapi.vfs.LocalFileSystem;
47 import com.intellij.openapi.vfs.VirtualFile;
48 import com.intellij.ui.DocumentAdapter;
49 import com.intellij.ui.SimpleColoredComponent;
50 import com.intellij.ui.SimpleTextAttributes;
51 import com.intellij.util.Consumer;
52 import com.intellij.util.containers.Convertor;
53 import org.jetbrains.annotations.NonNls;
54 import org.jetbrains.annotations.NotNull;
55 import org.jetbrains.annotations.Nullable;
56
57 import javax.swing.*;
58 import javax.swing.event.DocumentEvent;
59 import javax.swing.tree.DefaultTreeModel;
60 import java.awt.*;
61 import java.io.File;
62 import java.io.IOException;
63 import java.util.*;
64 import java.util.List;
65 import java.util.concurrent.atomic.AtomicReference;
66
67 public class ApplyPatchDifferentiatedDialog extends DialogWrapper {
68   private final ZipperUpdater myLoadQueue;
69   private final TextFieldWithBrowseButton myPatchFile;
70
71   private final List<FilePatchInProgress> myPatches;
72   private final MyChangeTreeList myChangesTreeList;
73
74   private JComponent myCenterPanel;
75   private JComponent mySouthPanel;
76   private final Project myProject;
77
78   private final AtomicReference<FilePresentation> myRecentPathFileChange;
79   private final ApplyPatchDifferentiatedDialog.MyUpdater myUpdater;
80   private final Runnable myReset;
81   private final ChangeListChooserPanel myChangeListChooser;
82   private final ChangesLegendCalculator myInfoCalculator;
83   private final CommitLegendPanel myCommitLegendPanel;
84   private final Consumer<ApplyPatchDifferentiatedDialog> myCallback;
85
86   private boolean myContainBasedChanges;
87
88   public ApplyPatchDifferentiatedDialog(final Project project, final Consumer<ApplyPatchDifferentiatedDialog> callback,
89                                         @Nullable final VirtualFile patchFile) {
90     super(project, true);
91     myCallback = callback;
92     setModal(false);
93     setTitle(VcsBundle.message("patch.apply.dialog.title"));
94
95     final FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
96       @Override
97       public boolean isFileSelectable(VirtualFile file) {
98         return file.getFileType() == StdFileTypes.PATCH || file.getFileType() == FileTypes.PLAIN_TEXT;
99       }
100     };
101     descriptor.setTitle(VcsBundle.message("patch.apply.select.title"));
102     myUpdater = new MyUpdater();
103     myPatchFile = new TextFieldWithBrowseButton();
104     myPatchFile.addBrowseFolderListener(VcsBundle.message("patch.apply.select.title"), "", project, descriptor);
105     myPatchFile.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
106       protected void textChanged(DocumentEvent e) {
107         setPathFileChangeDefault();
108         myLoadQueue.queue(myUpdater);
109       }
110     });
111
112     myProject = project;
113     myLoadQueue = new ZipperUpdater(500);
114     myPatches = new LinkedList<FilePatchInProgress>();
115     myRecentPathFileChange = new AtomicReference<FilePresentation>();
116     myChangesTreeList = new MyChangeTreeList(project, Collections.<FilePatchInProgress.PatchChange>emptyList(),
117       new Runnable() {
118         public void run() {
119           final NamedTrinity includedTrinity = new NamedTrinity();
120           final Collection<FilePatchInProgress.PatchChange> includedChanges = myChangesTreeList.getIncludedChanges();
121           final Set<Pair<String, String>> set = new HashSet<Pair<String, String>>();
122           for (FilePatchInProgress.PatchChange change : includedChanges) {
123             final TextFilePatch patch = change.getPatchInProgress().getPatch();
124             final Pair<String, String> pair = new Pair<String, String>(patch.getBeforeName(), patch.getAfterName());
125             if (set.contains(pair)) continue;
126             set.add(pair);
127             acceptChange(includedTrinity, change);
128           }
129           myInfoCalculator.setIncluded(includedTrinity);
130           myCommitLegendPanel.update();
131         }
132       }, new MyChangeNodeDecorator());
133     myReset = new Runnable() {
134       public void run() {
135         reset();
136       }
137     };
138
139     myChangeListChooser = new ChangeListChooserPanel(null, new Consumer<String>() {
140       public void consume(final String errorMessage) {
141         setOKActionEnabled(errorMessage == null);
142         setErrorText(errorMessage);
143       }
144     });
145     ChangeListManager changeListManager = ChangeListManager.getInstance(project);
146     myChangeListChooser.setChangeLists(changeListManager.getChangeListsCopy());
147     myChangeListChooser.setDefaultSelection(changeListManager.getDefaultChangeList());
148     myChangeListChooser.init(project);
149
150     myInfoCalculator = new ChangesLegendCalculator();
151     myCommitLegendPanel = new CommitLegendPanel(myInfoCalculator);
152
153     init();
154
155     if (patchFile != null && patchFile.isValid()) {
156       init(patchFile);
157     } else {
158       final FileChooserDialog fileChooserDialog = FileChooserFactory.getInstance().createFileChooser(descriptor, project);
159       final VirtualFile[] files = fileChooserDialog.choose(null, project);
160       if (files != null && files.length > 0) {
161         init(files[0]);
162       }
163     }
164   }
165
166   @Override
167   @NonNls
168   protected String getDimensionServiceKey() {
169     return "vcs.ApplyPatchDifferentiatedDialog";
170   }
171
172   @Override
173   protected String getHelpId() {
174     return "reference.dialogs.vcs.patch.apply";
175   }
176
177   private void setPathFileChangeDefault() {
178     myRecentPathFileChange.set(new FilePresentation(myPatchFile.getText()));
179   }
180
181   public void init(final VirtualFile patchFile) {
182     myPatchFile.setText(patchFile.getPath());
183     myRecentPathFileChange.set(new FilePresentation(patchFile));
184     myLoadQueue.queue(myUpdater);
185   }
186
187   private class MyUpdater implements Runnable {
188     public void run() {
189       final FilePresentation filePresentation = myRecentPathFileChange.get();
190       if ((filePresentation == null) || (filePresentation.getVf() == null)) {
191         SwingUtilities.invokeLater(myReset);
192         return;
193       }
194       final VirtualFile file = filePresentation.getVf();
195
196       final List<TextFilePatch> patches = loadPatches(file);
197       final AutoMatchIterator autoMatchIterator = new AutoMatchIterator(myProject);
198       final List<FilePatchInProgress> matchedPathes = autoMatchIterator.execute(patches);
199
200       SwingUtilities.invokeLater(new Runnable() {
201         public void run() {
202           myChangeListChooser.setDefaultName(file.getNameWithoutExtension().replace('_', ' ').trim());
203           myPatches.clear();
204           myPatches.addAll(matchedPathes);
205
206           updateTree(true);
207         }
208       });
209     }
210   }
211
212   private List<TextFilePatch> loadPatches(final VirtualFile patchFile) {
213     if (! patchFile.isValid()) {
214       //todo
215       //queueUpdateStatus("Cannot find patch file");
216       return Collections.emptyList();
217     }
218     PatchReader reader;
219     try {
220       reader = PatchVirtualFileReader.create(patchFile);
221     }
222     catch (IOException e) {
223       //todo
224       //queueUpdateStatus(VcsBundle.message("patch.apply.open.error", e.getMessage()));
225       return Collections.emptyList();
226     }
227     final List<TextFilePatch> result = new LinkedList<TextFilePatch>();
228     while(true) {
229       FilePatch patch;
230       try {
231         patch = reader.readNextPatch();
232       }
233       catch (PatchSyntaxException e) {
234         // todo
235         if (e.getLine() >= 0) {
236           //queueUpdateStatus(VcsBundle.message("patch.apply.load.error.line", e.getMessage(), e.getLine()));
237         }
238         else {
239           //queueUpdateStatus(VcsBundle.message("patch.apply.load.error", e.getMessage()));
240         }
241         return Collections.emptyList();
242       }
243       if (patch == null) {
244         break;
245       }
246
247       result.add((TextFilePatch) patch);
248     }
249     if (myPatches.isEmpty()) {
250       // todo
251       //queueUpdateStatus(VcsBundle.message("patch.apply.no.patches.found"));
252     }
253     return result;
254   }
255
256   private static class FilePresentation {
257     private final VirtualFile myVf;
258     private final String myPath;
259
260     private FilePresentation(VirtualFile vf) {
261       myVf = vf;
262       myPath = null;
263     }
264
265     private FilePresentation(String path) {
266       myPath = path;
267       myVf = null;
268     }
269
270     @Nullable
271     public VirtualFile getVf() {
272       if (myVf != null) {
273         return myVf;
274       }
275       final VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(myPath);
276       return (file != null) && (! file.isDirectory()) ? file : null;
277     }
278   }
279
280   public void reset() {
281     myPatches.clear();
282     myChangesTreeList.setChangesToDisplay(Collections.<FilePatchInProgress.PatchChange>emptyList());
283     myChangesTreeList.repaint();
284     myContainBasedChanges = false;
285   }
286
287   @Override
288   protected JComponent createCenterPanel() {
289     if (myCenterPanel == null) {
290       myCenterPanel = new JPanel(new GridBagLayout());
291       final GridBagConstraints gb =
292         new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(1, 1, 1, 1), 0, 0);
293
294       final JLabel label = new JLabel(VcsBundle.message("create.patch.file.name.field"));
295       label.setLabelFor(myPatchFile);
296       myCenterPanel.add(label, gb);
297
298       ++ gb.gridx;
299       gb.fill = GridBagConstraints.HORIZONTAL;
300       gb.weightx = 1;
301       myCenterPanel.add(myPatchFile, gb);
302
303       gb.gridx = 0;
304       ++ gb.gridy;
305       gb.weightx = 1;
306       gb.weighty = 0;
307       gb.fill = GridBagConstraints.HORIZONTAL;
308       gb.gridwidth = 2;
309
310       final DefaultActionGroup group = new DefaultActionGroup();
311       final AnAction[] treeActions = myChangesTreeList.getTreeActions();
312       group.addAll(treeActions);
313       group.add(new MapDirectory());
314
315       final MyShowDiff diffAction = new MyShowDiff();
316       diffAction.registerCustomShortcutSet(CommonShortcuts.getDiff(), getRootPane());
317       group.add(diffAction);
318
319       group.add(new StripUp());
320       group.add(new StripDown());
321       group.add(new ResetStrip());
322       group.add(new ZeroStrip());
323
324       final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("APPLY_PATCH", group, true);
325       myCenterPanel.add(toolbar.getComponent(), gb);
326
327       gb.gridx = 0;
328       ++ gb.gridy;
329       gb.weighty = 1;
330       gb.gridwidth = 2;
331       gb.fill = GridBagConstraints.BOTH;
332       myCenterPanel.add(myChangesTreeList, gb);
333
334       final JPanel wrapper = new JPanel(new GridBagLayout());
335       final GridBagConstraints gb1 =
336         new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.NORTHWEST, GridBagConstraints.HORIZONTAL, new Insets(1, 1, 1, 1), 0, 0);
337       wrapper.add(myChangeListChooser, gb1);
338       ++ gb1.gridx;
339       gb1.fill = GridBagConstraints.NONE;
340       gb1.weightx = 0;
341       gb1.insets.left = 10;
342       wrapper.add(myCommitLegendPanel.getComponent(), gb1);
343
344       gb.gridx = 0;
345       ++ gb.gridy;
346       gb.weightx = 1;
347       gb.weighty = 0;
348       gb.fill = GridBagConstraints.HORIZONTAL;
349       myCenterPanel.add(wrapper, gb);
350     }
351     return myCenterPanel;
352   }
353
354   private static class MyChangeTreeList extends ChangesTreeList<FilePatchInProgress.PatchChange> {
355     private MyChangeTreeList(Project project,
356                              Collection<FilePatchInProgress.PatchChange> initiallyIncluded,
357                              @Nullable Runnable inclusionListener,
358                              @Nullable ChangeNodeDecorator decorator) {
359       super(project, initiallyIncluded, true, false, inclusionListener, decorator);
360     }
361
362     @Override
363     protected DefaultTreeModel buildTreeModel(List<FilePatchInProgress.PatchChange> changes, ChangeNodeDecorator changeNodeDecorator) {
364       TreeModelBuilder builder = new TreeModelBuilder(myProject, false);
365       return builder.buildModel(ObjectsConvertor.convert(changes,
366                                                          new Convertor<FilePatchInProgress.PatchChange, Change>() {
367                                                            public Change convert(FilePatchInProgress.PatchChange o) {
368                                                              return o;
369                                                            }
370                                                          }), changeNodeDecorator);
371     }
372
373     @Override
374     protected List<FilePatchInProgress.PatchChange> getSelectedObjects(ChangesBrowserNode<FilePatchInProgress.PatchChange> node) {
375       final List<Change> under = node.getAllChangesUnder();
376       return ObjectsConvertor.convert(under, new Convertor<Change, FilePatchInProgress.PatchChange>() {
377         public FilePatchInProgress.PatchChange convert(Change o) {
378           return (FilePatchInProgress.PatchChange) o;
379         }
380       });
381     }
382
383     @Override
384     protected FilePatchInProgress.PatchChange getLeadSelectedObject(ChangesBrowserNode node) {
385       final Object o = node.getUserObject();
386       if (o instanceof FilePatchInProgress.PatchChange) {
387         return (FilePatchInProgress.PatchChange) o;
388       }
389       return null;
390     }
391   }
392
393   private class MapDirectory extends AnAction {
394     private final NewBaseSelector myNewBaseSelector;
395
396     private MapDirectory() {
397       super("Map base directory", "Map base directory", IconLoader.getIcon("/vcs/mapBase.png"));
398       myNewBaseSelector = new NewBaseSelector();
399     }
400
401     @Override
402     public void actionPerformed(AnActionEvent e) {
403       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
404       if ((selectedChanges.size() >= 1) && (sameBase(selectedChanges))) {
405         final FilePatchInProgress.PatchChange patchChange = selectedChanges.get(0);
406         final FilePatchInProgress patch = patchChange.getPatchInProgress();
407         final List<VirtualFile> autoBases = patch.getAutoBasesCopy();
408         if (autoBases.isEmpty() || (autoBases.size() == 1 && autoBases.get(0).equals(patch.getBase()))) {
409           myNewBaseSelector.run();
410         } else {
411           autoBases.add(null);
412           final MapPopup step = new MapPopup(autoBases, myNewBaseSelector);
413           JBPopupFactory.getInstance().createListPopup(step).showCenteredInCurrentWindow(myProject);
414         }
415       }
416     }
417
418     @Override
419     public void update(AnActionEvent e) {
420       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
421       e.getPresentation().setEnabled((selectedChanges.size() >= 1) && (sameBase(selectedChanges)));
422     }
423   }
424
425   private boolean sameBase(final List<FilePatchInProgress.PatchChange> selectedChanges) {
426     VirtualFile base = null;
427     for (FilePatchInProgress.PatchChange change : selectedChanges) {
428       final VirtualFile changeBase = change.getPatchInProgress().getBase();
429       if (base == null) {
430         base = changeBase;
431       } else if (! base.equals(changeBase)) {
432         return false;
433       }
434     }
435     return true;
436   }
437
438   private void updateTree(boolean doInitCheck) {
439     final List<FilePatchInProgress> patchesToSelect = changes2patches(myChangesTreeList.getSelectedChanges());
440     final List<FilePatchInProgress.PatchChange> changes = getAllChanges();
441     final Collection<FilePatchInProgress.PatchChange> included = getIncluded(doInitCheck, changes);
442
443     myChangesTreeList.setChangesToDisplay(changes);
444     myChangesTreeList.setIncludedChanges(included);
445     myChangesTreeList.repaint();
446     if ((! doInitCheck) && patchesToSelect != null) {
447       final List<FilePatchInProgress.PatchChange> toSelect = new ArrayList<FilePatchInProgress.PatchChange>(patchesToSelect.size());
448       for (FilePatchInProgress.PatchChange change : changes) {
449         if (patchesToSelect.contains(change.getPatchInProgress())) {
450           toSelect.add(change);
451         }
452       }
453       myChangesTreeList.select(toSelect);
454     }
455
456     myContainBasedChanges = false;
457     for (FilePatchInProgress patch : myPatches) {
458       if (patch.baseExistsOrAdded()) {
459         myContainBasedChanges = true;
460         break;
461       }
462     }
463   }
464
465   private List<FilePatchInProgress.PatchChange> getAllChanges() {
466     return ObjectsConvertor.convert(myPatches,
467                                                                                    new Convertor<FilePatchInProgress, FilePatchInProgress.PatchChange>() {
468                                                                                      public FilePatchInProgress.PatchChange convert(FilePatchInProgress o) {
469                                                                                        return o.getChange();
470                                                                                      }
471                                                                                    });
472   }
473
474   private void acceptChange(final NamedTrinity trinity, final FilePatchInProgress.PatchChange change) {
475     final FilePatchInProgress patchInProgress = change.getPatchInProgress();
476     if (FilePatchStatus.ADDED.equals(patchInProgress.getStatus())) {
477       trinity.plusAdded();
478     } else if (FilePatchStatus.DELETED.equals(patchInProgress.getStatus())) {
479       trinity.plusDeleted();
480     } else {
481       trinity.plusModified();
482     }
483   }
484
485   private Collection<FilePatchInProgress.PatchChange> getIncluded(boolean doInitCheck, List<FilePatchInProgress.PatchChange> changes) {
486     final NamedTrinity totalTrinity = new NamedTrinity();
487     final NamedTrinity includedTrinity = new NamedTrinity();
488
489     final Collection<FilePatchInProgress.PatchChange> included = new LinkedList<FilePatchInProgress.PatchChange>();
490     if (doInitCheck) {
491       for (FilePatchInProgress.PatchChange change : changes) {
492         acceptChange(totalTrinity, change);
493         final FilePatchInProgress filePatchInProgress = change.getPatchInProgress();
494         if (filePatchInProgress.baseExistsOrAdded()) {
495           acceptChange(includedTrinity, change);
496           included.add(change);
497         }
498       }
499     } else {
500       // todo maybe written pretty
501       final Collection<FilePatchInProgress.PatchChange> includedNow = myChangesTreeList.getIncludedChanges();
502       final Set<FilePatchInProgress> toBeIncluded = new HashSet<FilePatchInProgress>();
503       for (FilePatchInProgress.PatchChange change : includedNow) {
504         final FilePatchInProgress patch = change.getPatchInProgress();
505         toBeIncluded.add(patch);
506       }
507       for (FilePatchInProgress.PatchChange change : changes) {
508         final FilePatchInProgress patch = change.getPatchInProgress();
509         acceptChange(totalTrinity, change);
510         if (toBeIncluded.contains(patch) && patch.baseExistsOrAdded()) {
511           acceptChange(includedTrinity, change);
512           included.add(change);
513         }
514       }
515     }
516     myInfoCalculator.setTotal(totalTrinity);
517     myInfoCalculator.setIncluded(includedTrinity);
518     myCommitLegendPanel.update();
519     return included;
520   }
521
522   private class NewBaseSelector implements Runnable {
523     public void run() {
524       final FileChooserDescriptor descriptor = new FileChooserDescriptor(false, true, false, false, false, false);
525       VirtualFile[] selectedFiles = FileChooser.chooseFiles(myProject, descriptor);
526       if (selectedFiles.length != 1 || selectedFiles[0] == null) {
527         return;
528       }
529       final VirtualFile selectedValue = selectedFiles[0];
530
531       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
532       if (selectedChanges.size() >= 1) {
533         for (FilePatchInProgress.PatchChange patchChange : selectedChanges) {
534           final FilePatchInProgress patch = patchChange.getPatchInProgress();
535           patch.setNewBase(selectedValue);
536         }
537         updateTree(false);
538       }
539     }
540   }
541
542   private List<FilePatchInProgress> changes2patches(final List<FilePatchInProgress.PatchChange> selectedChanges) {
543     return ObjectsConvertor.convert(selectedChanges, new Convertor<FilePatchInProgress.PatchChange, FilePatchInProgress>() {
544       public FilePatchInProgress convert(FilePatchInProgress.PatchChange o) {
545         return o.getPatchInProgress();
546       }
547     });
548   }
549
550   private class MapPopup extends BaseListPopupStep<VirtualFile> {
551     private final Runnable myNewBaseSelector;
552
553     private MapPopup(final @NotNull List<? extends VirtualFile> aValues, Runnable newBaseSelector) {
554       super("Select base directory for a path", aValues);
555       myNewBaseSelector = newBaseSelector;
556     }
557
558     @Override
559     public boolean isSpeedSearchEnabled() {
560       return true;
561     }
562
563     @Override
564     public PopupStep onChosen(final VirtualFile selectedValue, boolean finalChoice) {
565       if (selectedValue == null) {
566         myNewBaseSelector.run();
567         return null;
568       }
569       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
570       if (selectedChanges.size() >= 1) {
571         for (FilePatchInProgress.PatchChange patchChange : selectedChanges) {
572           final FilePatchInProgress patch = patchChange.getPatchInProgress();
573           patch.setNewBase(selectedValue);
574         }
575         updateTree(false);
576       }
577       return null;
578     }
579
580     @NotNull
581     @Override
582     public String getTextFor(VirtualFile value) {
583       return value == null ? "Select base for a path" : value.getPath();
584     }
585   }
586
587   private static class NamedTrinity {
588     private int myAdded;
589     private int myModified;
590     private int myDeleted;
591
592     public NamedTrinity() {
593       myAdded = 0;
594       myModified = 0;
595       myDeleted = 0;
596     }
597
598     public NamedTrinity(int added, int modified, int deleted) {
599       myAdded = added;
600       myModified = modified;
601       myDeleted = deleted;
602     }
603
604     public void plusAdded() {
605       ++ myAdded;
606     }
607
608     public void plusModified() {
609       ++ myModified;
610     }
611
612     public void plusDeleted() {
613       ++ myDeleted;
614     }
615
616     public int getAdded() {
617       return myAdded;
618     }
619
620     public int getModified() {
621       return myModified;
622     }
623
624     public int getDeleted() {
625       return myDeleted;
626     }
627   }
628
629   private static class ChangesLegendCalculator implements CommitLegendPanel.InfoCalculator {
630     private NamedTrinity myTotal;
631     private NamedTrinity myIncluded;
632
633     private ChangesLegendCalculator() {
634       myTotal = new NamedTrinity();
635       myIncluded = new NamedTrinity();
636     }
637
638     public void setTotal(final NamedTrinity trinity) {
639       myTotal = trinity;
640     }
641
642     public void setIncluded(final NamedTrinity trinity) {
643       myIncluded = trinity;
644     }
645
646     public int getNew() {
647       return myTotal.getAdded();
648     }
649
650     public int getModified() {
651       return myTotal.getModified();
652     }
653
654     public int getDeleted() {
655       return myTotal.getDeleted();
656     }
657
658     public int getIncludedNew() {
659       return myIncluded.getAdded();
660     }
661
662     public int getIncludedModified() {
663       return myIncluded.getModified();
664     }
665
666     public int getIncludedDeleted() {
667       return myIncluded.getDeleted();
668     }
669   }
670
671   private static class MyChangeNodeDecorator implements ChangeNodeDecorator {
672     public void decorate(Change change, SimpleColoredComponent component, boolean isShowFlatten) {
673       if (change instanceof FilePatchInProgress.PatchChange) {
674         final FilePatchInProgress.PatchChange patchChange = (FilePatchInProgress.PatchChange) change;
675         if (! isShowFlatten) {
676           // add change subpath
677           final TextFilePatch filePatch = patchChange.getPatchInProgress().getPatch();
678           final String patchPath = filePatch.getAfterName() == null ? filePatch.getBeforeName() : filePatch.getAfterName();
679           component.append("   ");
680           component.append("["+ patchPath + "]", SimpleTextAttributes.GRAY_ATTRIBUTES);
681         }
682         if (patchChange.getPatchInProgress().getCurrentStrip() > 0) {
683           component.append(" stripped " + patchChange.getPatchInProgress().getCurrentStrip(), SimpleTextAttributes.GRAY_ITALIC_ATTRIBUTES);
684         }
685         final String text;
686         if (FilePatchStatus.ADDED.equals(patchChange.getPatchInProgress().getStatus())) {
687           text = "(Added)";
688         } else if (FilePatchStatus.DELETED.equals(patchChange.getPatchInProgress().getStatus())) {
689           text = "(Deleted)";
690         } else {
691           text = "(Modified)";
692         }
693         component.append("   ");
694         component.append(text, SimpleTextAttributes.GRAY_ATTRIBUTES);
695       }
696     }
697
698     public List<Pair<String, Stress>> stressPartsOfFileName(final Change change, final String parentPath) {
699       if (change instanceof FilePatchInProgress.PatchChange) {
700         final FilePatchInProgress.PatchChange patchChange = (FilePatchInProgress.PatchChange) change;
701         final String basePath = patchChange.getPatchInProgress().getBase().getPath();
702         final String basePathCorrected = basePath.trim().replace('/', File.separatorChar);
703         if (parentPath.startsWith(basePathCorrected)) {
704           return Arrays.asList(new Pair<String, Stress>(basePathCorrected, Stress.BOLD),
705                                new Pair<String, Stress>(StringUtil.tail(parentPath, basePathCorrected.length()), Stress.PLAIN));
706         }
707       }
708       return null;
709     }
710
711     public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
712     }
713   }
714
715   public Collection<FilePatchInProgress> getIncluded() {
716     return ObjectsConvertor.convert(myChangesTreeList.getIncludedChanges(),
717                                     new Convertor<FilePatchInProgress.PatchChange, FilePatchInProgress>() {
718                                       public FilePatchInProgress convert(FilePatchInProgress.PatchChange o) {
719                                         return o.getPatchInProgress();
720                                       }
721                                     });
722   }
723
724   public LocalChangeList getSelectedChangeList() {
725     return myChangeListChooser.getSelectedList(myProject);
726   }
727
728   @Override
729   protected void doOKAction() {
730     super.doOKAction();
731     myCallback.consume(this);
732   }
733
734   private class ZeroStrip extends AnAction {
735     private ZeroStrip() {
736       super("Remove Directories", "Remove Directories", IconLoader.getIcon("/vcs/stripNull.png"));
737     }
738
739     @Override
740     public void actionPerformed(AnActionEvent e) {
741       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
742       for (FilePatchInProgress.PatchChange change : selectedChanges) {
743         change.getPatchInProgress().setZero();
744       }
745       updateTree(false);
746     }
747   }
748
749   private class StripDown extends AnAction {
750     private StripDown() {
751       super("Restore Directory", "Restore Directory", IconLoader.getIcon("/vcs/stripDown.png"));
752     }
753
754     @Override
755     public void update(AnActionEvent e) {
756       e.getPresentation().setEnabled(isEnabled());
757     }
758
759     @Override
760     public void actionPerformed(AnActionEvent e) {
761       if (! isEnabled()) return;
762       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
763       for (FilePatchInProgress.PatchChange change : selectedChanges) {
764         change.getPatchInProgress().down();
765       }
766       updateTree(false);
767     }
768
769     private boolean isEnabled() {
770       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
771       if (selectedChanges.isEmpty()) return false;
772       for (FilePatchInProgress.PatchChange change : selectedChanges) {
773         if (! change.getPatchInProgress().canDown()) return false;
774       }
775       return true;
776     }
777   }
778
779   private class StripUp extends AnAction {
780     private StripUp() {
781       super("Strip Directory", "Strip Directory", IconLoader.getIcon("/vcs/stripUp.png"));
782     }
783
784     @Override
785     public void update(AnActionEvent e) {
786       e.getPresentation().setEnabled(isEnabled());
787     }
788
789     @Override
790     public void actionPerformed(AnActionEvent e) {
791       if (! isEnabled()) return;
792       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
793       for (FilePatchInProgress.PatchChange change : selectedChanges) {
794         change.getPatchInProgress().up();
795       }
796       updateTree(false);
797     }
798
799     private boolean isEnabled() {
800       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
801       if (selectedChanges.isEmpty()) return false;
802       for (FilePatchInProgress.PatchChange change : selectedChanges) {
803         if (! change.getPatchInProgress().canUp()) return false;
804       }
805       return true;
806     }
807   }
808
809   private class ResetStrip extends AnAction {
810     private ResetStrip() {
811       super("Reset Directories", "Reset Directories", IconLoader.getIcon("/vcs/resetStrip.png"));
812     }
813
814     @Override
815     public void actionPerformed(AnActionEvent e) {
816       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
817       for (FilePatchInProgress.PatchChange change : selectedChanges) {
818         change.getPatchInProgress().reset();
819       }
820       updateTree(false);
821     }
822   }
823
824   private class MyShowDiff extends AnAction {
825     private final MyChangeComparator myMyChangeComparator;
826     private MyShowDiff() {
827       super("Show Diff", "Show Diff", IconLoader.getIcon("/actions/diff.png"));
828       myMyChangeComparator = new MyChangeComparator();
829     }
830
831     public void update(AnActionEvent e) {
832       e.getPresentation().setEnabled((! myPatches.isEmpty()) && myContainBasedChanges);
833     }
834
835     public void actionPerformed(AnActionEvent e) {
836       if (myPatches.isEmpty() || (! myContainBasedChanges)) return;
837       final List<FilePatchInProgress.PatchChange> changes = getAllChanges();
838       Collections.sort(changes, myMyChangeComparator);
839       final List<FilePatchInProgress.PatchChange> selectedChanges = myChangesTreeList.getSelectedChanges();
840
841       int selectedIdx = 0;
842       final ArrayList<DiffRequestPresentable> diffRequestPresentables = new ArrayList<DiffRequestPresentable>(changes.size());
843       if (selectedChanges.isEmpty()) {
844         selectedChanges.addAll(changes);
845       }
846       if (! selectedChanges.isEmpty()) {
847         final FilePatchInProgress.PatchChange c = selectedChanges.get(0);
848         for (FilePatchInProgress.PatchChange change : changes) {
849           final FilePatchInProgress patchInProgress = change.getPatchInProgress();
850           if (! patchInProgress.baseExistsOrAdded()) continue;
851           final DiffRequestPresentable diffRequestPresentable = change.createDiffRequestPresentable(myProject);
852           if (diffRequestPresentable != null) {
853             diffRequestPresentables.add(diffRequestPresentable);
854           }
855           if (change.equals(c)) {
856             selectedIdx = diffRequestPresentables.size() - 1;
857           }
858         }
859       }
860       if (diffRequestPresentables.isEmpty()) return;
861       ShowDiffAction.showDiffImpl(myProject, diffRequestPresentables, selectedIdx, new ShowDiffUIContext(false));
862     }
863   }
864
865   private class MyChangeComparator implements Comparator<FilePatchInProgress.PatchChange> {
866     public int compare(FilePatchInProgress.PatchChange o1, FilePatchInProgress.PatchChange o2) {
867       if (PropertiesComponent.getInstance(myProject).isTrueValue("ChangesBrowser.SHOW_FLATTEN")) {
868         return o1.getPatchInProgress().getIoCurrentBase().getName().compareTo(o2.getPatchInProgress().getIoCurrentBase().getName());
869       }
870       return o1.getPatchInProgress().getIoCurrentBase().compareTo(o2.getPatchInProgress().getIoCurrentBase());
871     }
872   }
873 }