Skip to main content
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteffen Pingel2010-11-19 17:50:53 -0500
committerSteffen Pingel2010-11-19 17:50:53 -0500
commit448b7d842f053c1bca8eafd8f2bb82c4fc780b6e (patch)
tree876ebc0453fdc6876d1f03b7a83f86568c85eac7
downloadorg.eclipse.mylyn.reviews.ui-448b7d842f053c1bca8eafd8f2bb82c4fc780b6e.tar.gz
org.eclipse.mylyn.reviews.ui-448b7d842f053c1bca8eafd8f2bb82c4fc780b6e.tar.xz
org.eclipse.mylyn.reviews.ui-448b7d842f053c1bca8eafd8f2bb82c4fc780b6e.zip
bug 326729: support line-based commenting
https://bugs.eclipse.org/bugs/show_bug.cgi?id=326729
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/.classpath11
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/.project46
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/META-INF/MANIFEST.MF54
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/about.html27
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/build.properties8
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/plugin.xml111
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/commons/crucible/api/model/ReviewModelUtil.java32
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/IReviewChangeListenerAction.java29
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleConstants.java47
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleUtil.java390
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/TaskRepositoryUtil.java84
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/CrucibleClient.java63
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/model/IReviewCacheListener.java30
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ActiveReviewManager.java281
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/AvatarImages.java96
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleImages.java140
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileInput.java105
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileStorage.java112
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiConstants.java35
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiPlugin.java222
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiUtil.java452
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ICrucibleFileProvider.java20
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewAction.java26
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewActionListener.java27
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/SwitchingPerspectiveReviewActivationListener.java112
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractAddCommentAction.java183
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractListenableReviewAction.java40
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewAction.java115
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewFromResourcesAction.java181
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddChangesetToActiveReviewAction.java72
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddFileCommentAction.java109
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddGeneralCommentToFileAction.java103
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddLineCommentToFileAction.java123
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/CreateReviewFromResourcesAction.java78
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditActiveTaskAction.java61
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditCommentAction.java107
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/LocalTeamResourceConnector.java125
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/PostDraftCommentAction.java89
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/RemoveCommentAction.java86
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/ReplyToCommentAction.java108
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/actions.properties9
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHover.java405
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHoverInput.java37
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModel.java361
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModelManager.java62
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentAnnotation.java104
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentPopupDialog.java217
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCompareAnnotationModel.java645
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControl.java264
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControlCreator.java27
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleAnnotationModel.java19
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleCompareSourceViewer.java23
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleProjectsLabelProvider.java31
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleRepositoriesLabelProvider.java31
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserLabelProvider.java33
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserSorter.java42
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleCommentDialog.java298
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleReviewActionDialog.java171
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddCommentDialog.java316
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddFileAddCommentDialog.java152
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleEditCommentDialog.java378
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/ReviewerSelectionDialog.java61
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/AbstractCommentPart.java340
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/CommentPart.java59
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ExpandablePart.java440
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ReviewersSelectionTreePart.java137
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/VersionedCommentPart.java230
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerColumn.java274
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerHover.java375
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/operations/CrucibleFileInfoCompareEditorInput.java223
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/CommentUiUtil.java190
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/EditorUtil.java198
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/AbstractCrucibleWizardPage.java24
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleAddPatchPage.java181
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewDetailsPage.java456
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewWizard.java52
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingButton.java72
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingsPage.java177
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/RepositorySelectionWizard.java57
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ResourceSelectionPage.java242
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewTypeSelectionPage.java211
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewWizard.java307
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectChangesetsFromCruciblePage.java643
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectScmChangesetsPage.java731
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/actions/AbstractResourceAction.java153
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/AtlassianUiUtil.java145
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/DecoratedResource.java109
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/EditorResourceAdapterFactory.java114
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/IEditorResource.java22
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceEditorBean.java34
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceSelectionTree.java642
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/dialogs/ProgressDialog.java283
-rw-r--r--framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/forms/SizeCachingComposite.java40
93 files changed, 14987 insertions, 0 deletions
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/.classpath b/framework/com.atlassian.connector.eclipse.crucible.ui/.classpath
new file mode 100644
index 0000000..21e1584
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/.classpath
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins">
+ <accessrules>
+ <accessrule kind="accessible" pattern="org/eclipse/mylyn/internal/provisional/**"/>
+ </accessrules>
+ </classpathentry>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/.project b/framework/com.atlassian.connector.eclipse.crucible.ui/.project
new file mode 100644
index 0000000..96e1b76
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/.project
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>com.atlassian.connector.eclipse.crucible.ui</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.api.tools.apiAnalysisBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.atlassw.tools.eclipse.checkstyle.CheckstyleBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.pde.api.tools.apiAnalysisNature</nature>
+ <nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
+ <nature>com.atlassw.tools.eclipse.checkstyle.CheckstyleNature</nature>
+ </natures>
+</projectDescription>
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/META-INF/MANIFEST.MF b/framework/com.atlassian.connector.eclipse.crucible.ui/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..56cb711
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/META-INF/MANIFEST.MF
@@ -0,0 +1,54 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Atlassian Connector for Eclipse Crucible UI
+Bundle-SymbolicName: com.atlassian.connector.eclipse.crucible.ui;singleton:=true
+Bundle-Version: 2.3.0.qualifier
+Require-Bundle: org.eclipse.core.runtime,
+ org.eclipse.ui,
+ org.eclipse.mylyn.tasks.core;bundle-version="[3.2.0,4.0.0)",
+ org.eclipse.mylyn.tasks.ui;bundle-version="[3.2.0,4.0.0)",
+ org.eclipse.mylyn.wikitext.core;bundle-version="[1.2.0,2.0.0)",
+ org.eclipse.mylyn.wikitext.tasks.ui;bundle-version="[1.2.0,2.0.0)",
+ org.eclipse.mylyn.wikitext.confluence.core;bundle-version="[1.2.0,2.0.0)",
+ org.eclipse.ui.forms,
+ org.eclipse.mylyn.commons.ui;bundle-version="[3.2.0,4.0.0)",
+ org.eclipse.jface.text,
+ org.eclipse.ui.editors,
+ org.eclipse.ui.ide,
+ org.eclipse.jdt.ui;resolution:=optional,
+ org.eclipse.core.resources,
+ org.eclipse.mylyn.commons.core,
+ org.eclipse.mylyn.monitor.ui,
+ org.eclipse.compare,
+ org.eclipse.mylyn.commons.net,
+ org.eclipse.core.expressions,
+ org.eclipse.team.ui,
+ org.apache.commons.io,
+ org.eclipse.ui.views.log;resolution:=optional,
+ org.eclipse.ui.workbench.texteditor,
+ org.eclipse.core.filesystem,
+ com.atlassian.connector.eclipse.team.ui;bundle-version="2.2.0",
+ com.atlassian.connector.eclipse.model;bundle-version="2.2.0"
+Bundle-Activator: com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin
+Bundle-ActivationPolicy: lazy
+Bundle-RequiredExecutionEnvironment: J2SE-1.5
+Export-Package: com.atlassian.connector.commons.crucible.api.model,
+ com.atlassian.connector.eclipse.internal.crucible;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.core;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.core.client;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.core.client.model;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui;x-friends:="com.atlassian.connector.eclipse.fisheye.ui",
+ com.atlassian.connector.eclipse.internal.crucible.ui.actions;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.annotations;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.commons;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.editor.ruler;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.operations;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.util;x-internal:=true,
+ com.atlassian.connector.eclipse.internal.crucible.ui.wizards;x-internal:=true,
+ com.atlassian.connector.eclipse.ui.actions,
+ com.atlassian.connector.eclipse.ui.commons,
+ com.atlassian.connector.eclipse.ui.dialogs,
+ com.atlassian.connector.eclipse.ui.forms
+Bundle-Vendor: Atlassian
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/about.html b/framework/com.atlassian.connector.eclipse.crucible.ui/about.html
new file mode 100644
index 0000000..d774b07
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/about.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
+<html>
+<head>
+<title>About</title>
+<meta http-equiv=Content-Type content="text/html; charset=ISO-8859-1">
+</head>
+<body lang="EN-US">
+<h2>About This Content</h2>
+
+<p>June 25, 2008</p>
+<h3>License</h3>
+
+<p>The Eclipse Foundation makes available all content in this plug-in (&quot;Content&quot;). Unless otherwise
+indicated below, the Content is provided to you under the terms and conditions of the
+Eclipse Public License Version 1.0 (&quot;EPL&quot;). A copy of the EPL is available
+at <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
+For purposes of the EPL, &quot;Program&quot; will mean the Content.</p>
+
+<p>If you did not receive this Content directly from the Eclipse Foundation, the Content is
+being redistributed by another party (&quot;Redistributor&quot;) and different terms and conditions may
+apply to your use of any object code in the Content. Check the Redistributor's license that was
+provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
+indicated below, the terms and conditions of the EPL still apply to any source code in the Content
+and such source code may be obtained at <a href="/">http://www.eclipse.org</a>.</p>
+
+</body>
+</html> \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/build.properties b/framework/com.atlassian.connector.eclipse.crucible.ui/build.properties
new file mode 100644
index 0000000..1af8807
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/build.properties
@@ -0,0 +1,8 @@
+bin.includes = META-INF/,\
+ plugin.xml,\
+ .,\
+ icons/,\
+ about.html
+jars.compile.order = .
+source.. = src/
+output.. = bin/
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/plugin.xml b/framework/com.atlassian.connector.eclipse.crucible.ui/plugin.xml
new file mode 100644
index 0000000..fe854a6
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/plugin.xml
@@ -0,0 +1,111 @@
+<plugin>
+ <!-- Menus -->
+ <extension point="org.eclipse.ui.popupMenus">
+ <objectContribution
+ objectClass="com.atlassian.connector.eclipse.ui.commons.IEditorResource"
+ adaptable="true"
+ id="com.atlassian.connector.eclipse.crucible.ui.ReviewResource2">
+ <action
+ class="com.atlassian.connector.eclipse.internal.crucible.ui.actions.CreateReviewFromResourcesAction"
+ id="com.atlassian.connector.eclipse.internal.crucible.ui.CreateReviewAction2"
+ menubarPath="team.main/group10"
+ icon="icons/obj16/crucible.png"
+ label="Create Review"
+ tooltip="Create Review">
+ </action>
+ </objectContribution>
+ </extension>
+
+ <extension id="com.atlassian.connector.eclipse.crucible.ui.popup" name="Crucible Review Actions" point="org.eclipse.ui.popupMenus">
+ <objectContribution id="com.atlassian.connector.eclipse.crucible.ui.objectContribution1" objectClass="org.eclipse.ui.IEditorInput">
+ <action class="com.atlassian.connector.eclipse.internal.crucible.ui.actions.AddLineCommentToFileAction"
+ enablesFor="1" icon="icons/obj16/pin_addcomment.png" id="com.atlassian.connector.eclipse.crucible.ui.action.add.line.comment"
+ label="Comment on Selected Lines..." menubarPath="group.undo" tooltip="Add Line Comment to Active Review">
+ </action>
+ <action class="com.atlassian.connector.eclipse.internal.crucible.ui.actions.AddGeneralCommentToFileAction"
+ enablesFor="1" id="com.atlassian.connector.eclipse.crucible.ui.action.add.file.comment"
+ label="Add General File Comment..." menubarPath="group.undo" tooltip="Add Comment to Active Review">
+ </action>
+ <visibility>
+ <systemProperty name="com.atlassian.connector.eclipse.crucible.ui.review.active" value="true"/>
+ </visibility>
+ </objectContribution>
+
+ </extension>
+
+ <extension point="org.eclipse.ui.editors.annotationTypes">
+ <type name="com.atlassian.connector.eclipse.cruicible.ui.comment.annotation"/>
+ </extension>
+
+ <extension point="org.eclipse.ui.editors.markerAnnotationSpecification">
+ <specification annotationType="com.atlassian.connector.eclipse.cruicible.ui.comment.annotation"
+ colorPreferenceKey="comment_color"
+ colorPreferenceValue="179,215,255"
+ contributesToHeader="true"
+ highlightPreferenceKey="comment_highlight"
+ highlightPreferenceValue="true"
+ icon="/icons/obj16/crucible.png"
+ includeOnPreferencePage="true"
+ isGoToNextNavigationTarget="false"
+ isGoToNextNavigationTargetKey="comment_isGoToNextNavigationTargetKey"
+ isGoToPreviousNavigationTarget="false"
+ isGoToPreviousNavigationTargetKey="commet_isGoToPreviousNavigationTargetKey"
+ label="Active Review Comments"
+ overviewRulerPreferenceKey="comment_overviewRuler"
+ overviewRulerPreferenceValue="true"
+ presentationLayer="0"
+ showInNextPrevDropdownToolbarAction="false"
+ showInNextPrevDropdownToolbarActionKey="comment_showInNextPrevDropdownToolbarAction"
+ textPreferenceKey="comment_text"
+ textPreferenceValue="true"
+ textStylePreferenceKey="comment_stylePreferences"
+ textStylePreferenceValue="BOX"
+ verticalRulerPreferenceKey="comment_verticalRuler"
+ verticalRulerPreferenceValue="true" />
+ </extension>
+
+<!-- no preferences for Crucible
+ <extension point="org.eclipse.ui.preferencePages">
+ <page category="com.atlassian.connector.eclipse.ui.preferences.AtlassianPreferencePage"
+ class="com.atlassian.connector.eclipse.crucible.ui.preferences.CruciblePreferencePage"
+ id="com.atlassian.connector.eclipse.crucible.ui.CruciblePreferencePage" name="Crucible">
+ </page>
+ </extension>
+ <extension point="org.eclipse.core.runtime.preferences">
+ <initializer class="com.atlassian.connector.eclipse.crucible.ui.preferences.PreferenceInitializer">
+ </initializer>
+ </extension>
+-->
+
+
+ <extension
+ point="org.eclipse.core.runtime.adapters">
+ <!-- I declare that I can adapt from IAdaptable in order actually to make it work in as many contexts as possible -
+ not only for instanceofs IResource, but also for CompilationUnit, IJavaProject, etc. which do adapt to IResource, but
+ do not implement this interface. It also handles IEditorInput -->
+ <factory
+ adaptableType="org.eclipse.core.runtime.IAdaptable"
+ class="com.atlassian.connector.eclipse.ui.commons.EditorResourceAdapterFactory">
+ <adapter
+ type="com.atlassian.connector.eclipse.ui.commons.IEditorResource">
+ </adapter>
+ </factory>
+ </extension>
+
+ <extension point="org.eclipse.ui.workbench.texteditor.rulerColumns">
+ <column id="com.atlassian.connector.eclipse.crucible.ui.editor.annotationRuler" name="Crucible Comments"
+ icon="/icons/obj16/crucible.png"
+ class="com.atlassian.connector.eclipse.internal.crucible.ui.editor.ruler.CommentAnnotationRulerColumn"
+ enabled="true"
+ global="true"
+ includeInMenu="true">
+
+ <placement gravity="1.0">
+ <!--<after id="org.eclipse.ui.editors.columns.annotations"/> -->
+ </placement>
+
+ <targetClass class="org.eclipse.ui.texteditor.AbstractDecoratedTextEditor" />
+ </column>
+ </extension>
+
+</plugin>
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/commons/crucible/api/model/ReviewModelUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/commons/crucible/api/model/ReviewModelUtil.java
new file mode 100644
index 0000000..918bb8f
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/commons/crucible/api/model/ReviewModelUtil.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.commons.crucible.api.model;
+
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+public final class ReviewModelUtil {
+
+ private ReviewModelUtil() {
+
+ }
+
+ // VersionedComment -> Coment -> Comment -> ***
+ // GeneralComment -> Coment -> Comment -> ***
+ public static VersionedComment getParentVersionedComment(Comment comment) {
+ while (comment != null && !(comment instanceof VersionedComment)) {
+ comment = comment.getParentComment();
+ }
+ return (VersionedComment) comment;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/IReviewChangeListenerAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/IReviewChangeListenerAction.java
new file mode 100644
index 0000000..639b20e
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/IReviewChangeListenerAction.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible;
+
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.jface.action.IAction;
+
+/**
+ *
+ * @author Jacek Jaroczynski
+ */
+public interface IReviewChangeListenerAction extends IAction {
+ void updateReview(Review updatedReview, CrucibleFileInfo updatedFile);
+
+ void updateReview(Review updatedReview, CrucibleFileInfo updatedFile, VersionedComment updatedComment);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleConstants.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleConstants.java
new file mode 100644
index 0000000..dd3b890
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleConstants.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.core;
+
+/**
+ * This is a class to encapsulate all of the constants used in the crucible connector
+ *
+ * @author Shawn Minto
+ */
+public final class CrucibleConstants {
+
+ public static final String REVIEW_ACTIVE_SYSTEM_PROPERTY = "com.atlassian.connector.eclipse.crucible.ui.review.active";
+
+ public static final String CRUCIBLE_EDITOR_PAGE_ID = "com.atlassian.connector.eclipse.crucible.review.editor";
+
+ private CrucibleConstants() {
+ // ignore
+ }
+
+ public static final String CLASSIFICATION_CUSTOM_FIELD_KEY = "classification";
+
+ public static final String CRUCIBLE_URL_START = "cru/";
+
+ public static final String CUSTOM_FILER_START = CRUCIBLE_URL_START + "?filter=custom&";
+
+ public static final String KEY_FILTER_ID = "FilterId";
+
+ public static final String PREDEFINED_FILER_START = CRUCIBLE_URL_START + "?filter=";
+
+ public static final String RANK_CUSTOM_FIELD_KEY = "rank";
+
+ public static final String HAS_CHANGED_TASKDATA_KEY = "hasChanged";
+
+ public static final String CHANGED_HASH_CODE_KEY = "hasChangedHash";
+
+ public static final String HAS_NOTIFIED_NEW = "hasNotifiedNew";
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleUtil.java
new file mode 100644
index 0000000..790c7d9
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/CrucibleUtil.java
@@ -0,0 +1,390 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.core;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.theplugin.commons.crucible.api.model.BasicReview;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomField;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.Reviewer;
+import com.atlassian.theplugin.commons.crucible.api.model.State;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.tasks.core.TasksUtil;
+import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utility methods for dealing with Crucible
+ *
+ * @author Shawn Minto
+ */
+public final class CrucibleUtil {
+
+ private static final int FALSE_HASH_MAGIC = 1237;
+
+ private static final int TRUE_HASH_MAGIC = 1231;
+
+ private CrucibleUtil() {
+ }
+
+ public static String getPermIdFromTaskId(String taskId) {
+ if (!taskId.contains("%")) {
+ // this means that it was already encoded
+ return taskId;
+ }
+ return TasksUtil.decode(taskId);
+ }
+
+ public static String getTaskIdFromPermId(String permId) {
+ if (permId.contains("%")) {
+ // this means that it was already encoded
+ return permId;
+ }
+ return TasksUtil.encode(permId);
+ }
+
+ public static String getPredefinedFilterWebUrl(String repositoryUrl, String filterId) {
+ String url = addTrailingSlash(repositoryUrl);
+ url += CrucibleConstants.PREDEFINED_FILER_START + filterId;
+ return url;
+ }
+
+ public static String addTrailingSlash(String repositoryUrl) {
+ if (repositoryUrl.endsWith("/")) {
+ return repositoryUrl;
+ } else {
+ return repositoryUrl + "/";
+ }
+ }
+
+ public static String getReviewUrl(String repositoryUrl, String taskId) {
+ // TODO handle both taskid and task key
+ String url = addTrailingSlash(repositoryUrl);
+ url += CrucibleConstants.CRUCIBLE_URL_START + getPermIdFromTaskId(taskId);
+ return url;
+ }
+
+ public static String getTaskIdFromUrl(String taskFullUrl) {
+ int index = taskFullUrl.indexOf(CrucibleConstants.CRUCIBLE_URL_START);
+ if (index != -1 && index + CrucibleConstants.CRUCIBLE_URL_START.length() < taskFullUrl.length()) {
+ String permId = taskFullUrl.substring(index + CrucibleConstants.CRUCIBLE_URL_START.length());
+ if (permId.contains("/")) {
+ // this isnt the url of the task
+ return null;
+ } else {
+ return getTaskIdFromPermId(permId);
+ }
+ }
+ return null;
+ }
+
+ public static String getRepositoryUrlFromUrl(String taskFullUrl) {
+ int index = taskFullUrl.indexOf(CrucibleConstants.CRUCIBLE_URL_START);
+ if (index != -1) {
+ return taskFullUrl.substring(0, index);
+ }
+ return null;
+ }
+
+ public static boolean isFilterDefinition(IRepositoryQuery query) {
+ String filterId = query.getAttribute(CrucibleConstants.KEY_FILTER_ID);
+ return filterId == null || filterId.length() == 0;
+ }
+
+ public static State[] getStatesFromString(String statesString) {
+ Set<State> states = new HashSet<State>();
+ String[] statesArray = statesString.split(",");
+ for (String stateString : statesArray) {
+ if (stateString.trim().length() == 0) {
+ continue;
+ }
+ try {
+ State state = State.fromValue(stateString);
+ if (state != null) {
+ states.add(state);
+ }
+ } catch (IllegalArgumentException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ }
+ }
+ return states.toArray(new State[0]);
+ }
+
+ public static String getTaskIdFromReview(BasicReview review) {
+ String key = review.getPermId().getId();
+ return CrucibleUtil.getTaskIdFromPermId(key);
+ }
+
+ public static int createHash(Review review) {
+
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (review.isAllowReviewerToJoin() ? TRUE_HASH_MAGIC : FALSE_HASH_MAGIC);
+ result = prime * result + ((review.getAuthor() == null) ? 0 : review.getAuthor().getUsername().hashCode());
+ result = prime * result + ((review.getCloseDate() == null) ? 0 : review.getCloseDate().hashCode());
+ result = prime * result + ((review.getCreateDate() == null) ? 0 : review.getCreateDate().hashCode());
+ result = prime * result + ((review.getCreator() == null) ? 0 : review.getCreator().getUsername().hashCode());
+ result = prime * result + ((review.getProjectKey() == null) ? 0 : review.getProjectKey().hashCode());
+ result = prime * result + ((review.getDescription() == null) ? 0 : review.getDescription().hashCode());
+
+ int miniResult = 0;
+ for (CrucibleFileInfo file : review.getFiles()) {
+ miniResult += ((file.getFileDescriptor() == null) ? 0 : file.getFileDescriptor().getUrl().hashCode());
+ for (VersionedComment comment : file.getVersionedComments()) {
+ miniResult = createHashForVersionedComment(miniResult, comment);
+ }
+ }
+ result = prime * result + miniResult;
+
+ miniResult = 0;
+ for (Comment comment : review.getGeneralComments()) {
+ miniResult = createHashForGeneralComment(miniResult, comment);
+ }
+ result = prime * result + miniResult;
+
+ result = prime * result
+ + ((review.getModerator() == null) ? 0 : review.getModerator().getUsername().hashCode());
+ result = prime * result + ((review.getName() == null) ? 0 : review.getName().hashCode());
+ result = prime * result
+ + ((review.getParentReview() == null) ? 0 : review.getParentReview().getId().hashCode());
+ result = prime * result + ((review.getPermId() == null) ? 0 : review.getPermId().getId().hashCode());
+ result = prime * result + ((review.getProjectKey() == null) ? 0 : review.getProjectKey().hashCode());
+ result = prime * result + ((review.getRepoName() == null) ? 0 : review.getRepoName().hashCode());
+
+ miniResult = 0;
+ for (Reviewer reviewer : review.getReviewers()) {
+ miniResult += (reviewer.getUsername().hashCode());
+ miniResult += (reviewer.isCompleted() ? TRUE_HASH_MAGIC : FALSE_HASH_MAGIC);
+ }
+ result = prime * result + miniResult;
+
+ result = prime * result + ((review.getState() == null) ? 0 : review.getState().name().hashCode());
+ result = prime * result + ((review.getSummary() == null) ? 0 : review.getSummary().hashCode());
+
+ return result;
+ }
+
+ private static int createHashForGeneralComment(int result, Comment comment) {
+
+ result += (comment.isDraft() ? TRUE_HASH_MAGIC : FALSE_HASH_MAGIC);
+ result += ((comment.getMessage() == null) ? 0 : comment.getMessage().hashCode());
+ result += ((comment.getAuthor() == null) ? 0 : comment.getAuthor().getUsername().hashCode());
+ result += ((comment.getCreateDate() == null) ? 0 : comment.getCreateDate().hashCode());
+ result += ((comment.getPermId() == null) ? 0 : comment.getPermId().getId().hashCode());
+
+ for (CustomField customValue : comment.getCustomFields().values()) {
+ result += ((customValue == null) ? 0 : customValue.getValue().hashCode());
+ result += ((customValue == null) ? 0 : customValue.getConfigVersion());
+ }
+
+ for (Comment reply : comment.getReplies()) {
+ result = createHashForGeneralComment(result, reply);
+ }
+
+ return result;
+ }
+
+ private static int createHashForVersionedComment(int result, VersionedComment comment) {
+
+ result += comment.getFromEndLine();
+ result += comment.getFromStartLine();
+ result += comment.getToEndLine();
+ result += comment.getToStartLine();
+ result += comment.getLineRanges() != null ? comment.getLineRanges().hashCode() : 0;
+ result += (comment.isDraft() ? TRUE_HASH_MAGIC : FALSE_HASH_MAGIC);
+ result += ((comment.getMessage() == null) ? 0 : comment.getMessage().hashCode());
+ result += ((comment.getAuthor() == null) ? 0 : comment.getAuthor().getUsername().hashCode());
+ result += ((comment.getCreateDate() == null) ? 0 : comment.getCreateDate().hashCode());
+ result += ((comment.getPermId() == null) ? 0 : comment.getPermId().getId().hashCode());
+
+ for (CustomField customValue : comment.getCustomFields().values()) {
+ result += ((customValue == null) ? 0 : customValue.getValue().hashCode());
+ result += ((customValue == null) ? 0 : customValue.getConfigVersion());
+ }
+
+ for (Comment reply : comment.getReplies()) {
+ if (reply instanceof VersionedComment) {
+ result = createHashForVersionedComment(result, (VersionedComment) reply);
+ } else {
+ result = createHashForGeneralComment(result, comment);
+ }
+ }
+
+ return result;
+ }
+
+ public static boolean canAddCommentToReview(Review review) {
+ return true;
+ }
+
+ public static boolean isCompleted(Review review) {
+ State state = review.getState();
+ return state == State.ABANDONED || state == State.CLOSED || state == State.DEAD || state == State.REJECTED;
+ }
+
+ public static boolean isUserCompleted(String userName, Review review) {
+ for (Reviewer reviewer : review.getReviewers()) {
+ if (reviewer.getUsername().equals(userName)) {
+ return reviewer.isCompleted();
+ }
+ }
+ return false;
+ }
+
+ // TODO add a param for whether it should be a deep comaparison?
+ public static boolean areVersionedCommentsDeepEquals(VersionedComment c1, VersionedComment c2) {
+ if (c1 == c2) {
+ return true;
+ }
+
+ if (!c1.equals(c2)) {
+ return false;
+ }
+
+ if (!areCommentsEqual(c1, c2)) {
+ return false;
+ }
+
+ if (c1.getReplies() != null ? !c1.getReplies().equals(c2.getReplies()) : c2.getReplies() != null) {
+ return false;
+ }
+
+ if (c1.getReplies().size() != c2.getReplies().size()) {
+ return false;
+ }
+
+ for (Comment vc1 : c1.getReplies()) {
+ boolean found = false;
+ for (Comment vc2 : c2.getReplies()) {
+ if (vc1.getPermId() == vc2.getPermId()
+ && areVersionedCommentsDeepEquals((VersionedComment) vc1, (VersionedComment) vc2)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean areCommentsEqual(Comment c1, Comment c2) {
+ if (c1.isDefectApproved() != c2.isDefectApproved()) {
+ return false;
+ }
+ if (c1.isDefectRaised() != c2.isDefectRaised()) {
+ return false;
+ }
+ if (c1.isDeleted() != c2.isDeleted()) {
+ return false;
+ }
+ if (c1.isDraft() != c2.isDraft()) {
+ return false;
+ }
+ if (c1.isReply() != c2.isReply()) {
+ return false;
+ }
+ if (c1.getAuthor() != null ? !c1.getAuthor().equals(c2.getAuthor()) : c2.getAuthor() != null) {
+ return false;
+ }
+ if (c1.getCreateDate() != null ? !c1.getCreateDate().equals(c2.getCreateDate()) : c2.getCreateDate() != null) {
+ return false;
+ }
+ if (c1.getCustomFields() != null ? !c1.getCustomFields().equals(c2.getCustomFields())
+ : c2.getCustomFields() != null) {
+ return false;
+ }
+ if (c1.getMessage() != null ? !c1.getMessage().equals(c2.getMessage()) : c2.getMessage() != null) {
+ return false;
+ }
+ if (c1.getPermId() != null ? !c1.getPermId().equals(c2.getPermId()) : c2.getPermId() != null) {
+ return false;
+ }
+ return true;
+ }
+
+ // TODO add a param for whether it should be a deep comaparison?
+ public static boolean areCrucibleFilesDeepEqual(CrucibleFileInfo file, CrucibleFileInfo file2) {
+ if (file.getPermId() != null ? !file.getPermId().getId().equals(file2.getPermId().getId())
+ : file2.getPermId() != null) {
+ return false;
+ }
+
+ if (file.getNumberOfComments() != file2.getNumberOfComments()) {
+ return false;
+ }
+ for (VersionedComment comment : file.getVersionedComments()) {
+ boolean found = false;
+ for (VersionedComment comment2 : file2.getVersionedComments()) {
+ if (CrucibleUtil.areVersionedCommentsDeepEquals(comment, comment2)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // TODO add a param for whether it should be a deep comaparison?
+ public static boolean areGeneralCommentsDeepEquals(Comment c1, Comment c2) {
+ if (c1 == c2) {
+ return true;
+ }
+
+ if (!areCommentsEqual(c1, c2)) {
+ return false;
+ }
+
+ if (c1.getReplies() != null ? !c1.getReplies().equals(c2.getReplies()) : c2.getReplies() != null) {
+ return false;
+ }
+
+ if (c1.getReplies().size() != c2.getReplies().size()) {
+ return false;
+ }
+
+ for (Comment vc1 : c1.getReplies()) {
+ boolean found = false;
+ for (Comment vc2 : c2.getReplies()) {
+ if (vc1.getPermId() == vc2.getPermId() && areGeneralCommentsDeepEquals(vc1, vc2)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static boolean canPublishDraft(Comment comment) {
+ return (comment.isDraft() && !comment.hasDraftParents());
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/TaskRepositoryUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/TaskRepositoryUtil.java
new file mode 100644
index 0000000..72efd4b
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/TaskRepositoryUtil.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.core;
+
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+
+import java.net.URI;
+import java.util.Map;
+
+public final class TaskRepositoryUtil {
+
+ private static final String FAILED_TO_DE_SERIALIZE_MAPPINGS = "Failed to de-serialize mappings";
+
+ private static final String TASK_REPOSITORY_SCM_MAPPINGS_KEY = "com.atlassian.connector.eclipse.crucible.core.scmRepositoryMappings";
+
+ private TaskRepositoryUtil() {
+ }
+
+ /**
+ * Returns a mapping between SCM urls and Crucible/FishEye source repositories. It's is persisted as a
+ * TaskRepository property.
+ *
+ * @param taskRepository
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public static Map<String, String> getScmRepositoryMappings(TaskRepository taskRepository) {
+ String property = taskRepository.getProperty(TASK_REPOSITORY_SCM_MAPPINGS_KEY);
+ return MiscUtil.buildHashMap();
+ }
+
+ public static void setScmRepositoryMappings(TaskRepository taskRepository, Map<String, String> mappings) {
+ }
+
+ public static Map.Entry<String, String> getMatchingSourceRepository(Map<String, String> repositories, String scmPath) {
+
+ Map.Entry<String, String> matching = null;
+ for (Map.Entry<String, String> prefix : repositories.entrySet()) {
+ try {
+ URI prefixUri = URI.create(prefix.getKey()).normalize();
+ URI scmUri = URI.create(scmPath).normalize();
+ if (scmUri.toString().startsWith(prefixUri.toString())) {
+ if (matching == null || prefix.getKey().length() > matching.getKey().length()) {
+ matching = prefix;
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ // in case mapping key is not a proper URL (i.e. CVS) compare strings
+ if (scmPath.startsWith(prefix.getKey())) {
+ if (matching == null || prefix.getKey().length() > matching.getKey().length()) {
+ matching = prefix;
+ }
+ }
+ }
+ }
+ return matching;
+ }
+
+ public static Map.Entry<String, String> getNamedSourceRepository(Map<String, String> repositories, String name) {
+ for (Map.Entry<String, String> entry : repositories.entrySet()) {
+ if (entry.getValue().equals(name)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ public static void setScmRepositoryMapping(TaskRepository repository, String scmPath, String value) {
+ Map<String, String> mappings = getScmRepositoryMappings(repository);
+ mappings.put(scmPath, value);
+ setScmRepositoryMappings(repository, mappings);
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/CrucibleClient.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/CrucibleClient.java
new file mode 100644
index 0000000..e0a4500
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/CrucibleClient.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.core.client;
+
+import com.atlassian.theplugin.commons.crucible.api.model.BasicReview;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleAction;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleVersionInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.core.data.TaskData;
+
+/**
+ * Bridge between Mylyn and the ACC API's
+ *
+ * @author Shawn Minto
+ * @author Thomas Ehrnhoefer
+ * @author Wojciech Seliga
+ */
+public class CrucibleClient {
+
+ public CrucibleClient() {
+ }
+
+ public TaskData getTaskData(TaskRepository taskRepository, final String taskId, IProgressMonitor monitor)
+ throws CoreException {
+ return null;
+ }
+
+ public Review getReview(TaskRepository repository, String taskId, boolean getWorkingCopy, IProgressMonitor monitor)
+ throws CoreException {
+ return null;
+ }
+
+ public CrucibleVersionInfo updateVersionInfo(IProgressMonitor monitor, TaskRepository taskRepository)
+ throws CoreException {
+ return null;
+ }
+
+ public void updateRepositoryData(IProgressMonitor monitor, TaskRepository taskRepository) throws CoreException {
+ }
+
+ public void updateProjectDetails(IProgressMonitor monitor, TaskRepository taskRepository, final String projectKey)
+ throws CoreException {
+ }
+
+ public Review changeReviewState(final BasicReview review, final CrucibleAction action, TaskRepository repository,
+ IProgressMonitor progressMonitor) throws CoreException {
+ return null;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/model/IReviewCacheListener.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/model/IReviewCacheListener.java
new file mode 100644
index 0000000..f3a9714
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/core/client/model/IReviewCacheListener.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.core.client.model;
+
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.notification.CrucibleNotification;
+
+import java.util.List;
+
+/**
+ * Listener for cache model changes
+ *
+ * @author Shawn Minto
+ */
+public interface IReviewCacheListener {
+
+ void reviewUpdated(String repositoryUrl, String taskId, Review review, List<CrucibleNotification> differences);
+
+ void reviewAdded(String repositoryUrl, String taskId, Review review);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ActiveReviewManager.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ActiveReviewManager.java
new file mode 100644
index 0000000..7b7a072
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ActiveReviewManager.java
@@ -0,0 +1,281 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleConstants;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.model.IReviewCacheListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleAnnotationModelManager;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.notification.CrucibleNotification;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.jobs.JobChangeAdapter;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.tasks.ui.util.TasksUiInternal;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.ITaskActivationListener;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.core.sync.SynchronizationJob;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class to manage the currently active review for other models
+ *
+ * @author sminto
+ */
+public class ActiveReviewManager implements ITaskActivationListener, IReviewCacheListener {
+
+ /**
+ * All methods are never called from UI thread.
+ *
+ * @author pniewiadomski
+ */
+ public interface IReviewActivationListener {
+ void reviewActivated(ITask task, Review review);
+
+ void reviewDeactivated(ITask task, Review review);
+
+ void reviewUpdated(ITask task, Review review, Collection<CrucibleNotification> differences);
+ };
+
+ private static final long ACTIVE_REVIEW_POLLING_INTERVAL = 120000L;
+
+ private final JobChangeAdapter refreshJobRescheduler = new JobChangeAdapter() {
+ @Override
+ public void done(IJobChangeEvent event) {
+ synchronizeJob = null;
+ createAndScheduleRefreshJob();
+ }
+ };
+
+ private final List<IReviewActivationListener> activationListeners;
+
+ private Review activeReview;
+
+ private ITask activeTask;
+
+ private SynchronizationJob synchronizeJob;
+
+ private final boolean increasedRefresh;
+
+ private final Map<ITask, Review> reviewByTask = new HashMap<ITask, Review>();
+
+ public ActiveReviewManager(boolean increasedRefresh) {
+ this.activationListeners = MiscUtil.buildArrayList();
+ this.increasedRefresh = increasedRefresh;
+ }
+
+ public synchronized void addReviewActivationListener(IReviewActivationListener l) {
+ activationListeners.add(l);
+ }
+
+ public synchronized void removeReviewActivationListener(IReviewActivationListener l) {
+ activationListeners.remove(l);
+ }
+
+ private synchronized void fireReviewActivated(final ITask task, final Review review) {
+ for (final IReviewActivationListener l : activationListeners) {
+ l.reviewActivated(task, review);
+ }
+ }
+
+ private synchronized void fireReviewDectivated(final ITask task, final Review review) {
+ for (final IReviewActivationListener l : activationListeners) {
+ l.reviewDeactivated(task, review);
+ }
+ }
+
+ private synchronized void fireReviewUpdated(final ITask task, final Review review,
+ Collection<CrucibleNotification> differences) {
+ for (final IReviewActivationListener l : activationListeners) {
+ l.reviewUpdated(task, review, differences);
+ }
+ }
+
+ public void dispose() {
+ }
+
+ public synchronized void taskActivated(ITask task) {
+ System.setProperty(CrucibleConstants.REVIEW_ACTIVE_SYSTEM_PROPERTY, "true");
+
+ this.activeTask = task;
+ this.activeReview = reviewByTask.get(task);
+ if (activeReview != null) {
+ scheduleDownloadJob(task);
+
+ if (increasedRefresh) {
+ startIncreasedChangePolling();
+ }
+ fireReviewActivated(task, activeReview);
+ }
+ }
+
+ public synchronized void taskDeactivated(ITask task) {
+ Review oldReview = this.activeReview;
+ ITask oldTask = this.activeTask;
+
+ this.activeTask = null;
+ this.activeReview = null;
+ System.setProperty(CrucibleConstants.REVIEW_ACTIVE_SYSTEM_PROPERTY, "false");
+ stopIncreasedChangePolling();
+
+ fireReviewDectivated(oldTask, oldReview);
+ }
+
+ public void preTaskActivated(ITask task) {
+ // ignore
+ }
+
+ public synchronized void preTaskDeactivated(ITask task) {
+ // ignore
+ }
+
+ private synchronized void activeReviewUpdated(Review cachedReview, ITask task,
+ Collection<CrucibleNotification> differences) {
+ if (activeTask != null && task != null && activeTask.equals(task)) {
+ reviewByTask.put(task, cachedReview);
+ if (activeReview == null) {
+ this.activeReview = cachedReview;
+ fireReviewActivated(activeTask, activeReview);
+ } else {
+ this.activeReview = cachedReview;
+ CrucibleAnnotationModelManager.updateAllOpenEditors(activeReview);
+ fireReviewUpdated(activeTask, activeReview, differences);
+ }
+ }
+ }
+
+ public synchronized Review getActiveReview() {
+ return activeReview;
+ }
+
+ public synchronized ITask getActiveTask() {
+ return activeTask;
+ }
+
+ private void startIncreasedChangePolling() {
+ createAndScheduleRefreshJob();
+ }
+
+ private synchronized void createAndScheduleRefreshJob() {
+ if (synchronizeJob == null && getActiveTask() != null) {
+ Set<ITask> tasks = new HashSet<ITask>();
+ tasks.add(getActiveTask());
+// synchronizeJob = TasksUiPlugin.getTaskJobFactory().createSynchronizeTasksJob(
+// CrucibleCorePlugin.getRepositoryConnector(), tasks);
+// synchronizeJob.setUser(false);
+// synchronizeJob.addJobChangeListener(refreshJobRescheduler);
+// synchronizeJob.schedule(ACTIVE_REVIEW_POLLING_INTERVAL);
+ }
+ }
+
+ private void stopIncreasedChangePolling() {
+ if (synchronizeJob != null) {
+ synchronizeJob.removeJobChangeListener(refreshJobRescheduler);
+ synchronizeJob.cancel();
+ synchronizeJob = null;
+ }
+ }
+
+ private void scheduleDownloadJob(final ITask task) {
+ Job downloadJob = new Job("Retrieving review from server") {
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ IStatus errorStatus = null;
+ try {
+ TaskRepository repository = CrucibleUiUtil.getCrucibleTaskRepository(task.getRepositoryUrl());
+
+ if (repository != null) {
+ String taskId = task.getTaskId();
+
+ CrucibleClient client = CrucibleUiPlugin.getClient(repository);
+ if (client != null) {
+ // This should fire off a listener that we listen to to update the review properly
+ client.getReview(repository, taskId, false, monitor);
+ } else {
+ errorStatus = new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to get crucible client for repository");
+
+ }
+ } else {
+ errorStatus = new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Crucible repository does not exist");
+
+ }
+ } catch (CoreException e) {
+ errorStatus = e.getStatus();
+
+ } finally {
+ if (errorStatus != null && !errorStatus.isOK()) {
+ StatusHandler.log(errorStatus);
+ TasksUiInternal.asyncDisplayStatus("Unable to retrieve Review", errorStatus);
+ }
+ }
+ return Status.OK_STATUS;
+ }
+ };
+ downloadJob.setUser(true);
+ downloadJob.setPriority(Job.INTERACTIVE);
+ downloadJob.schedule();
+ }
+
+ public synchronized boolean isReviewActive() {
+ return activeTask != null && activeReview != null;
+ }
+
+ public void reviewAdded(String repositoryUrl, String taskId, Review review) {
+ if (activeTask != null) {
+ if (activeTask.getRepositoryUrl().equals(repositoryUrl) && activeTask.getTaskId().equals(taskId)) {
+ activeReviewUpdated(review, activeTask, Collections.<CrucibleNotification> emptyList());
+ }
+ }
+ }
+
+ public synchronized void reviewUpdated(String repositoryUrl, String taskId, Review review,
+ List<CrucibleNotification> differences) {
+ if (activeTask != null) {
+ if (activeTask.getRepositoryUrl().equals(repositoryUrl) && activeTask.getTaskId().equals(taskId)) {
+ activeReviewUpdated(review, activeTask, differences);
+ }
+ }
+ }
+
+ /**
+ * public for testing only!
+ *
+ * @param review
+ * @param task
+ */
+ public void setActiveReview(Review review, ITask task) {
+ this.activeTask = task;
+ this.activeReview = review;
+ }
+
+ public void activeReviewUpdated() {
+ activeReviewUpdated(activeReview, activeTask, Collections.<CrucibleNotification> emptyList());
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/AvatarImages.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/AvatarImages.java
new file mode 100644
index 0000000..10ca43d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/AvatarImages.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+
+import org.apache.commons.io.IOUtils;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.ImageRegistry;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.services.IDisposable;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public final class AvatarImages implements IDisposable {
+
+ public enum AvatarSize {
+ ORIGINAL, SMALL, LARGE
+ }
+
+ private static ImageRegistry imageRegistry;
+
+ public void dispose() {
+ if (imageRegistry != null) {
+ imageRegistry.dispose();
+ imageRegistry = null;
+ }
+ }
+
+ public void init() {
+ assert Display.getCurrent() != null;
+ if (imageRegistry == null) {
+ imageRegistry = new ImageRegistry();
+ }
+ }
+
+ private static ImageRegistry getImageRegistry() {
+ if (imageRegistry == null) {
+ imageRegistry = new ImageRegistry();
+ }
+
+ return imageRegistry;
+ }
+
+ public static Image getImage(String url, ImageDescriptor imageDescriptor) {
+ ImageRegistry registry = getImageRegistry();
+
+ Image image = registry.get(url);
+ if (image == null) {
+ image = imageDescriptor.createImage();
+ registry.put(url, image);
+ }
+ return image;
+ }
+
+ public Image getAvatar(User author, AvatarSize size) {
+ return getImageRegistry().get(author.getAvatarUrl() + size.toString());
+ }
+
+ public Image getAvatarOrDefaultImage(User author, AvatarSize size) {
+ if (author.getAvatarUrl() != null) {
+ final Image image = getImageRegistry().get(author.getAvatarUrl() + size.toString());
+ if (image != null) {
+ return image;
+ }
+ }
+ return CrucibleImages.getImage(size == AvatarSize.LARGE ? CrucibleImages.DEFAULT_AVATAR_LARGE
+ : CrucibleImages.DEFAULT_AVATAR);
+ }
+
+ public void addAvatar(User key, byte[] value) {
+ InputStream is = new ByteArrayInputStream(value);
+ try {
+ Image image = getImage(key.getAvatarUrl() + AvatarSize.ORIGINAL.toString(),
+ ImageDescriptor.createFromImageData(new ImageData(is)));
+ getImageRegistry().put(key.getAvatarUrl() + AvatarSize.SMALL.toString(),
+ new Image(Display.getDefault(), image.getImageData().scaledTo(16, 16)));
+ getImageRegistry().put(key.getAvatarUrl() + AvatarSize.LARGE.toString(),
+ new Image(Display.getDefault(), image.getImageData().scaledTo(32, 32)));
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleImages.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleImages.java
new file mode 100644
index 0000000..6363e65
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleImages.java
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.ImageRegistry;
+import org.eclipse.swt.graphics.Image;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * @author Steffen Pingel
+ */
+public final class CrucibleImages {
+
+ private static ImageRegistry imageRegistry;
+
+ private static final URL BASE_URL = CrucibleUiPlugin.getDefault().getBundle().getEntry("/icons/"); //$NON-NLS-1$
+
+ private static final String T_OBJ = "obj16"; //$NON-NLS-1$
+
+ private static final String OVR = "ovr"; //$NON-NLS-1$
+
+ public static final ImageDescriptor REVIEWER_COMPLETE = create(T_OBJ, "reviewerComplete.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor COMMENT_SMALL = create(T_OBJ, "comment-small.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor CRUCIBLE = create(T_OBJ, "crucible.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor ADD_COMMENT = create(T_OBJ, "pin_addcomment.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor ABANDON = create(T_OBJ, "pin_abandon.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor SUMMARIZE = create(T_OBJ, "pin_summarise.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor CLOSE = create(T_OBJ, "pin_close.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor RECOVER = create(T_OBJ, "pin_recover.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor REOPEN = create(T_OBJ, "pin_reopen.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor UNCOMPLETE = create(T_OBJ, "pin_uncomplete.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor COMPLETE = create(T_OBJ, "pin_complete.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor SUBMIT = create(T_OBJ, "pin_submit.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor COMMENT_EDIT = create(T_OBJ, "ico_small_edit.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor COMMENT_DELETE = create(T_OBJ, "ico_small_delete.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor COMMENT_POST = create(T_OBJ, "ico_small_publish.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor JOIN = create(T_OBJ, "pin_join.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor LEAVE = create(T_OBJ, "pin_leave.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor APPROVE = create(T_OBJ, "pin_approve.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor SET_REVIEWERS = create(T_OBJ, "pin_setreviewers.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor POST = create(T_OBJ, "pin_submit.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor FILE = create(T_OBJ, "file_obj.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor CHANGESET = create(T_OBJ, "changeset_obj.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor ADD_CHANGESET = create(T_OBJ, "add_changeset.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor ADD_PATCH = create(T_OBJ, "add_patch.gif"); //$NON-NLS-1$
+
+ public static final ImageDescriptor DEFAULT_AVATAR = create(T_OBJ, "default_avatar.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor DEFAULT_AVATAR_LARGE = create(T_OBJ, "default_avatar-large.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor OVR_DELETED = create(OVR, "deleted.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor OVR_ADDED = create(OVR, "added.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor PUBLISH_DRAFT_COMMENTS = create(T_OBJ, "ico_comment_toggle_read.png"); //$NON-NLS-1$
+
+ public static final ImageDescriptor ADD_FILE_COMMENT = create(T_OBJ, "pin_filecomment.png"); //$NON-NLS-1$
+
+ /**
+ * this icon is borrowed from org.eclipse.ui (access scope is there internal, so I don't want to have compilation warning
+ * just because of it
+ */
+ public static final ImageDescriptor OVR_MODIFIED = create(OVR, "dirty_ov.gif"); //$NON-NLS-1$
+
+ private CrucibleImages() {
+ }
+
+ private static ImageDescriptor create(String prefix, String name) {
+ try {
+ return ImageDescriptor.createFromURL(makeIconFileURL(prefix, name));
+ } catch (MalformedURLException e) {
+ return ImageDescriptor.getMissingImageDescriptor();
+ }
+ }
+
+ private static URL makeIconFileURL(String prefix, String name) throws MalformedURLException {
+ if (BASE_URL == null) {
+ throw new MalformedURLException();
+ }
+
+ StringBuffer buffer = new StringBuffer(prefix);
+ buffer.append('/');
+ buffer.append(name);
+ return new URL(BASE_URL, buffer.toString());
+ }
+
+ private static ImageRegistry getImageRegistry() {
+ if (imageRegistry == null) {
+ imageRegistry = new ImageRegistry();
+ }
+
+ return imageRegistry;
+ }
+
+ public static Image getImage(ImageDescriptor imageDescriptor) {
+ ImageRegistry registry = getImageRegistry();
+
+ Image image = registry.get("" + imageDescriptor.hashCode());
+ if (image == null) {
+ image = imageDescriptor.createImage();
+ registry.put("" + imageDescriptor.hashCode(), image);
+ }
+ return image;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileInput.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileInput.java
new file mode 100644
index 0000000..871dbe4
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileInput.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+
+import org.eclipse.core.internal.filesystem.local.LocalFile;
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IPathEditorInput;
+import org.eclipse.ui.IPersistableElement;
+import org.eclipse.ui.IStorageEditorInput;
+import org.eclipse.ui.ide.FileStoreEditorInput;
+
+/**
+ *
+ * @author Jacek Jaroczynski
+ */
+@SuppressWarnings("restriction")
+public class CruciblePreCommitFileInput extends FileStoreEditorInput implements IStorageEditorInput, IPathEditorInput,
+ ICrucibleFileProvider {
+ private final CruciblePreCommitFileStorage storage;
+
+ public CruciblePreCommitFileInput(CruciblePreCommitFileStorage storage, LocalFile localFile) {
+ super(localFile);
+ this.storage = storage;
+ }
+
+ public boolean exists() {
+ return true;
+ }
+
+ public ImageDescriptor getImageDescriptor() {
+ return null;
+ }
+
+ public String getName() {
+ return storage.getName() + String.format(" [%s]", storage.getCrucibleFile().getSelectedFile().getRevision());
+ }
+
+ public IPersistableElement getPersistable() {
+ return null;
+ }
+
+ public IStorage getStorage() {
+ return storage;
+ }
+
+ public String getToolTipText() {
+ return storage.getFullPath().toString();
+ }
+
+ public CrucibleFile getCrucibleFile() {
+ return storage.getCrucibleFile();
+ }
+
+ public IPath getPath() {
+ if (storage.getLocalFilePath() != null) {
+ return new Path(storage.getLocalFilePath());
+ }
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((storage == null) ? 0 : storage.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CruciblePreCommitFileInput other = (CruciblePreCommitFileInput) obj;
+ if (storage == null) {
+ if (other.storage != null) {
+ return false;
+ }
+ } else if (!storage.equals(other.storage)) {
+ return false;
+ }
+ return true;
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileStorage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileStorage.java
new file mode 100644
index 0000000..1359d67
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CruciblePreCommitFileStorage.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.VersionedVirtualFile;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.util.Arrays;
+
+/**
+ *
+ * @author Jacek Jaroczynski
+ */
+public class CruciblePreCommitFileStorage implements IStorage {
+ private final byte[] content;
+
+ private final VersionedVirtualFile virtualFile;
+
+ private final CrucibleFile crucibleFile;
+
+ private final File localCopy;
+
+ public CruciblePreCommitFileStorage(CrucibleFile crucibleFile, byte[] content, File localCopy) {
+ this.crucibleFile = crucibleFile;
+ this.localCopy = localCopy;
+ this.virtualFile = crucibleFile.getSelectedFile();
+ this.content = content;
+ }
+
+ public InputStream getContents() {
+ return new ByteArrayInputStream(content);
+ }
+
+ public IPath getFullPath() {
+ return new Path(virtualFile.getUrl());
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Object getAdapter(Class adapter) {
+ return null;
+ }
+
+ public String getName() {
+ return virtualFile.getName();
+ }
+
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ public CrucibleFile getCrucibleFile() {
+ return crucibleFile;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(content);
+ result = prime * result + ((virtualFile == null) ? 0 : virtualFile.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CruciblePreCommitFileStorage other = (CruciblePreCommitFileStorage) obj;
+ if (!Arrays.equals(content, other.content)) {
+ return false;
+ }
+ if (virtualFile == null) {
+ if (other.virtualFile != null) {
+ return false;
+ }
+ } else if (!virtualFile.equals(other.virtualFile)) {
+ return false;
+ }
+ return true;
+ }
+
+ public String getLocalFilePath() {
+ if (localCopy != null) {
+ return localCopy.getAbsolutePath();
+ }
+ return null;
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiConstants.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiConstants.java
new file mode 100644
index 0000000..8e6dea3
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiConstants.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+public final class CrucibleUiConstants {
+
+ private CrucibleUiConstants() {
+ }
+
+ public static final String PREFERENCE_ACTIVATE_REVIEW = "prefs_activate_review";
+
+ public static final String PREFERRED_TEAM_RESOURCE_CONNECTOR_NAME = "preferred_team_resource_connector_name";
+
+ public static final String PREVIOUS_PATCH_REVIEW_SELECTION = "previous_patch_review_selection";
+
+ public static final String PREVIOUS_WORKSPACE_PATCH_REVIEW_SELECTION = "previous_workspace_patch_review_selection";
+
+ public static final String PREVIOUS_CHANGESET_REVIEW_SELECTION = "previous_changeset_review_selection";
+
+ public static final String PREFERENCE_RESOURCE_TREE_VIEW_MODE = "prefs_resources_tree_view_mode";
+
+ public static final String REVIEW_EXPLORER_VIEW = "com.atlassian.connector.eclipse.crucible.ui.explorerView";
+
+ public static final String PREFERENCE_SECURE_STORAGE_MIGRATED = "secure_storage.migration.done";
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiPlugin.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiPlugin.java
new file mode 100644
index 0000000..514e57d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiPlugin.java
@@ -0,0 +1,222 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.ui.commons.ResourceSelectionTree.TreeViewMode;
+
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.jface.dialogs.MessageDialogWithToggle;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TasksUi;
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The activator class controls the plug-in life cycle
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleUiPlugin extends AbstractUIPlugin {
+
+ // The plug-in ID
+ public static final String PLUGIN_ID = "com.atlassian.connector.eclipse.crucible.ui";
+
+ public static final String REVIEW_PERSPECTIVE_ID = PLUGIN_ID + ".reviewPerspective";
+
+ public static final String COMMENT_VIEW_ID = PLUGIN_ID + ".commentView";
+
+ public static final String EXPLORER_VIEW_ID = PLUGIN_ID + ".explorerView";
+
+ public static final String PRODUCT_NAME = "Atlassian Crucible Connector";
+
+ private static final String DEFAULT_PROJECT = "defaultProject";
+
+ private static final String ALLOW_ANYONE_TO_JOIN = "allowAnyoneToJoin";
+
+ private static final String START_REVIEW = "startReview";
+
+ // The shared instance
+ private static CrucibleUiPlugin plugin;
+
+ private static CrucibleClient client;
+
+ private ActiveReviewManager activeReviewManager;
+
+ private SwitchingPerspectiveReviewActivationListener switchingPerspectivesListener;
+
+ private AvatarImages avatarImages;
+
+ /**
+ * The constructor
+ */
+ public CrucibleUiPlugin() {
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
+ */
+ @Override
+ public void start(BundleContext context) throws Exception {
+ super.start(context);
+ plugin = this;
+
+ switchingPerspectivesListener = new SwitchingPerspectiveReviewActivationListener();
+ activeReviewManager = new ActiveReviewManager(true);
+ activeReviewManager.addReviewActivationListener(switchingPerspectivesListener);
+
+ avatarImages = new AvatarImages();
+
+ enableActiveReviewManager();
+
+ plugin.getPreferenceStore().setDefault(CrucibleUiConstants.PREFERENCE_ACTIVATE_REVIEW,
+ MessageDialogWithToggle.PROMPT);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
+ */
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ disableActiveReviewManager();
+
+ activeReviewManager.dispose();
+ activeReviewManager = null;
+
+ avatarImages.dispose();
+ avatarImages = null;
+
+ plugin = null;
+ super.stop(context);
+ }
+
+ /**
+ * Returns the shared instance
+ *
+ * @return the shared instance
+ */
+ public static CrucibleUiPlugin getDefault() {
+ return plugin;
+ }
+
+ public ActiveReviewManager getActiveReviewManager() {
+ return activeReviewManager;
+ }
+
+ /**
+ * Method for testing purposes
+ */
+ public void disableActiveReviewManager() {
+ if (activeReviewManager != null) {
+ TasksUi.getTaskActivityManager().removeActivationListener(activeReviewManager);
+ }
+ }
+
+ /**
+ * Method for testing purposes
+ */
+ public void enableActiveReviewManager() {
+ if (activeReviewManager != null) {
+ TasksUi.getTaskActivityManager().addActivationListener(activeReviewManager);
+ }
+ }
+
+ public boolean getPreviousChangesetReviewSelection() {
+ return plugin.getPreferenceStore().getBoolean(CrucibleUiConstants.PREVIOUS_CHANGESET_REVIEW_SELECTION);
+ }
+
+ public boolean getPreviousPatchReviewSelection() {
+ return plugin.getPreferenceStore().getBoolean(CrucibleUiConstants.PREVIOUS_PATCH_REVIEW_SELECTION);
+ }
+
+ public boolean getPreviousWorkspacePatchReviewSelection() {
+ return plugin.getPreferenceStore().getBoolean(CrucibleUiConstants.PREVIOUS_WORKSPACE_PATCH_REVIEW_SELECTION);
+ }
+
+ public void setPreviousChangesetReviewSelection(boolean value) {
+ plugin.getPreferenceStore().setValue(CrucibleUiConstants.PREVIOUS_CHANGESET_REVIEW_SELECTION, value);
+ }
+
+ public void setPreviousPatchReviewSelection(boolean value) {
+ plugin.getPreferenceStore().setValue(CrucibleUiConstants.PREVIOUS_PATCH_REVIEW_SELECTION, value);
+ }
+
+ public void setPreviousWorkspacePatchReviewSelection(boolean value) {
+ plugin.getPreferenceStore().setValue(CrucibleUiConstants.PREVIOUS_WORKSPACE_PATCH_REVIEW_SELECTION, value);
+ }
+
+ public TreeViewMode getResourcesTreeViewMode() {
+ int mode = plugin.getPreferenceStore().getInt(CrucibleUiConstants.PREFERENCE_RESOURCE_TREE_VIEW_MODE);
+ for (TreeViewMode treeMode : TreeViewMode.values()) {
+ if (treeMode.ordinal() == mode) {
+ return treeMode;
+ }
+ }
+
+ return TreeViewMode.MODE_COMPRESSED_FOLDERS;
+ }
+
+ public void setResourcesTreeViewMode(TreeViewMode mode) {
+ plugin.getPreferenceStore().setValue(CrucibleUiConstants.PREFERENCE_RESOURCE_TREE_VIEW_MODE, mode.ordinal());
+ }
+
+ public IDialogSettings getDialogSettingsSection(String name) {
+ IDialogSettings dialogSettings = getDialogSettings();
+ IDialogSettings section = dialogSettings.getSection(name);
+ if (section == null) {
+ section = dialogSettings.addNewSection(name);
+ }
+ return section;
+ }
+
+ public AvatarImages getAvatarsCache() {
+ return this.avatarImages;
+ }
+
+ public void updateLastSelectedProject(TaskRepository repository, String projectKey) {
+ repository.setProperty(DEFAULT_PROJECT, projectKey);
+ }
+
+ public String getLastSelectedProjectKey(TaskRepository repository) {
+ return repository.getProperty(DEFAULT_PROJECT);
+ }
+
+ public boolean getAllowAnyoneOption(TaskRepository repository) {
+ final String prop = repository.getProperty(ALLOW_ANYONE_TO_JOIN);
+ return prop != null && Boolean.valueOf(prop);
+ }
+
+ public void updateAllowAnyoneOption(TaskRepository taskRepository, boolean allowAnyone) {
+ taskRepository.setProperty(ALLOW_ANYONE_TO_JOIN, String.valueOf(allowAnyone));
+ }
+
+ public boolean getStartReviewOption(TaskRepository repository) {
+ final String prop = repository.getProperty(START_REVIEW);
+ return prop != null && Boolean.valueOf(prop);
+ }
+
+ public void updateStartReviewOption(TaskRepository taskRepository, boolean startReview) {
+ taskRepository.setProperty(START_REVIEW, String.valueOf(startReview));
+ }
+
+ public static CrucibleClient getClient(TaskRepository taskRepository) {
+ if (client == null) {
+ client = new CrucibleClient();
+ }
+ return client;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiUtil.java
new file mode 100644
index 0000000..ead6a95
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/CrucibleUiUtil.java
@@ -0,0 +1,452 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.util.EditorUtil;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.TeamUiResourceManager;
+import com.atlassian.connector.eclipse.team.ui.TeamUiUtils;
+import com.atlassian.connector.eclipse.team.ui.exceptions.UnsupportedTeamProviderException;
+import com.atlassian.theplugin.commons.crucible.api.model.BasicProject;
+import com.atlassian.theplugin.commons.crucible.api.model.BasicReview;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Repository;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.Reviewer;
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.tasks.core.LocalRepositoryConnector;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TasksUi;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.texteditor.ITextEditor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility class for the UI
+ *
+ * @author Shawn Minto
+ * @author Wojciech Seliga
+ */
+public final class CrucibleUiUtil {
+
+ private CrucibleUiUtil() {
+ }
+
+ public static TaskRepository getCrucibleTaskRepository(String repositoryUrl) {
+ return TasksUi.getRepositoryManager().getRepository(LocalRepositoryConnector.CONNECTOR_KIND,
+ LocalRepositoryConnector.REPOSITORY_URL);
+ }
+
+ public static ITask getCrucibleTask(TaskRepository taskRepository, String taskId) {
+ if (taskRepository != null && taskId != null) {
+ return TasksUi.getRepositoryModel().getTask(taskRepository, taskId);
+ }
+ return null;
+ }
+
+ public static TaskRepository getCrucibleTaskRepository(BasicReview review) {
+ if (review != null) {
+ String repositoryUrl = review.getServerUrl();
+ if (repositoryUrl != null) {
+ return getCrucibleTaskRepository(repositoryUrl);
+ }
+ }
+ return null;
+ }
+
+ public static CrucibleClient getClient(BasicReview review) {
+ return CrucibleUiPlugin.getClient(getCrucibleTaskRepository(review));
+ }
+
+ public static ITask getCrucibleTask(Review review) {
+ if (review != null) {
+ TaskRepository taskRepository = getCrucibleTaskRepository(review);
+ String taskId = CrucibleUtil.getTaskIdFromPermId(review.getPermId().getId());
+ if (taskRepository != null && taskId != null) {
+ return getCrucibleTask(taskRepository, taskId);
+ }
+ }
+
+ return null;
+ }
+
+ public static boolean hasCurrentUserCompletedReview(Review review) {
+ String currentUser = getCurrentUsername(review);
+ return CrucibleUtil.isUserCompleted(currentUser, review);
+ }
+
+ public static String getCurrentUsername(Review review) {
+ return getCurrentUsername(CrucibleUiUtil.getCrucibleTaskRepository(review));
+ }
+
+ public static User getCurrentCachedUser(TaskRepository repository) {
+ return getCachedUser(getCurrentUsername(repository), repository);
+ }
+
+ public static User getCurrentCachedUser(Review review) {
+ TaskRepository repository = CrucibleUiUtil.getCrucibleTaskRepository(review);
+ return getCachedUser(getCurrentUsername(repository), repository);
+ }
+
+ private static boolean hasReviewerCompleted(Review review, String username) {
+ for (Reviewer r : review.getReviewers()) {
+ if (r.getUsername().equals(username)) {
+ return r.isCompleted();
+ }
+ }
+ return false;
+ }
+
+ public static Reviewer createReviewerFromCachedUser(Review review, User user) {
+ boolean completed = hasReviewerCompleted(review, user.getUsername());
+ return new Reviewer(user.getUsername(), user.getDisplayName(), completed);
+ }
+
+ public static String getCurrentUsername(TaskRepository repository) {
+ /*
+ * String currentUser = CrucibleCorePlugin.getRepositoryConnector() .getClientManager() .getClient(repository)
+ * .getUserName();
+ */
+ return repository.getUserName();
+ }
+
+ public static User getCachedUser(String userName, TaskRepository repository) {
+ if (userName != null) {
+ for (User user : getCachedUsers(repository)) {
+ if (userName.equals(user.getUsername())) {
+ return user;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static boolean isUserReviewer(String userName, Review review) {
+ for (Reviewer reviewer : review.getReviewers()) {
+ if (reviewer.getUsername().equals(userName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isCurrentUserReviewer(Review review) {
+ return isUserReviewer(CrucibleUiUtil.getCurrentUsername(review), review);
+ }
+
+ public static boolean isFilePartOfActiveReview(CrucibleFile crucibleFile) {
+ Review activeReview = CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ return isFilePartOfReview(crucibleFile, activeReview);
+ }
+
+ public static boolean isFilePartOfReview(CrucibleFile crucibleFile, Review review) {
+ if (review == null || crucibleFile == null || crucibleFile.getCrucibleFileInfo() == null
+ || crucibleFile.getCrucibleFileInfo().getFileDescriptor() == null) {
+ return false;
+ }
+ for (CrucibleFileInfo fileInfo : review.getFiles()) {
+ if (fileInfo != null
+ && fileInfo.getFileDescriptor() != null
+ && fileInfo.getFileDescriptor()
+ .getUrl()
+ .equals(crucibleFile.getCrucibleFileInfo().getFileDescriptor().getUrl())
+ && fileInfo.getFileDescriptor()
+ .getRevision()
+ .equals(crucibleFile.getCrucibleFileInfo().getFileDescriptor().getRevision())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static Set<User> getCachedUsers(Review review) {
+ return getCachedUsers(getCrucibleTaskRepository(review));
+ }
+
+ public static Set<User> getCachedUsers(TaskRepository repository) {
+ final Set<User> users;
+ users = new HashSet<User>();
+ users.add(new User("User A"));
+ users.add(new User("User B"));
+ return users;
+ }
+
+ public static Set<Repository> getCachedRepositories(TaskRepository repository) {
+ Set<Repository> repositories;
+ repositories = new HashSet<Repository>();
+ return repositories;
+ }
+
+ public static Collection<BasicProject> getCachedProjects(TaskRepository repository) {
+ final Set<BasicProject> projects;
+ projects = new HashSet<BasicProject>();
+ projects.add(new BasicProject("id", "key", "Review Project"));
+ return projects;
+ }
+
+ public static BasicProject getCachedProject(TaskRepository repository, String projectKey) {
+ return getCachedProjects(repository).iterator().next();
+ }
+
+ public static Collection<User> getUsersFromUsernames(TaskRepository taskRepository, Collection<String> usernames) {
+ Set<User> users = CrucibleUiUtil.getCachedUsers(taskRepository);
+ Set<User> result = MiscUtil.buildHashSet();
+ for (User user : users) {
+ if (usernames.contains(user.getUsername())) {
+ result.add(user);
+ }
+ }
+ return result;
+ }
+
+ public static boolean canModifyComment(Review review, Comment comment) {
+ return true;
+ }
+
+ public static boolean canMarkAsReadOrUnread(Review review, Comment comment) {
+ return true;
+ }
+
+ public static Set<String> getUsernamesFromUsers(Collection<? extends User> users) {
+ final Set<String> userNames = new HashSet<String>();
+ for (User user : users) {
+ userNames.add(user.getUsername());
+ }
+ return userNames;
+ }
+
+ public static Set<Reviewer> getAllCachedUsersAsReviewers(TaskRepository taskRepository) {
+ return toReviewers(CrucibleUiUtil.getCachedUsers(taskRepository));
+ }
+
+ public static Set<Reviewer> toReviewers(Collection<User> users) {
+ Set<Reviewer> allReviewers = new HashSet<Reviewer>();
+ for (User user : users) {
+ allReviewers.add(new Reviewer(user.getUsername(), user.getDisplayName(), false));
+ }
+ return allReviewers;
+ }
+
+ public static Set<User> toUsers(Collection<Reviewer> users) {
+ Set<User> res = new HashSet<User>();
+ for (Reviewer user : users) {
+ res.add(new User(user.getUsername(), user.getDisplayName(), user.getAvatarUrl()));
+ }
+ return res;
+ }
+
+ public static void focusOnComment(IEditorPart editor, CrucibleFile crucibleFile, VersionedComment versionedComment) {
+ if (editor instanceof ITextEditor) {
+ ITextEditor textEditor = ((ITextEditor) editor);
+ if (versionedComment != null) {
+ EditorUtil.selectAndReveal(textEditor, versionedComment, crucibleFile.getSelectedFile());
+ }
+ }
+ }
+
+ /**
+ * Gets file from review (both pre- or post-commit)
+ *
+ * @param resource
+ * @param review
+ * @return
+ */
+ public static CrucibleFile getCrucibleFileFromResource(IResource resource, Review review, IProgressMonitor monitor) {
+ CrucibleFile cruFile = getCruciblePostCommitFile(resource, review);
+
+ if (cruFile != null) {
+ return cruFile;
+ }
+
+ try {
+ return getCruciblePreCommitFile(resource, review, monitor);
+ } catch (CoreException e) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PRODUCT_NAME,
+ "Cannot find pre-commit file for selected resource.", e));
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets post-commit file form review
+ *
+ * @param resource
+ * @param review
+ * @return
+ */
+ public static CrucibleFile getCruciblePostCommitFile(IResource resource, Review review) {
+ if (review == null || !(resource instanceof IFile)) {
+ return null;
+ }
+
+ IFile file = (IFile) resource;
+
+ TeamUiResourceManager teamResourceManager = AtlassianTeamUiPlugin.getDefault().getTeamResourceManager();
+
+ for (ITeamUiResourceConnector connector : teamResourceManager.getTeamConnectors()) {
+ if (connector.isEnabled() && connector.canHandleFile(file)) {
+ CrucibleFile fileInfo;
+ try {
+ fileInfo = connector.getCrucibleFileFromReview(review, file);
+ } catch (UnsupportedTeamProviderException e) {
+ return null;
+ }
+ if (fileInfo != null) {
+ return fileInfo;
+ }
+ }
+ }
+
+ try {
+ CrucibleFile crucibleFile = TeamUiUtils.getDefaultConnector().getCrucibleFileFromReview(review, file);
+ if (crucibleFile != null) {
+ return crucibleFile;
+ }
+ } catch (UnsupportedTeamProviderException e) {
+ // ignore
+ }
+
+ for (CrucibleFileInfo crucibleFile : review.getFiles()) {
+ if (file.getName().equals(crucibleFile.getPermId().getId())) {
+ return new CrucibleFile(crucibleFile, true);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets pre-commit file form review
+ *
+ * @param resource
+ * @param review
+ * @return
+ * @throws CoreException
+ */
+ public static CrucibleFile getCruciblePreCommitFile(final IResource resource, Review review,
+ IProgressMonitor monitor) throws CoreException {
+
+ if (review == null || !(resource instanceof IFile)) {
+ return null;
+ }
+
+ IFile file = (IFile) resource;
+
+// String localFileUrl = StringUtil.removeLeadingAndTrailingSlashes(file.getFullPath().toString());
+ String localFileUrl = file.getFullPath().toString();
+
+ List<CrucibleFile> matchingFiles = new ArrayList<CrucibleFile>();
+
+ for (CrucibleFileInfo cruFile : review.getFiles()) {
+// String newFileUrl = StringUtil.removeLeadingAndTrailingSlashes(cruFile.getFileDescriptor().getUrl());
+// String oldFileUrl = StringUtil.removeLeadingAndTrailingSlashes(cruFile.getOldFileDescriptor().getUrl());
+ String newFileUrl = cruFile.getFileDescriptor().getUrl();
+ String oldFileUrl = cruFile.getOldFileDescriptor().getUrl();
+
+ if (newFileUrl != null && newFileUrl.equals(localFileUrl)) {
+ matchingFiles.add(new CrucibleFile(cruFile, false));
+ } else if (oldFileUrl != null && oldFileUrl.equals(localFileUrl)) {
+ matchingFiles.add(new CrucibleFile(cruFile, true));
+ }
+ }
+
+ if (matchingFiles.size() > 0) {
+ CrucibleClient client = CrucibleUiUtil.getClient(review);
+ TaskRepository repository = CrucibleUiUtil.getCrucibleTaskRepository(review);
+
+ for (final CrucibleFile cruFile : matchingFiles) {
+ final String url = cruFile.getSelectedFile().getContentUrl();
+ if (url == null || url.length() == 0) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PRODUCT_NAME,
+ "Cannot find pre-commit file for selected resource. Matching review item content url is empty"));
+ continue;
+ }
+// Boolean ret = client.execute(new RemoteOperation<Boolean, CrucibleServerFacade2>(monitor, repository) {
+//
+// @Override
+// public Boolean run(CrucibleServerFacade2 server, ConnectionCfg serverCfg, IProgressMonitor monitor)
+// throws RemoteApiException, ServerPasswordNotProvidedException {
+//
+// final byte[] content = OpenVirtualFileJob.getContent(url, server.getSession(serverCfg),
+// serverCfg.getUrl());
+//
+// if (content == null) {
+// return false;
+// }
+//
+// File localFile;
+// try {
+// localFile = OpenVirtualFileJob.createTempFile(cruFile.getSelectedFile().getName(), content);
+//
+// if (FileUtils.contentEquals(localFile, resource.getRawLocation().toFile())) {
+// return true;
+// }
+// } catch (IOException e) {
+// StatusHandler.log(new Status(
+// IStatus.ERROR,
+// CrucibleUiPlugin.PRODUCT_NAME,
+// "Cannot create local temporary file. Cannot compare selected resource with review item.",
+// e));
+// }
+// return false;
+// }
+// }, true);
+
+// if (ret) {
+// return cruFile;
+// }
+ }
+ }
+
+ return null;
+ }
+
+ public static String getDisplayNameOrUsername(User user) {
+ return user.getDisplayName() == null || "".equals(user.getDisplayName()) ? user.getUsername()
+ : user.getDisplayName();
+ }
+
+ public static boolean hasCachedData(TaskRepository taskRepository) {
+ // ignore
+ return false;
+ }
+
+ public static void updateTaskRepositoryCache(TaskRepository taskRepository, IWizardContainer container,
+ IWizardPage selectChangesetsFromCruciblePage) {
+ // ignore
+
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ICrucibleFileProvider.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ICrucibleFileProvider.java
new file mode 100644
index 0000000..82170e1
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/ICrucibleFileProvider.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+
+public interface ICrucibleFileProvider {
+
+ CrucibleFile getCrucibleFile();
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewAction.java
new file mode 100644
index 0000000..767a473
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewAction.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import org.eclipse.jface.action.IAction;
+
+/**
+ * An action that is used in a part for a review. This is used to notify other parts when the action has run (e.g. close
+ * the annotation popup).
+ *
+ * @author Shawn Minto
+ */
+public interface IReviewAction extends IAction {
+
+ void setActionListener(IReviewActionListener listener);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewActionListener.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewActionListener.java
new file mode 100644
index 0000000..8653f51
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/IReviewActionListener.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import org.eclipse.jface.action.Action;
+
+/**
+ * Listener for when an IReviewAction has ran
+ *
+ * @author Shawn Minto
+ */
+public interface IReviewActionListener {
+
+ void actionRan(Action action);
+
+ void actionAboutToRun(Action action);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/SwitchingPerspectiveReviewActivationListener.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/SwitchingPerspectiveReviewActivationListener.java
new file mode 100644
index 0000000..b4fd2b3
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/SwitchingPerspectiveReviewActivationListener.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.ActiveReviewManager.IReviewActivationListener;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.notification.CrucibleNotification;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.tasks.ui.actions.TaskActivateAction;
+import org.eclipse.mylyn.internal.tasks.ui.actions.TaskSelectionDialogWithRandom;
+import org.eclipse.mylyn.internal.tasks.ui.commands.ActivateTaskHandler;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IPerspectiveDescriptor;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.WorkbenchException;
+import java.util.Collection;
+
+/**
+ *
+ * @author Pawel Niewiadomski
+ */
+public class SwitchingPerspectiveReviewActivationListener implements IReviewActivationListener {
+
+ private IPerspectiveDescriptor previousPerspective;
+
+ private IPerspectiveDescriptor getActivePerspective() {
+ IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ IWorkbenchPage activePage = window.getActivePage();
+ if (activePage != null) {
+ return activePage.getPerspective();
+ }
+ return null;
+ }
+
+ public void reviewActivated(ITask task, Review review) {
+ if (!isUserInteraction()) {
+ return;
+ }
+
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ IPerspectiveDescriptor perspective = getActivePerspective();
+ if (!perspective.getId().equals(CrucibleUiPlugin.REVIEW_PERSPECTIVE_ID)) {
+ previousPerspective = perspective;
+ try {
+ PlatformUI.getWorkbench().showPerspective(CrucibleUiPlugin.REVIEW_PERSPECTIVE_ID,
+ PlatformUI.getWorkbench().getActiveWorkbenchWindow());
+ } catch (WorkbenchException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to switch perspectives", e));
+ }
+ } else {
+ previousPerspective = null;
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("restriction")
+ private boolean isUserInteraction() {
+ Exception e = new Exception();
+ e.fillInStackTrace();
+ StackTraceElement[] stack = e.getStackTrace();
+ for (StackTraceElement element : stack) {
+ String className = element.getClassName();
+ if (className.contains(TaskActivateAction.class.getName())
+ || className.contains(ActivateTaskHandler.class.getName())
+ || className.contains(TaskSelectionDialogWithRandom.class.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void reviewDeactivated(ITask task, Review review) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ IPerspectiveDescriptor perspective = getActivePerspective();
+ if (previousPerspective != null) {
+ if (perspective.getId().equals(CrucibleUiPlugin.REVIEW_PERSPECTIVE_ID)) {
+ try {
+ PlatformUI.getWorkbench().showPerspective(previousPerspective.getId(),
+ PlatformUI.getWorkbench().getActiveWorkbenchWindow());
+ } catch (WorkbenchException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to switch perspectives", e));
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public void reviewUpdated(ITask task, Review review, Collection<CrucibleNotification> differences) {
+ // do nothing
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractAddCommentAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractAddCommentAction.java
new file mode 100644
index 0000000..c26f408
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractAddCommentAction.java
@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.ICrucibleFileProvider;
+import com.atlassian.connector.eclipse.internal.crucible.ui.dialogs.CrucibleAddFileAddCommentDialog;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.connector.eclipse.ui.commons.AtlassianUiUtil;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.ui.IEditorInput;
+
+/**
+ * Abstract class to deal with adding comments to a review
+ *
+ * @author Shawn Minto
+ * @author Thomas Ehrnhoefer
+ */
+public abstract class AbstractAddCommentAction extends AbstractReviewAction {
+
+ private CrucibleAddFileAddCommentDialog commentDialog;
+
+ public class GetCrucibleFileJob extends Job {
+
+ private final IEditorInput editorInput;
+
+ private final Review review;
+
+ private final LineRange commentLines;
+
+ public GetCrucibleFileJob(String name, IEditorInput editorInput, LineRange commentLines, Review review) {
+ super(name);
+ this.editorInput = editorInput;
+ this.commentLines = commentLines;
+ this.review = review;
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ CrucibleFile crucibleFile = null;
+
+ if (editorInput instanceof ICrucibleFileProvider) {
+ crucibleFile = ((ICrucibleFileProvider) editorInput).getCrucibleFile();
+ }
+
+ if (!CrucibleUiUtil.isFilePartOfReview(crucibleFile, review)) {
+ crucibleFile = null;
+ }
+
+ if (crucibleFile == null) {
+
+ IResource resource = (IResource) editorInput.getAdapter(IResource.class);
+
+ if (resource instanceof IFile) {
+ crucibleFile = CrucibleUiUtil.getCrucibleFileFromResource(resource, review, monitor);
+ }
+ }
+
+ if (crucibleFile == null) {
+ AtlassianUiUtil.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ openDialog((IResource) getEditorInput().getAdapter(IResource.class), commentLines);
+ }
+ });
+ } else {
+ final CrucibleFile cf = crucibleFile;
+
+ AtlassianUiUtil.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ openDialog(cf, commentLines);
+ }
+ });
+ }
+ return Status.OK_STATUS;
+ }
+ }
+
+ protected AbstractAddCommentAction(String text) {
+ super(text);
+ }
+
+ public void run(IAction action) {
+
+ if (review == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Cannot add comment to file. Review is null."));
+ return;
+ }
+
+ LineRange commentLines = getSelectedRange();
+ CrucibleFile crucibleFile = getCrucibleFile();
+
+ CrucibleClient client = CrucibleUiPlugin.getClient(getTaskRepository());
+ if (client == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to get client, please try to refresh"));
+ return;
+ }
+
+ commentDialog = new CrucibleAddFileAddCommentDialog(WorkbenchUtil.getShell(), getDialogTitle(), review,
+ getTaskKey(), getTaskId(), getTaskRepository(), client);
+
+ if (crucibleFile != null) {
+ openDialog(crucibleFile, commentLines);
+ } else {
+ if (getEditorInput() == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to determine crucible file. EditorInput object is null"));
+ return;
+ }
+
+ GetCrucibleFileJob getCrucibleFileJob = new GetCrucibleFileJob("Getting Crucible file data",
+ getEditorInput(), commentLines, review);
+
+ getCrucibleFileJob.setUser(true);
+ getCrucibleFileJob.schedule();
+
+ }
+ }
+
+ private void openDialog(IResource resource, LineRange commentLines) {
+ commentDialog.setResource(resource);
+ if (commentLines != null) {
+ commentDialog.setCommentLines(commentLines);
+ }
+ commentDialog.open();
+ }
+
+ private void openDialog(CrucibleFile crucibleFile, LineRange commentLines) {
+ commentDialog.setReviewItem(crucibleFile);
+ if (commentLines != null) {
+ commentDialog.setCommentLines(commentLines);
+ }
+ commentDialog.open();
+ }
+
+ protected abstract String getDialogTitle();
+
+ protected abstract IEditorInput getEditorInput();
+
+ protected CrucibleFile getCrucibleFile() {
+ return null;
+ }
+
+ protected LineRange getSelectedRange() {
+ return null;
+ }
+
+ @Override
+ protected Review getReview() {
+ return CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ Review myReview = getReview();
+ return super.isEnabled() && (myReview != null && CrucibleUtil.canAddCommentToReview(getReview()));
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractListenableReviewAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractListenableReviewAction.java
new file mode 100644
index 0000000..dabfb78
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractListenableReviewAction.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+
+public abstract class AbstractListenableReviewAction extends AbstractReviewAction implements IReviewAction {
+
+ private IReviewActionListener actionListener;
+
+ public AbstractListenableReviewAction(String text) {
+ super(text);
+ }
+
+ public void setActionListener(IReviewActionListener listener) {
+ actionListener = listener;
+ }
+
+ @Override
+ public final void run() {
+ if (actionListener != null) {
+ actionListener.actionAboutToRun(this);
+ }
+ run(this);
+ if (actionListener != null) {
+ actionListener.actionRan(this);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewAction.java
new file mode 100644
index 0000000..89fd7c9
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewAction.java
@@ -0,0 +1,115 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+/**
+ * Abstract action of review actions
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public abstract class AbstractReviewAction extends BaseSelectionListenerAction implements
+ IWorkbenchWindowActionDelegate {
+
+ protected IWorkbenchWindow workbenchWindow;
+
+ protected Review review;
+
+ public AbstractReviewAction(String text) {
+ super(text);
+ }
+
+ public void dispose() {
+ // ignore
+ }
+
+ public void init(IWorkbenchWindow window) {
+ this.workbenchWindow = window;
+ }
+
+ @Override
+ public void run() {
+ review = getReview();
+ run(this);
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ review = getReview();
+ if (review != null) {
+ action.setEnabled(true);
+ setEnabled(true);
+ } else {
+ action.setEnabled(false);
+ setEnabled(false);
+ }
+ }
+
+ protected IEditorPart getActiveEditor() {
+ IWorkbenchWindow window = workbenchWindow;
+ if (window == null) {
+ window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ }
+ if (window != null && window.getActivePage() != null) {
+ return window.getActivePage().getActiveEditor();
+ }
+ return null;
+ }
+
+ protected IEditorInput getEditorInputFromSelection(ISelection selection) {
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = ((IStructuredSelection) selection);
+ if (structuredSelection.getFirstElement() instanceof IEditorInput) {
+ return (IEditorInput) structuredSelection.getFirstElement();
+ }
+ }
+ return null;
+ }
+
+ protected String getTaskKey() {
+ if (review == null) {
+ return null;
+ }
+ return review.getPermId().getId();
+ }
+
+ protected String getTaskId() {
+ if (review == null) {
+ return null;
+ }
+ return CrucibleUtil.getTaskIdFromPermId(review.getPermId().getId());
+ }
+
+ protected abstract Review getReview();
+
+ protected TaskRepository getTaskRepository() {
+ if (review == null) {
+ return null;
+ }
+
+ return CrucibleUiUtil.getCrucibleTaskRepository(review);
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewFromResourcesAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewFromResourcesAction.java
new file mode 100644
index 0000000..4ec9edd
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AbstractReviewFromResourcesAction.java
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector.State;
+import com.atlassian.connector.eclipse.ui.actions.AbstractResourceAction;
+import com.atlassian.connector.eclipse.ui.commons.ResourceEditorBean;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class AbstractReviewFromResourcesAction extends AbstractResourceAction {
+
+ protected AbstractReviewFromResourcesAction(String text) {
+ super(text);
+ }
+
+ @Override
+ protected void processResources(List<ResourceEditorBean> selection, final Shell shell) {
+
+ final Set<ITeamUiResourceConnector> connectors = new HashSet<ITeamUiResourceConnector>();
+
+ for (ResourceEditorBean resourceBean : selection) {
+ ITeamUiResourceConnector connector = AtlassianTeamUiPlugin.getDefault()
+ .getTeamResourceManager()
+ .getTeamConnector(resourceBean.getResource());
+
+ if (connector != null) {
+ connectors.add(connector);
+ } else {
+ connectors.add(new LocalTeamResourceConnector());
+ }
+ }
+
+ if (connectors.size() > 1) {
+ MessageDialog.openInformation(shell, CrucibleUiPlugin.PLUGIN_ID,
+ "Cannot create review for more than one SCM provider at once.");
+ return;
+ } else if (connectors.size() == 0) {
+ MessageDialog.openInformation(shell, CrucibleUiPlugin.PLUGIN_ID,
+ "Cannot create review. No Atlassian SCM provider found.");
+ return;
+ }
+
+ if (selection.size() > 1
+ || (selection.size() == 1 && selection.get(0) != null && selection.get(0).getLineRange() == null)) {
+ // process workbench selection
+ processWorkbenchSelection(selection, connectors.iterator().next(), shell);
+ } else if (selection.size() == 1 && selection.get(0) != null && selection.get(0).getLineRange() != null) {
+ // process editor selection
+ processEditorSelection(selection.get(0), connectors.iterator().next(), shell);
+ } else {
+ // we should not be here
+ handleError(shell, "Cannot determine selection.");
+ return;
+ }
+
+ }
+
+ private void processEditorSelection(ResourceEditorBean selection, ITeamUiResourceConnector connector, Shell shell) {
+ boolean isPostCommit = false;
+
+ if (connector.isResourceAcceptedByFilter(selection.getResource(), State.SF_VERSIONED)
+ && !connector.isResourceAcceptedByFilter(selection.getResource(), State.SF_ANY_CHANGE)) {
+ // versioned and committed file found (without any local change)
+ // we need Crucible 2.1 to add such file to the review
+ isPostCommit = true;
+ }
+
+ openReviewWizard(selection, connector, isPostCommit, shell);
+ }
+
+ protected abstract void openReviewWizard(ResourceEditorBean selection, ITeamUiResourceConnector connector,
+ boolean isPostCommit, Shell shell);
+
+ private void processWorkbenchSelection(List<ResourceEditorBean> selection,
+ final ITeamUiResourceConnector teamConnector, final Shell shell) {
+
+ final List<IResource> resourcesList = new ArrayList<IResource>();
+
+ for (ResourceEditorBean resultBean : selection) {
+ resourcesList.add(resultBean.getResource());
+ }
+
+ final IResource[] resourcesArray = resourcesList.toArray(new IResource[resourcesList.size()]);
+
+ Job analyzeResource = new Job("Analyzing selected resources") {
+
+ public IStatus run(IProgressMonitor monitor) {
+ monitor.beginTask("Analyzing selected resources for Crucible compatibility", IProgressMonitor.UNKNOWN);
+
+ final boolean[] isCrucible21Required = { false };
+
+ try {
+ Collection<IResource> allResources = teamConnector.getResourcesByFilterRecursive(resourcesArray,
+ ITeamUiResourceConnector.State.SF_ALL);
+ for (IResource resource : allResources) {
+ if (teamConnector.isResourceAcceptedByFilter(resource, State.SF_VERSIONED)
+ && !teamConnector.isResourceAcceptedByFilter(resource, State.SF_ANY_CHANGE)) {
+ // versioned and committed file found (without any local change)
+ // we need Crucible 2.1 to add such file to the review
+ isCrucible21Required[0] = true;
+ break;
+ }
+ }
+ } finally {
+ monitor.done();
+ }
+
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ openReviewWizard(teamConnector, resourcesList, isCrucible21Required[0], shell);
+ }
+ });
+
+ return Status.OK_STATUS;
+ }
+ };
+
+ analyzeResource.setUser(true);
+ analyzeResource.schedule();
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection structuredSelection) {
+ if (!super.updateSelection(structuredSelection)) {
+ return false;
+ }
+
+ List<ResourceEditorBean> selection = getSelectionData();
+
+ if (selection == null) {
+ return false;
+ }
+
+ for (ResourceEditorBean resourceBean : selection) {
+ IResource resource = resourceBean.getResource();
+ if (resource instanceof IProject && !((IProject) resource).isOpen()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected void handleError(final Shell shell, String message) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID, message));
+ MessageDialog.openInformation(shell, CrucibleUiPlugin.PLUGIN_ID, message);
+ }
+
+ protected abstract void openReviewWizard(final ITeamUiResourceConnector teamConnector,
+ final List<IResource> resources, boolean isCrucible21Required, Shell shell);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddChangesetToActiveReviewAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddChangesetToActiveReviewAction.java
new file mode 100644
index 0000000..ea7a416
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddChangesetToActiveReviewAction.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.ActiveReviewManager.IReviewActivationListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.wizards.ReviewWizard;
+import com.atlassian.connector.eclipse.internal.crucible.ui.wizards.ReviewWizard.Type;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleAction;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.notification.CrucibleNotification;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.swt.widgets.Display;
+import java.util.Collection;
+import java.util.Set;
+
+public class AddChangesetToActiveReviewAction extends Action implements IReviewActivationListener {
+
+ private Review activeReview;
+
+ public AddChangesetToActiveReviewAction() {
+ setText("Add changesets...");
+ setToolTipText("Add changesets to the Review.");
+ setImageDescriptor(CrucibleImages.ADD_CHANGESET);
+ }
+
+ public void run() {
+ ReviewWizard wizard = new ReviewWizard(activeReview, Type.ADD_CHANGESET);
+ wizard.setWindowTitle("Add Changeset");
+ WizardDialog wd = new WizardDialog(WorkbenchUtil.getShell(), wizard);
+ wd.setBlockOnOpen(true);
+ wd.open();
+ };
+
+ public void reviewActivated(final ITask task, final Review review) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ activeReview = review;
+
+ Set<CrucibleAction> actions = activeReview.getActions();
+
+ setEnabled(activeReview != null && actions != null && actions.contains(CrucibleAction.MODIFY_FILES));
+ }
+ });
+ }
+
+ public void reviewDeactivated(ITask task, Review review) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ activeReview = null;
+ setEnabled(false);
+ }
+ });
+ }
+
+ public void reviewUpdated(ITask task, Review review, Collection<CrucibleNotification> differences) {
+ reviewActivated(task, review);
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddFileCommentAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddFileCommentAction.java
new file mode 100644
index 0000000..f7bddfb
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddFileCommentAction.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.commons.crucible.api.model.ReviewModelUtil;
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.dialogs.CrucibleAddCommentDialog;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+/**
+ * Action to add a general file comment to the active review
+ *
+ * @author Shawn Minto
+ * @author Thomas Ehrnhoefer
+ * @author Pawel Niewiadomski
+ */
+public class AddFileCommentAction extends BaseSelectionListenerAction {
+
+ private Review review;
+
+ private CrucibleFileInfo fileInfo;
+
+ @Override
+ public void run() {
+ TaskRepository taskRepository = CrucibleUiUtil.getCrucibleTaskRepository(review);
+ ITask task = CrucibleUiUtil.getCrucibleTask(review);
+
+ CrucibleClient client = CrucibleUiPlugin.getClient(taskRepository);
+ if (client == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to get client, please try to refresh"));
+ return;
+ }
+
+ CrucibleAddCommentDialog commentDialog = new CrucibleAddCommentDialog(WorkbenchUtil.getShell(), getText(),
+ review, task.getTaskKey(), task.getTaskId(), taskRepository, client);
+
+ commentDialog.setReviewItem(new CrucibleFile(fileInfo, true));
+ commentDialog.open();
+ }
+
+ public AddFileCommentAction() {
+ super("Add File Comment");
+ setEnabled(false);
+ setToolTipText("Add File Comment");
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return CrucibleImages.ADD_FILE_COMMENT;
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ this.review = null;
+ this.fileInfo = null;
+ if (selection.size() != 1) {
+ return false;
+ }
+
+ Object element = selection.getFirstElement();
+ if (element instanceof CrucibleFileInfo && selection.size() == 1) {
+ this.review = CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ if (this.review != null && CrucibleUtil.canAddCommentToReview(review)) {
+ this.fileInfo = (CrucibleFileInfo) element;
+ return true;
+ }
+ }
+
+ if (element instanceof Comment) {
+ this.review = CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ final VersionedComment parentVersionedComment = ReviewModelUtil.getParentVersionedComment((Comment) element);
+ if (parentVersionedComment != null) {
+ this.fileInfo = parentVersionedComment.getCrucibleFileInfo();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddGeneralCommentToFileAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddGeneralCommentToFileAction.java
new file mode 100644
index 0000000..6f69c89
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddGeneralCommentToFileAction.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IEditorInput;
+
+/**
+ * Action to add a general file comment to the active review
+ *
+ * @author Shawn Minto
+ * @author Thomas Ehrnhoefer
+ */
+public class AddGeneralCommentToFileAction extends AbstractAddCommentAction implements IReviewAction {
+
+ private CrucibleFile crucibleFile;
+
+ private IReviewActionListener actionListener;
+
+ private IEditorInput editorInput;
+
+ public AddGeneralCommentToFileAction() {
+ super("Create General File Comment...");
+ }
+
+ public AddGeneralCommentToFileAction(CrucibleFile crucibleFile) {
+ this();
+ this.crucibleFile = crucibleFile;
+ }
+
+ @Override
+ public void selectionChanged(IAction action, ISelection selection) {
+ super.selectionChanged(action, selection);
+
+ editorInput = getEditorInputFromSelection(selection);
+
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ if (crucibleFile != null && CrucibleUtil.canAddCommentToReview(getReview())
+ && CrucibleUiUtil.isFilePartOfActiveReview(crucibleFile)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected CrucibleFile getCrucibleFile() {
+ return crucibleFile;
+ }
+
+ @Override
+ protected LineRange getSelectedRange() {
+ return null;
+ }
+
+ @Override
+ protected String getDialogTitle() {
+ return "Create General File Comment";
+ }
+
+ @Override
+ public final void run() {
+ super.run();
+ if (actionListener != null) {
+ actionListener.actionRan(this);
+ }
+ }
+
+ @Override
+ public String getToolTipText() {
+ return "Add General File Comment...";
+ }
+
+ public void setActionListener(IReviewActionListener listener) {
+ this.actionListener = listener;
+ }
+
+ @Override
+ protected IEditorInput getEditorInput() {
+ return editorInput;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddLineCommentToFileAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddLineCommentToFileAction.java
new file mode 100644
index 0000000..192733e
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/AddLineCommentToFileAction.java
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.ICrucibleCompareSourceViewer;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.connector.eclipse.ui.commons.AtlassianUiUtil;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+
+/**
+ * Action for adding a comment to a line in the active review
+ *
+ * @author Shawn Minto
+ */
+public class AddLineCommentToFileAction extends AbstractAddCommentAction {
+
+ private LineRange selectedRange = null;
+
+ private CrucibleFile crucibleFile = null;
+
+ private ICrucibleCompareSourceViewer crucibleCompareSourceViewer;
+
+ private IEditorInput editorInput;
+
+ public AddLineCommentToFileAction() {
+ super("Create Line Comment...");
+ }
+
+ public AddLineCommentToFileAction(ICrucibleCompareSourceViewer crucibleCompareSourceViewer,
+ CrucibleFile crucibleFile) {
+ this();
+ this.crucibleCompareSourceViewer = crucibleCompareSourceViewer;
+ this.crucibleFile = crucibleFile;
+ }
+
+ @Override
+ protected String getDialogTitle() {
+ return "Create Line Comment";
+ }
+
+ @Override
+ public void selectionChanged(IAction action, ISelection selection) {
+ super.selectionChanged(action, selection);
+
+ editorInput = getEditorInputFromSelection(selection);
+
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ if (crucibleCompareSourceViewer != null) {
+ selectedRange = crucibleCompareSourceViewer.getSelection();
+ if (selectedRange != null && crucibleFile != null && CrucibleUtil.canAddCommentToReview(getReview())
+ && CrucibleUiUtil.isFilePartOfActiveReview(crucibleFile)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private LineRange getJavaEditorSelection(IEditorInput editorInput) {
+ LineRange lines = null;
+
+ IEditorPart editorPart = getActiveEditor();
+
+ if (editorPart != null && editorInput != null) {
+ lines = AtlassianUiUtil.getSelectedLineNumberRangeFromEditorInput(editorPart, editorInput);
+ if (lines == null) {
+ StatusHandler.log(new Status(IStatus.INFO, CrucibleUiPlugin.PLUGIN_ID,
+ "Editor is not an ITextEditor or there's no text selection available."));
+ }
+ }
+
+ return lines;
+ }
+
+ @Override
+ protected CrucibleFile getCrucibleFile() {
+ return crucibleFile;
+ }
+
+ @Override
+ public String getToolTipText() {
+ return "Add Line Comment...";
+ }
+
+ @Override
+ protected LineRange getSelectedRange() {
+ //if its the action from the compareeditor, get currently selected lines
+ if (crucibleCompareSourceViewer != null) {
+ return crucibleCompareSourceViewer.getSelection();
+ } else {
+ return getJavaEditorSelection(getEditorInput());
+ }
+ }
+
+ @Override
+ protected IEditorInput getEditorInput() {
+ return editorInput;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/CreateReviewFromResourcesAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/CreateReviewFromResourcesAction.java
new file mode 100644
index 0000000..5e8c3af
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/CreateReviewFromResourcesAction.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.wizards.RepositorySelectionWizard;
+import com.atlassian.connector.eclipse.internal.crucible.ui.wizards.ReviewWizard;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.ui.commons.ResourceEditorBean;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.jface.wizard.IWizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.mylyn.internal.tasks.core.ITaskRepositoryFilter;
+import org.eclipse.mylyn.internal.tasks.ui.wizards.SelectRepositoryPage;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author Jacek Jaroczynski
+ */
+@SuppressWarnings("restriction")
+public class CreateReviewFromResourcesAction extends AbstractReviewFromResourcesAction {
+
+ public CreateReviewFromResourcesAction() {
+ super("Create Review Action");
+ }
+
+ protected void openReviewWizard(final ResourceEditorBean selection, final ITeamUiResourceConnector connector,
+ boolean isPostCommit, final Shell shell) {
+ SelectRepositoryPage selectRepositoryPage = new SelectRepositoryPage(ITaskRepositoryFilter.ALL) {
+ @Override
+ protected IWizard createWizard(TaskRepository taskRepository) {
+ ReviewWizard wizard = new ReviewWizard(taskRepository, MiscUtil.buildHashSet(
+ ReviewWizard.Type.ADD_RESOURCES, ReviewWizard.Type.ADD_COMMENT_TO_FILE));
+ wizard.setRoots(connector, Arrays.asList(selection.getResource()));
+ wizard.setFilesCommentData(Arrays.asList(selection));
+ return wizard;
+ }
+ };
+
+ WizardDialog wd = new WizardDialog(WorkbenchUtil.getShell(),
+ new RepositorySelectionWizard(selectRepositoryPage));
+ wd.setBlockOnOpen(true);
+ wd.open();
+ }
+
+ protected void openReviewWizard(final ITeamUiResourceConnector teamConnector, final List<IResource> resources,
+ boolean isCrucible21Required, Shell shell) {
+ SelectRepositoryPage selectRepositoryPage = new SelectRepositoryPage(ITaskRepositoryFilter.ALL) {
+ @Override
+ protected IWizard createWizard(TaskRepository taskRepository) {
+ ReviewWizard wizard = new ReviewWizard(taskRepository, MiscUtil.buildHashSet(
+ ReviewWizard.Type.ADD_SCM_RESOURCES, ReviewWizard.Type.ADD_COMMENT_TO_FILE));
+ wizard.setRoots(teamConnector, resources);
+ return wizard;
+ }
+ };
+
+ WizardDialog wd = new WizardDialog(shell, new RepositorySelectionWizard(selectRepositoryPage));
+ wd.setBlockOnOpen(true);
+ wd.open();
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditActiveTaskAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditActiveTaskAction.java
new file mode 100644
index 0000000..e5a0215
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditActiveTaskAction.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.ActiveReviewManager.IReviewActivationListener;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.notification.CrucibleNotification;
+import org.eclipse.jface.action.Action;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonImages;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.ui.TasksUiUtil;
+import org.eclipse.swt.widgets.Display;
+import java.util.Collection;
+
+public class EditActiveTaskAction extends Action implements IReviewActivationListener {
+
+ private ITask activeTask;
+
+ public EditActiveTaskAction() {
+ setImageDescriptor(CommonImages.BROWSER_OPEN_TASK);
+ setText("Open Active Task");
+ setToolTipText("Open Active Task Editor");
+ setEnabled(false);
+ }
+
+ public void run() {
+ TasksUiUtil.openTask(activeTask);
+ }
+
+ public void reviewActivated(final ITask task, Review review) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ activeTask = task;
+ setEnabled(activeTask != null);
+ }
+ });
+ }
+
+ public void reviewDeactivated(ITask task, Review review) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ activeTask = null;
+ setEnabled(false);
+ }
+ });
+ }
+
+ public void reviewUpdated(ITask task, Review review, Collection<CrucibleNotification> differences) {
+ reviewActivated(task, review);
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditCommentAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditCommentAction.java
new file mode 100644
index 0000000..4b214b1
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/EditCommentAction.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.dialogs.CrucibleEditCommentDialog;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+/**
+ *
+ * @author Pawel Niewiadomski
+ */
+public class EditCommentAction extends BaseSelectionListenerAction implements IReviewAction {
+
+ private Review review;
+
+ private IReviewActionListener actionListener;
+
+ public EditCommentAction() {
+ super("Edit Comment");
+ setEnabled(false);
+ }
+
+ @Override
+ public void run() {
+ TaskRepository taskRepository = CrucibleUiUtil.getCrucibleTaskRepository(review);
+ ITask task = CrucibleUiUtil.getCrucibleTask(review);
+ CrucibleClient client = CrucibleUiPlugin.getClient(taskRepository);
+ if (client == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to get client, please try to refresh"));
+ return;
+ }
+
+ CrucibleEditCommentDialog commentDialog = new CrucibleEditCommentDialog(WorkbenchUtil.getShell(),
+ "Edit Comment", review, (Comment) getStructuredSelection().getFirstElement(), task.getTaskKey(),
+ task.getTaskId(), taskRepository, client);
+ commentDialog.open();
+
+ if (this.actionListener != null) {
+ this.actionListener.actionRan(this);
+ }
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return CrucibleImages.COMMENT_EDIT;
+ }
+
+ @Override
+ public String getToolTipText() {
+ return "Edit Comment";
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ this.review = null;
+
+ Object element = selection.getFirstElement();
+ if (element instanceof Comment && selection.size() == 1) {
+ this.review = getActiveReview();
+ if (this.review != null && CrucibleUiUtil.canModifyComment(review, (Comment) element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setActionListener(IReviewActionListener listener) {
+ this.actionListener = listener;
+ }
+
+ /**
+ * Return active review this action should be run against to. Review is associated during
+ * {@link #updateSelection(IStructuredSelection)}
+ *
+ * @return
+ */
+ protected Review getActiveReview() {
+ return CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/LocalTeamResourceConnector.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/LocalTeamResourceConnector.java
new file mode 100644
index 0000000..5383ba1
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/LocalTeamResourceConnector.java
@@ -0,0 +1,125 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.team.ui.AbstractTeamUiConnector;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.connector.eclipse.team.ui.LocalStatus;
+import com.atlassian.connector.eclipse.team.ui.ScmRepository;
+import com.atlassian.theplugin.commons.crucible.api.UploadItem;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.apache.commons.lang.NotImplementedException;
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+
+import java.util.Collection;
+import java.util.List;
+
+public class LocalTeamResourceConnector extends AbstractTeamUiConnector {
+
+ public boolean isEnabled() {
+ return true;
+ }
+
+ public LocalStatus getLocalRevision(IResource resource) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public ScmRepository getApplicableRepository(IResource resource) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getName() {
+ throw new NotImplementedException();
+ }
+
+ public boolean haveMatchingResourcesRecursive(IResource[] roots, State filter) {
+ throw new NotImplementedException();
+ }
+
+ public List<IResource> getResourcesByFilterRecursive(IResource[] roots, State filter) {
+ final List<IResource> result = MiscUtil.buildArrayList();
+ if (filter == State.SF_ALL || filter == State.SF_UNVERSIONED) {
+ IResourceVisitor visitor = new IResourceVisitor() {
+ public boolean visit(IResource resource) throws CoreException {
+ result.add(resource);
+ return true;
+ }
+ };
+
+ for (IResource root : roots) {
+ try {
+ root.accept(visitor);
+ } catch (CoreException e) {
+ StatusHandler.log(e.getStatus());
+ }
+ }
+ }
+ return result;
+ }
+
+ public Collection<UploadItem> getUploadItemsForResources(IResource[] resources, IProgressMonitor monitor)
+ throws CoreException {
+ List<UploadItem> items = MiscUtil.buildArrayList();
+ for (IResource resource : resources) {
+ if (resource.getType() != IResource.FILE) {
+ // ignore anything but files
+ continue;
+ }
+
+ final String fileName = getResourcePathWithProjectName(resource);
+
+ IFile file = ((IFile) resource);
+ byte[] newContent = getResourceContent(file.getContents());
+ items.add(new UploadItem(fileName, UploadItem.DEFAULT_CONTENT_TYPE, UploadItem.DEFAULT_CHARSET,
+ new byte[0], getContentType(file), getCharset(file), newContent.length == 0 ? EMPTY_ITEM
+ : newContent));
+ }
+ return items;
+ }
+
+ public IResource[] getMembersForContainer(IContainer element) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isResourceManagedBy(IResource resource) {
+ return true;
+ }
+
+ public boolean canHandleFile(IFile file) {
+ throw new NotImplementedException();
+ }
+
+ public CrucibleFile getCrucibleFileFromReview(Review activeReview, String fileUrl, String revision) {
+ throw new NotImplementedException();
+ }
+
+ public CrucibleFile getCrucibleFileFromReview(Review activeReview, IFile file) {
+ throw new NotImplementedException();
+ }
+
+ public Collection<ScmRepository> getRepositories(IProgressMonitor monitor) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isResourceAcceptedByFilter(IResource resource, State state) {
+ return (state == State.SF_ALL || state == State.SF_UNVERSIONED);
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/PostDraftCommentAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/PostDraftCommentAction.java
new file mode 100644
index 0000000..966f293
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/PostDraftCommentAction.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+public class PostDraftCommentAction extends BaseSelectionListenerAction implements IReviewAction {
+
+ private static final String PUBLISH_COMMENT = "Publish Comment";
+
+ private Review review;
+
+ private IReviewActionListener actionListener;
+
+ public PostDraftCommentAction() {
+ super(PUBLISH_COMMENT);
+ setEnabled(false);
+ }
+
+ public void run() {
+ final Comment comment = (Comment) getStructuredSelection().getFirstElement();
+// IAction action = new BackgroundJobReviewAction("Publish Comment", review,
+// WorkbenchUtil.getShell(), "Publishing selected comment for review " + review.getPermId().getId(),
+// CrucibleImages.COMMENT_POST, new RemoteCrucibleOperation() {
+// public void run(CrucibleServerFacade2 server, ConnectionCfg serverCfg)
+// throws CrucibleLoginException, RemoteApiException, ServerPasswordNotProvidedException {
+// server.publishComment(serverCfg, review.getPermId(), comment.getPermId());
+// }
+// }, true);
+// action.run();
+
+ if (actionListener != null) {
+ actionListener.actionRan(this);
+ }
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ review = null;
+
+ Object element = selection.getFirstElement();
+ if (element instanceof Comment && selection.size() == 1) {
+ review = getActiveReview();
+ if (review != null && CrucibleUiUtil.canModifyComment(review, (Comment) element)
+ && CrucibleUtil.canPublishDraft((Comment) element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected Review getActiveReview() {
+ return CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ }
+
+ public void setActionListener(IReviewActionListener listener) {
+ this.actionListener = listener;
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return CrucibleImages.COMMENT_POST;
+ }
+
+ @Override
+ public String getToolTipText() {
+ return PUBLISH_COMMENT;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/RemoveCommentAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/RemoveCommentAction.java
new file mode 100644
index 0000000..d324756
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/RemoveCommentAction.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+public class RemoveCommentAction extends BaseSelectionListenerAction implements IReviewAction {
+ private static String REMOVE_COMMENT = "Remove Comment";
+
+ private Review review;
+
+ private IReviewActionListener actionListener;
+
+ public RemoveCommentAction() {
+ super(REMOVE_COMMENT);
+ setEnabled(false);
+ }
+
+ public void run() {
+ final Comment comment = (Comment) getStructuredSelection().getFirstElement();
+// IAction action = new BackgroundJobReviewAction(getText(), review, WorkbenchUtil.getShell(),
+// "Removing a comment from review " + review.getPermId().getId(), CrucibleImages.COMMENT_DELETE,
+// new RemoteCrucibleOperation() {
+// public void run(CrucibleServerFacade2 server, ConnectionCfg serverCfg)
+// throws CrucibleLoginException, RemoteApiException, ServerPasswordNotProvidedException {
+// server.removeComment(serverCfg, review.getPermId(), comment);
+// }
+// }, true);
+// action.run();
+
+ if (actionListener != null) {
+ actionListener.actionRan(this);
+ }
+ };
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ this.review = null;
+
+ Object element = selection.getFirstElement();
+ if (element instanceof Comment && selection.size() == 1) {
+ this.review = getActiveReview();
+ if (this.review != null && CrucibleUiUtil.canModifyComment(review, (Comment) element)) {
+ return ((Comment) element).getReplies().size() == 0;
+ }
+ }
+ return false;
+ }
+
+ protected Review getActiveReview() {
+ return CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ }
+
+ public void setActionListener(IReviewActionListener listener) {
+ this.actionListener = listener;
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return CrucibleImages.COMMENT_DELETE;
+ }
+
+ @Override
+ public String getToolTipText() {
+ return REMOVE_COMMENT;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/ReplyToCommentAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/ReplyToCommentAction.java
new file mode 100644
index 0000000..61e08f9
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/ReplyToCommentAction.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.actions;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.dialogs.CrucibleAddCommentDialog;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TasksUiImages;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+/**
+ * Action to reply to a comment
+ *
+ * @author Shawn Minto
+ * @author Thomas Ehrnhoefer
+ * @author Pawel Niewiadomski
+ */
+public class ReplyToCommentAction extends BaseSelectionListenerAction implements IReviewAction {
+
+ private static final String REPLY_TO_COMMENT = "Reply to Comment";
+
+ private Review review;
+
+ private IReviewActionListener actionListener;
+
+ public ReplyToCommentAction() {
+ super(REPLY_TO_COMMENT);
+ setEnabled(false);
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return TasksUiImages.COMMENT_REPLY_SMALL;
+ }
+
+ @Override
+ public void run() {
+ TaskRepository taskRepository = CrucibleUiUtil.getCrucibleTaskRepository(review);
+ ITask task = CrucibleUiUtil.getCrucibleTask(review);
+ CrucibleClient client = CrucibleUiPlugin.getClient(taskRepository);
+ if (client == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to get client, please try to refresh"));
+ return;
+ }
+
+ CrucibleAddCommentDialog commentDialog = new CrucibleAddCommentDialog(WorkbenchUtil.getShell(),
+ REPLY_TO_COMMENT, review, task.getTaskKey(), task.getTaskId(), taskRepository, client);
+
+ commentDialog.setParentComment((Comment) getStructuredSelection().getFirstElement());
+ commentDialog.open();
+
+ if (actionListener != null) {
+ actionListener.actionRan(this);
+ }
+ }
+
+ @Override
+ public String getToolTipText() {
+ return REPLY_TO_COMMENT;
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ this.review = null;
+
+ Object element = selection.getFirstElement();
+ if (element instanceof Comment && selection.size() == 1) {
+ this.review = getActiveReview();
+ if (this.review != null && CrucibleUtil.canAddCommentToReview(review)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected Review getActiveReview() {
+ return CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview();
+ }
+
+ public void setActionListener(IReviewActionListener listener) {
+ this.actionListener = listener;
+ }
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/actions.properties b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/actions.properties
new file mode 100644
index 0000000..c3849ef
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/actions/actions.properties
@@ -0,0 +1,9 @@
+action.navigateNext.label=Ne&xt Comment
+action.navigateNext.tooltip=Next Comment
+action.navigateNext.description=Navigate to the Next Comment
+action.navigateNext.image=next_nav.gif
+
+action.navigatePrevious.label=Pre&vious Comment
+action.navigatePrevious.tooltip=Previous Comment
+action.navigatePrevious.description=Navigate to the Previous Comment
+action.navigatePrevious.image=prev_nav.gif
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHover.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHover.java
new file mode 100644
index 0000000..39104a9
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHover.java
@@ -0,0 +1,405 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import org.eclipse.core.commands.Command;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.internal.text.html.HTMLPrinter;
+import org.eclipse.jface.text.AbstractInformationControlManager;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IInformationControlCreator;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.jface.text.information.IInformationProvider;
+import org.eclipse.jface.text.information.IInformationProviderExtension;
+import org.eclipse.jface.text.information.IInformationProviderExtension2;
+import org.eclipse.jface.text.information.InformationPresenter;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.CompositeRuler;
+import org.eclipse.jface.text.source.IAnnotationHover;
+import org.eclipse.jface.text.source.IAnnotationHoverExtension;
+import org.eclipse.jface.text.source.IAnnotationHoverExtension2;
+import org.eclipse.jface.text.source.IAnnotationModel;
+import org.eclipse.jface.text.source.ILineRange;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.ISourceViewerExtension2;
+import org.eclipse.jface.text.source.IVerticalRulerInfo;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.jface.text.source.projection.AnnotationBag;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.commands.ICommandService;
+import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
+import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class to determine the annotations to show the hover for. This class delegates to a parent hover if it exists.
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleAnnotationHover implements IAnnotationHover, IAnnotationHoverExtension, IAnnotationHoverExtension2 {
+
+ private final IAnnotationHover parentHover;
+
+ private final IInformationControlCreator informationControlCreator;
+
+ private static ISourceViewer currentSourceViewer;
+
+ private static CrucibleAnnotationHover currentAnnotationHover;
+
+ public CrucibleAnnotationHover(IAnnotationHover hover) {
+ this.parentHover = hover;
+ informationControlCreator = new CrucibleInformationControlCreator();
+ }
+
+ public void dispose() {
+ // ignore for now
+ }
+
+ public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) {
+ List<CrucibleCommentAnnotation> commentAnnotations = getCrucibleAnnotationsForLine(sourceViewer, lineNumber);
+ if (commentAnnotations != null && commentAnnotations.size() > 0) {
+
+ if (commentAnnotations.size() == 1) {
+ CrucibleCommentAnnotation annotation = commentAnnotations.get(0);
+ String message = annotation.getText();
+ if (message != null && message.trim().length() > 0) {
+ return formatSingleMessage(message);
+ }
+
+ } else {
+
+ List<String> messages = new ArrayList<String>();
+ for (CrucibleCommentAnnotation annotation : commentAnnotations) {
+ String message = annotation.getText();
+ if (message != null && message.trim().length() > 0) {
+ messages.add(message.trim());
+ }
+ }
+
+ if (messages.size() == 1) {
+ return formatSingleMessage(messages.get(0));
+ }
+
+ if (messages.size() > 1) {
+ return formatMultipleMessages(messages);
+ }
+ }
+ } else {
+ if (parentHover != null) {
+ return parentHover.getHoverInfo(sourceViewer, lineNumber);
+ }
+ }
+ return null;
+ }
+
+ public IInformationControlCreator getHoverControlCreator() {
+ return informationControlCreator;
+ }
+
+ public boolean canHandleMouseCursor() {
+ return true;
+ }
+
+ public boolean canHandleMouseWheel() {
+ return true; // does not work on Ubuntu, but it should be here (maybe works on Windows ;))
+ }
+
+ public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) {
+ List<CrucibleCommentAnnotation> annotationsForLine = getCrucibleAnnotationsForLine(sourceViewer,
+ lineRange.getStartLine());
+ if (annotationsForLine == null || annotationsForLine.size() == 0) {
+ return getHoverInfo(sourceViewer, lineRange.getStartLine());
+ } else {
+ return new CrucibleAnnotationHoverInput(annotationsForLine);
+ }
+ }
+
+ public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) {
+ currentAnnotationHover = this;
+ currentSourceViewer = viewer;
+ List<CrucibleCommentAnnotation> commentAnnotations = getCrucibleAnnotationsForLine(viewer, lineNumber);
+ if (commentAnnotations != null && commentAnnotations.size() > 0) {
+ IDocument document = viewer.getDocument();
+ int lowestStart = Integer.MAX_VALUE;
+ int highestEnd = 0;
+ for (Annotation a : commentAnnotations) {
+ if (a instanceof CrucibleCommentAnnotation) {
+ Position p = ((CrucibleCommentAnnotation) a).getPosition();
+ try {
+
+ int start = document.getLineOfOffset(p.offset);
+ int end = document.getLineOfOffset(p.offset + p.length);
+
+ if (start < lowestStart) {
+ lowestStart = start;
+ }
+
+ if (end > highestEnd) {
+ highestEnd = end;
+ }
+ } catch (BadLocationException e) {
+ // ignore
+ }
+ }
+ }
+ if (lowestStart != Integer.MAX_VALUE) {
+ return new LineRange(lowestStart, highestEnd - lowestStart);
+ } else {
+ return new LineRange(lineNumber, 1);
+ }
+ }
+
+ return new LineRange(lineNumber, 1);
+ }
+
+ @SuppressWarnings("restriction")
+ protected String formatSingleMessage(String message) {
+ StringBuffer buffer = new StringBuffer();
+ HTMLPrinter.addPageProlog(buffer);
+ HTMLPrinter.addParagraph(buffer, HTMLPrinter.convertToHTMLContent(message));
+ HTMLPrinter.addPageEpilog(buffer);
+ return buffer.toString();
+ }
+
+ @SuppressWarnings("restriction")
+ protected String formatMultipleMessages(List<String> messages) {
+ StringBuffer buffer = new StringBuffer();
+ HTMLPrinter.addPageProlog(buffer);
+ HTMLPrinter.addParagraph(buffer, HTMLPrinter.convertToHTMLContent("There are multiple comments on this line"));
+
+ HTMLPrinter.startBulletList(buffer);
+ for (String message : messages) {
+ HTMLPrinter.addBullet(buffer, HTMLPrinter.convertToHTMLContent(message));
+ }
+ HTMLPrinter.endBulletList(buffer);
+
+ HTMLPrinter.addPageEpilog(buffer);
+ return buffer.toString();
+ }
+
+ private boolean isRulerLine(Position position, IDocument document, int line) {
+ if (position.getOffset() > -1 && position.getLength() > -1) {
+ try {
+ return line == document.getLineOfOffset(position.getOffset());
+ } catch (BadLocationException x) {
+ // ignore
+ }
+ }
+ return false;
+ }
+
+ private IAnnotationModel getAnnotationModel(ISourceViewer viewer) {
+ if (viewer instanceof ISourceViewerExtension2) {
+ ISourceViewerExtension2 extension = (ISourceViewerExtension2) viewer;
+ return extension.getVisualAnnotationModel();
+ }
+ return viewer.getAnnotationModel();
+ }
+
+ private boolean includeAnnotation(Annotation annotation, Position position,
+ List<CrucibleCommentAnnotation> annotations) {
+ if (!(annotation instanceof CrucibleCommentAnnotation)) {
+ return false;
+ }
+
+ return (annotation != null && !annotations.contains(annotation));
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<CrucibleCommentAnnotation> getCrucibleAnnotationsForLine(ISourceViewer viewer, int line) {
+ IAnnotationModel model = getAnnotationModel(viewer);
+ if (model == null) {
+ return null;
+ }
+
+ IDocument document = viewer.getDocument();
+ List<CrucibleCommentAnnotation> commentAnnotations = new ArrayList<CrucibleCommentAnnotation>();
+ Iterator<Annotation> iterator = model.getAnnotationIterator();
+
+ while (iterator.hasNext()) {
+ Annotation annotation = iterator.next();
+
+ Position position = model.getPosition(annotation);
+ if (position == null) {
+ continue;
+ }
+
+ if (!isRulerLine(position, document, line)) {
+ continue;
+ }
+
+ if (annotation instanceof AnnotationBag) {
+ AnnotationBag bag = (AnnotationBag) annotation;
+ Iterator<Annotation> e = bag.iterator();
+ while (e.hasNext()) {
+ annotation = e.next();
+ position = model.getPosition(annotation);
+ if (position != null && includeAnnotation(annotation, position, commentAnnotations)
+ && annotation instanceof CrucibleCommentAnnotation) {
+ commentAnnotations.add((CrucibleCommentAnnotation) annotation);
+ }
+ }
+ continue;
+ }
+
+ if (includeAnnotation(annotation, position, commentAnnotations)
+ && annotation instanceof CrucibleCommentAnnotation) {
+ commentAnnotations.add((CrucibleCommentAnnotation) annotation);
+ }
+ }
+
+ return commentAnnotations;
+ }
+
+ /**
+ * Tries to make an annotation hover focusable (or "sticky").
+ *
+ * @return <code>true</code> if successful, <code>false</code> otherwise
+ */
+ public static boolean makeAnnotationHoverFocusable() {
+ // check sourceviewer and hover
+ if (currentSourceViewer == null || currentSourceViewer.getTextWidget().isDisposed()
+ || currentAnnotationHover == null) {
+ return false;
+ }
+
+ IVerticalRulerInfo info = null;
+ try {
+ Method declaredMethod2 = SourceViewer.class.getDeclaredMethod("getVerticalRuler");
+ declaredMethod2.setAccessible(true);
+ info = (CompositeRuler) declaredMethod2.invoke(currentSourceViewer);
+ } catch (Exception e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Error getting CompareEditor's vertical ruler. ", e));
+ }
+
+ if (info == null) {
+ return false;
+ }
+
+ int line = info.getLineOfLastMouseButtonActivity();
+ if (line == -1) {
+ return false;
+ }
+
+ try {
+
+ // compute the hover information
+ Object hoverInfo;
+ if (currentAnnotationHover instanceof IAnnotationHoverExtension) {
+ IAnnotationHoverExtension extension = currentAnnotationHover;
+ ILineRange hoverLineRange = extension.getHoverLineRange(currentSourceViewer, line);
+ if (hoverLineRange == null) {
+ return false;
+ }
+ final int maxVisibleLines = Integer.MAX_VALUE;
+ hoverInfo = extension.getHoverInfo(currentSourceViewer, hoverLineRange, maxVisibleLines);
+ } else {
+ hoverInfo = currentAnnotationHover.getHoverInfo(currentSourceViewer, line);
+ }
+
+ // hover region: the beginning of the concerned line to place the control right over the line
+ IDocument document = currentSourceViewer.getDocument();
+ int offset = document.getLineOffset(line);
+ String partitioning = new TextSourceViewerConfiguration().getConfiguredDocumentPartitioning(currentSourceViewer);
+ String contentType = TextUtilities.getContentType(document, partitioning, offset, true);
+
+ IInformationControlCreator controlCreator = null;
+ if (currentAnnotationHover instanceof IInformationProviderExtension2) {
+ IInformationProviderExtension2 provider = (IInformationProviderExtension2) currentAnnotationHover;
+ controlCreator = provider.getInformationPresenterControlCreator();
+ } else if (currentAnnotationHover instanceof IAnnotationHoverExtension) {
+ controlCreator = ((IAnnotationHoverExtension) currentAnnotationHover).getHoverControlCreator();
+ }
+
+ IInformationProvider informationProvider = new InformationProvider(new Region(offset, 0), hoverInfo,
+ controlCreator);
+
+ CrucibleCommentPopupDialog dialog = CrucibleCommentPopupDialog.getCurrentPopupDialog();
+ if (dialog != null) {
+
+ InformationPresenter fInformationPresenter = dialog.getInformationControl().getInformationPresenter();
+ fInformationPresenter.setSizeConstraints(100, 12, true, true);
+ fInformationPresenter.install(currentSourceViewer);
+ fInformationPresenter.setDocumentPartitioning(partitioning);
+ fInformationPresenter.setOffset(offset);
+ fInformationPresenter.setAnchor(AbstractInformationControlManager.ANCHOR_RIGHT);
+ fInformationPresenter.setMargins(4, 0); // AnnotationBarHoverManager sets (5,0), minus SourceViewer.GAP_SIZE_1
+ fInformationPresenter.setInformationProvider(informationProvider, contentType);
+ fInformationPresenter.showInformation();
+
+ // remove our own handler as F2 focus handler
+ ICommandService commandService = (ICommandService) PlatformUI.getWorkbench().getService(
+ ICommandService.class);
+ Command showInfoCommand = commandService.getCommand(ITextEditorActionDefinitionIds.SHOW_INFORMATION);
+ showInfoCommand.setHandler(null);
+
+ return true;
+ }
+
+ } catch (BadLocationException e) {
+ return false;
+ }
+ return false;
+ }
+
+ /**
+ * Information provider used to present focusable information shells.
+ *
+ * @since 3.3
+ */
+ private static final class InformationProvider implements IInformationProvider, IInformationProviderExtension,
+ IInformationProviderExtension2 {
+
+ private final IRegion fHoverRegion;
+
+ private final Object fHoverInfo;
+
+ private final IInformationControlCreator fControlCreator;
+
+ InformationProvider(IRegion hoverRegion, Object hoverInfo, IInformationControlCreator controlCreator) {
+ fHoverRegion = hoverRegion;
+ fHoverInfo = hoverInfo;
+ fControlCreator = controlCreator;
+ }
+
+ public IRegion getSubject(ITextViewer textViewer, int invocationOffset) {
+ return fHoverRegion;
+ }
+
+ @Deprecated
+ public String getInformation(ITextViewer textViewer, IRegion subject) {
+ return fHoverInfo.toString();
+ }
+
+ public Object getInformation2(ITextViewer textViewer, IRegion subject) {
+ return fHoverInfo;
+ }
+
+ public IInformationControlCreator getInformationPresenterControlCreator() {
+ return fControlCreator;
+ }
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHoverInput.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHoverInput.java
new file mode 100644
index 0000000..a7ba5e6
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationHoverInput.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import java.util.List;
+
+/**
+ * Data model to represent the annotations that we need to display in the hover.
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleAnnotationHoverInput {
+
+ private final List<CrucibleCommentAnnotation> annotations;
+
+ public CrucibleAnnotationHoverInput(List<CrucibleCommentAnnotation> annotations) {
+ this.annotations = annotations;
+ }
+
+ public boolean containsInput() {
+ return annotations != null && annotations.size() > 0;
+ }
+
+ public List<CrucibleCommentAnnotation> getCrucibleAnnotations() {
+ return annotations;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModel.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModel.java
new file mode 100644
index 0000000..f13086e
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModel.java
@@ -0,0 +1,361 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.connector.commons.misc.IntRanges;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentListener;
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.AnnotationModelEvent;
+import org.eclipse.jface.text.source.IAnnotationModel;
+import org.eclipse.jface.text.source.IAnnotationModelListener;
+import org.eclipse.jface.text.source.IAnnotationModelListenerExtension;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.texteditor.ITextEditor;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The model for the annotations
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleAnnotationModel implements IAnnotationModel, ICrucibleAnnotationModel {
+
+ private final Set<CrucibleCommentAnnotation> annotations = new HashSet<CrucibleCommentAnnotation>(32);
+
+ private final Set<IAnnotationModelListener> annotationModelListeners = new HashSet<IAnnotationModelListener>(2);
+
+ private final ITextEditor textEditor;
+
+ private final IEditorInput editorInput;
+
+ private IDocument editorDocument;
+
+ private CrucibleFile crucibleFile;
+
+ private boolean annotated = false;
+
+ private Review review;
+
+ private final IDocumentListener documentListener = new IDocumentListener() {
+ public void documentChanged(DocumentEvent event) {
+ updateAnnotations(false);
+ }
+
+ public void documentAboutToBeChanged(DocumentEvent event) {
+ }
+ };
+
+ public CrucibleAnnotationModel(ITextEditor editor, IEditorInput editorInput, IDocument document,
+ CrucibleFile crucibleFile, Review review) {
+ this.textEditor = editor;
+ this.editorInput = editorInput;
+ this.editorDocument = document;
+ this.crucibleFile = crucibleFile;
+ this.review = review;
+ updateAnnotations(true);
+ }
+
+ protected void updateAnnotations(boolean force) {
+
+ boolean annotate = false;
+
+ // TODO make sure that the local files is in sync otherwise remove the annotations
+
+ if (textEditor == null && editorInput == null && editorDocument != null) {
+ annotate = true;
+ } else {
+ if (editorDocument == null) {
+ annotate = false;
+ } else if (!textEditor.isDirty() && editorInput != null && crucibleFile != null) {
+ annotate = true;
+ } else {
+ annotate = false;
+ }
+ }
+
+ if (annotate) {
+ if (!annotated || force) {
+ createAnnotations();
+ annotated = true;
+ }
+ } else {
+ if (annotated) {
+ clear();
+ annotated = false;
+ }
+ }
+ }
+
+ protected void clear() {
+ AnnotationModelEvent event = new AnnotationModelEvent(this);
+ clear(event);
+ fireModelChanged(event);
+ }
+
+ protected void clear(AnnotationModelEvent event) {
+ for (CrucibleCommentAnnotation commentAnnotation : annotations) {
+ event.annotationRemoved(commentAnnotation, commentAnnotation.getPosition());
+ }
+ annotations.clear();
+ }
+
+ protected void createAnnotations() {
+ AnnotationModelEvent event = new AnnotationModelEvent(this);
+ clear(event);
+
+ if (crucibleFile != null) {
+
+ for (VersionedComment comment : crucibleFile.getCrucibleFileInfo().getVersionedComments()) {
+ createCommentAnnotation(event, comment);
+ }
+ }
+
+ fireModelChanged(event);
+ }
+
+ private void createCommentAnnotation(AnnotationModelEvent event, VersionedComment comment) {
+ try {
+
+ int startLine = 0;
+ int endLine = 0;
+
+ final Map<String, IntRanges> lineRanges = comment.getLineRanges();
+ if (lineRanges != null && !lineRanges.isEmpty()) {
+
+ final String displayedRev = crucibleFile.getSelectedFile().getRevision();
+ final IntRanges intRanges = lineRanges.get(displayedRev);
+ if (intRanges != null) {
+ startLine = intRanges.getTotalMin();
+ endLine = intRanges.getTotalMax();
+ } else {
+ return;
+ }
+ } else {
+
+ // if fromLineInfo and not toLineInfo and not oldFile -> old line comment but current model is new file
+ if (comment.isFromLineInfo() && !comment.isToLineInfo() && !crucibleFile.isOldFile()) {
+ return;
+ }
+
+ // if toLineInfo and not fromLineInfo and and oldfile -> new line comment but current model is old file
+ if (comment.isToLineInfo() && !comment.isFromLineInfo() && crucibleFile.isOldFile()) {
+ return;
+ }
+ startLine = comment.getToStartLine();
+ if (crucibleFile.isOldFile()) {
+ startLine = comment.getFromStartLine();
+ }
+
+ endLine = comment.getToEndLine();
+ if (crucibleFile.isOldFile()) {
+ endLine = comment.getFromEndLine();
+ }
+ }
+
+ int offset = 0;
+ int length = 0;
+ if (startLine != 0) {
+ offset = editorDocument.getLineOffset(startLine - 1);
+ if (endLine == 0) {
+ endLine = startLine;
+ }
+ length = Math.max(editorDocument.getLineOffset(endLine - 1) - offset, 0);
+
+ }
+ CrucibleCommentAnnotation ca = new CrucibleCommentAnnotation(offset, length, comment,
+ crucibleFile.getCrucibleFileInfo(), review);
+ annotations.add(ca);
+ event.annotationAdded(ca);
+
+ } catch (BadLocationException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, "Unable to add annotation.", e));
+ }
+ }
+
+ public void addAnnotationModelListener(IAnnotationModelListener listener) {
+ if (!annotationModelListeners.contains(listener)) {
+ annotationModelListeners.add(listener);
+ fireModelChanged(new AnnotationModelEvent(this, true));
+ }
+ }
+
+ public void removeAnnotationModelListener(IAnnotationModelListener listener) {
+ annotationModelListeners.remove(listener);
+ }
+
+ protected void fireModelChanged(AnnotationModelEvent event) {
+ event.markSealed();
+ if (!event.isEmpty()) {
+ for (IAnnotationModelListener listener : annotationModelListeners) {
+ if (listener instanceof IAnnotationModelListenerExtension) {
+ ((IAnnotationModelListenerExtension) listener).modelChanged(event);
+ } else {
+ listener.modelChanged(this);
+ }
+ }
+ }
+ }
+
+ public void connect(IDocument document) {
+
+ for (CrucibleCommentAnnotation commentAnnotation : annotations) {
+ try {
+ document.addPosition(commentAnnotation.getPosition());
+ } catch (BadLocationException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ }
+ }
+ document.addDocumentListener(documentListener);
+ }
+
+ public void disconnect(IDocument document) {
+
+ for (CrucibleCommentAnnotation commentAnnotation : annotations) {
+ document.removePosition(commentAnnotation.getPosition());
+ }
+
+ document.removeDocumentListener(documentListener);
+ }
+
+ public void addAnnotation(Annotation annotation, Position position) {
+ // do nothing, we do not support external modification
+ }
+
+ public void removeAnnotation(Annotation annotation) {
+ // do nothing, we do not support external modification
+ }
+
+ public Iterator<CrucibleCommentAnnotation> getAnnotationIterator() {
+ return annotations.iterator();
+ }
+
+ public Position getPosition(Annotation annotation) {
+ if (annotation instanceof CrucibleCommentAnnotation) {
+ return ((CrucibleCommentAnnotation) annotation).getPosition();
+ } else {
+ // we dont understand any other annotations
+ return null;
+ }
+ }
+
+ public void updateCrucibleFile(CrucibleFile newCrucibleFile, Review newReview) {
+ // TODO we could just update the annotations appropriately instead of remove and re-add
+ this.review = newReview;
+ this.crucibleFile = newCrucibleFile;
+ updateAnnotations(true);
+ }
+
+ public CrucibleFile getCrucibleFile() {
+ return crucibleFile;
+ }
+
+ /**
+ * Returns the first annotation that this knows about for the given offset in the document
+ */
+ public CrucibleCommentAnnotation getFirstAnnotationForOffset(int offset) {
+ for (CrucibleCommentAnnotation annotation : annotations) {
+ if (annotation.getPosition().offset <= offset
+ && (annotation.getPosition().length + annotation.getPosition().offset) >= offset) {
+ return annotation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first annotation that this knows about for the given offset in the document
+ */
+ public List<CrucibleCommentAnnotation> getAnnotationsForOffset(int offset) {
+ List<CrucibleCommentAnnotation> result = MiscUtil.buildArrayList();
+ for (CrucibleCommentAnnotation annotation : this.annotations) {
+ if (annotation.getPosition().offset <= offset
+ && (annotation.getPosition().length + annotation.getPosition().offset) >= offset) {
+ result.add(annotation);
+ }
+ }
+ return result;
+ }
+
+ public void setEditorDocument(IDocument editorDocument) {
+ this.editorDocument = editorDocument;
+ updateAnnotations(true);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime
+ * result
+ + ((crucibleFile.getCrucibleFileInfo().getFileDescriptor().getAbsoluteUrl() == null) ? 0
+ : crucibleFile.getCrucibleFileInfo().getFileDescriptor().getAbsoluteUrl().hashCode());
+ result = prime
+ * result
+ + ((crucibleFile.getCrucibleFileInfo().getFileDescriptor().getRevision() == null) ? 0
+ : crucibleFile.getCrucibleFileInfo().getFileDescriptor().getRevision().hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CrucibleAnnotationModel other = (CrucibleAnnotationModel) obj;
+ if (crucibleFile.getCrucibleFileInfo().getFileDescriptor().getAbsoluteUrl() == null) {
+ if (other.crucibleFile.getCrucibleFileInfo().getFileDescriptor().getAbsoluteUrl() != null) {
+ return false;
+ }
+ } else if (!crucibleFile.getCrucibleFileInfo()
+ .getFileDescriptor()
+ .getAbsoluteUrl()
+ .equals(other.crucibleFile.getCrucibleFileInfo().getFileDescriptor().getAbsoluteUrl())) {
+ return false;
+ }
+ if (crucibleFile.getCrucibleFileInfo().getFileDescriptor().getRevision() == null) {
+ if (other.crucibleFile.getCrucibleFileInfo().getFileDescriptor().getRevision() != null) {
+ return false;
+ }
+ } else if (!crucibleFile.getCrucibleFileInfo()
+ .getFileDescriptor()
+ .getRevision()
+ .equals(other.crucibleFile.getCrucibleFileInfo().getFileDescriptor().getRevision())) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModelManager.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModelManager.java
new file mode 100644
index 0000000..6b54209
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleAnnotationModelManager.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.operations.CrucibleFileInfoCompareEditorInput;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.compare.internal.CompareEditor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * Class to manage the annotation model for the open editors
+ *
+ * @author Shawn Minto
+ */
+public final class CrucibleAnnotationModelManager {
+
+ private CrucibleAnnotationModelManager() {
+ // ignore
+ }
+
+ public static void updateAllOpenEditors(Review activeReview) {
+ for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
+ for (IWorkbenchPage page : window.getPages()) {
+ for (IEditorReference editorReference : page.getEditorReferences()) {
+ IWorkbenchPart editorPart = editorReference.getPart(false);
+ if (editorPart instanceof CompareEditor) {
+ update((CompareEditor) editorPart, activeReview);
+ }
+ }
+ }
+ }
+ }
+
+ private static void update(CompareEditor editor, Review activeReview) {
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof CrucibleFileInfoCompareEditorInput) {
+ if (!CrucibleUiPlugin.getDefault().getActiveReviewManager().isReviewActive() || activeReview == null
+ || CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview() != activeReview) {
+ return;
+ }
+ ((CrucibleFileInfoCompareEditorInput) editorInput).getAnnotationModelToAttach().updateCrucibleFile(
+ activeReview);
+ }
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentAnnotation.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentAnnotation.java
new file mode 100644
index 0000000..880ab89
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentAnnotation.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.source.Annotation;
+
+/**
+ * Class to represent a comment in a Crucible review
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleCommentAnnotation extends Annotation {
+ public static final String COMMENT_ANNOTATION_ID = "com.atlassian.connector.eclipse.cruicible.ui.comment.annotation";
+
+ private final Position position;
+
+ private final VersionedComment comment;
+
+ private final CrucibleFileInfo crucibleFileInfo;
+
+ private final Review review;
+
+ public CrucibleCommentAnnotation(int offset, int length, VersionedComment comment,
+ CrucibleFileInfo crucibleFileInfo, Review review) {
+ super(COMMENT_ANNOTATION_ID, false, null);
+ position = new Position(offset, length);
+ this.comment = comment;
+ this.review = review;
+ this.crucibleFileInfo = crucibleFileInfo;
+ }
+
+ public Position getPosition() {
+ return position;
+ }
+
+ @Override
+ public String getText() {
+ return comment.getAuthor().getDisplayName() + " - " + comment.getMessage();
+ }
+
+ public VersionedComment getVersionedComment() {
+ return comment;
+ }
+
+ public CrucibleFileInfo getCrucibleFileInfo() {
+ return crucibleFileInfo;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((comment == null) ? 0 : comment.hashCode());
+ result = prime * result + ((position == null) ? 0 : position.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof CrucibleCommentAnnotation)) {
+ return false;
+ }
+ final CrucibleCommentAnnotation other = (CrucibleCommentAnnotation) obj;
+ if (comment == null) {
+ if (other.comment != null) {
+ return false;
+ }
+ } else if (!comment.equals(other.comment)) {
+ return false;
+ }
+ if (position == null) {
+ if (other.position != null) {
+ return false;
+ }
+ } else if (!position.equals(other.position)) {
+ return false;
+ }
+ return true;
+ }
+
+ public Review getReview() {
+ return review;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentPopupDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentPopupDialog.java
new file mode 100644
index 0000000..3a177ae
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCommentPopupDialog.java
@@ -0,0 +1,217 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts.VersionedCommentPart;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.PopupDialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.forms.IFormColors;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+
+/**
+ * Popup to show the information about the annotation in
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleCommentPopupDialog extends PopupDialog implements IReviewActionListener {
+
+ private static final int MAX_WIDTH = 500;
+
+ private int maxWidth;
+
+ private CrucibleAnnotationHoverInput annotationInput;
+
+ private FormToolkit toolkit;
+
+ private Composite composite;
+
+ private Label focusLabel;
+
+ private ScrolledComposite scrolledComposite;
+
+ private CrucibleInformationControl informationControl;
+
+ private static CrucibleCommentPopupDialog currentPopupDialog;
+
+ public CrucibleCommentPopupDialog(Shell parent, int shellStyle) {
+ super(parent, shellStyle, false, false, false, false, false, null, null);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+
+ if (toolkit == null) {
+ toolkit = new FormToolkit(getShell().getDisplay());
+ }
+
+ scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL);
+ scrolledComposite.setExpandHorizontal(true);
+ scrolledComposite.setExpandVertical(true);
+ toolkit.adapt(scrolledComposite);
+
+ composite = toolkit.createComposite(scrolledComposite, SWT.NONE);
+ composite.setLayout(new GridLayout());
+ scrolledComposite.setContent(composite);
+
+ parent.setBackground(toolkit.getColors().getBackground());
+ getShell().setBackground(toolkit.getColors().getBackground());
+
+ return composite;
+ }
+
+ public void dispose() {
+ currentPopupDialog = null;
+ close();
+ toolkit.dispose();
+ }
+
+ public void setFocus() {
+ getShell().forceFocus();
+
+ if (focusLabel != null) {
+ focusLabel.dispose();
+ }
+
+ if (composite.getChildren().length > 0) {
+ composite.getChildren()[0].setFocus();
+ }
+
+ Point computeSize = composite.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ if (computeSize.y > scrolledComposite.getSize().y) {
+ scrolledComposite.setExpandVertical(false);
+ composite.setSize(computeSize);
+ }
+ }
+
+ public Point computeSizeHint() {
+ int widthHint = MAX_WIDTH;
+ if (maxWidth < widthHint) {
+ widthHint = maxWidth;
+ }
+
+ return getShell().computeSize(widthHint, SWT.DEFAULT, true);
+ }
+
+ public void removeFocusListener(FocusListener listener) {
+ composite.removeFocusListener(listener);
+ }
+
+ public void addFocusListener(FocusListener listener) {
+ composite.addFocusListener(listener);
+
+ }
+
+ public boolean isFocusControl() {
+ //return composite.isFocusControl();
+ return getShell().getDisplay().getActiveShell() == getShell();
+ }
+
+ public void removeDisposeListener(DisposeListener listener) {
+ getShell().removeDisposeListener(listener);
+
+ }
+
+ public void addDisposeListener(DisposeListener listener) {
+ getShell().addDisposeListener(listener);
+ }
+
+ public Rectangle getBounds() {
+ return getShell().getBounds();
+ }
+
+ public Rectangle computeTrim() {
+ return getShell().computeTrim(0, 0, 0, 0);
+ }
+
+ public void setSizeConstraints(int newMaxWidth, int newMaxHeight) {
+ this.maxWidth = newMaxWidth;
+ }
+
+ public void setLocation(Point location) {
+ getShell().setLocation(location);
+ }
+
+ public void setSize(int width, int height) {
+ Point computeSize = composite.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ if (computeSize.x > width) {
+ width = computeSize.x;
+ }
+ getShell().setSize(width, height);
+ scrolledComposite.setSize(width, height);
+ }
+
+ public void setInput(Object input) {
+ if (input instanceof CrucibleAnnotationHoverInput) {
+ this.annotationInput = (CrucibleAnnotationHoverInput) input;
+
+ // clear the composite in case we are re-using it
+ for (Control control : composite.getChildren()) {
+ control.dispose();
+ }
+
+ currentPopupDialog = this;
+ for (CrucibleCommentAnnotation annotation : annotationInput.getCrucibleAnnotations()) {
+ VersionedCommentPart part = new VersionedCommentPart(annotation.getVersionedComment(),
+ annotation.getReview(), annotation.getCrucibleFileInfo());
+
+ part.hookCustomActionRunListener(this);
+
+ toolkit.adapt(part.createControl(composite, toolkit), true, true);
+ toolkit.adapt(composite);
+ toolkit.adapt(scrolledComposite);
+ toolkit.adapt(scrolledComposite.getParent());
+
+ getShell().setBackground(toolkit.getColors().getBackground());
+ }
+ focusLabel = toolkit.createLabel(composite, "Press 'F2' for focus.");
+ focusLabel.setForeground(toolkit.getColors().getColor(IFormColors.TITLE));
+ GridDataFactory.fillDefaults().align(SWT.RIGHT, SWT.CENTER).applyTo(focusLabel);
+ } else {
+ input = null;
+ }
+
+ }
+
+ public void actionAboutToRun(Action action) {
+ close();
+ }
+
+ public void actionRan(Action action) {
+ close();
+ }
+
+ public static CrucibleCommentPopupDialog getCurrentPopupDialog() {
+ return currentPopupDialog;
+ }
+
+ public void setInformationControl(CrucibleInformationControl crucibleInformationControl) {
+ this.informationControl = crucibleInformationControl;
+ }
+
+ public CrucibleInformationControl getInformationControl() {
+ return informationControl;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCompareAnnotationModel.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCompareAnnotationModel.java
new file mode 100644
index 0000000..072a19d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleCompareAnnotationModel.java
@@ -0,0 +1,645 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.connector.commons.misc.IntRanges;
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.actions.AddGeneralCommentToFileAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.actions.AddLineCommentToFileAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.editor.ruler.CommentAnnotationRulerColumn;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.VersionedVirtualFile;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
+import org.eclipse.compare.internal.MergeSourceViewer;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextInputListener;
+import org.eclipse.jface.text.TextSelection;
+import org.eclipse.jface.text.TextViewer;
+import org.eclipse.jface.text.source.AnnotationBarHoverManager;
+import org.eclipse.jface.text.source.CompositeRuler;
+import org.eclipse.jface.text.source.IAnnotationHover;
+import org.eclipse.jface.text.source.IAnnotationModel;
+import org.eclipse.jface.text.source.IAnnotationModelExtension;
+import org.eclipse.jface.text.source.ISharedTextColors;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.text.source.OverviewRuler;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.swt.custom.LineBackgroundEvent;
+import org.eclipse.swt.custom.LineBackgroundListener;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.editors.text.EditorsUI;
+import org.eclipse.ui.internal.editors.text.EditorsPlugin;
+import org.eclipse.ui.internal.texteditor.AnnotationColumn;
+import org.eclipse.ui.internal.texteditor.PropertyEventDispatcher;
+import org.eclipse.ui.texteditor.AnnotationPreference;
+import org.eclipse.ui.texteditor.AnnotationPreferenceLookup;
+import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Model for annotations in the diff view.
+ *
+ * @author Thomas Ehrnhoefer
+ */
+@SuppressWarnings("restriction")
+public class CrucibleCompareAnnotationModel {
+
+ private static SourceViewer getSourceViewer(MergeSourceViewer sourceViewer) {
+ if (SourceViewer.class.isInstance(sourceViewer)) {
+ return SourceViewer.class.cast(sourceViewer);
+ } else {
+ Object returnValue;
+ try {
+ Method getSourceViewerRefl = MergeSourceViewer.class.getDeclaredMethod("getSourceViewer");
+ getSourceViewerRefl.setAccessible(true);
+ returnValue = getSourceViewerRefl.invoke(sourceViewer);
+ if (returnValue instanceof SourceViewer) {
+ return (SourceViewer) returnValue;
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+
+ private final class CrucibleViewerTextInputListener implements ITextInputListener, ICrucibleCompareSourceViewer {
+ private final class ColoringLineBackgroundListener implements LineBackgroundListener {
+ private final StyledText styledText;
+
+ private Color colorCommented;
+
+ private PropertyEventDispatcher fDispatcher;
+
+ private ColoringLineBackgroundListener(StyledText styledText) {
+ this.styledText = styledText;
+ initialize();
+ }
+
+ private void updateCommentedColor(AnnotationPreference pref, IPreferenceStore store) {
+ if (pref != null) {
+ RGB rgb = CommentAnnotationRulerColumn.getColorFromAnnotationPreference(store, pref);
+ colorCommented = getSharedColors().getColor(rgb);
+ }
+ }
+
+ private void initialize() {
+ final IPreferenceStore store = EditorsUI.getPreferenceStore();
+ if (store == null) {
+ return;
+ }
+
+ AnnotationPreferenceLookup lookup = EditorsUI.getAnnotationPreferenceLookup();
+ final AnnotationPreference commentedPref = lookup.getAnnotationPreference(CrucibleCommentAnnotation.COMMENT_ANNOTATION_ID);
+
+ updateCommentedColor(commentedPref, store);
+
+ fDispatcher = new PropertyEventDispatcher(store);
+
+ if (commentedPref != null) {
+ fDispatcher.addPropertyChangeListener(commentedPref.getColorPreferenceKey(),
+ new IPropertyChangeListener() {
+ public void propertyChange(PropertyChangeEvent event) {
+ updateCommentedColor(commentedPref, store);
+ }
+ });
+ }
+ }
+
+ public void lineGetBackground(LineBackgroundEvent event) {
+ int documentOffset = 0;
+ documentOffset = getDocumentOffset(event);
+ int lineNr = styledText.getLineAtOffset(event.lineOffset) + 1 + documentOffset;
+ Iterator<CrucibleCommentAnnotation> it = crucibleAnnotationModel.getAnnotationIterator();
+ while (it.hasNext()) {
+ CrucibleCommentAnnotation annotation = it.next();
+ int startLine;
+ int endLine;
+ VersionedComment comment = annotation.getVersionedComment();
+ if (oldFile) {
+ startLine = comment.getFromStartLine();
+ endLine = comment.getFromEndLine();
+ } else {
+ startLine = comment.getToStartLine();
+ endLine = comment.getToEndLine();
+ }
+ if (lineNr >= startLine && lineNr <= endLine) {
+ AnnotationPreference pref = new AnnotationPreferenceLookup().getAnnotationPreference(annotation);
+ if (pref.getHighlightPreferenceValue()) {
+ event.lineBackground = colorCommented;
+ }
+ }
+ }
+ }
+
+ /**
+ * Galileo hack to deal with slaveDocuments (when clicking on java structure elements). The styledText will
+ * not contain the whole text anymore, so our line numbering is off
+ *
+ * @param event
+ * @return
+ */
+ private int getDocumentOffset(LineBackgroundEvent event) {
+ /*
+ * there is no access to DefaultDocumentAdapter and thus the (master or slave) document.. so we have to assume
+ * that on first call this event actually has the full text. this text, and the text of the current styled text
+ * will be used to calculate the offset
+ */
+ if (event.widget instanceof StyledText) {
+ String currentText = ((StyledText) event.widget).getText();
+ if (initialText == null) {
+ initialText = currentText;
+ // since it is initial call, offset should be 0 anyway
+ return 0;
+ }
+ // if text is unchanged, offset it 0
+ if (currentText.equals(initialText)) {
+ return 0;
+ }
+ // current text is different, check if it is contained in initialText
+ if (initialText.contains(currentText)) {
+ // calculate the offset
+ int charoffset = initialText.indexOf(currentText);
+ int lineOffset = 0;
+ String delimiter = ((StyledText) event.widget).getLineDelimiter();
+ for (String line : initialText.split(delimiter)) {
+ if (charoffset > 0) {
+ charoffset -= (line.length() + delimiter.length());
+ lineOffset++;
+ } else {
+ break;
+ }
+ }
+ return lineOffset;
+ } else {
+ // log error since we assume the initial text contains all slaveTexts.
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Could not find text offset for annotation highlighting"
+ + " - current text not contained in initial text."));
+ }
+ }
+ return 0;
+ }
+ }
+
+ private final SourceViewer sourceViewer;
+
+ private final CrucibleAnnotationModel crucibleAnnotationModel;
+
+ private final boolean oldFile;
+
+ private final MergeSourceViewer mergeSourceViewer;
+
+ private AddLineCommentToFileAction addLineCommentAction;
+
+ private AddGeneralCommentToFileAction addGeneralCommentAction;
+
+ private String initialText;
+
+ private CrucibleViewerTextInputListener(MergeSourceViewer sourceViewer,
+ CrucibleAnnotationModel crucibleAnnotationModel, boolean oldFile) {
+ this.sourceViewer = getSourceViewer(sourceViewer);
+ this.mergeSourceViewer = sourceViewer;
+ this.crucibleAnnotationModel = crucibleAnnotationModel;
+ this.oldFile = oldFile;
+ }
+
+ public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
+ // ignore
+ }
+
+ public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
+ if (oldInput != null) {
+ crucibleAnnotationModel.disconnect(oldInput);
+ }
+ if (newInput != null && sourceViewer != null) {
+ IAnnotationModel annotationModel = sourceViewer.getAnnotationModel();
+ if (annotationModel instanceof IAnnotationModelExtension) {
+ IAnnotationModelExtension annotationModelExtension = (IAnnotationModelExtension) annotationModel;
+ annotationModelExtension.addAnnotationModel("test", crucibleAnnotationModel);
+ crucibleAnnotationModel.setEditorDocument(sourceViewer.getDocument());
+ } else {
+ try {
+ Class<SourceViewer> sourceViewerClazz = SourceViewer.class;
+ Field declaredField2 = sourceViewerClazz.getDeclaredField("fVisualAnnotationModel");
+ declaredField2.setAccessible(true);
+ Method declaredMethod = sourceViewerClazz.getDeclaredMethod("createVisualAnnotationModel",
+ IAnnotationModel.class);
+ declaredMethod.setAccessible(true);
+ annotationModel = (IAnnotationModel) declaredMethod.invoke(sourceViewer,
+ crucibleAnnotationModel);
+ declaredField2.set(sourceViewer, annotationModel);
+ annotationModel.connect(newInput);
+ sourceViewer.showAnnotations(true);
+
+ crucibleAnnotationModel.setEditorDocument(sourceViewer.getDocument());
+ createVerticalRuler(newInput, sourceViewerClazz);
+ // createOverviewRuler(newInput, sourceViewerClazz);
+ createHighlighting(sourceViewerClazz);
+ } catch (Throwable t) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Error attaching Crucible annotation model", t));
+ }
+ }
+ }
+ }
+
+ private void createHighlighting(Class<SourceViewer> sourceViewerClazz) throws IllegalArgumentException,
+ IllegalAccessException, SecurityException, NoSuchFieldException {
+ // TODO this could use some performance tweaks
+ final StyledText styledText = sourceViewer.getTextWidget();
+ styledText.addLineBackgroundListener(new ColoringLineBackgroundListener(styledText));
+ }
+
+ /*
+ * overview ruler problem: displayed in both viewers. the diff editor ruler is actually custom drawn (see
+ * TextMergeViewer.fBirdsEyeCanvas) the ruler that gets created in this method is longer than the editor, meaning its
+ * not an overview (not next to the scrollbar)
+ */
+ @SuppressWarnings("unused")
+ private void createOverviewRuler(IDocument newInput, Class<SourceViewer> sourceViewerClazz)
+ throws SecurityException, NoSuchMethodException, NoSuchFieldException, IllegalArgumentException,
+ IllegalAccessException, InvocationTargetException {
+
+ sourceViewer.setOverviewRulerAnnotationHover(new CrucibleAnnotationHover(null));
+
+ OverviewRuler ruler = new OverviewRuler(new DefaultMarkerAnnotationAccess(), 15, EditorsPlugin.getDefault()
+ .getSharedTextColors());
+ Field compositeField = sourceViewerClazz.getDeclaredField("fComposite");
+ compositeField.setAccessible(true);
+
+ ruler.createControl((Composite) compositeField.get(sourceViewer), sourceViewer);
+ ruler.setModel(leftAnnotationModel);
+ ruler.update();
+
+ Field overViewRulerField = sourceViewerClazz.getDeclaredField("fOverviewRuler");
+ overViewRulerField.setAccessible(true);
+
+ if (overViewRulerField.get(sourceViewer) == null) {
+ overViewRulerField.set(sourceViewer, ruler);
+ }
+
+ Method declareMethod = sourceViewerClazz.getDeclaredMethod("ensureOverviewHoverManagerInstalled");
+ declareMethod.setAccessible(true);
+ declareMethod.invoke(sourceViewer);
+ // overviewRuler is null
+
+ Field hoverManager = sourceViewerClazz.getDeclaredField("fOverviewRulerHoveringController");
+ hoverManager.setAccessible(true);
+ AnnotationBarHoverManager manager = (AnnotationBarHoverManager) hoverManager.get(sourceViewer);
+ if (manager != null) {
+ Field annotationHover = AnnotationBarHoverManager.class.getDeclaredField("fAnnotationHover");
+ annotationHover.setAccessible(true);
+ IAnnotationHover hover = (IAnnotationHover) annotationHover.get(manager);
+ annotationHover.set(manager, new CrucibleAnnotationHover(null));
+ }
+ sourceViewer.showAnnotations(true);
+ sourceViewer.showAnnotationsOverview(true);
+
+ declareMethod = sourceViewerClazz.getDeclaredMethod("showAnnotationsOverview", new Class[] { Boolean.TYPE });
+ declareMethod.setAccessible(true);
+ }
+
+ private void createVerticalRuler(IDocument newInput, Class<SourceViewer> sourceViewerClazz)
+ throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
+ InvocationTargetException, NoSuchFieldException {
+
+ forceCustomAnnotationHover();
+
+ Method declaredMethod2 = sourceViewerClazz.getDeclaredMethod("getVerticalRuler");
+ declaredMethod2.setAccessible(true);
+ CompositeRuler ruler = (CompositeRuler) declaredMethod2.invoke(sourceViewer);
+ boolean hasDecorator = false;
+
+ Iterator<?> iter = (ruler).getDecoratorIterator();
+ while (iter.hasNext()) {
+ Object obj = iter.next();
+ if (obj instanceof AnnotationColumn) {
+ hasDecorator = true;
+ }
+ }
+
+ if (!hasDecorator) {
+ AnnotationColumn annotationColumn = new AnnotationColumn();
+ annotationColumn.createControl(ruler, ruler.getControl().getParent());
+ ruler.addDecorator(0, annotationColumn);
+ }
+ }
+
+ public void forceCustomAnnotationHover() throws NoSuchFieldException, IllegalAccessException {
+ Class<SourceViewer> sourceViewerClazz = SourceViewer.class;
+ sourceViewer.setAnnotationHover(new CrucibleAnnotationHover(null));
+
+ // FIXME: hack for e3.5
+ try {
+ Field hoverControlCreator = TextViewer.class.getDeclaredField("fHoverControlCreator");
+ hoverControlCreator.setAccessible(true);
+ hoverControlCreator.set(sourceViewer, new CrucibleInformationControlCreator());
+ } catch (Throwable t) {
+ // ignore as it may not exist in other versions
+ }
+
+ // FIXME: hack for e3.5
+ try {
+ Method ensureMethod = sourceViewerClazz.getDeclaredMethod("ensureAnnotationHoverManagerInstalled");
+ ensureMethod.setAccessible(true);
+ ensureMethod.invoke(sourceViewer);
+ } catch (Throwable t) {
+ // ignore as it may not exist in other versions
+ }
+
+ Field hoverManager = SourceViewer.class.getDeclaredField("fVerticalRulerHoveringController");
+ hoverManager.setAccessible(true);
+ AnnotationBarHoverManager manager = (AnnotationBarHoverManager) hoverManager.get(sourceViewer);
+ if (manager != null) {
+ Field annotationHover = AnnotationBarHoverManager.class.getDeclaredField("fAnnotationHover");
+ annotationHover.setAccessible(true);
+ IAnnotationHover hover = (IAnnotationHover) annotationHover.get(manager);
+ annotationHover.set(manager, new CrucibleAnnotationHover(hover));
+ }
+ sourceViewer.showAnnotations(true);
+ sourceViewer.showAnnotationsOverview(true);
+ }
+
+ public void focusOnLines(IntRanges range) {
+ // editors count lines from 0, Crucible counts from 1
+ final int startLine = range.getTotalMin() - 1;
+ final int endLine = range.getTotalMax() - 1;
+ if (sourceViewer != null) {
+ IDocument document = sourceViewer.getDocument();
+ if (document != null) {
+ try {
+ int offset = document.getLineOffset(startLine);
+ int length = document.getLineOffset(endLine) - offset;
+ StyledText widget = sourceViewer.getTextWidget();
+ try {
+ widget.setRedraw(false);
+ //sourceViewer.revealRange(offset, length);
+ //sourceViewer.setSelectedRange(offset, 0);
+ sourceViewer.setSelection(new TextSelection(offset, length), true);
+ } finally {
+ widget.setRedraw(true);
+ }
+ } catch (BadLocationException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ }
+ }
+ }
+ }
+
+ public void registerContextMenu() {
+ if (CrucibleUtil.canAddCommentToReview(review) && addLineCommentAction == null
+ && addGeneralCommentAction == null) {
+ addLineCommentAction = new AddLineCommentToFileAction(this, crucibleAnnotationModel.getCrucibleFile());
+ addLineCommentAction.setImageDescriptor(CrucibleImages.ADD_COMMENT);
+ addGeneralCommentAction = new AddGeneralCommentToFileAction(crucibleAnnotationModel.getCrucibleFile());
+
+ if (sourceViewer != null) {
+ sourceViewer.addSelectionChangedListener(addLineCommentAction);
+ sourceViewer.addSelectionChangedListener(addGeneralCommentAction);
+ }
+ mergeSourceViewer.addTextAction(addLineCommentAction);
+ mergeSourceViewer.addTextAction(addGeneralCommentAction);
+ }
+ }
+
+ public LineRange getSelection() {
+ if (sourceViewer != null) {
+ TextSelection selection = (TextSelection) sourceViewer.getSelection();
+ return new LineRange(selection.getStartLine() + 1, selection.getEndLine() - selection.getStartLine());
+ }
+ return null;
+ }
+
+ public boolean isListenerFor(MergeSourceViewer viewer, CrucibleAnnotationModel annotationModel) {
+ return this.mergeSourceViewer == viewer && this.crucibleAnnotationModel == annotationModel;
+ }
+ }
+
+ private final CrucibleAnnotationModel leftAnnotationModel;
+
+ private final CrucibleAnnotationModel rightAnnotationModel;
+
+ private final Review review;
+
+ private CrucibleViewerTextInputListener leftViewerListener;
+
+ private CrucibleViewerTextInputListener rightViewerListener;
+
+ private final VersionedComment commentToFocus;
+
+ private TextMergeViewer fMergeViewer;
+
+ private MergeSourceViewer fRightSourceViewer;
+
+ private MergeSourceViewer fLeftSourceViewer;
+
+ public CrucibleCompareAnnotationModel(CrucibleFileInfo crucibleFile, Review review, VersionedComment commentToFocus) {
+ super();
+ this.review = review;
+ this.leftAnnotationModel = new CrucibleAnnotationModel(null, null, null, new CrucibleFile(crucibleFile, false),
+ review);
+ this.rightAnnotationModel = new CrucibleAnnotationModel(null, null, null, new CrucibleFile(crucibleFile, true),
+ review);
+ this.commentToFocus = commentToFocus;
+ }
+
+ public void attachToViewer(final TextMergeViewer viewer, final MergeSourceViewer fLeft,
+ final MergeSourceViewer fRight) {
+ fMergeViewer = viewer;
+ fLeftSourceViewer = fLeft;
+ fRightSourceViewer = fRight;
+
+ /*
+ * only create listeners if they are not already existing
+ */
+ if (!isListenerFor(leftViewerListener, fLeft, leftAnnotationModel)) {
+ leftViewerListener = addTextInputListener(fLeft, leftAnnotationModel, false);
+ } else {
+ /*
+ * Using asyncExec here because if the underlying slaveDocument (part of the file that gets displayed when clicking
+ * on a java structure in the compare editor) is changed, but the master document is not, we do not get any event
+ * afterwards that would give us a place to hook our code to override the annotationHover. Since all is done in the
+ * UI thread, using this asyncExec hack works because the unconfigure and configure of the document is finished and
+ * our hover-hack stays.
+ */
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ try {
+ // if listeners exist, just make sure the hover hack is in there
+ leftViewerListener.forceCustomAnnotationHover();
+ } catch (Exception e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Error attaching Crucible annotation hover", e));
+ }
+ }
+ });
+ }
+ if (!isListenerFor(rightViewerListener, fRight, rightAnnotationModel)) {
+ rightViewerListener = addTextInputListener(fRight, rightAnnotationModel, true);
+ } else {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ try {
+ // if listeners exist, just make sure the hover hack is in there
+ rightViewerListener.forceCustomAnnotationHover();
+ } catch (Exception e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Error attaching Crucible annotation hover", e));
+ }
+ }
+ });
+ }
+ }
+
+ private boolean isListenerFor(CrucibleViewerTextInputListener listener, MergeSourceViewer viewer,
+ CrucibleAnnotationModel annotationModel) {
+ if (listener == null) {
+ return false;
+ }
+ return listener.isListenerFor(viewer, annotationModel);
+ }
+
+ private CrucibleViewerTextInputListener addTextInputListener(final MergeSourceViewer sourceViewer,
+ final CrucibleAnnotationModel crucibleAnnotationModel, boolean oldFile) {
+ CrucibleViewerTextInputListener listener = new CrucibleViewerTextInputListener(sourceViewer,
+ crucibleAnnotationModel, oldFile);
+ SourceViewer viewer = getSourceViewer(sourceViewer);
+ if (viewer != null) {
+ viewer.addTextInputListener(listener);
+ }
+ return listener;
+ }
+
+ public void updateCrucibleFile(Review newReview) {
+ CrucibleFile leftOldFile = leftAnnotationModel.getCrucibleFile();
+ CrucibleFile rightOldFile = rightAnnotationModel.getCrucibleFile();
+ CrucibleFileInfo newLeftFileInfo = newReview.getFileByPermId(leftOldFile.getCrucibleFileInfo().getPermId());
+ CrucibleFileInfo newRightFileInfo = newReview.getFileByPermId(rightOldFile.getCrucibleFileInfo().getPermId());
+ if (newLeftFileInfo != null && newRightFileInfo != null) {
+ leftAnnotationModel.updateCrucibleFile(new CrucibleFile(newLeftFileInfo, leftOldFile.isOldFile()),
+ newReview);
+ rightAnnotationModel.updateCrucibleFile(new CrucibleFile(newRightFileInfo, rightOldFile.isOldFile()),
+ newReview);
+ }
+ }
+
+ public void focusOnComment() {
+ focusOnComment(commentToFocus);
+ }
+
+ public void focusOnComment(VersionedComment commentToFocus) {
+ if (commentToFocus != null) {
+ CrucibleFile leftFile = leftAnnotationModel.getCrucibleFile();
+ VersionedVirtualFile virtualLeft = leftFile.getSelectedFile();
+
+ CrucibleFile rightFile = rightAnnotationModel.getCrucibleFile();
+ VersionedVirtualFile virtualRight = rightFile.getSelectedFile();
+
+ MergeSourceViewer focusViewer = null;
+ Map<String, IntRanges> lineRanges = commentToFocus.getLineRanges();
+ if (lineRanges != null) {
+ IntRanges range;
+ if ((range = lineRanges.get(virtualLeft.getRevision())) != null) {
+ // get the correct listener (new file is left)
+ leftViewerListener.focusOnLines(range);
+ focusViewer = fLeftSourceViewer;
+ } else if ((range = lineRanges.get(virtualRight.getRevision())) != null) {
+ rightViewerListener.focusOnLines(range);
+ focusViewer = fRightSourceViewer;
+ }
+ setActiveViewer(focusViewer);
+ }
+ }
+ }
+
+ private void setActiveViewer(MergeSourceViewer focusViewer) {
+ try {
+ Method setActiveViewer = TextMergeViewer.class.getDeclaredMethod("setActiveViewer",
+ MergeSourceViewer.class, boolean.class);
+ setActiveViewer.setAccessible(true);
+ setActiveViewer.invoke(fMergeViewer, focusViewer, true);
+ } catch (Exception e) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID, "Failed to activate viewer", e));
+ }
+ }
+
+ public void registerContextMenu() {
+ rightViewerListener.registerContextMenu();
+ leftViewerListener.registerContextMenu();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((leftAnnotationModel == null) ? 0 : leftAnnotationModel.hashCode());
+ result = prime * result + ((rightAnnotationModel == null) ? 0 : rightAnnotationModel.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CrucibleCompareAnnotationModel other = (CrucibleCompareAnnotationModel) obj;
+ if (leftAnnotationModel == null) {
+ if (other.leftAnnotationModel != null) {
+ return false;
+ }
+ } else if (!leftAnnotationModel.equals(other.leftAnnotationModel)) {
+ return false;
+ }
+ if (rightAnnotationModel == null) {
+ if (other.rightAnnotationModel != null) {
+ return false;
+ }
+ } else if (!rightAnnotationModel.equals(other.rightAnnotationModel)) {
+ return false;
+ }
+ return true;
+ }
+
+ private ISharedTextColors getSharedColors() {
+ return EditorsUI.getSharedTextColors();
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControl.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControl.java
new file mode 100644
index 0000000..fff265b
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControl.java
@@ -0,0 +1,264 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment.ReadState;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.internal.text.html.HTMLTextPresenter;
+import org.eclipse.jface.text.DefaultInformationControl;
+import org.eclipse.jface.text.IInformationControlCreator;
+import org.eclipse.jface.text.IInformationControlExtension2;
+import org.eclipse.jface.text.information.InformationPresenter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A custom control to display on hover, or delegates to the default control to display if we aren't dealing with a
+ * CrucibleCommentAnnotation.
+ *
+ * @author sminto
+ */
+public class CrucibleInformationControl extends DefaultInformationControl implements IInformationControlExtension2 {
+
+ private Object input;
+
+ private CrucibleCommentPopupDialog commentPopupDialog;
+
+ private final CrucibleInformationControlCreator informationControlCreator;
+
+ private Job markAsReadJob;
+
+ @SuppressWarnings("restriction")
+ public CrucibleInformationControl(Shell parent, CrucibleInformationControlCreator crucibleInformationControlCreator) {
+ super(parent, new HTMLTextPresenter(true));
+ this.informationControlCreator = crucibleInformationControlCreator;
+ commentPopupDialog = new CrucibleCommentPopupDialog(parent, SWT.NO_FOCUS | SWT.ON_TOP);
+ // Force create early so that listeners can be added at all times with API.
+ commentPopupDialog.create();
+ commentPopupDialog.setInformationControl(this);
+ }
+
+ public InformationPresenter getInformationPresenter() {
+ return new InformationPresenter(informationControlCreator);
+ }
+
+ @Override
+ public void setInformation(String content) {
+ this.input = content;
+ commentPopupDialog.setInput(input);
+ super.setInformation(content);
+ }
+
+ public void setInput(Object input) {
+ this.input = input;
+ commentPopupDialog.setInput(input);
+ }
+
+ @Override
+ public boolean hasContents() {
+ return input != null || super.hasContents();
+ }
+
+ private void runMarkCommentAsReadJob(CrucibleAnnotationHoverInput input) {
+ List<CrucibleCommentAnnotation> annotations = input.getCrucibleAnnotations();
+ if (annotations == null || annotations.size() == 0) {
+ return;
+ }
+
+ Set<Comment> comments = MiscUtil.buildHashSet();
+ for (CrucibleCommentAnnotation annotation : annotations) {
+ comments.addAll(getUnreadComments(annotation.getVersionedComment()));
+ }
+
+ if (comments.size() > 0) {
+// markAsReadJob = new MarkCommentsReadJob(comments.iterator().next().getReview(), comments, true);
+// markAsReadJob.schedule(MarkCommentsReadJob.DEFAULT_DELAY_INTERVAL);
+ }
+ }
+
+ private Collection<? extends Comment> getUnreadComments(Comment comment) {
+ Set<Comment> result = MiscUtil.buildHashSet();
+ if (comment.getReadState().equals(ReadState.UNREAD)) {
+ result.add(comment);
+ }
+ for (Comment reply : comment.getReplies()) {
+ result.addAll(getUnreadComments(reply));
+ }
+ return result;
+ }
+
+ private void cancelMarkCommentAsReadJob() {
+ if (markAsReadJob != null) {
+ markAsReadJob.cancel();
+ markAsReadJob = null;
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ cancelMarkCommentAsReadJob();
+
+ if (input instanceof String) {
+ setInformation((String) input);
+ super.setVisible(visible);
+ } else if (input instanceof CrucibleAnnotationHoverInput) {
+ if (visible) {
+ commentPopupDialog.open();
+ runMarkCommentAsReadJob((CrucibleAnnotationHoverInput) input);
+ } else {
+ commentPopupDialog.getShell().setVisible(false);
+ }
+ } else {
+ super.setVisible(visible);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ cancelMarkCommentAsReadJob();
+
+ commentPopupDialog.dispose();
+ commentPopupDialog = null;
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ super.setSize(width, height);
+ commentPopupDialog.setSize(width, height);
+ }
+
+ @Override
+ public void setLocation(Point location) {
+ super.setLocation(location);
+ commentPopupDialog.setLocation(location);
+ }
+
+ @Override
+ public void setSizeConstraints(int maxWidth, int maxHeight) {
+ super.setSizeConstraints(maxWidth, maxHeight);
+ commentPopupDialog.setSizeConstraints(maxWidth, maxHeight);
+ }
+
+ @Override
+ public Rectangle computeTrim() {
+ if (input instanceof String) {
+ return super.computeTrim();
+ } else if (input instanceof CrucibleAnnotationHoverInput) {
+ return commentPopupDialog.computeTrim();
+ } else {
+ return super.computeTrim();
+ }
+
+ }
+
+ @Override
+ public Rectangle getBounds() {
+ if (input instanceof String) {
+ return super.getBounds();
+ } else if (input instanceof CrucibleAnnotationHoverInput) {
+ return commentPopupDialog.getBounds();
+ } else {
+ return super.getBounds();
+ }
+ }
+
+ @Override
+ public void addDisposeListener(DisposeListener listener) {
+ super.addDisposeListener(listener);
+ if (commentPopupDialog != null) {
+ commentPopupDialog.addDisposeListener(listener);
+ }
+ }
+
+ @Override
+ public void removeDisposeListener(DisposeListener listener) {
+ super.removeDisposeListener(listener);
+ commentPopupDialog.removeDisposeListener(listener);
+ }
+
+ @Override
+ public void setForegroundColor(Color foreground) {
+ super.setForegroundColor(foreground);
+ }
+
+ @Override
+ public void setBackgroundColor(Color background) {
+ super.setBackgroundColor(background);
+ }
+
+ @Override
+ public boolean isFocusControl() {
+ if (input instanceof String) {
+ return super.isFocusControl();
+ } else if (input instanceof CrucibleAnnotationHoverInput) {
+ return commentPopupDialog.isFocusControl();
+ } else {
+ return super.isFocusControl();
+ }
+
+ }
+
+ @Override
+ public void setFocus() {
+
+ if (input instanceof String) {
+ super.setFocus();
+ } else if (input instanceof CrucibleAnnotationHoverInput) {
+ commentPopupDialog.setFocus();
+ } else {
+ super.setFocus();
+ }
+ }
+
+ @Override
+ public void addFocusListener(FocusListener listener) {
+ super.addFocusListener(listener);
+ commentPopupDialog.addFocusListener(listener);
+ }
+
+ @Override
+ public void removeFocusListener(FocusListener listener) {
+ super.removeFocusListener(listener);
+ commentPopupDialog.removeFocusListener(listener);
+ }
+
+ @Override
+ public Point computeSizeHint() {
+ if (input instanceof String) {
+ setInformation((String) input);
+ return super.computeSizeHint();
+ } else if (input instanceof CrucibleAnnotationHoverInput) {
+ return commentPopupDialog.computeSizeHint();
+ } else {
+ return super.computeSizeHint();
+ }
+
+ }
+
+ @Override
+ public IInformationControlCreator getInformationPresenterControlCreator() {
+ return new CrucibleInformationControlCreator();
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControlCreator.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControlCreator.java
new file mode 100644
index 0000000..1ec61b9
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/CrucibleInformationControlCreator.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import org.eclipse.jface.text.IInformationControl;
+import org.eclipse.jface.text.IInformationControlCreator;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * The class that will created the information control for the annotation
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleInformationControlCreator implements IInformationControlCreator {
+ public IInformationControl createInformationControl(Shell parent) {
+ return new CrucibleInformationControl(parent, this);
+ }
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleAnnotationModel.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleAnnotationModel.java
new file mode 100644
index 0000000..0741e35
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleAnnotationModel.java
@@ -0,0 +1,19 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import org.eclipse.jface.text.IDocument;
+
+public interface ICrucibleAnnotationModel {
+ void setEditorDocument(IDocument editorDocument);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleCompareSourceViewer.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleCompareSourceViewer.java
new file mode 100644
index 0000000..ceba59d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/annotations/ICrucibleCompareSourceViewer.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.annotations;
+
+import com.atlassian.connector.commons.misc.IntRanges;
+import org.eclipse.jface.text.source.LineRange;
+
+public interface ICrucibleCompareSourceViewer {
+ LineRange getSelection();
+
+ void focusOnLines(IntRanges ranges);
+
+ void registerContextMenu();
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleProjectsLabelProvider.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleProjectsLabelProvider.java
new file mode 100644
index 0000000..cc9994d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleProjectsLabelProvider.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.commons;
+
+import com.atlassian.theplugin.commons.crucible.api.model.BasicProject;
+
+import org.eclipse.jface.viewers.LabelProvider;
+
+/**
+ * LabelProvider for crucible projects
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class CrucibleProjectsLabelProvider extends LabelProvider {
+ @Override
+ public String getText(Object element) {
+ if (element instanceof BasicProject) {
+ return ((BasicProject) element).getName();
+ }
+ return super.getText(element);
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleRepositoriesLabelProvider.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleRepositoriesLabelProvider.java
new file mode 100644
index 0000000..8e6d96f
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleRepositoriesLabelProvider.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.commons;
+
+import com.atlassian.theplugin.commons.crucible.api.model.Repository;
+
+import org.eclipse.jface.viewers.LabelProvider;
+
+/**
+ * label provider for crucible repositories
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class CrucibleRepositoriesLabelProvider extends LabelProvider {
+ @Override
+ public String getText(Object element) {
+ if (element instanceof Repository) {
+ return ((Repository) element).getName();
+ }
+ return super.getText(element);
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserLabelProvider.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserLabelProvider.java
new file mode 100644
index 0000000..cb09fdb
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserLabelProvider.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.commons;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+
+import org.eclipse.jface.viewers.LabelProvider;
+
+/**
+ * Label provider for cached users
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleUserLabelProvider extends LabelProvider {
+
+ @Override
+ public String getText(Object element) {
+ if (element instanceof User) {
+ return CrucibleUiUtil.getDisplayNameOrUsername((User) element);
+ }
+ return super.getText(element);
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserSorter.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserSorter.java
new file mode 100644
index 0000000..164c8ab
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/commons/CrucibleUserSorter.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.commons;
+
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerSorter;
+
+/**
+ * Sorter for cached users
+ *
+ * @author Shawn Minto
+ */
+public class CrucibleUserSorter extends ViewerSorter {
+
+ @Override
+ public int compare(Viewer viewer, Object e1, Object e2) {
+ String displayName1 = null;
+ String displayName2 = null;
+
+ if (e1 instanceof User && e2 instanceof User) {
+ displayName1 = ((User) e1).getDisplayName();
+ displayName2 = ((User) e2).getDisplayName();
+ }
+
+ if (displayName1 != null && displayName2 != null) {
+ return displayName1.compareTo(displayName2);
+ }
+
+ return super.compare(viewer, e1, e2);
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleCommentDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleCommentDialog.java
new file mode 100644
index 0000000..24c1b39
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleCommentDialog.java
@@ -0,0 +1,298 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.ui.dialogs.ProgressDialog;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomField;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldDef;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.viewers.ComboViewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonImages;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonTextSupport;
+import org.eclipse.mylyn.internal.tasks.ui.editors.Messages;
+import org.eclipse.mylyn.internal.tasks.ui.editors.RichTextEditor;
+import org.eclipse.mylyn.internal.tasks.ui.editors.RichTextEditor.State;
+import org.eclipse.mylyn.internal.tasks.ui.editors.RichTextEditor.StateChangedEvent;
+import org.eclipse.mylyn.internal.tasks.ui.editors.RichTextEditor.StateChangedListener;
+import org.eclipse.mylyn.internal.wikitext.tasks.ui.editor.ConfluenceMarkupTaskEditorExtension;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.editors.AbstractRenderingEngine;
+import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorExtension;
+import org.eclipse.mylyn.wikitext.core.parser.MarkupParser;
+import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage;
+import org.eclipse.mylyn.wikitext.core.util.ServiceLocator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.ActiveShellExpression;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.contexts.IContextActivation;
+import org.eclipse.ui.contexts.IContextService;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.handlers.IHandlerService;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+public abstract class AbstractCrucibleCommentDialog extends ProgressDialog {
+
+ private final String taskKey;
+
+ private final String taskId;
+
+ private Review review;
+
+ protected final HashMap<CustomFieldDef, ComboViewer> customCombos;
+
+ protected final HashMap<String, CustomField> customFieldSelections;
+
+ protected Button defectButton;
+
+ protected CommonTextSupport textSupport;
+
+ protected IContextService contextService;
+
+ protected IContextActivation commentContext;
+
+ private Action toggleEditAction;
+
+ private Action toggleBrowserAction;
+
+ protected boolean ignoreToggleEvents;
+
+ protected RichTextEditor commentText;
+
+ protected final TaskRepository taskRepository;
+
+ public AbstractCrucibleCommentDialog(Shell parentShell, TaskRepository taskRepository, Review review,
+ String taskKey, String taskId) {
+ super(parentShell);
+ this.taskRepository = taskRepository;
+ this.review = review;
+ this.taskKey = taskKey;
+ this.taskId = taskId;
+ customCombos = new HashMap<CustomFieldDef, ComboViewer>();
+ customFieldSelections = new HashMap<String, CustomField>();
+ }
+
+ public Review getReview() {
+ return review;
+ }
+
+ protected void setReview(Review review) {
+ this.review = review;
+ }
+
+ protected void createWikiTextControl(Composite composite, FormToolkit toolkit) {
+ final AbstractTaskEditorExtension extension = new ConfluenceMarkupTaskEditorExtension();
+ final String contextId = extension.getEditorContextId();
+
+ contextService = (IContextService) PlatformUI.getWorkbench().getService(IContextService.class);
+ commentContext = contextService.activateContext(contextId, new ActiveShellExpression(getShell()));
+
+ commentText = new RichTextEditor(taskRepository, SWT.MULTI | SWT.BORDER, contextService, extension);
+ final MarkupParser markupParser = new MarkupParser();
+ final MarkupLanguage markupLanguage = ServiceLocator.getInstance().getMarkupLanguage("Confluence");
+ if (markupLanguage != null) {
+ commentText.setRenderingEngine(new AbstractRenderingEngine() {
+
+ @Override
+ public String renderAsHtml(TaskRepository repository, String text, IProgressMonitor monitor)
+ throws CoreException {
+
+ markupParser.setMarkupLanguage(markupLanguage);
+ String htmlContent = markupParser.parseToHtml(text);
+ return htmlContent;
+ }
+ });
+ } else {
+ StatusHandler.log(new Status(IStatus.INFO, CrucibleUiPlugin.PLUGIN_ID,
+ "Unable to locate Confluence MarkupLanguage. Browser preview will not be possible"));
+ }
+
+ commentText.setReadOnly(false);
+ commentText.setSpellCheckingEnabled(true);
+
+ // creating toolbar - must be created before actually commentText widget is created, to stay on top
+ ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+ fillToolBar(toolBarManager, commentText);
+
+ if (toolBarManager.getSize() > 0) {
+ Composite toolbarComposite = toolkit.createComposite(composite);
+ GridDataFactory.fillDefaults()
+ .grab(false, false)
+ .hint(SWT.DEFAULT, SWT.DEFAULT)
+ .align(SWT.END, SWT.FILL)
+ .applyTo(toolbarComposite);
+ toolbarComposite.setBackground(null);
+ RowLayout rowLayout = new RowLayout();
+ rowLayout.marginLeft = 0;
+ rowLayout.marginRight = 0;
+ rowLayout.marginTop = 0;
+ rowLayout.marginBottom = 0;
+ rowLayout.center = true;
+ toolbarComposite.setLayout(rowLayout);
+
+ toolBarManager.createControl(toolbarComposite);
+ }
+ // end of toolbar creation stuff
+
+ // auto-completion support
+ commentText.createControl(composite, toolkit);
+ IHandlerService handlerService = (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);
+ if (handlerService != null) {
+ textSupport = new CommonTextSupport(handlerService);
+ textSupport.install(commentText.getViewer(), true);
+ }
+
+ commentText.showEditor();
+ commentText.getViewer().getTextWidget().setBackground(null);
+
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(commentText.getControl());
+
+ GridData textGridData = new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL
+ | GridData.GRAB_VERTICAL | GridData.VERTICAL_ALIGN_FILL);
+ textGridData.heightHint = 100;
+ textGridData.widthHint = 500;
+ commentText.getControl().setLayoutData(textGridData);
+ }
+
+ protected void fillToolBar(ToolBarManager manager, final RichTextEditor editor) {
+ if (editor.hasPreview()) {
+ toggleEditAction = new Action("", SWT.TOGGLE) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ if (isChecked()) {
+ editor.showEditor();
+ } else {
+ editor.showPreview();
+ }
+
+ if (toggleBrowserAction != null) {
+ toggleBrowserAction.setChecked(false);
+ }
+ }
+ };
+ toggleEditAction.setImageDescriptor(CommonImages.EDIT_SMALL);
+ toggleEditAction.setToolTipText(Messages.TaskEditorRichTextPart_Edit_Tooltip);
+ toggleEditAction.setChecked(true);
+ editor.addStateChangedListener(new StateChangedListener() {
+ public void stateChanged(StateChangedEvent event) {
+ try {
+ ignoreToggleEvents = true;
+ toggleEditAction.setChecked(event.state == State.EDITOR || event.state == State.DEFAULT);
+ } finally {
+ ignoreToggleEvents = false;
+ }
+ }
+ });
+ manager.add(toggleEditAction);
+ }
+ if (/* toggleEditAction == null && */editor.hasBrowser()) {
+ toggleBrowserAction = new Action("", SWT.TOGGLE) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ if (ignoreToggleEvents) {
+ return;
+ }
+ if (isChecked()) {
+ editor.showBrowser();
+ } else {
+ editor.showEditor();
+ }
+
+ if (toggleEditAction != null) {
+ toggleEditAction.setChecked(false);
+ }
+ }
+ };
+ toggleBrowserAction.setImageDescriptor(CommonImages.PREVIEW_WEB);
+ toggleBrowserAction.setToolTipText(Messages.TaskEditorRichTextPart_Browser_Preview);
+ toggleBrowserAction.setChecked(false);
+ editor.addStateChangedListener(new StateChangedListener() {
+ public void stateChanged(StateChangedEvent event) {
+ try {
+ ignoreToggleEvents = true;
+ toggleBrowserAction.setChecked(event.state == State.BROWSER);
+ } finally {
+ ignoreToggleEvents = false;
+ }
+ }
+ });
+ manager.add(toggleBrowserAction);
+ }
+ }
+
+ @Override
+ protected Collection<? extends Control> getDisableableControls() {
+ Set<Control> controls = new HashSet<Control>(super.getDisableableControls());
+ if (customCombos.size() > 0) {
+ for (ComboViewer viewer : customCombos.values()) {
+ controls.add(viewer.getControl());
+ }
+ }
+
+ if (defectButton != null) {
+ controls.add(defectButton);
+ }
+
+ return controls;
+ }
+
+ @Override
+ public boolean close() {
+ if (contextService != null && commentContext != null) {
+ contextService.deactivateContext(commentContext);
+ commentContext = null;
+ }
+
+ if (textSupport != null) {
+ textSupport.dispose();
+ }
+ return super.close();
+ }
+
+ public TaskRepository getTaskRepository() {
+ return taskRepository;
+ }
+
+ public void cancelUpdateComment() {
+ setReturnCode(Window.CANCEL);
+ close();
+ }
+
+ public String getTaskKey() {
+ return taskKey;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleReviewActionDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleReviewActionDialog.java
new file mode 100644
index 0000000..4a4670b
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/AbstractCrucibleReviewActionDialog.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.ui.dialogs.ProgressDialog;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.Reviewer;
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public abstract class AbstractCrucibleReviewActionDialog extends ProgressDialog {
+
+ protected final Review review;
+ protected Review updatedReview;
+ protected final String userName;
+ protected final TaskRepository taskRepository;
+ protected final String taskKey;
+ protected final String taskId;
+ protected final CrucibleClient client;
+ protected boolean discardDrafts = false;
+ private final String actionText;
+
+ public String getTaskKey() {
+ return taskKey;
+ }
+
+ public AbstractCrucibleReviewActionDialog(Shell parentShell, Review review, String userName,
+ TaskRepository taskRepository, String taskKey, String taskId, CrucibleClient client, String actionText) {
+ super(parentShell);
+ this.review = review;
+ this.userName = userName;
+ this.taskRepository = taskRepository;
+ this.taskKey = taskKey;
+ this.taskId = taskId;
+ this.client = client;
+ this.actionText = actionText;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public TaskRepository getTaskRepository() {
+ return taskRepository;
+ }
+
+ public Review getUpdatedReview() {
+ return updatedReview;
+ }
+
+ public void handleUserDrafts(Composite draftComp) {
+ boolean hasDrafts = checkForDrafts();
+ if (hasDrafts) {
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(
+ new Label(draftComp, SWT.SEPARATOR | SWT.HORIZONTAL));
+ final Label draftComments = new Label(draftComp, SWT.NONE);
+ final int numDraftComments = review.getNumberOfGeneralCommentsDrafts()
+ + review.getNumberOfVersionedCommentsDrafts();
+ final String commentStr = numDraftComments == 1 ? "comment" : "comments";
+
+ draftComments.setText("You have " + numDraftComments + " draft "
+ + commentStr + ". " + "Draft comments that aren't posted will be deleted.\n"
+ + "Please choose an action:");
+ GridDataFactory.fillDefaults().span(2, 1).applyTo(draftComments);
+
+ Button deleteDrafts = new Button(draftComp, SWT.RADIO);
+ deleteDrafts.setText("Discard Drafts");
+ deleteDrafts.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ discardDrafts = true;
+ }
+ });
+ Button postDrafts = new Button(draftComp, SWT.RADIO);
+ postDrafts.setText("Post Drafts");
+ postDrafts.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ discardDrafts = false;
+ }
+ });
+ if (discardDrafts) {
+ deleteDrafts.setSelection(true);
+ } else {
+ postDrafts.setSelection(true);
+ }
+ }
+ }
+
+ private boolean checkForDrafts() {
+ if ((review.getNumberOfGeneralCommentsDrafts() + review.getNumberOfVersionedCommentsDrafts()) > 0) {
+ return true;
+ }
+ return false;
+ }
+
+ protected abstract void doAction();
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+
+ Button summarizeButton = createButton(parent, IDialogConstants.CLIENT_ID + 1, actionText, false);
+ summarizeButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ doAction();
+ }
+ });
+
+ Button cancelButton = createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+
+ cancelButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ cancelPressed();
+ }
+ });
+
+ }
+
+ protected Reviewer getReviewer(User author) {
+ for (Reviewer reviewer : review.getReviewers()) {
+ if (reviewer.getUsername().equals(author.getUsername())) {
+ return reviewer;
+ }
+ }
+ return null;
+ }
+
+ protected Set<Reviewer> getOpenReviewers() {
+ Set<Reviewer> openReviewers = new LinkedHashSet<Reviewer>();
+ for (Reviewer reviewer : review.getReviewers()) {
+ if (!reviewer.isCompleted()) {
+ openReviewers.add(reviewer);
+ }
+ }
+ return openReviewers;
+ }
+
+ protected Set<Reviewer> getCompletedReviewers() {
+ Set<Reviewer> completedReviewers = new LinkedHashSet<Reviewer>();
+ for (Reviewer reviewer : review.getReviewers()) {
+ if (reviewer.isCompleted()) {
+ completedReviewers.add(reviewer);
+ }
+ }
+ return completedReviewers;
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddCommentDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddCommentDialog.java
new file mode 100644
index 0000000..988833e
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddCommentDialog.java
@@ -0,0 +1,316 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;
+
+import com.atlassian.connector.commons.misc.IntRange;
+import com.atlassian.connector.commons.misc.IntRanges;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment.ReadState;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldBean;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldDef;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldValue;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.text.ITextListener;
+import org.eclipse.jface.text.TextEvent;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ComboViewer;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.window.Window;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Dialog shown to the user when they add a comment to a review
+ *
+ * @author Thomas Ehrnhoefer
+ * @author Shawn Minto
+ */
+public class CrucibleAddCommentDialog extends AbstractCrucibleCommentDialog {
+
+ public class AddCommentRunnable implements IRunnableWithProgress {
+
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ monitor.beginTask("Adding comment", IProgressMonitor.UNKNOWN);
+ if (newComment.length() > 0) {
+ CrucibleFileInfo crucibleFileInfo = crucibleFile.getCrucibleFileInfo();
+ VersionedComment comment = new VersionedComment(getReview(), crucibleFileInfo);
+ comment.setMessage(newComment);
+ comment.setAuthor(CrucibleUiUtil.getCachedUsers(getReview()).iterator().next());
+ comment.setReadState(ReadState.UNREAD);
+ Map<String, IntRanges> lineRanges = new HashMap<String, IntRanges>();
+ lineRanges.put("1.1",
+ new IntRanges(new IntRange(commentLines.getStartLine(), commentLines.getStartLine()
+ + commentLines.getNumberOfLines())));
+ comment.setLineRanges(lineRanges);
+ crucibleFile.getCrucibleFileInfo().addComment(comment);
+ CrucibleUiPlugin.getDefault().getActiveReviewManager().activeReviewUpdated();
+ }
+ }
+ }
+
+ private final String shellTitle;
+
+ private final CrucibleClient client;
+
+ private LineRange commentLines;
+
+ private Comment parentComment;
+
+ private CrucibleFile crucibleFile;
+
+ private static final String SAVE_LABEL = "&Post";
+
+ private static final String DRAFT_LABEL = "Post as &Draft";
+
+ private static final String DEFECT_LABEL = "Defect";
+
+ private final boolean edit = false;
+
+ private FormToolkit toolkit;
+
+ private boolean draft = false;
+
+ private boolean defect = false;
+
+ private String newComment;
+
+ private Button saveButton;
+
+ private Button saveDraftButton;
+
+ public CrucibleAddCommentDialog(Shell parentShell, String shellTitle, Review review, String taskKey, String taskId,
+ TaskRepository taskRepository, CrucibleClient client) {
+ super(parentShell, taskRepository, review, taskKey, taskId);
+ this.shellTitle = shellTitle;
+ this.client = client;
+ }
+
+ @Override
+ protected Control createPageControls(Composite parent) {
+ // CHECKSTYLE:MAGIC:OFF
+ getShell().setText(shellTitle);
+ setTitle(shellTitle);
+
+ if (parentComment == null) {
+ setMessage("Create a new comment");
+ } else {
+ setMessage("Reply to a comment from: " + parentComment.getAuthor().getDisplayName());
+ }
+
+ // CHECKSTYLE:MAGIC:OFF
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(1, false));
+
+ if (toolkit == null) {
+ toolkit = new FormToolkit(getShell().getDisplay());
+ }
+ parent.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ if (toolkit != null) {
+ toolkit.dispose();
+ }
+ }
+ });
+
+ createAdditionalControl(composite);
+ createWikiTextControl(composite, toolkit);
+
+ commentText.getViewer().addTextListener(new ITextListener() {
+
+ public void textChanged(TextEvent event) {
+ boolean enabled = false;
+ if (commentText != null && commentText.getText().trim().length() > 0) {
+ enabled = true;
+ }
+
+ if (saveButton != null && !saveButton.isDisposed()
+ && (parentComment == null || !parentComment.isDraft())) {
+ saveButton.setEnabled(enabled);
+ }
+
+ if (saveDraftButton != null && !saveDraftButton.isDisposed()) {
+ saveDraftButton.setEnabled(enabled);
+ }
+ }
+ });
+
+ ((GridLayout) parent.getLayout()).makeColumnsEqualWidth = false;
+ // create buttons according to (implicit) reply type
+ int nrOfCustomFields = 0;
+ if (parentComment == null) { // "defect button" needed if new comment
+ Composite compositeCustomFields = new Composite(composite, SWT.NONE);
+ compositeCustomFields.setLayout(new GridLayout(1, false));
+ createDefectButton(compositeCustomFields);
+ GridDataFactory.fillDefaults()
+ .grab(true, false)
+ .span(nrOfCustomFields + 1, 1)
+ .applyTo(compositeCustomFields);
+ }
+
+ GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, SWT.DEFAULT).applyTo(composite);
+
+ applyDialogFont(composite);
+ return composite;
+ }
+
+ protected void createAdditionalControl(Composite composite) {
+ }
+
+ protected void processFields() {
+ newComment = commentText.getText();
+ if (defect) { // process custom field selection only when defect is selected
+ for (CustomFieldDef field : customCombos.keySet()) {
+ CustomFieldValue customValue = (CustomFieldValue) customCombos.get(field).getElementAt(
+ customCombos.get(field).getCombo().getSelectionIndex());
+ if (customValue != null) {
+ CustomFieldBean bean = new CustomFieldBean();
+ bean.setConfigVersion(field.getConfigVersion());
+ bean.setValue(customValue.getName());
+ customFieldSelections.put(field.getName(), bean);
+ }
+ }
+ }
+ }
+
+ protected Button createDefectButton(Composite parent) {
+ // increment the number of columns in the button bar
+ ((GridLayout) parent.getLayout()).numColumns++;
+ defectButton = new Button(parent, SWT.CHECK);
+ defectButton.setText(DEFECT_LABEL);
+ defectButton.setFont(JFaceResources.getDialogFont());
+ defectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ defect = !defect;
+ // toggle combos
+ for (CustomFieldDef field : customCombos.keySet()) {
+ customCombos.get(field).getCombo().setEnabled(defect);
+ }
+ }
+ });
+ return defectButton;
+ }
+
+ protected void createCombo(Composite parent, final CustomFieldDef customField, int selection) {
+ ((GridLayout) parent.getLayout()).numColumns++;
+ Label label = new Label(parent, SWT.NONE);
+ label.setText("Select " + customField.getName());
+ ((GridLayout) parent.getLayout()).numColumns++;
+ ComboViewer comboViewer = new ComboViewer(parent);
+ comboViewer.setContentProvider(new ArrayContentProvider());
+ comboViewer.setLabelProvider(new LabelProvider() {
+ @Override
+ public String getText(Object element) {
+ CustomFieldValue fieldValue = (CustomFieldValue) element;
+ return fieldValue.getName();
+ }
+ });
+ comboViewer.setInput(customField.getValues());
+ comboViewer.getCombo().setEnabled(false);
+ customCombos.put(customField, comboViewer);
+ }
+
+ public boolean addComment() {
+ try {
+ newComment = commentText.getText();
+ processFields();
+ setMessage("");
+ run(true, false, new AddCommentRunnable());
+ } catch (InvocationTargetException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ setErrorMessage("Unable to add the comment to the review");
+ return false;
+ } catch (InterruptedException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ setErrorMessage("Unable to add the comment to the review");
+ return false;
+ }
+
+ setReturnCode(Window.OK);
+ close();
+ return true;
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ saveButton = createButton(parent, IDialogConstants.CLIENT_ID + 2, SAVE_LABEL, false);
+ saveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ addComment();
+ }
+ });
+ saveButton.setEnabled(false);
+ if (!edit) { // if it is a new reply, saving as draft is possible
+ saveDraftButton = createButton(parent, IDialogConstants.CLIENT_ID + 2, DRAFT_LABEL, false);
+ saveDraftButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ draft = true;
+ addComment();
+ }
+ });
+ saveDraftButton.setEnabled(false);
+ }
+ createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false).addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ cancelPressed();
+ }
+ });
+ }
+
+ public void setReviewItem(CrucibleFile reviewItem) {
+ this.crucibleFile = reviewItem;
+ }
+
+ public void setParentComment(Comment comment) {
+ this.parentComment = comment;
+ }
+
+ public void setCommentLines(LineRange commentLines2) {
+ this.commentLines = commentLines2;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddFileAddCommentDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddFileAddCommentDialog.java
new file mode 100644
index 0000000..e341e7f
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleAddFileAddCommentDialog.java
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.connector.eclipse.ui.commons.DecoratedResource;
+import com.atlassian.theplugin.commons.VersionedVirtualFile;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.PermId;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class CrucibleAddFileAddCommentDialog extends CrucibleAddCommentDialog {
+
+ public class AddFileToReviewRunable implements IRunnableWithProgress {
+
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ VersionedVirtualFile fileDescriptor = new VersionedVirtualFile(resource.getLocation().toPortableString(),
+ "1.1");
+ CrucibleFileInfo crucibleFileInfo = new CrucibleFileInfo(fileDescriptor, fileDescriptor, new PermId(
+ resource.getName()));
+ getReview().getFiles().add(crucibleFileInfo);
+
+ reviewItem = new CrucibleFile(crucibleFileInfo, true);
+// ITeamUiResourceConnector connector = TeamUiUtils.getTeamConnector(resource);
+//
+// if (connector == null) {
+// connector = new LocalTeamResourceConnector();
+// }
+//
+// decoratedResource = TeamUiUtils.getDecoratedResource(resource, connector);
+// if (decoratedResource != null) {
+//
+// monitor.beginTask("Adding selected file to the review", IProgressMonitor.UNKNOWN);
+// reviewItem = CrucibleUiUtil.getCrucibleFileFromResource(resource, review, monitor);
+//
+// } else {
+// reportError("Cannot determine SCM details for resource. Your SCM is probably not supported.", null);
+// }
+ }
+ }
+
+ private final Review review;
+
+ private CrucibleFile reviewItem;
+
+ private IResource resource;
+
+ private DecoratedResource decoratedResource;
+
+ public CrucibleAddFileAddCommentDialog(Shell shell, String dialogTitle, Review review, String taskKey,
+ String taskId, TaskRepository taskRepository, CrucibleClient client) {
+ super(shell, dialogTitle, review, taskKey, taskId, taskRepository, client);
+ this.review = review;
+ }
+
+ @Override
+ public boolean addComment() {
+
+ // add file to review first if needed
+ if (resource != null) {
+ try {
+ run(true, false, new AddFileToReviewRunable());
+ } catch (InvocationTargetException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ setErrorMessage("Unable to add file to the review.");
+ return false;
+ } catch (InterruptedException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ setErrorMessage("Unable to add file to the review.");
+ return false;
+ }
+
+ if (reviewItem == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, "Adding file to review failed."));
+ setErrorMessage("Unable to determine CrucibleFile.");
+ return false;
+ }
+
+ if (review == null) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Failed to refresh review after file was added."));
+ setErrorMessage("Unable to refresh the review");
+ return false;
+ }
+
+ setReview(review);
+ setReviewItem(reviewItem);
+ }
+
+ // add comment
+ boolean ok = super.addComment();
+ if (ok && resource != null && decoratedResource != null && !decoratedResource.isUpToDate()) {
+ MessageDialog.openInformation(getShell(), "",
+ "Please reopen the file in Review Explorer in order to see comment annotation.");
+ }
+
+ return ok;
+ }
+
+ @Override
+ protected void createAdditionalControl(Composite composite) {
+ if (resource != null) {
+
+ Composite labels = new Composite(composite, SWT.NONE);
+
+ GridLayout layout = new GridLayout(2, false);
+ layout.horizontalSpacing = 1;
+ layout.marginWidth = 1;
+ labels.setLayout(layout);
+
+ Label icon = new Label(labels, SWT.NONE);
+// icon.setImage(AtlassianImages.getImage(AtlassianImages.IMG_ECLIPSE_INFO));
+
+ Label explanation = new Label(labels, SWT.NONE);
+ String text = "This file is currently not under review. Adding a comment will add this file to review ";
+ text += CrucibleUiUtil.getCrucibleTask(review).getTaskKey() + ".";
+ explanation.setText(text);
+ }
+ }
+
+ public void setResource(IResource resource) {
+ this.resource = resource;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleEditCommentDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleEditCommentDialog.java
new file mode 100644
index 0000000..b94c172
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/CrucibleEditCommentDialog.java
@@ -0,0 +1,378 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.core.client.CrucibleClient;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomField;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldBean;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldDef;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomFieldValue;
+import com.atlassian.theplugin.commons.crucible.api.model.GeneralComment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.text.ITextListener;
+import org.eclipse.jface.text.TextEvent;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ComboViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+
+/**
+ * Dialog shown to the user when they add a comment to a review
+ *
+ * @author Wojciech Seliga
+ */
+public class CrucibleEditCommentDialog extends AbstractCrucibleCommentDialog {
+
+ private class UpdateCommentRunnable implements IRunnableWithProgress {
+
+ private final boolean shouldPostIfDraft;
+
+ public UpdateCommentRunnable(boolean shouldPostIfDraft) {
+ this.shouldPostIfDraft = shouldPostIfDraft;
+ }
+
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ try {
+ monitor.beginTask("Updating comment", IProgressMonitor.UNKNOWN);
+
+// try {
+// client.execute(new UpdateCommentRemoteOperation(taskRepository, getReview(), prepareNewComment(comment,
+// shouldPostIfDraft), monitor));
+// } catch (CoreException e) {
+// StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, "Unable to update comment",
+// e));
+// throw e; // rethrow exception so dialog stays open and displays error message
+// }
+ client.getReview(getTaskRepository(), getTaskId(), true, monitor);
+ } catch (CoreException e) {
+ throw new InvocationTargetException(e);
+
+ }
+
+ }
+ }
+
+ private final Comment comment;
+
+ private final String shellTitle;
+
+ private final CrucibleClient client;
+
+ private static final String UPDATE_LABEL = "&Update";
+
+ private static final String DEFECT_LABEL = "Defect";
+
+ private static final String DRAFT_LABEL = "Update && &Post";
+
+ private FormToolkit toolkit;
+
+ private boolean defect;
+
+ private String newComment;
+
+ private Button updateButton;
+
+ private Button saveDraftButton;
+
+ public CrucibleEditCommentDialog(Shell parentShell, String shellTitle, Review review, Comment comment,
+ String taskKey, String taskId, TaskRepository taskRepository, CrucibleClient client) {
+ super(parentShell, taskRepository, review, taskKey, taskId);
+ this.shellTitle = shellTitle;
+ if (comment == null) {
+ throw new IllegalArgumentException("Comment must not be null");
+ }
+ this.comment = comment;
+ this.client = client;
+ this.defect = comment.isDefectRaised();
+ }
+
+ private Comment prepareNewComment(Comment oldComment, boolean shouldPostIfDraft) {
+ final Comment commentBean;
+ if (oldComment instanceof VersionedComment) {
+ commentBean = new VersionedComment((VersionedComment) oldComment);
+ } else if (oldComment instanceof Comment) {
+ commentBean = new GeneralComment(oldComment);
+ } else {
+ throw new IllegalArgumentException("Unhandled type of comment class "
+ + oldComment.getClass().getSimpleName());
+ }
+
+ commentBean.setMessage(newComment);
+ commentBean.getCustomFields().clear();
+ commentBean.getCustomFields().putAll(customFieldSelections);
+// commentBean.setAuthor(new User(client.getUsername()));
+ commentBean.setDefectRaised(defect);
+ if (commentBean.isDraft() && shouldPostIfDraft) {
+ commentBean.setDraft(false);
+ }
+ return commentBean;
+ }
+
+ @Override
+ protected Control createPageControls(Composite parent) {
+ getShell().setText(shellTitle);
+ setTitle(shellTitle);
+ if (comment.isReply()) {
+ setMessage("Update reply");
+ } else {
+ setMessage("Update comment");
+ }
+
+ // CHECKSTYLE:MAGIC:OFF
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(1, false));
+
+ if (toolkit == null) {
+ toolkit = new FormToolkit(getShell().getDisplay());
+ }
+ parent.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ if (toolkit != null) {
+ toolkit.dispose();
+ }
+ }
+ });
+
+ // if (commentPart != null) {
+ // commentPart.disableToolbar();
+ //
+ // ScrolledComposite scrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL | SWT.BORDER);
+ // scrolledComposite.setExpandHorizontal(true);
+ //
+ // scrolledComposite.setBackground(toolkit.getColors().getBackground());
+ // GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 100).applyTo(scrolledComposite);
+ //
+ // Composite commentComposite = toolkit.createComposite(scrolledComposite, SWT.NONE);
+ // commentComposite.setLayout(new GridLayout());
+ // scrolledComposite.setContent(commentComposite);
+ //
+ // Control commentControl = commentPart.createControl(commentComposite, toolkit);
+ // commentComposite.setSize(commentControl.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+ //
+ // }
+
+ createWikiTextControl(composite, toolkit);
+ commentText.setText(comment.getMessage());
+
+ commentText.getViewer().addTextListener(new ITextListener() {
+
+ public void textChanged(TextEvent event) {
+ updateButtonsState();
+ }
+
+ });
+
+ if (!comment.isReply()) {
+
+ ((GridLayout) parent.getLayout()).makeColumnsEqualWidth = false;
+ // create buttons according to (implicit) reply type
+ int nrOfCustomFields = 0;
+ Composite compositeCustomFields = new Composite(composite, SWT.NONE);
+ compositeCustomFields.setLayout(new GridLayout(1, false));
+ createDefectButton(compositeCustomFields);
+ GridDataFactory.fillDefaults()
+ .grab(true, false)
+ .span(nrOfCustomFields + 1, 1)
+ .applyTo(compositeCustomFields);
+ }
+ GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, SWT.DEFAULT).applyTo(composite);
+ return composite;
+ }
+
+ // CHECKSTYLE:MAGIC:ON
+
+ private void updateButtonsState() {
+ processFields();
+ boolean areMetricsModified = !comment.isReply()
+ && (defect != comment.isDefectRaised() || !customFieldSelections.equals(comment.getCustomFields()));
+ boolean isModified = !commentText.getText().equals(comment.getMessage()) || areMetricsModified;
+ if (updateButton != null && !updateButton.isDisposed()) {
+ updateButton.setEnabled(isModified);
+ }
+
+ if (saveDraftButton != null && !saveDraftButton.isDisposed()) {
+ saveDraftButton.setEnabled(isModified);
+ }
+ }
+
+ protected void processFields() {
+ newComment = commentText.getText();
+ customFieldSelections.clear();
+ if (defect) { // process custom field selection only when defect is selected
+ for (CustomFieldDef field : customCombos.keySet()) {
+ CustomFieldValue customValue = (CustomFieldValue) customCombos.get(field).getElementAt(
+ customCombos.get(field).getCombo().getSelectionIndex());
+ if (customValue != null/* && customValue != EMPTY_CUSTOM_FIELD_VALUE */) {
+ CustomFieldBean bean = new CustomFieldBean();
+ bean.setConfigVersion(field.getConfigVersion());
+ bean.setValue(customValue.getName());
+ customFieldSelections.put(field.getName(), bean);
+ }
+ }
+ }
+ }
+
+ protected Button createDefectButton(Composite parent) {
+ // increment the number of columns in the button bar
+ ((GridLayout) parent.getLayout()).numColumns++;
+ defectButton = new Button(parent, SWT.CHECK);
+ defectButton.setText(DEFECT_LABEL);
+ defectButton.setFont(JFaceResources.getDialogFont());
+ defectButton.setSelection(defect);
+ defectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ defect = !defect;
+ // toggle combos
+ updateComboEnablement();
+ updateButtonsState();
+ }
+
+ });
+ return defectButton;
+ }
+
+ private void updateComboEnablement() {
+ for (CustomFieldDef field : customCombos.keySet()) {
+ customCombos.get(field).getCombo().setEnabled(defect);
+ }
+ }
+
+ private static final CustomFieldValue EMPTY_CUSTOM_FIELD_VALUE = new CustomFieldValue("", null);
+
+ protected void createCombo(Composite parent, final CustomFieldDef customField) {
+ ((GridLayout) parent.getLayout()).numColumns++;
+ Label label = new Label(parent, SWT.NONE);
+ label.setText("Select " + customField.getName());
+ ((GridLayout) parent.getLayout()).numColumns++;
+ ComboViewer comboViewer = new ComboViewer(parent);
+ comboViewer.setContentProvider(new ArrayContentProvider());
+
+ comboViewer.setLabelProvider(new LabelProvider() {
+ @Override
+ public String getText(Object element) {
+ CustomFieldValue fieldValue = (CustomFieldValue) element;
+ return fieldValue.getName();
+ }
+ });
+ final ArrayList<CustomFieldValue> values = MiscUtil.buildArrayList(EMPTY_CUSTOM_FIELD_VALUE);
+ values.addAll(customField.getValues());
+ comboViewer.setInput(values);
+ comboViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ updateButtonsState();
+ }
+ });
+
+ // setting default values for combo
+ final CustomField commentCustomField = comment.getCustomFields().get(customField.getName());
+ if (commentCustomField != null) {
+ for (CustomFieldValue value : customField.getValues()) {
+ if (value.getName().equals(commentCustomField.getValue())) {
+ ISelection selection = new StructuredSelection(MiscUtil.buildArrayList(value));
+ comboViewer.setSelection(selection, true);
+ break;
+ }
+ }
+ } else {
+ comboViewer.setSelection(new StructuredSelection(MiscUtil.buildArrayList(EMPTY_CUSTOM_FIELD_VALUE)), true);
+ }
+
+ customCombos.put(customField, comboViewer);
+ }
+
+ public void updateComment(boolean shouldPostIfDraft) {
+
+ try {
+ processFields();
+ setMessage("");
+ run(true, false, new UpdateCommentRunnable(shouldPostIfDraft));
+ } catch (InvocationTargetException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ setErrorMessage("Unable to update the comment");
+ return;
+ } catch (InterruptedException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ setErrorMessage("Unable to update the comment");
+ return;
+ }
+
+ setReturnCode(Window.OK);
+ close();
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+
+ updateButton = createButton(parent, IDialogConstants.CLIENT_ID + 2, UPDATE_LABEL, false);
+ updateButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateComment(false);
+ }
+ });
+ updateButton.setEnabled(false);
+ if (CrucibleUtil.canPublishDraft(comment)) {
+ saveDraftButton = createButton(parent, IDialogConstants.CLIENT_ID + 2, DRAFT_LABEL, false);
+ saveDraftButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateComment(true);
+ }
+ });
+ saveDraftButton.setEnabled(false);
+ }
+ createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false).addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ cancelPressed();
+ }
+ });
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/ReviewerSelectionDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/ReviewerSelectionDialog.java
new file mode 100644
index 0000000..d8b5143
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/dialogs/ReviewerSelectionDialog.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.dialogs;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts.ReviewersSelectionTreePart;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Dialog for selecting reviewers
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class ReviewerSelectionDialog extends Dialog {
+
+ private final Set<User> selectedReviewers;
+
+ private final Set<User> allReviewers;
+
+ private ReviewersSelectionTreePart reviewersSelectionTreePart;
+
+ public ReviewerSelectionDialog(Shell shell, Review review, Collection<User> users) {
+ super(shell);
+ setShellStyle(getShellStyle() | SWT.RESIZE);
+ selectedReviewers = CrucibleUiUtil.toUsers(review.getReviewers());
+ allReviewers = MiscUtil.buildHashSet(users);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ getShell().setText("Select Reviewer(s)");
+ reviewersSelectionTreePart = new ReviewersSelectionTreePart(selectedReviewers, allReviewers);
+ final Composite composite = reviewersSelectionTreePart.createControl(parent);
+ applyDialogFont(composite);
+ return composite;
+ }
+
+ public Set<User> getSelectedReviewers() {
+ return reviewersSelectionTreePart.getSelectedReviewers();
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/AbstractCommentPart.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/AbstractCommentPart.java
new file mode 100644
index 0000000..46609ea
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/AbstractCommentPart.java
@@ -0,0 +1,340 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleConstants;
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.AvatarImages.AvatarSize;
+import com.atlassian.connector.eclipse.internal.crucible.ui.actions.EditCommentAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.actions.PostDraftCommentAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.actions.RemoveCommentAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.actions.ReplyToCommentAction;
+import com.atlassian.connector.eclipse.ui.commons.AtlassianUiUtil;
+import com.atlassian.connector.eclipse.ui.forms.SizeCachingComposite;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomField;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.mylyn.internal.tasks.ui.editors.RichTextEditor;
+import org.eclipse.mylyn.internal.tasks.ui.editors.TaskEditorExtensions;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TasksUi;
+import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorExtension;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A UI part to represent a comment in a review
+ *
+ * @author Shawn Minto
+ * @author Thomas Ehrnhoefer
+ */
+@SuppressWarnings("restriction")
+public abstract class AbstractCommentPart<V extends ExpandablePart<Comment, V>> extends ExpandablePart<Comment, V> {
+
+ protected Comment comment;
+
+ // protected final CrucibleFile crucibleFile;
+
+ protected Control commentTextComposite;
+
+ protected SizeCachingComposite sectionClient;
+
+ public AbstractCommentPart(Comment comment, Review crucibleReview) {
+ super(crucibleReview);
+ this.comment = comment;
+ // this.crucibleFile = crucibleFile;
+ }
+
+ @Override
+ protected String getSectionHeaderText() {
+ String headerText = comment.getAuthor().getDisplayName() + " ";
+ headerText += DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(
+ comment.getCreateDate());
+ return headerText;
+ }
+
+ @Override
+ protected Comparator<Comment> getComparator() {
+ return new Comparator<Comment>() {
+
+ public int compare(Comment o1, Comment o2) {
+ if (o1 != null && o2 != null) {
+ return o1.getCreateDate().compareTo(o2.getCreateDate());
+ }
+ return 0;
+ }
+
+ };
+ }
+
+ // TODO handle changed highlighting properly
+
+ protected final Control createOrUpdateControl(Composite parentComposite, FormToolkit toolkit) {
+ Control createdControl = null;
+ if (getSection() == null) {
+
+ Control newControl = createControl(parentComposite, toolkit);
+
+ setIncomming(true);
+
+ createdControl = newControl;
+ } else {
+
+ if (commentTextComposite != null && !commentTextComposite.isDisposed()) {
+ Composite parent = commentTextComposite.getParent();
+ commentTextComposite.dispose();
+ createCommentArea(toolkit, sectionClient);
+ if (parent.getChildren().length > 0) {
+ commentTextComposite.moveAbove(parent.getChildren()[0]);
+ }
+
+ }
+ updateChildren(sectionClient, toolkit, true, comment.getReplies());
+
+ createdControl = getSection();
+ }
+
+ if (sectionClient != null && !sectionClient.isDisposed()) {
+ sectionClient.clearCache();
+ }
+ getSection().layout(true, true);
+
+ update();
+
+ return createdControl;
+
+ }
+
+ @Override
+ protected Composite createSectionContents(Section section, FormToolkit toolkit) {
+ // CHECKSTYLE:MAGIC:OFF
+ section.clientVerticalSpacing = 0;
+
+ sectionClient = new SizeCachingComposite(section, SWT.NONE);
+ toolkit.adapt(sectionClient);
+ GridLayout layout = new GridLayout(1, false);
+ layout.marginTop = 0;
+ layout.marginLeft = 9;
+ sectionClient.setLayout(layout);
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(sectionClient);
+
+ createCommentArea(toolkit, sectionClient);
+
+ updateChildren(sectionClient, toolkit, false, comment.getReplies());
+
+ // CHECKSTYLE:MAGIC:ON
+ return sectionClient;
+ }
+
+ protected void createCommentArea(FormToolkit toolkit, Composite parentComposite) {
+ final Composite twoColumnComposite = new Composite(parentComposite, SWT.NONE);
+ twoColumnComposite.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).create());
+ GridDataFactory.fillDefaults().hint(500, SWT.DEFAULT).applyTo(twoColumnComposite);
+ toolkit.adapt(twoColumnComposite);
+
+ final Label avatarLabel = new Label(twoColumnComposite, SWT.NONE);
+ toolkit.adapt(avatarLabel, false, false);
+ avatarLabel.setImage(CrucibleUiPlugin.getDefault().getAvatarsCache().getAvatarOrDefaultImage(
+ comment.getAuthor(), AvatarSize.LARGE));
+
+ GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.TOP).applyTo(avatarLabel);
+
+ commentTextComposite = createReadOnlyText(toolkit, twoColumnComposite, getCommentText());
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(commentTextComposite);
+ }
+
+ // TODO could be moved to a util method
+ private String getCommentText() {
+ String commentText = comment.getMessage();
+
+ String customFieldsString = "";
+ if (comment.getCustomFields() != null && comment.getCustomFields().size() > 0) {
+
+ Map<String, CustomField> customFields = comment.getCustomFields();
+ CustomField classificationField = customFields.get(CrucibleConstants.CLASSIFICATION_CUSTOM_FIELD_KEY);
+ CustomField rankField = customFields.get(CrucibleConstants.RANK_CUSTOM_FIELD_KEY);
+
+ String classification = null;
+ if (classificationField != null) {
+ classification = classificationField.getValue();
+ }
+
+ String rank = null;
+ if (rankField != null) {
+ rank = rankField.getValue();
+ }
+
+ if (rank != null || classification != null) {
+ customFieldsString = "(";
+
+ if (comment.isDefectApproved() || comment.isDefectRaised()) {
+ customFieldsString += "Defect, ";
+ }
+ }
+
+ if (classification != null) {
+ customFieldsString += "Classification:" + classification;
+ if (rank != null) {
+ customFieldsString += ", ";
+ }
+ }
+
+ if (rank != null) {
+ customFieldsString += "Rank:" + rank;
+ }
+
+ if (customFieldsString.length() > 0) {
+ customFieldsString += ")";
+ }
+
+ }
+ if (customFieldsString.length() > 0) {
+ commentText += " " + customFieldsString;
+ }
+ return commentText;
+ }
+
+ @Override
+ protected String getAnnotationText() {
+ String text = "";
+ if (comment.isDraft()) {
+ text = "DRAFT ";
+ }
+ return text;
+ }
+
+ private Control createReadOnlyText(FormToolkit toolkit, Composite composite, String value) {
+
+ int style = SWT.FLAT | SWT.READ_ONLY | SWT.MULTI | SWT.WRAP;
+
+ ITask task = CrucibleUiUtil.getCrucibleTask(crucibleReview);
+
+ TaskRepository repository = TasksUi.getRepositoryManager().getRepository(task.getConnectorKind(),
+ task.getRepositoryUrl());
+
+ TaskEditorExtensions.setTaskEditorExtensionId(repository, AtlassianUiUtil.CONFLUENCE_WIKI_TASK_EDITOR_EXTENSION);
+ AbstractTaskEditorExtension extension = TaskEditorExtensions.getTaskEditorExtension(repository);
+
+ final RichTextEditor editor = new RichTextEditor(repository, style, null, extension);
+ editor.setReadOnly(true);
+ editor.setText(value);
+ editor.createControl(composite, toolkit);
+
+ // HACK: this is to make sure that we can't have multiple things highlighted
+ editor.getViewer().getTextWidget().addFocusListener(new FocusListener() {
+
+ public void focusGained(FocusEvent e) {
+ }
+
+ public void focusLost(FocusEvent e) {
+ editor.getViewer().getTextWidget().setSelection(0);
+ }
+ });
+
+ return editor.getControl();
+ }
+
+ @Override
+ protected boolean canExpand() {
+ return !comment.isReply();
+ }
+
+ @Override
+ protected boolean hasContents() {
+ return true;
+ }
+
+ @Override
+ protected ImageDescriptor getAnnotationImage() {
+ if (comment.isDefectRaised() || comment.isDefectApproved()) {
+
+ // TODO get an image for a bug
+ return null;
+ }
+ return null;
+ }
+
+ @Override
+ protected List<IReviewAction> getToolbarActions(boolean isExpanded) {
+ List<IReviewAction> actions = new ArrayList<IReviewAction>();
+ if (isExpanded) {
+ if (!comment.isReply() && CrucibleUtil.canAddCommentToReview(crucibleReview)) {
+ ReplyToCommentAction action = new ReplyToCommentAction();
+ action.selectionChanged(new StructuredSelection(comment));
+ actions.add(action);
+ }
+
+ if (CrucibleUiUtil.canModifyComment(crucibleReview, comment)) {
+ EditCommentAction action = new EditCommentAction();
+ action.selectionChanged(new StructuredSelection(comment));
+ actions.add(action);
+
+ if (!comment.isReply() && comment.getReplies().size() > 0) {
+ actions.add(new CannotRemoveCommentAction("Remove Comment", CrucibleImages.COMMENT_DELETE));
+ } else {
+ RemoveCommentAction action1 = new RemoveCommentAction();
+ action1.selectionChanged(new StructuredSelection(comment));
+ actions.add(action1);
+ }
+
+ if (CrucibleUtil.canPublishDraft(comment)) {
+ PostDraftCommentAction action1 = new PostDraftCommentAction();
+ action1.selectionChanged(new StructuredSelection(comment));
+ actions.add(action1);
+ }
+ }
+ }
+ return actions;
+ }
+
+ private final class CannotRemoveCommentAction extends Action implements IReviewAction {
+ public CannotRemoveCommentAction(String text, ImageDescriptor icon) {
+ super(text);
+ setImageDescriptor(icon);
+ }
+
+ public void setActionListener(IReviewActionListener listner) {
+ }
+
+ @Override
+ public void run() {
+ MessageDialog.openInformation(getSection().getShell(), "Delete",
+ "Cannot delete comment with replies. You must delete replies first.");
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/CommentPart.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/CommentPart.java
new file mode 100644
index 0000000..9741386
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/CommentPart.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+public class CommentPart extends AbstractCommentPart<CommentPart> {
+
+ private Composite composite;
+
+ public CommentPart(Comment comment, Review crucibleReview) {
+ super(comment, crucibleReview);
+ }
+
+ @Override
+ protected boolean represents(Comment comment) {
+ return this.comment.getPermId().equals(comment.getPermId());
+ }
+
+ @Override
+ protected Control update(Composite parentComposite, FormToolkit toolkit, Comment newComment, Review newReview) {
+ // TODO update the text
+ if (!CrucibleUtil.areGeneralCommentsDeepEquals(newComment, comment)) {
+ this.comment = newComment;
+ Control createControl = createOrUpdateControl(parentComposite, toolkit);
+ return createControl;
+ }
+ return getSection();
+ }
+
+ @Override
+ protected CommentPart createChildPart(Comment comment, Review crucibleReview2) {
+ return new CommentPart(comment, crucibleReview2);
+ }
+
+ @Override
+ protected Composite createSectionContents(Section section, FormToolkit toolkit) {
+ composite = super.createSectionContents(section, toolkit);
+
+ updateChildren(composite, toolkit, false, comment.getReplies());
+ return composite;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ExpandablePart.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ExpandablePart.java
new file mode 100644
index 0000000..a5ae1ed
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ExpandablePart.java
@@ -0,0 +1,440 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewActionListener;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonFormUtil;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonImages;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.ui.forms.IFormColors;
+import org.eclipse.ui.forms.events.ExpansionAdapter;
+import org.eclipse.ui.forms.events.ExpansionEvent;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.ExpandableComposite;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ImageHyperlink;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A UI part that is expandable like a tree
+ *
+ * @author Shawn Minto
+ */
+public abstract class ExpandablePart<T extends Comment, V extends ExpandablePart<T, V>> {
+
+ private Section commentSection;
+
+ private boolean isExpanded;
+
+ private boolean enableToolbar = true;
+
+ private boolean isIncomming = false;
+
+ private final List<V> childrenParts;
+
+ protected Review crucibleReview;
+
+ private IReviewActionListener actionListener;
+
+ private ToolBarManager toolBarManager;
+
+ private Label annotationImageLabel;
+
+ private Label annotationsTextLabel;
+
+ public ExpandablePart(Review crucibleReview) {
+ this.crucibleReview = crucibleReview;
+ childrenParts = new ArrayList<V>();
+ }
+
+ protected void addChildPart(V part) {
+ childrenParts.add(part);
+ }
+
+ protected List<V> getChildrenParts() {
+ return childrenParts;
+ }
+
+ public Control createControl(Composite parent, final FormToolkit toolkit) {
+ int style = ExpandableComposite.LEFT_TEXT_CLIENT_ALIGNMENT;
+ if (canExpand()) {
+ style |= ExpandableComposite.TWISTIE;
+ }
+ style |= ExpandableComposite.EXPANDED;
+
+ commentSection = toolkit.createSection(parent, style);
+ updateSectionText();
+ commentSection.setTitleBarForeground(toolkit.getColors().getColor(IFormColors.TITLE));
+ GridData gd = GridDataFactory.fillDefaults().grab(true, false).create();
+ if (!canExpand()) {
+ gd.horizontalIndent = 9;
+ }
+ commentSection.setLayoutData(gd);
+
+ final Composite actionsComposite = createSectionAnnotationsAndToolbar(commentSection, toolkit);
+
+ toolBarManager = new ToolBarManager(SWT.FLAT);
+
+ ToolBar toolbarControl = toolBarManager.createControl(actionsComposite);
+ toolkit.adapt(toolbarControl);
+
+ if (commentSection.isExpanded()) {
+ isExpanded = true;
+ fillToolBar(toolBarManager, isExpanded);
+ if (hasContents()) {
+ Composite composite = createSectionContents(commentSection, toolkit);
+ commentSection.setClient(composite);
+ commentSection.addExpansionListener(new ExpansionAdapter() {
+ @Override
+ public void expansionStateChanged(ExpansionEvent e) {
+ fillToolBar(toolBarManager, e.getState());
+ if (getSection() != null && !getSection().isDisposed()) {
+ getSection().layout();
+ }
+ }
+ });
+ }
+ }
+ return commentSection;
+ }
+
+ protected void update() {
+ updateSectionText();
+ updateAnnotationsArea();
+ updateToolbar();
+ }
+
+ private void updateSectionText() {
+ if (commentSection != null && !commentSection.isDisposed()) {
+ commentSection.setText(getSectionHeaderText());
+ }
+ }
+
+ private void updateToolbar() {
+ fillToolBar(toolBarManager, commentSection.isExpanded());
+ }
+
+ private void updateAnnotationsArea() {
+
+ ImageDescriptor annotationImage = getAnnotationImage();
+ if (annotationImageLabel != null && !annotationImageLabel.isDisposed()) {
+ if (annotationImage != null) {
+ annotationImageLabel.setImage(CommonImages.getImage(annotationImage));
+ } else {
+ annotationImageLabel.setImage(null);
+ }
+ }
+
+ String annotationsText = getAnnotationText();
+ if (annotationsText == null) {
+ annotationsText = "";
+ }
+ if (annotationsTextLabel != null && !annotationsTextLabel.isDisposed()) {
+ annotationsTextLabel.setText(annotationsText);
+ }
+ }
+
+ protected boolean canExpand() {
+ return true;
+ }
+
+ protected boolean hasContents() {
+ return canExpand();
+ }
+
+ public Section getSection() {
+ return commentSection;
+ }
+
+ private void fillToolBar(ToolBarManager toolbarManager, boolean expanded) {
+ if (!enableToolbar) {
+ return;
+ }
+
+ List<IReviewAction> toolbarActions = getToolbarActions(expanded);
+
+// for (Control control : actionsComposite.getChildren()) {
+// if (control instanceof ImageHyperlink) {
+// control.setMenu(null);
+// control.dispose();
+// }
+// }
+
+ toolbarManager.removeAll();
+
+ if (toolbarActions != null) {
+
+ for (final IReviewAction action : toolbarActions) {
+ action.setActionListener(actionListener);
+ toolbarManager.add(action);
+// ImageHyperlink link = createActionHyperlink(actionsComposite, toolkit, action);
+// if (!action.isEnabled()) {
+// link.setEnabled(false);
+// }
+ }
+ }
+ toolbarManager.markDirty();
+ toolbarManager.update(true);
+// actionsComposite.getParent().layout();
+ }
+
+ protected ImageHyperlink createActionHyperlink(Composite actionsComposite, FormToolkit toolkit, final IAction action) {
+
+ if (action instanceof IReviewAction) {
+ ((IReviewAction) action).setActionListener(actionListener);
+ }
+ ImageHyperlink link = toolkit.createImageHyperlink(actionsComposite, SWT.NONE);
+ if (action.getImageDescriptor() != null) {
+ link.setImage(CommonImages.getImage(action.getImageDescriptor()));
+ } else {
+ link.setText(action.getText());
+ }
+ link.setToolTipText(action.getToolTipText());
+ link.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ action.run();
+ }
+ });
+ return link;
+ }
+
+ /**
+ * @return A composite that image hyperlinks can be placed on
+ */
+ protected Composite createSectionAnnotationsAndToolbar(Section section, FormToolkit toolkit) {
+
+ Composite toolbarComposite = toolkit.createComposite(section);
+ section.setTextClient(toolbarComposite);
+ RowLayout rowLayout = new RowLayout();
+ rowLayout.marginTop = 0;
+ rowLayout.marginBottom = 0;
+ toolbarComposite.setLayout(rowLayout);
+
+ Composite annotationsComposite = toolkit.createComposite(toolbarComposite);
+
+ rowLayout = new RowLayout();
+ rowLayout.marginTop = 0;
+ rowLayout.marginBottom = 0;
+ rowLayout.spacing = 0;
+
+ annotationsComposite.setLayout(rowLayout);
+
+ annotationImageLabel = toolkit.createLabel(annotationsComposite, "");
+
+ annotationsTextLabel = toolkit.createLabel(annotationsComposite, "");
+
+ createCustomAnnotations(annotationsComposite, toolkit);
+
+ updateAnnotationsArea();
+
+// Composite actionsComposite = toolkit.createComposite(toolbarComposite);
+// actionsComposite.setBackground(null);
+// rowLayout = new RowLayout();
+// rowLayout.marginTop = 0;
+// rowLayout.marginBottom = 0;
+// actionsComposite.setLayout(rowLayout);
+
+ return toolbarComposite;
+ }
+
+ public boolean isExpanded() {
+ return isExpanded && areChildrenExpanded();
+ }
+
+ private boolean areChildrenExpanded() {
+ for (V child : childrenParts) {
+ if (!child.isExpanded()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void setExpanded(boolean expanded) {
+ if (expanded != commentSection.isExpanded()) {
+ CommonFormUtil.setExpanded(commentSection, expanded);
+ }
+ for (V child : childrenParts) {
+ child.setExpanded(expanded);
+ }
+ }
+
+ public void hookCustomActionRunListener(IReviewActionListener actionRunListener) {
+ this.actionListener = actionRunListener;
+ }
+
+ public IReviewActionListener getActionListener() {
+ return actionListener;
+ }
+
+ protected void createCustomAnnotations(Composite toolbarComposite, FormToolkit toolkit) {
+ // default do nothing
+ }
+
+ public void disableToolbar() {
+ enableToolbar = false;
+ }
+
+ public void setIncomming(boolean newIncomming) {
+ this.isIncomming = newIncomming;
+ }
+
+ public boolean isIncomming() {
+ return isIncomming;
+ }
+
+ private void highlightControl(Control client, Color highlightColor) {
+ if (highlightColor == null || highlightColor.isDisposed()) {
+ return;
+ }
+ if (!client.isDisposed()) {
+ if (highlightColor != null) {
+ client.setBackground(highlightColor);
+ }
+ if (client instanceof Composite) {
+ for (Control child : ((Composite) client).getChildren()) {
+ highlightControl(child, highlightColor);
+ }
+ }
+ }
+ }
+
+ public void dispose() {
+ if (getSection() != null) {
+ getSection().dispose();
+ }
+ }
+
+ protected final V findPart(T comment) {
+
+ for (V part : childrenParts) {
+ if (part.represents(comment)) {
+ return part;
+ }
+ }
+
+ return null;
+ }
+
+ protected final void updateChildren(Composite composite, FormToolkit toolkit, boolean shouldHighlight,
+ Collection<T> childrenObjects) {
+
+ List<V> toRemove = new ArrayList<V>();
+ List<V> newParts = new ArrayList<V>();
+
+ if (childrenObjects.size() > 0) {
+ List<T> generalComments = new ArrayList<T>(childrenObjects);
+ Collections.sort(generalComments, getComparator());
+
+ // The following code is almost duplicated in the crucible review files part
+ Control prevControl = null;
+
+ for (int i = 0; i < generalComments.size(); i++) {
+ T comment = generalComments.get(i);
+
+ V oldPart = findPart(comment);
+
+ if (oldPart != null) {
+ Control commentControl = oldPart.update(composite, toolkit, comment, crucibleReview);
+ if (commentControl != null && !commentControl.isDisposed()) {
+
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(commentControl);
+
+ if (prevControl != null) {
+ commentControl.moveBelow(prevControl);
+ } else if (composite.getChildren().length > 1) {
+ commentControl.moveAbove(composite);
+ }
+ prevControl = commentControl;
+ } else {
+ Thread.dumpStack();
+ }
+
+ newParts.add(oldPart);
+ } else {
+ V commentPart = createChildPart(comment, crucibleReview);
+ commentPart.hookCustomActionRunListener(actionListener);
+ newParts.add(commentPart);
+ Control commentControl = commentPart.createControl(composite, toolkit);
+
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(commentControl);
+ if (prevControl != null) {
+ commentControl.moveBelow(prevControl);
+ } else if (composite.getChildren().length > 1) {
+ commentControl.moveAbove(composite);
+ }
+ prevControl = commentControl;
+ }
+ }
+
+ for (V part : childrenParts) {
+ if (!newParts.contains(part)) {
+ toRemove.add(part);
+ }
+ }
+
+ } else {
+ for (V part : childrenParts) {
+ toRemove.add(part);
+ }
+ }
+
+ for (V part : toRemove) {
+ part.dispose();
+ }
+
+ childrenParts.clear();
+ childrenParts.addAll(newParts);
+ }
+
+ protected abstract V createChildPart(T comment, Review crucibleReview2);
+
+ protected abstract Control update(Composite parentComposite, FormToolkit toolkit, T newComment, Review newReview);
+
+ protected abstract boolean represents(T comment);
+
+ protected abstract Comparator<T> getComparator();
+
+ protected abstract List<IReviewAction> getToolbarActions(boolean expanded);
+
+ protected abstract String getAnnotationText();
+
+ protected abstract ImageDescriptor getAnnotationImage();
+
+ protected abstract String getSectionHeaderText();
+
+ protected abstract Composite createSectionContents(Section section, FormToolkit toolkit);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ReviewersSelectionTreePart.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ReviewersSelectionTreePart.java
new file mode 100644
index 0000000..33c850d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/ReviewersSelectionTreePart.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.commons.CrucibleUserLabelProvider;
+import com.atlassian.connector.eclipse.ui.viewers.ArrayTreeContentProvider;
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.ViewerSorter;
+import org.eclipse.mylyn.internal.provisional.commons.ui.SubstringPatternFilter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.dialogs.FilteredTree;
+import org.eclipse.ui.dialogs.PatternFilter;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Part containing a selection tree for reviewers
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class ReviewersSelectionTreePart {
+
+ private final Set<User> selectedReviewers;
+
+ private CheckboxFilteredTree tree;
+
+ private ICheckStateListener externalListener;
+
+ private final Collection<User> allReviewers;
+
+ private class CheckboxFilteredTree extends FilteredTree {
+
+ protected CheckboxFilteredTree(Composite parent) {
+ super(parent);
+ }
+
+ public CheckboxFilteredTree(Composite parent, int treeStyle, PatternFilter filter) {
+ super(parent, treeStyle, filter);
+ }
+
+ @Override
+ protected TreeViewer doCreateTreeViewer(Composite parent, int style) {
+ return new CheckboxTreeViewer(parent, style);
+ }
+
+ @Override
+ public CheckboxTreeViewer getViewer() {
+ return (CheckboxTreeViewer) super.getViewer();
+ }
+
+ }
+
+ public ReviewersSelectionTreePart(Set<User> selectedUsers, Collection<User> allReviewers) {
+ selectedReviewers = selectedUsers == null ? new HashSet<User>() : new HashSet<User>(selectedUsers);
+ this.allReviewers = new HashSet<User>(allReviewers);
+ }
+
+ public void setCheckStateListener(ICheckStateListener listener) {
+ externalListener = listener;
+ }
+
+ public Composite createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout());
+ GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL | GridData.FILL_BOTH);
+ composite.setLayoutData(gd);
+
+ tree = new CheckboxFilteredTree(composite, SWT.CHECK | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER,
+ new SubstringPatternFilter());
+
+ GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 250).applyTo(tree);
+
+ tree.getViewer().setContentProvider(ArrayTreeContentProvider.getInstance());
+
+ tree.getViewer().setInput(allReviewers);
+ // updateInput();
+
+ tree.getViewer().setLabelProvider(new CrucibleUserLabelProvider());
+
+ for (TreeItem item : tree.getViewer().getTree().getItems()) {
+ if (selectedReviewers.contains(item.getData())) {
+ item.setChecked(true);
+ }
+ }
+
+ tree.getViewer().setSorter(new ViewerSorter());
+ tree.getViewer().setCheckedElements(selectedReviewers.toArray(new User[selectedReviewers.size()]));
+
+ tree.getViewer().addCheckStateListener(new ICheckStateListener() {
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ if (event.getChecked()) {
+ selectedReviewers.add((User) event.getElement());
+ } else {
+ selectedReviewers.remove(event.getElement());
+ }
+ if (externalListener != null) {
+ externalListener.checkStateChanged(event);
+ }
+ }
+ });
+
+ parent.pack();
+
+ return composite;
+ }
+
+ public void setAllReviewers(Collection<User> allReviewers) {
+ this.allReviewers.clear();
+ this.allReviewers.addAll(allReviewers);
+ tree.getViewer().setInput(this.allReviewers);
+ }
+
+ public Set<User> getSelectedReviewers() {
+ return selectedReviewers;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/VersionedCommentPart.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/VersionedCommentPart.java
new file mode 100644
index 0000000..ea0c62b
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/parts/VersionedCommentPart.java
@@ -0,0 +1,230 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts;
+
+import com.atlassian.connector.commons.misc.IntRanges;
+import com.atlassian.connector.eclipse.internal.crucible.IReviewChangeListenerAction;
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.IReviewAction;
+import com.atlassian.theplugin.commons.VersionedVirtualFile;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ImageHyperlink;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A UI part to represent a general comment in a review
+ *
+ * @author Shawn Minto
+ */
+public class VersionedCommentPart extends AbstractCommentPart<CommentPart> {
+
+ private VersionedComment versionedComment;
+
+ private CrucibleFileInfo crucibleFileInfo;
+
+ private final List<IAction> customActions;
+
+ private Composite composite;
+
+ private final List<IReviewChangeListenerAction> reviewActions = new ArrayList<IReviewChangeListenerAction>();
+
+ public VersionedCommentPart(VersionedComment comment, Review review, CrucibleFileInfo crucibleFileInfo) {
+ super(comment, review);
+ this.versionedComment = comment;
+ this.crucibleFileInfo = crucibleFileInfo;
+ customActions = MiscUtil.buildArrayList();
+ }
+
+ @Override
+ protected Composite createSectionContents(Section section, FormToolkit toolkit) {
+ composite = super.createSectionContents(section, toolkit);
+
+ updateChildren(composite, toolkit, false, comment.getReplies());
+ return composite;
+ }
+
+ @Override
+ protected String getAnnotationText() {
+
+ String text = super.getAnnotationText();
+ if ((comment.isDefectRaised() || comment.isDefectApproved()) && !comment.isReply()) {
+ text += "DEFECT ";
+ }
+
+ if (!comment.isReply()) {
+ text += getLineNumberText(crucibleFileInfo);
+ }
+ return text;
+ }
+
+ private String getLineInfo(IntRanges intRanges, String revision) {
+ String revStr = (revision != null) ? ("Rev: " + revision + ", ") : "";
+ if (intRanges.getTotalMin() == intRanges.getTotalMax()) {
+ return revStr + "Line: " + intRanges.getTotalMin();
+ } else {
+ return revStr + "Lines: " + intRanges.toNiceString();
+ }
+ }
+
+ /**
+ * We base here on the fact that <code>lineRanges</code> is in fact {@link LinkedHashMap} and preserve ordering. We
+ * cannot reasonably sort revisions, as they e.g. for CVS can be whatever.
+ *
+ * @param lineRanges
+ * @return
+ */
+ private IntRanges getLastLineRange(Map<String, IntRanges> lineRanges) {
+ final Iterator<String> it = lineRanges.keySet().iterator();
+ String candidate = null;
+ while (it.hasNext()) {
+ candidate = it.next();
+ }
+ return lineRanges.get(candidate);
+ }
+
+ private String getLineNumberText(CrucibleFileInfo cfi) {
+ Set<String> displayedRevisions = new HashSet<String>();
+ final VersionedVirtualFile toFile = cfi.getFileDescriptor();
+ if (toFile != null && toFile.getRevision() != null) {
+ displayedRevisions.add(toFile.getRevision());
+ }
+ final VersionedVirtualFile fromFile = cfi.getOldFileDescriptor();
+ if (fromFile != null && fromFile.getRevision() != null) {
+ displayedRevisions.add(fromFile.getRevision());
+ }
+
+ // new Crucible 2.1 API for iterative reviews - let's handle it at least partially
+ final Map<String, IntRanges> lineRanges = versionedComment.getLineRanges();
+ if (lineRanges != null && !lineRanges.isEmpty()) {
+ final boolean omitRevisions = displayedRevisions.containsAll(lineRanges.keySet());
+ if (omitRevisions) {
+ // if all line ranges are identical in each revision - just display one
+ final Set<IntRanges> uniqueSet = MiscUtil.buildHashSet(lineRanges.values());
+ if (uniqueSet.size() == 1) {
+ return "[" + getLineInfo(uniqueSet.iterator().next(), null) + "]";
+ }
+ }
+
+ final StringBuilder builder = new StringBuilder("[");
+ final Iterator<String> it = lineRanges.keySet().iterator();
+ while (it.hasNext()) {
+ final String revision = it.next();
+ final IntRanges intRanges = lineRanges.get(revision);
+ builder.append(getLineInfo(intRanges, omitRevisions ? null : revision));
+
+ if (it.hasNext()) {
+ builder.append("; ");
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+ if (versionedComment.isToLineInfo()) {
+ return "[" + getLineInfo(versionedComment.getToLineRanges(), null) + "]";
+ } else if (versionedComment.isFromLineInfo()) {
+ return "[" + getLineInfo(versionedComment.getFromLineRanges(), null) + "]";
+ } else {
+ return "[General File]";
+ }
+ }
+
+ @Override
+ protected void createCustomAnnotations(Composite toolbarComposite, FormToolkit toolkit) {
+
+ reviewActions.clear();
+
+ for (IAction customAction : customActions) {
+ ImageHyperlink textHyperlink = toolkit.createImageHyperlink(toolbarComposite, SWT.NONE);
+ textHyperlink.setText(" ");
+ textHyperlink.setEnabled(false);
+ textHyperlink.setUnderlined(false);
+
+ createActionHyperlink(toolbarComposite, toolkit, customAction);
+ }
+ }
+
+ @Override
+ protected List<IReviewAction> getToolbarActions(boolean isExpanded) {
+ List<IReviewAction> actions = new ArrayList<IReviewAction>();
+ actions.addAll(super.getToolbarActions(isExpanded));
+
+ return actions;
+ }
+
+ public void addAction(IAction action) {
+ customActions.add(action);
+ }
+
+ @Override
+ protected Control update(Composite parentComposite, FormToolkit toolkit, Comment newComment, Review newReview) {
+ this.crucibleReview = newReview;
+ if (reviewActions != null) {
+
+ final Set<CrucibleFileInfo> files = crucibleReview.getFiles();
+ // FIXME we need new file here with refreshed comments collection (we have to find it as we do not get as param)
+ // workaround for PLE-727 (expandable part generic is an obstacle to solve that in the right way)
+ for (CrucibleFileInfo file : files) {
+ if (file.equals(crucibleFileInfo)) {
+ this.crucibleFileInfo = file;
+ break;
+ }
+ }
+
+ for (IReviewChangeListenerAction reviewAction : reviewActions) {
+ reviewAction.updateReview(newReview, crucibleFileInfo, (VersionedComment) newComment);
+ }
+ }
+ // TODO update the text
+ if (newComment instanceof VersionedComment
+ && !CrucibleUtil.areVersionedCommentsDeepEquals((VersionedComment) newComment, versionedComment)) {
+ if (newComment instanceof VersionedComment) {
+ this.versionedComment = (VersionedComment) newComment;
+ }
+ this.comment = newComment;
+
+ Control createControl = createOrUpdateControl(parentComposite, toolkit);
+
+ return createControl;
+
+ }
+ return getSection();
+ }
+
+ @Override
+ protected boolean represents(Comment comment) {
+ return versionedComment.getPermId().equals(comment.getPermId());
+ }
+
+ @Override
+ protected CommentPart createChildPart(Comment comment, Review crucibleReview2) {
+ return new CommentPart(comment, crucibleReview2);
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerColumn.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerColumn.java
new file mode 100644
index 0000000..53aabaf
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerColumn.java
@@ -0,0 +1,274 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.ruler;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.ActiveReviewManager.IReviewActivationListener;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.ICrucibleFileProvider;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleAnnotationModel;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleCommentAnnotation;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.notification.CrucibleNotification;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferenceConverter;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.source.AbstractRulerColumn;
+import org.eclipse.jface.text.source.AnnotationModel;
+import org.eclipse.jface.text.source.CompositeRuler;
+import org.eclipse.jface.text.source.IAnnotationModel;
+import org.eclipse.jface.text.source.IAnnotationModelListener;
+import org.eclipse.jface.text.source.ISharedTextColors;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.editors.text.EditorsUI;
+import org.eclipse.ui.internal.texteditor.PropertyEventDispatcher;
+import org.eclipse.ui.texteditor.AnnotationPreference;
+import org.eclipse.ui.texteditor.AnnotationPreferenceLookup;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.rulers.IContributedRulerColumn;
+import org.eclipse.ui.texteditor.rulers.RulerColumnDescriptor;
+
+import java.util.Collection;
+import java.util.List;
+
+public class CommentAnnotationRulerColumn extends AbstractRulerColumn implements IContributedRulerColumn,
+ IReviewActivationListener {
+
+ /** The contribution descriptor. */
+ private RulerColumnDescriptor fDescriptor;
+
+ private CrucibleAnnotationModel annotationModel;
+
+ private ITextEditor fEditor;
+
+ private Color colorCommented;
+
+ private ISourceViewer fViewer;
+
+ private PropertyEventDispatcher fDispatcher;
+
+ private IDocumentProvider fDocumentProvider;
+
+ public CommentAnnotationRulerColumn() {
+ setTextInset(10);
+ setHover(new CommentAnnotationRulerHover(this));
+ }
+
+ @Override
+ public void dispose() {
+ colorCommented.dispose();
+
+ super.dispose();
+ }
+
+ public RulerColumnDescriptor getDescriptor() {
+ return fDescriptor;
+ }
+
+ public void setDescriptor(RulerColumnDescriptor descriptor) {
+ fDescriptor = descriptor;
+ }
+
+ public void setEditor(ITextEditor editor) {
+ fEditor = editor;
+ fDocumentProvider = fEditor.getDocumentProvider();
+ }
+
+ public ITextEditor getEditor() {
+ return fEditor;
+ }
+
+ public void columnCreated() {
+ }
+
+ public void columnRemoved() {
+ }
+
+ protected Color computeLeftBackground(int line) {
+ List<CrucibleCommentAnnotation> annotations = getAnnotations(line);
+ if (annotations == null || annotations.size() == 0) {
+ return super.computeBackground(line);
+ } else {
+ return colorCommented;
+ }
+ }
+
+ @Override
+ protected Color computeForeground(int line) {
+ return Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
+ }
+
+ @Override
+ protected void paintLine(GC gc, int modelLine, int widgetLine, int linePixel, int lineHeight) {
+ gc.setBackground(computeLeftBackground(modelLine));
+ gc.fillRectangle(0, linePixel, getWidth(), lineHeight);
+ }
+
+ public List<CrucibleCommentAnnotation> getAnnotations(int startLine) {
+ try {
+ int offset = fEditor.getDocumentProvider().getDocument(fEditor.getEditorInput()).getLineOffset(startLine);
+ return annotationModel == null ? null : annotationModel.getAnnotationsForOffset(offset);
+ } catch (BadLocationException e) {
+ }
+ return null;
+ }
+
+ private ISharedTextColors getSharedColors() {
+ return EditorsUI.getSharedTextColors();
+ }
+
+ public static RGB getColorFromAnnotationPreference(IPreferenceStore store, AnnotationPreference pref) {
+ String key = pref.getColorPreferenceKey();
+ RGB rgb = null;
+ if (store.contains(key)) {
+ if (store.isDefault(key)) {
+ rgb = pref.getColorPreferenceValue();
+ } else {
+ rgb = PreferenceConverter.getColor(store, key);
+ }
+ }
+ if (rgb == null) {
+ rgb = pref.getColorPreferenceValue();
+ }
+ return rgb;
+ }
+
+ private void updateCommentedColor(AnnotationPreference pref, IPreferenceStore store) {
+ if (pref != null) {
+ RGB rgb = getColorFromAnnotationPreference(store, pref);
+ colorCommented = getSharedColors().getColor(rgb);
+ }
+ }
+
+ /**
+ * Initializes the given line number ruler column from the preference store.
+ */
+ private void initialize() {
+ final IPreferenceStore store = EditorsUI.getPreferenceStore();
+ if (store == null) {
+ return;
+ }
+
+ AnnotationPreferenceLookup lookup = EditorsUI.getAnnotationPreferenceLookup();
+ final AnnotationPreference commentedPref = lookup.getAnnotationPreference(CrucibleCommentAnnotation.COMMENT_ANNOTATION_ID);
+
+ updateCommentedColor(commentedPref, store);
+
+ redraw();
+
+ // listen to changes
+ fDispatcher = new PropertyEventDispatcher(store);
+
+ if (commentedPref != null) {
+ fDispatcher.addPropertyChangeListener(commentedPref.getColorPreferenceKey(), new IPropertyChangeListener() {
+ public void propertyChange(PropertyChangeEvent event) {
+ updateCommentedColor(commentedPref, store);
+ redraw();
+ }
+ });
+ }
+ }
+
+ @Override
+ public Control createControl(CompositeRuler parentRuler, Composite parentControl) {
+ ITextViewer viewer = parentRuler.getTextViewer();
+ Assert.isLegal(viewer instanceof ISourceViewer);
+ fViewer = (ISourceViewer) viewer;
+ fViewer.showAnnotations(true);
+ IAnnotationModel model = fViewer.getAnnotationModel();
+ if (model == null) {
+ fViewer.setDocument(fViewer.getDocument(), new AnnotationModel());
+ }
+
+ fViewer.getAnnotationModel().addAnnotationModelListener(new IAnnotationModelListener() {
+ public void modelChanged(IAnnotationModel model) {
+ if (CrucibleUiPlugin.getDefault().getActiveReviewManager().isReviewActive()) {
+ reviewActivated(CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveTask(),
+ CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview());
+ } else {
+ reviewDeactivated(null, null);
+ }
+ }
+ });
+
+ initialize();
+
+ CrucibleUiPlugin.getDefault().getActiveReviewManager().addReviewActivationListener(this);
+ return super.createControl(parentRuler, parentControl);
+ }
+
+ public void reviewActivated(ITask task, Review review) {
+ annotationModel = null;
+ CrucibleFileInfo currentFileInfo = null;
+ CrucibleFile file = null;
+
+ if (fEditor.getEditorInput() instanceof ICrucibleFileProvider) {
+ ICrucibleFileProvider fileProvider = (ICrucibleFileProvider) fEditor.getEditorInput();
+ file = fileProvider.getCrucibleFile();
+ }
+
+ if (fEditor.getEditorInput() instanceof IFileEditorInput) {
+ IFileEditorInput input = (IFileEditorInput) fEditor.getEditorInput();
+ file = CrucibleUiUtil.getCruciblePostCommitFile(input.getFile(), review);
+ }
+
+ if (file != null) {
+ currentFileInfo = review.getFileByPermId(file.getCrucibleFileInfo().getPermId());
+
+ if (currentFileInfo != null) {
+ annotationModel = new CrucibleAnnotationModel(fEditor, fEditor.getEditorInput(),
+ fDocumentProvider.getDocument(fEditor.getEditorInput()), new CrucibleFile(currentFileInfo,
+ file.isOldFile()), review);
+ }
+ }
+
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ redraw();
+ }
+ });
+ }
+
+ /**
+ * task and review might be null when called internally
+ */
+ public void reviewDeactivated(ITask task, Review review) {
+ annotationModel = null;
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ redraw();
+ }
+ });
+ }
+
+ public void reviewUpdated(ITask task, Review review, Collection<CrucibleNotification> differences) {
+ reviewActivated(task, review);
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerHover.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerHover.java
new file mode 100644
index 0000000..44d5ecd
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/editor/ruler/CommentAnnotationRulerHover.java
@@ -0,0 +1,375 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.editor.ruler;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleAnnotationHoverInput;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleCommentAnnotation;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleCommentPopupDialog;
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleInformationControlCreator;
+
+import org.eclipse.core.commands.Command;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.internal.text.html.HTMLPrinter;
+import org.eclipse.jface.text.AbstractInformationControlManager;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IInformationControlCreator;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.jface.text.information.IInformationProvider;
+import org.eclipse.jface.text.information.IInformationProviderExtension;
+import org.eclipse.jface.text.information.IInformationProviderExtension2;
+import org.eclipse.jface.text.information.InformationPresenter;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.CompositeRuler;
+import org.eclipse.jface.text.source.IAnnotationHover;
+import org.eclipse.jface.text.source.IAnnotationHoverExtension;
+import org.eclipse.jface.text.source.IAnnotationHoverExtension2;
+import org.eclipse.jface.text.source.IAnnotationModel;
+import org.eclipse.jface.text.source.ILineRange;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.ISourceViewerExtension2;
+import org.eclipse.jface.text.source.IVerticalRulerInfo;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.jface.text.source.projection.AnnotationBag;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.commands.ICommandService;
+import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
+import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class CommentAnnotationRulerHover implements IAnnotationHover, IAnnotationHoverExtension,
+ IAnnotationHoverExtension2 {
+
+ private final IInformationControlCreator informationControlCreator = new CrucibleInformationControlCreator();
+
+ private final CommentAnnotationRulerColumn rulerColumn;
+
+ private static ISourceViewer currentSourceViewer;
+
+ private static CommentAnnotationRulerHover currentAnnotationHover;
+
+ public CommentAnnotationRulerHover(CommentAnnotationRulerColumn column) {
+ this.rulerColumn = column;
+ }
+
+ public void dispose() {
+ // ignore for now
+ }
+
+ /**
+ * This is from {@link IAnnotationHover} but we also implement {@link IAnnotationHoverExtension} and
+ * {@link IAnnotationHoverExtension2} which supersede this so there's no point in implementing it.
+ */
+ public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) {
+ throw new UnsupportedOperationException("This API should not be used");
+ }
+
+ public IInformationControlCreator getHoverControlCreator() {
+ return informationControlCreator;
+ }
+
+ public boolean canHandleMouseCursor() {
+ return true;
+ }
+
+ public boolean canHandleMouseWheel() {
+ return true; // does not work on Ubuntu, but it should be here (maybe works on Windows ;))
+ }
+
+ public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) {
+ List<CrucibleCommentAnnotation> annotationsForLine = rulerColumn.getAnnotations(lineRange.getStartLine());
+ if (annotationsForLine != null && annotationsForLine.size() > 0) {
+ return new CrucibleAnnotationHoverInput(annotationsForLine);
+ }
+ return null;
+ }
+
+ public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) {
+ currentAnnotationHover = this;
+ currentSourceViewer = viewer;
+ List<CrucibleCommentAnnotation> commentAnnotations = getCrucibleAnnotationsForLine(viewer, lineNumber);
+ if (commentAnnotations != null && commentAnnotations.size() > 0) {
+ IDocument document = viewer.getDocument();
+ int lowestStart = Integer.MAX_VALUE;
+ int highestEnd = 0;
+ for (Annotation a : commentAnnotations) {
+ if (a instanceof CrucibleCommentAnnotation) {
+ Position p = ((CrucibleCommentAnnotation) a).getPosition();
+ try {
+
+ int start = document.getLineOfOffset(p.offset);
+ int end = document.getLineOfOffset(p.offset + p.length);
+
+ if (start < lowestStart) {
+ lowestStart = start;
+ }
+
+ if (end > highestEnd) {
+ highestEnd = end;
+ }
+ } catch (BadLocationException e) {
+ // ignore
+ }
+ }
+ }
+ if (lowestStart != Integer.MAX_VALUE) {
+ return new LineRange(lowestStart, highestEnd - lowestStart);
+ } else {
+ return new LineRange(lineNumber, 1);
+ }
+ }
+
+ return new LineRange(lineNumber, 1);
+ }
+
+ @SuppressWarnings("restriction")
+ protected String formatSingleMessage(String message) {
+ StringBuffer buffer = new StringBuffer();
+ HTMLPrinter.addPageProlog(buffer);
+ HTMLPrinter.addParagraph(buffer, HTMLPrinter.convertToHTMLContent(message));
+ HTMLPrinter.addPageEpilog(buffer);
+ return buffer.toString();
+ }
+
+ @SuppressWarnings("restriction")
+ protected String formatMultipleMessages(List<String> messages) {
+ StringBuffer buffer = new StringBuffer();
+ HTMLPrinter.addPageProlog(buffer);
+ HTMLPrinter.addParagraph(buffer, HTMLPrinter.convertToHTMLContent("There are multiple comments on this line"));
+
+ HTMLPrinter.startBulletList(buffer);
+ for (String message : messages) {
+ HTMLPrinter.addBullet(buffer, HTMLPrinter.convertToHTMLContent(message));
+ }
+ HTMLPrinter.endBulletList(buffer);
+
+ HTMLPrinter.addPageEpilog(buffer);
+ return buffer.toString();
+ }
+
+ private boolean isRulerLine(Position position, IDocument document, int line) {
+ if (position.getOffset() > -1 && position.getLength() > -1) {
+ try {
+ return line == document.getLineOfOffset(position.getOffset());
+ } catch (BadLocationException x) {
+ // ignore
+ }
+ }
+ return false;
+ }
+
+ private IAnnotationModel getAnnotationModel(ISourceViewer viewer) {
+ if (viewer instanceof ISourceViewerExtension2) {
+ ISourceViewerExtension2 extension = (ISourceViewerExtension2) viewer;
+ return extension.getVisualAnnotationModel();
+ }
+ return viewer.getAnnotationModel();
+ }
+
+ private boolean includeAnnotation(Annotation annotation, Position position,
+ List<CrucibleCommentAnnotation> annotations) {
+ if (!(annotation instanceof CrucibleCommentAnnotation)) {
+ return false;
+ }
+
+ return (annotation != null && !annotations.contains(annotation));
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<CrucibleCommentAnnotation> getCrucibleAnnotationsForLine(ISourceViewer viewer, int line) {
+ IAnnotationModel model = getAnnotationModel(viewer);
+ if (model == null) {
+ return null;
+ }
+
+ IDocument document = viewer.getDocument();
+ List<CrucibleCommentAnnotation> commentAnnotations = new ArrayList<CrucibleCommentAnnotation>();
+ Iterator<Annotation> iterator = model.getAnnotationIterator();
+
+ while (iterator.hasNext()) {
+ Annotation annotation = iterator.next();
+
+ Position position = model.getPosition(annotation);
+ if (position == null) {
+ continue;
+ }
+
+ if (!isRulerLine(position, document, line)) {
+ continue;
+ }
+
+ if (annotation instanceof AnnotationBag) {
+ AnnotationBag bag = (AnnotationBag) annotation;
+ Iterator<Annotation> e = bag.iterator();
+ while (e.hasNext()) {
+ annotation = e.next();
+ position = model.getPosition(annotation);
+ if (position != null && includeAnnotation(annotation, position, commentAnnotations)
+ && annotation instanceof CrucibleCommentAnnotation) {
+ commentAnnotations.add((CrucibleCommentAnnotation) annotation);
+ }
+ }
+ continue;
+ }
+
+ if (includeAnnotation(annotation, position, commentAnnotations)
+ && annotation instanceof CrucibleCommentAnnotation) {
+ commentAnnotations.add((CrucibleCommentAnnotation) annotation);
+ }
+ }
+
+ return commentAnnotations;
+ }
+
+ /**
+ * Tries to make an annotation hover focusable (or "sticky").
+ *
+ * @return <code>true</code> if successful, <code>false</code> otherwise
+ */
+ public static boolean makeAnnotationHoverFocusable() {
+ // check sourceviewer and hover
+ if (currentSourceViewer == null || currentSourceViewer.getTextWidget().isDisposed()
+ || currentAnnotationHover == null) {
+ return false;
+ }
+
+ IVerticalRulerInfo info = null;
+ try {
+ Method declaredMethod2 = SourceViewer.class.getDeclaredMethod("getVerticalRuler");
+ declaredMethod2.setAccessible(true);
+ info = (CompositeRuler) declaredMethod2.invoke(currentSourceViewer);
+ } catch (Exception e) {
+ StatusHandler.log(new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "Error getting CompareEditor's vertical ruler. ", e));
+ }
+
+ if (info == null) {
+ return false;
+ }
+
+ int line = info.getLineOfLastMouseButtonActivity();
+ if (line == -1) {
+ return false;
+ }
+
+ try {
+
+ // compute the hover information
+ Object hoverInfo = null;
+ if (currentAnnotationHover instanceof IAnnotationHoverExtension) {
+ /*FIXME: IAnnotationHoverExtension extension = currentAnnotationHover;
+ ILineRange hoverLineRange = extension.getHoverLineRange(currentSourceViewer, line);
+ if (hoverLineRange == null) {
+ return false;
+ }
+ final int maxVisibleLines = Integer.MAX_VALUE;
+ hoverInfo = extension.getHoverInfo(currentSourceViewer, hoverLineRange, maxVisibleLines);*/
+ } else {
+ hoverInfo = currentAnnotationHover.getHoverInfo(currentSourceViewer, line);
+ }
+
+ // hover region: the beginning of the concerned line to place the control right over the line
+ IDocument document = currentSourceViewer.getDocument();
+ int offset = document.getLineOffset(line);
+ String partitioning = new TextSourceViewerConfiguration().getConfiguredDocumentPartitioning(currentSourceViewer);
+ String contentType = TextUtilities.getContentType(document, partitioning, offset, true);
+
+ IInformationControlCreator controlCreator = null;
+ if (currentAnnotationHover instanceof IInformationProviderExtension2) {
+ IInformationProviderExtension2 provider = (IInformationProviderExtension2) currentAnnotationHover;
+ controlCreator = provider.getInformationPresenterControlCreator();
+ } else if (currentAnnotationHover instanceof IAnnotationHoverExtension) {
+ controlCreator = ((IAnnotationHoverExtension) currentAnnotationHover).getHoverControlCreator();
+ }
+
+ IInformationProvider informationProvider = new InformationProvider(new Region(offset, 0), hoverInfo,
+ controlCreator);
+
+ CrucibleCommentPopupDialog dialog = CrucibleCommentPopupDialog.getCurrentPopupDialog();
+ if (dialog != null) {
+
+ InformationPresenter fInformationPresenter = dialog.getInformationControl().getInformationPresenter();
+ fInformationPresenter.setSizeConstraints(100, 12, true, true);
+ fInformationPresenter.install(currentSourceViewer);
+ fInformationPresenter.setDocumentPartitioning(partitioning);
+ fInformationPresenter.setOffset(offset);
+ fInformationPresenter.setAnchor(AbstractInformationControlManager.ANCHOR_RIGHT);
+ fInformationPresenter.setMargins(4, 0); // AnnotationBarHoverManager sets (5,0), minus SourceViewer.GAP_SIZE_1
+ fInformationPresenter.setInformationProvider(informationProvider, contentType);
+ fInformationPresenter.showInformation();
+
+ // remove our own handler as F2 focus handler
+ ICommandService commandService = (ICommandService) PlatformUI.getWorkbench().getService(
+ ICommandService.class);
+ Command showInfoCommand = commandService.getCommand(ITextEditorActionDefinitionIds.SHOW_INFORMATION);
+ showInfoCommand.setHandler(null);
+
+ return true;
+ }
+
+ } catch (BadLocationException e) {
+ return false;
+ }
+ return false;
+ }
+
+ /**
+ * Information provider used to present focusable information shells.
+ *
+ * @since 3.3
+ */
+ private static final class InformationProvider implements IInformationProvider, IInformationProviderExtension,
+ IInformationProviderExtension2 {
+
+ private final IRegion fHoverRegion;
+
+ private final Object fHoverInfo;
+
+ private final IInformationControlCreator fControlCreator;
+
+ InformationProvider(IRegion hoverRegion, Object hoverInfo, IInformationControlCreator controlCreator) {
+ fHoverRegion = hoverRegion;
+ fHoverInfo = hoverInfo;
+ fControlCreator = controlCreator;
+ }
+
+ public IRegion getSubject(ITextViewer textViewer, int invocationOffset) {
+ return fHoverRegion;
+ }
+
+ @Deprecated
+ public String getInformation(ITextViewer textViewer, IRegion subject) {
+ return fHoverInfo.toString();
+ }
+
+ public Object getInformation2(ITextViewer textViewer, IRegion subject) {
+ return fHoverInfo;
+ }
+
+ public IInformationControlCreator getInformationPresenterControlCreator() {
+ return fControlCreator;
+ }
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/operations/CrucibleFileInfoCompareEditorInput.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/operations/CrucibleFileInfoCompareEditorInput.java
new file mode 100644
index 0000000..2bb3739
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/operations/CrucibleFileInfoCompareEditorInput.java
@@ -0,0 +1,223 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.operations;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.annotations.CrucibleCompareAnnotationModel;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.theplugin.commons.VersionedVirtualFile;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+
+import org.apache.commons.io.FilenameUtils;
+import org.eclipse.compare.CompareConfiguration;
+import org.eclipse.compare.CompareEditorInput;
+import org.eclipse.compare.IStreamContentAccessor;
+import org.eclipse.compare.ITypedElement;
+import org.eclipse.compare.contentmergeviewer.ContentMergeViewer;
+import org.eclipse.compare.contentmergeviewer.IMergeViewerContentProvider;
+import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
+import org.eclipse.compare.internal.MergeSourceViewer;
+import org.eclipse.compare.structuremergeviewer.Differencer;
+import org.eclipse.compare.structuremergeviewer.ICompareInput;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class CrucibleFileInfoCompareEditorInput extends CompareEditorInput {
+
+ static class ByteArrayInput implements ITypedElement, IStreamContentAccessor {
+
+ byte[] content;
+
+ private final String name;
+
+ public ByteArrayInput(byte[] content, String name) {
+ this.content = content;
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Image getImage() {
+ return null;
+ }
+
+ public String getType() {
+ String extension = FilenameUtils.getExtension(name);
+ return extension.length() > 0 ? extension : ITypedElement.TEXT_TYPE;
+ }
+
+ public InputStream getContents() throws CoreException {
+ return new ByteArrayInputStream(content);
+ }
+
+ }
+
+ private final byte[] content1;
+
+ private final byte[] content2;
+
+ private final CrucibleCompareAnnotationModel annotationModel;
+
+ private final CrucibleFileInfo fileInfo;
+
+ public CrucibleFileInfoCompareEditorInput(CrucibleFileInfo fileInfo, byte[] content1, byte[] content2,
+ CrucibleCompareAnnotationModel annotationModel, CompareConfiguration compareConfiguration) {
+ super(compareConfiguration);
+ this.content1 = content1;
+ this.content2 = content2;
+ this.annotationModel = annotationModel;
+ this.fileInfo = fileInfo;
+
+ VersionedVirtualFile oldFile = fileInfo.getOldFileDescriptor();
+ VersionedVirtualFile newFile = fileInfo.getFileDescriptor();
+
+ setTitle("Compare " + oldFile.getName() + " " + newFile.getRevision() + " and " + oldFile.getRevision());
+ }
+
+ @Override
+ protected Object prepareInput(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ Differencer d = new Differencer();
+ Object diff = d.findDifferences(false, monitor, null, null, new ByteArrayInput(content1,
+ fileInfo.getFileDescriptor().getName()), new ByteArrayInput(content2, fileInfo.getOldFileDescriptor()
+ .getName()));
+ return diff;
+ }
+
+ @Override
+ public Viewer findContentViewer(Viewer oldViewer, ICompareInput input, Composite parent) {
+ Viewer contentViewer = super.findContentViewer(oldViewer, input, parent);
+ return CrucibleFileInfoCompareEditorInput.findContentViewer(contentViewer, input, parent, annotationModel);
+ }
+
+ private static Viewer findContentViewer(Viewer contentViewer, ICompareInput input, Composite parent,
+ CrucibleCompareAnnotationModel annotationModel) {
+
+ // FIXME: hack
+ if (contentViewer instanceof TextMergeViewer) {
+ TextMergeViewer textMergeViewer = (TextMergeViewer) contentViewer;
+ try {
+ Class<TextMergeViewer> clazz = TextMergeViewer.class;
+ Field declaredField = clazz.getDeclaredField("fLeft");
+ declaredField.setAccessible(true);
+ final MergeSourceViewer fLeft = (MergeSourceViewer) declaredField.get(textMergeViewer);
+
+ declaredField = clazz.getDeclaredField("fRight");
+ declaredField.setAccessible(true);
+ final MergeSourceViewer fRight = (MergeSourceViewer) declaredField.get(textMergeViewer);
+
+ annotationModel.attachToViewer(textMergeViewer, fLeft, fRight);
+ annotationModel.focusOnComment();
+ annotationModel.registerContextMenu();
+
+ Method setActiveViewer = clazz.getDeclaredMethod("setActiveViewer", MergeSourceViewer.class,
+ boolean.class);
+ setActiveViewer.setAccessible(true);
+ setActiveViewer.invoke(textMergeViewer, fRight, true);
+
+ hackGalileo(contentViewer, textMergeViewer, fLeft, fRight);
+ } catch (Throwable t) {
+ StatusHandler.log(new Status(IStatus.WARNING, AtlassianTeamUiPlugin.PLUGIN_ID,
+ "Could not initialize annotation model for " + input.getName(), t));
+ }
+ }
+ return contentViewer;
+ }
+
+ private static void hackGalileo(Viewer contentViewer, TextMergeViewer textMergeViewer,
+ final MergeSourceViewer fLeft, final MergeSourceViewer fRight) {
+ // FIXME: hack for e3.5
+ try {
+ Method getCompareConfiguration = ContentMergeViewer.class.getDeclaredMethod("getCompareConfiguration");
+ getCompareConfiguration.setAccessible(true);
+ CompareConfiguration cc = (CompareConfiguration) getCompareConfiguration.invoke(textMergeViewer);
+
+ Method getMergeContentProvider = ContentMergeViewer.class.getDeclaredMethod("getMergeContentProvider");
+ getMergeContentProvider.setAccessible(true);
+ IMergeViewerContentProvider cp = (IMergeViewerContentProvider) getMergeContentProvider.invoke(textMergeViewer);
+
+ Method getSourceViewer = MergeSourceViewer.class.getDeclaredMethod("getSourceViewer");
+
+ Method configureSourceViewer = TextMergeViewer.class.getDeclaredMethod("configureSourceViewer",
+ SourceViewer.class, boolean.class);
+ configureSourceViewer.setAccessible(true);
+ configureSourceViewer.invoke(contentViewer, getSourceViewer.invoke(fLeft), cc.isLeftEditable()
+ && cp.isLeftEditable(textMergeViewer.getInput()));
+ configureSourceViewer.invoke(contentViewer, getSourceViewer.invoke(fRight), cc.isRightEditable()
+ && cp.isRightEditable(textMergeViewer.getInput()));
+
+ Field isConfiguredField = TextMergeViewer.class.getDeclaredField("isConfigured");
+ isConfiguredField.setAccessible(true);
+ isConfiguredField.set(contentViewer, true);
+ } catch (Throwable t) {
+ // ignore as it may not exist in other versions
+ }
+ }
+
+ public CrucibleCompareAnnotationModel getAnnotationModelToAttach() {
+ return annotationModel;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((annotationModel == null) ? 0 : annotationModel.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CrucibleFileInfoCompareEditorInput other = (CrucibleFileInfoCompareEditorInput) obj;
+ if (annotationModel == null) {
+ if (other.annotationModel != null) {
+ return false;
+ }
+ } else if (!annotationModel.equals(other.annotationModel)) {
+ return false;
+ }
+ return true;
+ }
+
+ public CrucibleFileInfo getCrucibleFileInfo() {
+ return fileInfo;
+ }
+
+ @Override
+ protected void contentsCreated() {
+ super.contentsCreated();
+ getAnnotationModelToAttach().focusOnComment();
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/CommentUiUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/CommentUiUtil.java
new file mode 100644
index 0000000..e58ae7d
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/CommentUiUtil.java
@@ -0,0 +1,190 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.util;
+
+import com.atlassian.connector.commons.misc.IntRanges;
+import com.atlassian.connector.eclipse.internal.crucible.core.CrucibleConstants;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.theplugin.commons.crucible.api.model.Comment;
+import com.atlassian.theplugin.commons.crucible.api.model.CustomField;
+import org.eclipse.mylyn.internal.tasks.ui.editors.RichTextEditor;
+import org.eclipse.mylyn.internal.wikitext.tasks.ui.editor.ConfluenceMarkupTaskEditorExtension;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TasksUi;
+import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorExtension;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.contexts.IContextService;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import java.text.DateFormat;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ *
+ * @author Wojciech Seliga
+ */
+public final class CommentUiUtil {
+
+ private CommentUiUtil() {
+
+ }
+
+ public static String getCommentInfoHeaderText(Comment comment) {
+ StringBuilder headerText = new StringBuilder();
+ headerText.append(comment.getAuthor().getDisplayName());
+ headerText.append("\n");
+ headerText.append(DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
+ .format(comment.getCreateDate()));
+
+ if (comment.getReadState() != null) {
+ if (comment.getReadState().equals(Comment.ReadState.READ)) {
+ headerText.append(", Read");
+ } else if (comment.getReadState().equals(Comment.ReadState.UNREAD)
+ || comment.getReadState().equals(Comment.ReadState.LEAVE_UNREAD)) {
+ headerText.append(", Unread");
+ }
+ }
+
+ if (comment.isDraft()) {
+ headerText.append(", ");
+ headerText.append("Draft");
+ }
+
+ if (comment.isDefectRaised()) {
+ headerText.append(", ");
+ headerText.append("Defect");
+
+ Map<String, CustomField> fields = comment.getCustomFields();
+ if (fields != null) {
+ boolean shouldCloseBracket = false;
+ if (fields.containsKey(CrucibleConstants.RANK_CUSTOM_FIELD_KEY)) {
+ headerText.append(" (");
+ shouldCloseBracket = true;
+ headerText.append(fields.get(CrucibleConstants.RANK_CUSTOM_FIELD_KEY).getValue());
+ }
+
+ if (fields.containsKey(CrucibleConstants.CLASSIFICATION_CUSTOM_FIELD_KEY)) {
+ if (shouldCloseBracket) {
+ headerText.append(",");
+ }
+ headerText.append(" ");
+ headerText.append(fields.get(CrucibleConstants.CLASSIFICATION_CUSTOM_FIELD_KEY)
+ .getValue());
+ }
+ if (shouldCloseBracket) {
+ headerText.append(")");
+ }
+
+ }
+ }
+ return headerText.toString();
+ }
+
+ public static boolean isSimpleInfoEnough(Map<String, IntRanges> ranges) {
+ if (ranges.size() <= 1) {
+ return true;
+ }
+ final Iterator<Entry<String, IntRanges>> it = ranges.entrySet().iterator();
+ final IntRanges lines = it.next().getValue();
+ while (it.hasNext()) {
+ if (!lines.equals(it.next().getValue())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static String getCompactedLineInfoText(Map<String, IntRanges> ranges) {
+
+ if (isSimpleInfoEnough(ranges)) {
+ final StringBuilder infoText = new StringBuilder("File comment for ");
+ final Iterator<Entry<String, IntRanges>> it = ranges.entrySet().iterator();
+ Entry<String, IntRanges> curEntry = it.next();
+ IntRanges lines = curEntry.getValue();
+ infoText.append(getLineInfo(lines));
+ if (it.hasNext()) {
+ infoText.append(" in revisions: ");
+ } else {
+ infoText.append(" in revision: ");
+ }
+
+ do {
+ infoText.append(curEntry.getKey());
+ if (it.hasNext()) {
+ infoText.append(", ");
+ } else {
+ break;
+ }
+ curEntry = it.next();
+ } while (true);
+ return infoText.toString();
+ } else {
+ final StringBuilder infoText = new StringBuilder("File comment for:\n");
+ for (Map.Entry<String, IntRanges> range : ranges.entrySet()) {
+ infoText.append("- ");
+ infoText.append(getLineInfo(range.getValue()));
+ infoText.append(" in revision: ");
+ infoText.append(range.getKey());
+ infoText.append("\n");
+ }
+ return infoText.toString();
+ }
+ }
+
+ public static String getLineInfo(IntRanges intRanges) {
+ if (intRanges.getTotalMin() == intRanges.getTotalMax()) {
+ return "line " + intRanges.getTotalMin();
+ } else {
+ return "lines " + intRanges.toNiceString();
+ }
+ }
+
+ @SuppressWarnings("restriction")
+ public static RichTextEditor createWikiTextControl(FormToolkit toolkit, Composite parent, Comment comment) {
+
+ int style = SWT.FLAT | SWT.READ_ONLY | SWT.MULTI | SWT.WRAP;
+
+ ITask task = CrucibleUiUtil.getCrucibleTask(comment.getReview());
+
+ TaskRepository repository = TasksUi.getRepositoryManager().getRepository(task.getConnectorKind(),
+ task.getRepositoryUrl());
+
+ final AbstractTaskEditorExtension extension = new ConfluenceMarkupTaskEditorExtension();
+ IContextService contextService = (IContextService) PlatformUI.getWorkbench().getService(IContextService.class);
+
+ final RichTextEditor editor = new RichTextEditor(repository, style, contextService, extension);
+
+ editor.setReadOnly(true);
+ editor.setText(comment.getMessage());
+ editor.createControl(parent, toolkit);
+
+ // HACK: this is to make sure that we can't have multiple things highlighted
+ editor.getViewer().getTextWidget().addFocusListener(new FocusListener() {
+
+ public void focusGained(FocusEvent e) {
+ }
+
+ public void focusLost(FocusEvent e) {
+ editor.getViewer().getTextWidget().setSelection(0);
+ }
+ });
+
+ return editor;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/EditorUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/EditorUtil.java
new file mode 100644
index 0000000..455ffe0
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/util/EditorUtil.java
@@ -0,0 +1,198 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.util;
+
+import com.atlassian.connector.commons.misc.IntRanges;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.ICrucibleFileProvider;
+import com.atlassian.connector.eclipse.internal.crucible.ui.operations.CrucibleFileInfoCompareEditorInput;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.CrucibleFile;
+import com.atlassian.theplugin.commons.VersionedVirtualFile;
+import com.atlassian.theplugin.commons.crucible.api.model.CrucibleFileInfo;
+import com.atlassian.theplugin.commons.crucible.api.model.VersionedComment;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.texteditor.AbstractTextEditor;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.ui.texteditor.ITextEditor;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+public final class EditorUtil {
+ private EditorUtil() {
+ // do not instantiate
+ }
+
+ /**
+ * Tests if a CU is currently shown in an editor
+ *
+ * @param inputElement
+ * the input element
+ * @return the IEditorPart if shown, null if element is not open in an editor
+ */
+ public static IEditorPart isOpenInEditor(Object inputElement) {
+ IWorkbenchPage page = getActivePage();
+
+ CrucibleFileInfo fileInfo = null;
+ VersionedComment parentComment = null;
+ if (inputElement instanceof CrucibleFileInfo) {
+ fileInfo = (CrucibleFileInfo) inputElement;
+ }
+// else if (inputElement instanceof Comment) {
+// parentComment = ReviewModelUtil.getParentVersionedComment((Comment) inputElement);
+// if (parentComment != null) {
+// fileInfo = parentComment.getCrucibleFileInfo();
+// }
+// }
+
+ if (fileInfo == null) {
+ return null;
+ }
+
+ if (page != null) {
+ // check current first because we can match multiple editors and would be bad if we switched
+ // user to another editor if current matches too
+ IEditorPart editor = page.getActiveEditor();
+ if (editor != null) {
+ if (isOpenInEditor(fileInfo, parentComment, editor.getEditorInput())) {
+ return editor;
+ }
+ }
+
+ IEditorReference[] editors = page.getEditorReferences();
+ if (editors != null) {
+ for (IEditorReference ref : editors) {
+ try {
+ if (isOpenInEditor(fileInfo, parentComment, ref.getEditorInput())) {
+ return ref.getEditor(true);
+ }
+ } catch (PartInitException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean isOpenInEditor(CrucibleFileInfo fileInfo, VersionedComment parentComment, IEditorInput input) {
+ if (input instanceof ICrucibleFileProvider) {
+ CrucibleFile crucibleFile = ((ICrucibleFileProvider) input).getCrucibleFile();
+ if (fileInfo.equals(crucibleFile.getCrucibleFileInfo())) {
+ if (parentComment != null) {
+ Map<String, IntRanges> commentRanges = parentComment.getLineRanges();
+ if (commentRanges != null
+ && commentRanges.containsKey(crucibleFile.getSelectedFile().getRevision())) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ }
+ if (input instanceof CrucibleFileInfoCompareEditorInput) {
+ if (fileInfo.equals(((CrucibleFileInfoCompareEditorInput) input).getCrucibleFileInfo())) {
+ return true;
+ }
+ }
+ if (input instanceof IFileEditorInput) {
+ CrucibleFile fromEditor = CrucibleUiUtil.getCruciblePostCommitFile(((IFileEditorInput) input).getFile(),
+ CrucibleUiPlugin.getDefault().getActiveReviewManager().getActiveReview());
+ if (fromEditor != null && fileInfo.equals(fromEditor.getCrucibleFileInfo())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void internalSelectAndReveal(ITextEditor textEditor, final int offset, final int length) {
+ textEditor.selectAndReveal(offset, length);
+ }
+
+ public static void selectAndReveal(final ITextEditor textEditor, int startLine) {
+ IDocumentProvider documentProvider = textEditor.getDocumentProvider();
+ IEditorInput editorInput = textEditor.getEditorInput();
+ if (documentProvider != null) {
+ IDocument document = documentProvider.getDocument(editorInput);
+ if (document != null) {
+ try {
+ final int offset = document.getLineOffset(startLine);
+ if (Display.getCurrent() != null) {
+ internalSelectAndReveal(textEditor, offset, 0);
+ } else {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ internalSelectAndReveal(textEditor, offset, 0);
+ }
+ });
+ }
+
+ } catch (BadLocationException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, AtlassianTeamUiPlugin.PLUGIN_ID, e.getMessage(), e));
+ }
+ }
+ }
+ }
+
+ public static void selectAndReveal(ITextEditor textEditor, VersionedComment comment, VersionedVirtualFile file) {
+ Map<String, IntRanges> lineRanges = comment.getLineRanges();
+ if (lineRanges == null) {
+ return;
+ }
+
+ IntRanges lineRange = lineRanges.get(file.getRevision());
+ if (lineRange != null) {
+ EditorUtil.selectAndReveal(textEditor, lineRange.getTotalMin() - 1);
+ }
+ }
+
+ public static IWorkbenchPage getActivePage() {
+ IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (window == null) {
+ return null;
+ }
+ return window.getActivePage();
+ }
+
+ public static SourceViewer getSourceViewer(ITextEditor editor) {
+ if (editor instanceof AbstractTextEditor) {
+ Method getSourceViewer;
+ try {
+ getSourceViewer = AbstractTextEditor.class.getDeclaredMethod("getSourceViewer");
+ getSourceViewer.setAccessible(true);
+ return (SourceViewer) getSourceViewer.invoke(editor);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/AbstractCrucibleWizardPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/AbstractCrucibleWizardPage.java
new file mode 100644
index 0000000..ce1e79a
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/AbstractCrucibleWizardPage.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import org.eclipse.jface.wizard.WizardPage;
+
+public abstract class AbstractCrucibleWizardPage extends WizardPage {
+
+ protected AbstractCrucibleWizardPage(String pageName) {
+ super(pageName);
+ }
+
+ public abstract void validatePage();
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleAddPatchPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleAddPatchPage.java
new file mode 100644
index 0000000..717dd5e
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleAddPatchPage.java
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.commons.CrucibleRepositoriesLabelProvider;
+import com.atlassian.theplugin.commons.crucible.api.model.Repository;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ComboViewer;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.ViewerSorter;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.Set;
+
+/**
+ * Page for adding a patch (from the clipboard) to a review
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class CrucibleAddPatchPage extends WizardPage {
+
+ private Text patchText;
+
+ private final TaskRepository taskRepository;
+
+ private String selectedRepository;
+
+ private Set<Repository> cachedRepositories;
+
+ private ComboViewer comboViewer;
+
+ private boolean includePatch = false;
+
+ protected boolean patchPasted = false;
+
+ public CrucibleAddPatchPage(TaskRepository repository) {
+ super("cruciblePatch"); //$NON-NLS-1$
+ setTitle("Add Patch to Review");
+ setDescription("Attach a patch from the clipboard to the review.");
+ this.taskRepository = repository;
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).margins(5, 5).create());
+
+ final Button includePatchButton = new Button(composite, SWT.CHECK);
+ includePatchButton.setText("Include this Patch from the clipboard in the review:");
+ GridDataFactory.fillDefaults().span(2, 1).applyTo(includePatchButton);
+ includePatchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ includePatch = includePatchButton.getSelection();
+ if (includePatch) {
+ //copy content from clipboard, only if not already done
+ Clipboard clipboard = new Clipboard(Display.getDefault());
+ Object patch = clipboard.getContents(TextTransfer.getInstance());
+ if (patch != null && patch instanceof String && !patchPasted) {
+ patchText.setText((String) patch);
+ patchPasted = true;
+ }
+ if (cachedRepositories == null) {
+ cachedRepositories = CrucibleUiUtil.getCachedRepositories(taskRepository);
+ }
+ comboViewer.setInput(cachedRepositories);
+ }
+
+ validatePage();
+ }
+ });
+
+ patchText = new Text(composite, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
+ GridDataFactory.fillDefaults().span(2, 1).hint(SWT.DEFAULT, 220).grab(true, true).applyTo(patchText);
+ patchText.setEditable(false);
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setText("Select the repository on Crucible:");
+ GridDataFactory.fillDefaults().grab(false, false).applyTo(label);
+ comboViewer = new ComboViewer(composite);
+ comboViewer.getCombo().setText("Select Repository");
+ comboViewer.setContentProvider(new ArrayContentProvider());
+ comboViewer.setLabelProvider(new CrucibleRepositoriesLabelProvider());
+ comboViewer.setSorter(new ViewerSorter());
+ comboViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ if (comboViewer.getSelection() instanceof IStructuredSelection) {
+ Object selected = ((IStructuredSelection) comboViewer.getSelection()).getFirstElement();
+ if (cachedRepositories.contains(selected)) {
+ selectedRepository = ((Repository) selected).getName();
+ }
+ }
+ validatePage();
+ }
+ });
+ GridDataFactory.fillDefaults()
+ .align(SWT.BEGINNING, SWT.BEGINNING)
+ .grab(true, false)
+ .hint(100, SWT.DEFAULT)
+ .applyTo(comboViewer.getCombo());
+
+ Button updateData = new Button(composite, SWT.PUSH);
+ updateData.setText("Update Repository Data");
+ GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.BEGINNING).applyTo(updateData);
+ updateData.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ }
+ });
+
+ Dialog.applyDialogFont(composite);
+ setControl(composite);
+ }
+
+ public boolean hasPatch() {
+ return includePatch && patchText.getText().length() > 0 && selectedRepository != null;
+ }
+
+ public String getPatch() {
+ return patchText.getText();
+ }
+
+ /*
+ * checks if page is complete updates the buttons
+ */
+ private void validatePage() {
+ setErrorMessage(null);
+
+ boolean allFine = true;
+ String errorMessage = null;
+ if (patchText.getText().length() < 1) {
+ errorMessage = "In order to create a review from a patch,"
+ + " copy the patch to the clipboard before opening this Wizard.";
+ allFine = false;
+ } else if (selectedRepository == null) {
+ errorMessage = "Choose a repository on Crucible this patch relates to.";
+ allFine = false;
+ }
+ if (includePatch) {
+ setPageComplete(allFine);
+ if (errorMessage != null) {
+ setErrorMessage(errorMessage);
+ }
+ } else {
+ setPageComplete(true);
+ }
+
+ getContainer().updateButtons();
+ }
+
+ public String getPatchRepository() {
+ return selectedRepository;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewDetailsPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewDetailsPage.java
new file mode 100644
index 0000000..de463f7
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewDetailsPage.java
@@ -0,0 +1,456 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.commons.CrucibleProjectsLabelProvider;
+import com.atlassian.connector.eclipse.internal.crucible.ui.commons.CrucibleUserLabelProvider;
+import com.atlassian.connector.eclipse.internal.crucible.ui.editor.parts.ReviewersSelectionTreePart;
+import com.atlassian.theplugin.commons.crucible.api.model.BasicProject;
+import com.atlassian.theplugin.commons.crucible.api.model.ExtendedCrucibleProject;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+import com.atlassian.theplugin.commons.crucible.api.model.ReviewType;
+import com.atlassian.theplugin.commons.crucible.api.model.User;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.ComboViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.jface.viewers.ViewerSorter;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Page for entering details for the new crucible review
+ *
+ * @author Thomas Ehrnhoefer
+ * @author Pawel Niewiadomski
+ */
+public class CrucibleReviewDetailsPage extends WizardPage {
+ private static final String ENTER_THE_DETAILS_OF_THE_REVIEW = "Enter the details of the review.";
+
+ private final TaskRepository taskRepository;
+
+ private ComboViewer authorComboViewer;
+
+ private ComboViewer projectsComboViewer;
+
+ private ComboViewer moderatorComboViewer;
+
+ private Text titleText;
+
+ private Text objectivesText;
+
+ private ReviewersSelectionTreePart reviewersSelectionTreePart;
+
+ private Button anyoneCanJoin;
+
+ private Button startReview;
+
+ private boolean firstTimeCheck = true;
+
+ private final boolean addComment;
+
+ private Text commentText;
+
+ public CrucibleReviewDetailsPage(TaskRepository repository) {
+ this(repository, false);
+ }
+
+ public CrucibleReviewDetailsPage(TaskRepository repository, boolean addComment) {
+ super("crucibleDetails"); //$NON-NLS-1$
+ this.addComment = addComment;
+ Assert.isNotNull(repository);
+ setTitle("New Crucible Review");
+ setDescription(ENTER_THE_DETAILS_OF_THE_REVIEW);
+ this.taskRepository = repository;
+ }
+
+ private boolean firstTime = true;
+
+ @Override
+ public void setVisible(final boolean visible) {
+ // check if cached data is available, if not, start background process to fetch it
+ if (visible && firstTime) {
+ firstTime = false;
+ // preselect
+ setInputAndInitialSelections();
+ }
+ super.setVisible(visible);
+ if (visible) {
+ titleText.setFocus();
+ }
+
+ }
+
+ private void setInputAndInitialSelections() {
+ updateInput();
+
+ final String lastSelectedProjectKey = CrucibleUiPlugin.getDefault().getLastSelectedProjectKey(taskRepository);
+ final BasicProject lastSelectedProject = CrucibleUiUtil.getCachedProject(taskRepository, lastSelectedProjectKey);
+ if (lastSelectedProject != null) {
+ projectsComboViewer.setSelection(new StructuredSelection(lastSelectedProject));
+ } else {
+ if (projectsComboViewer.getElementAt(0) != null) {
+ projectsComboViewer.setSelection(new StructuredSelection(projectsComboViewer.getElementAt(0)));
+ }
+ }
+
+ User currentUser = CrucibleUiUtil.getCurrentCachedUser(taskRepository);
+ if (currentUser != null) {
+ moderatorComboViewer.setSelection(new StructuredSelection(currentUser));
+ authorComboViewer.setSelection(new StructuredSelection(currentUser));
+ } else {
+ if (moderatorComboViewer.getElementAt(0) != null) {
+ moderatorComboViewer.setSelection(new StructuredSelection(moderatorComboViewer.getElementAt(0)));
+ }
+ if (authorComboViewer.getElementAt(0) != null) {
+ authorComboViewer.setSelection(new StructuredSelection(authorComboViewer.getElementAt(0)));
+ }
+ }
+
+ // restore checkboxes selection
+ anyoneCanJoin.setSelection(CrucibleUiPlugin.getDefault().getAllowAnyoneOption(taskRepository));
+ startReview.setSelection(CrucibleUiPlugin.getDefault().getStartReviewOption(taskRepository));
+ updateReviewersControl();
+ }
+
+ private void updateInput() {
+ Collection<BasicProject> cachedProjects = CrucibleUiUtil.getCachedProjects(taskRepository);
+ projectsComboViewer.setInput(cachedProjects);
+
+ Set<User> cachedUsers = CrucibleUiUtil.getCachedUsers(taskRepository);
+ moderatorComboViewer.setInput(cachedUsers);
+ authorComboViewer.setInput(cachedUsers);
+ }
+
+ private void updateInputAndRestoreSelections() {
+ BasicProject previousProject = (BasicProject) ((IStructuredSelection) projectsComboViewer.getSelection()).getFirstElement();
+ User previousModerator = (User) ((IStructuredSelection) moderatorComboViewer.getSelection()).getFirstElement();
+ User previousAuthor = (User) ((IStructuredSelection) authorComboViewer.getSelection()).getFirstElement();
+
+ updateInput();
+
+ if (previousProject != null) {
+ projectsComboViewer.setSelection(new StructuredSelection(previousProject));
+ }
+
+ if (previousModerator != null) {
+ moderatorComboViewer.setSelection(new StructuredSelection(previousModerator));
+ }
+
+ if (previousAuthor != null) {
+ authorComboViewer.setSelection(new StructuredSelection(previousAuthor));
+ }
+ updateReviewersControl();
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(6).margins(5, 5).create());
+
+ new Label(composite, SWT.NONE).setText("Title:");
+ titleText = new Text(composite, SWT.BORDER);
+ titleText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ getContainer().updateButtons();
+ }
+ });
+ GridDataFactory.fillDefaults().span(5, 1).grab(true, false).applyTo(titleText);
+
+ new Label(composite, SWT.NONE).setText("Project:");
+ projectsComboViewer = new ComboViewer(composite);
+ projectsComboViewer.setLabelProvider(new CrucibleProjectsLabelProvider());
+ projectsComboViewer.setContentProvider(new ArrayContentProvider());
+ projectsComboViewer.setSorter(new ViewerSorter());
+ projectsComboViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ updateReviewersControl();
+ }
+ });
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(projectsComboViewer.getCombo());
+
+ new Label(composite, SWT.NONE).setText("Moderator:");
+ moderatorComboViewer = new ComboViewer(composite);
+ moderatorComboViewer.setLabelProvider(new CrucibleUserLabelProvider());
+ moderatorComboViewer.setContentProvider(new ArrayContentProvider());
+ moderatorComboViewer.setComparator(new ViewerComparator());
+ moderatorComboViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ Object firstElement = ((IStructuredSelection) event.getSelection()).getFirstElement();
+ if (firstElement != null) {
+ getWizard().getContainer().updateButtons();
+ }
+ }
+ });
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(moderatorComboViewer.getCombo());
+
+ new Label(composite, SWT.NONE).setText("Author:");
+ authorComboViewer = new ComboViewer(composite);
+ authorComboViewer.setLabelProvider(new CrucibleUserLabelProvider());
+ authorComboViewer.setContentProvider(new ArrayContentProvider());
+ authorComboViewer.setComparator(new ViewerComparator());
+ authorComboViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ Object firstElement = ((IStructuredSelection) event.getSelection()).getFirstElement();
+ if (firstElement != null) {
+ getWizard().getContainer().updateButtons();
+ }
+ }
+ });
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(authorComboViewer.getCombo());
+
+ Label label = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
+ GridDataFactory.fillDefaults().grab(true, false).span(6, 1).applyTo(label);
+
+ label = new Label(composite, SWT.NONE);
+ label.setText("Objectives:");
+
+ GridDataFactory.fillDefaults().span(4, 1).applyTo(label);
+
+ label = new Label(composite, SWT.NONE);
+ label.setText("Reviewers:");
+ GridDataFactory.fillDefaults().span(2, 1).indent(5, SWT.DEFAULT).applyTo(label);
+
+ Composite textAreacomposite = new Composite(composite, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.marginLeft = 0;
+ layout.marginTop = 0;
+ layout.marginWidth = 0;
+ layout.horizontalSpacing = 0;
+ layout.verticalSpacing = 10;
+ textAreacomposite.setLayout(layout);
+ GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL | GridData.FILL_BOTH);
+ gd.horizontalIndent = 0;
+ textAreacomposite.setLayoutData(gd);
+ GridDataFactory.fillDefaults().grab(true, true).hint(480, 200).span(4, 1).applyTo(textAreacomposite);
+
+ objectivesText = new Text(textAreacomposite, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.WRAP);
+ GridDataFactory.fillDefaults().grab(true, true).hint(480, 200).applyTo(objectivesText);
+
+ if (addComment) {
+ label = new Label(textAreacomposite, SWT.NONE);
+ label.setText("Selection Comment:");
+ GridDataFactory.fillDefaults().applyTo(label);
+ commentText = new Text(textAreacomposite, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.WRAP);
+ GridDataFactory.fillDefaults().grab(true, true).hint(480, 200).applyTo(commentText);
+ }
+
+ reviewersSelectionTreePart = new ReviewersSelectionTreePart(Collections.<User> emptySet(),
+ Collections.<User> emptyList());
+ Composite reviewersComp = reviewersSelectionTreePart.createControl(composite);
+ GridDataFactory.fillDefaults().grab(true, true).span(2, 1).hint(SWT.DEFAULT, 150).applyTo(reviewersComp);
+ reviewersSelectionTreePart.setCheckStateListener(new ICheckStateListener() {
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ getWizard().getContainer().updateButtons();
+ }
+ });
+
+ Button updateData = new Button(composite, SWT.PUSH);
+ updateData.setText("Update Repository Data");
+ GridDataFactory.fillDefaults().span(4, 2).align(SWT.BEGINNING, SWT.BEGINNING).applyTo(updateData);
+ updateData.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ CrucibleUiUtil.updateTaskRepositoryCache(taskRepository, getContainer(), CrucibleReviewDetailsPage.this);
+ updateInputAndRestoreSelections();
+ }
+ });
+
+ anyoneCanJoin = new Button(composite, SWT.CHECK);
+ anyoneCanJoin.setText("Allow anyone to join");
+ GridDataFactory.fillDefaults().indent(5, SWT.DEFAULT).span(2, 1).applyTo(anyoneCanJoin);
+
+ startReview = new Button(composite, SWT.CHECK);
+ startReview.setText("Start review immediately (if permitted)");
+ GridDataFactory.fillDefaults()
+ .span(2, 1)
+ .align(SWT.BEGINNING, SWT.BEGINNING)
+ .indent(5, SWT.DEFAULT)
+ .applyTo(startReview);
+
+ Dialog.applyDialogFont(composite);
+ setControl(composite);
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ Review review = getReview();
+ return review != null && hasRequiredFields(review) && hasValidReviewers(review);
+ }
+
+ private boolean hasRequiredFields(Review newReview) {
+ setErrorMessage(null);
+ String newMessage = null;
+
+ if (getSelectedProject() == null) {
+ newMessage = "Select a project";
+ }
+ if (newReview.getModerator() == null) {
+ newMessage = "Select a moderator";
+ }
+ if (getSelectedAuthor() == null) {
+ newMessage = "Select an author";
+ }
+ if (newReview.getSummary() == null || newReview.getSummary().trim().length() == 0) {
+ newMessage = "Enter a title for the review";
+ }
+ if (newMessage != null) {
+ if (firstTimeCheck) {
+ setMessage(newMessage);
+ } else {
+ setErrorMessage(newMessage);
+ }
+ return false;
+ }
+ setMessage(ENTER_THE_DETAILS_OF_THE_REVIEW);
+ firstTimeCheck = false;
+ return true;
+ }
+
+ private boolean hasValidReviewers(Review newReview) {
+ setErrorMessage(null);
+ for (User reviewer : reviewersSelectionTreePart.getSelectedReviewers()) {
+ if (newReview.getAuthor().getUsername().equals(reviewer.getUsername())) {
+ setErrorMessage("The author might not be a reviewer as well.");
+ return false;
+ }
+ if (newReview.getModerator().getUsername().equals(reviewer.getUsername())) {
+ setErrorMessage("The moderator might not be a reviewer as well.");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean canFlipToNextPage() {
+ return false;
+ }
+
+ public Review getReview() {
+ final User author = getSelectedAuthor();
+ final BasicProject project = getSelectedProject();
+
+ if (project == null || project.getKey() == null) {
+ return null;
+ }
+
+ Review review = new Review(ReviewType.REVIEW, taskRepository.getUrl(), project.getKey(), author, null);
+
+ if (titleText != null) {
+ review.setName(titleText.getText());
+ review.setSummary(titleText.getText());
+ }
+
+ if (objectivesText != null) {
+ review.setDescription(objectivesText.getText());
+ }
+
+ if (reviewersSelectionTreePart != null) {
+ review.setReviewers(CrucibleUiUtil.toReviewers(reviewersSelectionTreePart.getSelectedReviewers()));
+ }
+
+ if (anyoneCanJoin != null) {
+ review.setAllowReviewerToJoin(anyoneCanJoin.getEnabled());
+ }
+ review.setCreator(CrucibleUiUtil.getCurrentCachedUser(taskRepository));
+
+ if (moderatorComboViewer != null) {
+ review.setModerator((User) ((IStructuredSelection) moderatorComboViewer.getSelection()).getFirstElement());
+ }
+
+ return review;
+ }
+
+ private User getSelectedAuthor() {
+ User author = (authorComboViewer != null) ? (User) ((IStructuredSelection) authorComboViewer.getSelection()).getFirstElement()
+ : null;
+ return author;
+ }
+
+ Set<User> getReviewers() {
+ return reviewersSelectionTreePart.getSelectedReviewers();
+ }
+
+ boolean isStartReviewImmediately() {
+ return startReview.getSelection();
+ }
+
+ BasicProject getSelectedProject() {
+ if (projectsComboViewer == null) {
+ return null;
+ }
+ Object firstElement = ((IStructuredSelection) projectsComboViewer.getSelection()).getFirstElement();
+ if (firstElement != null) {
+ return (BasicProject) firstElement;
+ }
+ return null;
+ }
+
+ boolean isAllowAnyoneToJoin() {
+ return anyoneCanJoin.getSelection();
+ }
+
+ public String getComment() {
+ return commentText.getText();
+ }
+
+ private void updateReviewersControl() {
+ BasicProject project = getSelectedProject();
+ if (project != null) {
+ BasicProject details = CrucibleUiUtil.getCachedProject(taskRepository, project.getKey());
+
+ if (!(details instanceof ExtendedCrucibleProject)) {
+ details = CrucibleUiUtil.getCachedProject(taskRepository, project.getKey());
+ }
+
+ if (details instanceof ExtendedCrucibleProject
+ && ((ExtendedCrucibleProject) details).getAllowedReviewers() != null
+ && ((ExtendedCrucibleProject) details).getAllowedReviewers().size() > 0) {
+ Collection<String> allowedReviewersNames = ((ExtendedCrucibleProject) details).getAllowedReviewers();
+ final Collection<User> allowedReviewers = CrucibleUiUtil.getUsersFromUsernames(taskRepository,
+ allowedReviewersNames);
+ reviewersSelectionTreePart.setAllReviewers(allowedReviewers);
+ } else {
+ reviewersSelectionTreePart.setAllReviewers(CrucibleUiUtil.getCachedUsers(taskRepository));
+ }
+
+ getWizard().getContainer().updateButtons();
+ }
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewWizard.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewWizard.java
new file mode 100644
index 0000000..cc8ba78
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/CrucibleReviewWizard.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+
+/**
+ * Wizard for creating a new review
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class CrucibleReviewWizard extends Wizard implements INewWizard {
+ private final TaskRepository taskRepository;
+
+ public CrucibleReviewWizard(TaskRepository taskRepository) {
+ setWindowTitle("New Crucible Review");
+ setNeedsProgressMonitor(true);
+ setForcePreviousAndNextButtons(true);
+ this.taskRepository = taskRepository;
+ }
+
+ @Override
+ public void addPages() {
+ addPage(new ReviewTypeSelectionPage(getTaskRepository()));
+ }
+
+ @Override
+ public boolean performFinish() {
+ return true;
+ }
+
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ // ignore
+ }
+
+ public TaskRepository getTaskRepository() {
+ return taskRepository;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingButton.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingButton.java
new file mode 100644
index 0000000..277aaae
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingButton.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+public class DefineRepositoryMappingButton {
+
+// private final AbstractCrucibleWizardPage page;
+
+ private final Composite composite;
+
+// private final TaskRepository repository;
+
+ private final Control defineMappingButton;
+
+// private String missingMapping; //N.A. to open source
+
+ public DefineRepositoryMappingButton(final AbstractCrucibleWizardPage page, Composite composite,
+ final TaskRepository repository) {
+// this.page = page;
+ this.composite = composite;
+// this.repository = repository;
+
+ this.defineMappingButton = createDefineRepositoryMappingsButton();
+
+ }
+
+ public Control getControl() {
+ return defineMappingButton;
+ }
+
+ public void setMissingMapping(String mapping) {
+// this.missingMapping = mapping; //N.A. to open source
+ }
+
+ private Control createDefineRepositoryMappingsButton() {
+
+ Button updateData = new Button(composite, SWT.PUSH);
+ updateData.setText("Define Repository Mappings");
+
+ //Removed: Not applicable to open source
+// updateData.addSelectionListener(new SelectionAdapter() {
+// @Override
+// public void widgetSelected(SelectionEvent e) {
+// FishEyePreferenceContextData data = page.isPageComplete() ? null : new FishEyePreferenceContextData(
+// missingMapping == null ? "" : missingMapping, repository);
+// final PreferenceDialog prefDialog = PreferencesUtil.createPreferenceDialogOn(page.getShell(),
+// SourceRepositoryMappingPreferencePage.ID, null, data);
+// if (prefDialog != null) {
+// if (prefDialog.open() == Window.OK) {
+// page.validatePage();
+// }
+// }
+// }
+// });
+ return updateData;
+ }
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingsPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingsPage.java
new file mode 100644
index 0000000..aa8a408
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/DefineRepositoryMappingsPage.java
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.TaskRepositoryUtil;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.LocalStatus;
+import com.atlassian.connector.eclipse.team.ui.TeamUiResourceManager;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+
+import java.util.Collection;
+import java.util.Set;
+
+public class DefineRepositoryMappingsPage extends WizardPage {
+
+ private final TaskRepository taskRepository;
+
+// private SourceRepostioryMappingEditor mappingEditor;
+
+ private Set<IResource> resources;
+
+ private Collection<String> scmPaths;
+
+ public DefineRepositoryMappingsPage(TaskRepository repository, Collection<IResource> resources) {
+ super("crucibleRepoMapping");
+
+ this.taskRepository = repository;
+ this.resources = MiscUtil.buildHashSet();
+ this.resources.addAll(resources);
+
+ setTitle("Define Repository Mapping");
+ setDescription("Define repository mapping used to create review.");
+ }
+
+ public DefineRepositoryMappingsPage(Collection<String> scmPaths, TaskRepository repository) {
+ super("crucibleRepoMapping");
+
+ this.taskRepository = repository;
+ this.scmPaths = MiscUtil.buildArrayList();
+ this.scmPaths.addAll(scmPaths);
+
+ setTitle("Define Repository Mapping");
+ setDescription("Define repository mapping used to create review.");
+ }
+
+ public Composite createRepositoryMappingComposite(Composite ancestor, int hSizeHint) {
+ final Composite parent = new Composite(ancestor, SWT.NONE);
+
+// mappingEditor = new SourceRepostioryMappingEditor(parent, taskRepository);
+// mappingEditor.addModifyListener(new ModifyListener() {
+// public void modifyText(ModifyEvent event) {
+// try {
+// FishEyeUiUtil.setScmRepositoryMappings(mappingEditor.getMapping());
+// } catch (IOException e) {
+// ErrorDialog.openError(getShell(), "", "Error while saving FishEye mapping configuration",
+// new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e));
+// }
+// validatePage();
+// }
+// });
+// mappingEditor.setRepositoryMappings(FishEyeUiUtil.getActiveScmRepositoryMappings());
+
+ return parent;
+ }
+
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ validatePage();
+
+// if (visible && !CrucibleUiUtil.hasCachedData(getTaskRepository())) {
+// final WizardPage page = this;
+// Display.getDefault().asyncExec(new Runnable() {
+// public void run() {
+// if (!CrucibleUiUtil.hasCachedData(getTaskRepository())) {
+// CrucibleUiUtil.updateTaskRepositoryCache(taskRepository, getContainer(), page);
+// }
+// }
+// });
+// }
+ }
+
+ public TaskRepository getTaskRepository() {
+ return taskRepository;
+ }
+
+ protected void validatePage() {
+ setErrorMessage(null);
+ setMessage("All necessary mappings are defined.");
+
+ //check if all custom repositories are mapped to Crucible repositories
+ boolean allFine = true;
+
+ if (scmPaths == null) {
+ if (resources != null) {
+ TeamUiResourceManager teamManager = AtlassianTeamUiPlugin.getDefault().getTeamResourceManager();
+
+ for (IResource resource : resources) {
+ ITeamUiResourceConnector teamConnector = teamManager.getTeamConnector(resource);
+
+ final LocalStatus revision;
+ try {
+ revision = teamConnector.getLocalRevision(resource);
+ } catch (CoreException e) {
+ setErrorMessage(e.getMessage());
+ return;
+ }
+
+ if (revision != null && revision.isVersioned()) {
+ if (TaskRepositoryUtil.getMatchingSourceRepository(
+ TaskRepositoryUtil.getScmRepositoryMappings(getTaskRepository()), revision.getScmPath()) == null) {
+ setErrorMessage(NLS.bind(
+ "Unable to map SCM path {0} to Crucible repository. Please define a mapping.",
+ revision.getScmPath()));
+ allFine = false;
+ break;
+ }
+ }
+ }
+ }
+ /*
+ else {
+ // no input to validate so the page is valid
+ }
+ */
+ } else {
+ for (String path : scmPaths) {
+ if (TaskRepositoryUtil.getMatchingSourceRepository(
+ TaskRepositoryUtil.getScmRepositoryMappings(getTaskRepository()), path) == null) {
+ allFine = false;
+ setErrorMessage(NLS.bind(
+ "Unable to map SCM path {0} to Crucible repository. Please define a mapping.", path));
+ }
+ }
+ }
+
+ setPageComplete(allFine);
+ getContainer().updateButtons();
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(3).margins(5, 5).create());
+
+ Composite repositoryMappingViewer = createRepositoryMappingComposite(composite, 700);
+ GridDataFactory.fillDefaults()
+ .span(3, 1)
+ .align(SWT.FILL, SWT.FILL)
+ .grab(true, true)
+ .applyTo(repositoryMappingViewer);
+ repositoryMappingViewer.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).create());
+
+ Dialog.applyDialogFont(composite);
+ setControl(composite);
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/RepositorySelectionWizard.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/RepositorySelectionWizard.java
new file mode 100644
index 0000000..a08e8cf
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/RepositorySelectionWizard.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.mylyn.internal.tasks.ui.wizards.SelectRepositoryPage;
+import org.eclipse.mylyn.tasks.ui.TasksUiImages;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+
+/**
+ * Wizard for selecting a repository and opening the newCrucibleReviewWizard afterwards
+ *
+ * @author thomas
+ */
+public class RepositorySelectionWizard extends Wizard implements INewWizard {
+
+ private final SelectRepositoryPage selectRepositoryPage;
+
+ public RepositorySelectionWizard(SelectRepositoryPage srp) {
+ this.selectRepositoryPage = srp;
+ setForcePreviousAndNextButtons(true);
+ setNeedsProgressMonitor(true);
+ setWindowTitle("Create");
+ setDefaultPageImageDescriptor(TasksUiImages.BANNER_REPOSITORY);
+ }
+
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ // ignore
+ }
+
+ @Override
+ public void addPages() {
+ addPage(selectRepositoryPage);
+ }
+
+ @Override
+ public boolean canFinish() {
+ return selectRepositoryPage.canFinish();
+ }
+
+ @Override
+ public boolean performFinish() {
+ return selectRepositoryPage.performFinish();
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ResourceSelectionPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ResourceSelectionPage.java
new file mode 100644
index 0000000..6a67b52
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ResourceSelectionPage.java
@@ -0,0 +1,242 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.TaskRepositoryUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.TeamUiUtils;
+import com.atlassian.connector.eclipse.ui.commons.CustomToolTip;
+import com.atlassian.connector.eclipse.ui.commons.DecoratedResource;
+import com.atlassian.connector.eclipse.ui.commons.ResourceSelectionTree;
+import com.atlassian.connector.eclipse.ui.commons.ResourceSelectionTree.ITreeViewModeSettingProvider;
+import com.atlassian.connector.eclipse.ui.commons.ResourceSelectionTree.TreeViewMode;
+import com.atlassian.connector.eclipse.ui.viewers.DecoratedResourceInfoProvider;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class ResourceSelectionPage extends AbstractCrucibleWizardPage {
+
+ private final List<IResource> roots = new ArrayList<IResource>();
+
+ private final List<DecoratedResource> resourcesToShow = new ArrayList<DecoratedResource>();
+
+ private final Collection<String> scmPaths = new ArrayList<String>();
+
+ private final ITeamUiResourceConnector teamConnector;
+
+ private final TaskRepository taskRepository;
+
+ private DefineRepositoryMappingButton mappingButtonFactory;
+
+ private ResourceSelectionTree resourceSelectionTree;
+
+ public ResourceSelectionPage(TaskRepository taskRepository, ITeamUiResourceConnector teamConnector,
+ List<IResource> roots) {
+ super("Add Resources to Review");
+ this.taskRepository = taskRepository;
+ setTitle("Add Resources to Review");
+ setDescription("Add selected resources to Review. Modified or unversioned resources will be added as pre-commit review items.");
+
+ this.roots.addAll(roots);
+ this.teamConnector = teamConnector;
+ }
+
+ public List<DecoratedResource> getSelection() {
+
+ DecoratedResource[] selectedResources = resourceSelectionTree.getSelectedResources();
+
+ List<DecoratedResource> ret = new ArrayList<DecoratedResource>();
+
+ for (DecoratedResource resource : selectedResources) {
+ ret.add(resource);
+ }
+
+ // could return array instead of list
+ return ret;
+ }
+
+ /**
+ * Allow the user to chose to save the patch to the workspace or outside of the workspace.
+ */
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).margins(5, 5).create());
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(composite);
+
+ Dialog.applyDialogFont(composite);
+ initializeDialogUnits(composite);
+ setControl(composite);
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setText("Include resources:");
+
+ resourceSelectionTree = new ResourceSelectionTree(composite, "", resourcesToShow, null,
+ new ITreeViewModeSettingProvider() {
+
+ public void setTreeViewMode(TreeViewMode mode) {
+ CrucibleUiPlugin.getDefault().setResourcesTreeViewMode(mode);
+ }
+
+ public TreeViewMode getTreeViewMode() {
+ return CrucibleUiPlugin.getDefault().getResourcesTreeViewMode();
+ }
+ });
+
+ resourceSelectionTree.getTreeViewer().addCheckStateListener(new ICheckStateListener() {
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ validatePage();
+ }
+ });
+
+ GridDataFactory.fillDefaults()
+ .span(2, 1)
+ .hint(SWT.DEFAULT, 220)
+ .grab(true, true)
+ .applyTo(resourceSelectionTree);
+
+ resourceSelectionTree.getTreeViewer().getTree().setToolTipText("");
+ final CustomToolTip toolTip = new CustomToolTip(resourceSelectionTree.getTreeViewer().getControl());
+ toolTip.setInfoProvider(DecoratedResourceInfoProvider.getInstance());
+
+ resourceSelectionTree.getTreeViewer().getTree().addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ toolTip.hide();
+ }
+ });
+
+ mappingButtonFactory = new DefineRepositoryMappingButton(this, composite, taskRepository);
+ Control buttonControl = mappingButtonFactory.getControl();
+ GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.BEGINNING).applyTo(buttonControl);
+
+ populateResourcesTree();
+
+ validatePage();
+ }
+
+ private void populateResourcesTree() {
+
+ resourcesToShow.clear();
+
+ IRunnableWithProgress getModifiedResources = new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ SubMonitor.convert(monitor, "Getting workspace resources data", IProgressMonitor.UNKNOWN);
+
+ final Collection<IResource> resources = MiscUtil.buildLinkedHashSet();
+
+ resources.addAll(teamConnector.getResourcesByFilterRecursive(
+ roots.toArray(new IResource[roots.size()]), ITeamUiResourceConnector.State.SF_ALL));
+
+ for (IResource resource : resources) {
+ if (resource instanceof IFile) {
+
+ // collect all scmPaths in order to find missing mappings
+ String path = TeamUiUtils.getScmPath(resource, teamConnector);
+ if (path != null) {
+ scmPaths.add(path);
+ }
+
+ DecoratedResource dr = TeamUiUtils.getDecoratedResource(resource, teamConnector);
+ if (dr != null) {
+ resourcesToShow.add(dr);
+ }
+ }
+ }
+ }
+ };
+
+ try {
+ getContainer().run(false, false, getModifiedResources);
+ } catch (InvocationTargetException e) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID,
+ "Can't get list of modified resources", e));
+ } catch (InterruptedException e) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID,
+ "Can't get list of modified resources", e));
+ }
+
+ resourceSelectionTree.setResources(resourcesToShow);
+ resourceSelectionTree.refresh();
+ resourceSelectionTree.setAllChecked(true);
+
+ validatePage();
+
+ }
+
+ public void validatePage() {
+ setErrorMessage(null);
+
+ String errorMessage = null;
+
+ // check selection
+ if (resourceSelectionTree.getSelectedResources().length == 0) {
+ errorMessage = "Nothing is selected.";
+ }
+
+ // check repository mapping for committed root resources
+ for (String scmPath : scmPaths) {
+ Map.Entry<String, String> sourceRepository = null;
+ sourceRepository = TaskRepositoryUtil.getMatchingSourceRepository(
+ TaskRepositoryUtil.getScmRepositoryMappings(taskRepository), scmPath);
+
+ if (sourceRepository == null || sourceRepository.getValue() == null
+ || sourceRepository.getValue().length() == 0) {
+ errorMessage = "SCM Repository Mapping is not defined.";
+ // TODO PLE-841 (do not suggest mapping to user)
+// mappingButtonFactory.setMissingMapping(scmPath);
+ }
+ }
+
+ // validate page
+ if (errorMessage == null) {
+ setPageComplete(true);
+ } else {
+ setPageComplete(false);
+ setErrorMessage(errorMessage);
+ }
+
+ if (getContainer().getCurrentPage() != null) {
+ getContainer().updateButtons();
+ }
+ }
+
+ public ITeamUiResourceConnector getTeamResourceConnector() {
+ return teamConnector;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewTypeSelectionPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewTypeSelectionPage.java
new file mode 100644
index 0000000..5e1fc00
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewTypeSelectionPage.java
@@ -0,0 +1,211 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.wizards.ReviewWizard.Type;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.TeamUiUtils;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.wizard.IWizard;
+import org.eclipse.jface.wizard.IWizardNode;
+import org.eclipse.jface.wizard.WizardSelectionPage;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Page for selecting which kind of review to create
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class ReviewTypeSelectionPage extends WizardSelectionPage {
+
+ private Button patchReview;
+
+ private Button changesetReview;
+
+ private Button workspacePatchReview;
+
+ private final TaskRepository taskRepository;
+
+ private final CrucibleUiPlugin plugin;
+
+ public ReviewTypeSelectionPage(TaskRepository taskRepository) {
+ super("crucibleSelection"); //$NON-NLS-1$
+ setTitle("Select type of review to create");
+ setDescription("Select which kind of review you want to create.");
+ this.taskRepository = taskRepository;
+ this.plugin = CrucibleUiPlugin.getDefault();
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().margins(5, 5).create());
+
+ new Label(composite, SWT.NONE).setText("Select how you want to add files to the review:");
+
+ Composite buttonComp = new Composite(composite, SWT.NONE);
+ buttonComp.setLayout(GridLayoutFactory.fillDefaults().margins(10, 5).create());
+
+ changesetReview = new Button(buttonComp, SWT.CHECK);
+ changesetReview.setText("From a Changeset");
+ changesetReview.setSelection(plugin.getPreviousChangesetReviewSelection());
+ changesetReview.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ plugin.setPreviousChangesetReviewSelection(changesetReview.getSelection());
+ }
+ });
+
+ patchReview = new Button(buttonComp, SWT.CHECK);
+ patchReview.setText("From a Patch");
+ patchReview.setSelection(plugin.getPreviousPatchReviewSelection());
+ patchReview.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ plugin.setPreviousPatchReviewSelection(patchReview.getSelection());
+ }
+ });
+
+ workspacePatchReview = new Button(buttonComp, SWT.CHECK);
+ workspacePatchReview.setText("From Workspace Changes");
+ workspacePatchReview.setSelection(plugin.getPreviousWorkspacePatchReviewSelection());
+ workspacePatchReview.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ plugin.setPreviousWorkspacePatchReviewSelection(workspacePatchReview.getSelection());
+ }
+ });
+
+ Set<ITeamUiResourceConnector> teamConnectors = AtlassianTeamUiPlugin.getDefault()
+ .getTeamResourceManager()
+ .getTeamConnectors();
+
+ if (teamConnectors.size() == 0) {
+ disablePreCommits();
+ showScmRelatedWarning(
+ buttonComp,
+ "You don't have any SCM integration installed for Atlassian Connector for Eclipse. "
+ + "You need to install at least Subversion or Perforce integration to be able to create reviews. "
+ + "<A href=\"http://confluence.atlassian.com/display/IDEPLUGIN/Installing+the+Eclipse+Connector\">"
+ + "Check installation guide for details</A>. "
+ + "\n\nIf you need <A href=\"https://studio.atlassian.com/browse/PLE-728\">CVS</A>"
+ + " integration help us prioritize our backlog by voting for it.");
+ } else {
+ final int[] repoCount = { 0 };
+ final IRunnableWithProgress getRepositories = new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ repoCount[0] = TeamUiUtils.getRepositories(monitor).size();
+ }
+ };
+ try {
+ getContainer().run(true, true, getRepositories);
+ } catch (InvocationTargetException e) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID,
+ "Failed to retrieve repositories", e));
+ } catch (InterruptedException e) {
+ StatusHandler.log(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID,
+ "Failed to retrieve repositories", e));
+ }
+ if (repoCount[0] == 0) {
+ disablePreCommits();
+ showScmRelatedWarning(
+ buttonComp,
+ "In order to create regular pre- or post-commit review you need to have at least one "
+ + "supported SCM repository configured in your Eclipse workspace.\n"
+ + "If you use SVN, make sure that your \"SVN Repositories\" View contain at least one entry.");
+ }
+ }
+
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(buttonComp);
+ setControl(composite);
+
+ setSelectedNode(new IWizardNode() {
+ private ReviewWizard wizard;
+
+ public boolean isContentCreated() {
+ // re-create this wizard every time
+ return false;
+ }
+
+ public IWizard getWizard() {
+ dispose();
+ wizard = new ReviewWizard(taskRepository, getTypes());
+ return wizard;
+ }
+
+ public Point getExtent() {
+ return null;
+ }
+
+ public void dispose() {
+ if (wizard != null) {
+ wizard.dispose();
+ }
+ }
+ });
+
+ }
+
+ private void disablePreCommits() {
+ workspacePatchReview.setSelection(false);
+ workspacePatchReview.setEnabled(false);
+ }
+
+ private void showScmRelatedWarning(Composite parent, String infoMessage) {
+ Link missingTeamConnectors = new Link(parent, SWT.WRAP | SWT.MULTI | SWT.READ_ONLY);
+ GridDataFactory.fillDefaults().grab(true, true).hint(250, SWT.DEFAULT).applyTo(missingTeamConnectors);
+ missingTeamConnectors.setText(infoMessage);
+ final SelectionAdapter openInBrowserListener = new SelectionAdapter() {
+ public void widgetSelected(SelectionEvent e) {
+ WorkbenchUtil.openUrl(e.text, IWorkbenchBrowserSupport.AS_EXTERNAL);
+ }
+ };
+ missingTeamConnectors.addSelectionListener(openInBrowserListener);
+ }
+
+ public Set<Type> getTypes() {
+ Set<Type> types = new HashSet<Type>();
+ if (patchReview.getSelection()) {
+ types.add(Type.ADD_PATCH);
+ }
+ if (changesetReview.getSelection()) {
+ types.add(Type.ADD_CHANGESET);
+ }
+ if (workspacePatchReview.getSelection()) {
+ types.add(Type.ADD_WORKSPACE_PATCH);
+ }
+ return types;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewWizard.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewWizard.java
new file mode 100644
index 0000000..9966598
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/ReviewWizard.java
@@ -0,0 +1,307 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.TaskRepositoryUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.team.ui.AtlassianTeamUiPlugin;
+import com.atlassian.connector.eclipse.team.ui.ICustomChangesetLogEntry;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.LocalStatus;
+import com.atlassian.connector.eclipse.team.ui.TeamUiUtils;
+import com.atlassian.connector.eclipse.ui.commons.DecoratedResource;
+import com.atlassian.connector.eclipse.ui.commons.ResourceEditorBean;
+import com.atlassian.theplugin.commons.crucible.api.UploadItem;
+import com.atlassian.theplugin.commons.crucible.api.model.BasicProject;
+import com.atlassian.theplugin.commons.crucible.api.model.PermId;
+import com.atlassian.theplugin.commons.crucible.api.model.Review;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.mylyn.internal.tasks.core.LocalTask;
+import org.eclipse.mylyn.internal.tasks.ui.util.TasksUiInternal;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TasksUi;
+import org.eclipse.mylyn.tasks.ui.TasksUiUtil;
+import org.eclipse.mylyn.tasks.ui.wizards.NewTaskWizard;
+import org.eclipse.ui.INewWizard;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+
+/**
+ * Wizard for creating a new review
+ *
+ * @author Thomas Ehrnhoefer
+ * @author Pawel Niewiadomski
+ * @author Jacek Jaroczynski
+ */
+@SuppressWarnings("restriction")
+public class ReviewWizard extends NewTaskWizard implements INewWizard {
+
+ public enum Type {
+ ADD_CHANGESET, ADD_PATCH, ADD_WORKSPACE_PATCH, ADD_SCM_RESOURCES, ADD_UPLOAD_ITEMS, ADD_RESOURCES, ADD_COMMENT_TO_FILE;
+ }
+
+ private CrucibleReviewDetailsPage detailsPage;
+
+ private Review crucibleReview;
+
+ private SelectScmChangesetsPage addChangeSetsPage;
+
+ private CrucibleAddPatchPage addPatchPage;
+
+// private WorkspacePatchSelectionPage addWorkspacePatchPage;
+
+ private DefineRepositoryMappingsPage defineMappingPage;
+
+ private ResourceSelectionPage resourceSelectionPage;
+
+ private final Set<Type> types;
+
+ private SortedSet<ICustomChangesetLogEntry> preselectedLogEntries;
+
+ private String previousPatch;
+
+ private String previousPatchRepository;
+
+ private final List<IResource> selectedWorkspaceResources = new ArrayList<IResource>();
+
+ private IResource[] previousWorkspaceSelection;
+
+ private List<UploadItem> uploadItems;
+
+ private List<ResourceEditorBean> versionedCommentsToAdd = new ArrayList<ResourceEditorBean>();
+
+ private SelectChangesetsFromCruciblePage addChangeSetsFromCruciblePage;
+
+ private ITeamUiResourceConnector selectedWorkspaceTeamConnector;
+
+ public ReviewWizard(TaskRepository taskRepository, Set<Type> types) {
+ super(taskRepository, null);
+ setWindowTitle("New Crucible Review");
+ setNeedsProgressMonitor(true);
+ this.types = types;
+ this.selectedWorkspaceResources.addAll(Arrays.asList((IResource[]) ResourcesPlugin.getWorkspace()
+ .getRoot()
+ .getProjects()));
+ }
+
+ public ReviewWizard(Review review, Set<Type> types) {
+ this(CrucibleUiUtil.getCrucibleTaskRepository(review), types);
+ this.crucibleReview = review;
+ }
+
+ public ReviewWizard(Review review, Type type) {
+ this(review, new HashSet<Type>(Arrays.asList(type)));
+ }
+
+ @Override
+ public void addPages() {
+ if (types.contains(Type.ADD_CHANGESET)) {
+ addChangeSetsFromCruciblePage = new SelectChangesetsFromCruciblePage(getTaskRepository(),
+ preselectedLogEntries);
+ addPage(addChangeSetsFromCruciblePage);
+ }
+
+ if (types.contains(Type.ADD_PATCH)) {
+ addPatchPage = new CrucibleAddPatchPage(getTaskRepository());
+ addPage(addPatchPage);
+ }
+
+ // pre-commit
+ if (types.contains(Type.ADD_WORKSPACE_PATCH)) {
+// addWorkspacePatchPage = new WorkspacePatchSelectionPage(getTaskRepository(), selectedWorkspaceResources);
+// addPage(addWorkspacePatchPage);
+ }
+
+ // post-commit for editor selection
+ if (types.contains(Type.ADD_SCM_RESOURCES)) {
+
+ if (selectedWorkspaceResources.size() > 0 && selectedWorkspaceResources.get(0) != null) {
+
+ // single SCM integration selection supported
+ final ITeamUiResourceConnector teamConnector = AtlassianTeamUiPlugin.getDefault()
+ .getTeamResourceManager()
+ .getTeamConnector(selectedWorkspaceResources.get(0));
+ if (teamConnector == null) {
+ MessageDialog.openInformation(getShell(), CrucibleUiPlugin.PRODUCT_NAME,
+ "Cannot find Atlassian SCM Integration for '" + selectedWorkspaceResources.get(0).getName()
+ + "'.");
+ } else {
+ boolean missingMapping = false;
+ Collection<String> scmPaths = new ArrayList<String>();
+ // TODO use job below if there are plenty of resource (currently it is used for single resource)
+ for (IResource resource : selectedWorkspaceResources) {
+ try {
+ LocalStatus status = teamConnector.getLocalRevision(resource);
+ if (status.getScmPath() != null && status.getScmPath().length() > 0) {
+ String scmPath = TeamUiUtils.getScmPath(resource, teamConnector);
+
+ if (TaskRepositoryUtil.getMatchingSourceRepository(
+ TaskRepositoryUtil.getScmRepositoryMappings(getTaskRepository()), scmPath) == null) {
+ // we need to see mapping page
+ missingMapping = true;
+ scmPaths.add(scmPath);
+ }
+
+ }
+ } catch (CoreException e) {
+ // resource is probably not under version control
+ // skip
+ }
+ }
+
+ if (missingMapping) {
+ defineMappingPage = new DefineRepositoryMappingsPage(scmPaths, getTaskRepository());
+ addPage(defineMappingPage);
+ }
+ }
+ }
+ }
+
+ // mixed review
+ if (types.contains(Type.ADD_RESOURCES)) {
+ resourceSelectionPage = new ResourceSelectionPage(getTaskRepository(), selectedWorkspaceTeamConnector,
+ selectedWorkspaceResources);
+ addPage(resourceSelectionPage);
+ }
+
+ // only add details page if review is not already existing
+ if (crucibleReview == null) {
+ detailsPage = new CrucibleReviewDetailsPage(getTaskRepository(), types.contains(Type.ADD_COMMENT_TO_FILE));
+ addPage(detailsPage);
+ }
+ }
+
+ @Override
+ public boolean canFinish() {
+ if (detailsPage != null) {
+ return detailsPage.isPageComplete();
+ }
+ return super.canFinish();
+ }
+
+ @Override
+ public boolean performFinish() {
+
+ setErrorMessage(null);
+
+ crucibleReview = detailsPage.getReview();
+ LocalTask task = TasksUiInternal.createNewLocalTask("Review: " + crucibleReview.getSummary());
+ crucibleReview.setPermId(new PermId(task.getTaskId()));
+
+ if (detailsPage != null) {
+ // save project selection
+ final BasicProject selectedProject = detailsPage.getSelectedProject();
+ CrucibleUiPlugin.getDefault().updateLastSelectedProject(getTaskRepository(),
+ selectedProject != null ? selectedProject.getKey() : null);
+
+ // save checkbox selections
+ CrucibleUiPlugin.getDefault().updateAllowAnyoneOption(getTaskRepository(),
+ detailsPage.isAllowAnyoneToJoin());
+ CrucibleUiPlugin.getDefault().updateStartReviewOption(getTaskRepository(),
+ detailsPage.isStartReviewImmediately());
+ }
+
+ if (addPatchPage != null) {
+ String patchToAdd = addPatchPage.hasPatch() ? addPatchPage.getPatch() : null;
+ String patchRepositoryToAdd = addPatchPage.hasPatch() ? addPatchPage.getPatchRepository() : null;
+
+ if (patchToAdd != null && patchRepositoryToAdd != null && !patchToAdd.equals(previousPatch)
+ && !patchRepositoryToAdd.equals(previousPatchRepository)) {
+ // create patch review
+ }
+ }
+
+// if (addWorkspacePatchPage != null) {
+// final IResource[] selection = addWorkspacePatchPage.getSelection();
+//
+// if (selection != null && selection.length > 0 && !Arrays.equals(selection, previousWorkspaceSelection)
+// && addWorkspacePatchPage.getSelectedTeamResourceConnector() != null) {
+// // create pre-commit review
+// }
+// }
+
+ if (addChangeSetsPage != null || addChangeSetsFromCruciblePage != null) {
+ final Map<String, Set<String>> changesetsToAdd = addChangeSetsPage != null ? addChangeSetsPage.getSelectedChangesets()
+ : addChangeSetsFromCruciblePage.getSelectedChangesets();
+ if (changesetsToAdd != null && changesetsToAdd.size() > 0) {
+ // create review from changeset
+ }
+ }
+
+ if (types.contains(Type.ADD_SCM_RESOURCES)) {
+ if (selectedWorkspaceResources != null) {
+ // create review from editor selection (post-commit)
+ }
+ }
+
+ if (types.contains(Type.ADD_UPLOAD_ITEMS)) {
+ if (uploadItems.size() > 0) {
+ // create review from editor selection (pre-commit)
+ }
+ }
+
+ if (resourceSelectionPage != null && types.contains(Type.ADD_RESOURCES)) {
+ final List<DecoratedResource> resources = resourceSelectionPage.getSelection();
+ if (resources != null && resources.size() > 0) {
+ // create review from workbench selection (post- and pre-commit)
+ }
+ }
+
+ TasksUiUtil.openTask(task);
+ TasksUi.getTaskActivityManager().activateTask(task);
+ CrucibleUiPlugin.getDefault()
+ .getActiveReviewManager()
+ .reviewAdded(task.getRepositoryUrl(), task.getTaskId(), crucibleReview);
+
+ return true;
+ }
+
+ private void setErrorMessage(String message) {
+ IWizardPage page = getContainer().getCurrentPage();
+ if (page instanceof WizardPage) {
+ ((WizardPage) page).setErrorMessage(message != null ? message.replace("\n", " ") : null);
+ }
+ }
+
+ public void setLogEntries(SortedSet<ICustomChangesetLogEntry> logEntries) {
+ this.preselectedLogEntries = logEntries;
+ }
+
+ public void setRoots(ITeamUiResourceConnector teamConnector, List<IResource> list) {
+ this.selectedWorkspaceResources.clear();
+ this.selectedWorkspaceResources.addAll(list);
+ this.selectedWorkspaceTeamConnector = teamConnector;
+ }
+
+ public void setUploadItems(List<UploadItem> uploadItems) {
+ this.uploadItems = uploadItems;
+ }
+
+ public void setFilesCommentData(List<ResourceEditorBean> list) {
+ this.versionedCommentsToAdd = list;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectChangesetsFromCruciblePage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectChangesetsFromCruciblePage.java
new file mode 100644
index 0000000..ead50be
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectChangesetsFromCruciblePage.java
@@ -0,0 +1,643 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.TaskRepositoryUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.team.ui.ICustomChangesetLogEntry;
+import com.atlassian.connector.eclipse.ui.viewers.ArrayTreeContentProvider;
+import com.atlassian.theplugin.commons.crucible.api.model.Repository;
+import com.atlassian.theplugin.commons.crucible.api.model.changes.Change;
+import com.atlassian.theplugin.commons.crucible.api.model.changes.Changes;
+import com.atlassian.theplugin.commons.crucible.api.model.changes.Revision;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.ITreeViewerListener;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeExpansionEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonImages;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.views.navigator.ResourceComparator;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Page for selecting changeset for the new review
+ *
+ * @author Thomas Ehrnhoefer
+ * @author Pawel Niewiadomski
+ */
+public class SelectChangesetsFromCruciblePage extends AbstractCrucibleWizardPage {
+
+ private static final int LIMIT = 25;
+
+ private static final String EMPTY_NODE = "No changesets available.";
+
+ private final class GetChangesetsRunnable implements IRunnableWithProgress {
+ private final int numberToRetrieve;
+
+ private final IStatus[] status;
+
+ private final Repository repository;
+
+ private GetChangesetsRunnable(int numberToRetrieve, IStatus[] status, Repository repository) {
+ this.numberToRetrieve = numberToRetrieve;
+ this.status = status;
+ this.repository = repository;
+ }
+
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ }
+ }
+
+ private static class ChangesetLabelProvider extends LabelProvider {
+ private static final int COMMENT_PREVIEW_LENGTH = 50;
+
+ @Override
+ public Image getImage(Object element) {
+ if (element == null) {
+ return null;
+ }
+ if (element instanceof Change) {
+ return CommonImages.getImage(CrucibleImages.CHANGESET);
+ } else if (element == EMPTY_NODE) {
+ return null;
+ } else if (element instanceof String || element instanceof Revision) {
+ return CommonImages.getImage(CrucibleImages.FILE);
+ }
+ return super.getImage(element);
+ }
+
+ @Override
+ public String getText(Object element) {
+ if (element instanceof Repository) {
+ return ((Repository) element).getName();
+ }
+ if (element instanceof Change) {
+ return changeLabel((Change) element);
+ }
+ if (element instanceof Revision) {
+ return ((Revision) element).getPath();
+ }
+ return super.getText(element);
+ }
+
+ public static String changeLabel(Change element) {
+ String shortComment = (element).getComment();
+ if (shortComment.length() > COMMENT_PREVIEW_LENGTH) {
+ shortComment = shortComment.substring(0, COMMENT_PREVIEW_LENGTH) + "...";
+ }
+ return (element).getCsid() + " [" + (element).getAuthor() + "] - " + shortComment.replace("\n", " ");
+ }
+ }
+
+ private class ChangesetContentProvider extends ArrayTreeContentProvider {
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof Repository) {
+ Changes changes = availableChanges.get(parentElement);
+ if (changes == null || changes.getChanges().size() == 0) {
+ return new String[] { EMPTY_NODE };
+ }
+ return changes.getChanges().toArray();
+ }
+ if (parentElement instanceof Change) {
+ return ((Change) parentElement).getRevisions().toArray();
+ }
+ return super.getChildren(parentElement);
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof Repository || element instanceof Change) {
+ return true;
+ }
+ return super.hasChildren(element);
+ }
+
+ }
+
+ private final Map<Repository, Changes> availableChanges = MiscUtil.buildHashMap();
+
+ private final Map<Repository, Set<Change>> selectedChanges = MiscUtil.buildHashMap();
+
+ private TreeViewer availableTreeViewer;
+
+ private TreeViewer selectedTreeViewer;
+
+ private Button addButton;
+
+ private Button removeButton;
+
+ private MenuItem removeChangesetMenuItem;
+
+ private MenuItem getNextRevisionsMenuItem;
+
+ private MenuItem addChangesetMenuItem;
+
+ private final TaskRepository taskRepository;
+
+ public SelectChangesetsFromCruciblePage(TaskRepository repository) {
+ this(repository, new TreeSet<ICustomChangesetLogEntry>());
+ }
+
+ public SelectChangesetsFromCruciblePage(TaskRepository repository, SortedSet<ICustomChangesetLogEntry> logEntries) {
+ super("crucibleChangesets"); //$NON-NLS-1$
+ setTitle("Select Changesets from Crucible");
+ setDescription("Select the changesets that should be included in the review. Changesets information is provided by Crucible.");
+
+ this.taskRepository = repository;
+
+ if (logEntries != null && logEntries.size() > 0) {
+ Map<String, String> mappings = TaskRepositoryUtil.getScmRepositoryMappings(repository);
+ for (ICustomChangesetLogEntry logEntry : logEntries) {
+ Entry<String, String> mapping = TaskRepositoryUtil.getMatchingSourceRepository(mappings,
+ logEntry.getRepository().getScmPath());
+ if (mappings != null) {
+ if (!selectedChanges.containsKey(mapping.getValue())) {
+ selectedChanges.put(new Repository(mapping.getValue(), null, true), new HashSet<Change>());
+ }
+ selectedChanges.get(mapping.getValue()).add(
+ new Change(logEntry.getAuthor(), logEntry.getDate(), logEntry.getRevision(), null,
+ logEntry.getComment(), null));
+ }
+ }
+ }
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(3).margins(5, 5).create());
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setText("Select changesets from your repositories:");
+ GridDataFactory.fillDefaults().span(2, 1).applyTo(label);
+
+ new Label(composite, SWT.NONE).setText("Changesets selected for the review:");
+
+ createChangesViewer(composite);
+
+ createButtonComp(composite);
+
+ createSelectedChangesViewer(composite);
+
+ Dialog.applyDialogFont(composite);
+ setControl(composite);
+ }
+
+ /*
+ * checks if page is complete updates the buttons
+ */
+ public void validatePage() {
+ setErrorMessage(null);
+
+ setPageComplete(true);
+ getContainer().updateButtons();
+ }
+
+ private void createChangesViewer(Composite parent) {
+ Tree tree = new Tree(parent, SWT.MULTI | SWT.BORDER);
+ availableTreeViewer = new TreeViewer(tree);
+
+ GridDataFactory.fillDefaults().grab(true, true).hint(300, 220).applyTo(tree);
+ availableTreeViewer.setLabelProvider(new ChangesetLabelProvider());
+ availableTreeViewer.setContentProvider(new ChangesetContentProvider());
+ availableTreeViewer.setComparator(new ResourceComparator(ResourceComparator.NAME));
+ availableTreeViewer.setComparator(new ViewerComparator() {
+ @Override
+ public int compare(Viewer viewer, Object e1, Object e2) {
+ if (e1 instanceof Change && e2 instanceof Change) {
+ return ((Change) e2).getDate().compareTo(((Change) e1).getDate());
+ }
+ return super.compare(viewer, e1, e2);
+ }
+ });
+
+ tree.setMenu(createChangesContextMenu());
+
+ tree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateButtonEnablement();
+ }
+ });
+ availableTreeViewer.addTreeListener(new ITreeViewerListener() {
+ public void treeCollapsed(TreeExpansionEvent event) {
+ // ignore
+ }
+
+ public void treeExpanded(TreeExpansionEvent event) {
+ // first time of expanding: retrieve first 10 changesets
+ final Object object = event.getElement();
+ if (object instanceof Repository) {
+ refreshRepository((Repository) object);
+ }
+
+ }
+ });
+ availableTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+ Object object = selection.getFirstElement();
+ if (availableTreeViewer.isExpandable(object)) {
+ if (!availableTreeViewer.getExpandedState(object) && object instanceof Repository) {
+ refreshRepository((Repository) object);
+ return;
+ }
+ availableTreeViewer.setExpandedState(object, !availableTreeViewer.getExpandedState(object));
+ }
+ }
+ });
+ }
+
+ private Menu createChangesContextMenu() {
+ final Menu contextMenuSource = new Menu(getShell(), SWT.POP_UP);
+
+ addChangesetMenuItem = new MenuItem(contextMenuSource, SWT.PUSH);
+ addChangesetMenuItem.setText("Add to Review");
+
+ new MenuItem(contextMenuSource, SWT.SEPARATOR);
+
+ addChangesetMenuItem.setEnabled(false);
+ getNextRevisionsMenuItem = new MenuItem(contextMenuSource, SWT.PUSH);
+ getNextRevisionsMenuItem.setText(String.format("Get %d More Revisions", LIMIT));
+ getNextRevisionsMenuItem.setEnabled(false);
+ getNextRevisionsMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ TreeSelection selection = getTreeSelection(availableTreeViewer);
+ if (selection != null && selection.getPaths() != null) {
+ for (TreePath path : selection.getPaths()) {
+ Repository repository = (Repository) path.getFirstSegment();
+ if (repository != null) {
+ Changes changes = availableChanges.get(repository);
+ int currentNumberOfEntries = changes == null ? 0 : changes.getChanges().size();
+ updateChangesets(repository, currentNumberOfEntries + LIMIT);
+ }
+ }
+ } else {
+ // update all
+ for (Repository repository : availableChanges.keySet()) {
+ Changes changes = availableChanges.get(repository);
+ int currentNumberOfEntries = changes == null ? 0 : changes.getChanges().size();
+ updateChangesets(repository, currentNumberOfEntries + LIMIT);
+ }
+ }
+ }
+ });
+
+ addChangesetMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ addChangesets();
+ updateButtonEnablement();
+ }
+ });
+ return contextMenuSource;
+ }
+
+ private void refreshRepository(final Repository object) {
+ Changes changes = availableChanges.get(object);
+ if (changes == null) {
+ updateChangesets(object, LIMIT);
+ }
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ // expand tree after filling
+ availableTreeViewer.expandToLevel(object, 1);
+ }
+ });
+ }
+
+ private void createButtonComp(Composite composite) {
+ Composite buttonComp = new Composite(composite, SWT.NONE);
+ buttonComp.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).create());
+ GridDataFactory.fillDefaults().grab(false, true).applyTo(buttonComp);
+
+ addButton = new Button(buttonComp, SWT.PUSH);
+ addButton.setText("Add -->");
+ addButton.setToolTipText("Add all selected changesets");
+ addButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ addChangesets();
+ updateButtonEnablement();
+ }
+ });
+ addButton.setEnabled(false);
+ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(addButton);
+
+ removeButton = new Button(buttonComp, SWT.PUSH);
+ removeButton.setText("<-- Remove");
+ removeButton.setToolTipText("Remove all selected changesets");
+ removeButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ removeChangesets();
+ updateButtonEnablement();
+ }
+
+ });
+ removeButton.setEnabled(false);
+ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(removeButton);
+ }
+
+ private void createSelectedChangesViewer(Composite composite) {
+ Tree tree = new Tree(composite, SWT.MULTI | SWT.BORDER);
+ selectedTreeViewer = new TreeViewer(tree);
+
+ GridDataFactory.fillDefaults().grab(true, true).hint(300, 220).applyTo(tree);
+ selectedTreeViewer.setLabelProvider(new ChangesetLabelProvider());
+ selectedTreeViewer.setContentProvider(new ITreeContentProvider() {
+ private Map<String, Set<String>> currentMap;
+
+ public Object[] getChildren(Object parentElement) {
+ return currentMap.get(parentElement).toArray();
+ }
+
+ public Object[] getElements(Object inputElement) {
+ return currentMap.keySet().toArray();
+ }
+
+ public boolean hasChildren(Object element) {
+ return currentMap.containsKey(element);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ this.currentMap = (Map<String, Set<String>>) newInput;
+ }
+
+ public Object getParent(Object element) {
+ return null;
+ }
+
+ public void dispose() {
+ }
+ });
+ selectedTreeViewer.setComparator(new ResourceComparator(ResourceComparator.NAME));
+
+ tree.setMenu(createSelectedChangesMenu());
+ tree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateButtonEnablement();
+ }
+ });
+ }
+
+ private Menu createSelectedChangesMenu() {
+ final Menu contextMenuSource = new Menu(getShell(), SWT.POP_UP);
+
+ removeChangesetMenuItem = new MenuItem(contextMenuSource, SWT.PUSH);
+ removeChangesetMenuItem.setText("Remove from Review");
+ removeChangesetMenuItem.setEnabled(false);
+ removeChangesetMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ removeChangesets();
+ updateButtonEnablement();
+ }
+ });
+ return contextMenuSource;
+ }
+
+ private void updateButtonEnablement() {
+ // right viewer
+ TreeSelection selection = validateTreeSelection(selectedTreeViewer);
+ removeButton.setEnabled(selection != null && !selection.isEmpty());
+ removeChangesetMenuItem.setEnabled(selection != null && !selection.isEmpty());
+
+ // left viewer
+ selection = validateTreeSelection(availableTreeViewer);
+ boolean changesetsOnly = hasChangesetsOnly(selection);
+
+ addButton.setEnabled(selection != null && !selection.isEmpty() && !hasAlreadyChosenChangesetSelected(selection)
+ && changesetsOnly);
+ addChangesetMenuItem.setEnabled(selection != null && !selection.isEmpty()
+ && !hasAlreadyChosenChangesetSelected(selection) && changesetsOnly);
+ getNextRevisionsMenuItem.setEnabled(selection != null && !selection.isEmpty());
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean hasChangesetsOnly(TreeSelection selection) {
+ if (selection != null) {
+ Iterator<Object> iterator = (selection).iterator();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof Repository) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean hasAlreadyChosenChangesetSelected(TreeSelection selection) {
+ for (Repository repository : selectedChanges.keySet()) {
+ Iterator<Object> iterator = (selection).iterator();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof Change) {
+ if (selectedChanges.get(repository).contains(element)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private void downloadRepositoriesFromCrucible(boolean force) {
+ if (force || CrucibleUiUtil.getCachedRepositories(getTaskRepository()).size() == 0) {
+ CrucibleUiUtil.updateTaskRepositoryCache(getTaskRepository(), getContainer(), this);
+ }
+
+ availableTreeViewer.setInput(CrucibleUiUtil.getCachedRepositories(getTaskRepository()));
+ }
+
+ private void updateChangesets(final Repository repository, final int numberToRetrieve) {
+ final IStatus[] status = { Status.OK_STATUS };
+
+ IRunnableWithProgress getChangesets = new GetChangesetsRunnable(numberToRetrieve, status, repository);
+
+ try {
+ setErrorMessage(null);
+ getContainer().run(false, true, getChangesets); // blocking operation
+ } catch (Exception e) {
+ status[0] = new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID, e.getMessage(), e);
+ }
+
+ if (!status[0].isOK()) { //only log errors, swallow warnings
+ setErrorMessage(String.format("Error while retrieving changesets (%s). See Error Log for details.",
+ status[0].getMessage()));
+ StatusHandler.log(status[0]);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible && (availableChanges.isEmpty() || !CrucibleUiUtil.hasCachedData(getTaskRepository()))) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ if (availableChanges.isEmpty()) {
+ downloadRepositoriesFromCrucible(false);
+ selectedTreeViewer.setInput(selectedChanges);
+ validatePage();
+ }
+ }
+ });
+ }
+ }
+
+ private TaskRepository getTaskRepository() {
+ return taskRepository;
+ }
+
+ private void addChangesets() {
+ TreeSelection selection = getTreeSelection(availableTreeViewer);
+ if (selection != null && selection.getPaths() != null) {
+ Object[] expanded = selectedTreeViewer.getExpandedElements();
+
+ for (TreePath path : selection.getPaths()) {
+ if (path.getSegmentCount() < 2) {
+ continue;
+ }
+
+ Repository repository = (Repository) path.getFirstSegment();
+ Change change = (Change) path.getSegment(1);
+
+ if (!selectedChanges.containsKey(repository)) {
+ selectedChanges.put(repository, new HashSet<Change>());
+ }
+ selectedChanges.get(repository).add(change);
+ }
+
+ selectedTreeViewer.setInput(selectedChanges);
+ selectedTreeViewer.setExpandedElements(expanded);
+ }
+ validatePage();
+ }
+
+ private void removeChangesets() {
+ TreeSelection selection = getTreeSelection(selectedTreeViewer);
+ if (selection != null && selection.getPaths() != null) {
+ Object[] expanded = selectedTreeViewer.getExpandedElements();
+
+ for (TreePath path : selection.getPaths()) {
+ Repository repository = (Repository) path.getFirstSegment();
+ Change change = path.getSegmentCount() > 1 ? (Change) path.getSegment(1) : null;
+
+ if (selectedChanges.containsKey(repository)) {
+ if (change != null) {
+ Set<Change> csids = selectedChanges.get(repository);
+ csids.remove(change);
+ if (csids.size() == 0) {
+ selectedChanges.remove(repository);
+ }
+ } else {
+ selectedChanges.remove(repository);
+ }
+ }
+ }
+
+ selectedTreeViewer.setInput(selectedChanges);
+ selectedTreeViewer.setExpandedElements(expanded);
+ }
+ validatePage();
+ }
+
+ @SuppressWarnings("unchecked")
+ private TreeSelection validateTreeSelection(TreeViewer treeViewer) {
+ TreeSelection selection = getTreeSelection(treeViewer);
+ if (selection != null) {
+ ArrayList<TreePath> validSelections = new ArrayList<TreePath>();
+ Iterator<Object> iterator = (selection).iterator();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof Change) {
+ validSelections.add((selection).getPathsFor(element)[0]);
+ } else if (element instanceof Repository) {
+ validSelections.add((selection).getPathsFor(element)[0]);
+ }
+ }
+ //set new selection
+ TreeSelection newSelection = new TreeSelection(
+ validSelections.toArray(new TreePath[validSelections.size()]), (selection).getElementComparer());
+ treeViewer.setSelection(newSelection);
+ } else {
+ treeViewer.setSelection(new TreeSelection());
+ }
+ return getTreeSelection(treeViewer);
+ }
+
+ private TreeSelection getTreeSelection(TreeViewer treeViewer) {
+ ISelection selection = treeViewer.getSelection();
+ if (selection instanceof TreeSelection) {
+ return (TreeSelection) selection;
+ }
+ return null;
+ }
+
+ public Map<String, Set<String>> getSelectedChangesets() {
+ Map<String, Set<String>> result = MiscUtil.buildHashMap();
+ for (Map.Entry<Repository, Set<Change>> changes : selectedChanges.entrySet()) {
+ Set<String> csids = MiscUtil.buildHashSet();
+ for (Change change : changes.getValue()) {
+ csids.add(change.getCsid());
+ }
+ result.put(changes.getKey().getName(), csids);
+ }
+ return result;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectScmChangesetsPage.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectScmChangesetsPage.java
new file mode 100644
index 0000000..b3f15cf
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/internal/crucible/ui/wizards/SelectScmChangesetsPage.java
@@ -0,0 +1,731 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.internal.crucible.ui.wizards;
+
+import com.atlassian.connector.eclipse.internal.crucible.core.TaskRepositoryUtil;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleImages;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiPlugin;
+import com.atlassian.connector.eclipse.internal.crucible.ui.CrucibleUiUtil;
+import com.atlassian.connector.eclipse.team.ui.ICustomChangesetLogEntry;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector;
+import com.atlassian.connector.eclipse.team.ui.ITeamUiResourceConnector2;
+import com.atlassian.connector.eclipse.team.ui.ScmRepository;
+import com.atlassian.connector.eclipse.team.ui.TeamUiUtils;
+import com.atlassian.theplugin.commons.util.MiscUtil;
+
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.MultiStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.ITreeViewerListener;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeExpansionEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.internal.provisional.commons.ui.CommonImages;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.views.navigator.ResourceComparator;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Page for selecting changeset for the new review
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public class SelectScmChangesetsPage extends AbstractCrucibleWizardPage {
+
+ private static final int LIMIT = 25;
+
+ private static final String EMPTY_NODE = "No changesets available.";
+
+ private class ChangesetLabelProvider extends LabelProvider {
+ @Override
+ public Image getImage(Object element) {
+ if (element == null) {
+ return null;
+ }
+ if (element instanceof ScmRepository) {
+ //return FishEyeImages.getImage(FishEyeImages.REPOSITORY);
+ } else if (element instanceof ICustomChangesetLogEntry) {
+ return CommonImages.getImage(CrucibleImages.CHANGESET);
+ } else if (element == EMPTY_NODE) {
+ return null;
+ } else if (element instanceof String) {
+ return CommonImages.getImage(CrucibleImages.FILE);
+ }
+ return null;
+ }
+
+ @Override
+ public String getText(Object element) {
+ if (element == null) {
+ return "";
+ }
+ if (element instanceof ScmRepository) {
+ return ((ScmRepository) element).getScmPath();
+ } else if (element instanceof ICustomChangesetLogEntry) {
+ ICustomChangesetLogEntry logEntry = (ICustomChangesetLogEntry) element;
+ return logEntry.getRevision() + " [" + logEntry.getAuthor() + "] - " + logEntry.getComment();
+ } else if (element == EMPTY_NODE) {
+ return EMPTY_NODE;
+ } else if (element instanceof String) {
+ return (String) element;
+ }
+ return "";
+ }
+ }
+
+ private class ChangesetContentProvider implements ITreeContentProvider {
+
+ private Map<ScmRepository, SortedSet<ICustomChangesetLogEntry>> logEntries;
+
+ public Object[] getChildren(Object parentElement) {
+ if (logEntries == null || parentElement == null) {
+ return new Object[0];
+ }
+ if (parentElement instanceof ScmRepository) {
+ //root, repository URLs
+ if (logEntries.get(parentElement) == null || logEntries.get(parentElement).size() == 0) {
+ //if no retrieved changeset, create fake node for lazy loading
+ return new String[] { EMPTY_NODE };
+ }
+ return logEntries.get(parentElement).toArray();
+ } else if (parentElement instanceof ICustomChangesetLogEntry) {
+ //changeset files
+ return ((ICustomChangesetLogEntry) parentElement).getChangedFiles();
+ }
+ return new Object[0];
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Object getParent(Object element) {
+ if (logEntries == null) {
+ return null;
+ }
+ if (element instanceof Map || element instanceof ScmRepository) {
+ //root, repository URLs
+ return null;
+ } else if (element instanceof ICustomChangesetLogEntry) {
+ //changeset elements
+ return ((ICustomChangesetLogEntry) element).getRepository();
+ }
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public boolean hasChildren(Object element) {
+ if (logEntries == null) {
+ return false;
+ }
+ if (element instanceof Map) {
+ //root, repository URLs
+ return logEntries.size() > 0;
+ } else if (element instanceof ScmRepository) {
+ //change sets for a repository
+// return logEntries.get(element).size() > 0;
+ return true;
+ } else if (element instanceof ICustomChangesetLogEntry) {
+ //changeset elements
+ return ((ICustomChangesetLogEntry) element).getChangedFiles().length > 0;
+ }
+ return false;
+ }
+
+ /**
+ * @return array of map keys (Repository URLs)
+ */
+ public Object[] getElements(Object inputElement) {
+ if (logEntries == null) {
+ return new Object[0];
+ }
+ //repositories
+ return logEntries.keySet().toArray();
+ }
+
+ public void dispose() {
+ // ignore
+
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ if (newInput instanceof Map) {
+ logEntries = (Map<ScmRepository, SortedSet<ICustomChangesetLogEntry>>) newInput;
+ }
+ }
+
+ }
+
+ private final Map<ScmRepository, SortedSet<ICustomChangesetLogEntry>> availableLogEntries;
+
+ private final Map<ScmRepository, SortedSet<ICustomChangesetLogEntry>> selectedLogEntries;
+
+ private TreeViewer availableTreeViewer;
+
+ private TreeViewer selectedTreeViewer;
+
+ private Button addButton;
+
+ private Button removeButton;
+
+ private MenuItem removeChangesetMenuItem;
+
+ private MenuItem getNextRevisionsMenuItem;
+
+ private MenuItem addChangesetMenuItem;
+
+ private final TaskRepository taskRepository;
+
+ private DefineRepositoryMappingButton mappingButton;
+
+ public SelectScmChangesetsPage(TaskRepository repository) {
+ this(repository, new TreeSet<ICustomChangesetLogEntry>());
+ }
+
+ public SelectScmChangesetsPage(TaskRepository repository, SortedSet<ICustomChangesetLogEntry> logEntries) {
+ super("crucibleChangesets"); //$NON-NLS-1$
+ setTitle("Select Changesets");
+ setDescription("Select the changesets that should be included in the review.");
+
+ this.taskRepository = repository;
+ this.availableLogEntries = new HashMap<ScmRepository, SortedSet<ICustomChangesetLogEntry>>();
+ this.selectedLogEntries = new HashMap<ScmRepository, SortedSet<ICustomChangesetLogEntry>>();
+
+ if (logEntries != null && logEntries.size() > 0) {
+ this.selectedLogEntries.put(logEntries.first().getRepository(), logEntries);
+ }
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(3).margins(5, 5).create());
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setText("Select changesets from your repositories:");
+ GridDataFactory.fillDefaults().span(2, 1).applyTo(label);
+
+ new Label(composite, SWT.NONE).setText("Changesets selected for the review:");
+
+ createLeftViewer(composite);
+
+ createButtonComp(composite);
+
+ createRightViewer(composite);
+
+ mappingButton = new DefineRepositoryMappingButton(this, composite, getTaskRepository());
+
+ Control button = mappingButton.getControl();
+ GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.BEGINNING).applyTo(button);
+
+ Dialog.applyDialogFont(composite);
+ setControl(composite);
+ }
+
+ /*
+ * checks if page is complete updates the buttons
+ */
+ public void validatePage() {
+ setErrorMessage(null);
+
+ // Check if all custom repositories are mapped to Crucible source repositories
+ boolean allFine = true;
+ for (Set<ICustomChangesetLogEntry> entries : selectedLogEntries.values()) {
+ for (ICustomChangesetLogEntry entry : entries) {
+ String[] files = entry.getChangedFiles();
+ if (files == null || files.length == 0) {
+ continue;
+ }
+ for (String file : files) {
+ String scmPath = entry.getRepository().getRootPath() + '/' + file;
+ Map.Entry<String, String> sourceRepository = TaskRepositoryUtil.getMatchingSourceRepository(
+ TaskRepositoryUtil.getScmRepositoryMappings(getTaskRepository()), scmPath);
+
+ if (sourceRepository == null) {
+ mappingButton.setMissingMapping(entry.getRepository().getScmPath());
+ setErrorMessage(NLS.bind("SCM repository path {0} is not mapped to Crucible repository.",
+ scmPath));
+ allFine = false;
+ break;
+ }
+ }
+ if (!allFine) {
+ break;
+ }
+ }
+ if (!allFine) {
+ break;
+ }
+ }
+ setPageComplete(allFine);
+ getContainer().updateButtons();
+ }
+
+ private void createLeftViewer(Composite parent) {
+ Tree tree = new Tree(parent, SWT.MULTI | SWT.BORDER);
+ availableTreeViewer = new TreeViewer(tree);
+
+ GridDataFactory.fillDefaults().grab(true, true).hint(300, 220).applyTo(tree);
+ availableTreeViewer.setLabelProvider(new ChangesetLabelProvider());
+ availableTreeViewer.setContentProvider(new ChangesetContentProvider());
+ availableTreeViewer.setComparator(new ResourceComparator(ResourceComparator.NAME));
+ availableTreeViewer.setComparator(new ViewerComparator() {
+ @Override
+ public int compare(Viewer viewer, Object e1, Object e2) {
+ if (e1 instanceof ICustomChangesetLogEntry && e2 instanceof ICustomChangesetLogEntry) {
+ return ((ICustomChangesetLogEntry) e2).getDate().compareTo(
+ ((ICustomChangesetLogEntry) e1).getDate());
+ }
+ return super.compare(viewer, e1, e2);
+ }
+ });
+ availableTreeViewer.setInput(ResourcesPlugin.getWorkspace().getRoot());
+ final Menu contextMenuSource = new Menu(getShell(), SWT.POP_UP);
+ tree.setMenu(contextMenuSource);
+ addChangesetMenuItem = new MenuItem(contextMenuSource, SWT.PUSH);
+ addChangesetMenuItem.setText("Add to Review");
+
+ new MenuItem(contextMenuSource, SWT.SEPARATOR);
+
+ addChangesetMenuItem.setEnabled(false);
+ getNextRevisionsMenuItem = new MenuItem(contextMenuSource, SWT.PUSH);
+ getNextRevisionsMenuItem.setText(String.format("Get %d More Revisions", LIMIT));
+ getNextRevisionsMenuItem.setEnabled(false);
+ getNextRevisionsMenuItem.addSelectionListener(new SelectionAdapter() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ TreeSelection selection = getTreeSelection(availableTreeViewer);
+ if (selection != null) {
+ Iterator<Object> iterator = (selection).iterator();
+ Set<ScmRepository> alreadyDone = new TreeSet<ScmRepository>();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ ScmRepository repository = null;
+ if (element instanceof ICustomChangesetLogEntry) {
+ repository = ((ICustomChangesetLogEntry) element).getRepository();
+ } else if (element instanceof ScmRepository) {
+ repository = (ScmRepository) element;
+ }
+ if (repository != null && !alreadyDone.contains(repository)) {
+ SortedSet<ICustomChangesetLogEntry> logEntries = availableLogEntries.get(repository);
+ int currentNumberOfEntries = logEntries == null ? 0 : logEntries.size();
+ updateChangesets(repository, currentNumberOfEntries + LIMIT);
+ alreadyDone.add(repository);
+ }
+ }
+ } else {
+ //update all
+ for (ScmRepository repository : availableLogEntries.keySet()) {
+ SortedSet<ICustomChangesetLogEntry> logEntries = availableLogEntries.get(repository);
+ int currentNumberOfEntries = logEntries == null ? 0 : logEntries.size();
+ updateChangesets(repository, currentNumberOfEntries + LIMIT);
+ }
+ }
+ }
+ });
+ addChangesetMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ addChangesets();
+ updateButtonEnablement();
+ }
+ });
+ tree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateButtonEnablement();
+ }
+ });
+ availableTreeViewer.addTreeListener(new ITreeViewerListener() {
+ public void treeCollapsed(TreeExpansionEvent event) {
+ // ignore
+ }
+
+ public void treeExpanded(TreeExpansionEvent event) {
+ // first time of expanding: retrieve first 10 changesets
+ final Object object = event.getElement();
+ if (object instanceof ScmRepository) {
+ refreshRepository((ScmRepository) object);
+ }
+
+ }
+ });
+ availableTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+ Object object = selection.getFirstElement();
+ if (availableTreeViewer.isExpandable(object)) {
+ if (!availableTreeViewer.getExpandedState(object) && object instanceof ScmRepository) {
+ refreshRepository((ScmRepository) object);
+ return;
+ }
+ availableTreeViewer.setExpandedState(object, !availableTreeViewer.getExpandedState(object));
+ }
+ }
+ });
+ }
+
+ private void refreshRepository(final ScmRepository object) {
+ SortedSet<ICustomChangesetLogEntry> logEntries = availableLogEntries.get(object);
+ if (logEntries == null) {
+ updateChangesets(object, LIMIT);
+ }
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ // expand tree after filling
+ availableTreeViewer.expandToLevel(object, 1);
+ }
+ });
+ }
+
+ private void createButtonComp(Composite composite) {
+ Composite buttonComp = new Composite(composite, SWT.NONE);
+ buttonComp.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).create());
+ GridDataFactory.fillDefaults().grab(false, true).applyTo(buttonComp);
+
+ addButton = new Button(buttonComp, SWT.PUSH);
+ addButton.setText("Add -->");
+ addButton.setToolTipText("Add all selected changesets");
+ addButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ addChangesets();
+ updateButtonEnablement();
+ }
+ });
+ addButton.setEnabled(false);
+ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(addButton);
+
+ removeButton = new Button(buttonComp, SWT.PUSH);
+ removeButton.setText("<-- Remove");
+ removeButton.setToolTipText("Remove all selected changesets");
+ removeButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ removeChangesets();
+ updateButtonEnablement();
+ }
+
+ });
+ removeButton.setEnabled(false);
+ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(removeButton);
+ }
+
+ private void createRightViewer(Composite composite) {
+ Tree tree = new Tree(composite, SWT.MULTI | SWT.BORDER);
+ selectedTreeViewer = new TreeViewer(tree);
+
+ GridDataFactory.fillDefaults().grab(true, true).hint(300, 220).applyTo(tree);
+ //GridDataFactory.fillDefaults().grab(true, true).hint(300, SWT.DEFAULT).applyTo(tree);
+ selectedTreeViewer.setLabelProvider(new ChangesetLabelProvider());
+ selectedTreeViewer.setContentProvider(new ChangesetContentProvider());
+ selectedTreeViewer.setComparator(new ResourceComparator(ResourceComparator.NAME));
+ final Menu contextMenuSource = new Menu(getShell(), SWT.POP_UP);
+ tree.setMenu(contextMenuSource);
+ removeChangesetMenuItem = new MenuItem(contextMenuSource, SWT.PUSH);
+ removeChangesetMenuItem.setText("Remove from Review");
+ removeChangesetMenuItem.setEnabled(false);
+ removeChangesetMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ removeChangesets();
+ updateButtonEnablement();
+ }
+ });
+
+ tree.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateButtonEnablement();
+ }
+ });
+ }
+
+ private void updateButtonEnablement() {
+ //right viewer
+ TreeSelection selection = validateTreeSelection(selectedTreeViewer, false);
+ removeButton.setEnabled(selection != null && !selection.isEmpty());
+ removeChangesetMenuItem.setEnabled(selection != null && !selection.isEmpty());
+ //left viewer
+ selection = validateTreeSelection(availableTreeViewer, true);
+ boolean changesetsOnly = hasChangesetsOnly(selection);
+ addButton.setEnabled(selection != null && !selection.isEmpty() && !hasAlreadyChosenChangesetSelected(selection)
+ && changesetsOnly);
+ addChangesetMenuItem.setEnabled(selection != null && !selection.isEmpty()
+ && !hasAlreadyChosenChangesetSelected(selection) && changesetsOnly);
+ getNextRevisionsMenuItem.setEnabled(selection != null && !selection.isEmpty());
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean hasChangesetsOnly(TreeSelection selection) {
+ if (selection != null) {
+ Iterator<Object> iterator = (selection).iterator();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof ScmRepository) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean hasAlreadyChosenChangesetSelected(TreeSelection selection) {
+ for (ScmRepository repository : selectedLogEntries.keySet()) {
+ Iterator<Object> iterator = (selection).iterator();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof ICustomChangesetLogEntry) {
+ if (selectedLogEntries.get(repository).contains(element)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private void updateRepositories() {
+ final MultiStatus status = new MultiStatus(CrucibleUiPlugin.PLUGIN_ID, IStatus.WARNING,
+ "Error while retrieving repositories", null);
+
+ IRunnableWithProgress getRepositories = new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ Collection<ScmRepository> repositories = TeamUiUtils.getRepositories(monitor);
+ if (repositories != null) {
+ for (ScmRepository repository : repositories) {
+ availableLogEntries.put(repository, null);
+ }
+ }
+ }
+ };
+
+ try {
+ setErrorMessage(null);
+ getContainer().run(true, true, getRepositories); // blocking operation
+ } catch (Exception e) {
+ status.add(new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID, "Failed to retrieve repositories", e));
+ }
+
+ if (availableLogEntries != null) {
+ availableTreeViewer.setInput(availableLogEntries);
+ }
+
+ if (status.getChildren().length > 0 && status.getSeverity() == IStatus.ERROR) { //only log errors, swallow warnings
+ setErrorMessage("Error while retrieving repositories. See Error Log for details.");
+ StatusHandler.log(status);
+ }
+ }
+
+ private void updateChangesets(final ScmRepository repository, final int numberToRetrieve) {
+ final IStatus[] status = { Status.OK_STATUS };
+
+ IRunnableWithProgress getChangesets = new IRunnableWithProgress() {
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ try {
+ ITeamUiResourceConnector tc = repository.getTeamResourceConnector();
+ if (tc instanceof ITeamUiResourceConnector2) {
+ SortedSet<ICustomChangesetLogEntry> retrieved = ((ITeamUiResourceConnector2) tc).getLatestChangesets(
+ repository.getScmPath(), numberToRetrieve, monitor);
+
+ if (availableLogEntries.containsKey(repository) && availableLogEntries.get(repository) != null) {
+ availableLogEntries.get(repository).addAll(retrieved);
+ } else {
+ availableLogEntries.put(repository, retrieved);
+ }
+ } else {
+ status[0] = new Status(IStatus.ERROR, CrucibleUiPlugin.PLUGIN_ID,
+ "This repository is managed by SCM integration that's compatible only with Crucible 2.x");
+ }
+ } catch (CoreException e) {
+ status[0] = e.getStatus();
+ }
+ }
+ };
+
+ try {
+ setErrorMessage(null);
+ getContainer().run(false, true, getChangesets); // blocking operation
+ } catch (Exception e) {
+ status[0] = new Status(IStatus.WARNING, CrucibleUiPlugin.PLUGIN_ID, "Failed to retrieve changesets", e);
+ }
+
+ if (availableLogEntries != null && availableLogEntries.get(repository) != null) {
+ availableTreeViewer.setInput(availableLogEntries);
+ }
+
+ if (status[0].getSeverity() == IStatus.ERROR) { //only log errors, swallow warnings
+ setErrorMessage(String.format("Error while retrieving changesets (%s). See Error Log for details.",
+ status[0].getMessage()));
+ StatusHandler.log(status[0]);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible && (availableLogEntries.isEmpty() || !CrucibleUiUtil.hasCachedData(getTaskRepository()))) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ if (availableLogEntries.isEmpty()) {
+ updateRepositories();
+ selectedTreeViewer.setInput(selectedLogEntries);
+ validatePage();
+ }
+ }
+ });
+ }
+ }
+
+ private TaskRepository getTaskRepository() {
+ return taskRepository;
+ }
+
+ private void addChangesets() {
+ TreeSelection selection = getTreeSelection(availableTreeViewer);
+ addOrRemoveChangesets(selection, true);
+ }
+
+ private void removeChangesets() {
+ TreeSelection selection = getTreeSelection(selectedTreeViewer);
+ addOrRemoveChangesets(selection, false);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addOrRemoveChangesets(TreeSelection selection, boolean add) {
+ if (selection != null) {
+ Iterator<Object> iterator = (selection).iterator();
+ Set<ScmRepository> expandedRepositories = new HashSet<ScmRepository>();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof ICustomChangesetLogEntry) {
+ ScmRepository repository = ((ICustomChangesetLogEntry) element).getRepository();
+ SortedSet<ICustomChangesetLogEntry> changesets = selectedLogEntries.get(repository);
+ if (changesets == null) {
+ changesets = new TreeSet<ICustomChangesetLogEntry>();
+ }
+ if (add) {
+ changesets.add((ICustomChangesetLogEntry) element);
+ } else {
+ changesets.remove(element);
+ }
+ if (changesets.size() > 0) {
+ selectedLogEntries.put(repository, changesets);
+ } else {
+ selectedLogEntries.remove(repository);
+ }
+ expandedRepositories.add(repository);
+ }
+ }
+ selectedTreeViewer.setInput(selectedLogEntries);
+ selectedTreeViewer.setExpandedElements(expandedRepositories.toArray());
+ }
+ validatePage();
+ }
+
+ @SuppressWarnings("unchecked")
+ private TreeSelection validateTreeSelection(TreeViewer treeViewer, boolean allowChangesetsSelection) {
+ TreeSelection selection = getTreeSelection(treeViewer);
+ if (selection != null) {
+ ArrayList<TreePath> validSelections = new ArrayList<TreePath>();
+ Iterator<Object> iterator = (selection).iterator();
+ while (iterator.hasNext()) {
+ Object element = iterator.next();
+ if (element instanceof ICustomChangesetLogEntry) {
+ validSelections.add((selection).getPathsFor(element)[0]);
+ } else if (allowChangesetsSelection && element instanceof ScmRepository) {
+ validSelections.add((selection).getPathsFor(element)[0]);
+ }
+ }
+ //set new selection
+ TreeSelection newSelection = new TreeSelection(
+ validSelections.toArray(new TreePath[validSelections.size()]), (selection).getElementComparer());
+ treeViewer.setSelection(newSelection);
+ } else {
+ treeViewer.setSelection(new TreeSelection());
+ }
+ return getTreeSelection(treeViewer);
+ }
+
+ private TreeSelection getTreeSelection(TreeViewer treeViewer) {
+ ISelection selection = treeViewer.getSelection();
+ if (selection instanceof TreeSelection) {
+ return (TreeSelection) selection;
+ }
+ return null;
+ }
+
+ public Map<String, Set<String>> getSelectedChangesets() {
+ Map<String, Set<String>> result = MiscUtil.buildHashMap();
+
+ for (SortedSet<ICustomChangesetLogEntry> entries : selectedLogEntries.values()) {
+ for (ICustomChangesetLogEntry entry : entries) {
+ String[] files = entry.getChangedFiles();
+ if (files == null || files.length == 0) {
+ continue;
+ }
+ for (String file : files) {
+ Map.Entry<String, String> sourceRepository = TaskRepositoryUtil.getMatchingSourceRepository(
+ TaskRepositoryUtil.getScmRepositoryMappings(getTaskRepository()), entry.getRepository()
+ .getRootPath() + '/' + file);
+ if (sourceRepository != null) {
+ if (!result.containsKey(sourceRepository.getValue())) {
+ result.put(sourceRepository.getValue(), new HashSet<String>());
+ }
+ result.get(sourceRepository.getValue()).add(entry.getRevision());
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/actions/AbstractResourceAction.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/actions/AbstractResourceAction.java
new file mode 100644
index 0000000..e0561b7
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/actions/AbstractResourceAction.java
@@ -0,0 +1,153 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.actions;
+
+import com.atlassian.connector.eclipse.ui.commons.AtlassianUiUtil;
+import com.atlassian.connector.eclipse.ui.commons.IEditorResource;
+import com.atlassian.connector.eclipse.ui.commons.ResourceEditorBean;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IActionDelegate;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class AbstractResourceAction extends BaseSelectionListenerAction implements IActionDelegate {
+
+ private IWorkbenchWindow workbenchWindow;
+
+ private List<ResourceEditorBean> selectionData;
+
+ protected AbstractResourceAction(String text) {
+ super(text);
+ }
+
+ protected List<ResourceEditorBean> getSelectionData() {
+ return selectionData;
+ }
+
+ public void dispose() {
+ }
+
+ public void init(IWorkbenchWindow window) {
+ this.workbenchWindow = window;
+ }
+
+ public void run(IAction action) {
+ if (selectionData != null && selectionData.get(0) != null) {
+ processResources(selectionData, WorkbenchUtil.getShell());
+ }
+ }
+
+ private List<ResourceEditorBean> getData(ISelection selection) {
+ List<ResourceEditorBean> ret = new ArrayList<ResourceEditorBean>();
+ if (selection instanceof IStructuredSelection) {
+ final IStructuredSelection structuredSelection = (IStructuredSelection) selection;
+
+ Object[] selectedObjects = structuredSelection.toArray();
+
+ for (Object selectedObject : selectedObjects) {
+
+ if (selectedObject instanceof IEditorResource) {
+ IEditorResource a = (IEditorResource) selectedObject;
+ ret.add(new ResourceEditorBean(a.getResource(), a.getLineRange()));
+
+ } else if (structuredSelection.getFirstElement() instanceof IAdaptable) {
+ IResource resource = null;
+ LineRange lineRange = null;
+ resource = (IResource) ((IAdaptable) structuredSelection.getFirstElement()).getAdapter(IResource.class);
+ lineRange = getJavaEditorSelection(structuredSelection);
+ ret.add(new ResourceEditorBean(resource, lineRange));
+ }
+ }
+ } else {
+ IEditorPart activeEditor = getActiveEditor();
+ if (activeEditor != null) {
+ IEditorInput editorInput = getEditorInputFromSelection(selection);
+ if (editorInput != null) {
+ IResource resource = null;
+ LineRange lineRange = null;
+ resource = (IResource) editorInput.getAdapter(IResource.class);
+ // such call:
+ // lineRange = new LineRange(textSelection.getStartLine(), textSelection.getEndLine()
+ // - textSelection.getStartLine());
+ // does not work (i.e. it returns previously selected text region rather than selected now ?!?
+ lineRange = AtlassianUiUtil.getSelectedLineNumberRangeFromEditorInput(activeEditor,
+ activeEditor.getEditorInput());
+ ret.add(new ResourceEditorBean(resource, lineRange));
+ }
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ protected boolean updateSelection(IStructuredSelection selection) {
+ selectionData = getData(selection);
+ return super.updateSelection(selection);
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ if (selection instanceof IStructuredSelection) {
+ selectionChanged((IStructuredSelection) selection);
+ } else {
+ selectionChanged(StructuredSelection.EMPTY);
+ }
+ action.setEnabled(isEnabled());
+ }
+
+ private IEditorPart getActiveEditor() {
+ IWorkbenchWindow window = workbenchWindow;
+ if (window == null) {
+ window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ }
+ if (window != null && window.getActivePage() != null) {
+ return window.getActivePage().getActiveEditor();
+ }
+ return null;
+ }
+
+ private IEditorInput getEditorInputFromSelection(ISelection selection) {
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = ((IStructuredSelection) selection);
+ if (structuredSelection.getFirstElement() instanceof IEditorInput) {
+ return (IEditorInput) structuredSelection.getFirstElement();
+ }
+ }
+ return null;
+ }
+
+ private LineRange getJavaEditorSelection(ISelection selection) {
+ IEditorPart editorPart = getActiveEditor();
+ IEditorInput editorInput = getEditorInputFromSelection(selection);
+ if (editorInput != null && editorPart != null) {
+ return AtlassianUiUtil.getSelectedLineNumberRangeFromEditorInput(editorPart, editorInput);
+ }
+ return null;
+ }
+
+ protected abstract void processResources(List<ResourceEditorBean> selection, final Shell shell);
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/AtlassianUiUtil.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/AtlassianUiUtil.java
new file mode 100644
index 0000000..a631635
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/AtlassianUiUtil.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.commons;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.text.TextSelection;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.mylyn.internal.provisional.commons.ui.WorkbenchUtil;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IViewReference;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPreferenceConstants;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.texteditor.ITextEditor;
+
+/**
+ * Provides utility methods for the Atlassian Connector for Eclipse
+ *
+ * @author Thomas Ehrnhoefer
+ */
+public final class AtlassianUiUtil {
+
+ public static final String CONFLUENCE_WIKI_TASK_EDITOR_EXTENSION = "org.eclipse.mylyn.wikitext.tasks.ui.editor.confluenceTaskEditorExtension";
+
+ private AtlassianUiUtil() {
+ }
+
+ public static boolean isAnimationsEnabled() {
+ IPreferenceStore store = PlatformUI.getPreferenceStore();
+ return store.getBoolean(IWorkbenchPreferenceConstants.ENABLE_ANIMATIONS);
+ }
+
+ /**
+ * Must be invoked in UI thread
+ *
+ * @param viewId
+ * @return <code>true</code> when the view has been successfully made visible or it already was, <code>false</code>
+ * if operation failed
+ */
+ public static boolean ensureViewIsVisible(String viewId) {
+ final IWorkbenchWindow activeWorkbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (activeWorkbenchWindow == null) {
+ return false;
+ }
+ IWorkbenchPage activePage = activeWorkbenchWindow.getActivePage();
+ if (activePage == null) {
+ return false;
+ }
+ for (IViewReference view : activePage.getViewReferences()) {
+ if (view.getId().equals(viewId)) {
+ return true;
+ }
+ }
+ try {
+ activePage.showView(viewId, null, IWorkbenchPage.VIEW_ACTIVATE);
+ return true;
+ } catch (PartInitException e) {
+// StatusHandler.log(new Status(IStatus.ERROR, AtlassianUiPlugin.PLUGIN_ID, "Could not initialize " + viewId
+// + " view."));
+ return false;
+ }
+ }
+
+ public static boolean showViewInActiveWorkbenchPage(String viewId) {
+ final IWorkbenchWindow activeWorkbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (activeWorkbenchWindow == null) {
+ return false;
+ }
+ IWorkbenchPage activePage = activeWorkbenchWindow.getActivePage();
+ if (activePage == null) {
+ return false;
+ }
+ try {
+ activePage.showView(viewId);
+ return true;
+ } catch (PartInitException e) {
+// StatusHandler.log(new Status(IStatus.ERROR, AtlassianUiPlugin.PLUGIN_ID, "Could not initialize " + viewId
+// + " view."));
+ return false;
+ }
+ }
+
+ public static LineRange getSelectedLineNumberRangeFromEditorInput(IEditorPart editor, IEditorInput editorInput) {
+
+ if (editor instanceof ITextEditor && editor.getEditorInput() == editorInput) {
+ ISelection selection = ((ITextEditor) editor).getSelectionProvider().getSelection();
+ return getLineRange(selection);
+ } else if (editor.getAdapter(ITextEditor.class) != null) {
+ ISelection selection = ((ITextEditor) editor.getAdapter(ITextEditor.class)).getSelectionProvider()
+ .getSelection();
+ return getLineRange(selection);
+ }
+ return null;
+ }
+
+ private static LineRange getLineRange(ISelection selection) {
+ if (selection instanceof TextSelection) {
+ TextSelection textSelection = ((TextSelection) selection);
+ return new LineRange(textSelection.getStartLine() + 1, textSelection.getEndLine()
+ - textSelection.getStartLine() + 1);
+ }
+ return null;
+ }
+
+ public static Display getDisplay(Shell shell) {
+ if (shell != null) {
+ Display d = shell.getDisplay();
+
+ if (d != null) {
+ return d;
+ }
+ }
+
+ return getDisplay();
+ }
+
+ public static Display getDisplay() {
+
+ Display d = Display.getCurrent();
+
+ if (d == null) {
+ d = Display.getDefault();
+ }
+
+ if (d == null) {
+ d = WorkbenchUtil.getShell().getDisplay();
+ }
+ return d;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/DecoratedResource.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/DecoratedResource.java
new file mode 100644
index 0000000..58b80aa
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/DecoratedResource.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.commons;
+
+import com.atlassian.connector.eclipse.ui.viewers.ICustomToolTipInfo;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.model.WorkbenchLabelProvider;
+
+public class DecoratedResource implements ICustomToolTipInfo {
+
+ private final String decorationText;
+
+ private final boolean upToDate;
+
+ private final IResource resource;
+
+ private final String tooltipText;
+
+ private final WorkbenchLabelProvider workbenchLabelProvider = new WorkbenchLabelProvider();
+
+ public DecoratedResource(IResource resource, boolean upToDate, String decorationText, String tooltipText) {
+ this.resource = resource;
+ this.upToDate = upToDate;
+ this.decorationText = decorationText;
+ this.tooltipText = tooltipText;
+ }
+
+ public DecoratedResource(IResource parent) {
+ // treat the resource as up-to-date (may need to be changed later)
+ this(parent, true, "", "");
+ }
+
+ public IResource getResource() {
+ return resource;
+ }
+
+ public String getDecorationText() {
+ return decorationText;
+ }
+
+ public boolean isUpToDate() {
+ return upToDate;
+ }
+
+ public String getTooltipText() {
+ return tooltipText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((resource == null) ? 0 : resource.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DecoratedResource other = (DecoratedResource) obj;
+ if (resource == null) {
+ if (other.resource != null) {
+ return false;
+ }
+ } else if (!resource.equals(other.resource)) {
+ return false;
+ }
+ return true;
+ }
+
+ public Image getImage() {
+ return workbenchLabelProvider.getImage(resource);
+ }
+
+ public void createToolTipArea(CustomToolTip tooltip, Composite composite) {
+ tooltip.addIconAndLabel(composite, getImage(), getResource().getName(), true);
+
+ String detailsText = getTooltipText();
+ if (detailsText != null) {
+ tooltip.addIconAndLabel(composite, null, detailsText);
+ }
+ }
+
+ public boolean isContainer() {
+ return resource instanceof IContainer;
+ }
+
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/EditorResourceAdapterFactory.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/EditorResourceAdapterFactory.java
new file mode 100644
index 0000000..8529029
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/EditorResourceAdapterFactory.java
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.commons;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IAdapterFactory;
+import org.eclipse.jface.text.source.LineRange;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+
+public class EditorResourceAdapterFactory implements IAdapterFactory {
+
+ private final class BasicEditorResource implements IEditorResource {
+ private final IResource resource;
+
+ private final LineRange lineRange;
+
+ public BasicEditorResource(IResource resource, LineRange lineRange) {
+ this.resource = resource;
+ this.lineRange = lineRange;
+ }
+
+ private BasicEditorResource(IResource resource) {
+ this(resource, null);
+ }
+
+ public LineRange getLineRange() {
+ return lineRange;
+ }
+
+ public IResource getResource() {
+ return resource;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Object getAdapter(Class adapter) {
+ if (!IResource.class.equals(adapter)) {
+ return null;
+ }
+
+ return resource;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static final Class[] ADAPTERS = { IEditorResource.class };
+
+ @SuppressWarnings("unchecked")
+ public Object getAdapter(Object adaptableObject, Class adapterType) {
+ if (!IEditorResource.class.equals(adapterType)) {
+ return null;
+ }
+
+ if (adaptableObject instanceof IResource) {
+ return new BasicEditorResource((IResource) adaptableObject);
+ }
+
+ if (adaptableObject instanceof IEditorInput) {
+ final IEditorInput editorInput = (IEditorInput) adaptableObject;
+ final IResource resource = (IResource) editorInput.getAdapter(IResource.class);
+ if (resource == null) {
+ return null;
+ }
+ IEditorPart editorPart = getActiveEditor();
+
+ // such call:
+ // lineRange = new LineRange(textSelection.getStartLine(), textSelection.getEndLine()
+ // - textSelection.getStartLine());
+ // does not work (i.e. it returns previously selected text region rather than selected now ?!?
+ final LineRange lineRange = AtlassianUiUtil.getSelectedLineNumberRangeFromEditorInput(editorPart,
+ editorInput);
+ return new BasicEditorResource(resource, lineRange);
+ }
+
+ if (adaptableObject instanceof IAdaptable) {
+ IAdaptable adaptable = (IAdaptable) adaptableObject;
+ final IResource resource = (IResource) adaptable.getAdapter(IResource.class);
+ if (resource != null) {
+ return new BasicEditorResource(resource);
+ }
+ }
+
+ return null;
+ }
+
+ private IEditorPart getActiveEditor() {
+ IWorkbenchWindow window = null;
+ if (window == null) {
+ window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ }
+ if (window != null && window.getActivePage() != null) {
+ return window.getActivePage().getActiveEditor();
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Class[] getAdapterList() {
+ return ADAPTERS;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/IEditorResource.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/IEditorResource.java
new file mode 100644
index 0000000..106dfb5
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/IEditorResource.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.commons;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.text.source.LineRange;
+
+public interface IEditorResource extends IAdaptable {
+ LineRange getLineRange();
+
+ IResource getResource();
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceEditorBean.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceEditorBean.java
new file mode 100644
index 0000000..6dad079
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceEditorBean.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.commons;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.jface.text.source.LineRange;
+
+public final class ResourceEditorBean {
+ private final IResource resource;
+
+ private final LineRange lineRange;
+
+ public ResourceEditorBean(IResource resource, LineRange lineRange) {
+ this.resource = resource;
+ this.lineRange = lineRange;
+ }
+
+ public IResource getResource() {
+ return resource;
+ }
+
+ public LineRange getLineRange() {
+ return lineRange;
+ }
+} \ No newline at end of file
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceSelectionTree.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceSelectionTree.java
new file mode 100644
index 0000000..a34dfb6
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/commons/ResourceSelectionTree.java
@@ -0,0 +1,642 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+package com.atlassian.connector.eclipse.ui.commons;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
+import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.custom.ViewForm;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.model.WorkbenchContentProvider;
+import org.eclipse.ui.model.WorkbenchLabelProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @author Jacek Jaroczynski
+ */
+public class ResourceSelectionTree extends Composite {
+ private static final int TREE_WIDTH = 500;
+
+ private Tree tree;
+
+ private TreeViewMode mode;
+
+ private List<DecoratedResource> compressedFolders = null;
+
+ private DecoratedResource[] folders;
+
+ private DecoratedResource[] rootFolders;
+
+ private CheckboxTreeViewer treeViewer;
+
+ private final String label;
+
+ private Action treeAction;
+
+ private Action flatAction;
+
+ private Action compressedAction;
+
+ private final ResourceComparator comparator = new ResourceComparator();
+
+ private final IToolbarControlCreator toolbarControlCreator;
+
+ private final ResourceSelectionContentProvider resourceSelectionContentProvider = new ResourceSelectionContentProvider();
+
+ public static enum TreeViewMode {
+ MODE_COMPRESSED_FOLDERS, MODE_FLAT, MODE_TREE;
+ }
+
+ private final ITreeViewModeSettingProvider settingsProvider;
+
+ private Collection<DecoratedResource> resourcesToShow;
+
+ /**
+ *
+ * @param parent
+ * parent composite for this tree
+ * @param label
+ * label in the toolbar
+ * @param resourcesToShow
+ * list of resources to show
+ * @param toolbarControlCreator
+ * toolbar actions creator if want to add additional actions (can be null)
+ * @param settingsProvider
+ * settings provider to store/restore resources tree mode (can be null)
+ */
+ public ResourceSelectionTree(Composite parent, String label, List<DecoratedResource> resourcesToShow,
+ IToolbarControlCreator toolbarControlCreator, ITreeViewModeSettingProvider settingsProvider) {
+ super(parent, SWT.NONE);
+ this.label = label;
+ this.resourcesToShow = resourcesToShow;
+ this.settingsProvider = settingsProvider;
+ this.toolbarControlCreator = toolbarControlCreator;
+
+ if (settingsProvider == null) {
+ settingsProvider = new ITreeViewModeSettingProvider() {
+
+ public void setTreeViewMode(TreeViewMode mode) {
+ }
+
+ public TreeViewMode getTreeViewMode() {
+ return TreeViewMode.MODE_COMPRESSED_FOLDERS;
+ }
+ };
+ }
+
+ mode = settingsProvider.getTreeViewMode();
+
+ createControls();
+ }
+
+ public CheckboxTreeViewer getTreeViewer() {
+ return treeViewer;
+ }
+
+ public DecoratedResource[] getSelectedResources() {
+ ArrayList<DecoratedResource> selected = new ArrayList<DecoratedResource>();
+ Object[] checkedResources = treeViewer.getCheckedElements();
+ for (Object checkedResource : checkedResources) {
+ if (resourcesToShow.contains(checkedResource)) {
+ selected.add((DecoratedResource) checkedResource);
+ }
+ }
+ DecoratedResource[] selectedResources = new DecoratedResource[selected.size()];
+ selected.toArray(selectedResources);
+ return selectedResources;
+ }
+
+ private void createControls() {
+ GridLayout layout = new GridLayout(2, false);
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ layout.horizontalSpacing = 0;
+ layout.verticalSpacing = 0;
+ setLayout(layout);
+ GridData layoutData = new GridData(GridData.FILL_BOTH);
+ layoutData.widthHint = 500;
+ setLayoutData(layoutData);
+
+ ViewForm viewerPane = new ViewForm(this, SWT.BORDER | SWT.FLAT);
+ layoutData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
+ layoutData.widthHint = 500;
+ viewerPane.setLayoutData(layoutData);
+
+ CLabel toolbarLabel = new CLabel(viewerPane, SWT.NONE) {
+ @Override
+ public Point computeSize(int wHint, int hHint, boolean changed) {
+ return super.computeSize(wHint, Math.max(24, hHint), changed);
+ }
+ };
+
+ if (label != null) {
+ toolbarLabel.setText(label);
+ }
+ viewerPane.setTopLeft(toolbarLabel);
+
+ int buttonGroupColumns = 1;
+ if (toolbarControlCreator != null) {
+ buttonGroupColumns = buttonGroupColumns + toolbarControlCreator.getControlCount();
+ }
+
+ ToolBar toolbar = new ToolBar(viewerPane, SWT.FLAT);
+ viewerPane.setTopCenter(toolbar);
+
+ ToolBarManager toolbarManager = new ToolBarManager(toolbar);
+
+ if (toolbarControlCreator != null) {
+ toolbarControlCreator.createToolbarControls(toolbarManager);
+ toolbarManager.add(new Separator());
+ }
+
+ flatAction = new Action("Flat Mode", IAction.AS_RADIO_BUTTON) {
+ @Override
+ public void run() {
+ mode = TreeViewMode.MODE_FLAT;
+ settingsProvider.setTreeViewMode(mode);
+ treeAction.setChecked(false);
+ compressedAction.setChecked(false);
+ refresh();
+ }
+ };
+// flatAction.setImageDescriptor(AtlassianImages.IMG_FLAT_MODE);
+ toolbarManager.add(flatAction);
+
+ treeAction = new Action("Tree Mode", IAction.AS_RADIO_BUTTON) {
+ @Override
+ public void run() {
+ mode = TreeViewMode.MODE_TREE;
+ settingsProvider.setTreeViewMode(mode);
+ flatAction.setChecked(false);
+ compressedAction.setChecked(false);
+ refresh();
+ }
+ };
+// treeAction.setImageDescriptor(AtlassianImages.IMG_TREE_MODE);
+ toolbarManager.add(treeAction);
+
+ compressedAction = new Action("Compressed Folders Mode", IAction.AS_RADIO_BUTTON) {
+ @Override
+ public void run() {
+ mode = TreeViewMode.MODE_COMPRESSED_FOLDERS;
+ settingsProvider.setTreeViewMode(mode);
+ treeAction.setChecked(false);
+ flatAction.setChecked(false);
+ refresh();
+ }
+ };
+// compressedAction.setImageDescriptor(AtlassianImages.IMG_COMPRESSED_FOLDER_MODE);
+ toolbarManager.add(compressedAction);
+
+ toolbarManager.update(true);
+
+ switch (mode) {
+ case MODE_COMPRESSED_FOLDERS:
+ compressedAction.setChecked(true);
+ break;
+ case MODE_FLAT:
+ flatAction.setChecked(true);
+ break;
+ case MODE_TREE:
+ treeAction.setChecked(true);
+ break;
+ default:
+ compressedAction.setChecked(true);
+ mode = TreeViewMode.MODE_COMPRESSED_FOLDERS;
+ settingsProvider.setTreeViewMode(mode);
+ break;
+ }
+
+ treeViewer = new CheckboxTreeViewer(viewerPane, SWT.SINGLE);
+
+ tree = treeViewer.getTree();
+ layoutData = new GridData(SWT.FILL, SWT.FILL, true, true);
+ layoutData.widthHint = 500;
+ tree.setLayoutData(layoutData);
+ viewerPane.setContent(tree);
+
+ final DelegatingStyledCellLabelProvider labelProvider = new DelegatingStyledCellLabelProvider(
+ new ResourceSelectionLabelProvider());
+
+ treeViewer.setLabelProvider(labelProvider);
+ treeViewer.setContentProvider(resourceSelectionContentProvider);
+ treeViewer.setUseHashlookup(true);
+ layoutData = new GridData(SWT.FILL, SWT.FILL, true, true);
+ layoutData.heightHint = 125;
+ layoutData.widthHint = TREE_WIDTH;
+ treeViewer.getControl().setLayoutData(layoutData);
+ treeViewer.setInput(this);
+
+ treeViewer.expandAll();
+
+ setAllChecked(true);
+ if (mode == TreeViewMode.MODE_TREE) {
+ treeViewer.collapseAll();
+ }
+ treeViewer.addCheckStateListener(new ICheckStateListener() {
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ handleCheckStateChange(event);
+ }
+ });
+
+ MenuManager menuMgr = new MenuManager();
+ Menu menu = menuMgr.createContextMenu(treeViewer.getTree());
+ menuMgr.addMenuListener(new IMenuListener() {
+ public void menuAboutToShow(IMenuManager menuMgr) {
+ fillTreeMenu(menuMgr);
+ }
+ });
+ menuMgr.setRemoveAllWhenShown(true);
+ treeViewer.getTree().setMenu(menu);
+ }
+
+ public void setAllChecked(boolean state) {
+ for (DecoratedResource resource : resourcesToShow) {
+ treeViewer.setChecked(resource, state);
+ handleCheckStateChange(resource, state);
+ }
+ }
+
+ protected void fillTreeMenu(IMenuManager menuMgr) {
+ Action selectAllAction = new Action("Select All") {
+ @Override
+ public void run() {
+ setAllChecked(true);
+ }
+ };
+ menuMgr.add(selectAllAction);
+
+ Action deselectAllAction = new Action("Deselect All") {
+ @Override
+ public void run() {
+ setAllChecked(false);
+ }
+ };
+ menuMgr.add(deselectAllAction);
+
+ menuMgr.add(new Separator());
+
+ Action deselectPreCommit = new Action("Deselect Pre-Commit") {
+ @Override
+ public void run() {
+ for (DecoratedResource res : getSelectedResources()) {
+ if (res.isUpToDate()) {
+ treeViewer.setChecked(res, false);
+ handleCheckStateChange(res, false);
+ }
+ }
+
+ }
+ };
+ menuMgr.add(deselectPreCommit);
+
+ Action deselectPostCommit = new Action("Deselect Post-Commit") {
+ @Override
+ public void run() {
+ for (DecoratedResource res : getSelectedResources()) {
+ if (res.isUpToDate()) {
+ treeViewer.setChecked(res, false);
+ handleCheckStateChange(res, false);
+ }
+ }
+ }
+ };
+ menuMgr.add(deselectPostCommit);
+
+ menuMgr.add(new Separator());
+
+ if (mode != TreeViewMode.MODE_FLAT) {
+ Action expandAllAction = new Action("Expand All") {
+ @Override
+ public void run() {
+ treeViewer.expandAll();
+ }
+ };
+ menuMgr.add(expandAllAction);
+ }
+ }
+
+ private void handleCheckStateChange(DecoratedResource element, boolean checked) {
+ treeViewer.setGrayed(element, false);
+ treeViewer.setSubtreeChecked(element, checked);
+ updateParentState(element, checked);
+ }
+
+ private void handleCheckStateChange(CheckStateChangedEvent event) {
+ handleCheckStateChange((DecoratedResource) event.getElement(), event.getChecked());
+ }
+
+ private void updateParentState(DecoratedResource child, boolean baseChildState) {
+ if (mode == TreeViewMode.MODE_FLAT || child == null || child.getResource().getParent() == null
+ || resourcesToShow.contains(new DecoratedResource(child.getResource().getParent()))) {
+ return;
+ }
+ if (child == null) {
+ return;
+ }
+ Object parent = resourceSelectionContentProvider.getParent(child);
+ if (parent == null) {
+ return;
+ }
+ boolean allSameState = true;
+ Object[] children = null;
+ children = resourceSelectionContentProvider.getChildren(parent);
+ for (int i = children.length - 1; i >= 0; i--) {
+ if (treeViewer.getChecked(children[i]) != baseChildState || treeViewer.getGrayed(children[i])) {
+ allSameState = false;
+ break;
+ }
+ }
+ treeViewer.setGrayed(parent, !allSameState);
+ treeViewer.setChecked(parent, !allSameState || baseChildState);
+ updateParentState((DecoratedResource) parent, baseChildState);
+ }
+
+ public void refresh() {
+ Object[] checkedElements = null;
+ checkedElements = treeViewer.getCheckedElements();
+ treeViewer.refresh();
+ treeViewer.expandAll();
+ treeViewer.setCheckedElements(checkedElements);
+ for (Object checkedElement : checkedElements) {
+ updateParentState((DecoratedResource) checkedElement, true);
+ }
+ if (mode == TreeViewMode.MODE_TREE) {
+ treeViewer.collapseAll();
+ }
+ }
+
+ private DecoratedResource[] getRootFolders() {
+ if (rootFolders == null) {
+ getFolders();
+ }
+ return rootFolders;
+ }
+
+ private DecoratedResource[] getCompressedFolders() {
+ if (compressedFolders == null) {
+ compressedFolders = new ArrayList<DecoratedResource>();
+ for (DecoratedResource res : resourcesToShow) {
+ IResource resource = res.getResource();
+ if (resource instanceof IContainer) {
+ DecoratedResource decoratedContainer = new DecoratedResource(resource);
+ if (!compressedFolders.contains(decoratedContainer)) {
+ compressedFolders.add(decoratedContainer);
+ }
+ }
+ if (!(resource instanceof IContainer)) {
+ IContainer parent = resource.getParent();
+ if (parent != null && !(parent instanceof IWorkspaceRoot)) {
+ DecoratedResource decoratedContainer = new DecoratedResource(parent);
+ if (!compressedFolders.contains(decoratedContainer)) {
+ compressedFolders.add(decoratedContainer);
+ }
+ }
+ }
+ }
+ Collections.sort(compressedFolders, comparator);
+ }
+ return compressedFolders.toArray(new DecoratedResource[compressedFolders.size()]);
+ }
+
+ private DecoratedResource[] getChildResources(IContainer directory) {
+ ArrayList<DecoratedResource> children = new ArrayList<DecoratedResource>();
+ for (DecoratedResource res : resourcesToShow) {
+ if (!(res.getResource() instanceof IContainer)) {
+ IContainer parentFolder = res.getResource().getParent();
+ if (parentFolder != null && parentFolder.equals(directory) /*&& !children.contains(parentFolder)*/) {
+ children.add(res);
+ }
+ }
+ }
+
+ DecoratedResource[] childArray = new DecoratedResource[children.size()];
+ children.toArray(childArray);
+ return childArray;
+ }
+
+ private DecoratedResource[] getFolderChildren(IContainer parent) {
+ ArrayList<DecoratedResource> children = new ArrayList<DecoratedResource>();
+ folders = getFolders();
+ for (DecoratedResource folder : folders) {
+ if (folder.getResource().getParent() != null && folder.getResource().getParent().equals(parent)) {
+ children.add(folder);
+ }
+ }
+ for (DecoratedResource res : resourcesToShow) {
+ if (!(res.getResource() instanceof IContainer) && res.getResource().getParent() != null
+ && res.getResource().getParent().equals(parent)) {
+ children.add(res);
+ }
+ }
+ DecoratedResource[] childArray = new DecoratedResource[children.size()];
+ children.toArray(childArray);
+ return childArray;
+ }
+
+ private DecoratedResource[] getFolders() {
+ List<DecoratedResource> rootList = new ArrayList<DecoratedResource>();
+
+ if (folders == null) {
+ ArrayList<DecoratedResource> folderList = new ArrayList<DecoratedResource>();
+ for (DecoratedResource resource : resourcesToShow) {
+ if (resource.getResource() instanceof IContainer) {
+ folderList.add(resource);
+ }
+ IResource parent = resource.getResource();
+// if (parent != null && !(parent instanceof IContainer || parent instanceof IWorkspaceRoot)) {
+// parent = parent.getParent();
+// }
+ while (parent != null && !(parent instanceof IWorkspaceRoot)) {
+ DecoratedResource decoratedParent = new DecoratedResource(parent);
+ DecoratedResource decoratedParentParent = new DecoratedResource(parent.getParent());
+ if (!(parent.getParent() instanceof IWorkspaceRoot) && folderList.contains(decoratedParentParent)) {
+ break;
+ }
+ if (parent.getParent() == null || parent.getParent() instanceof IWorkspaceRoot) {
+ if (!rootList.contains(decoratedParent)) {
+ rootList.add(decoratedParent);
+ }
+ }
+ parent = parent.getParent();
+ folderList.add(decoratedParentParent);
+ }
+ }
+ folders = new DecoratedResource[folderList.size()];
+ folderList.toArray(folders);
+ Arrays.sort(folders, comparator);
+ rootFolders = new DecoratedResource[rootList.size()];
+ rootList.toArray(rootFolders);
+ Arrays.sort(rootFolders, comparator);
+ }
+ return folders;
+ }
+
+ private class ResourceSelectionContentProvider extends WorkbenchContentProvider {
+ @Override
+ public Object getParent(Object element) {
+ IContainer parent = ((DecoratedResource) element).getResource().getParent();
+ if (parent != null) {
+ return new DecoratedResource(parent);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (mode != TreeViewMode.MODE_FLAT && ((DecoratedResource) element).getResource() instanceof IContainer) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return getChildren(inputElement);
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof ResourceSelectionTree) {
+ if (mode == TreeViewMode.MODE_FLAT) {
+ return resourcesToShow.toArray(new DecoratedResource[resourcesToShow.size()]);
+ } else if (mode == TreeViewMode.MODE_COMPRESSED_FOLDERS) {
+ return getCompressedFolders();
+ } else {
+ return getRootFolders();
+ }
+ }
+ IResource resource = ((DecoratedResource) parentElement).getResource();
+ if (resource instanceof IContainer) {
+ IContainer directory = (IContainer) resource;
+ if (mode == TreeViewMode.MODE_COMPRESSED_FOLDERS) {
+ return getChildResources(directory);
+ }
+ if (mode == TreeViewMode.MODE_TREE) {
+ return getFolderChildren(directory);
+ }
+ }
+ return new Object[0];
+ }
+ }
+
+ private class ResourceSelectionLabelProvider extends ColumnLabelProvider implements IStyledLabelProvider {
+ private final WorkbenchLabelProvider workbenchLabelProvider = new WorkbenchLabelProvider();
+
+ private final static String colorRed = "com.atlassian.connector.eclipse.internal.red";
+
+ public ResourceSelectionLabelProvider() {
+ super();
+ JFaceResources.getColorRegistry().put(colorRed, new RGB(150, 20, 20));
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ return workbenchLabelProvider.getImage(((DecoratedResource) element).getResource());
+ }
+
+ private String getTextForResource(DecoratedResource decoratedResource) {
+ String text = null;
+ IResource resource = decoratedResource.getResource();
+ if (mode == TreeViewMode.MODE_FLAT) {
+ text = resource.getFullPath().toString();
+ } else if (mode == TreeViewMode.MODE_COMPRESSED_FOLDERS) {
+ if (resource instanceof IContainer) {
+ IContainer container = (IContainer) resource;
+ text = container.getFullPath().makeRelative().toString();
+ } else {
+ text = resource.getName();
+ }
+ } else {
+ text = resource.getName();
+ }
+ return text == null ? "" : text;
+ }
+
+ public StyledString getStyledText(Object element) {
+ StyledString styledString = new StyledString();
+
+ DecoratedResource resource = (DecoratedResource) element;
+
+ styledString.append(getTextForResource(resource));
+
+ if (resourcesToShow.contains(resource)) {
+ styledString.append(" ");
+ styledString.append(resource.getDecorationText(),
+ StyledString.createColorRegistryStyler(colorRed, null));
+ }
+
+ return styledString;
+ }
+ }
+
+ private class ResourceComparator implements Comparator<DecoratedResource> {
+ public int compare(DecoratedResource obj0, DecoratedResource obj1) {
+ return obj0.getResource()
+ .getFullPath()
+ .toOSString()
+ .compareTo(obj1.getResource().getFullPath().toOSString());
+ }
+ }
+
+ public static interface IToolbarControlCreator {
+ void createToolbarControls(ToolBarManager toolbarManager);
+
+ public int getControlCount();
+ }
+
+ public static interface ITreeViewModeSettingProvider {
+ TreeViewMode getTreeViewMode();
+
+ void setTreeViewMode(TreeViewMode mode);
+ }
+
+ public void setResources(List<DecoratedResource> resourcesToShow) {
+ this.resourcesToShow = resourcesToShow;
+
+ compressedFolders = null;
+ folders = null;
+ rootFolders = null;
+ }
+
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/dialogs/ProgressDialog.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/dialogs/ProgressDialog.java
new file mode 100644
index 0000000..28afac6
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/dialogs/ProgressDialog.java
@@ -0,0 +1,283 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+
+package com.atlassian.connector.eclipse.ui.dialogs;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.operation.ModalContext;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.wizard.ProgressMonitorPart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * Dialog that can display progress
+ *
+ * @author Shawn Minto
+ */
+public abstract class ProgressDialog extends TitleAreaDialog {
+
+ private boolean lockedUI = false;
+
+ private Composite pageContainer;
+
+ private Cursor waitCursor;
+
+ private ProgressMonitorPart progressMonitorPart;
+
+ private long activeRunningOperations = 0;
+
+ private final HashMap<Integer, Button> buttons = new HashMap<Integer, Button>();
+
+ public ProgressDialog(Shell parentShell) {
+ super(parentShell);
+ setDialogHelpAvailable(false);
+ setHelpAvailable(false);
+ }
+
+ /*
+ * (non-Javadoc) Method declared on Dialog.
+ */
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite composite = (Composite) super.createDialogArea(parent);
+ // Build the Page container
+ pageContainer = new Composite(composite, SWT.NONE);
+ pageContainer.setLayout(new GridLayout());
+ GridData gd = new GridData(GridData.FILL_BOTH | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+ pageContainer.setLayoutData(gd);
+ pageContainer.setFont(parent.getFont());
+ createPageControls(pageContainer);
+
+ // Insert a progress monitor
+ GridLayout pmlayout = new GridLayout();
+ pmlayout.numColumns = 1;
+ progressMonitorPart = createProgressMonitorPart(composite, pmlayout);
+ GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
+ progressMonitorPart.setLayoutData(gridData);
+ progressMonitorPart.setVisible(true);
+ // Build the separator line
+ Label separator = new Label(parent, SWT.HORIZONTAL | SWT.SEPARATOR);
+ separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ applyDialogFont(progressMonitorPart);
+ return composite;
+ }
+
+ protected abstract Control createPageControls(Composite parent);
+
+ protected Collection<? extends Control> getDisableableControls() {
+ return buttons.values();
+ }
+
+ /**
+ * About to start a long running operation triggered through the wizard. Shows the progress monitor and disables the
+ * wizard's buttons and controls.
+ *
+ * @param enableCancelButton
+ * <code>true</code> if the Cancel button should be enabled, and <code>false</code> if it should be
+ * disabled
+ * @return the saved UI state
+ */
+ private void aboutToStart(boolean enableCancelButton) {
+ if (getShell() != null) {
+ // Save focus control
+ Control focusControl = getShell().getDisplay().getFocusControl();
+ if (focusControl != null && focusControl.getShell() != getShell()) {
+ focusControl = null;
+ }
+ // Set the busy cursor to all shells.
+ Display d = getShell().getDisplay();
+ waitCursor = new Cursor(d, SWT.CURSOR_WAIT);
+ setDisplayCursor(waitCursor);
+
+ for (Control button : getDisableableControls()) {
+ button.setEnabled(false);
+ }
+
+ progressMonitorPart.setVisible(true);
+ }
+ }
+
+ /**
+ * A long running operation triggered through the wizard was stopped either by user input or by normal end. Hides
+ * the progress monitor and restores the enable state wizard's buttons and controls.
+ *
+ * @param savedState
+ * the saved UI state as returned by <code>aboutToStart</code>
+ * @see #aboutToStart
+ */
+ private void stopped(Object savedState) {
+ if (getShell() != null) {
+ progressMonitorPart.setVisible(false);
+ setDisplayCursor(null);
+ waitCursor.dispose();
+ waitCursor = null;
+
+ for (Control button : getDisableableControls()) {
+ button.setEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ }
+
+ /**
+ * Create the progress monitor part in the receiver.
+ *
+ * @param composite
+ * @param pmlayout
+ * @return ProgressMonitorPart
+ */
+ protected ProgressMonitorPart createProgressMonitorPart(Composite composite, GridLayout pmlayout) {
+ return new ProgressMonitorPart(composite, pmlayout, SWT.DEFAULT) {
+ private String currentTask = null;
+
+ @Override
+ public void setBlocked(IStatus reason) {
+ super.setBlocked(reason);
+ if (!lockedUI) {
+ getBlockedHandler().showBlocked(getShell(), this, reason, currentTask);
+ }
+ }
+
+ @Override
+ public void clearBlocked() {
+ super.clearBlocked();
+ if (!lockedUI) {
+ getBlockedHandler().clearBlocked();
+ }
+ }
+
+ @Override
+ public void beginTask(String name, int totalWork) {
+ super.beginTask(name, totalWork);
+ currentTask = name;
+ }
+
+ @Override
+ public void setTaskName(String name) {
+ super.setTaskName(name);
+ currentTask = name;
+ }
+
+ @Override
+ public void subTask(String name) {
+ super.subTask(name);
+ if (currentTask == null) {
+ currentTask = name;
+ }
+ }
+ };
+ }
+
+ /**
+ * This implementation of IRunnableContext#run(boolean, boolean, IRunnableWithProgress) blocks until the runnable
+ * has been run, regardless of the value of <code>fork</code>. It is recommended that <code>fork</code> is set to
+ * true in most cases. If <code>fork</code> is set to <code>false</code>, the runnable will run in the UI thread and
+ * it is the runnable's responsibility to call <code>Display.readAndDispatch()</code> to ensure UI responsiveness.
+ *
+ * UI state is saved prior to executing the long-running operation and is restored after the long-running operation
+ * completes executing. Any attempt to change the UI state of the wizard in the long-running operation will be
+ * nullified when original UI state is restored.
+ *
+ */
+ public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) throws InvocationTargetException,
+ InterruptedException {
+ // The operation can only be canceled if it is executed in a separate
+ // thread.
+ // Otherwise the UI is blocked anyway.
+ Object state = null;
+ if (activeRunningOperations == 0) {
+ aboutToStart(fork && cancelable);
+ }
+ activeRunningOperations++;
+ try {
+ if (!fork) {
+ lockedUI = true;
+ }
+ ModalContext.run(runnable, fork, getProgressMonitor(), getShell().getDisplay());
+ lockedUI = false;
+ } finally {
+ activeRunningOperations--;
+ // Stop if this is the last one
+ if (activeRunningOperations <= 0) {
+ stopped(state);
+ }
+ }
+ }
+
+ /**
+ * Returns the progress monitor for this wizard dialog (if it has one).
+ *
+ * @return the progress monitor, or <code>null</code> if this wizard dialog does not have one
+ */
+ protected IProgressMonitor getProgressMonitor() {
+ return progressMonitorPart;
+ }
+
+ /**
+ * Sets the given cursor for all shells currently active for this window's display.
+ *
+ * @param c
+ * the cursor
+ */
+ private void setDisplayCursor(Cursor c) {
+ Shell[] shells = getShell().getDisplay().getShells();
+ for (Shell element : shells) {
+ element.setCursor(c);
+ }
+ }
+
+ @Override
+ protected Button createButton(Composite parent, int id, String label, boolean defaultButton) {
+ // increment the number of columns in the button bar
+ ((GridLayout) parent.getLayout()).numColumns++;
+ Button button = new Button(parent, SWT.PUSH);
+ button.setText(label);
+ button.setFont(JFaceResources.getDialogFont());
+ button.setData(new Integer(id));
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ buttonPressed(((Integer) event.widget.getData()).intValue());
+ }
+ });
+ if (defaultButton) {
+ Shell shell = parent.getShell();
+ if (shell != null) {
+ shell.setDefaultButton(button);
+ }
+ }
+ buttons.put(new Integer(id), button);
+ setButtonLayoutData(button);
+ return button;
+ }
+}
diff --git a/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/forms/SizeCachingComposite.java b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/forms/SizeCachingComposite.java
new file mode 100644
index 0000000..f3b7083
--- /dev/null
+++ b/framework/com.atlassian.connector.eclipse.crucible.ui/src/com/atlassian/connector/eclipse/ui/forms/SizeCachingComposite.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * Copyright (c) 2009 Atlassian and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Atlassian - initial API and implementation
+ ******************************************************************************/
+package com.atlassian.connector.eclipse.ui.forms;
+
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * A composite that can cache its size for faster layout computation
+ *
+ * @author Shawn Minto
+ */
+public class SizeCachingComposite extends Composite {
+
+ private Point cachedSize = null;
+
+ public SizeCachingComposite(Composite parent, int style) {
+ super(parent, style);
+ }
+
+ @Override
+ public Point computeSize(int wHint, int hHint, boolean changed) {
+ if (cachedSize == null) {
+ cachedSize = super.computeSize(wHint, hHint, changed);
+ }
+ return cachedSize;
+ }
+
+ public void clearCache() {
+ cachedSize = null;
+ }
+} \ No newline at end of file

Back to the top