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