// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id$
//
package com.salas.bb.views.feeds.twitter;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.uif.action.ActionManager;
import com.jgoodies.uif.util.SystemUtils;
import com.salas.bb.core.GlobalModel;
import com.salas.bb.core.actions.ActionsTable;
import com.salas.bb.domain.IArticle;
import com.salas.bb.domain.IArticleListener;
import com.salas.bb.domain.NetworkFeed;
import com.salas.bb.twitter.ReplyAction;
import com.salas.bb.twitter.TwitterFeature;
import com.salas.bb.utils.ReadItLater;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.uif.*;
import com.salas.bb.utils.uif.html.CustomHTMLEditorKit;
import com.salas.bb.views.feeds.ArticlePinControl;
import com.salas.bb.views.feeds.IFeedDisplayConstants;
import com.salas.bb.views.feeds.html.AbstractArticleDisplay;
import com.salas.bb.views.feeds.html.HTMLArticleDisplay;
import com.salas.bb.views.feeds.html.IArticleDisplayConfig;
import javax.swing.*;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.Document;
import javax.swing.text.Style;
import javax.swing.text.html.HTMLDocument;
import java.awt.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Twitter article display.
*/
public class TwitterArticleDisplay extends AbstractArticleDisplay implements IArticleListener
{
/** Name of the style we use to apply customized fonts. */
private static final String TEXT_STYLE_NAME = "normal";
private static final CellConstraints CELL_CONSTRAINTS = new CellConstraints();
private static final Pattern PATTERN_USERNAME =
Pattern.compile("^\\s*(<b>)?\\s*([^\\s<:]+)\\s*(</b>)?\\s*:\\s*(.*)$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private final IArticleDisplayConfig config;
private final IArticle article;
private int mode;
private boolean selected;
private JEditorPane tfText;
private JEditorPane tfFullText;
private ProgressSpinner spinner;
private JLabel lbDate;
private ArticlePinControl lbPin;
private LinkLabel lnReply;
private LinkLabel lnFullText;
private LinkLabel lnHideText;
private JPanel pnlControls;
/**
* Creates the twitter article display component.
*
* @param article article to display.
* @param config configuration.
*/
public TwitterArticleDisplay(IArticle article, IArticleDisplayConfig config)
{
this.article = article;
this.config = config;
initComponents();
setBorder(new UpDownBorder(HTMLArticleDisplay.COLOR_BORDER_LINE));
initTextStyle(article, config);
// Order of the following lines is important (affects the layout on Win)
onThemeChange();
setText();
setViewMode(config.getViewMode());
}
private void initTextStyle(IArticle article, IArticleDisplayConfig config)
{
// Create new style for article and init it with default style settings
HTMLDocument doc = (HTMLDocument)tfText.getDocument();
doc.setBase(article.getLink());
Style def = doc.getStyle("default");
doc.addStyle(TEXT_STYLE_NAME, def);
UifUtilities.setFontAttributes(doc, TEXT_STYLE_NAME, config.getTitleFont(article.isRead()));
doc = (HTMLDocument)tfFullText.getDocument();
doc.setBase(article.getLink());
def = doc.getStyle("default");
doc.addStyle(TEXT_STYLE_NAME, def);
UifUtilities.setFontAttributes(doc, TEXT_STYLE_NAME, config.getTextFont());
}
private void setText()
{
String name;
String link = null;
String text = article.getHtmlText();
if (text != null)
{
Matcher m = PATTERN_USERNAME.matcher(text);
if (m.find())
{
// There's the user name at the beginning of the line, like "username: ...."
name = m.group(2);
text = m.group(4);
} else
{
// Take the author
name = article.getAuthor().split("\\s")[0];
}
link = "http://twitter.com/" + name;
if (TwitterFeature.areAdvancedFeaturesAvailable())
{
text = "<a href='" + link + "' rel='twitter'>" + name + "</a>: " + text;
// Wrap "@name" and "#tag" with links
text = text.replaceAll("@([\\w\\d]+)", "<a href=\"http://twitter.com/$1\">@$1</a>");
text = text.replaceAll("#([\\w\\d]+)", "<a href=\"http://search.twitter.com/search?q=%23$1\">#$1</a>");
} else
{
text = name + ": " + text;
}
// This is the special trick to outsmart Mac OS implementation of HTMLEditorKit.
// Otherwise under some conditions the height will be equal to zero and
// no text will be displayed.
if (SystemUtils.IS_OS_MAC) text = "<p id='start'>" + text;
}
HTMLDocument doc = (HTMLDocument)tfText.getDocument();
doc.putProperty(Document.StreamDescriptionProperty, ((NetworkFeed) article.getFeed()).getXmlURL());
tfText.setText(text);
UifUtilities.installTextStyle(tfText, TEXT_STYLE_NAME);
if (link != null)
{
try
{
lnReply.setLink(new URL(link));
} catch (MalformedURLException e)
{
lnReply.setVisible(false);
}
}
lnHideText.setVisible(true);
if (!article.getPlainText().contains("http://"))
{
lnFullText.setVisible(false);
} else
{
tfFullText.setVisible(false);
// Important as it won't render otherwise for some weird reason
tfFullText.setText("<p id='start'> </p>");
UifUtilities.installTextStyle(tfFullText, TEXT_STYLE_NAME);
java.util.List<String> links = StringUtils.collectLinks(article.getPlainText());
try
{
lnFullText.setLink(new URL(links.get(0)));
lnFullText.setVisible(true);
} catch (MalformedURLException e)
{
lnFullText.setVisible(false);
}
}
}
private void setTextButton(boolean full)
{
Component btn = full ? lnFullText : lnHideText;
Component other = full ? lnHideText : lnFullText;
pnlControls.remove(other);
pnlControls.add(btn, CELL_CONSTRAINTS.xy(7, 2));
}
/**
* Creates and initializes view components.
*/
private void initComponents()
{
Date date = article.getPublicationDate();
GlobalModel model = GlobalModel.SINGLETON;
tfText = createTextArea();
tfFullText = createTextArea();
tfFullText.setBorder(
BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(0, 30, 0, 0),
BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 5, 0, 0, Color.lightGray),
BorderFactory.createEmptyBorder(0, 10, 0, 0)
)));
lbDate = new JLabel(SimpleDateFormat.getDateInstance().format(date), SwingConstants.LEFT);
lbPin = new ArticlePinControl(model.getSelectedGuide(), model.getSelectedFeed(), article);
lnHideText = new LinkButton("hide.text.button")
{
protected void doAction()
{
setTextButton(true);
tfFullText.setVisible(false);
}
};
lnFullText = new LinkButton("full.text.button")
{
protected void doAction()
{
setTextButton(false);
spinner.start();
final String link = this.getLink().toString();
new SwingWorker<String, String>()
{
@Override
protected String doInBackground()
throws Exception
{
return ReadItLater.mobilize(link);
}
@Override
protected void done()
{
spinner.stop();
try
{
String readable = super.get();
tfFullText.setText(readable);
tfFullText.setVisible(true);
UifUtilities.installTextStyle(tfFullText, TEXT_STYLE_NAME);
} catch (InterruptedException ignored)
{
} catch (ExecutionException e)
{
JOptionPane.showMessageDialog(TwitterArticleDisplay.this, "Failed to load full article text");
}
}
}.execute();
}
};
lnFullText.setForeground(LinkLabel.HIGHLIGHT_COLOR);
lnHideText.setForeground(LinkLabel.HIGHLIGHT_COLOR);
lnReply = new LinkButton("reply.button")
{
protected void doAction()
{
ReplyAction action = (ReplyAction)ActionManager.get(ActionsTable.CMD_TWITTER_REPLY);
action.setUserURL(lnReply.getLink());
action.actionPerformed(null);
}
};
lnReply.setForeground(LinkLabel.HIGHLIGHT_COLOR);
spinner = new ProgressSpinner();
setLayout(new FormLayout("5dlu, min:grow, 5dlu", "5dlu, pref, pref, pref, 5dlu"));
pnlControls = new JPanel(new FormLayout("pref, 5dlu, pref, 5dlu, pref, 5dlu, pref, 5dlu, pref, 0:grow", "3dlu, pref"));
pnlControls.add(lbPin, CELL_CONSTRAINTS.xy(1, 2));
pnlControls.add(lbDate, CELL_CONSTRAINTS.xy(3, 2));
pnlControls.add(lnReply, CELL_CONSTRAINTS.xy(5, 2));
setTextButton(true);
pnlControls.add(spinner, CELL_CONSTRAINTS.xy(9, 2));
add(tfText, CELL_CONSTRAINTS.xy(2, 2));
add(tfFullText, CELL_CONSTRAINTS.xy(2, 3));
add(pnlControls, CELL_CONSTRAINTS.xy(2, 4));
// Register delegating mouse listener
DelegatingMouseListener ml = new DelegatingMouseListener(this, true);
this.addMouseListener(ml);
lbDate.addMouseListener(ml);
tfText.addMouseListener(ml);
tfFullText.addMouseListener(ml);
}
/**
* Creates the pane.
*
* @return pane.
*/
private JEditorPane createTextArea()
{
final JEditorPane pane = new JEditorPane();
pane.setEditorKit(new CustomHTMLEditorKit());
pane.setAlignmentX(0.0f);
pane.setEditable(false);
return pane;
}
/**
* Updates the view according to read state.
*/
private void updateReadState()
{
updateFonts();
}
/**
* Updates foreground colors.
*/
private void updateForegrounds()
{
lbDate.setForeground(config.getDateFGColor(selected));
}
/**
* Updates background colors.
*/
private void updateBackgrounds()
{
// Image / Item colors
Color color = config.getGlobalBGColor(selected);
setBackground(color);
pnlControls.setBackground(color);
tfText.setBackground(color);
tfFullText.setBackground(color);
}
/**
* Updates fonts.
*/
private void updateFonts()
{
Font dateFont = config.getDateFont();
lbDate.setFont(dateFont);
lnReply.setFont(config.getTextFont());
HTMLDocument doc = (HTMLDocument)tfText.getDocument();
UifUtilities.setFontAttributes(doc, TEXT_STYLE_NAME, config.getTitleFont(article.isRead()));
UifUtilities.installTextStyle(tfText, TEXT_STYLE_NAME);
doc = (HTMLDocument)tfFullText.getDocument();
UifUtilities.setFontAttributes(doc, TEXT_STYLE_NAME, config.getTextFont());
UifUtilities.installTextStyle(tfFullText, TEXT_STYLE_NAME);
doLayout();
}
// --------------------------------------------------------------------------------------------
// IArticleDisplay
// --------------------------------------------------------------------------------------------
/**
* Returns assigned article.
*
* @return article.
*/
public IArticle getArticle()
{
return article;
}
/**
* Returns listener.
*
* @return listener.
*/
public IArticleListener getArticleListener()
{
return this;
}
/**
* Returns visual component.
*
* @return visual component.
*/
public Component getComponent()
{
return this;
}
/**
* Registers hyperlink listener.
*
* @param aListener listener.
*/
public void addHyperlinkListener(HyperlinkListener aListener)
{
tfText.addHyperlinkListener(aListener);
tfFullText.addHyperlinkListener(aListener);
}
/**
* Invoked on view mode change.
*/
public void onViewModeChange()
{
setViewMode(config.getViewMode());
doLayout();
}
/**
* Returns current article display view mode.
*
* @return mode.
*/
public int getViewMode()
{
return mode;
}
/**
* Sets the mode of this display.
*
* @param aMode mode of display.
*/
public void setViewMode(int aMode)
{
mode = aMode;
pnlControls.setVisible(aMode != IFeedDisplayConstants.MODE_MINIMAL);
}
/**
* Sets <code>TRUE</code> if the display is currently selected.
*
* @param sel <code>TRUE</code> if the display is currently selected.
*/
public void setSelected(boolean sel)
{
if (selected != sel)
{
handleAutoOpeningOnSelection(sel);
selected = sel;
updateBackgrounds();
}
}
/** Invoked when font bias changes. */
public void onFontBiasChange()
{
updateFonts();
}
/** Invoked on theme change. */
public void onThemeChange()
{
updateFonts();
updateBackgrounds();
updateForegrounds();
}
/**
* Sets <code>TRUE</code> if the display should become collapsed.
*
* @param col <code>TRUE</code> if the display is currently selected.
*/
public void setCollapsed(boolean col)
{
}
/**
* Requests focus and returns the state.
*
* @return <code>FALSE</code> if focus isn't likely to be changed.
*/
public boolean focus()
{
return this.requestFocusInWindow();
}
/** Invoked when article should update highlights. */
public void updateHighlights()
{
}
/** Updates a color code. */
public void updateColorCode()
{
}
// --------------------------------------------------------------------------------------------
// IArticleListener
// --------------------------------------------------------------------------------------------
/**
* Invoked when the property of the article has been changed.
*
* @param article article.
* @param property property of the article.
* @param oldValue old property value.
* @param newValue new property value.
*/
public void propertyChanged(IArticle article, String property, Object oldValue, Object newValue)
{
if (IArticle.PROP_READ.equals(property))
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
updateReadState();
}
});
}
}
}