vcs: Moved several classes to [svn4idea]
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / integrate / ToBeMergedDialog.java
1 /*
2  * Copyright 2000-2010 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 org.jetbrains.idea.svn.integrate;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.openapi.actionSystem.*;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.progress.ProgressIndicator;
22 import com.intellij.openapi.progress.Task;
23 import com.intellij.openapi.project.DumbAwareAction;
24 import com.intellij.openapi.ui.DialogWrapper;
25 import com.intellij.openapi.ui.MessageType;
26 import com.intellij.openapi.ui.Splitter;
27 import com.intellij.openapi.ui.popup.util.PopupUtil;
28 import com.intellij.openapi.util.Condition;
29 import com.intellij.openapi.util.Pair;
30 import com.intellij.openapi.vcs.VcsException;
31 import com.intellij.openapi.vcs.changes.Change;
32 import com.intellij.openapi.vcs.changes.committed.CommittedChangeListRenderer;
33 import com.intellij.openapi.vcs.changes.committed.RepositoryChangesBrowser;
34 import com.intellij.openapi.vcs.changes.issueLinks.AbstractBaseTagMouseListener;
35 import com.intellij.openapi.vcs.changes.ui.ChangeNodeDecorator;
36 import com.intellij.openapi.vcs.changes.ui.ChangesBrowserNodeRenderer;
37 import com.intellij.ui.*;
38 import com.intellij.ui.table.TableView;
39 import com.intellij.util.ObjectUtils;
40 import com.intellij.util.ui.ColumnInfo;
41 import com.intellij.util.ui.JBUI;
42 import com.intellij.util.ui.ListTableModel;
43 import com.intellij.util.ui.UIUtil;
44 import com.intellij.util.ui.components.BorderLayoutPanel;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.idea.svn.history.SvnChangeList;
47 import org.jetbrains.idea.svn.mergeinfo.ListMergeStatus;
48 import org.jetbrains.idea.svn.mergeinfo.MergeChecker;
49 import org.jetbrains.idea.svn.mergeinfo.SvnMergeInfoCache;
50
51 import javax.swing.*;
52 import javax.swing.event.ListSelectionEvent;
53 import javax.swing.event.ListSelectionListener;
54 import javax.swing.table.TableCellRenderer;
55 import java.awt.*;
56 import java.awt.event.ActionEvent;
57 import java.awt.event.KeyAdapter;
58 import java.awt.event.KeyEvent;
59 import java.awt.event.MouseEvent;
60 import java.util.*;
61 import java.util.List;
62 import java.util.stream.Collectors;
63
64 import static com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser.collectChanges;
65 import static com.intellij.util.containers.ContainerUtil.*;
66 import static com.intellij.util.containers.ContainerUtilRt.emptyList;
67 import static com.intellij.util.containers.ContainerUtilRt.newHashSet;
68 import static java.util.Collections.singletonList;
69 import static java.util.Collections.synchronizedMap;
70 import static org.jetbrains.idea.svn.integrate.MergeCalculatorTask.getBunchSize;
71 import static org.jetbrains.idea.svn.integrate.MergeCalculatorTask.loadChangeLists;
72
73 public class ToBeMergedDialog extends DialogWrapper {
74   public static final int MERGE_ALL_CODE = 222;
75   private final JPanel myPanel;
76   @NotNull private final MergeContext myMergeContext;
77   @NotNull private final ListTableModel<SvnChangeList> myRevisionsModel;
78   private TableView<SvnChangeList> myRevisionsList;
79   private RepositoryChangesBrowser myRepositoryChangesBrowser;
80   private Splitter mySplitter;
81
82   private final QuantitySelection<Long> myWiseSelection;
83
84   private final Set<Change> myAlreadyMerged;
85   private final MergeChecker myMergeChecker;
86   private final boolean myAllStatusesCalculated;
87   private volatile boolean myAllListsLoaded;
88
89   private final Map<Long, ListMergeStatus> myStatusMap;
90   private ToBeMergedDialog.MoreXAction myMore100Action;
91   private ToBeMergedDialog.MoreXAction myMore500Action;
92
93   public ToBeMergedDialog(@NotNull MergeContext mergeContext,
94                           @NotNull List<SvnChangeList> changeLists,
95                           final String title,
96                           @NotNull MergeChecker mergeChecker,
97                           boolean allStatusesCalculated,
98                           boolean allListsLoaded) {
99     super(mergeContext.getProject(), true);
100     myMergeContext = mergeContext;
101     myAllListsLoaded = allListsLoaded;
102     myStatusMap = synchronizedMap(newHashMap());
103     myMergeChecker = mergeChecker;
104     myAllStatusesCalculated = allStatusesCalculated;
105     setTitle(title);
106
107     myRevisionsModel = new ListTableModel<>(new ColumnInfo[]{FAKE_COLUMN}, changeLists);
108     myPanel = new JPanel(new BorderLayout());
109     myWiseSelection = new QuantitySelection<>(allStatusesCalculated);
110     myAlreadyMerged = newHashSet();
111     setOKButtonText("Merge Selected");
112     initUI();
113     init();
114     enableLoadButtons();
115
116     if (!myAllStatusesCalculated) {
117       refreshListStatus(changeLists);
118     }
119   }
120
121   private void enableLoadButtons() {
122     myMore100Action.setVisible(!myAllListsLoaded);
123     myMore500Action.setVisible(!myAllListsLoaded);
124     myMore100Action.setEnabled(!myAllListsLoaded);
125     myMore500Action.setEnabled(!myAllListsLoaded);
126   }
127
128   public void setAllListsLoaded() {
129     myAllListsLoaded = true;
130     enableLoadButtons();
131   }
132
133   public long getLastNumber() {
134     int totalRows = myRevisionsModel.getRowCount();
135
136     return totalRows > 0 ? myRevisionsModel.getItem(totalRows - 1).getNumber() : 0;
137   }
138
139   public void addMoreLists(@NotNull List<SvnChangeList> changeLists) {
140     myRevisionsModel.addRows(changeLists);
141     myRevisionsList.revalidate();
142     myRevisionsList.repaint();
143     myMore100Action.setEnabled(true);
144     myMore500Action.setEnabled(true);
145     // TODO: This is necessary because myMore500Action was hidden in MoreXAction.actionPerformed()
146     myMore500Action.setVisible(true);
147     refreshListStatus(changeLists);
148   }
149
150   private boolean myDisposed;
151
152   @Override
153   protected void dispose() {
154     super.dispose();
155     myDisposed = true;
156   }
157
158   private void refreshListStatus(@NotNull final List<SvnChangeList> changeLists) {
159     if (myDisposed) return;
160     ApplicationManager.getApplication().executeOnPooledThread(() -> {
161       int cnt = 10;
162       for (SvnChangeList list : changeLists) {
163         // at the moment we calculate only "merged" since we don;t have branch copy point
164         myStatusMap.put(list.getNumber(), toListMergeStatus(myMergeChecker.checkList(list)));
165
166         --cnt;
167         if (cnt <= 0) {
168           ApplicationManager.getApplication().invokeLater(() -> {
169             myRevisionsList.revalidate();
170             myRevisionsList.repaint();
171           });
172           cnt = 10;
173         }
174       }
175       myRevisionsList.revalidate();
176       myRevisionsList.repaint();
177     });
178   }
179
180   @NotNull
181   private static ListMergeStatus toListMergeStatus(@NotNull SvnMergeInfoCache.MergeCheckResult mergeCheckResult) {
182     ListMergeStatus result;
183
184     switch (mergeCheckResult) {
185       case MERGED:
186         result = ListMergeStatus.MERGED;
187         break;
188       case NOT_EXISTS:
189         result = ListMergeStatus.ALIEN;
190         break;
191       default:
192         result = ListMergeStatus.REFRESHING;
193         break;
194     }
195
196     return result;
197   }
198
199   @NotNull
200   @Override
201   protected Action[] createActions() {
202     if (myAllStatusesCalculated) {
203       return new Action[]{getOKAction(), new DialogWrapperAction("Merge All") {
204         @Override
205         protected void doAction(ActionEvent e) {
206           close(MERGE_ALL_CODE);
207         }
208       }, getCancelAction()};
209     }
210     else {
211       return super.createActions();
212     }
213   }
214
215   @NotNull
216   public List<SvnChangeList> getSelected() {
217     Set<Long> selected = myWiseSelection.getSelected();
218     Set<Long> unselected = myWiseSelection.getUnselected();
219     // todo: can be made faster
220     Condition<SvnChangeList> filter =
221       myWiseSelection.areAllSelected() ? list -> !unselected.contains(list.getNumber()) : list -> selected.contains(list.getNumber());
222
223     return filter(myRevisionsModel.getItems(), filter);
224   }
225
226   @Override
227   protected String getDimensionServiceKey() {
228     // TODO: Currently class is in other package, but key to persist dimension is preserved.
229     // TODO: Rename later to some "neutral" constant not to be confusing relative to current class location.
230     return "org.jetbrains.idea.svn.dialogs.ToBeMergedDialog";
231   }
232
233   private void initUI() {
234     final ListSelectionListener selectionListener = new ListSelectionListener() {
235       @Override
236       public void valueChanged(ListSelectionEvent e) {
237         List<SvnChangeList> changeLists = myRevisionsList.getSelectedObjects();
238
239         myAlreadyMerged.clear();
240         for (SvnChangeList changeList : changeLists) {
241           myAlreadyMerged.addAll(getAlreadyMergedPaths(changeList));
242         }
243         myRepositoryChangesBrowser.setChangesToDisplay(collectChanges(changeLists, false));
244
245         mySplitter.doLayout();
246         myRepositoryChangesBrowser.repaint();
247       }
248     };
249     final MyListCellRenderer listCellRenderer = new MyListCellRenderer();
250     myRevisionsList = new TableView<SvnChangeList>() {
251       @Override
252       public TableCellRenderer getCellRenderer(int row, int column) {
253         return listCellRenderer;
254       }
255
256       @Override
257       public void valueChanged(ListSelectionEvent e) {
258         super.valueChanged(e);
259         selectionListener.valueChanged(e);
260       }
261     };
262     myRevisionsList.setExpandableItemsEnabled(false);
263     new TableViewSpeedSearch<SvnChangeList>(myRevisionsList) {
264       @Override
265       protected String getItemText(@NotNull SvnChangeList element) {
266         return element.getComment();
267       }
268     };
269     myRevisionsList.setModelAndUpdateColumns(myRevisionsModel);
270     myRevisionsList.setTableHeader(null);
271     myRevisionsList.setShowGrid(false);
272     final AbstractBaseTagMouseListener mouseListener = new AbstractBaseTagMouseListener() {
273       @Override
274       public Object getTagAt(@NotNull MouseEvent e) {
275         JTable table = (JTable)e.getSource();
276         int row = table.rowAtPoint(e.getPoint());
277         int column = table.columnAtPoint(e.getPoint());
278         if (row == -1 || column == -1) return null;
279         listCellRenderer.customizeCellRenderer(table, table.getValueAt(row, column), table.isRowSelected(row));
280         return listCellRenderer.myRenderer.getFragmentTagAt(e.getPoint().x - table.getCellRect(row, column, false).x);
281       }
282     };
283     mouseListener.installOn(myRevisionsList);
284
285     myMore100Action = new MoreXAction(100);
286     myMore500Action = new MoreXAction(500);
287
288     BorderLayoutPanel panel = JBUI.Panels.simplePanel()
289       .addToCenter(ScrollPaneFactory.createScrollPane(myRevisionsList))
290       .addToTop(createToolbar().getComponent());
291
292     mySplitter = new Splitter(false, 0.7f);
293     mySplitter.setFirstComponent(panel);
294
295     myRepositoryChangesBrowser =
296       new RepositoryChangesBrowser(myMergeContext.getProject(), Collections.<SvnChangeList>emptyList(), emptyList(), null);
297     myRepositoryChangesBrowser.getDiffAction()
298       .registerCustomShortcutSet(myRepositoryChangesBrowser.getDiffAction().getShortcutSet(), myRevisionsList);
299     setChangesDecorator();
300     mySplitter.setSecondComponent(myRepositoryChangesBrowser);
301     mySplitter.setDividerWidth(2);
302
303     addRevisionListListeners();
304
305     myPanel.add(mySplitter, BorderLayout.CENTER);
306   }
307
308   @NotNull
309   private ActionToolbar createToolbar() {
310     DefaultActionGroup actions = new DefaultActionGroup(new MySelectAll(), new MyUnselectAll(), myMore100Action, myMore500Action);
311
312     return ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, actions, true);
313   }
314
315   @NotNull
316   private List<Change> getAlreadyMergedPaths(@NotNull SvnChangeList svnChangeList) {
317     Collection<String> notMerged = myMergeChecker.getNotMergedPaths(svnChangeList);
318
319     return isEmpty(notMerged) ? emptyList() : svnChangeList.getAffectedPaths().stream()
320       .filter(path -> !notMerged.contains(path))
321       .map(svnChangeList::getByPath)
322       .collect(Collectors.toList());
323   }
324
325   private void setChangesDecorator() {
326     myRepositoryChangesBrowser.setDecorator(new ChangeNodeDecorator() {
327       @Override
328       public void decorate(Change change, SimpleColoredComponent component, boolean isShowFlatten) {
329       }
330
331       @Override
332       public List<Pair<String, Stress>> stressPartsOfFileName(Change change, String parentPath) {
333         return null;
334       }
335
336       @Override
337       public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
338         if (myAlreadyMerged.contains(change)) {
339           renderer.append(" [already merged] ", SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES);
340         }
341       }
342     });
343   }
344
345   private void addRevisionListListeners() {
346     final int checkboxWidth = new JCheckBox().getPreferredSize().width;
347     new ClickListener() {
348       @Override
349       public boolean onClick(@NotNull MouseEvent e, int clickCount) {
350         final int idx = myRevisionsList.rowAtPoint(e.getPoint());
351         if (idx >= 0) {
352           final Rectangle baseRect = myRevisionsList.getCellRect(idx, 0, false);
353           baseRect.setSize(checkboxWidth, baseRect.height);
354           if (baseRect.contains(e.getPoint())) {
355             toggleInclusion(myRevisionsModel.getRowValue(idx));
356             myRevisionsList.repaint(baseRect);
357           }
358         }
359         return true;
360       }
361     }.installOn(myRevisionsList);
362
363     myRevisionsList.addKeyListener(new KeyAdapter() {
364       @Override
365       public void keyReleased(KeyEvent e) {
366         if (KeyEvent.VK_SPACE == e.getKeyCode()) {
367           List<SvnChangeList> selected = myRevisionsList.getSelectedObjects();
368           if (!selected.isEmpty()) {
369             selected.forEach(ToBeMergedDialog.this::toggleInclusion);
370             myRevisionsList.repaint();
371             e.consume();
372           }
373         }
374       }
375     });
376   }
377
378   private void toggleInclusion(@NotNull SvnChangeList list) {
379     long number = list.getNumber();
380
381     if (myWiseSelection.isSelected(number)) {
382       myWiseSelection.remove(number);
383     }
384     else {
385       myWiseSelection.add(number);
386     }
387   }
388
389   @Override
390   protected JComponent createCenterPanel() {
391     return myPanel;
392   }
393
394   private class MoreXAction extends MoreAction {
395     private final int myQuantity;
396
397     private MoreXAction(final int quantity) {
398       super("Load +" + quantity);
399       myQuantity = quantity;
400     }
401
402     @Override
403     public void actionPerformed(AnActionEvent e) {
404       // TODO: This setVisible() is necessary because MoreXAction shows "Loading..." text when disabled
405       myMore500Action.setVisible(false);
406       myMore100Action.setEnabled(false);
407       myMore500Action.setEnabled(false);
408
409       new LoadChangeListsTask(getLastNumber(), myQuantity).queue();
410     }
411   }
412
413   private class MySelectAll extends DumbAwareAction {
414     private MySelectAll() {
415       super("Select All", "Select All", AllIcons.Actions.Selectall);
416     }
417
418     @Override
419     public void actionPerformed(AnActionEvent e) {
420       myWiseSelection.setAll();
421       myRevisionsList.repaint();
422     }
423   }
424
425   private class MyUnselectAll extends DumbAwareAction {
426     private MyUnselectAll() {
427       super("Unselect All", "Unselect All", AllIcons.Actions.Unselectall);
428     }
429
430     @Override
431     public void actionPerformed(AnActionEvent e) {
432       myWiseSelection.clearAll();
433       myRevisionsList.repaint();
434     }
435   }
436
437   private class LoadChangeListsTask extends Task.Backgroundable {
438
439     private final long myStartNumber;
440     private final int myQuantity;
441     private List<SvnChangeList> myLists;
442     private boolean myIsLastListLoaded;
443
444     public LoadChangeListsTask(long startNumber, int quantity) {
445       super(myMergeContext.getProject(), "Loading recent " + myMergeContext.getBranchName() + " revisions", true);
446       myStartNumber = startNumber;
447       myQuantity = quantity;
448     }
449
450     @Override
451     public void run(@NotNull ProgressIndicator indicator) {
452       try {
453         Pair<List<SvnChangeList>, Boolean> loadResult = loadChangeLists(myMergeContext, myStartNumber, getBunchSize(myQuantity));
454
455         myLists = loadResult.first;
456         myIsLastListLoaded = loadResult.second;
457       }
458       catch (VcsException e) {
459         setEmptyData();
460         PopupUtil.showBalloonForActiveComponent(e.getMessage(), MessageType.ERROR);
461       }
462     }
463
464     @Override
465     public void onCancel() {
466       setEmptyData();
467       updateDialog();
468     }
469
470     @Override
471     public void onSuccess() {
472       updateDialog();
473     }
474
475     private void setEmptyData() {
476       myLists = emptyList();
477       myIsLastListLoaded = false;
478     }
479
480     private void updateDialog() {
481       addMoreLists(myLists);
482       if (myIsLastListLoaded) {
483         setAllListsLoaded();
484       }
485     }
486   }
487
488   private class MyListCellRenderer implements TableCellRenderer {
489     private final JPanel myPanel;
490     private final CommittedChangeListRenderer myRenderer;
491     private JCheckBox myCheckBox;
492
493     private MyListCellRenderer() {
494       myPanel = new JPanel(new BorderLayout());
495       myCheckBox = new JCheckBox();
496       myCheckBox.setEnabled(true);
497       myCheckBox.setSelected(true);
498       myRenderer = new CommittedChangeListRenderer(myMergeContext.getProject(), singletonList(list -> {
499         ListMergeStatus status = myAllStatusesCalculated
500                                  ? ListMergeStatus.NOT_MERGED
501                                  : ObjectUtils.notNull(myStatusMap.get(list.getNumber()), ListMergeStatus.REFRESHING);
502
503         return status.getIcon();
504       }));
505     }
506
507     protected void customizeCellRenderer(JTable table, Object value, boolean selected) {
508       myPanel.removeAll();
509       myPanel.setBackground(null);
510       myRenderer.clear();
511       myRenderer.setBackground(null);
512
513       // 7-8, a hack
514       if (value instanceof SvnChangeList) {
515         final SvnChangeList changeList = (SvnChangeList)value;
516         myRenderer.renderChangeList(table, changeList);
517
518         final Color bg = selected ? UIUtil.getTableSelectionBackground() : UIUtil.getTableBackground();
519         final Color fg = selected ? UIUtil.getTableSelectionForeground() : UIUtil.getTableForeground();
520
521         myRenderer.setBackground(bg);
522         myRenderer.setForeground(fg);
523         myCheckBox.setBackground(bg);
524         myCheckBox.setForeground(fg);
525
526         myPanel.setBackground(bg);
527         myPanel.setForeground(fg);
528
529         myCheckBox.setSelected(myWiseSelection.isSelected(changeList.getNumber()));
530         myPanel.add(myCheckBox, BorderLayout.WEST);
531         myPanel.add(myRenderer, BorderLayout.CENTER);
532       }
533     }
534
535     @Override
536     public final Component getTableCellRendererComponent(
537       JTable table,
538       Object value,
539       boolean isSelected,
540       boolean hasFocus,
541       int row,
542       int column
543     ) {
544       customizeCellRenderer(table, value, isSelected);
545       return myPanel;
546     }
547   }
548
549   private static final ColumnInfo FAKE_COLUMN = new ColumnInfo<SvnChangeList, SvnChangeList>("fake column") {
550     @Override
551     public SvnChangeList valueOf(SvnChangeList changeList) {
552       return changeList;
553     }
554   };
555 }