jalview rss reader based on jswingreader (JAL-943)
[jalview.git] / src / jalview / gui / BlogReader.java
1 package jalview.gui;
2
3 import jalview.bin.Cache;
4 import jalview.jbgui.GBlogReader;
5
6 import java.awt.BorderLayout;
7 import java.awt.Component;
8 import java.awt.Dimension;
9 import java.awt.Font;
10 import java.awt.Rectangle;
11 import java.awt.event.ActionEvent;
12 import java.awt.event.ActionListener;
13 import java.awt.event.KeyEvent;
14 import java.awt.event.MouseEvent;
15 import java.awt.event.WindowAdapter;
16 import java.awt.event.WindowEvent;
17 import java.beans.PropertyChangeListener;
18 import java.text.DateFormat;
19 import java.text.SimpleDateFormat;
20 import java.util.ArrayList;
21 import java.util.Calendar;
22 import java.util.Collections;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26
27 import javax.swing.AbstractAction;
28 import javax.swing.AbstractButton;
29 import javax.swing.Action;
30 import javax.swing.DefaultListCellRenderer;
31 import javax.swing.DefaultListModel;
32 import javax.swing.Icon;
33 import javax.swing.ImageIcon;
34 import javax.swing.JButton;
35 import javax.swing.JLabel;
36 import javax.swing.JList;
37 import javax.swing.JMenuItem;
38 import javax.swing.JPanel;
39 import javax.swing.JPopupMenu;
40 import javax.swing.JScrollPane;
41 import javax.swing.JSplitPane;
42 import javax.swing.JToolBar;
43 import javax.swing.ListSelectionModel;
44 import javax.swing.SwingUtilities;
45 import javax.swing.event.HyperlinkEvent;
46 import javax.swing.event.HyperlinkListener;
47 import javax.swing.event.ListSelectionEvent;
48 import javax.swing.event.ListSelectionListener;
49 import javax.swing.text.DateFormatter;
50
51 import org.robsite.jswingreader.action.AboutAction;
52 import org.robsite.jswingreader.action.MarkChannelAsRead;
53 import org.robsite.jswingreader.action.MarkChannelAsUnread;
54 import org.robsite.jswingreader.action.MarkItemAsRead;
55 import org.robsite.jswingreader.action.MarkItemAsUnread;
56 import org.robsite.jswingreader.action.RefreshChannelAction;
57 import org.robsite.jswingreader.action.UpdatableAction;
58 import org.robsite.jswingreader.model.Channel;
59 import org.robsite.jswingreader.model.ChannelListModel;
60 import org.robsite.jswingreader.model.Item;
61 import org.robsite.jswingreader.model.SimpleRSSParser;
62 import org.robsite.jswingreader.ui.BlogContentPane;
63 import org.robsite.jswingreader.ui.ItemReadTimer;
64 import org.robsite.jswingreader.ui.Main;
65 import org.robsite.jswingreader.ui.util.ContextMenuMouseAdapter;
66 import org.robsite.jswingreader.util.BrowserUtils;
67
68 import quicktime.std.movies.media.TextDescription;
69
70 /**
71  * Blog reading window, adapted from JSwingReader's
72  * org.robsite.jswingreader.ui.MainWindow class
73  */
74
75 public class BlogReader extends GBlogReader
76 {
77   private JButton buttonAbout = new JButton();
78
79   private JButton buttonRefresh = new JButton();
80
81   private JToolBar toolBar = new JToolBar();
82
83   private JLabel statusBar = new JLabel();
84
85   private JPanel panelMain = new JPanel();
86
87   private BorderLayout layoutMain = new BorderLayout();
88
89   private BorderLayout borderLayout1 = new BorderLayout();
90
91   private JPanel topPanel = new JPanel();
92
93   private JPanel bottomPanel = new JPanel();
94
95   private BorderLayout borderLayout2 = new BorderLayout();
96
97   private BorderLayout borderLayout3 = new BorderLayout();
98
99   private JSplitPane topBottomSplitPane = new JSplitPane();
100
101   private JList listItems = new JList(new DefaultListModel());
102
103   // SWITCH IN JALVIEW HTML VIEWER PANE HERE
104   private BlogContentPane textDescription = new BlogContentPane();
105
106   // ADD IN JALVIEW BANNER FOR PRETTINESS
107   private BorderLayout borderLayout4 = new BorderLayout();
108
109   private BorderLayout borderLayout5 = new BorderLayout();
110
111   private ChannelListModel _channelModel = null;
112
113   private JList listChannels = new JList();
114
115   private Action exitAction = new Action()
116   {
117
118     @Override
119     public void actionPerformed(ActionEvent arg0)
120     {
121       setVisible(false);
122       if (parent != null)
123       {
124         parent.showNews(false);
125       }
126
127     }
128
129     @Override
130     public void setEnabled(boolean arg0)
131     {
132
133     }
134
135     @Override
136     public void removePropertyChangeListener(PropertyChangeListener arg0)
137     {
138       // TODO Auto-generated method stub
139
140     }
141
142     @Override
143     public void putValue(String arg0, Object arg1)
144     {
145       // TODO Auto-generated method stub
146
147     }
148
149     @Override
150     public boolean isEnabled()
151     {
152       // TODO Auto-generated method stub
153       return true;
154     }
155
156     @Override
157     public Object getValue(String arg0)
158     {
159       // TODO Auto-generated method stub
160       return null;
161     }
162
163     @Override
164     public void addPropertyChangeListener(PropertyChangeListener arg0)
165     {
166       // TODO Auto-generated method stub
167
168     }
169   };
170
171   private JLabel lblChannels = new JLabel();
172
173   private List _updatableActions = new ArrayList();
174
175   private ItemReadTimer _itemTimer = null;
176
177   private JPopupMenu _popupItems = null;
178
179   private JPopupMenu _popupChannels = null;
180
181   private String lastm = "";
182
183   private boolean newsnew = false;
184
185   private Desktop parent = null;
186
187   BlogReader()
188   {
189     this(null);
190   }
191
192   // should we ignore fake gui events
193   private boolean updating = false;
194
195   public BlogReader(Desktop desktop)
196   {
197     parent = desktop;
198     if (parent == null)
199     {
200       this.setSize(new Dimension(550, 350));
201     }
202     else
203     {
204       Rectangle bounds = parent
205               .getLastKnownDimensions("JALVIEW_RSS_WINDOW_");
206       if (bounds == null)
207       {
208         setBounds(parent.getX(), parent.getY(), 550, 350);
209       }
210       else
211       {
212         setBounds(bounds.x, bounds.y, bounds.width, bounds.height);
213       }
214     }
215     _channelModel = new ChannelListModel();
216     // Construct our jalview news channel
217     Channel chan = new Channel();
218     chan.setURL(jalview.bin.Cache.getDefault(
219             "JALVIEW_NEWS_RSS",
220             jalview.bin.Cache.getDefault("www.jalview.org",
221                     "http://www.jalview.org") + "/feeds/desktop/rss"));
222     loadLastM();
223     _channelModel.addChannel(chan);
224     updating = true;
225     try
226     {
227       jbInit();
228       postInit();
229     } catch (Exception e)
230     {
231       e.printStackTrace();
232     }
233
234     initItems(chan);
235     updating = false;
236     boolean setvisible = checkForNew(chan, true);
237     if (setvisible)
238     {
239       Cache.log.info("Will show jalview news automatically");
240       SwingUtilities.invokeLater(new Runnable()
241       {
242         @Override
243         public void run()
244         {
245           if (parent != null)
246           {
247             parent.showNews(true);
248           }
249           else
250           {
251             setVisible(true);
252           }
253         }
254       });
255     }
256   }
257
258   /**
259    * update hasnew flag and mark all new messages as unread.
260    */
261   private boolean checkForNew(Channel chan, boolean updateItems)
262   {
263     if (!updating || updateItems)
264     {
265       newsnew = false;
266     }
267     if (chan != null && chan.getItems() != null)
268     {
269       for (Item i : (List<Item>) chan.getItems())
270       {
271         boolean isread = (lastDate != null && i.getPublishDate() != null && !lastDate
272                 .before(i.getPublishDate()));
273
274         if (i.getPublishDate() != null && (!updating || updateItems))
275         {
276           newsnew |= !isread;
277         }
278         if (updateItems)
279         {
280           i.setRead(isread);
281         }
282       }
283     }
284     return newsnew;
285   }
286
287   java.util.Date lastDate = null;
288
289   private void loadLastM()
290   {
291     lastDate = Cache.getDateProperty("JALVIEW_NEWS_RSS_LASTMODIFIED");
292   }
293
294   private void saveLastM(Item item)
295   {
296     if (item != null && item.getPublishDate() != null
297             && (lastDate == null || item.getPublishDate().after(lastDate)))
298     {
299       lastDate = item.getPublishDate();
300       if (lastDate != null)
301       {
302         jalview.bin.Cache.setDateProperty("JALVIEW_NEWS_RSS_LASTMODIFIED",
303                 lastDate);
304         jalview.bin.Cache.log.info("Saved last read date as "
305                 + jalview.bin.Cache.date_format.format(lastDate));
306
307       }
308       if (_channelModel.getElementAt(0) != null)
309       {
310         checkForNew((Channel) _channelModel.getElementAt(0), false);
311       }
312     }
313   }
314
315   private void jbInit() throws Exception
316   {
317     this.setTitle("News from www.jalview.org");
318     this.getContentPane().setLayout(layoutMain);
319     panelMain.setLayout(borderLayout1);
320     topPanel.setLayout(borderLayout5);
321     bottomPanel.setLayout(borderLayout4);
322     topBottomSplitPane.setOrientation(JSplitPane.VERTICAL_SPLIT);
323     topBottomSplitPane.setDividerLocation(100);
324     topBottomSplitPane.setTopComponent(topPanel);
325     topBottomSplitPane.setBottomComponent(bottomPanel);
326     JScrollPane spTextDescription = new JScrollPane(textDescription);
327     textDescription.setText("");
328     statusBar.setText(" [Status] ");
329     buttonRefresh.addActionListener(new ActionListener()
330     {
331       
332       @Override
333       public void actionPerformed(ActionEvent e)
334       {
335         refreshNews();
336       }
337     });
338     this.getContentPane().add(statusBar, BorderLayout.SOUTH);
339     toolBar.add(buttonRefresh);
340     toolBar.addSeparator();
341     JLabel about=new JLabel("brought to you by JSwingReader (jswingreader.sourceforge.net)");
342     toolBar.add(about);
343     toolBar.setFloatable(false);
344     this.getContentPane().add(toolBar, BorderLayout.NORTH);
345     panelMain.add(topBottomSplitPane, BorderLayout.CENTER);
346     this.getContentPane().add(panelMain, BorderLayout.CENTER);
347     JScrollPane spListItems = new JScrollPane(listItems);
348     listItems
349             .setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
350     topPanel.add(spListItems, BorderLayout.CENTER);
351     bottomPanel.add(spTextDescription, BorderLayout.CENTER);
352     listChannels.setModel(_channelModel);
353
354     listItems.addMouseListener(new java.awt.event.MouseAdapter()
355     {
356       public void mouseClicked(MouseEvent e)
357       {
358         listItems_mouseClicked(e);
359       }
360     });
361     _popupItems = _buildItemsPopupMenu();
362     _popupChannels = _buildChannelsPopupMenu();
363     ContextMenuMouseAdapter popupAdapter = new ContextMenuMouseAdapter(
364             _popupItems);
365     ContextMenuMouseAdapter popupChannelsAdapter = new ContextMenuMouseAdapter(
366             _popupChannels);
367     listItems.addMouseListener(popupAdapter);
368     listItems.setCellRenderer(new ItemsRenderer());
369     lblChannels.setText("Channels");
370   }
371
372   private void postInit()
373   {
374     // clear the default hyperlink listener and replace with our own.
375     for (HyperlinkListener hll : textDescription.getHyperlinkListeners())
376     {
377       textDescription.removeHyperlinkListener(hll);
378     }
379     textDescription.addHyperlinkListener(new HyperlinkListener()
380     {
381       public void hyperlinkUpdate(HyperlinkEvent e)
382       {
383         if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
384         {
385           Desktop.showUrl(e.getURL().toExternalForm());
386         }
387       }
388     });
389
390     this.addWindowListener(new WindowAdapter()
391     {
392       public void windowClosing(WindowEvent e)
393       {
394         ActionEvent actionEvent = new ActionEvent(this,
395                 ActionEvent.ACTION_FIRST, (String) exitAction
396                         .getValue(Action.NAME));
397         exitAction.actionPerformed(actionEvent);
398       }
399
400       public void windowOpened(WindowEvent e)
401       {
402       }
403     });
404
405     listItems.addListSelectionListener(new ListSelectionListener()
406     {
407       public void valueChanged(ListSelectionEvent e)
408       {
409         if (e.getValueIsAdjusting() == false)
410         {
411           _itemsValueChanged(listItems);
412         }
413       }
414     });
415     listChannels.setSelectedIndex(1);
416     _updateAllActions();
417     _updateToolbarButtons();
418
419     _itemTimer = new ItemReadTimer(listChannels, listItems);
420     _itemsValueChanged(listItems);
421   }
422
423   public class LaunchJvBrowserOnItem extends AbstractAction implements
424           UpdatableAction
425   {
426     JList _listItems = null;
427
428     public LaunchJvBrowserOnItem(JList listItems)
429     {
430       super("Open in Browser");
431       this.putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_O));
432       this.putValue(Action.LONG_DESCRIPTION, "Open in Browser");
433       _listItems = listItems;
434     }
435
436     public void actionPerformed(ActionEvent e)
437     {
438       Object o = _listItems.getSelectedValue();
439       if (o instanceof Item)
440       {
441         Item item = (Item) o;
442         item.setRead(true);
443         _listItems.repaint();
444
445         Desktop.showUrl(item.getLink());
446       }
447     }
448
449     public void update(Object o)
450     {
451       setEnabled(true);
452       if (_listItems == null || _listItems.getModel().getSize() == 0)
453       {
454         setEnabled(false);
455       }
456       else if (_listItems.getSelectedIndex() == -1)
457       {
458         setEnabled(false);
459       }
460     }
461
462   }
463
464   private JPopupMenu _buildItemsPopupMenu()
465   {
466     JPopupMenu popup = new JPopupMenu();
467     popup.add(new JMenuItem(new LaunchJvBrowserOnItem(listItems)));
468     popup.addSeparator();
469     popup.add(new JMenuItem(new MarkItemAsRead(listItems)));
470     popup.add(new JMenuItem(new MarkItemAsUnread(listItems)));
471     return popup;
472   }
473
474   private JPopupMenu _buildChannelsPopupMenu()
475   {
476     JPopupMenu popup = new JPopupMenu();
477     popup.add(new JMenuItem(new MarkChannelAsRead(listChannels, listItems)));
478     popup.add(new JMenuItem(
479             new MarkChannelAsUnread(listChannels, listItems)));
480     return popup;
481   }
482
483   private void initItems(Channel channel)
484   {
485     if (channel == null)
486     {
487       channel = new Channel();
488     }
489     if (!channel.isOpen() && channel.getURL() != null)
490     {
491       try
492       {
493         SimpleRSSParser.parse(channel);
494       } catch (Exception ex)
495       {
496         ex.printStackTrace();
497       }
498     }
499     DefaultListModel itemsModel = (DefaultListModel) listItems.getModel();
500     itemsModel.clear();
501     Iterator iter = (channel.getItems() != null) ? channel.getItems()
502             .iterator() : Collections.EMPTY_LIST.iterator();
503     while (iter.hasNext())
504     {
505       itemsModel.addElement(iter.next());
506     }
507     if (itemsModel.getSize() > 0)
508     {
509       listItems.setSelectedIndex(0);
510       _itemsValueChanged(listItems);
511     }
512     setStatusBarText(channel.getURL());
513     _updateAllActions();
514   }
515
516   private void _itemsValueChanged(JList itemList)
517   {
518     Item item = (Item) itemList.getSelectedValue();
519     if (item == null)
520     {
521       if (itemList.getModel().getSize() > 0)
522       {
523         item = (Item) itemList.getModel().getElementAt(0);
524       }
525       if (item == null)
526       {
527         item = new Item();
528       }
529       else
530       {
531         itemList.setSelectedIndex(0);
532       }
533     }
534
535     if (_itemTimer != null)
536     {
537       // prefer a shorter delay than 5s
538       _itemTimer.setDelay(300);
539       _itemTimer.start();
540       _itemTimer.setLastItem(item);
541       final Item lastitem = item;
542       _itemTimer.addActionListener(new ActionListener()
543       {
544
545         @Override
546         public void actionPerformed(ActionEvent e)
547         {
548           saveLastM(lastitem);
549         }
550       });
551     }
552
553     setStatusBarText(item.getLink());
554     textDescription.setBlogText(item);
555     _updateAllActions();
556   }
557
558   public void setStatusBarText(String text)
559   {
560     statusBar.setText(text);
561   }
562
563   private void _updateAllActions()
564   {
565     Iterator iter = _updatableActions.iterator();
566     while (iter.hasNext())
567     {
568       UpdatableAction action = (UpdatableAction) iter.next();
569       action.update(this);
570     }
571   }
572
573   private void _updateToolbarButtons()
574   {
575     Map general = (Map) Main.getPreferences().get("general");
576     if (general == null)
577     {
578       return;
579     }
580
581     Component[] components = toolBar.getComponents();
582     for (int i = 0; i < components.length; i++)
583     {
584       Component component = components[i];
585       if (component instanceof JButton)
586       {
587         JButton button = (JButton) component;
588         if (Boolean.toString(false).equals(general.get("useToolBarText")))
589         {
590           // Remove the text if preferences state no toolbar text
591           button.setText("");
592         }
593         if (Boolean.toString(true).equals(general.get("radioTextBelow")))
594         {
595           button.setVerticalTextPosition(AbstractButton.BOTTOM);
596           button.setHorizontalTextPosition(AbstractButton.CENTER);
597         }
598         else if (Boolean.toString(true).equals(
599                 general.get("radioTextRight")))
600         {
601           button.setVerticalTextPosition(AbstractButton.CENTER);
602           button.setHorizontalTextPosition(AbstractButton.RIGHT);
603         }
604       }
605     }
606   }
607
608   private void listItems_mouseClicked(MouseEvent e)
609   {
610     if (e.getClickCount() == 2 && e.getModifiersEx() == MouseEvent.NOBUTTON)
611     {
612       Item item = (Item) listItems.getSelectedValue();
613       item.setRead(true);
614       saveLastM(item);
615       if (_itemTimer != null)
616       {
617         _itemTimer.stop();
618       }
619
620       Action action = new LaunchJvBrowserOnItem(listItems);
621       ActionEvent event = new ActionEvent(this,
622               ActionEvent.ACTION_PERFORMED, "LaunchBrowserOnItem");
623       action.actionPerformed(event);
624     }
625   }
626
627   /**
628    * force the news panel to refresh
629    */
630   public void refreshNews()
631   {
632     try {
633       initItems((Channel)_channelModel.getElementAt(0));
634       
635     } catch (Exception x)
636     {}
637   }
638
639   public static void main(String args[])
640   {
641     // this tests the detection of new news based on the last read date stored
642     // in jalview properties
643     jalview.bin.Cache.loadProperties(null);
644     jalview.bin.Cache.initLogger();
645     // test will advance read date each time
646     Calendar today = Calendar.getInstance(), lastread = Calendar
647             .getInstance();
648     lastread.set(1983, 01, 01);
649     while (lastread.before(today))
650     {
651       Cache.setDateProperty("JALVIEW_NEWS_RSS_LASTMODIFIED",
652               lastread.getTime());
653       BlogReader me = new BlogReader();
654       System.out.println("Set last date to "
655               + jalview.bin.Cache.date_format.format(lastread.getTime()));
656       if (me.isNewsNew())
657       {
658         Cache.log.info("There is news to read.");
659       }
660       else
661       {
662         Cache.log.info("There is no new news.");
663       }
664       me.setTitle("Testing : Last read is " + me.lastDate);
665       me.setVisible(true);
666       me.toFront();
667       while (me.isVisible())
668       {
669         try
670         {
671           Thread.sleep(100);
672         } catch (InterruptedException x)
673         {
674         }
675         ;
676       }
677       if (me.isNewsNew())
678       {
679         Cache.log.info("Still new news after reader displayed.");
680       }
681       if (lastread.getTime().before(me.lastDate))
682       {
683         Cache.log.info("The news was read.");
684         lastread.setTime(me.lastDate);
685       }
686       else
687       {
688         lastread.add(Calendar.MONTH, 1);
689       }
690       me.dispose();
691     }
692   }
693
694   boolean isNewsNew()
695   {
696     return newsnew;
697   }
698 }
699
700 class ChannelsRenderer extends DefaultListCellRenderer
701 {
702   private final static Icon _icon = new ImageIcon(
703           Main.class.getResource("image/ComposeMail16.gif"));
704
705   public Component getListCellRendererComponent(JList list, Object value,
706           int index, boolean isSelected, boolean cellHasFocus)
707   {
708     JLabel component = (JLabel) super.getListCellRendererComponent(list,
709             value, index, isSelected, cellHasFocus);
710     component.setIcon(ChannelsRenderer._icon);
711     if (value instanceof Channel)
712     {
713       Channel channel = (Channel) value;
714       component.setText(channel.getTitle() + " ("
715               + channel.getUnreadItemCount() + ")");
716       component.setToolTipText(channel.getURL());
717     }
718     return component;
719   }
720 }
721
722 class ItemsRenderer extends DefaultListCellRenderer
723 {
724   private final static Icon _icon = new ImageIcon(
725           Main.class.getResource("image/ComposeMail16.gif"));
726
727   public Component getListCellRendererComponent(JList list, Object value,
728           int index, boolean isSelected, boolean cellHasFocus)
729   {
730     JLabel component = (JLabel) super.getListCellRendererComponent(list,
731             value, index, isSelected, cellHasFocus);
732     component.setIcon(ItemsRenderer._icon);
733     if (value instanceof Item)
734     {
735       Item item = (Item) value;
736       if (item.getPublishDate() != null)
737       {
738         component.setText(DateFormat.getDateInstance().format(
739                 item.getPublishDate())
740                 + " " + item.getTitle());
741       }
742       component.setToolTipText(item.getLink());
743       if (!item.isRead())
744       {
745         component.setFont(component.getFont().deriveFont(Font.BOLD));
746       }
747       else
748       {
749         component.setFont(component.getFont().deriveFont(Font.PLAIN));
750       }
751     }
752     return component;
753   }
754 }