/*
* jMemorize - Learning made easy (and fun) - A Leitner flashcards tool
* Copyright(C) 2004-2008 Riad Djemili and contributors
*
* 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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package jmemorize.gui.swing.panels;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.event.CaretListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BoxView;
import javax.swing.text.ComponentView;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.IconView;
import javax.swing.text.LabelView;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.ParagraphView;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import jmemorize.core.FormattedText;
import jmemorize.core.Main;
import jmemorize.gui.LC;
import jmemorize.gui.Localization;
import jmemorize.gui.swing.CardFont;
import jmemorize.gui.swing.ColorConstants;
/**
* @author djemili
*/
public class CardSidePanel extends JPanel
{
public interface CardImageObserver
{
public void onImageChanged();
}
private class ScaledImagePanel extends JPanel
{
private Image m_image;
private int m_padding = 2;
public void setImageToDisplay(Image imageToDisplay)
{
m_image = imageToDisplay;
}
@Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
if (m_image == null)
return;
int imgWidth = m_image.getWidth(null);
int imgHeight = m_image.getHeight(null);
Dimension dimension = getSize();
int w = dimension.width;
int h = dimension.height;
int padding = 0;
if (imgWidth > w || imgHeight > h)
{
float ratio = imgWidth / (float)w;
h = (int)(imgHeight / ratio);
if (h > dimension.height)
{
h = dimension.height;
ratio = imgHeight / (float)h;
w = (int)(imgWidth/ ratio);
}
padding = m_padding;
}
else
{
w = imgWidth;
h = imgHeight;
}
int left = padding + (dimension.width - w) / 2;
int top = padding + (dimension.height - h) / 2;
if (g instanceof Graphics2D)
{
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
g.drawImage(m_image,
left, top, left + w - 2*padding, top + h - 2*padding,
0, 0, imgWidth, imgHeight, null);
}
}
private class MyEditorKit extends StyledEditorKit
{
public ViewFactory getViewFactory()
{
return new StyledViewFactory();
}
// TODO make static
class StyledViewFactory implements ViewFactory
{
/* (non-Javadoc)
* @see javax.swing.text.ViewFactory
*/
public View create(Element elem)
{
String kind = elem.getName();
if (kind != null)
{
if (kind.equals(AbstractDocument.ContentElementName))
{
return new LabelView(elem);
}
else if (kind.equals(AbstractDocument.ParagraphElementName))
{
return new ParagraphView(elem);
}
else if (kind.equals(AbstractDocument.SectionElementName))
{
return new CenteredBoxView(elem, View.Y_AXIS);
}
else if (kind.equals(StyleConstants.ComponentElementName))
{
return new ComponentView(elem);
}
else if (kind.equals(StyleConstants.IconElementName))
{
return new IconView(elem);
}
}
return new LabelView(elem); // default to text display
}
}
}
private class CenteredBoxView extends BoxView
{
public CenteredBoxView(Element elem, int axis)
{
super(elem, axis);
}
/* (non-Javadoc)
* @see javax.swing.text.BoxView
*/
protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans)
{
super.layoutMajorAxis(targetSpan, axis, offsets, spans);
int textBlockHeight = 0;
int offset = 0;
for (int i = 0; i < spans.length; i++)
{
textBlockHeight += spans[ i ];
}
offset = (targetSpan - textBlockHeight) / 2;
for (int i = 0; i < offsets.length; i++)
{
offsets[ i ] += offset;
}
}
}
private class SetImageModeAction implements ActionListener
{
private Mode m_mode;
public SetImageModeAction(Mode mode)
{
m_mode = mode;
}
public void actionPerformed(ActionEvent e)
{
setImageMode(m_mode);
}
}
private enum Mode {TEXT, IMAGE, TEXT_AND_IMAGE};
private JPanel m_contentPanel;
private JToolBar m_imageBar;
private JLabel m_imageLabel;
private JTextPane m_textPane = new JTextPane();
private JScrollPane m_textScrollPane = new JScrollPane(m_textPane);
private ScaledImagePanel m_imagePanel = new ScaledImagePanel();
private List<ImageIcon> m_images = new LinkedList<ImageIcon>();
private int m_currentImage = 0;
private Mode m_mode;
private List<CardImageObserver> m_imageObservers = new LinkedList<CardImageObserver>();
private CardFont m_cardFont;
private JButton m_prevImageButton;
private JButton m_nextImageButton;
private JButton m_textModeButton;
private JButton m_imageModeButton;
private JButton m_imageTexModeButton;
public CardSidePanel()
{
initComponents();
setupTabBehavior();
setupShiftBavior();
updateImage();
setImageMode(Mode.TEXT);
}
/**
* @return The text inside of the Frontside textpane.
*/
public FormattedText getText()
{
return FormattedText.formatted(m_textPane.getStyledDocument());
}
public void setEditable(boolean editable)
{
m_textPane.setEditable(editable);
}
public void requestFocus()
{
m_textPane.requestFocus();
}
public void setCardFont(CardFont cardFont)
{
m_cardFont = cardFont;
m_textPane.setFont(cardFont.getFont());
FormattedText fText = getText();
m_textPane.setEditorKit(cardFont.isVerticallyCentered() ?
new MyEditorKit() : new StyledEditorKit()); // HACK
setText(fText);
StyledDocument doc = (StyledDocument)m_textPane.getDocument();
setDocAlignment(doc, cardFont);
}
/**
* Sets the text of one EditorPane. Using EditorPane#setText caused some
* weird rendering artifacts. This methods fixes this by completly replacing
* the document by a new one.
*/
public Document setText(FormattedText text)
{
StyledDocument doc = text.getDocument();
m_textPane.setDocument(doc);
setDocAlignment(doc, m_cardFont);
clearInputAttributes(m_textPane);
// scroll to top
m_textPane.scrollRectToVisible(new Rectangle());
return doc;
}
public void setImages(List<ImageIcon> images)
{
m_images.clear();
for (ImageIcon image : images)
{
m_images.add(image);
}
m_currentImage = 0;
updateImage();
if (images.size() > 0) // HACK
{
if (m_mode != Mode.TEXT_AND_IMAGE && m_mode != Mode.IMAGE)
setImageMode(Mode.TEXT_AND_IMAGE);
}
else
{
setImageMode(Mode.TEXT);
}
}
public void addImage(ImageIcon image)
{
m_images.add(image);
m_currentImage = m_images.size() - 1;
updateImage();
if (m_images.size() == 1)
setImageMode(Mode.TEXT_AND_IMAGE);
notifyImageObservers();
}
/**
* Removes the currently visible image.
*/
public void removeImage()
{
if (m_images.size() == 0)
return;
m_images.remove(m_currentImage);
if (m_currentImage > 0)
m_currentImage--;
updateImage();
notifyImageObservers();
}
/**
* @return a unmodifiable list of the images added to this card side.
*/
public List<ImageIcon> getImages()
{
return Collections.unmodifiableList(m_images);
}
public void addCaretListener(CaretListener listener)
{
m_textPane.addCaretListener(listener);
/*
* Our problem is that the TextPane inserts new CaretListeners at the
* first position. Because we add our text actions after the editor kit
* has already been implictly set, they will get fired before the
* AttributeTracker in StyledEditorKit (which also listens as caret
* listener) has the chance to update the input attributes which our
* text actions rely on. By resetting the editor kit, the editor kit
* will be removed from the caret listeners and reinserted at the first
* position, so that our text actions can correctly access the current
* input attributes.
*/
m_textPane.setEditorKit(m_textPane.getEditorKit());
}
public void addImageListener(CardImageObserver listener)
{
if (!m_imageObservers.contains(listener))
m_imageObservers.add(listener);
}
public JTextPane getTextPane()
{
return m_textPane;
}
private void notifyImageObservers()
{
for (CardImageObserver observer : m_imageObservers)
observer.onImageChanged();
}
private void setImageMode(Mode mode)
{
m_mode = mode;
m_textModeButton.setSelected(mode == Mode.TEXT);
m_imageModeButton.setSelected(mode == Mode.IMAGE);
m_imageTexModeButton.setSelected(mode == Mode.TEXT_AND_IMAGE);
m_contentPanel.removeAll();
// JScrollPane textScrollPane = new JScrollPane(m_textPane);
// textScrollPane.setBorder(null);
switch (mode)
{
case TEXT:
m_contentPanel.setLayout(new BorderLayout());
m_contentPanel.add(m_textScrollPane, BorderLayout.CENTER);
m_textPane.requestFocus();
break;
case IMAGE:
m_contentPanel.setLayout(new BorderLayout());
m_contentPanel.add(m_imagePanel, BorderLayout.CENTER);
break;
case TEXT_AND_IMAGE:
m_contentPanel.setLayout(new GridLayout(1, 2));
m_contentPanel.add(m_textScrollPane, BorderLayout.CENTER);
m_contentPanel.add(m_imagePanel, BorderLayout.EAST);
m_textPane.requestFocus();
break;
}
// Document doc = m_textPane.getDocument();
// m_textPane.setDocument(new DefaultStyledDocument());
// m_textPane.setDocument(doc);
//
m_imagePanel.validate();
m_textPane.validate();
m_contentPanel.validate();
m_contentPanel.repaint();
}
private void clearInputAttributes(JEditorPane editorPane)
{
StyledEditorKit kit = (StyledEditorKit)editorPane.getEditorKit();
MutableAttributeSet attr = kit.getInputAttributes();
attr.removeAttributes(attr.getAttributeNames());
}
private void setDocAlignment(StyledDocument doc, CardFont cardFont)
{
int swingAlign = StyleConstants.ALIGN_LEFT;
if (cardFont != null)
swingAlign = cardFont.getSwingAlign();
SimpleAttributeSet sas = new SimpleAttributeSet();
StyleConstants.setAlignment(sas, swingAlign);
doc.setParagraphAttributes(0, doc.getLength() + 1, sas, false);
}
private void setupTabBehavior()
{
// focus next pane with TAB instead of CTRL+TAB
Set<KeyStroke> key = new HashSet<KeyStroke>();
key.add(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0));
int forwardTraversal = KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS;
m_textPane.setFocusTraversalKeys(forwardTraversal, key);
// focus previous pane with SHIFT+TAB instead of SHIFT+CTRL+TAB
key = new HashSet<KeyStroke>();
key.add(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK));
int backwardTraversal = KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS;
m_textPane.setFocusTraversalKeys(backwardTraversal, key);
int shortcutKey = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
KeyStroke ctrlTab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, shortcutKey);
// insert tab with CTRL+TAB instead of TAB
m_textPane.getInputMap(JComponent.WHEN_FOCUSED).put(ctrlTab,
DefaultEditorKit.insertTabAction);
}
private void setupShiftBavior()
{
int shift = InputEvent.SHIFT_DOWN_MASK;
InputMap inputMap = m_textPane.getInputMap(JComponent.WHEN_FOCUSED);
KeyStroke shiftDel = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, shift);
inputMap.put(shiftDel, DefaultEditorKit.deleteNextCharAction);
KeyStroke shiftBS = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, shift);
inputMap.put(shiftBS, DefaultEditorKit.deletePrevCharAction);
}
private void updateImage()
{
int imgCount = m_images.size();
m_imageBar.setVisible(imgCount > 0);
if (imgCount == 0)
{
setImageMode(Mode.TEXT);
}
else
{
String text = String.format(" %s %d/%d ", //$NON-NLS-1$
Localization.get(LC.IMAGE), m_currentImage + 1, imgCount);
m_imageLabel.setText(text);
m_imagePanel.setImageToDisplay(m_images.get(m_currentImage).getImage());
m_imagePanel.repaint();
}
}
private void initComponents()
{
buildImageBar();
m_textPane.setBackground(ColorConstants.CARD_PANEL_COLOR);
m_textScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
m_textScrollPane.setBorder(null);
m_contentPanel = new JPanel(new BorderLayout());
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(m_contentPanel, BorderLayout.CENTER);
mainPanel.add(m_imageBar, BorderLayout.SOUTH);
m_imagePanel.setBackground(m_textPane.getBackground());
m_imagePanel.setForeground(m_textPane.getForeground());
m_imagePanel.addMouseListener(new MouseAdapter(){
@Override
public void mousePressed(MouseEvent e)
{
if (SwingUtilities.isLeftMouseButton(e))
m_nextImageButton.doClick();
}
});
// we want to use the default scrollpane border
Color color = UIManager.getColor("InternalFrame.borderShadow"); //$NON-NLS-1$
if (color == null)
{
color = new Color(167, 166, 170);
Main.getLogger().warning("UI key for card side border not found!"); //$NON-NLS-1$
}
Border border = new LineBorder(color);
mainPanel.setBorder(border);
setLayout(new BorderLayout());
add(mainPanel, BorderLayout.CENTER);
}
private void buildImageBar()
{
m_imageBar = new JToolBar();
m_imageBar.setBackground(ColorConstants.SIDEBAR_COLOR);
m_imageBar.setFloatable(false);
m_imageLabel = new JLabel();
m_imageLabel.setHorizontalAlignment(StyleConstants.ALIGN_LEFT);
m_imageBar.add(m_imageLabel);
m_prevImageButton = new JButton(loadIcon("arrow_left.png")); //$NON-NLS-1$
m_prevImageButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
if (m_currentImage > 0)
m_currentImage--;
else
m_currentImage = m_images.size() - 1;
updateImage();
m_textPane.requestFocus();
}
});
m_imageBar.add(m_prevImageButton);
m_nextImageButton = new JButton(loadIcon("arrow_right.png")); //$NON-NLS-1$
m_nextImageButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
if (m_currentImage < m_images.size() - 1)
m_currentImage++;
else
m_currentImage = 0;
updateImage();
m_textPane.requestFocus();
}
});
m_imageBar.add(m_nextImageButton);
m_imageBar.addSeparator();
m_imageTexModeButton = new JButton(loadIcon("picture_and_text.png")); //$NON-NLS-1$
m_imageTexModeButton.addActionListener(new SetImageModeAction(Mode.TEXT_AND_IMAGE));
m_imageBar.add(m_imageTexModeButton);
m_textModeButton = new JButton(loadIcon("text.png")); //$NON-NLS-1$
m_imageBar.add(m_textModeButton);
m_textModeButton.addActionListener(new SetImageModeAction(Mode.TEXT));
m_imageModeButton = new JButton(loadIcon("picture.png")); //$NON-NLS-1$
m_imageBar.add(m_imageModeButton);
m_imageModeButton.addActionListener(new SetImageModeAction(Mode.IMAGE));
}
private ImageIcon loadIcon(String imgName)
{
String path = "/resource/icons/"+imgName; //$NON-NLS-1$
return new ImageIcon(getClass().getResource(path));
}
}