/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2003-2006 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License 1.0 which accompanies this     **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl - initial API and implementation (bpasero@rssowl.org)         **
 **                                                                          **
 **  **********************************************************************  */

package net.sourceforge.rssowl.controller;

import net.sourceforge.rssowl.controller.panel.BrowserPanel;
import net.sourceforge.rssowl.controller.sort.SortingSelectionAdapter;
import net.sourceforge.rssowl.model.Category;
import net.sourceforge.rssowl.model.Channel;
import net.sourceforge.rssowl.model.Favorite;
import net.sourceforge.rssowl.model.NewsItem;
import net.sourceforge.rssowl.model.TabItemData;
import net.sourceforge.rssowl.model.TableData;
import net.sourceforge.rssowl.model.TableItemData;
import net.sourceforge.rssowl.model.TreeItemData;
import net.sourceforge.rssowl.util.DateParser;
import net.sourceforge.rssowl.util.GlobalSettings;
import net.sourceforge.rssowl.util.document.DocumentGenerator;
import net.sourceforge.rssowl.util.i18n.Dictionary;
import net.sourceforge.rssowl.util.i18n.ITranslatable;
import net.sourceforge.rssowl.util.search.SearchDefinition;
import net.sourceforge.rssowl.util.shop.FontShop;
import net.sourceforge.rssowl.util.shop.PaintShop;
import net.sourceforge.rssowl.util.shop.WidgetShop;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MenuAdapter;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

import java.util.Hashtable;
import java.util.Vector;

/**
 * Class to create a table holding news from a NewsItem.
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class NewsTable implements ITranslatable {

  /** Sort order of columns in newstable */
  private static final String[] columnOrder = new String[] { "TABLE_HEADER_STATUS", "TABLE_HEADER_NEWSTITLE", "TABLE_HEADER_PUBDATE", "TABLE_HEADER_AUTHOR", "TABLE_HEADER_CATEGORY", "TABLE_HEADER_PUBLISHER", "TABLE_HEADER_FEED" };

  private MenuItem addFeedToFav;
  private MenuItem blogNews;
  private MenuItem copyUrlItem;
  private MenuItem generateHTML;
  private MenuItem generatePDF;
  private MenuItem generateRTF;
  private MenuItem mailLinkToFriend;
  private MenuItem markAllReadItem;
  private MenuItem markUnreadItem;
  private MenuItem reloadFeed;
  private MenuItem searchFeed;
  private CTabItem tabItem;
  private Menu tableMenu;
  EventManager eventManager;
  boolean isNewsSelected;
  String language;
  GUI rssOwlGui;

  private MenuItem exportItem;

  /**
   * Instantiate a new NewsTable
   * 
   * @param rssOwlGui The maincontroller
   * @param tabItem The CTabItem this Newstable is added to
   * @param eventManager The event manager
   */
  public NewsTable(GUI rssOwlGui, CTabItem tabItem, EventManager eventManager) {
    this.rssOwlGui = rssOwlGui;
    this.tabItem = tabItem;
    this.eventManager = eventManager;
    isNewsSelected = true;

    /** The language used for the popup */
    language = Dictionary.selectedLanguage;
  }

  /**
   * Sets the CTabItem this Table is part of. This method is only called when
   * the initial TabItem was moved to a different TabItem.
   * 
   * @param tabItem The CTabItem this Table is part of.
   */
  public void setTabItem(CTabItem tabItem) {
    this.tabItem = tabItem;
  }

  /**
   * Fill the given table using the given items. <a
   * href="mailto:masterludo@gmx.net">Ludovic Kim-Xuan Galibert </a>
   * 
   * @param table the Table to fill.
   * @param newsItems the Hashtable of RSSNewsItems to be displayed.
   * @param newsItemOrder the ordering of the RSSNewsItems as keys to retrieve
   * the RSSNewsItems from the Hashtable.
   * @param newsItemInfos a vector of identifiers for the type of info.
   * @param performSearch true if a search should be performed, otherwise false.
   */
  public static void fillTable(Table table, Hashtable newsItems, Vector newsItemOrder, Vector newsItemInfos, boolean performSearch) {
    fillTable(table, newsItems, newsItemOrder, newsItemInfos, performSearch, new int[] { 0 });
  }

  /**
   * Fill the given table using the given items. <a
   * href="mailto:masterludo@gmx.net">Ludovic Kim-Xuan Galibert </a>
   * 
   * @param table the Table to fill.
   * @param newsItems the Hashtable of RSSNewsItems to be displayed.
   * @param newsItemOrder the ordering of the RSSNewsItems as keys to retrieve
   * the RSSNewsItems from the Hashtable.
   * @param newsItemInfos a vector of identifiers for the type of info.
   * @param performSearch true if a search should be performed, otherwise false.
   * @param columnWidth The widths of each column in an int array
   */
  public static void fillTable(Table table, Hashtable newsItems, Vector newsItemOrder, Vector newsItemInfos, boolean performSearch, int columnWidth[]) {

    /** Foreach newsheader in the newsitem */
    for (int a = 0; a < newsItemOrder.size(); a++) {
      NewsItem rssNewsItem = (NewsItem) newsItems.get(newsItemOrder.get(a));

      /** Create a new tableitem */
      TableItem item = new TableItem(table, SWT.NONE);

      /** Set Title */
      item.setText(1, rssNewsItem.getTitle());

      /** Set read / unread status as data to the TableItem */
      item.setData(TableItemData.createNewsheaderData(rssNewsItem.isRead()));

      /** Update Style of TableItem */
      updateTableItemStyle(item);

      /** First col (0): Unread/Read status, Second col (1): News title */
      int currentCol = 2;

      /** Set possible pubDate */
      if (newsItemInfos.contains("TABLE_HEADER_PUBDATE")) {
        if (rssNewsItem.getPubDate() != null) {
          if (rssNewsItem.getPubDateParsed() != null)
            item.setText(currentCol, DateParser.formatDate(rssNewsItem.getPubDateParsed(), true));
          else
            item.setText(currentCol, rssNewsItem.getPubDate());
        }

        /** News is not containing pubDate */
        else
          item.setText(currentCol, "-");
        currentCol++;
      }

      /** Set possible author */
      if (newsItemInfos.contains("TABLE_HEADER_AUTHOR")) {

        /** Often a "mailto:" is given, remove it! */
        if (rssNewsItem.getAuthor() != null)
          item.setText(currentCol, rssNewsItem.getAuthor().replaceAll("mailto:", ""));
        else
          item.setText(currentCol, "-");
        currentCol++;
      }

      /** Set possible category */
      if (newsItemInfos.contains("TABLE_HEADER_CATEGORY")) {
        if (rssNewsItem.getCategory() != null)
          item.setText(currentCol, rssNewsItem.getCategory());
        else
          item.setText(currentCol, "-");
        currentCol++;
      }

      /** Set possible publisher */
      if (newsItemInfos.contains("TABLE_HEADER_PUBLISHER")) {
        if (rssNewsItem.getPublisher() != null)
          item.setText(currentCol, rssNewsItem.getPublisher());
        else
          item.setText(currentCol, "-");
        currentCol++;
      }

      /** Set possible newsefeed title */
      if (newsItemInfos.contains("TABLE_HEADER_FEED")) {
        if (rssNewsItem.getNewsfeedTitle() != null)
          item.setText(currentCol, rssNewsItem.getNewsfeedTitle());
        else
          item.setText(currentCol, "-");
        currentCol++;
      }

      /** Reset syntaxhighlight words if search is not performed */
      if (!performSearch) {
        rssNewsItem.clearHighlightWords();
        rssNewsItem.setRequiresViewUpdate(true);
      }
    }

    /** Pack columns if column widths are not given */
    if (columnWidth[0] == 0) {
      for (int i = 0; i < table.getColumnCount(); i++)
        table.getColumn(i).pack();
    }

    /** Set width explicitly if widths are given */
    else {
      for (int i = 0; i < table.getColumnCount(); i++)
        table.getColumn(i).setWidth(columnWidth[i]);
    }
  }

  /**
   * Fill columns of the given table
   * 
   * @param table The table to add the columns
   * @param newsItems The Newsitems
   * @param newsItemOrder The order of the Newsitems
   * @param newsItemInfos Infos to the Newsitems
   * @param performSearch TRUE if a search is performed
   */
  public static void fillTableColumns(Table table, Hashtable newsItems, Vector newsItemOrder, Vector newsItemInfos, boolean performSearch) {

    /** Create a new tabledata to store the column sorter inside */
    TableData tableData = TableData.createTableData();

    /** For each possible column */
    for (int a = 0; a < columnOrder.length; a++) {

      /** Only create column if Channel provides this information */
      if (newsItemInfos.contains(columnOrder[a])) {
        boolean isStatus = columnOrder[a].equals("TABLE_HEADER_STATUS");
        TableColumn column = new TableColumn(table, SWT.LEFT);
        column.setData(columnOrder[a]);

        /** Do not set text to status column */
        if (!isStatus)
          column.setText(GUI.i18n.getTranslation(columnOrder[a]));

        /** Status Column is not resizable and has a ToolTip */
        if (isStatus) {
          column.setResizable(false);
          column.setToolTipText(GUI.i18n.getTranslation(columnOrder[a]));
        }

        /** Apply sorting selection adapter */
        SortingSelectionAdapter sorter = new SortingSelectionAdapter(table, newsItems, newsItemOrder, newsItemInfos, performSearch);
        column.addSelectionListener(sorter);

        /** Add sorter into table data */
        tableData.addColumnSorter(sorter, columnOrder[a]);

        /** Add column into table data */
        tableData.addColumn(column, columnOrder[a]);
      }
    }

    /** Apply table data to Table */
    table.setData(tableData);
  }

  /**
   * Display the next news in the table. If the last news (either unread or
   * read) is reached, continue with news displayed in other opened Tabs.
   * 
   * @param showUnread TRUE if only unread news shoulb be displayed
   */
  static void actionDisplayNextNews(boolean showUnread) {
    CTabFolder newsHeaderTabFolder = GUI.rssOwlGui.getRSSOwlNewsTabFolder().getNewsHeaderTabFolder();
    int selectionIndex = newsHeaderTabFolder.getSelectionIndex();

    /** First try to display the next (unread/read) news of the selected TabItem */
    if (selectionIndex >= 0 && actionDisplayNextNews(newsHeaderTabFolder.getSelection(), showUnread))
      return;

    /** Second try to display the next news from the Tabs on the right side */
    int itemCount = newsHeaderTabFolder.getItemCount();
    for (int a = selectionIndex + 1; a < itemCount; a++) {

      /** Next news on other Tab was selected, return */
      if (actionDisplayNextNews(newsHeaderTabFolder.getItem(a), showUnread))
        return;
    }

    /** Third try to display the next news from the Tabs on the left side */
    for (int a = 0; a < selectionIndex; a++) {

      /** Next news on other Tab was selected, return */
      if (actionDisplayNextNews(newsHeaderTabFolder.getItem(a), showUnread))
        return;
    }

    /** Fourth try to traverse the Favorites Tree to the next unread Item */
    if (GUI.rssOwlGui.getRSSOwlFavoritesTree().getFavoritesTree().getSelectionCount() > 0) {
      TreeItem selection = GUI.rssOwlGui.getRSSOwlFavoritesTree().getFavoritesTree().getSelection()[0];
      TreeNode treeNode = new TreeNode(selection, showUnread);

      /** Traverse the Tree */
      while (treeNode != null && WidgetShop.isset(treeNode.getItem())) {
        TreeItem item = treeNode.getItem();

        /** Try out this Item */
        if (actionDisplayNextNews(item, showUnread, false))
          return;

        /** Node */
        if (treeNode.hasChildNodes()) {
          treeNode = treeNode.getFirstChild();
        }

        /** Leaf */
        else {

          /** Find the parent level */
          while (treeNode != null && treeNode.getNextSibling() == null)
            treeNode = treeNode.getParent();

          if (treeNode != null)
            treeNode = treeNode.getNextSibling();
        }
      }
    }

    /** Fifth try to open any next unread News from the Tree of Favorites */
    Tree tree = GUI.rssOwlGui.getRSSOwlFavoritesTree().getFavoritesTree();
    TreeItem items[] = tree.getItems();
    for (int i = 0; i < items.length; i++) {
      if (actionDisplayNextNews(items[i], showUnread, true))
        return;
    }
  }

  /**
   * Try to select the next news from the given TreeItem.
   * 
   * @param item The current Item to search for Channels.
   * @param showUnread If TRUE only show unread news
   * @param deep If TRUE, recursivly try childs of the given Item
   * @return TRUE in case a Channel with News has been found.
   */
  static boolean actionDisplayNextNews(TreeItem item, boolean showUnread, boolean deep) {
    TreeItemData data = (TreeItemData) item.getData();

    /** Item is a Favorite with unread News */
    if (data.getFavorite() != null && !data.getFavorite().isErrorLoading() && !data.getFavorite().isSynchronizer() && (!showUnread || data.isStatusUnread())) {
      Favorite fav = data.getFavorite();

      /** Show if not yet opened in TabFolder */
      if (!GUI.rssOwlGui.getRSSOwlNewsTabFolder().isFeedOpened(fav.getUrl())) {
        GUI.rssOwlGui.loadNewsFeed(fav.getUrl(), SearchDefinition.NO_SEARCH, true, false, showUnread ? NewsTabFolder.DISPLAY_MODE_SELECT_UNREAD_NEWS : NewsTabFolder.DISPLAY_MODE_SELECT_NEWS);
        return true;
      }
    }

    /** Item is a Category */
    else if (deep && (data.isCategory() || data.isBlogroll()) && (!showUnread || data.isStatusUnread())) {
      TreeItem childs[] = item.getItems();
      for (int i = 0; i < childs.length; i++)
        if (actionDisplayNextNews(childs[i], showUnread, deep))
          return true;
    }

    return false;
  }

  /**
   * Try to select the next (unread/read) news from the given TabItem
   * 
   * @param tabItem The tabitem containing news
   * @param showUnread If TRUE only show unread news
   * @return boolean TRUE if the next news was selected
   */
  static boolean actionDisplayNextNews(CTabItem tabItem, boolean showUnread) {
    CTabFolder newsHeaderTabFolder = GUI.rssOwlGui.getRSSOwlNewsTabFolder().getNewsHeaderTabFolder();
    TabItemData data = (TabItemData) tabItem.getData();

    /**
     * This tab could be an empty search or not containing any newsfeed. So
     * return if there is no Table with news available
     */
    if (data.getNewsHeaderTable() == null)
      return false;

    /** Get table and tableitems that are holding the news */
    Table newsTable = data.getNewsHeaderTable();
    TableItem items[] = newsTable.getItems();
    int selected = newsTable.getSelectionIndex();
    int newSelectionIndex = -1;
    boolean newSelect = false;

    /** Foreach TableItem */
    for (int a = selected + 1; a < items.length; a++) {

      /** Display this unread news */
      if (showUnread && !((TableItemData) items[a].getData()).isNewsRead()) {
        newSelectionIndex = a;
        newSelect = true;
        break;
      }

      /** Display the next news */
      else if (!showUnread) {
        newSelectionIndex = a;
        break;
      }
    }

    /** Display the next unread news, even if its before the current ones */
    if (showUnread && !newSelect) {
      for (int a = 0; a < items.length; a++) {
        if (showUnread && !((TableItemData) items[a].getData()).isNewsRead()) {
          newSelectionIndex = a;
          break;
        }
      }
    }

    /** Set the new selection in the table and notify listeners */
    if (newSelectionIndex != -1) {

      /**
       * In case the news is from a tabitem that is not selected, select it to
       * show the user
       */
      if (!newsHeaderTabFolder.getSelection().equals(tabItem)) {
        newsHeaderTabFolder.setSelection(tabItem);
        GUI.rssOwlGui.getRSSOwlNewsTabFolder().updateTabFolderState();
      }

      /** Restore Focus if Focus is not on the Browser */
      boolean focusOnNewsTextBrowser = false;
      BrowserPanel panel = GUI.rssOwlGui.getRSSOwlNewsText().getBrowserPanel();
      if (panel != null && WidgetShop.isset(panel.getBrowser()))
        focusOnNewsTextBrowser = panel.getBrowser().isFocusControl();

      if (!focusOnNewsTextBrowser)
        newsTable.setFocus();

      /** Apply Selection */
      newsTable.setSelection(newSelectionIndex);

      /** Notify MouseDown Listener */
      Event leftMouseClickEvent = new Event();
      leftMouseClickEvent.button = 1;
      newsTable.notifyListeners(SWT.MouseDown, leftMouseClickEvent);

      /** Return: Next news was selected */
      return true;
    }

    /** Return: No next news was selected */
    return false;
  }

  /**
   * Mark all tableitems of the given table read
   * 
   * @param newsTable The table to mark read
   */
  static void markAllRead(Table newsTable) {

    /** Foreach TableItem of the Table */
    int itemCount = newsTable.getItemCount();
    for (int a = 0; a < itemCount; a++) {

      /** Set data to read */
      newsTable.getItem(a).setData(TableItemData.createNewsheaderData(true));

      /** Remove bold font style from this read item */
      updateTableItemStyle(newsTable.getItem(a));
    }
  }

  /**
   * Update the table items style. Style unread items different from read ones.
   * 
   * @param item A TableItem
   */
  static void updateTableItemStyle(TableItem item) {
    boolean isNewsRead = ((TableItemData) item.getData()).isNewsRead();

    /** News is read */
    if (isNewsRead) {
      item.setFont(FontShop.tableFont);
      item.setImage(0, PaintShop.iconRead);
    }

    /** News is unread */
    else {
      item.setFont(FontShop.tableBoldFont);
      item.setImage(0, PaintShop.iconUnread);
    }
  }

  /**
   * Create a new table holding news
   * 
   * @param newsHeaderTableHolder The composite that holds the table
   * @return Table The created table for the news
   */
  public Table createNewsTable(Composite newsHeaderTableHolder) {

    /** New table holding the news */
    final Table newsTable = new Table(newsHeaderTableHolder, SWT.BORDER | SWT.FULL_SELECTION);
    newsTable.setLinesVisible(false);
    newsTable.setHeaderVisible(true);
    newsTable.setFont(FontShop.tableFont);
    newsTable.setLayoutData(new GridData(GridData.FILL_BOTH));

    /** Show Hand-Cursor if mouse is over Column 1 */
    newsTable.addListener(SWT.MouseMove, new Listener() {
      private Cursor lastCursor;

      public void handleEvent(Event event) {
        Point p = new Point(event.x, event.y);
        TableItem item = newsTable.getItem(p);

        /** Problem - reset */
        if (!WidgetShop.isset(item)) {
          if (lastCursor != null && WidgetShop.isset(newsTable)) {
            lastCursor = null;
            newsTable.setCursor(null);
          }
          return;
        }

        /** Update Cursor */
        boolean showHand = item.getImageBounds(0).contains(p);
        if (lastCursor == null && showHand) {
          lastCursor = GUI.display.getSystemCursor(SWT.CURSOR_HAND);
          newsTable.setCursor(lastCursor);
        } else if (lastCursor != null && !showHand) {
          lastCursor = null;
          newsTable.setCursor(null);
        }
      }
    });

    /** Special handling for Mac users */
    newsTable.addListener(SWT.MouseUp, new Listener() {
      public void handleEvent(Event event) {
        onMouseUp(newsTable, event);
      }
    });

    /** The user has selected a newsitem */
    newsTable.addListener(SWT.MouseDown, new Listener() {
      public void handleEvent(Event e) {
        onMouseDown(newsTable, e);
      }
    });

    /** Open URL of selected newsheader if available */
    newsTable.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        onKeyPressed(newsTable, e);
      }
    });

    /** Open URL of selected newsheader if available */
    newsTable.addListener(SWT.MouseDoubleClick, new Listener() {
      public void handleEvent(Event event) {
        onMouseDoubleClick(newsTable, event);
      }
    });

    /** Popup that is added to the table */
    tableMenu = new Menu(newsTable);

    /** Update the i18n translation if language changed */
    tableMenu.addMenuListener(new MenuAdapter() {
      public void menuShown(MenuEvent e) {
        if (!language.equals(Dictionary.selectedLanguage)) {
          updateI18N();
          language = Dictionary.selectedLanguage;
        }
      }
    });

    /** Add feed to favorites */
    addFeedToFav = new MenuItem(tableMenu, SWT.NONE);
    if (!GlobalSettings.isMac())
      addFeedToFav.setImage(PaintShop.iconAddToFavorites);
    addFeedToFav.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionAddToFavorites();
      }
    });

    /** Separator */
    new MenuItem(tableMenu, SWT.SEPARATOR);

    /** Mark selected news unread */
    markUnreadItem = new MenuItem(tableMenu, SWT.NONE);
    markUnreadItem.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionMarkNewsUnread();
      }
    });

    /** Mark all news read */
    markAllReadItem = new MenuItem(tableMenu, SWT.NONE);
    markAllReadItem.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionMarkAllNewsRead();
      }
    });

    /** Separator */
    new MenuItem(tableMenu, SWT.SEPARATOR);

    /** Reload newsfeed */
    reloadFeed = new MenuItem(tableMenu, SWT.NONE);
    if (!GlobalSettings.isMac())
      reloadFeed.setImage(PaintShop.iconReload);
    reloadFeed.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionReload();
      }
    });

    /** Separator */
    new MenuItem(tableMenu, SWT.SEPARATOR);

    /** Search in newsfeed */
    searchFeed = new MenuItem(tableMenu, SWT.NONE);
    if (!GlobalSettings.isMac())
      searchFeed.setImage(PaintShop.iconFind);
    searchFeed.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionSearch();
      }
    });

    /** Separator */
    new MenuItem(tableMenu, SWT.SEPARATOR);

    /** Mail newstip to a friend */
    mailLinkToFriend = new MenuItem(tableMenu, SWT.NONE);
    if (!GlobalSettings.isMac())
      mailLinkToFriend.setImage(PaintShop.iconMail);
    mailLinkToFriend.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionMailNewsTip();
      }
    });

    /** Copy URL of selected newsitem */
    copyUrlItem = new MenuItem(tableMenu, SWT.NONE);
    copyUrlItem.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionCopyNewsUrl();
      }
    });

    /** Separator */
    new MenuItem(tableMenu, SWT.SEPARATOR);

    /** Export */
    exportItem = new MenuItem(tableMenu, SWT.CASCADE);
    if (!GlobalSettings.isMac())
      exportItem.setImage(PaintShop.iconExport);

    Menu selectexport = new Menu(GUI.shell, SWT.DROP_DOWN);
    exportItem.setMenu(selectexport);

    /** In case iText is used */
    if (GlobalSettings.useIText()) {

      /** Generate PDF of selected news */
      generatePDF = new MenuItem(selectexport, SWT.NONE);
      generatePDF.setText(GUI.i18n.getTranslation("MENU_GENERATE_PDF_SELECTION") + "...");
      if (!GlobalSettings.isMac())
        generatePDF.setImage(PaintShop.iconPDF);
      generatePDF.addSelectionListener(new SelectionAdapter() {
        public void widgetSelected(SelectionEvent e) {
          eventManager.actionExportFeed(DocumentGenerator.PDF_FORMAT, false);
        }
      });

      /** Generate RTF of selected news */
      generateRTF = new MenuItem(selectexport, SWT.NONE);
      generateRTF.setText(GUI.i18n.getTranslation("MENU_GENERATE_RTF_SELECTION") + "...");
      if (!GlobalSettings.isMac())
        generateRTF.setImage(PaintShop.iconRTF);
      generateRTF.addSelectionListener(new SelectionAdapter() {
        public void widgetSelected(SelectionEvent e) {
          eventManager.actionExportFeed(DocumentGenerator.RTF_FORMAT, false);
        }
      });
    }

    /** Generate HTML of selected news */
    generateHTML = new MenuItem(selectexport, SWT.NONE);
    generateHTML.setText(GUI.i18n.getTranslation("MENU_GENERATE_HTML_SELECTION") + "...");
    if (!GlobalSettings.isMac())
      generateHTML.setImage(PaintShop.iconHTML);
    generateHTML.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionExportFeed(DocumentGenerator.HTML_FORMAT, false);
      }
    });

    /** Separator */
    new MenuItem(tableMenu, SWT.SEPARATOR);

    /** Blog selected news */
    blogNews = new MenuItem(tableMenu, SWT.NONE);
    blogNews.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        eventManager.actionBlogNews();
      }
    });

    /** Update accelerators */
    updateAccelerators();

    /** Init the Mnemonics */
    MenuManager.initMnemonics(tableMenu);

    /** Add menu */
    newsTable.setMenu(tableMenu);

    /** No news selected */
    if (newsTable.getSelectionIndex() < 0 && isNewsSelected == true)
      setNewsSelectedState(false);

    /** News selected */
    else if (newsTable.getSelectionIndex() >= 0 && isNewsSelected == false)
      setNewsSelectedState(true);

    return newsTable;
  }

  /**
   * @see net.sourceforge.rssowl.util.i18n.ITranslatable#updateI18N()
   */
  public void updateI18N() {
    if (GlobalSettings.useIText()) {
      generatePDF.setText(GUI.i18n.getTranslation("MENU_GENERATE_PDF_SELECTION") + "...");
      generateRTF.setText(GUI.i18n.getTranslation("MENU_GENERATE_RTF_SELECTION") + "...");
    }
    generateHTML.setText(GUI.i18n.getTranslation("MENU_GENERATE_HTML_SELECTION") + "...");

    /** Update accelerators */
    updateAccelerators();

    /** Init the Mnemonics */
    MenuManager.initMnemonics(tableMenu);
  }

  /** Update the accelerators on the menuitems */
  private void updateAccelerators() {
    rssOwlGui.getRSSOwlMenu().updateAccelerator(addFeedToFav, "BUTTON_ADDTO_FAVORITS", true, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(copyUrlItem, "POP_COPY_NEWS_URL", false, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(markUnreadItem, "POP_MARK_UNREAD", false, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(markAllReadItem, "POP_MARK_ALL_READ", false, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(reloadFeed, "MENU_RELOAD", false, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(searchFeed, "BUTTON_SEARCH", true, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(exportItem, "BUTTON_EXPORT", false, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(mailLinkToFriend, "POP_MAIL_LINK", false, false);
    rssOwlGui.getRSSOwlMenu().updateAccelerator(blogNews, "POP_BLOG_NEWS", false, false);
  }

  /**
   * Display the selected news in the table
   * 
   * @param next If TRUE select next news, if FALSE select previous news
   */
  void actionNavigateNews(boolean next) {
    TabItemData data = (TabItemData) tabItem.getData();

    /**
     * This tab could be an empty search. So return if there is no Table
     * available
     */
    if (data.getNewsHeaderTable() == null)
      return;

    /** Get table and tableitems that are holding the news */
    Table newsTable = data.getNewsHeaderTable();

    int minSelection = 0;
    int maxSelection = newsTable.getItemCount() - 1;

    /** Return on max / min element reached */
    if ((next && newsTable.getSelectionIndex() == maxSelection) || (!next && newsTable.getSelectionIndex() == minSelection))
      return;

    /** Select next news */
    if (next)
      actionSelectNews(newsTable, newsTable.getSelectionIndex() + 1);

    /** Select previous news */
    else
      actionSelectNews(newsTable, newsTable.getSelectionIndex() - 1);
  }

  /**
   * Select the index on the given Newstable
   * 
   * @param newsTable The table the event occurs
   * @param index The index to select in the table
   */
  void actionSelectNews(Table newsTable, int index) {

    /** Set Selection to index */
    newsTable.setSelection(index);

    /** Show Selection if necessary */
    newsTable.showSelection();

    /** Notify MouseDown listener in news table to select a news */
    Event leftMouseClickEvent = new Event();
    leftMouseClickEvent.button = 1;
    newsTable.notifyListeners(SWT.MouseDown, leftMouseClickEvent);
  }

  /**
   * Called whenever a key is pressed on the Table
   * 
   * @param newsTable The newstable the event is occuring on
   * @param e The occuring event
   */
  void onKeyPressed(Table newsTable, KeyEvent e) {
    boolean openExternal = (e.stateMask & SWT.CTRL) != 0 || (e.stateMask & SWT.COMMAND) != 0;

    /** Open URL on Enter pressed */
    if (e.character == SWT.CR && newsTable.getSelectionIndex() != -1)
      eventManager.actionOpenNewsURL(newsTable.getItem(newsTable.getSelectionIndex()).getText(1), openExternal);

    /** Navigate to next news */
    else if (e.keyCode == SWT.ARROW_DOWN)
      actionNavigateNews(true);

    /** Navigate to previous news */
    else if (e.keyCode == SWT.ARROW_UP)
      actionNavigateNews(false);

    /** Navigate to first news */
    else if (e.keyCode == SWT.HOME && newsTable.getItemCount() != 0)
      actionSelectNews(newsTable, 0);

    /** Navigate to last news */
    else if (e.keyCode == SWT.END && newsTable.getItemCount() != 0)
      actionSelectNews(newsTable, newsTable.getItemCount() - 1);

    /** Handle Page Up / Page Down */
    else if ((e.keyCode == SWT.PAGE_DOWN || e.keyCode == SWT.PAGE_UP) && newsTable.getItemCount() != 0) {
      int maxVisibleItemCount = (newsTable.getClientArea().height / newsTable.getItemHeight()) - 1;
      boolean isPageUp = (e.keyCode == SWT.PAGE_UP);
      boolean isSelection = (newsTable.getSelectionCount() > 0);
      int items = newsTable.getItemCount();
      int selectedItem = newsTable.getSelectionIndex();

      /**
       * In case there are less items in the table than displayable move to
       * first item on Page_Up and to last item on Page_Down
       */
      if (maxVisibleItemCount >= items)
        actionSelectNews(newsTable, isPageUp ? 0 : items - 1);

      /**
       * In case Page_Up was pressed and there are more items on top of the
       * selected one than maxVisibleItemCount, move maxVisibleItemCount items
       * above the selected item
       */
      else if (isPageUp && isSelection && (selectedItem - maxVisibleItemCount) >= 0)
        actionSelectNews(newsTable, selectedItem - maxVisibleItemCount);

      /**
       * In case Page_Up was pressed and less items on top of the selected one
       * than maxVisibleItemCount are present, move to the first item
       */
      else if (isPageUp && isSelection && (selectedItem - maxVisibleItemCount) < 0)
        actionSelectNews(newsTable, 0);

      /**
       * In case Page_Down was pressed and there are more items on bottom of the
       * selected one than maxVisibleItemCount, move maxVisibleItemCount items
       * down the selected item
       */
      else if (!isPageUp && isSelection && (selectedItem + maxVisibleItemCount) < (items - 1))
        actionSelectNews(newsTable, selectedItem + maxVisibleItemCount);

      /**
       * In case Page_Down was pressed and less items on bottom of the selected
       * one than maxVisibleItemCount are present, move to the last item
       */
      else if (!isPageUp && isSelection && (selectedItem + maxVisibleItemCount) >= (items - 1))
        actionSelectNews(newsTable, items - 1);
    }

    /** Process the OS default keys */
    else if (e.keyCode == SWT.ALT || e.keyCode == SWT.F10) {
      return;
    }

    /** Do not process the event, it was already processed */
    e.doit = false;
  }

  /**
   * Called whenever the table is doubleclicked on
   * 
   * @param newsTable The table the event is occuring on
   * @param e The occuring MouseEvent.
   */
  void onMouseDoubleClick(Table newsTable, Event e) {

    /** Force the URL to open externally if CTRL/CMD is pressed */
    boolean openExternal = (e.stateMask == SWT.CTRL || e.stateMask == SWT.COMMAND);
    if (newsTable.getSelectionIndex() != -1)
      eventManager.actionOpenNewsURL(newsTable.getItem(newsTable.getSelectionIndex()).getText(1), openExternal);
  }

  /**
   * An item of the newstable has been selected with the Mouse
   * 
   * @param newsTable The selected Table
   * @param e The occured event
   */
  void onMouseDown(Table newsTable, Event e) {

    /** TabItem Data */
    TabItemData data = (TabItemData) tabItem.getData();

    /** Tell MenuManager about state */
    if (newsTable.getSelectionCount() > 0)
      MenuManager.notifyState(MenuManager.NEWS_HEADER_SELECTED);

    /** No news selected */
    if (newsTable.getSelectionIndex() < 0 && isNewsSelected == true)
      setNewsSelectedState(false);

    /** News selected */
    else if (newsTable.getSelectionIndex() >= 0 && isNewsSelected == false)
      setNewsSelectedState(true);

    /**
     * Only display news if user has selected one, and with the first
     * mousebutton. In case of the mac, pressing first mousebutton in
     * combination with Ctrl is to call the context-menu, therefor return in
     * that case too.
     */
    if (e.button != 1 || newsTable.getSelectionCount() <= 0 || (GlobalSettings.isMac() && (e.stateMask & SWT.CTRL) != 0))
      return;

    /** Selected TableItem */
    TableItem selectedTableItem = newsTable.getSelection()[0];

    /** Check wether this News is to be marked as Unread */
    boolean markNewsUnread = false;
    Rectangle imageBounds = selectedTableItem.getImageBounds(0);
    if (((TableItemData) selectedTableItem.getData()).isNewsRead() && imageBounds != null && imageBounds.contains(new Point(e.x, e.y)))
      markNewsUnread = true;

    /** Get selected newsitem */
    final NewsItem selectedNewsItem = rssOwlGui.getRSSOwlNewsTabFolder().getSelectedNewsItem(selectedTableItem.getText(1));

    /**
     * Bug: In some rare cases the selected newsitem turns out to be NULL. As
     * long as no fix is in place, ignore Archive state in that case.
     */
    if (selectedNewsItem != null) {

      /** Add this news to the archive, because its read */
      rssOwlGui.getArchiveManager().getArchive().addEntry(selectedNewsItem);

      /** Set item to read status */
      selectedNewsItem.setRead(true);
    }

    /** Set tableitem data to "read" */
    ((TableItemData) selectedTableItem.getData()).setNewsRead(true);

    /** Remove bold font style from this read item */
    updateTableItemStyle(selectedTableItem);

    /** Update the TabItem status */
    rssOwlGui.getRSSOwlNewsTabFolder().updateTabItemStatus(tabItem);

    /** Retrieve feed's XML URL */
    String feedUrl = null;

    /** This is not an aggregated category */
    if (data.getUrl() != null && Category.getFavPool().containsKey(data.getUrl()))
      feedUrl = data.getUrl();

    /** This is an aggregated category */
    else if (selectedNewsItem != null && selectedNewsItem.getNewsfeedXmlUrl() != null && Category.getFavPool().containsKey(selectedNewsItem.getNewsfeedXmlUrl()))
      feedUrl = selectedNewsItem.getNewsfeedXmlUrl();

    /** Update favorites read status if this is a favorite */
    if (feedUrl != null) {
      Favorite rssOwlFavorite = (Favorite) Category.getFavPool().get(feedUrl);
      Channel selectedChannel = rssOwlGui.getRSSOwlNewsTabFolder().getSelectedChannel();

      /** The channel is given and not an aggregated category */
      if (selectedChannel != null && !selectedChannel.isAggregatedCat())
        rssOwlFavorite.updateReadStatus(selectedChannel.getUnreadNewsCount());

      /** The channel is given and an aggregated category */
      else if (selectedChannel != null)
        rssOwlFavorite.updateReadStatus(selectedChannel.getUnreadNewsCount(feedUrl));
    }

    /**
     * Mark Newstext as required to update since the user has clicked on the
     * news and therefor explicitly wants ot update the news.
     */
    rssOwlGui.getRSSOwlNewsText().setRequiresUpdate();

    /** Display the news on select of a row */
    if (data.getUrl() != null)
      rssOwlGui.getRSSOwlNewsText().displayNews(data.getUrl(), (selectedNewsItem != null) ? selectedNewsItem.getTitle() : selectedTableItem.getText(1));

    /** If this News is to be marked unread, run it outside this Event */
    if (markNewsUnread) {
      GUI.display.asyncExec(new Runnable() {
        public void run() {
          if (GUI.isAlive() && selectedNewsItem != null)
            eventManager.actionMarkNewsUnread(selectedNewsItem);
        }
      });
    }
  }

  /**
   * Called whenever the mouse button is released
   * 
   * @param newsTable The newstable the event is occuring on
   * @param event The occuring Event
   */
  void onMouseUp(Table newsTable, Event event) {

    /**
     * The mouse on Mac does not provide a second mouse button. Display the
     * popup when the first mouse button is clicked while the ctrl key is
     * pressed
     */
    if (GlobalSettings.isMac()) {
      if (event.stateMask == (SWT.BUTTON1 | SWT.CTRL))
        newsTable.getMenu().setVisible(true);
    }
  }

  /**
   * Set a new state to some items of the popup
   * 
   * @param newsSelected TRUE if a news is selected
   */
  void setNewsSelectedState(boolean newsSelected) {
    exportItem.setEnabled(newsSelected);
    copyUrlItem.setEnabled(newsSelected);
    markUnreadItem.setEnabled(newsSelected);
    mailLinkToFriend.setEnabled(newsSelected);
    blogNews.setEnabled(newsSelected);

    /** Save this state */
    isNewsSelected = newsSelected;
  }
}