View Javadoc

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