1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package com.atlassian.theplugin.idea.crucible;
17
18 import com.atlassian.theplugin.cfg.CfgUtil;
19 import com.atlassian.theplugin.commons.ServerType;
20 import com.atlassian.theplugin.commons.UiTaskExecutor;
21 import com.atlassian.theplugin.commons.cfg.CrucibleServerCfg;
22 import com.atlassian.theplugin.commons.cfg.ServerCfg;
23 import com.atlassian.theplugin.commons.crucible.CrucibleServerFacadeImpl;
24 import com.atlassian.theplugin.commons.crucible.api.model.*;
25 import com.atlassian.theplugin.commons.exception.ServerPasswordNotProvidedException;
26 import com.atlassian.theplugin.commons.remoteapi.RemoteApiException;
27 import com.atlassian.theplugin.commons.remoteapi.ServerData;
28 import com.atlassian.theplugin.configuration.CrucibleWorkspaceConfiguration;
29 import com.atlassian.theplugin.configuration.WorkspaceConfigurationBean;
30 import com.atlassian.theplugin.crucible.model.*;
31 import com.atlassian.theplugin.idea.Constants;
32 import com.atlassian.theplugin.idea.IdeaHelper;
33 import com.atlassian.theplugin.idea.PluginToolWindowPanel;
34 import com.atlassian.theplugin.idea.config.ProjectCfgManagerImpl;
35 import com.atlassian.theplugin.idea.crucible.editor.CommentHighlighter;
36 import com.atlassian.theplugin.idea.crucible.filters.CustomFilterChangeListener;
37 import com.atlassian.theplugin.idea.crucible.tree.*;
38 import com.atlassian.theplugin.idea.crucible.tree.node.CrucibleReviewTreeNode;
39 import com.atlassian.theplugin.idea.ui.PopupAwareMouseAdapter;
40 import com.atlassian.theplugin.idea.ui.tree.paneltree.TreeRenderer;
41 import com.atlassian.theplugin.idea.ui.tree.paneltree.TreeUISetup;
42 import com.atlassian.theplugin.util.PluginUtil;
43 import com.intellij.openapi.actionSystem.*;
44 import com.intellij.openapi.progress.ProgressIndicator;
45 import com.intellij.openapi.progress.ProgressManager;
46 import com.intellij.openapi.progress.Task;
47 import com.intellij.openapi.project.Project;
48 import com.intellij.ui.TreeSpeedSearch;
49 import org.jetbrains.annotations.NonNls;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import javax.swing.*;
54 import javax.swing.event.DocumentEvent;
55 import javax.swing.event.DocumentListener;
56 import javax.swing.tree.DefaultMutableTreeNode;
57 import javax.swing.tree.TreeCellRenderer;
58 import javax.swing.tree.TreePath;
59 import java.awt.*;
60 import java.awt.event.*;
61 import java.net.MalformedURLException;
62 import java.net.URL;
63 import java.util.ArrayList;
64 import java.util.Collection;
65 import java.util.List;
66
67
68
69
70 public class ReviewListToolWindowPanel extends PluginToolWindowPanel implements DataProvider {
71
72 public static final String PLACE_PREFIX = ReviewListToolWindowPanel.class.getSimpleName();
73 private static final TreeCellRenderer TREE_RENDERER = new TreeRenderer();
74
75 private final CrucibleWorkspaceConfiguration crucibleProjectConfiguration;
76 private ReviewTree reviewTree;
77 private CrucibleFilterListModel filterListModel;
78 private CrucibleFilterTreeModel filterTreeModel;
79
80 private CrucibleReviewGroupBy groupBy = CrucibleReviewGroupBy.NONE;
81 private FilterTree filterTree;
82 private CrucibleCustomFilterDetailsPanel detailsPanel;
83 private SearchingCrucibleReviewListModel searchingReviewListModel;
84 private final ProjectCfgManagerImpl projectCfgManager;
85 private final UiTaskExecutor uiTaskExecutor;
86 private final CrucibleReviewListModel reviewListModel;
87 private Timer timer;
88
89 private static final int ONE_SECOND = 1000;
90 private CrucibleReviewListModel currentReviewListModel;
91
92
93 public ReviewListToolWindowPanel(@NotNull final Project project,
94 @NotNull final WorkspaceConfigurationBean projectConfiguration,
95 @NotNull final ProjectCfgManagerImpl projectCfgManager,
96 @NotNull final CrucibleReviewListModel reviewListModel,
97 @NotNull final UiTaskExecutor uiTaskExecutor) {
98 super(project, "ThePlugin.Reviews.LeftToolBar", "ThePlugin.Reviews.RightToolBar");
99 this.projectCfgManager = projectCfgManager;
100 this.uiTaskExecutor = uiTaskExecutor;
101
102 crucibleProjectConfiguration = projectConfiguration.getCrucibleConfiguration();
103
104 filterListModel = new CrucibleFilterListModel(
105 crucibleProjectConfiguration.getCrucibleFilters().getManualFilter(),
106 crucibleProjectConfiguration.getCrucibleFilters().getRecenltyOpenFilter());
107 filterTreeModel = new CrucibleFilterTreeModel(filterListModel, reviewListModel);
108 this.reviewListModel = reviewListModel;
109 CrucibleReviewListModel sortingListModel = new SortingByKeyCrucibleReviewListModel(this.reviewListModel);
110 searchingReviewListModel = new SearchingCrucibleReviewListModel(sortingListModel);
111 currentReviewListModel = searchingReviewListModel;
112 init(Constants.DIALOG_MARGIN / 2);
113 this.reviewListModel.addListener(new LocalCrucibleReviewListModelListener());
114 filterTree.addSelectionListener(new LocalCrucibleFilterListModelListener());
115 }
116
117 @Override
118 public void init(int margin) {
119 super.init(margin);
120
121 addReviewTreeListeners();
122 setupReviewTree();
123
124 initToolBar();
125
126 detailsPanel = new CrucibleCustomFilterDetailsPanel(
127 getProject(), projectCfgManager, crucibleProjectConfiguration,
128 filterTree, CrucibleServerFacadeImpl.getInstance(), uiTaskExecutor);
129 detailsPanel.addCustomFilterChangeListener(new CustomFilterChangeListener() {
130 public void customFilterChanged(CustomFilter customFilter) {
131 refresh(UpdateReason.FILTER_CHANGED);
132 }
133 });
134
135 filterTreeModel.nodeChanged((DefaultMutableTreeNode) filterTreeModel.getRoot());
136
137 if (crucibleProjectConfiguration != null
138 && crucibleProjectConfiguration.getCrucibleFilters().getManualFilter() != null
139 && crucibleProjectConfiguration.getCrucibleFilters().getManualFilter().isEnabled()) {
140 showManualFilterPanel(true);
141 } else {
142 showManualFilterPanel(false);
143 }
144
145 addSearchBoxListener();
146 }
147
148 private void setupReviewTree() {
149 TreeUISetup uiSetup = new TreeUISetup(TREE_RENDERER);
150 uiSetup.initializeUI(reviewTree, getRightScrollPane());
151 }
152
153 private void initToolBar() {
154
155 if (crucibleProjectConfiguration.getView() != null && crucibleProjectConfiguration.getView().getGroupBy() != null) {
156 groupBy = crucibleProjectConfiguration.getView().getGroupBy();
157 }
158 reviewTree.setGroupBy(groupBy);
159 }
160
161 public void openReview(final ReviewAdapter review, boolean retrieveDetails) {
162 CommentHighlighter.removeCommentsInEditors(project);
163 reviewListModel.openReview(review, UpdateReason.OPEN_IN_IDE);
164 IdeaHelper.getReviewDetailsToolWindow(getProject()).showReview(review, retrieveDetails);
165 }
166
167 public void closeReviewDetailsWindow(final AnActionEvent event) {
168 reviewListModel.clearOpenInIde(UpdateReason.OPEN_IN_IDE);
169 IdeaHelper.getReviewDetailsToolWindow(project).closeToolWindow(event);
170 }
171
172
173 private void addReviewTreeListeners() {
174 reviewTree.addKeyListener(new KeyAdapter() {
175 @Override
176 public void keyPressed(final KeyEvent e) {
177 final ReviewAdapter review = reviewTree.getSelectedReview();
178 if (e.getKeyCode() == KeyEvent.VK_ENTER && review != null) {
179 openReview(review, true);
180 }
181 }
182 });
183
184 reviewTree.addMouseListener(new PopupAwareMouseAdapter() {
185
186 @Override
187 public void mouseClicked(final MouseEvent e) {
188 final ReviewAdapter review = reviewTree.getSelectedReview();
189 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 && review != null) {
190 openReview(review, true);
191 }
192 }
193
194 @Override
195 protected void onPopup(MouseEvent e) {
196 int selRow = reviewTree.getRowForLocation(e.getX(), e.getY());
197 TreePath selPath = reviewTree.getPathForLocation(e.getX(), e.getY());
198 if (selRow != -1 && selPath != null) {
199 reviewTree.setSelectionPath(selPath);
200 final ReviewAdapter review = reviewTree.getSelectedReview();
201 if (review != null) {
202 launchContextMenu(e);
203 }
204 }
205 }
206 });
207 }
208
209 private void launchContextMenu(MouseEvent e) {
210 final DefaultActionGroup actionGroup = new DefaultActionGroup();
211
212 final ActionGroup configActionGroup = (ActionGroup) ActionManager
213 .getInstance().getAction("ThePlugin.Reviews.ReviewPopupMenu");
214 actionGroup.addAll(configActionGroup);
215
216 final ActionPopupMenu popup = ActionManager.getInstance().createActionPopupMenu(getActionPlaceName(), actionGroup);
217
218 final JPopupMenu jPopupMenu = popup.getComponent();
219 jPopupMenu.show(e.getComponent(), e.getX(), e.getY());
220 }
221
222 private void setPanelEnabled(boolean enabled) {
223 if (reviewTree != null) {
224 reviewTree.setEnabled(enabled);
225 }
226 if (filterTree != null) {
227 filterTree.setEnabled(enabled);
228 }
229 if (getSearchField() != null) {
230 getSearchField().setEnabled(enabled);
231 }
232 if (getStatusBarPane() != null) {
233 getStatusBarPane().setEnabled(enabled);
234 }
235 filterTree.redrawNodes();
236 }
237
238 @Nullable
239 public Object getData(@NonNls String dataId) {
240 if (reviewTree != null) {
241 if (dataId.equals(Constants.REVIEW)) {
242 return reviewTree.getSelectedReview();
243 } else if (dataId.equals(Constants.REVIEW_WINDOW_ENABLED)) {
244 return reviewTree.isEnabled();
245 }
246 if (dataId.equals(Constants.SERVER)) {
247 if (reviewTree.getSelectedReview() != null) {
248 return reviewTree.getSelectedReview().getServerData();
249 }
250 }
251 }
252 return null;
253 }
254
255 @Override
256 protected void addSearchBoxListener() {
257 getSearchField().addDocumentListener(new DocumentListener() {
258 public void insertUpdate(DocumentEvent e) {
259 triggerDelayedSearchBoxUpdate();
260 }
261
262 public void removeUpdate(DocumentEvent e) {
263 triggerDelayedSearchBoxUpdate();
264 }
265
266 public void changedUpdate(DocumentEvent e) {
267 triggerDelayedSearchBoxUpdate();
268 }
269 });
270 getSearchField().addKeyboardListener(new KeyListener() {
271 public void keyTyped(KeyEvent e) {
272 }
273
274 public void keyPressed(KeyEvent e) {
275 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
276 getSearchField().addCurrentTextToHistory();
277 }
278 }
279
280 public void keyReleased(KeyEvent e) {
281 }
282 });
283 }
284
285 private void triggerDelayedSearchBoxUpdate() {
286 if (timer != null && timer.isRunning()) {
287 return;
288 }
289 timer = new Timer(ONE_SECOND, new ActionListener() {
290 public void actionPerformed(ActionEvent e) {
291 searchingReviewListModel.setSearchTerm(getSearchField().getText());
292 }
293 });
294 timer.setRepeats(false);
295 timer.start();
296 }
297
298 @Override
299 public JTree createRightTree() {
300 if (reviewTree == null) {
301 reviewTree = new ReviewTree(new ReviewTreeModel(currentReviewListModel, projectCfgManager,
302 CfgUtil.getProjectId(project)));
303
304 new TreeSpeedSearch(reviewTree) {
305 @Override
306 protected boolean isMatchingElement(Object o, String s) {
307 TreePath tp = (TreePath) o;
308 Object node = tp.getLastPathComponent();
309 if (node instanceof CrucibleReviewTreeNode) {
310 ReviewTreeNode rtn = (ReviewTreeNode) node;
311 ReviewAdapter review = rtn.getReview();
312 return review.getPermId().getId().toLowerCase().contains(s.toLowerCase())
313 || review.getName().toLowerCase().contains(s.toLowerCase());
314 } else {
315 return super.isMatchingElement(o, s);
316 }
317 }
318 };
319 }
320 return reviewTree;
321 }
322
323 @Override
324 public JTree createLeftTree() {
325 if (filterTree == null && filterTreeModel != null) {
326 filterTree = new FilterTree(filterTreeModel, crucibleProjectConfiguration);
327 }
328
329 return filterTree;
330 }
331
332
333 @Override
334 public String getActionPlaceName() {
335 return PLACE_PREFIX + this.getProject().getName();
336 }
337
338 public void setGroupBy(CrucibleReviewGroupBy groupBy) {
339 this.groupBy = groupBy;
340 reviewTree.groupBy(groupBy);
341
342 crucibleProjectConfiguration.getView().setGroupBy(groupBy);
343 }
344
345 public CrucibleReviewGroupBy getGroupBy() {
346 return groupBy;
347 }
348
349 protected void showManualFilterPanel(boolean visible) {
350 getSplitLeftPane().setOrientation(true);
351
352 if (visible) {
353 getSplitLeftPane().setSecondComponent(detailsPanel);
354 getSplitLeftPane().setProportion(MANUAL_FILTER_PROPORTION_VISIBLE);
355
356 } else {
357 getSplitLeftPane().setSecondComponent(null);
358 getSplitLeftPane().setProportion(MANUAL_FILTER_PROPORTION_HIDDEN);
359 }
360 }
361
362 public void refresh(final UpdateReason reason) {
363 Task.Backgroundable refresh = new Task.Backgroundable(getProject(), "Refreshing Crucible Panel", false) {
364 @Override
365 public void run(@NotNull final ProgressIndicator indicator) {
366 reviewListModel.rebuildModel(reason);
367 }
368 };
369 ProgressManager.getInstance().run(refresh);
370 }
371
372 public Collection<CrucibleServerCfg> getServers() {
373 return projectCfgManager.getCfgManager().getAllEnabledCrucibleServers(CfgUtil.getProjectId(project));
374 }
375
376 public List<ReviewAdapter> getLocalReviews(final String searchKey) {
377 List<ReviewAdapter> reviews = new ArrayList<ReviewAdapter>();
378 for (ReviewAdapter review : currentReviewListModel.getReviews()) {
379 if (searchKey.toUpperCase().equals(review.getPermId().getId().toUpperCase())) {
380 reviews.add(review);
381 }
382 }
383
384 return reviews;
385 }
386
387 public CrucibleWorkspaceConfiguration getCrucibleConfiguration() {
388 return crucibleProjectConfiguration;
389 }
390
391
392
393
394
395
396
397 public List<ReviewAdapter> getReviewAdapters(final List<ReviewRecentlyOpenBean> recentlyOpenReviews) {
398
399 List<ReviewAdapter> reviews = new ArrayList<ReviewAdapter>();
400
401 if (recentlyOpenReviews == null || recentlyOpenReviews.isEmpty()) {
402 return reviews;
403 }
404
405 for (ReviewRecentlyOpenBean recentlyOpenReview : recentlyOpenReviews) {
406
407 ReviewAdapter ra = getReviewFromLocalModel(recentlyOpenReview.getReviewId(), recentlyOpenReview.getServerId());
408
409 if (ra != null) {
410 reviews.add(ra);
411 } else {
412
413 ReviewAdapter rra = getReviewFromServer(recentlyOpenReview.getReviewId(), recentlyOpenReview.getServerId());
414
415 if (rra != null) {
416 reviews.add(rra);
417 }
418 }
419 }
420
421 return reviews;
422 }
423
424
425
426
427
428
429
430
431 private ReviewAdapter getReviewFromServer(final String reviewKey, final String serverId) {
432
433 ServerCfg server = CfgUtil.getEnabledServerCfgbyServerId(project, projectCfgManager, serverId);
434 if (server != null) {
435 try {
436 final ServerData serverData = projectCfgManager.getServerData(server);
437 Review r = CrucibleServerFacadeImpl.getInstance().getReview(serverData, new PermIdBean(reviewKey));
438 return new ReviewAdapter(r, serverData);
439 } catch (RemoteApiException e) {
440 PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
441 setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
442 } catch (ServerPasswordNotProvidedException e) {
443 PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
444 setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
445 }
446 }
447
448 return null;
449 }
450
451
452
453
454
455
456
457
458
459 private ReviewAdapter getReviewWithDetailsFromServer(final String reviewKey, final String serverId) {
460
461 ServerCfg server = CfgUtil.getEnabledServerCfgbyServerId(project, projectCfgManager, serverId);
462 if (server != null) {
463 try {
464 final ServerData serverData = projectCfgManager.getServerData(server);
465 Review r = CrucibleServerFacadeImpl.getInstance().getReview(serverData, new PermIdBean(reviewKey));
466 ReviewAdapter ra = new ReviewAdapter(r, serverData);
467 CrucibleServerFacadeImpl.getInstance().getDetailsForReview(ra);
468 return ra;
469 } catch (RemoteApiException e) {
470 PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
471 setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
472 } catch (ServerPasswordNotProvidedException e) {
473 PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
474 setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
475 }
476 }
477
478 return null;
479 }
480
481 private ReviewAdapter getReviewFromLocalModel(final String reviewKey, final String serverId) {
482 if (currentReviewListModel.getReviews() != null && !currentReviewListModel.getReviews().isEmpty()) {
483 for (ReviewAdapter localReview : currentReviewListModel.getReviews()) {
484 if (localReview.getPermId().getId().equals(reviewKey)
485 && localReview.getServerData().getServerId().equals(serverId)) {
486 return localReview;
487 }
488 }
489 }
490 return null;
491 }
492
493
494
495
496
497
498
499
500 public ReviewAdapter openReviewWithDetails(final String reviewKey, final String serverUrl) {
501 ServerData server = CfgUtil.findServer(serverUrl, projectCfgManager.getCfgManager().
502 getAllServers(CfgUtil.getProjectId(project), ServerType.CRUCIBLE_SERVER), projectCfgManager);
503
504 if (server == null) {
505
506 URL url;
507
508 try {
509 url = new URL(serverUrl);
510 } catch (MalformedURLException e) {
511 PluginUtil.getLogger().warn("Error opening review. Invalid url [" + serverUrl + "]", e);
512 return null;
513 }
514
515 server = CfgUtil.findServer(url, projectCfgManager.getCfgManager().
516 getAllServers(CfgUtil.getProjectId(project), ServerType.JIRA_SERVER), projectCfgManager);
517 }
518
519 if (server != null) {
520
521
522
523
524
525
526
527 ReviewAdapter ra = getReviewWithDetailsFromServer(reviewKey, server.getServerId());
528
529 if (ra != null) {
530 final ReviewAdapter reviewAdapter = ra;
531 EventQueue.invokeLater(new Runnable() {
532 public void run() {
533 openReview(reviewAdapter, false);
534 }
535 });
536
537 return ra;
538 }
539 }
540 return null;
541 }
542
543 private class LocalCrucibleFilterListModelListener extends CrucibleFilterSelectionListenerAdapter {
544 public void filterSelectionChanged() {
545
546 refresh(UpdateReason.FILTER_CHANGED);
547 }
548
549 public void selectedCustomFilter(CustomFilter customFilter) {
550 showManualFilterPanel(true);
551 }
552
553 public void unselectedCustomFilter() {
554 showManualFilterPanel(false);
555 }
556 }
557
558 private class LocalCrucibleReviewListModelListener extends CrucibleReviewListModelListenerAdapter {
559 private Exception exception;
560
561 @Override
562 public void reviewListUpdateStarted(UpdateContext updateContext) {
563 exception = null;
564 if (updateContext.getUpdateReason() != UpdateReason.TIMER_FIRED) {
565 setPanelEnabled(false);
566 setStatusInfoMessage("Loading reviews...");
567 }
568 }
569
570 @Override
571 public void reviewListUpdateFinished(UpdateContext updateContext) {
572 setPanelEnabled(true);
573 if (exception != null) {
574 setStatusErrorMessage(exception.getMessage(), exception);
575 } else {
576 setStatusInfoMessage("Loaded " + reviewListModel.getReviews().size() + " reviews");
577 }
578 }
579
580 @Override
581 public void reviewListUpdateError(final UpdateContext updateContext, final Exception e) {
582 this.exception = e;
583 }
584 }
585 }