View Javadoc

1   package com.atlassian.theplugin.idea.jira.editor;
2   
3   import com.atlassian.theplugin.idea.Constants;
4   import com.atlassian.theplugin.idea.IdeaHelper;
5   import com.atlassian.theplugin.idea.jira.editor.vfs.MemoryVirtualFile;
6   import com.atlassian.theplugin.idea.jira.IssueComment;
7   import com.atlassian.theplugin.idea.jira.CachedIconLoader;
8   import com.atlassian.theplugin.jira.JIRAServer;
9   import com.atlassian.theplugin.jira.JIRAServerFacade;
10  import com.atlassian.theplugin.jira.JIRAServerFacadeImpl;
11  import com.atlassian.theplugin.jira.api.JIRAComment;
12  import com.atlassian.theplugin.jira.api.JIRAException;
13  import com.atlassian.theplugin.jira.api.JIRAIssue;
14  import com.atlassian.theplugin.jira.api.JIRAConstant;
15  import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
16  import com.intellij.ide.BrowserUtil;
17  import com.intellij.ide.structureView.StructureViewBuilder;
18  import com.intellij.openapi.components.ApplicationComponent;
19  import com.intellij.openapi.fileEditor.*;
20  import com.intellij.openapi.project.Project;
21  import com.intellij.openapi.ui.VerticalFlowLayout;
22  import com.intellij.openapi.ui.Messages;
23  import com.intellij.openapi.ui.Splitter;
24  import com.intellij.openapi.util.IconLoader;
25  import com.intellij.openapi.util.Key;
26  import com.intellij.openapi.vfs.VirtualFile;
27  import com.intellij.openapi.actionSystem.ActionManager;
28  import com.intellij.openapi.actionSystem.ActionGroup;
29  import com.intellij.openapi.actionSystem.ActionToolbar;
30  import com.intellij.ui.HyperlinkLabel;
31  import com.intellij.util.ui.UIUtil;
32  import org.jdom.Element;
33  import org.jetbrains.annotations.NonNls;
34  import org.jetbrains.annotations.NotNull;
35  import org.jetbrains.annotations.Nullable;
36  
37  import javax.swing.*;
38  import javax.swing.border.Border;
39  import javax.swing.event.HyperlinkEvent;
40  import javax.swing.event.HyperlinkListener;
41  import java.awt.*;
42  import java.awt.event.MouseAdapter;
43  import java.awt.event.MouseEvent;
44  import java.beans.PropertyChangeListener;
45  import java.util.List;
46  import java.util.ArrayList;
47  import java.util.HashMap;
48  import java.util.concurrent.FutureTask;
49  
50  public class ThePluginJIRAEditorComponent implements ApplicationComponent, FileEditorProvider {
51  
52  	@NonNls
53  	@NotNull
54  	public String getComponentName() {
55  		return "ThePluginJIRAEditorComponent";
56  	}
57  
58  	public void initComponent() {
59  	}
60  
61  	public void disposeComponent() {
62  	}
63  
64  	public boolean accept(@NotNull Project project, @NotNull VirtualFile virtualFile) {
65  		boolean shouldIAccept = true;
66          // todo: probably not too pretty and there IS a possibility
67          // that some other custom editor will intercept our JIRA "file"
68          // - as it now has no extension. Is there a better way to do this?
69          if (!(virtualFile instanceof MemoryVirtualFile)) {
70              shouldIAccept = false;
71          }
72          return shouldIAccept;
73  	}
74  
75  	@NotNull
76  	public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile virtualFile) {
77  		String issueFromFileName = virtualFile.getNameWithoutExtension();
78  		JIRAIssue issue = IdeaHelper.getJIRAToolWindowPanel(project).getCurrentIssue();
79  		if (issueFromFileName.equals(issue.getKey())) {
80  			return new JIRAFileEditor(issue);
81  		}
82  		return new JIRAFileEditor();
83  
84  	}
85  
86  	public void disposeEditor(@NotNull FileEditor fileEditor) {
87  	}
88  
89  	@NotNull
90  	public FileEditorState readState(@NotNull Element element, @NotNull Project project, @NotNull VirtualFile virtualFile) {
91  		return DummyFileEditorState.DUMMY;
92  	}
93  
94  	public void writeState(@NotNull FileEditorState fileEditorState, @NotNull Project project, @NotNull Element element) {
95  	}
96  
97  	@NotNull
98  	@NonNls
99  	public String getEditorTypeId() {
100 		return getComponentName();
101 	}
102 
103 	@NotNull
104 	public FileEditorPolicy getPolicy() {
105 		return FileEditorPolicy.HIDE_DEFAULT_EDITOR;
106 	}
107 
108 	private static class DummyFileEditorState implements FileEditorState {
109 		public static final FileEditorState DUMMY = new DummyFileEditorState();
110 
111 		public boolean canBeMergedWith(FileEditorState otherState, FileEditorStateLevel level) {
112 			return false;
113 		}
114 	}
115 
116 	private class ScrollablePanel extends JPanel implements Scrollable {
117 		private static final int A_LOT = 100000;
118 
119 		// cheating obviously but this seems to do the right thing, so whatever :)
120 		public Dimension getPreferredScrollableViewportSize() {
121 			return new Dimension(1, A_LOT);
122 		}
123 
124 		public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
125 			return 1;
126 		}
127 
128 		public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
129 			return 1;
130 		}
131 
132 		public boolean getScrollableTracksViewportWidth() {
133 			return true;
134 		}
135 
136 		public boolean getScrollableTracksViewportHeight() {
137 			return false;
138 		}
139 	}
140 
141 	private class CommentsPanel extends JPanel {
142 
143 		private ScrollablePanel comments = new ScrollablePanel();
144         private JScrollPane scroll = new JScrollPane();
145         private List<CommentPanel> commentList = new ArrayList<CommentPanel>();
146 
147 		private Border border = BorderFactory.createTitledBorder("Comments");
148 
149 		public CommentsPanel(JIRAIssue issue) {
150 			setBorder(border);
151 			setLayout(new GridBagLayout());
152 			GridBagConstraints gbc = new GridBagConstraints();
153             gbc.gridx = 0;
154             gbc.gridy = 0;
155 			gbc.fill = GridBagConstraints.NONE;
156 
157             gbc.gridy = 1;
158             gbc.gridwidth = 2;
159             gbc.fill = GridBagConstraints.HORIZONTAL;
160 
161 			ActionManager manager = ActionManager.getInstance();
162 			ActionGroup group = (ActionGroup) manager.getAction("ThePlugin.JIRA.CommentsToolBar");
163 			ActionToolbar toolbar = manager.createActionToolbar(issue.getKey(), group, true);
164 
165             JComponent comp = toolbar.getComponent();
166             add(comp, gbc);
167 
168 			gbc.gridx = 0;
169             gbc.gridy = 2;
170             gbc.gridwidth = 2;
171             gbc.insets = new Insets(0, 0, 0, 0);
172 			gbc.fill = GridBagConstraints.BOTH;
173 			gbc.weightx = 1.0;
174 			gbc.weighty = 1.0;
175             comments.setLayout(new VerticalFlowLayout());
176 			scroll.setViewportView(comments);
177 			scroll.getViewport().setOpaque(false);
178 			scroll.setOpaque(false);
179 			scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
180 			scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
181 			scroll.setBorder(BorderFactory.createEmptyBorder());
182             add(scroll, gbc);
183         }
184 
185         public void setTitle(String title) {
186 			border = BorderFactory.createTitledBorder(title);
187 			setBorder(border);
188         }
189 
190         public void addComment(JIRAIssue issue, JIRAComment c, JIRAServer server) {
191             CommentPanel p = new CommentPanel(issue, c, server);
192             comments.add(p);
193             commentList.add(p);
194 		}
195 
196         public void clearComments() {
197             commentList.clear();
198             comments.removeAll();
199         }
200 
201         public void setAllVisible(boolean visible) {
202             for (CommentPanel c : commentList) {
203                 c.getShowHideButton().setState(visible);
204             }
205         }
206 
207        public void scrollToFirst() {
208             SwingUtilities.invokeLater(new Runnable() {
209                 public void run() {
210                     scroll.getVerticalScrollBar().setValue(0);
211                 }
212             });
213         }
214     }
215 
216      private abstract class AbstractShowHideButton extends JLabel {
217 
218         private Icon right = IconLoader.findIcon("/icons/navigate_right_10.gif");
219         private Icon down = IconLoader.findIcon("/icons/navigate_down_10.gif");
220         private boolean shown = true;
221 
222         public AbstractShowHideButton() {
223             setHorizontalAlignment(0);
224             setIcon(down);
225             setToolTipText(getTooltip());
226             addMouseListener(new MouseAdapter() {
227                 public void mouseClicked(MouseEvent e) {
228                     click();
229                 }
230             });
231         }
232 
233         public void setState(boolean visible) {
234             shown = visible;
235             setIcon(shown ?  down : right);
236             setComponentVisible(shown);
237         }
238         public void click() {
239             shown = !shown;
240             setState(shown);
241         }
242 
243         protected abstract void setComponentVisible(boolean visible);
244         protected abstract String getTooltip();
245     }
246 
247     private class ShowHideButton extends AbstractShowHideButton {
248         private JComponent body;
249         private JComponent container;
250 
251         public ShowHideButton(JComponent body, JComponent container) {
252             this.body = body;
253             this.container = container;
254         }
255 
256         protected void setComponentVisible(boolean visible) {
257             body.setVisible(visible);
258             container.validate();
259             container.getParent().validate();
260         }
261 
262         protected String getTooltip() {
263             return "Collapse/Expand";
264         }
265     }
266 
267     private class UserLabel extends HyperlinkLabel {
268 		UserLabel(final String serverUrl, final String userNameId) {
269 			super(userNameId, UIUtil.getTableSelectionForeground(),
270 					UIUtil.getTableSelectionBackground(), UIUtil.getTableSelectionForeground());
271 			addListener(serverUrl, userNameId);
272 		}
273 
274 		UserLabel(final String serverUrl, final String userName, final String userNameId) {
275             super(userName);
276 			addListener(serverUrl, userNameId);
277 		}
278 
279 		private void addListener(final String serverUrl, final String userNameId) {
280 			addHyperlinkListener(new HyperlinkListener() {
281                     public void hyperlinkUpdate(HyperlinkEvent e) {
282                         BrowserUtil.launchBrowser(
283                                 serverUrl
284                                 + "/secure/ViewProfile.jspa?name="
285                                 + userNameId);
286                     }
287             });
288 		}
289 	}
290 
291 	private class BoldLabel extends JLabel {
292 		public BoldLabel(String text) {
293 			super(text);
294 			setFont(getFont().deriveFont(Font.BOLD));
295 		}
296 
297 		public BoldLabel() {
298 			this("");
299 		}
300 	}
301 
302 	private class WhiteLabel extends BoldLabel {
303 		public WhiteLabel() {
304 			super();
305 			setForeground(UIUtil.getTableSelectionForeground());
306 		}
307 	}
308 	
309 	private class CommentPanel extends JPanel {
310 
311 		private ShowHideButton btnShowHide;
312 
313 		public CommentPanel(final JIRAIssue issue, final JIRAComment comment, final JIRAServer server) {
314 			setOpaque(true);
315 			setBackground(UIUtil.getTableSelectionBackground());
316 			
317 			setLayout(new GridBagLayout());
318 			GridBagConstraints gbc;
319 			gbc = new GridBagConstraints();
320 
321 			JEditorPane commentBody = new JEditorPane();
322             btnShowHide = new ShowHideButton(commentBody, this);
323 			gbc.gridx = 1;
324 			gbc.gridy = 0;
325 			gbc.anchor = GridBagConstraints.WEST;
326 			add(btnShowHide, gbc);
327 
328 			gbc.gridx = 2;
329 			gbc.gridy = 0;
330 			gbc.anchor = GridBagConstraints.WEST;
331 			gbc.insets = new Insets(0, Constants.DIALOG_MARGIN, 0, 0);
332 			UserLabel ul = new UserLabel(server.getServer().getUrlString(), comment.getAuthor());
333 			add(ul, gbc);
334 
335 			final JLabel hyphen = new WhiteLabel();
336             hyphen.setText("-");
337 			gbc = new GridBagConstraints();
338 			gbc.gridx = 3;
339 			gbc.gridy = 0;
340 			gbc.anchor = GridBagConstraints.WEST;
341 			gbc.insets = new Insets(0, Constants.DIALOG_MARGIN, 0, Constants.DIALOG_MARGIN);
342 			add(hyphen, gbc);
343 
344 			final JLabel creationDate = new WhiteLabel();
345 			creationDate.setText(comment.getCreationDate().getTime().toString());
346 			gbc = new GridBagConstraints();
347 			gbc.gridx = 4;
348 			gbc.gridy = 0;
349 			gbc.anchor = GridBagConstraints.WEST;
350 			gbc.weightx = 1.0;
351 			gbc.fill = GridBagConstraints.BOTH;
352 			add(creationDate, gbc);
353 
354 			if (StackTraceDetector.containsStackTrace(comment.getBody())) {
355 				HyperlinkLabel analyze = new HyperlinkLabel("Analyse stack trace", UIUtil.getTableSelectionForeground(),
356 					UIUtil.getTableSelectionBackground(), UIUtil.getTableSelectionForeground());
357 				analyze.addHyperlinkListener(new HyperlinkListener() {
358 					public void hyperlinkUpdate(HyperlinkEvent e) {
359 						StackTraceConsole.getInstance().print(issue,
360 								"comment: " + comment.getAuthor() + " - " + creationDate.getText(), comment.getBody());
361 					}
362 				});
363 				gbc.gridx++;
364 				gbc.gridy = 0;
365 				gbc.weightx = 0.0;
366 				gbc.anchor = GridBagConstraints.EAST;
367 				gbc.insets = new Insets(0, Constants.DIALOG_MARGIN, 0, 0);
368 				add(analyze, gbc);
369 			}
370 
371 			commentBody.setEditable(false);
372 			commentBody.setOpaque(true);
373 			commentBody.setBackground(UIUtil.getPanelBackground());
374 			commentBody.setMargin(new Insets(0, Constants.DIALOG_MARGIN, 0, 0));
375 			commentBody.setContentType("text/html");
376 			commentBody.setText("<html><head></head><body>" + comment.getBody() + "</body></html>");
377 			gbc = new GridBagConstraints();
378 			gbc.gridx = 0;
379 			gbc.gridy = 1;
380 			gbc.gridwidth = 6;
381 			gbc.weightx = 1.0;
382 			gbc.weighty = 1.0;
383             gbc.insets = new Insets(0, 0, 0, 0);
384             gbc.fill = GridBagConstraints.BOTH;
385 			add(commentBody, gbc);
386 		}
387 
388         public AbstractShowHideButton getShowHideButton() {
389             return btnShowHide;
390         }
391     }
392 
393     private class DescriptionPanel extends JPanel {
394 		public DescriptionPanel(final JIRAIssue issue) {
395 			setLayout(new GridBagLayout());
396 			GridBagConstraints gbc = new GridBagConstraints();
397 
398 			gbc.gridx = 0;
399 			gbc.gridy = 0;
400 
401 			gbc.insets = new Insets(0, 0, 0, 0);
402 			gbc.fill = GridBagConstraints.BOTH;
403 			gbc.weightx = 1.0;
404 			gbc.weighty = 1.0;
405 
406 			JEditorPane	body = new JEditorPane();
407 			JScrollPane sp = new JScrollPane(body,
408 						ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
409 						ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
410 			sp.setBorder(BorderFactory.createEmptyBorder());
411 			sp.setOpaque(false);
412 			body.setEditable(false);
413 			body.addHyperlinkListener(new HyperlinkListener() {
414 				public void hyperlinkUpdate(HyperlinkEvent e) {
415 					if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
416 						BrowserUtil.launchBrowser(e.getURL().toString());
417 					}
418 				}
419 			});
420 
421 			body.setOpaque(false);
422             body.setBorder(BorderFactory.createEmptyBorder());
423             body.setContentType("text/html");
424             body.setText("<html><head></head><body>" + issue.getDescription() + "</body></html>");
425 			sp.getViewport().setOpaque(false);
426 			body.setCaretPosition(0);
427 			add(sp, gbc);
428 
429 			Border b = BorderFactory.createTitledBorder("Description");
430 			setBorder(b);
431 			Insets i = b.getBorderInsets(this);
432 			int minHeight = i.top + i.bottom;
433 			setMinimumSize(new Dimension(0, minHeight));
434 		}
435 	}
436 
437 	private class DetailsPanel extends JPanel {
438 
439 		private JLabel affectsVersions = new JLabel("Fetching...");
440 		private JLabel fixVersions = new JLabel("Fetching...");
441 		private JLabel components = new JLabel("Fetching...");
442 
443 		public DetailsPanel(final JIRAIssue issue) {
444 			JPanel body = new JPanel();
445 
446 			setLayout(new GridBagLayout());
447 			body.setLayout(new GridBagLayout());
448 
449 			GridBagConstraints gbc1 = new GridBagConstraints();
450 			GridBagConstraints gbc2 = new GridBagConstraints();
451 			gbc1.anchor = GridBagConstraints.FIRST_LINE_START;
452 			gbc1.insets = new Insets(0, Constants.DIALOG_MARGIN, 0, Constants.DIALOG_MARGIN);
453 			gbc2.anchor = GridBagConstraints.FIRST_LINE_START;
454 			gbc2.fill = GridBagConstraints.HORIZONTAL;
455 			gbc2.weightx = 1.0;
456 			gbc1.gridx = 0;
457 			gbc2.gridx = 1;
458 			gbc1.gridy = 0;
459 			gbc2.gridy = 0;
460 
461             body.add(new BoldLabel("Type"), gbc1);
462             body.add(new JLabel(issue.getType(), CachedIconLoader.getIcon(issue.getTypeIconUrl()),
463                     SwingConstants.LEFT), gbc2);
464             gbc1.gridy++;
465             gbc2.gridy++;
466 			body.add(new BoldLabel("Status"), gbc1);
467 			body.add(new JLabel(issue.getStatus(), CachedIconLoader.getIcon(issue.getStatusTypeUrl()),
468                     SwingConstants.LEFT), gbc2);
469 			gbc1.gridy++;
470 			gbc2.gridy++;
471             body.add(new BoldLabel("Priority"), gbc1);
472             body.add(new JLabel(issue.getPriority(), CachedIconLoader.getIcon(issue.getPriorityIconUrl()),
473                     SwingConstants.LEFT), gbc2);
474             gbc1.gridy++;
475             gbc2.gridy++;
476 			body.add(new BoldLabel("Assignee"), gbc1);
477             body.add(new UserLabel(issue.getServerUrl(), issue.getAssignee(), issue.getAssigneeId()), gbc2);
478 			gbc1.gridy++;
479 			gbc2.gridy++;
480             body.add(new BoldLabel("Reporter"), gbc1);
481             body.add(new UserLabel(issue.getServerUrl(), issue.getReporter(), issue.getReporterId()), gbc2);
482             gbc1.gridy++;
483             gbc2.gridy++;
484 			body.add(new BoldLabel("Resolution"), gbc1);
485 			body.add(new JLabel(issue.getResolution()), gbc2);
486 			gbc1.gridy++;
487 			gbc2.gridy++;
488 			body.add(new BoldLabel("Created"), gbc1);
489 			body.add(new JLabel(issue.getCreated()), gbc2);
490             gbc1.gridy++;
491             gbc2.gridy++;
492             body.add(new BoldLabel("Updated"), gbc1);
493             body.add(new JLabel(issue.getUpdated()), gbc2);
494 			gbc1.gridy++;
495 			gbc2.gridy++;
496 			body.add(new BoldLabel("Affects Version/s"), gbc1);
497 			body.add(affectsVersions, gbc2);
498 			gbc1.gridy++;
499 			gbc2.gridy++;
500 			body.add(new BoldLabel("Fix Version/s"), gbc1);
501 			body.add(fixVersions, gbc2);
502 			gbc1.gridy++;
503 			gbc2.gridy++;
504 			body.add(new BoldLabel("Component/s"), gbc1);
505 			body.add(components, gbc2);
506 
507 			gbc1.gridy++;
508 			gbc1.weighty = 1.0;
509 			gbc1.fill = GridBagConstraints.VERTICAL;
510 			body.add(new JPanel(), gbc1);
511 
512 			GridBagConstraints gbc = new GridBagConstraints();
513 			gbc.gridx = 0;
514 			gbc.gridy = 0;
515 			gbc.weightx = 1.0;
516 			gbc.weighty = 1.0;
517 			gbc.fill = GridBagConstraints.BOTH;
518 			JScrollPane scroll = new JScrollPane(body);
519 			scroll.setBorder(BorderFactory.createEmptyBorder());
520 			add(scroll, gbc);
521 
522 			Border b = BorderFactory.createTitledBorder("Details");
523 			setBorder(b);
524 			Insets i = b.getBorderInsets(this);
525 			setMinimumSize(new Dimension(0, i.top + i.bottom));
526 		}
527 
528 		public JLabel getAffectVersionsLabel() {
529 			return affectsVersions;
530 		}
531 
532 		public JLabel getFixVersionsLabel() {
533 			return fixVersions;
534 		}
535 
536 		public JLabel getComponentsLabel() {
537 			return components;
538 		}
539 	}
540 
541 	private class SummaryPanel extends JPanel {
542 
543 		private DetailsPanel details;
544 
545 		public SummaryPanel(final JIRAIssue issue) {
546 			setLayout(new GridBagLayout());
547 			GridBagConstraints gbc = new GridBagConstraints();
548 
549 			gbc.gridx = 0;
550 			gbc.gridy = 0;
551 			gbc.fill = GridBagConstraints.HORIZONTAL;
552 			
553 			ActionManager manager = ActionManager.getInstance();
554 			ActionGroup group = (ActionGroup) manager.getAction("ThePlugin.JIRA.EditorToolBar");
555 			ActionToolbar toolbar = manager.createActionToolbar(issue.getKey(), group, true);
556 
557             JComponent comp = toolbar.getComponent();
558             add(comp, gbc);
559 
560             gbc.gridy = 1;
561             gbc.gridx = 0;
562             gbc.anchor = GridBagConstraints.LINE_START;
563             gbc.fill = GridBagConstraints.BOTH;
564 			gbc.weightx = 1.0;
565 			JEditorPane summary = new JEditorPane();
566             summary.setContentType("text/html");
567 			Color bg = UIUtil.getTableSelectionBackground();
568 			Color fg = UIUtil.getTableSelectionForeground();
569 			String bgColor = String.format("#%1$2X%2$2X%3$2X", bg.getRed(), bg.getGreen(), bg.getBlue());
570 			String fgColor = String.format("#%1$2X%2$2X%3$2X", fg.getRed(), fg.getGreen(), fg.getBlue());
571 			String txt = "<html><body bgcolor=" + bgColor + " color=" + fgColor
572 					+ "><font size=\"+1\"><a href=\"" + issue.getIssueUrl() + "\">"
573 					+ issue.getKey() + "</a> " + issue.getSummary() + "</font></body></html>";
574 			summary.setText(txt);
575             summary.setEditable(false);
576             summary.setFont(summary.getFont().deriveFont(Font.BOLD));
577             summary.setBackground(bg);
578             summary.setOpaque(true);
579 			JPanel p = new JPanel();
580 			p.setLayout(new GridBagLayout());
581 			p.setBorder(BorderFactory.createLineBorder(bg, 6));
582 			GridBagConstraints gbcp = new GridBagConstraints();
583 			gbcp.fill = GridBagConstraints.BOTH;
584 			gbcp.weightx = 1.0;
585 			gbcp.weighty = 1.0;
586 			gbcp.gridx = 0;
587 			gbcp.gridy = 0;
588 			p.add(summary, gbcp);
589 			add(p, gbc);
590 
591 			gbc.gridx = 0;
592             gbc.gridy = 2;
593             gbc.gridwidth = 2;
594             gbc.fill = GridBagConstraints.BOTH;
595 			gbc.weightx = 1.0;
596 			gbc.weighty = 1.0;
597 			Splitter split = new Splitter(true);
598 			split.setFirstComponent(new DescriptionPanel(issue));
599 			details = new DetailsPanel(issue);
600 			split.setSecondComponent(details);
601 			split.setShowDividerControls(true);
602 			split.setHonorComponentsMinimumSize(true);
603 			add(split, gbc);
604 			if (issue.getDescription().length() == 0) {
605 				split.setProportion(0);
606 			}
607         }
608 
609 		public void setAffectsVersions(String[] versions) {
610 			setLabelText(details.getAffectVersionsLabel(), versions);
611 		}
612 
613 		public void setFixVersions(String[] versions) {
614 			setLabelText(details.getFixVersionsLabel(), versions);
615 		}
616 
617 		public void setComponents(String[] components) {
618 			setLabelText(details.getComponentsLabel(), components);
619 		}
620 
621 		private void setLabelText(JLabel label, String[] texts) {
622 			if (texts.length == 0) {
623 				label.setText("None");
624 			} else {
625 
626 				StringBuffer txt = new StringBuffer();
627 				for (int i = 0; i < texts.length; ++i) {
628 					if (i > 0) {
629 						txt.append(", ");
630 					}
631 					txt.append(texts[i]);
632 				}
633 				label.setText(txt.toString());
634 			}
635 		}
636 	}
637 
638     public class JIRAFileEditor implements FileEditor {
639 
640 		private final JIRAServerFacade facade;
641 		private final JIRAServer server;
642 
643 		private JPanel mainPanel;
644 		private JIRAIssue issue;
645         private CommentsPanel commentsPanel;
646 		private SummaryPanel summaryPanel;
647 		private boolean hasStackTrace;
648 
649 		public JIRAFileEditor() {
650 			mainPanel = new JPanel();
651 			mainPanel.setBackground(Color.RED);
652 			// todo: fix this
653 			mainPanel.add(new JLabel("Can't view issue, something is wrong"));
654 			facade = null;
655 			server = null;
656 		}
657 
658 		public JIRAFileEditor(JIRAIssue issue) {
659 			this.issue = issue;
660 			facade = JIRAServerFacadeImpl.getInstance();
661 			server = IdeaHelper.getCurrentJIRAServer();
662             editorMap.put(issue.getKey(), this);
663 
664 			hasStackTrace = StackTraceDetector.containsStackTrace(Html2text.translate(issue.getDescription()));
665 
666 			setupUI();
667 		}
668 
669 		private void setupUI() {
670 			mainPanel = new JPanel();
671             mainPanel.setLayout(new GridBagLayout());
672             final GridBagConstraints gbc = new GridBagConstraints();
673 
674 			gbc.gridx = 0;
675 			gbc.gridy = 0;
676 
677 			gbc.insets = new Insets(0, 0, 0, 0);
678 			gbc.fill = GridBagConstraints.BOTH;
679 			gbc.weightx = 1.0;
680 			gbc.weighty = 1.0;
681 			Splitter split = new Splitter(true);
682 			split.setShowDividerControls(true);
683 			split.setHonorComponentsMinimumSize(true);
684 			summaryPanel = new SummaryPanel(issue);
685 			split.setFirstComponent(summaryPanel);
686             commentsPanel = new CommentsPanel(issue);
687 			split.setSecondComponent(commentsPanel);
688 			mainPanel.add(split, gbc);
689 			getMoreIssueDetails();
690 			refreshComments();
691 		}
692 
693 		// the real reason I am introducing this task is for IDEA not to color all my bodies of future tasks yellow :P
694 		private class FutureTaskNoResult extends FutureTask {
695 			public FutureTaskNoResult(Runnable r) {
696 				super(r, null);
697 			}
698 		}
699 
700 		private String[] getStringArray(List<JIRAConstant> l) {
701 			List<String> sl = new ArrayList<String>(l.size());
702 			for (JIRAConstant c : l) {
703 				 sl.add(c.getName());
704 			}
705 			return sl.toArray(new String[l.size()]);
706 		}
707 
708 		public void addComment() {
709             final IssueComment issueComment = new IssueComment(issue.getKey());
710             issueComment.show();
711             if (issueComment.isOK()) {
712 				FutureTask task = new FutureTaskNoResult(new Runnable() {
713 					public void run() {
714                         try {
715                             facade.addComment(server.getServer(), issue, issueComment.getComment());
716                             refreshComments();
717                         } catch (JIRAException e) {
718                             final String msg = e.getMessage();
719                             SwingUtilities.invokeLater(new Runnable() {
720                                 public void run() {
721                                     Messages.showMessageDialog(
722                                             "Failed to add comment to issue " + issue.getKey() + ": " + msg,
723                                             "Error", Messages.getErrorIcon());
724                                 }
725                             });
726                         }
727                     }
728                 });
729                 new Thread(task, "atlassian-idea-plugin comment issue from editor").start();
730             }
731         }
732 
733 		public synchronized void getMoreIssueDetails() {
734 			if ((issue.getAffectsVersions() == null)
735 					|| (issue.getFixVersions() == null)
736 					|| (issue.getComponents() == null)) {
737 
738 				FutureTask task = new FutureTaskNoResult(new Runnable() {
739 					private String[] errorString = null;
740 
741 					public void run() {
742 
743 						try {
744 							final JIRAIssue issueDetails = facade.getIssueDetails(server.getServer(), issue);
745 							issue.setAffectsVersions(issueDetails.getAffectsVersions());
746 							issue.setFixVersions(issueDetails.getFixVersions());
747 							issue.setComponents(issueDetails.getComponents());
748 						} catch (JIRAException e) {
749 							errorString = new String[] { "Cannot retrieve: " + e.getMessage() };
750 						}
751 						SwingUtilities.invokeLater(new Runnable() {
752 							public void run() {
753 								if (errorString == null) {
754 									summaryPanel.setAffectsVersions(getStringArray(issue.getAffectsVersions()));
755 									summaryPanel.setFixVersions(getStringArray(issue.getFixVersions()));
756 									summaryPanel.setComponents(getStringArray(issue.getComponents()));
757 								} else {
758 									summaryPanel.setAffectsVersions(errorString);
759 									summaryPanel.setFixVersions(errorString);
760 									summaryPanel.setComponents(errorString);
761 								}
762 							}
763 						});
764 					}
765 				});
766 				new Thread(task, "atlassian-idea-plugin get issue details").start();
767 			} else {
768 				summaryPanel.setAffectsVersions(getStringArray(issue.getAffectsVersions()));
769 				summaryPanel.setFixVersions(getStringArray(issue.getFixVersions()));
770 				summaryPanel.setComponents(getStringArray(issue.getComponents()));
771 			}
772 		}
773 
774 		public void refreshComments() {
775             commentsPanel.clearComments();
776             commentsPanel.setTitle("Fetching comments...");
777             FutureTask task = new FutureTaskNoResult(new Runnable() {
778                 public void run() {
779                     try {
780                         final List<JIRAComment> comments = facade.getComments(server.getServer(), issue);
781                         SwingUtilities.invokeLater(new Runnable() {
782                             public void run() {
783                                 int size = comments.size();
784                                 if (size > 0) {
785                                     commentsPanel.setTitle("Comments (" + comments.size() + ")");
786                                     for (JIRAComment c : comments) {
787                                         commentsPanel.addComment(issue, c, server);
788 									}
789                                     commentsPanel.validate();
790                                     commentsPanel.scrollToFirst();
791                                 } else {
792                                     commentsPanel.setTitle("No comments");
793                                 }
794                             }
795                         });
796                     } catch (JIRAException e) {
797                         commentsPanel.setTitle("Cannot fetch comments: " + e.getMessage());
798                     }
799                 }
800             });
801             new Thread(task, "atlassian-idea-plugin refresh comments").start();
802         }
803 
804 		public JIRAIssue getIssue() {
805 			return issue;
806 		}
807 
808 		public boolean hasStackTraceInDescription() {
809 			return hasStackTrace;
810 		}
811 
812 		public void analyzeDescriptionStackTrace() {
813 			StackTraceConsole.getInstance().print(issue, "description",	Html2text.translate(issue.getDescription()));
814 		}
815 
816 		public void setCommentsExpanded(boolean expanded) {
817             commentsPanel.setAllVisible(expanded);
818         }
819         
820         @NotNull
821 		public JComponent getComponent() {
822 			return mainPanel;
823 		}
824 
825 		@Nullable
826 		public JComponent getPreferredFocusedComponent() {
827 			return mainPanel;
828 		}
829 
830 		@NonNls
831 		@NotNull
832 		public String getName() {
833 			return "JIRA Issue View";
834 		}
835 
836 		@NotNull
837 		public FileEditorState getState(@NotNull FileEditorStateLevel fileEditorStateLevel) {
838 			return DummyFileEditorState.DUMMY;
839 		}
840 
841 		public void setState(@NotNull FileEditorState fileEditorState) {
842 		}
843 
844 		public boolean isModified() {
845 			return false;  
846 		}
847 
848 		public boolean isValid() {
849 			return true;
850 		}
851 
852 		public void selectNotify() {
853 		}
854 
855 		public void deselectNotify() {
856 		}
857 
858 		public void addPropertyChangeListener(@NotNull PropertyChangeListener propertyChangeListener) {
859 		}
860 
861 		public void removePropertyChangeListener(@NotNull PropertyChangeListener propertyChangeListener) {
862 		}
863 
864 		@Nullable
865 		public BackgroundEditorHighlighter getBackgroundHighlighter() {
866 			return null;
867 		}
868 
869 		@Nullable
870 		public FileEditorLocation getCurrentLocation() {
871 			return null;
872 		}
873 
874 		@Nullable
875 		public StructureViewBuilder getStructureViewBuilder() {
876 			return null;
877 		}
878 
879 		public <T> T getUserData(Key<T> tKey) {
880 			return null;
881 		}
882 
883 		public <T> void putUserData(Key<T> tKey, T t) {
884 		}
885 
886 		public void dispose() {
887             editorMap.remove(issue.getKey());
888         }
889 	}
890 
891     private static HashMap<String, JIRAFileEditor> editorMap = new HashMap<String, JIRAFileEditor>();
892 
893     public static JIRAFileEditor getEditorByKey(String key) {
894         if (editorMap.containsKey(key)) {
895             return editorMap.get(key);
896         }
897         return null;
898     }
899 }