sticky documentation popup [take 1]
[fedora-idea.git] / platform / platform-impl / src / com / intellij / notification / impl / ui / NotificationsListPanel.java
blob3f6cfa46f54b08a0f662210e43276be3fbc44e73
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.notification.impl.ui;
18 import com.intellij.notification.Notification;
19 import com.intellij.notification.NotificationType;
20 import com.intellij.notification.impl.NotificationModelListener;
21 import com.intellij.notification.impl.NotificationsManagerImpl;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.popup.*;
26 import com.intellij.openapi.ui.popup.util.MinimizeButton;
27 import com.intellij.openapi.util.Disposer;
28 import com.intellij.openapi.wm.impl.content.GraphicsConfig;
29 import com.intellij.ui.components.panels.Wrapper;
30 import com.intellij.util.NotNullFunction;
31 import com.intellij.util.Processor;
32 import com.intellij.util.ui.UIUtil;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
36 import javax.swing.*;
37 import javax.swing.event.HyperlinkListener;
38 import javax.swing.event.ListDataListener;
39 import javax.swing.event.ListDataEvent;
40 import java.awt.*;
41 import java.awt.event.*;
42 import java.awt.geom.RoundRectangle2D;
43 import java.lang.ref.WeakReference;
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.List;
47 import java.util.Enumeration;
49 /**
50 * @author spleaner
52 public class NotificationsListPanel extends JPanel implements NotificationModelListener, Disposable {
53 private static final Logger LOG = Logger.getInstance("#com.intellij.notification.impl.ui.NotificationsListPanel");
54 private static final String REMOVE_KEY = "REMOVE";
56 private Project myProject;
57 private Wrapper myWrapper;
58 private JComponent myActiveComponent;
60 private JComponent myEmptyComponent;
61 private JComponent myListComponent;
63 public NotificationsListPanel(@Nullable final Project project) {
64 setLayout(new BorderLayout());
65 myProject = project;
67 myEmptyComponent = new JLabel("No new notifications.", JLabel.CENTER);
68 myListComponent = ItemsList.create(project, this);
70 myWrapper = new Wrapper();
71 myWrapper.setContent(getCurrentComponent(project));
73 add(myWrapper, BorderLayout.CENTER);
76 public void notificationsAdded(@NotNull Notification... notification) {
77 switchView(myProject);
80 public void notificationsRemoved(@NotNull Notification... notification) {
81 switchView(myProject);
84 public void notificationsRead(@NotNull Notification... notification) {
85 switchView(myProject);
88 @Override
89 public void addNotify() {
90 super.addNotify();
91 getManager().addListener(this);
94 @Override
95 public void removeNotify() {
96 getManager().removeListener(this);
97 super.removeNotify();
100 private JComponent getCurrentComponent(@Nullable final Project project) {
101 final boolean empty = getManager().count(project) == 0;
102 final JComponent component = empty ? myEmptyComponent : myListComponent;
103 if (myActiveComponent == component) return null;
105 myActiveComponent = component;
106 return myActiveComponent;
109 protected void switchView(@Nullable final Project project) {
110 final JComponent component = getCurrentComponent(project);
111 if (component != null) {
112 myWrapper.setContent(component);
113 myWrapper.revalidate();
114 myWrapper.repaint();
118 public void dispose() {
119 getManager().markRead();
121 myProject = null;
124 private static NotificationsManagerImpl getManager() {
125 return NotificationsManagerImpl.getNotificationsManagerImpl();
128 static Dimension getMinSize() {
129 final Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
130 size.width *= 0.1d;
131 size.height *= 0.1d;
132 return size;
135 public JComponent getPreferredFocusedComponent() {
136 return myWrapper.getTargetComponent();
139 public static JBPopup show(@Nullable final Project project, @NotNull final JComponent parent) {
140 final NotificationsListPanel panel = new NotificationsListPanel(project);
141 final ComponentPopupBuilder builder =
142 JBPopupFactory.getInstance().createComponentPopupBuilder(panel, panel.getPreferredFocusedComponent());
143 final JBPopup popup = builder.setResizable(true).setMinSize(getMinSize()).setDimensionServiceKey(null, "NotificationsPopup", true)
144 .setCancelOnClickOutside(false).setBelongsToGlobalPopupStack(false).setCancelButton(new MinimizeButton("Hide")).setMovable(true)
145 .setRequestFocus(true).setTitle("Notifications").createPopup();
147 popup.addListener(new JBPopupListener.Adapter() {
148 @Override
149 public void onClosed(LightweightWindowEvent event) {
150 Disposer.dispose(panel);
154 popup.showInCenterOf(SwingUtilities.getRootPane(parent));
155 return popup;
158 private static class NotificationsListRenderer extends JComponent implements ListCellRenderer {
159 private JTextPane myText;
160 private boolean mySelected;
161 private boolean myHasFocus;
162 private JLabel myIconLabel;
163 private Processor<Cursor> myProc;
164 private boolean myWasRead;
166 private NotificationsListRenderer() {
167 setLayout(new BorderLayout());
168 setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));
170 setOpaque(false);
172 myIconLabel = new JLabel();
173 myIconLabel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
174 myIconLabel.setOpaque(false);
176 myText = new JTextPane() {
177 @Override
178 public void setCursor(Cursor cursor) {
179 super.setCursor(cursor);
180 onCursorChanged(cursor);
184 myText.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 0));
185 myText.setOpaque(false);
186 myText.setEditable(false);
187 myText.setEditorKit(UIUtil.getHTMLEditorKit());
189 final Wrapper.North comp = new Wrapper.North(myIconLabel);
190 comp.setOpaque(false);
191 add(comp, BorderLayout.WEST);
192 add(myText, BorderLayout.CENTER);
195 public JTextPane getText() {
196 return myText;
199 public void setCursorHandler(Processor<Cursor> proc) {
200 myProc = proc;
203 public void resetCursorHandler() {
204 myProc = null;
207 public void onCursorChanged(Cursor cursor) {
208 if (myProc != null) myProc.process(cursor);
211 @Override
212 protected void paintComponent(Graphics g) {
213 final Graphics2D g2d = (Graphics2D)g;
215 final Rectangle bounds = getBounds();
216 final Insets insets = getInsets();
218 final GraphicsConfig cfg = new GraphicsConfig(g);
219 cfg.setAntialiasing(true);
221 final Shape shape = new RoundRectangle2D.Double(insets.left, insets.top, bounds.width - 1 - insets.left - insets.right,
222 bounds.height - 1 - insets.top - insets.bottom, 6, 6);
224 if (mySelected) {
225 g2d.setColor(UIUtil.getListSelectionBackground());
226 g2d.fillRect(0, 0, bounds.width, bounds.height);
229 g2d.setColor(Color.WHITE);
230 g2d.fill(shape);
233 Color bgColor = getBackground();
234 if (myWasRead) {
235 bgColor = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 60);
238 g2d.setColor(bgColor);
239 g2d.fill(shape);
241 g2d.setColor(myHasFocus || mySelected ? getBackground().darker().darker() : myWasRead ? getBackground() : getBackground().darker());
242 g2d.draw(shape);
243 cfg.restore();
245 super.paintComponent(g);
248 public Component getListCellRendererComponent(final JList list,
249 final Object value,
250 final int index,
251 final boolean isSelected,
252 final boolean cellHasFocus) {
253 LOG.assertTrue(value instanceof Notification);
254 final Notification notification = (Notification)value;
256 mySelected = isSelected;
257 myHasFocus = cellHasFocus;
259 myText.setText(NotificationsUtil.buildHtml(notification));
260 myIconLabel.setIcon(NotificationsUtil.getIcon(notification));
261 myWasRead = NotificationsManagerImpl.getNotificationsManagerImpl().wasRead(notification);
263 setBackground(NotificationsUtil.getBackground(notification));
265 return this;
269 private static class NotificationsListModel extends AbstractListModel implements NotificationModelListener, Disposable {
270 private List<Notification> myNotifications = new ArrayList<Notification>();
271 private NotificationType myType;
272 private Project myProject;
273 private NotNullFunction<Project, Collection<Notification>> myRebuildFunction;
274 private boolean myArchive;
276 private NotificationsListModel(@Nullable Project project) {
277 myProject = project;
279 myRebuildFunction = new NotNullFunction<Project, Collection<Notification>>() {
280 @NotNull
281 public Collection<Notification> fun(Project project) {
282 return getManager().getByType(myType, project);
286 getManager().addListener(this);
287 rebuildList();
290 public void dispose() {
291 getManager().removeListener(this);
292 myProject = null;
295 public int getSize() {
296 return myNotifications.size();
299 public Object getElementAt(final int index) {
300 return myNotifications.get(index);
303 private void rebuildList() {
304 myNotifications.clear();
305 myNotifications.addAll(myRebuildFunction.fun(myProject));
306 fireContentsChanged(this, 0, myNotifications.size() - 1);
309 public void filter(final NotificationType type) {
310 myType = type;
311 rebuildList();
314 public void notificationsAdded(@NotNull Notification... notification) {
315 rebuildList();
318 public void notificationsRemoved(@NotNull Notification... notification) {
319 rebuildList();
322 public void notificationsRead(@NotNull Notification... notification) {
323 rebuildList();
327 private static class ItemsList extends JList {
328 private ItemsList(final NotificationsListModel model) {
329 super(model);
330 setOpaque(false);
332 setCellRenderer(new NotificationsListRenderer());
333 getSelectionModel().setSelectionInterval(0, 0);
335 getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), REMOVE_KEY);
336 getActionMap().put(REMOVE_KEY, new AbstractAction() {
337 public void actionPerformed(final ActionEvent e) {
338 removeSelected();
342 setBackground(UIUtil.getPanelBackgound());
344 addMouseMotionListener(new MouseMotionListener() {
345 public void mouseMoved(MouseEvent e) {
346 processMouse(e, false);
349 public void mouseDragged(MouseEvent e) {
353 addMouseListener(new MouseAdapter() {
354 @Override
355 public void mouseClicked(final MouseEvent e) {
356 if (!e.isPopupTrigger()) {
357 processMouse(e, true);
363 private void processMouse(final MouseEvent e, final boolean click) {
364 final int index = locationToIndex(e.getPoint());
365 if (index > -1) {
366 final Object value = getModel().getElementAt(index);
367 if (value != null && value instanceof Notification) {
368 final Notification notification = (Notification)value;
369 final Component renderer = getCellRenderer().getListCellRendererComponent(this, value, index, false, false);
370 if (renderer instanceof NotificationsListRenderer) {
371 final Rectangle bounds = getCellBounds(index, index);
372 renderer.setBounds(bounds);
373 renderer.doLayout();
375 final JTextPane text = ((NotificationsListRenderer)renderer).getText();
377 Processor<Cursor> processor;
378 HyperlinkListener listener = null;
379 if (click) {
380 listener = NotificationsUtil.wrapListener(notification);
381 if (listener != null) text.addHyperlinkListener(listener);
383 else {
384 processor = new Processor<Cursor>() {
385 public boolean process(Cursor cursor) {
386 ItemsList.this.setCursor(cursor);
387 return true;
391 ((NotificationsListRenderer)renderer).setCursorHandler(processor);
394 final Point point = e.getPoint();
395 point.translate(-bounds.x, -bounds.y);
397 final Rectangle r = text.getBounds();
398 point.translate(-r.x, -r.y);
400 final MouseEvent newEvent =
401 new MouseEvent(text, e.getID(), e.getWhen(), e.getModifiers(), point.x, point.y, e.getClickCount(), e.isPopupTrigger(),
402 e.getButton());
404 text.dispatchEvent(newEvent);
406 ((NotificationsListRenderer)renderer).resetCursorHandler();
407 if (listener != null) {
408 text.removeHyperlinkListener(listener);
415 @SuppressWarnings({"ConstantConditions"})
416 @Override
417 public NotificationsListModel getModel() {
418 final ListModel listModel = super.getModel();
419 return listModel instanceof NotificationsListModel ? (NotificationsListModel)listModel : null;
422 public void removeSelected() {
423 final ListSelectionModel model = getSelectionModel();
424 final NotificationsListModel listModel = getModel();
425 if (!model.isSelectionEmpty()) {
426 final int min = model.getMinSelectionIndex();
427 final int max = model.getMaxSelectionIndex();
429 final List<Notification> tbr = new ArrayList<Notification>();
430 for (int i = min; i <= max; i++) {
431 if (model.isSelectedIndex(i)) {
432 final Notification notification = (Notification)listModel.getElementAt(i);
433 if (notification != null) {
434 tbr.add(notification);
439 if (tbr.size() > 0) {
440 getManager().remove(tbr.toArray(new Notification[tbr.size()]));
442 final int toSelect = Math.min(min, listModel.getSize() - 1);
443 model.clearSelection();
444 if (toSelect >= 0) {
445 model.setSelectionInterval(toSelect, toSelect);
446 scrollRectToVisible(getCellBounds(toSelect, toSelect));
451 revalidate();
452 repaint();
455 private static void createFilterButton(final JPanel parent,
456 final ButtonGroup group,
457 final String title,
458 final ActionListener listener,
459 final NotNullFunction<MyButton, String> titleCallback,
460 final char mnemonic,
461 final boolean active) {
462 final StickyButton b = new MyButton(title, listener) {
463 @Override
464 public void updateTitle() {
465 setText(titleCallback.fun(this));
469 parent.add(b);
470 group.add(b);
472 b.setFocusable(false);
473 b.setSelected(active);
474 b.setMnemonic(mnemonic);
477 private static void updateButtons(@NotNull final JComponent filterBar) {
478 final Component[] components = filterBar.getComponents();
479 for (final Component c : components) {
480 if (c instanceof MyButton) {
481 ((MyButton)c).updateTitle();
486 private static JComponent buildFilterBar(final ItemsList list, final Project project) {
487 final JPanel box = new JPanel();
488 box.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
489 box.setLayout(new BoxLayout(box, BoxLayout.X_AXIS));
491 final ButtonGroup buttonGroup = new ButtonGroup();
493 createFilterButton(box, buttonGroup, "All", new ActionListener() {
494 public void actionPerformed(final ActionEvent e) {
495 list.filter(null);
497 }, new NotNullFunction<MyButton, String>() {
498 @NotNull
499 public String fun(MyButton myButton) {
500 final int i = count(null, project);
501 if (i > 0) {
502 return String.format("All (%s)", i);
505 return "All";
507 }, 'A', true);
509 createFilterButton(box, buttonGroup, "Error", new ActionListener() {
510 public void actionPerformed(final ActionEvent e) {
511 list.filter(NotificationType.ERROR);
513 }, new NotNullFunction<MyButton, String>() {
514 @NotNull
515 public String fun(MyButton myButton) {
516 final int i = count(NotificationType.ERROR, project);
517 myButton.setVisible(i > 0);
518 if (i > 0) {
519 return String.format("Error (%s)", i);
520 } else if (myButton.isSelected()) {
521 switchToAll(buttonGroup);
524 return "Error";
526 }, 'E', false);
528 createFilterButton(box, buttonGroup, "Warning", new ActionListener() {
529 public void actionPerformed(final ActionEvent e) {
530 list.filter(NotificationType.WARNING);
532 }, new NotNullFunction<MyButton, String>() {
533 @NotNull
534 public String fun(MyButton myButton) {
535 final int i = count(NotificationType.WARNING, project);
536 myButton.setVisible(i > 0);
537 if (i > 0) {
538 return String.format("Warning (%s)", i);
539 } else if (myButton.isSelected()) {
540 switchToAll(buttonGroup);
543 return "Warning";
545 }, 'W', false);
547 createFilterButton(box, buttonGroup, "Information", new ActionListener() {
548 public void actionPerformed(final ActionEvent e) {
549 list.filter(NotificationType.INFORMATION);
551 }, new NotNullFunction<MyButton, String>() {
552 @NotNull
553 public String fun(MyButton myButton) {
554 final int i = count(NotificationType.INFORMATION, project);
555 myButton.setVisible(i > 0);
556 if (i > 0) {
557 return String.format("Information (%s)", i);
558 } else if (myButton.isSelected()) {
559 switchToAll(buttonGroup);
562 return "Information";
564 }, 'I', false);
566 return box;
569 private static void switchToAll(final ButtonGroup buttonGroup) {
570 final Enumeration<AbstractButton> enumeration = buttonGroup.getElements();
571 while (enumeration.hasMoreElements()) {
572 final AbstractButton button = enumeration.nextElement();
573 if (button.getText().startsWith("All")) {
574 button.doClick();
575 return;
580 private static int count(@Nullable final NotificationType type, @Nullable final Project project) {
581 return NotificationsManagerImpl.getNotificationsManagerImpl().getByType(type, project).size();
584 private void filter(@Nullable final NotificationType type) {
585 final NotificationsListModel listModel = getModel();
586 listModel.filter(type);
587 if (listModel.getSize() > 0) setSelectedIndex(0);
590 public static JComponent create(final Project project, final Disposable parentDisposable) {
591 final NotificationsListModel model = new NotificationsListModel(project);
592 Disposer.register(parentDisposable, model);
594 // TODO: switch filter if removed all of the notifications from current one!
596 final ItemsList list = new ItemsList(model);
597 final JScrollPane scrollPane = new JScrollPane(list);
598 scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
599 scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
601 scrollPane.setBorder(null);
602 scrollPane.getViewport().setBackground(UIUtil.getPanelBackgound());
604 final JComponent buttonBar = buildFilterBar(list, project);
605 model.addListDataListener(new ListDataListener() {
606 public void intervalAdded(ListDataEvent e) {
607 updateButtons(buttonBar);
610 public void intervalRemoved(ListDataEvent e) {
611 updateButtons(buttonBar);
614 public void contentsChanged(ListDataEvent e) {
615 updateButtons(buttonBar);
619 final JPanel panel = new JPanel(new BorderLayout()) {
620 @Override
621 public void requestFocus() {
622 updateButtons(buttonBar);
623 list.requestFocus();
627 panel.add(buttonBar, BorderLayout.NORTH);
628 panel.add(scrollPane, BorderLayout.CENTER);
630 return panel;
634 private abstract static class MyButton extends StickyButton {
635 private MyButton(String text, ActionListener listener) {
636 super(text, listener);
639 private MyButton(String text) {
640 super(text);
643 public abstract void updateTitle();