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 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());
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
);
89 public void addNotify() {
91 getManager().addListener(this);
95 public void removeNotify() {
96 getManager().removeListener(this);
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();
118 public void dispose() {
119 getManager().markRead();
124 private static NotificationsManagerImpl
getManager() {
125 return NotificationsManagerImpl
.getNotificationsManagerImpl();
128 static Dimension
getMinSize() {
129 final Dimension size
= Toolkit
.getDefaultToolkit().getScreenSize();
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() {
149 public void onClosed(LightweightWindowEvent event
) {
150 Disposer
.dispose(panel
);
154 popup
.showInCenterOf(SwingUtilities
.getRootPane(parent
));
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));
172 myIconLabel
= new JLabel();
173 myIconLabel
.setBorder(BorderFactory
.createEmptyBorder(3, 3, 3, 3));
174 myIconLabel
.setOpaque(false);
176 myText
= new JTextPane() {
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() {
199 public void setCursorHandler(Processor
<Cursor
> proc
) {
203 public void resetCursorHandler() {
207 public void onCursorChanged(Cursor cursor
) {
208 if (myProc
!= null) myProc
.process(cursor
);
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);
225 g2d
.setColor(UIUtil
.getListSelectionBackground());
226 g2d
.fillRect(0, 0, bounds
.width
, bounds
.height
);
229 g2d
.setColor(Color
.WHITE
);
233 Color bgColor
= getBackground();
235 bgColor
= new Color(bgColor
.getRed(), bgColor
.getGreen(), bgColor
.getBlue(), 60);
238 g2d
.setColor(bgColor
);
241 g2d
.setColor(myHasFocus
|| mySelected ?
getBackground().darker().darker() : myWasRead ?
getBackground() : getBackground().darker());
245 super.paintComponent(g
);
248 public Component
getListCellRendererComponent(final JList list
,
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
));
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
) {
279 myRebuildFunction
= new NotNullFunction
<Project
, Collection
<Notification
>>() {
281 public Collection
<Notification
> fun(Project project
) {
282 return getManager().getByType(myType
, project
);
286 getManager().addListener(this);
290 public void dispose() {
291 getManager().removeListener(this);
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
) {
314 public void notificationsAdded(@NotNull Notification
... notification
) {
318 public void notificationsRemoved(@NotNull Notification
... notification
) {
322 public void notificationsRead(@NotNull Notification
... notification
) {
327 private static class ItemsList
extends JList
{
328 private ItemsList(final NotificationsListModel model
) {
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
) {
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() {
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());
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
);
375 final JTextPane text
= ((NotificationsListRenderer
)renderer
).getText();
377 Processor
<Cursor
> processor
;
378 HyperlinkListener listener
= null;
380 listener
= NotificationsUtil
.wrapListener(notification
);
381 if (listener
!= null) text
.addHyperlinkListener(listener
);
384 processor
= new Processor
<Cursor
>() {
385 public boolean process(Cursor cursor
) {
386 ItemsList
.this.setCursor(cursor
);
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(),
404 text
.dispatchEvent(newEvent
);
406 ((NotificationsListRenderer
)renderer
).resetCursorHandler();
407 if (listener
!= null) {
408 text
.removeHyperlinkListener(listener
);
415 @SuppressWarnings({"ConstantConditions"})
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();
445 model
.setSelectionInterval(toSelect
, toSelect
);
446 scrollRectToVisible(getCellBounds(toSelect
, toSelect
));
455 private static void createFilterButton(final JPanel parent
,
456 final ButtonGroup group
,
458 final ActionListener listener
,
459 final NotNullFunction
<MyButton
, String
> titleCallback
,
461 final boolean active
) {
462 final StickyButton b
= new MyButton(title
, listener
) {
464 public void updateTitle() {
465 setText(titleCallback
.fun(this));
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
) {
497 }, new NotNullFunction
<MyButton
, String
>() {
499 public String
fun(MyButton myButton
) {
500 final int i
= count(null, project
);
502 return String
.format("All (%s)", i
);
509 createFilterButton(box
, buttonGroup
, "Error", new ActionListener() {
510 public void actionPerformed(final ActionEvent e
) {
511 list
.filter(NotificationType
.ERROR
);
513 }, new NotNullFunction
<MyButton
, String
>() {
515 public String
fun(MyButton myButton
) {
516 final int i
= count(NotificationType
.ERROR
, project
);
517 myButton
.setVisible(i
> 0);
519 return String
.format("Error (%s)", i
);
520 } else if (myButton
.isSelected()) {
521 switchToAll(buttonGroup
);
528 createFilterButton(box
, buttonGroup
, "Warning", new ActionListener() {
529 public void actionPerformed(final ActionEvent e
) {
530 list
.filter(NotificationType
.WARNING
);
532 }, new NotNullFunction
<MyButton
, String
>() {
534 public String
fun(MyButton myButton
) {
535 final int i
= count(NotificationType
.WARNING
, project
);
536 myButton
.setVisible(i
> 0);
538 return String
.format("Warning (%s)", i
);
539 } else if (myButton
.isSelected()) {
540 switchToAll(buttonGroup
);
547 createFilterButton(box
, buttonGroup
, "Information", new ActionListener() {
548 public void actionPerformed(final ActionEvent e
) {
549 list
.filter(NotificationType
.INFORMATION
);
551 }, new NotNullFunction
<MyButton
, String
>() {
553 public String
fun(MyButton myButton
) {
554 final int i
= count(NotificationType
.INFORMATION
, project
);
555 myButton
.setVisible(i
> 0);
557 return String
.format("Information (%s)", i
);
558 } else if (myButton
.isSelected()) {
559 switchToAll(buttonGroup
);
562 return "Information";
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")) {
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()) {
621 public void requestFocus() {
622 updateButtons(buttonBar
);
627 panel
.add(buttonBar
, BorderLayout
.NORTH
);
628 panel
.add(scrollPane
, BorderLayout
.CENTER
);
634 private abstract static class MyButton
extends StickyButton
{
635 private MyButton(String text
, ActionListener listener
) {
636 super(text
, listener
);
639 private MyButton(String text
) {
643 public abstract void updateTitle();