2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com
.intellij
.openapi
.vcs
.changes
.ui
;
18 import com
.intellij
.ide
.CopyProvider
;
19 import com
.intellij
.ide
.dnd
.*;
20 import com
.intellij
.ide
.util
.treeView
.TreeState
;
21 import com
.intellij
.openapi
.actionSystem
.*;
22 import com
.intellij
.openapi
.diagnostic
.Logger
;
23 import com
.intellij
.openapi
.fileChooser
.actions
.VirtualFileDeleteProvider
;
24 import com
.intellij
.openapi
.fileEditor
.OpenFileDescriptor
;
25 import com
.intellij
.openapi
.project
.Project
;
26 import com
.intellij
.openapi
.util
.Pair
;
27 import com
.intellij
.openapi
.vcs
.*;
28 import com
.intellij
.openapi
.vcs
.changes
.*;
29 import com
.intellij
.openapi
.vcs
.changes
.issueLinks
.TreeLinkMouseListener
;
30 import com
.intellij
.openapi
.vcs
.diff
.DiffProvider
;
31 import com
.intellij
.openapi
.vfs
.VfsUtil
;
32 import com
.intellij
.openapi
.vfs
.VirtualFile
;
33 import com
.intellij
.ui
.PopupHandler
;
34 import com
.intellij
.ui
.SmartExpander
;
35 import com
.intellij
.ui
.TreeCopyProvider
;
36 import com
.intellij
.ui
.TreeSpeedSearch
;
37 import com
.intellij
.ui
.awt
.RelativePoint
;
38 import com
.intellij
.ui
.awt
.RelativeRectangle
;
39 import com
.intellij
.ui
.treeStructure
.Tree
;
40 import com
.intellij
.util
.EditSourceOnDoubleClickHandler
;
41 import com
.intellij
.util
.EditSourceOnEnterKeyHandler
;
42 import com
.intellij
.util
.containers
.Convertor
;
43 import com
.intellij
.util
.containers
.MultiMap
;
44 import com
.intellij
.util
.ui
.tree
.TreeUtil
;
45 import org
.jetbrains
.annotations
.NonNls
;
46 import org
.jetbrains
.annotations
.NotNull
;
47 import org
.jetbrains
.annotations
.Nullable
;
50 import javax
.swing
.table
.TableCellRenderer
;
51 import javax
.swing
.tree
.DefaultTreeModel
;
52 import javax
.swing
.tree
.TreeCellRenderer
;
53 import javax
.swing
.tree
.TreePath
;
54 import javax
.swing
.tree
.TreeSelectionModel
;
56 import java
.awt
.event
.MouseEvent
;
57 import java
.awt
.image
.BufferedImage
;
59 import java
.util
.List
;
64 public class ChangesListView
extends Tree
implements TypeSafeDataProvider
, AdvancedDnDSource
{
65 private static final Logger LOG
= Logger
.getInstance("#com.intellij.openapi.vcs.changes.ui.ChangesListView");
67 private ChangesListView
.DropTarget myDropTarget
;
68 private DnDManager myDndManager
;
69 private ChangeListOwner myDragOwner
;
70 private final Project myProject
;
71 private TreeState myTreeState
;
72 private boolean myShowFlatten
= false;
73 private final CopyProvider myCopyProvider
;
75 @NonNls public static final String HELP_ID_KEY
= "helpId";
76 @NonNls public static final String ourHelpId
= "ideaInterface.changes";
77 @NonNls public static final DataKey
<List
<VirtualFile
>> UNVERSIONED_FILES_DATA_KEY
= DataKey
.create("ChangeListView.UnversionedFiles");
78 @NonNls public static final DataKey
<List
<FilePath
>> MISSING_FILES_DATA_KEY
= DataKey
.create("ChangeListView.MissingFiles");
79 @NonNls public static final DataKey
<String
> HELP_ID_DATA_KEY
= DataKey
.create(HELP_ID_KEY
);
81 private ActionGroup myMenuGroup
;
83 public ChangesListView(final Project project
) {
86 getModel().setRoot(ChangesBrowserNode
.create(myProject
, TreeModelBuilder
.ROOT_NODE_VALUE
));
88 setShowsRootHandles(true);
89 setRootVisible(false);
91 new TreeSpeedSearch(this, new NodeToTextConvertor());
92 SmartExpander
.installOn(this);
93 myCopyProvider
= new TreeCopyProvider(this);
94 new TreeLinkMouseListener(new ChangesBrowserNodeRenderer(myProject
, false, false)).install(this);
97 public DefaultTreeModel
getModel() {
98 return (DefaultTreeModel
)super.getModel();
101 public void installDndSupport(ChangeListOwner owner
) {
103 myDropTarget
= new DropTarget();
104 myDndManager
= DnDManager
.getInstance();
106 myDndManager
.registerSource(this);
107 myDndManager
.registerTarget(myDropTarget
, this);
110 public void dispose() {
111 if (myDropTarget
!= null) {
112 myDndManager
.unregisterSource(this);
113 myDndManager
.unregisterTarget(myDropTarget
, this);
121 private void storeState() {
122 myTreeState
= TreeState
.createOn(this, (ChangesBrowserNode
)getModel().getRoot());
125 private void restoreState() {
126 myTreeState
.applyTo(this, (ChangesBrowserNode
)getModel().getRoot());
129 public boolean isShowFlatten() {
130 return myShowFlatten
;
133 public void setShowFlatten(final boolean showFlatten
) {
134 myShowFlatten
= showFlatten
;
137 public void updateModel(List
<?
extends ChangeList
> changeLists
, List
<VirtualFile
> unversionedFiles
, final List
<LocallyDeletedChange
> locallyDeletedFiles
,
138 List
<VirtualFile
> modifiedWithoutEditing
,
139 MultiMap
<String
, VirtualFile
> switchedFiles
,
140 @Nullable Map
<VirtualFile
, String
> switchedRoots
,
141 @Nullable List
<VirtualFile
> ignoredFiles
,
142 final List
<VirtualFile
> lockedFolders
,
143 @Nullable final Map
<VirtualFile
, LogicalLock
> logicallyLockedFiles
) {
146 TreeModelBuilder builder
= new TreeModelBuilder(myProject
, isShowFlatten());
147 final DefaultTreeModel model
= builder
.buildModel(changeLists
, unversionedFiles
, locallyDeletedFiles
, modifiedWithoutEditing
,
148 switchedFiles
, switchedRoots
, ignoredFiles
, lockedFolders
, logicallyLockedFiles
);
150 setCellRenderer(new ChangesBrowserNodeRenderer(myProject
, isShowFlatten(), true));
152 expandPath(new TreePath(((ChangesBrowserNode
)model
.getRoot()).getPath()));
157 public void calcData(DataKey key
, DataSink sink
) {
158 if (key
== VcsDataKeys
.CHANGES
) {
159 sink
.put(VcsDataKeys
.CHANGES
, getSelectedChanges());
161 else if (key
== VcsDataKeys
.CHANGE_LEAD_SELECTION
) {
162 sink
.put(VcsDataKeys
.CHANGE_LEAD_SELECTION
, getLeadSelection());
164 else if (key
== VcsDataKeys
.CHANGE_LISTS
) {
165 sink
.put(VcsDataKeys
.CHANGE_LISTS
, getSelectedChangeLists());
167 else if (key
== PlatformDataKeys
.VIRTUAL_FILE_ARRAY
) {
168 sink
.put(PlatformDataKeys
.VIRTUAL_FILE_ARRAY
, getSelectedFiles());
170 else if (key
== PlatformDataKeys
.NAVIGATABLE
) {
171 final VirtualFile
[] files
= getSelectedFiles();
172 if (files
.length
== 1 && !files
[0].isDirectory()) {
173 sink
.put(PlatformDataKeys
.NAVIGATABLE
, new OpenFileDescriptor(myProject
, files
[0], 0));
176 else if (key
== PlatformDataKeys
.NAVIGATABLE_ARRAY
) {
177 sink
.put(PlatformDataKeys
.NAVIGATABLE_ARRAY
, ChangesUtil
.getNavigatableArray(myProject
, getSelectedFiles()));
179 else if (key
== PlatformDataKeys
.DELETE_ELEMENT_PROVIDER
) {
180 final TreePath
[] paths
= getSelectionPaths();
182 for(TreePath path
: paths
) {
183 ChangesBrowserNode node
= (ChangesBrowserNode
) path
.getLastPathComponent();
184 if (!(node
.getUserObject() instanceof ChangeList
)) {
185 sink
.put(PlatformDataKeys
.DELETE_ELEMENT_PROVIDER
, new VirtualFileDeleteProvider());
191 else if (key
== PlatformDataKeys
.COPY_PROVIDER
) {
192 sink
.put(PlatformDataKeys
.COPY_PROVIDER
, myCopyProvider
);
194 else if (key
== UNVERSIONED_FILES_DATA_KEY
) {
195 sink
.put(UNVERSIONED_FILES_DATA_KEY
, getSelectedUnversionedFiles());
197 else if (key
== VcsDataKeys
.MODIFIED_WITHOUT_EDITING_DATA_KEY
) {
198 sink
.put(VcsDataKeys
.MODIFIED_WITHOUT_EDITING_DATA_KEY
, getSelectedModifiedWithoutEditing());
200 else if (key
== MISSING_FILES_DATA_KEY
) {
201 sink
.put(MISSING_FILES_DATA_KEY
, getSelectedMissingFiles());
203 else if (key
== HELP_ID_DATA_KEY
) {
204 sink
.put(HELP_ID_DATA_KEY
, ourHelpId
);
206 else if (key
== VcsDataKeys
.CHANGES_IN_LIST_KEY
) {
207 final TreePath selectionPath
= getSelectionPath();
208 if (selectionPath
!= null && selectionPath
.getPathCount() > 1) {
209 ChangesBrowserNode firstNode
= (ChangesBrowserNode
)selectionPath
.getPathComponent(1);
210 if (firstNode
instanceof ChangesBrowserChangeListNode
) {
211 final List
<Change
> list
= firstNode
.getAllChangesUnder();
212 sink
.put(VcsDataKeys
.CHANGES_IN_LIST_KEY
, list
);
218 private List
<VirtualFile
> getSelectedUnversionedFiles() {
219 return getSelectedVirtualFiles(ChangesBrowserNode
.UNVERSIONED_FILES_TAG
);
222 private List
<VirtualFile
> getSelectedModifiedWithoutEditing() {
223 return getSelectedVirtualFiles(ChangesBrowserNode
.MODIFIED_WITHOUT_EDITING_TAG
);
226 private List
<VirtualFile
> getSelectedIgnoredFiles() {
227 return getSelectedVirtualFiles(ChangesBrowserNode
.IGNORED_FILES_TAG
);
230 private List
<VirtualFile
> getSelectedVirtualFiles(final Object tag
) {
231 Set
<VirtualFile
> files
= new HashSet
<VirtualFile
>();
232 final TreePath
[] paths
= getSelectionPaths();
234 for (TreePath path
: paths
) {
235 if (path
.getPathCount() > 1) {
236 ChangesBrowserNode firstNode
= (ChangesBrowserNode
)path
.getPathComponent(1);
237 if (tag
== null || firstNode
.getUserObject() == tag
) {
238 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
239 files
.addAll(node
.getAllFilesUnder());
244 return new ArrayList
<VirtualFile
>(files
);
247 private List
<FilePath
> getSelectedMissingFiles() {
248 List
<FilePath
> files
= new ArrayList
<FilePath
>();
249 final TreePath
[] paths
= getSelectionPaths();
251 for (TreePath path
: paths
) {
252 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
253 files
.addAll(node
.getAllFilePathsUnder());
259 private VirtualFile
[] getSelectedFiles() {
260 final Change
[] changes
= getSelectedChanges();
261 Collection
<VirtualFile
> files
= new HashSet
<VirtualFile
>();
262 for (Change change
: changes
) {
263 final ContentRevision afterRevision
= change
.getAfterRevision();
264 if (afterRevision
!= null) {
265 final VirtualFile file
= afterRevision
.getFile().getVirtualFile();
266 if (file
!= null && file
.isValid()) {
272 files
.addAll(getSelectedVirtualFiles(null));
274 return VfsUtil
.toVirtualFileArray(files
);
277 private Change
[] getLeadSelection() {
278 final Set
<Change
> changes
= new LinkedHashSet
<Change
>();
280 final TreePath
[] paths
= getSelectionPaths();
281 if (paths
== null) return new Change
[0];
283 for (TreePath path
: paths
) {
284 ChangesBrowserNode node
= (ChangesBrowserNode
) path
.getLastPathComponent();
285 if (node
instanceof ChangesBrowserChangeNode
) {
286 changes
.add(((ChangesBrowserChangeNode
) node
).getUserObject());
290 return changes
.toArray(new Change
[changes
.size()]);
294 private Change
[] getSelectedChanges() {
295 Set
<Change
> changes
= new LinkedHashSet
<Change
>();
297 final TreePath
[] paths
= getSelectionPaths();
298 if (paths
== null) return new Change
[0];
300 for (TreePath path
: paths
) {
301 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
302 changes
.addAll(node
.getAllChangesUnder());
305 if (changes
.size() == 0) {
306 final List
<VirtualFile
> selectedModifiedWithoutEditing
= getSelectedModifiedWithoutEditing();
307 if (selectedModifiedWithoutEditing
!= null && selectedModifiedWithoutEditing
.size() > 0) {
308 for(VirtualFile file
: selectedModifiedWithoutEditing
) {
309 AbstractVcs vcs
= ProjectLevelVcsManager
.getInstance(myProject
).getVcsFor(file
);
310 final DiffProvider diffProvider
= vcs
== null ?
null : vcs
.getDiffProvider();
311 if (diffProvider
!= null) {
312 ContentRevision beforeRevision
= new VcsCurrentRevisionProxy(diffProvider
, file
);
313 ContentRevision afterRevision
= new CurrentContentRevision(new FilePathImpl(file
));
314 changes
.add(new Change(beforeRevision
, afterRevision
, FileStatus
.HIJACKED
));
320 return changes
.toArray(new Change
[changes
.size()]);
324 private ChangeList
[] getSelectedChangeLists() {
325 Set
<ChangeList
> lists
= new HashSet
<ChangeList
>();
327 final TreePath
[] paths
= getSelectionPaths();
328 if (paths
== null) return new ChangeList
[0];
330 for (TreePath path
: paths
) {
331 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
332 final Object userObject
= node
.getUserObject();
333 if (userObject
instanceof ChangeList
) {
334 lists
.add((ChangeList
)userObject
);
338 return lists
.toArray(new ChangeList
[lists
.size()]);
341 public void setMenuActions(final ActionGroup menuGroup
) {
342 myMenuGroup
= menuGroup
;
344 EditSourceOnDoubleClickHandler
.install(this);
345 EditSourceOnEnterKeyHandler
.install(this);
348 private void updateMenu() {
349 PopupHandler
.installPopupHandler(this, myMenuGroup
, ActionPlaces
.CHANGES_VIEW_POPUP
, ActionManager
.getInstance());
352 public void updateUI() {
354 if (myMenuGroup
!= null) {
359 @SuppressWarnings({"UtilityClassWithoutPrivateConstructor"})
360 private static class DragImageFactory
{
361 private static void drawSelection(JTable table
, int column
, Graphics g
, final int width
) {
363 final int[] rows
= table
.getSelectedRows();
364 final int height
= table
.getRowHeight();
365 for (int row
: rows
) {
366 final TableCellRenderer renderer
= table
.getCellRenderer(row
, column
);
367 final Component component
= renderer
.getTableCellRendererComponent(table
, table
.getValueAt(row
, column
), false, false, row
, column
);
369 component
.setBounds(0, 0, width
, height
);
370 boolean wasOpaque
= false;
371 if (component
instanceof JComponent
) {
372 final JComponent j
= (JComponent
)component
;
373 if (j
.isOpaque()) wasOpaque
= true;
378 ((JComponent
)component
).setOpaque(true);
385 private static void drawSelection(JTree tree
, Graphics g
, final int width
) {
387 final int[] rows
= tree
.getSelectionRows();
388 final int height
= tree
.getRowHeight();
389 for (int row
: rows
) {
390 final TreeCellRenderer renderer
= tree
.getCellRenderer();
391 final Object value
= tree
.getPathForRow(row
).getLastPathComponent();
392 if (value
== null) continue;
393 final Component component
= renderer
.getTreeCellRendererComponent(tree
, value
, false, false, false, row
, false);
394 if (component
.getFont() == null) {
395 component
.setFont(tree
.getFont());
398 component
.setBounds(0, 0, width
, height
);
399 boolean wasOpaque
= false;
400 if (component
instanceof JComponent
) {
401 final JComponent j
= (JComponent
)component
;
402 if (j
.isOpaque()) wasOpaque
= true;
407 ((JComponent
)component
).setOpaque(true);
414 public static Image
createImage(final JTable table
, int column
) {
415 final int height
= Math
.max(20, Math
.min(100, table
.getSelectedRowCount() * table
.getRowHeight()));
416 final int width
= table
.getColumnModel().getColumn(column
).getWidth();
418 final BufferedImage image
= new BufferedImage(width
, height
, BufferedImage
.TYPE_INT_ARGB
);
419 Graphics2D g2
= (Graphics2D
)image
.getGraphics();
421 g2
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, 0.7f
));
423 drawSelection(table
, column
, g2
, width
);
427 public static Image
createImage(final JTree tree
) {
428 final TreeSelectionModel model
= tree
.getSelectionModel();
429 final TreePath
[] paths
= model
.getSelectionPaths();
432 final List
<ChangesBrowserNode
> nodes
= new ArrayList
<ChangesBrowserNode
>();
433 for (final TreePath path
: paths
) {
434 final ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
435 if (!node
.isLeaf()) {
437 count
+= node
.getCount();
441 for (TreePath path
: paths
) {
442 final ChangesBrowserNode element
= (ChangesBrowserNode
)path
.getLastPathComponent();
443 boolean child
= false;
444 for (final ChangesBrowserNode node
: nodes
) {
445 if (node
.isNodeChild(element
)) {
452 if (element
.isLeaf()) count
++;
453 } else if (!element
.isLeaf()) {
454 count
-= element
.getCount();
458 final JLabel label
= new JLabel(VcsBundle
.message("changes.view.dnd.label", count
));
459 label
.setOpaque(true);
460 label
.setForeground(tree
.getForeground());
461 label
.setBackground(tree
.getBackground());
462 label
.setFont(tree
.getFont());
463 label
.setSize(label
.getPreferredSize());
464 final BufferedImage image
= new BufferedImage(label
.getWidth(), label
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
466 Graphics2D g2
= (Graphics2D
)image
.getGraphics();
467 g2
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, 0.7f
));
477 public class DropTarget
implements DnDTarget
{
478 public boolean update(DnDEvent aEvent
) {
479 aEvent
.hideHighlighter();
480 aEvent
.setDropPossible(false, "");
482 Object attached
= aEvent
.getAttachedObject();
483 if (!(attached
instanceof ChangeListDragBean
)) return false;
485 final ChangeListDragBean dragBean
= (ChangeListDragBean
)attached
;
486 if (dragBean
.getSourceComponent() != ChangesListView
.this) return false;
487 dragBean
.setTargetNode(null);
489 RelativePoint dropPoint
= aEvent
.getRelativePoint();
490 Point onTree
= dropPoint
.getPoint(ChangesListView
.this);
491 final TreePath dropPath
= getPathForLocation(onTree
.x
, onTree
.y
);
493 if (dropPath
== null) return false;
495 ChangesBrowserNode dropNode
= (ChangesBrowserNode
)dropPath
.getLastPathComponent();
496 while(!((ChangesBrowserNode
) dropNode
.getParent()).isRoot()) {
497 dropNode
= (ChangesBrowserNode
)dropNode
.getParent();
500 if (!dropNode
.canAcceptDrop(dragBean
)) {
504 final Rectangle tableCellRect
= getPathBounds(new TreePath(dropNode
.getPath()));
505 if (fitsInBounds(tableCellRect
)) {
506 aEvent
.setHighlighting(new RelativeRectangle(ChangesListView
.this, tableCellRect
), DnDEvent
.DropTargetHighlightingType
.RECTANGLE
);
509 aEvent
.setDropPossible(true, null);
510 dragBean
.setTargetNode(dropNode
);
515 public void drop(DnDEvent aEvent
) {
516 Object attached
= aEvent
.getAttachedObject();
517 if (!(attached
instanceof ChangeListDragBean
)) return;
519 final ChangeListDragBean dragBean
= (ChangeListDragBean
)attached
;
520 final ChangesBrowserNode changesBrowserNode
= dragBean
.getTargetNode();
521 if (changesBrowserNode
!= null) {
522 changesBrowserNode
.acceptDrop(myDragOwner
, dragBean
);
526 public void cleanUpOnLeave() {
529 public void updateDraggedImage(Image image
, Point dropPoint
, Point imageOffset
) {
533 private boolean fitsInBounds(final Rectangle rect
) {
534 final Container container
= getParent();
535 if (container
instanceof JViewport
) {
536 final Container scrollPane
= container
.getParent();
537 if (scrollPane
instanceof JScrollPane
) {
538 final Rectangle rectangle
= SwingUtilities
.convertRectangle(this, rect
, scrollPane
.getParent());
539 return scrollPane
.getBounds().contains(rectangle
);
545 private static class NodeToTextConvertor
implements Convertor
<TreePath
, String
> {
546 public String
convert(final TreePath path
) {
547 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
548 return node
.getTextPresentation();
552 public boolean canStartDragging(DnDAction action
, Point dragOrigin
) {
553 return action
== DnDAction
.MOVE
&&
554 (getSelectedChanges().length
> 0 || getSelectedUnversionedFiles().size() > 0 || getSelectedIgnoredFiles().size() > 0);
557 public DnDDragStartBean
startDragging(DnDAction action
, Point dragOrigin
) {
558 return new DnDDragStartBean(new ChangeListDragBean(this, getSelectedChanges(), getSelectedUnversionedFiles(),
559 getSelectedIgnoredFiles()));
563 public Pair
<Image
, Point
> createDraggedImage(DnDAction action
, Point dragOrigin
) {
564 final Image image
= DragImageFactory
.createImage(this);
565 return new Pair
<Image
, Point
>(image
, new Point(-image
.getWidth(null), -image
.getHeight(null)));
568 public void dragDropEnd() {
571 public void dropActionChanged(final int gestureModifiers
) {
575 public JComponent
getComponent() {
579 public void processMouseEvent(final MouseEvent e
) {
580 if (MouseEvent
.MOUSE_RELEASED
== e
.getID() && !isSelectionEmpty() && !e
.isShiftDown() && !e
.isControlDown() &&
581 !e
.isMetaDown() && !e
.isPopupTrigger()) {
582 if (isOverSelection(e
.getPoint())) {
584 final TreePath path
= getPathForLocation(e
.getPoint().x
, e
.getPoint().y
);
586 setSelectionPath(path
);
593 super.processMouseEvent(e
);
596 public boolean isOverSelection(final Point point
) {
597 return TreeUtil
.isOverSelection(this, point
);
600 public void dropSelectionButUnderPoint(final Point point
) {
601 TreeUtil
.dropSelectionButUnderPoint(this, point
);