Java tutorial
// 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: AbstractPostEditor.java,v 1.7 2007/02/21 16:58:29 spyromus Exp $ // package com.salas.bb.remixfeeds.editor; import com.jgoodies.binding.list.ArrayListModel; import com.jgoodies.forms.builder.ButtonBarBuilder; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.uif.AbstractDialog; import com.jgoodies.uif.util.Resizer; import com.jgoodies.uif.util.SystemUtils; import com.jgoodies.uifextras.util.PopupAdapter; import com.salas.bb.domain.DirectFeed; import com.salas.bb.domain.IArticle; import com.salas.bb.domain.IFeed; import com.salas.bb.remixfeeds.api.IWeblogAPI; import com.salas.bb.remixfeeds.api.WeblogAPIException; import com.salas.bb.remixfeeds.api.WeblogPost; import com.salas.bb.remixfeeds.prefs.TargetBlog; import com.salas.bb.utils.Constants; import com.salas.bb.utils.StringUtils; import com.salas.bb.utils.i18n.Strings; import com.salas.bb.utils.uif.BBFormBuilder; import com.salas.bb.utils.uif.IconSource; import com.salas.bb.utils.uif.ProgressSpinner; import com.salas.bb.utils.uif.UifUtilities; import javax.swing.*; import javax.swing.event.UndoableEditEvent; import javax.swing.event.UndoableEditListener; import javax.swing.text.*; import javax.swing.text.html.HTMLEditorKit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Abstract post editor with basic functionality. */ public abstract class AbstractPostEditor extends AbstractDialog { private static final Logger LOG = Logger.getLogger(PostEditor.class.getName()); private static final Insets BTN_INSETS_MAC = new Insets(2, 2, 2, 2); private static final Insets BTN_INSETS = new Insets(0, 1, 0, 1); protected final Icon iconCut = IconSource.getIcon("editor.cut.icon"); protected final Icon iconCopy = IconSource.getIcon("editor.copy.icon"); protected final Icon iconPaste = IconSource.getIcon("editor.paste.icon"); protected final Icon iconAlignLeft = IconSource.getIcon("editor.align.left.icon"); protected final Icon iconAlignCenter = IconSource.getIcon("editor.align.center.icon"); protected final Icon iconAlignRight = IconSource.getIcon("editor.align.right.icon"); protected final Icon iconStyleBold = IconSource.getIcon("editor.bold.icon"); protected final Icon iconStyleItalic = IconSource.getIcon("editor.italic.icon"); protected final Icon iconStyleUnderline = IconSource.getIcon("editor.underline.icon"); protected final Icon iconStyleStrikethrough = IconSource.getIcon("editor.strikethrough.icon"); protected final PostToBlogAction actPostToBlog; protected JButton btnPostToBlog; protected JButton btnCancel; protected JTextField tfTitle; protected JEditorPane tfText; protected JCheckBox chDraft; protected JComboBox cbInsertLink; /** Listener for the edits on the current document. */ protected UndoableEditListener undoHandler = new UndoHandler(); /** UndoManager that we add edits to. */ protected UndoManager undo = new UndoManager(); protected UndoAction undoAction = new UndoAction(); protected RedoAction redoAction = new RedoAction(); protected Action cutAction = new DefaultEditorKit.CutAction(); protected Action copyAction = new DefaultEditorKit.CopyAction(); protected Action pasteAction = new DefaultEditorKit.PasteAction(); protected Action alignLeftAction = new StyledEditorKit.AlignmentAction(null, StyleConstants.ALIGN_LEFT); protected Action alignCenterAction = new StyledEditorKit.AlignmentAction(null, StyleConstants.ALIGN_CENTER); protected Action alignRightAction = new StyledEditorKit.AlignmentAction(null, StyleConstants.ALIGN_RIGHT); protected Action boldAction; protected Action italicAction; protected Action strikethroughAction; protected Action underlineAction; // Source data private URL sourceURL; private String sourceTitle; protected ProgressSpinner spnProgress; public AbstractPostEditor(Frame frame, boolean richText) { super(frame, Strings.message("ptb.editor.post.to.blog")); actPostToBlog = new PostToBlogAction(); btnPostToBlog = new JButton(actPostToBlog); btnCancel = createCancelButton(); tfTitle = new JTextField(); tfText = new JEditorPane(); chDraft = new JCheckBox(Strings.message("ptb.editor.post.as.draft")); configureTextBox(richText); tfText.addMouseListener(createRightClickMenuAdapter()); cbInsertLink = new JComboBox(new Object[] { Strings.message("ptb.editor.insert"), new InsertUserLinkAction( AbstractPostEditor.this, tfText, Strings.message("ptb.editor.insert.link.user")) }); cbInsertLink.addItemListener(new InsertLinkBoxItemListener(tfText)); spnProgress = new ProgressSpinner(); spnProgress.setText(Strings.message("ptb.editor.posting")); setModal(false); } protected void configureTextBox(boolean html) { // Configure the kit and the document StyledEditorKit kit = html ? new HTMLEditorKit() : new StyledEditorKit(); Document document = kit.createDefaultDocument(); document.addUndoableEditListener(undoHandler); resetUndoManager(); // Apply the kit and the document tfText.setEditorKit(kit); tfText.setDocument(document); // Configure actions if (html) { boldAction = new StyledEditorKit.BoldAction(); italicAction = new StyledEditorKit.ItalicAction(); strikethroughAction = new StrikeThroughAction(); underlineAction = new StyledEditorKit.UnderlineAction(); } else { boldAction = new InsertTextAction("plain-bold", "<strong>{0}</strong>"); italicAction = new InsertTextAction("plain-italic", "<i>{0}</i>"); strikethroughAction = new InsertTextAction("plain-strike", "<del>{0}</del>"); underlineAction = new InsertTextAction("plain-under", "<u>{0}</u>"); alignCenterAction.setEnabled(false); alignLeftAction.setEnabled(false); alignRightAction.setEnabled(false); } } /** Action for insert commands. */ private class InsertTextAction extends StyledEditorKit.StyledTextAction { private final String fmt; /** * Creates the action. * * @param nm action name. * @param fmt format string. */ public InsertTextAction(String nm, String fmt) { super(nm); this.fmt = fmt; } /** Invoked when an action occurs. */ public void actionPerformed(ActionEvent e) { JEditorPane ed = getEditor(e); String sel = ed.getSelectedText(); if (sel == null) sel = ""; ed.replaceSelection(MessageFormat.format(fmt, sel)); } } /** * Creates content pane. * * @return content pane. */ protected JComponent buildContent() { JPanel panel = new JPanel(new BorderLayout()); panel.add(buildMainPanel(), BorderLayout.CENTER); panel.add(buildButtonsBar(), BorderLayout.SOUTH); return panel; } /** * Creates main content panel. * * @return main panel. */ protected Component buildMainPanel() { Component controlPanel = buildControlPanel(); BBFormBuilder builder = new BBFormBuilder("max(p;150dlu):grow, 0"); builder.setDefaultDialogBorder(); builder.append(tfTitle, 2); builder.appendUnrelatedComponentsGapRow(2); builder.append(controlPanel, 2); builder.appendRelatedComponentsGapRow(2); builder.appendRow("50dlu:grow"); builder.append(new JScrollPane(tfText), 2, CellConstraints.FILL, CellConstraints.FILL); addCustomPanels(builder); return builder.getPanel(); } /** * The hook to add more custom panels after the main part. * * @param builder builder. */ protected void addCustomPanels(BBFormBuilder builder) { } /** * Creates buttons bar. * * @return bar. */ protected Component buildButtonsBar() { ButtonBarBuilder builder = new ButtonBarBuilder(); builder.getPanel().setBorder(Constants.DIALOG_BUTTON_BAR_BORDER); builder.addFixed(spnProgress); builder.addUnrelatedGap(); builder.addGlue(); addCustomButtons(builder); builder.addGriddedButtons(new JButton[] { btnPostToBlog, btnCancel }); return builder.getPanel(); } /** * The hook to add more buttons. * * @param builder builder. */ protected void addCustomButtons(ButtonBarBuilder builder) { } /** * Builds RTF control panel. * * @return panel. */ private Component buildControlPanel() { int spcInGroup = SystemUtils.IS_OS_MAC ? 0 : 4; int spcBetweenGroups = 8; String cols = MessageFormat.format("p, {0}px, p, {0}px, p, {1}px, " + "p, {0}px, p, {0}px, p, {0}px, p, {1}px, " + "p, {1}px:grow, " + "p", spcInGroup, spcBetweenGroups); BBFormBuilder builder = new BBFormBuilder(cols); registerActionShortcut(undoAction, KeyEvent.VK_Z, Event.CTRL_MASK); registerActionShortcut(redoAction, KeyEvent.VK_Z, Event.CTRL_MASK | Event.SHIFT_MASK); builder.append(createButton(alignLeftAction, iconAlignLeft, -1, -1)); builder.append(createButton(alignCenterAction, iconAlignCenter, -1, -1)); builder.append(createButton(alignRightAction, iconAlignRight, -1, -1)); builder.append(createButton(boldAction, iconStyleBold, KeyEvent.VK_B, Event.CTRL_MASK)); builder.append(createButton(italicAction, iconStyleItalic, KeyEvent.VK_I, Event.CTRL_MASK)); builder.append(createButton(underlineAction, iconStyleUnderline, KeyEvent.VK_U, Event.CTRL_MASK)); builder.append(createButton(strikethroughAction, iconStyleStrikethrough, KeyEvent.VK_S, Event.CTRL_MASK)); builder.append(cbInsertLink); addCustomControls(builder); return builder.getPanel(); } /** * The hook to add custom control elements to the toolbar. * * @param builder builder. */ protected void addCustomControls(BBFormBuilder builder) { } /** * Creates menu adapter. * * @return menu adapter. */ private PopupAdapter createRightClickMenuAdapter() { return new PopupAdapter() { protected JPopupMenu buildPopupMenu(MouseEvent mouseEvent) { JPopupMenu menu = new JPopupMenu(); menu.add(new JMenuItem(new ActionWrapper(cutAction, "ptb.editor.cut", iconCut))); menu.add(new JMenuItem(new ActionWrapper(copyAction, "ptb.editor.copy", iconCopy))); menu.add(new JMenuItem(new ActionWrapper(pasteAction, "ptb.editor.paste", iconPaste))); menu.addSeparator(); menu.add(new JMenuItem(new ActionWrapper(alignLeftAction, "ptb.editor.align.left", iconAlignLeft))); menu.add(new JMenuItem( new ActionWrapper(alignCenterAction, "ptb.editor.align.center", iconAlignCenter))); menu.add(new JMenuItem( new ActionWrapper(alignRightAction, "ptb.editor.align.right", iconAlignRight))); menu.addSeparator(); menu.add(new JMenuItem(new ActionWrapper(boldAction, "ptb.editor.style.bold", iconStyleBold))); menu.add( new JMenuItem(new ActionWrapper(italicAction, "ptb.editor.style.italic", iconStyleItalic))); menu.add(new JMenuItem( new ActionWrapper(underlineAction, "ptb.editor.style.underline", iconStyleUnderline))); menu.add(new JMenuItem(new ActionWrapper(strikethroughAction, "ptb.editor.style.strikethrough", iconStyleStrikethrough))); return menu; } }; } /** * Resets undo manager. */ protected void resetUndoManager() { undo.discardAllEdits(); undoAction.update(); redoAction.update(); } /** * Invoked when a user closes window */ protected void doCloseWindow() { String[] options = { Strings.message("ptb.editor.warning.post"), Strings.message("ptb.editor.warning.discard"), Strings.message("ptb.editor.warning.cancel") }; int res = JOptionPane.showOptionDialog(getParent(), Strings.message("ptb.editor.warning.wording"), getTitle(), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); switch (res) { case 0: // Post actPostToBlog.actionPerformed(null); break; case 1: // Discard doCancel(); break; default: break; } } /** * Sets editor font. * * @param font font. */ public void setEditorFont(Font font) { UifUtilities.setEditorFont(tfText, font); } /** * Sets the title of the post. * * @param title title. */ public void setPostTitle(String title) { tfTitle.setText(title); } /** * Sets the text of the post. * * @param text text. */ public void setPostText(String text) { tfText.setText(cleanHTML(text)); tfText.setCaretPosition(0); } /** * Sets the draft of the post. * * @param draft draft. */ public void setDraft(boolean draft) { chDraft.setSelected(draft); } /** * Notifies of a source of the original article. * * @param feedURL URL. * @param feedTitle title. */ public void setSource(URL feedURL, String feedTitle) { sourceURL = feedURL; sourceTitle = feedTitle; } /** * Updates the title of the dialog depending on the number of blogs selected. * * @param blogs selected blogs. */ protected void updateTitle(TargetBlog[] blogs) { // TODO: localize! switch (blogs.length) { case 0: setTitle("Post to Blog"); break; case 1: setTitle("Post to Blog: " + blogs[0].getTitle()); break; default: setTitle("Posting to " + blogs.length + " blogs"); } } /** * Sets source article and initializes links from it. * * @param source source article. */ public void setSourceArticle(IArticle source) { // Remove all but the first item int ic = cbInsertLink.getItemCount(); for (int i = ic - 1; i > 1; i--) cbInsertLink.removeItemAt(i); if (source == null) return; // Add article link item if the link is present if (source.getLink() != null) { InsertStaticLinkAction item = new InsertStaticLinkAction(tfText, source.getLink().toString(), source.getTitle(), Strings.message("ptb.editor.insert.link.article")); cbInsertLink.addItem(item); } // Add feed link item if present IFeed feed = source.getFeed(); if (feed instanceof DirectFeed) { DirectFeed dfeed = (DirectFeed) feed; URL siteURL = dfeed.getSiteURL(); if (siteURL != null) { InsertStaticLinkAction item = new InsertStaticLinkAction(tfText, siteURL.toString(), feed.getTitle(), Strings.message("ptb.editor.insert.link.feed")); cbInsertLink.addItem(item); } } } /** * Sets resizable flag. */ protected void setResizable() { setResizable(true); } /** * Shortcut for button creation. * * @param act button action. * @param icon button icon. * @param key key for shortcut. * @param modifiers key modifiers. * * @return button. */ protected JButton createButton(Action act, Icon icon, int key, int modifiers) { JButton btn = new JButton(act); btn.setText(null); btn.setMargin(SystemUtils.IS_OS_MAC ? BTN_INSETS_MAC : BTN_INSETS); btn.setIcon(icon); registerActionShortcut(act, key, modifiers); return btn; } /** * Registers keyboard shortcut for action. * * @param act action. * @param key key for shortcut. * @param modifiers key modifiers. */ protected void registerActionShortcut(Action act, int key, int modifiers) { if (key != -1) { if (modifiers == Event.CTRL_MASK && SystemUtils.IS_OS_MAC) modifiers = Event.META_MASK; KeyStroke ks = KeyStroke.getKeyStroke(key, modifiers); tfText.getKeymap().addActionForKeyStroke(ks, act); } } /** * Returns the list of selected target blog indexes. * * @return the list. */ public List<Integer> getSelectedBlogIndices() { List<Integer> sel = new ArrayList<Integer>(); sel.add(0); return sel; } /** * Enables / disables all controls during posting. * * @param en <code>TRUE</code> to enable. */ protected void enableControls(boolean en) { tfTitle.setEnabled(en); tfText.setEnabled(en); chDraft.setEnabled(en); cbInsertLink.setEnabled(en); undoAction.setEnabled(en); redoAction.setEnabled(en); cutAction.setEnabled(en); copyAction.setEnabled(en); pasteAction.setEnabled(en); boldAction.setEnabled(en); italicAction.setEnabled(en); strikethroughAction.setEnabled(en); underlineAction.setEnabled(en); alignLeftAction.setEnabled(en); alignCenterAction.setEnabled(en); alignRightAction.setEnabled(en); btnPostToBlog.setEnabled(en); btnCancel.setEnabled(en); } /** * Resizing hook. * * @param component component. */ protected void resizeHook(JComponent component) { component.setPreferredSize(Resizer.FOUR2THREE.fromHeight(500)); } /** * Prepares text for sending to the server. The text contains unnecessary HTML headers and * some redundant styles we wish to clean. * * @param text text. * * @return the result. */ static String cleanHTML(String text) { if (StringUtils.isNotEmpty(text)) { // Remove leading and trailing tags, and leading spaces text = Pattern.compile("(^.*<body>|</body>.*$)", Pattern.DOTALL).matcher(text).replaceAll(""); text = Pattern.compile("^\\s+|\\s+$", Pattern.MULTILINE).matcher(text).replaceAll(""); // Replace carrige returns with spaces text = Pattern.compile("\n", Pattern.MULTILINE).matcher(text).replaceAll(" "); // De-entitize Matcher m = Pattern.compile("&#([0-9]+);").matcher(text); StringBuffer b = new StringBuffer(text.length()); while (m.find()) { String n = m.group(1); char ch = (char) Integer.parseInt(n); m.appendReplacement(b, "" + ch); } m.appendTail(b); text = b.toString(); // Beautifying text = text.replaceAll("<(p|ol|ul)>", "\n\n$0"); text = text.replaceAll("<br\\s*/?>", "$0\n"); text = text.replaceAll("</(u|o)l>", "\n$0"); text = text.replaceAll("<li>", "\n $0"); text = text.trim(); } return text; } /** * Scans the list of given categories and returns the one which is like the given. * * @param categories categories. * @param cat category to find among these in the list. * * @return a category or <code>NULL</code>. */ protected static TargetBlog.Category findEqualCategory(ArrayListModel categories, TargetBlog.Category cat) { TargetBlog.Category found = null; if (cat != null) { for (Object o : categories) { TargetBlog.Category c = (TargetBlog.Category) o; if (c != null && c.equals(cat)) found = c; } } return found; } /** Strikethrough style action. */ class StrikeThroughAction extends StyledEditorKit.StyledTextAction { /** Creates action. */ public StrikeThroughAction() { super(StyleConstants.StrikeThrough.toString()); } /** * Invoked when a user presses ctrl-s or clicks strikethrough button. * * @param ae event. */ public void actionPerformed(ActionEvent ae) { JEditorPane editor = getEditor(ae); if (editor != null) { StyledEditorKit kit = getStyledEditorKit(editor); MutableAttributeSet attr = kit.getInputAttributes(); boolean strikeThrough = !StyleConstants.isStrikeThrough(attr); SimpleAttributeSet sas = new SimpleAttributeSet(); StyleConstants.setStrikeThrough(sas, strikeThrough); setCharacterAttributes(editor, sas, false); } } } /** Posts current contents to targetBlog. */ protected class PostToBlogAction extends AbstractAction { private final boolean cont; /** Creates action. */ public PostToBlogAction() { this(false); } /** * Defines an <code>Action</code> object with a default * description string and default icon. * * @param cont <code>TRUE</code> to continue on success. */ public PostToBlogAction(boolean cont) { super(cont ? Strings.message("ptb.editor.post.and.continue") : Strings.message("ptb.editor.post.to.blog")); this.cont = cont; } /** * Invoked when an action occurs. * * @param e event. */ public void actionPerformed(ActionEvent e) { enableControls(false); spnProgress.start(); final String title = tfTitle.getText(); final String text = cleanHTML(tfText.getText()); final boolean publish = !chDraft.isSelected(); new Thread("Posting to Blog") { { setDaemon(true); } public void run() { WeblogAPIException ex = null; WeblogPost post = new WeblogPost(title, text, sourceURL, sourceTitle, publish); addProperties(post); List<Integer> ind = getSelectedBlogIndices(); for (int i : ind) { TargetBlog targetBlog = getBlogAt(i); IWeblogAPI api = targetBlog.getApiType(); post.categories = getCategoriesForBlog(i); // Perform a call try { api.newPost(targetBlog, post); } catch (WeblogAPIException e1) { ex = e1; LOG.log(Level.WARNING, e1.getMessage(), e1); } if (ex != null) break; } // Report the results final WeblogAPIException err = ex; SwingUtilities.invokeLater(new Runnable() { public void run() { enableControls(true); spnProgress.stop(); if (err == null) onSuccess(); else onError(err); } }); } }.start(); } /** Invoked when sending the post was successfull. */ private void onSuccess() { if (!cont) doClose(); } /** * Invoked when sending the post errored out. * * @param ex exception. */ private void onError(WeblogAPIException ex) { JOptionPane.showMessageDialog(AbstractPostEditor.this, "<html><b>Posting to Blog failed</b>\n\n" + ex.getMessage(), AbstractPostEditor.this.getTitle(), JOptionPane.WARNING_MESSAGE); } } /** * Returns the list of categories selected for the given blog. * * @param blogIndex index of blog in list. * * @return categories. */ protected abstract List<TargetBlog.Category> getCategoriesForBlog(int blogIndex); /** * Adds more specific properties before sending. * * @param post post. */ protected void addProperties(WeblogPost post) { } /** * Returns a blog at a given index. * * @param i index. * * @return blog. */ protected abstract TargetBlog getBlogAt(int i); /** Action wrapper to alter title and icon of the target action. */ class ActionWrapper extends AbstractAction { private final Action target; /** * Create titled and iconed wrapper. * * @param target target action. * @param titleID resource ID of the title. * @param icon icon. */ public ActionWrapper(Action target, String titleID, Icon icon) { super(Strings.message(titleID), icon); this.target = target; } /** * Executes target action. * * @param e event. */ public void actionPerformed(ActionEvent e) { target.actionPerformed(e); } } /** * Handler of UNDO / REDO operations. */ class UndoHandler implements UndoableEditListener { /** * Messaged when the Document has created an edit, the edit is * added to <code>undo</code>, an instance of UndoManager. */ public void undoableEditHappened(UndoableEditEvent e) { undo.addEdit(e.getEdit()); undoAction.update(); redoAction.update(); } } /** * UNDO action. */ class UndoAction extends AbstractAction { /** Constructor. */ public UndoAction() { super("Undo"); setEnabled(false); } /** * Invoked when a user presses ctrl-z or clicks undo button. * * @param e even. */ public void actionPerformed(ActionEvent e) { try { undo.undo(); } catch (CannotUndoException ex) { System.out.println("Unable to undo: " + ex); ex.printStackTrace(); } update(); redoAction.update(); } /** Updates the state. */ protected void update() { setEnabled(undo.canUndo()); } } /** Redo action. */ class RedoAction extends AbstractAction { /** Creates action. */ public RedoAction() { super("Redo"); setEnabled(false); } /** * Invoked when a user presses ctrl-shift-z or clicks redo button. * * @param e event. */ public void actionPerformed(ActionEvent e) { try { undo.redo(); } catch (CannotRedoException ex) { System.err.println("Unable to redo: " + ex); ex.printStackTrace(); } update(); undoAction.update(); } /** Updates the state. */ protected void update() { setEnabled(undo.canRedo()); } } }