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
;
37 import javax
.swing
.event
.HyperlinkListener
;
38 import javax
.swing
.event
.ListDataListener
;
39 import javax
.swing
.event
.ListDataEvent
;
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
;
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 WeakReference
<JBPopup
> myPopupRef
;
58 private Project myProject
;
59 private Wrapper myWrapper
;
60 private JComponent myActiveComponent
;
62 private JComponent myEmptyComponent
;
63 private JComponent myListComponent
;
65 public NotificationsListPanel(@Nullable final Project project
) {
66 setLayout(new BorderLayout());
69 myEmptyComponent
= new JLabel("No new notifications.", JLabel
.CENTER
);
70 myListComponent
= ItemsList
.create(project
, this);
72 myWrapper
= new Wrapper();
73 myWrapper
.setContent(getCurrentComponent(project
));
75 add(myWrapper
, BorderLayout
.CENTER
);
78 public void notificationsAdded(@NotNull Notification
... notification
) {
79 switchView(myProject
);
82 public void notificationsRemoved(@NotNull Notification
... notification
) {
83 switchView(myProject
);
86 public void notificationsRead(@NotNull Notification
... notification
) {
87 switchView(myProject
);
91 public void addNotify() {
93 getManager().addListener(this);
97 public void removeNotify() {
98 getManager().removeListener(this);
102 private JComponent
getCurrentComponent(@Nullable final Project project
) {
103 final boolean empty
= getManager().count(project
) == 0;
104 final JComponent component
= empty ? myEmptyComponent
: myListComponent
;
105 if (myActiveComponent
== component
) return null;
107 myActiveComponent
= component
;
108 return myActiveComponent
;
111 protected void switchView(@Nullable final Project project
) {
112 final JComponent component
= getCurrentComponent(project
);
113 if (component
!= null) {
114 myWrapper
.setContent(component
);
115 myWrapper
.revalidate();
120 public void dispose() {
121 getManager().markRead();
126 private static NotificationsManagerImpl
getManager() {
127 return NotificationsManagerImpl
.getNotificationsManagerImpl();
130 static Dimension
getMinSize() {
131 final Dimension size
= Toolkit
.getDefaultToolkit().getScreenSize();
137 public void clear() {
138 if (myPopupRef
!= null) {
139 final JBPopup jbPopup
= myPopupRef
.get();
140 if (jbPopup
!= null) {
148 public JComponent
getPreferredFocusedComponent() {
149 return myWrapper
.getTargetComponent();
152 public static JBPopup
show(@Nullable final Project project
, @NotNull final JComponent parent
) {
153 final NotificationsListPanel panel
= new NotificationsListPanel(project
);
154 final ComponentPopupBuilder builder
=
155 JBPopupFactory
.getInstance().createComponentPopupBuilder(panel
, panel
.getPreferredFocusedComponent());
156 final JBPopup popup
= builder
.setResizable(true).setMinSize(getMinSize()).setDimensionServiceKey(null, "NotificationsPopup", true)
157 .setCancelOnClickOutside(false).setBelongsToGlobalPopupStack(false).setCancelButton(new MinimizeButton("Hide")).setMovable(true)
158 .setRequestFocus(true).setTitle("Notifications").createPopup();
160 popup
.addListener(new JBPopupListener
.Adapter() {
162 public void onClosed(LightweightWindowEvent event
) {
163 Disposer
.dispose(panel
);
167 popup
.showInCenterOf(SwingUtilities
.getRootPane(parent
));
171 private static class NotificationsListRenderer
extends JComponent
implements ListCellRenderer
{
172 private JTextPane myText
;
173 private boolean mySelected
;
174 private boolean myHasFocus
;
175 private JLabel myIconLabel
;
176 private Processor
<Cursor
> myProc
;
177 private boolean myWasRead
;
179 private NotificationsListRenderer() {
180 setLayout(new BorderLayout());
181 setBorder(BorderFactory
.createEmptyBorder(2, 4, 2, 4));
185 myIconLabel
= new JLabel();
186 myIconLabel
.setBorder(BorderFactory
.createEmptyBorder(3, 3, 3, 3));
187 myIconLabel
.setOpaque(false);
189 myText
= new JTextPane() {
191 public void setCursor(Cursor cursor
) {
192 super.setCursor(cursor
);
193 onCursorChanged(cursor
);
197 myText
.setBorder(BorderFactory
.createEmptyBorder(3, 0, 3, 0));
198 myText
.setOpaque(false);
199 myText
.setEditable(false);
200 myText
.setEditorKit(UIUtil
.getHTMLEditorKit());
202 final Wrapper
.North comp
= new Wrapper
.North(myIconLabel
);
203 comp
.setOpaque(false);
204 add(comp
, BorderLayout
.WEST
);
205 add(myText
, BorderLayout
.CENTER
);
208 public JTextPane
getText() {
212 public void setCursorHandler(Processor
<Cursor
> proc
) {
216 public void resetCursorHandler() {
220 public void onCursorChanged(Cursor cursor
) {
221 if (myProc
!= null) myProc
.process(cursor
);
225 protected void paintComponent(Graphics g
) {
226 final Graphics2D g2d
= (Graphics2D
)g
;
228 final Rectangle bounds
= getBounds();
229 final Insets insets
= getInsets();
231 final GraphicsConfig cfg
= new GraphicsConfig(g
);
232 cfg
.setAntialiasing(true);
234 final Shape shape
= new RoundRectangle2D
.Double(insets
.left
, insets
.top
, bounds
.width
- 1 - insets
.left
- insets
.right
,
235 bounds
.height
- 1 - insets
.top
- insets
.bottom
, 6, 6);
238 g2d
.setColor(UIUtil
.getListSelectionBackground());
239 g2d
.fillRect(0, 0, bounds
.width
, bounds
.height
);
242 g2d
.setColor(Color
.WHITE
);
246 Color bgColor
= getBackground();
248 bgColor
= new Color(bgColor
.getRed(), bgColor
.getGreen(), bgColor
.getBlue(), 60);
251 g2d
.setColor(bgColor
);
254 g2d
.setColor(myHasFocus
|| mySelected ?
getBackground().darker().darker() : myWasRead ?
getBackground() : getBackground().darker());
258 super.paintComponent(g
);
261 public Component
getListCellRendererComponent(final JList list
,
264 final boolean isSelected
,
265 final boolean cellHasFocus
) {
266 LOG
.assertTrue(value
instanceof Notification
);
267 final Notification notification
= (Notification
)value
;
269 mySelected
= isSelected
;
270 myHasFocus
= cellHasFocus
;
272 myText
.setText(NotificationsUtil
.buildHtml(notification
));
273 myIconLabel
.setIcon(NotificationsUtil
.getIcon(notification
));
274 myWasRead
= NotificationsManagerImpl
.getNotificationsManagerImpl().wasRead(notification
);
276 setBackground(NotificationsUtil
.getBackground(notification
));
282 private static class NotificationsListModel
extends AbstractListModel
implements NotificationModelListener
, Disposable
{
283 private List
<Notification
> myNotifications
= new ArrayList
<Notification
>();
284 private NotificationType myType
;
285 private Project myProject
;
286 private NotNullFunction
<Project
, Collection
<Notification
>> myRebuildFunction
;
287 private boolean myArchive
;
289 private NotificationsListModel(@Nullable Project project
) {
292 myRebuildFunction
= new NotNullFunction
<Project
, Collection
<Notification
>>() {
294 public Collection
<Notification
> fun(Project project
) {
295 return getManager().getByType(myType
, project
);
299 getManager().addListener(this);
303 public void dispose() {
304 getManager().removeListener(this);
308 public int getSize() {
309 return myNotifications
.size();
312 public Object
getElementAt(final int index
) {
313 return myNotifications
.get(index
);
316 private void rebuildList() {
317 myNotifications
.clear();
318 myNotifications
.addAll(myRebuildFunction
.fun(myProject
));
319 fireContentsChanged(this, 0, myNotifications
.size() - 1);
322 public void filter(final NotificationType type
) {
327 public void notificationsAdded(@NotNull Notification
... notification
) {
331 public void notificationsRemoved(@NotNull Notification
... notification
) {
335 public void notificationsRead(@NotNull Notification
... notification
) {
340 private static class ItemsList
extends JList
{
341 private ItemsList(final NotificationsListModel model
) {
345 setCellRenderer(new NotificationsListRenderer());
346 getSelectionModel().setSelectionInterval(0, 0);
348 getInputMap(WHEN_FOCUSED
).put(KeyStroke
.getKeyStroke(KeyEvent
.VK_DELETE
, 0), REMOVE_KEY
);
349 getActionMap().put(REMOVE_KEY
, new AbstractAction() {
350 public void actionPerformed(final ActionEvent e
) {
355 setBackground(UIUtil
.getPanelBackgound());
357 addMouseMotionListener(new MouseMotionListener() {
358 public void mouseMoved(MouseEvent e
) {
359 processMouse(e
, false);
362 public void mouseDragged(MouseEvent e
) {
366 addMouseListener(new MouseAdapter() {
368 public void mouseClicked(final MouseEvent e
) {
369 if (!e
.isPopupTrigger()) {
370 processMouse(e
, true);
376 private void processMouse(final MouseEvent e
, final boolean click
) {
377 final int index
= locationToIndex(e
.getPoint());
379 final Object value
= getModel().getElementAt(index
);
380 if (value
!= null && value
instanceof Notification
) {
381 final Notification notification
= (Notification
)value
;
382 final Component renderer
= getCellRenderer().getListCellRendererComponent(this, value
, index
, false, false);
383 if (renderer
instanceof NotificationsListRenderer
) {
384 final Rectangle bounds
= getCellBounds(index
, index
);
385 renderer
.setBounds(bounds
);
388 final JTextPane text
= ((NotificationsListRenderer
)renderer
).getText();
390 Processor
<Cursor
> processor
;
391 HyperlinkListener listener
= null;
393 listener
= NotificationsUtil
.wrapListener(notification
);
394 if (listener
!= null) text
.addHyperlinkListener(listener
);
397 processor
= new Processor
<Cursor
>() {
398 public boolean process(Cursor cursor
) {
399 ItemsList
.this.setCursor(cursor
);
404 ((NotificationsListRenderer
)renderer
).setCursorHandler(processor
);
407 final Point point
= e
.getPoint();
408 point
.translate(-bounds
.x
, -bounds
.y
);
410 final Rectangle r
= text
.getBounds();
411 point
.translate(-r
.x
, -r
.y
);
413 final MouseEvent newEvent
=
414 new MouseEvent(text
, e
.getID(), e
.getWhen(), e
.getModifiers(), point
.x
, point
.y
, e
.getClickCount(), e
.isPopupTrigger(),
417 text
.dispatchEvent(newEvent
);
419 ((NotificationsListRenderer
)renderer
).resetCursorHandler();
420 if (listener
!= null) {
421 text
.removeHyperlinkListener(listener
);
428 @SuppressWarnings({"ConstantConditions"})
430 public NotificationsListModel
getModel() {
431 final ListModel listModel
= super.getModel();
432 return listModel
instanceof NotificationsListModel ?
(NotificationsListModel
)listModel
: null;
435 public void removeSelected() {
436 final ListSelectionModel model
= getSelectionModel();
437 final NotificationsListModel listModel
= getModel();
438 if (!model
.isSelectionEmpty()) {
439 final int min
= model
.getMinSelectionIndex();
440 final int max
= model
.getMaxSelectionIndex();
442 final List
<Notification
> tbr
= new ArrayList
<Notification
>();
443 for (int i
= min
; i
<= max
; i
++) {
444 if (model
.isSelectedIndex(i
)) {
445 final Notification notification
= (Notification
)listModel
.getElementAt(i
);
446 if (notification
!= null) {
447 tbr
.add(notification
);
452 if (tbr
.size() > 0) {
453 getManager().remove(tbr
.toArray(new Notification
[tbr
.size()]));
455 final int toSelect
= Math
.min(min
, listModel
.getSize() - 1);
456 model
.clearSelection();
458 model
.setSelectionInterval(toSelect
, toSelect
);
459 scrollRectToVisible(getCellBounds(toSelect
, toSelect
));
468 private static void createFilterButton(final JPanel parent
,
469 final ButtonGroup group
,
471 final ActionListener listener
,
472 final NotNullFunction
<MyButton
, String
> titleCallback
,
474 final boolean active
) {
475 final StickyButton b
= new MyButton(title
, listener
) {
477 public void updateTitle() {
478 setText(titleCallback
.fun(this));
485 b
.setFocusable(false);
486 b
.setSelected(active
);
487 b
.setMnemonic(mnemonic
);
490 private static void updateButtons(@NotNull final JComponent filterBar
) {
491 final Component
[] components
= filterBar
.getComponents();
492 for (final Component c
: components
) {
493 if (c
instanceof MyButton
) {
494 ((MyButton
)c
).updateTitle();
499 private static JComponent
buildFilterBar(final ItemsList list
, final Project project
) {
500 final JPanel box
= new JPanel();
501 box
.setBorder(BorderFactory
.createEmptyBorder(3, 3, 3, 3));
502 box
.setLayout(new BoxLayout(box
, BoxLayout
.X_AXIS
));
504 final ButtonGroup buttonGroup
= new ButtonGroup();
506 createFilterButton(box
, buttonGroup
, "All", new ActionListener() {
507 public void actionPerformed(final ActionEvent e
) {
510 }, new NotNullFunction
<MyButton
, String
>() {
512 public String
fun(MyButton myButton
) {
513 final int i
= count(null, project
);
515 return String
.format("All (%s)", i
);
522 createFilterButton(box
, buttonGroup
, "Error", new ActionListener() {
523 public void actionPerformed(final ActionEvent e
) {
524 list
.filter(NotificationType
.ERROR
);
526 }, new NotNullFunction
<MyButton
, String
>() {
528 public String
fun(MyButton myButton
) {
529 final int i
= count(NotificationType
.ERROR
, project
);
530 myButton
.setVisible(i
> 0);
532 return String
.format("Error (%s)", i
);
533 } else if (myButton
.isSelected()) {
534 switchToAll(buttonGroup
);
541 createFilterButton(box
, buttonGroup
, "Warning", new ActionListener() {
542 public void actionPerformed(final ActionEvent e
) {
543 list
.filter(NotificationType
.WARNING
);
545 }, new NotNullFunction
<MyButton
, String
>() {
547 public String
fun(MyButton myButton
) {
548 final int i
= count(NotificationType
.WARNING
, project
);
549 myButton
.setVisible(i
> 0);
551 return String
.format("Warning (%s)", i
);
552 } else if (myButton
.isSelected()) {
553 switchToAll(buttonGroup
);
560 createFilterButton(box
, buttonGroup
, "Information", new ActionListener() {
561 public void actionPerformed(final ActionEvent e
) {
562 list
.filter(NotificationType
.INFORMATION
);
564 }, new NotNullFunction
<MyButton
, String
>() {
566 public String
fun(MyButton myButton
) {
567 final int i
= count(NotificationType
.INFORMATION
, project
);
568 myButton
.setVisible(i
> 0);
570 return String
.format("Information (%s)", i
);
571 } else if (myButton
.isSelected()) {
572 switchToAll(buttonGroup
);
575 return "Information";
582 private static void switchToAll(final ButtonGroup buttonGroup
) {
583 final Enumeration
<AbstractButton
> enumeration
= buttonGroup
.getElements();
584 while (enumeration
.hasMoreElements()) {
585 final AbstractButton button
= enumeration
.nextElement();
586 if (button
.getText().startsWith("All")) {
593 private static int count(@Nullable final NotificationType type
, @Nullable final Project project
) {
594 return NotificationsManagerImpl
.getNotificationsManagerImpl().getByType(type
, project
).size();
597 private void filter(@Nullable final NotificationType type
) {
598 final NotificationsListModel listModel
= getModel();
599 listModel
.filter(type
);
600 if (listModel
.getSize() > 0) setSelectedIndex(0);
603 public static JComponent
create(final Project project
, final Disposable parentDisposable
) {
604 final NotificationsListModel model
= new NotificationsListModel(project
);
605 Disposer
.register(parentDisposable
, model
);
607 // TODO: switch filter if removed all of the notifications from current one!
609 final ItemsList list
= new ItemsList(model
);
610 final JScrollPane scrollPane
= new JScrollPane(list
);
611 scrollPane
.setHorizontalScrollBarPolicy(JScrollPane
.HORIZONTAL_SCROLLBAR_NEVER
);
612 scrollPane
.setVerticalScrollBarPolicy(JScrollPane
.VERTICAL_SCROLLBAR_AS_NEEDED
);
614 scrollPane
.setBorder(null);
615 scrollPane
.getViewport().setBackground(UIUtil
.getPanelBackgound());
617 final JComponent buttonBar
= buildFilterBar(list
, project
);
618 model
.addListDataListener(new ListDataListener() {
619 public void intervalAdded(ListDataEvent e
) {
620 updateButtons(buttonBar
);
623 public void intervalRemoved(ListDataEvent e
) {
624 updateButtons(buttonBar
);
627 public void contentsChanged(ListDataEvent e
) {
628 updateButtons(buttonBar
);
632 final JPanel panel
= new JPanel(new BorderLayout()) {
634 public void requestFocus() {
635 updateButtons(buttonBar
);
640 panel
.add(buttonBar
, BorderLayout
.NORTH
);
641 panel
.add(scrollPane
, BorderLayout
.CENTER
);
647 private abstract static class MyButton
extends StickyButton
{
648 private MyButton(String text
, ActionListener listener
) {
649 super(text
, listener
);
652 private MyButton(String text
) {
656 public abstract void updateTitle();