/* ****************************************************************************** * * Copyright 2008-2010 Hans Dijkema * * JRichTextEditor is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * JRichTextEditor 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with JRichTextEditor. If not, see <http://www.gnu.org/licenses/>. * * ******************************************************************************/ package nl.dykema.jxmlnote.widgets; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.print.PrinterException; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.Vector; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JTextPane; import javax.swing.KeyStroke; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultCaret; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import nl.dykema.jxmlnote.clipboard.XMLNoteTransferHandler; import nl.dykema.jxmlnote.document.DocumentAdminEvent; import nl.dykema.jxmlnote.document.DocumentAdminListener; import nl.dykema.jxmlnote.document.XMLNoteDocument; import nl.dykema.jxmlnote.document.XMLNoteMark; import nl.dykema.jxmlnote.exceptions.BadStyleException; import nl.dykema.jxmlnote.exceptions.MarkExistsException; import nl.dykema.jxmlnote.exceptions.MarkNoExistException; import nl.dykema.jxmlnote.exceptions.NoSelectionException; import nl.dykema.jxmlnote.exceptions.NoStyleException; import nl.dykema.jxmlnote.interfaces.MarkMarkupProviderMaker; import nl.dykema.jxmlnote.interfaces.UpdateViewListener; import nl.dykema.jxmlnote.toolbar.JXMLNoteToolBar; import nl.dykema.jxmlnote.undo.JXMLNotePaneCaretUndoableEdit; import nl.dykema.jxmlnote.widgets.marks.DefaultMarkMarkupProviderMaker; import nl.dykema.jxmlnote.widgets.marks.MarkMouseListener; class TrackMouseMark implements MouseListener, MouseMotionListener { public void mouseDragged(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public enum Type { MOVE_IN, MOVE_OUT, CLICKED, DOUBLE_CLICKED } private JXMLNotePane _pane; private XMLNoteMark _trackedMark; private Cursor _markCursor; public void mouseMoved(MouseEvent e) { Point p=e.getPoint(); int offset=_pane.viewToModel(p); Vector<XMLNoteMark> v=_pane.getXMLNoteDoc().getMarksForOffset(offset); if (v.isEmpty()) { if (_trackedMark!=null) { if (_markCursor!=null) { _pane.setCursor(_markCursor); _markCursor=null; } _pane.informMarkMouseListeners(Type.MOVE_OUT,_trackedMark,e); _trackedMark=null; } } else { XMLNoteMark m=v.get(0); if (_trackedMark==null) { _markCursor=_pane.getCursor(); _trackedMark=m; Cursor q=_pane.informMarkMouseListeners(Type.MOVE_IN, m,e); if (q!=null) { _pane.setCursor(q); } } else if (_trackedMark!=m) { _pane.setCursor(_markCursor); _trackedMark=m; Cursor q=_pane.informMarkMouseListeners(Type.MOVE_IN, m,e); if (q!=null) { _pane.setCursor(q); } } else { // _trackedMark == m, do nothing } } } public void mouseClicked(MouseEvent e) { Point p=e.getPoint(); int offset=_pane.viewToModel(p); Vector<XMLNoteMark> v=_pane.getXMLNoteDoc().getMarksForOffset(offset); if (!v.isEmpty()) { XMLNoteMark m=v.get(0); int count=e.getClickCount(); if (count>1) { _pane.informMarkMouseListeners(Type.DOUBLE_CLICKED,m,e); } else { _pane.informMarkMouseListeners(Type.CLICKED,m,e); } } } public TrackMouseMark(JXMLNotePane p) { _pane=p; _trackedMark=null; } } public class JXMLNotePane extends JTextPane implements DocumentAdminListener, DocumentListener { /** * Version */ private static final long serialVersionUID = 1L; private MarkMarkupProviderMaker _maker; private Set<MarkMouseListener> _markMouseListeners; private TrackMouseMark _mouseMarkTracker; /** * Use this for the property change listener for zooming factor (see setZoomFactor()). */ public final static String PROPERTY_ZOOM="zoom"; //////////////////////////////////////////////////////////////////////////////////////////////////// // Mouse listener stuff //////////////////////////////////////////////////////////////////////////////////////////////////// /** * Add a MarkMouseListener to this pane, so that you can track whether marks are clicked, * double clicked, etc. * * @param l */ public void addMarkMouseListener(MarkMouseListener l) { _markMouseListeners.add(l); } /** * Remove a previously added MarkMouseListener. * * @param l */ public void removeMarkMouseListener(MarkMouseListener l) { _markMouseListeners.remove(l); } Cursor informMarkMouseListeners(TrackMouseMark.Type type,XMLNoteMark m,MouseEvent e) { Iterator<MarkMouseListener> it=_markMouseListeners.iterator(); Cursor q=null; while (it.hasNext()) { switch (type) { case MOVE_IN: Cursor a=it.next().mouseMovedIntoMark(m,e); if (a!=null) { q=a; } break; case MOVE_OUT: it.next().mouseMovedOutOfMark(m,e); break; case CLICKED: it.next().markClicked(m,e); break; case DOUBLE_CLICKED: it.next().markDoubleClicked(m,e); break; } } return q; } //////////////////////////////////////////////////////////////////////////////////////////////////// // Methods for manipulating the document //////////////////////////////////////////////////////////////////////////////////////////////////// /** * Indent the paragraph at the caret position more (to next tab stop). * * @param points The number of points (1/72 inch) to indent. * @throws BadLocationException */ public void indentMore(int points) throws BadLocationException { int offset=getCaretPosition(); int start=this.getSelectionStart(); int end=this.getSelectionEnd(); boolean b=getXMLNoteDoc().setLongEdit(true); getXMLNoteDoc().getUndoManager().addEdit(new JXMLNotePaneCaretUndoableEdit(this)); if (start==end) { getXMLNoteDoc().indentMore(offset,0,points); } else { getXMLNoteDoc().indentMore(start,end-start,points); } getXMLNoteDoc().getUndoManager().setLongEdit(b); } /** * Indent the paragraph at the caret position less (to previous tab stop). * * @param points The number of points (1/72 inch) to indent less. * @throws BadLocationException */ public void indentLess(int points) throws BadLocationException { int offset=getCaretPosition(); int start=this.getSelectionStart(); int end=this.getSelectionEnd(); boolean b=getXMLNoteDoc().setLongEdit(true); getXMLNoteDoc().getUndoManager().addEdit(new JXMLNotePaneCaretUndoableEdit(this)); if (start==end) { getXMLNoteDoc().indentLess(offset,0,points); } else { getXMLNoteDoc().indentLess(start,end-start,points); } getXMLNoteDoc().getUndoManager().setLongEdit(b); } /** * Apply the style with id styleId to the currently selected text. This is a paragraph style. It will always * apply to the whole paragraph with the selected text. If no text is selected, this is also OK. It will apply * to the paragraph where the caret is in. * @param styleId * @throws NoSelectionException * @throws BadLocationException */ public void applyStyle(String styleId) throws NoStyleException,NoSelectionException,BadLocationException { int offset=getSelectionStart(); int end=getSelectionEnd(); int len=end-offset; if (len<0) { throw new NoSelectionException(); } boolean b=getXMLNoteDoc().setLongEdit(true); getXMLNoteDoc().getUndoManager().addEdit(new JXMLNotePaneCaretUndoableEdit(this)); getXMLNoteDoc().applyStyle(styleId,offset,len); getXMLNoteDoc().getUndoManager().setLongEdit(b); } /** * Apply the <code>alignment</code> to the currently selected text. This is a paragraph style. It will always * apply to the whole paragraph with the selected text. If no text is selected, this is also OK. It will apply * to the paragraph where the caret is in. * * <code>alignment</code> must be one of StyleConstants.ALIGN_LEFT,ALIGN_RIGHT,ALIGN_CENTER or ALIGN_JUSTIFIED. * @param alignment * @throws NoSelectionException * @throws BadLocationException */ public void applyAlign(int alignment) throws NoSelectionException,BadLocationException { int offset=getSelectionStart(); int end=getSelectionEnd(); int len=end-offset; if (len<0) { throw new NoSelectionException(); } boolean b=getXMLNoteDoc().setLongEdit(true); getXMLNoteDoc().getUndoManager().addEdit(new JXMLNotePaneCaretUndoableEdit(this)); getXMLNoteDoc().applyAlign(alignment,offset,len); getXMLNoteDoc().getUndoManager().setLongEdit(b); } /** * Applies bold character style to the current selection of the document. * * @throws BadStyleException * @throws NoSelectionException * @throws BadLocationException */ public void applyBold() throws BadStyleException,BadLocationException { try { applyCharStyle(StyleConstants.Bold); } catch (NoSelectionException e) { changeCaretStyle(StyleConstants.Bold); } } /** * Applies italic character style to the current selection of the document. * * @throws BadStyleException * @throws NoSelectionException * @throws BadLocationException */ public void applyItalic() throws BadStyleException,BadLocationException { try { applyCharStyle(StyleConstants.Italic); } catch (NoSelectionException e) { changeCaretStyle(StyleConstants.Italic); } } /** * Applies underline character style to the current selection of the document. * * @throws BadStyleException * @throws NoSelectionException * @throws BadLocationException */ public void applyUnderline() throws BadStyleException,BadLocationException { try { applyCharStyle(StyleConstants.Underline); } catch (NoSelectionException e) { changeCaretStyle(StyleConstants.Underline); } } private void applyCharStyle(Object cstyle) throws BadStyleException,NoSelectionException,BadLocationException { SimpleAttributeSet set=new SimpleAttributeSet(); set.addAttribute(cstyle, true); int offset=getSelectionStart(); int end=getSelectionEnd(); int len=end-offset; if (len<=0) { throw new NoSelectionException(); } boolean b=getXMLNoteDoc().setLongEdit(true); getXMLNoteDoc().getUndoManager().addEdit(new JXMLNotePaneCaretUndoableEdit(this)); getXMLNoteDoc().applyOneCharStyle(set,offset,len); getXMLNoteDoc().getUndoManager().setLongEdit(b); } // special case for keyboard map private void changeCaretStyle(Object cstyle) { SimpleAttributeSet s=new SimpleAttributeSet(); s.addAttribute(cstyle,true); AttributeSet set=super.getInputAttributes(); SimpleAttributeSet s1=new SimpleAttributeSet(set); if (set.containsAttributes(s)) { s1.removeAttributes(s); } else { s1.addAttributes(s); } super.setCharacterAttributes(s1, true); } /* private void applyCharStyle(Object cstyle,int len) { SimpleAttributeSet set=new SimpleAttributeSet(); set.addAttribute(cstyle, true); int offset=getCaretPosition(); boolean b=getXMLNoteDoc().setLongEdit(true); getXMLNoteDoc().getUndoManager().addEdit(new JXMLNotePaneCaretUndoableEdit(this)); try { getXMLNoteDoc().applyOneCharStyle(set,offset,len); } catch (Exception e) { // ignore } getXMLNoteDoc().getUndoManager().setLongEdit(b); } */ /** * Insert a mark of a given color for the currently selected text * @param id The id of the mark * @param _class The class of the mark * @throws NoSelectionException * @throws BadLocationException * @return */ public boolean insertMark(String id,String _class) throws MarkExistsException, NoSelectionException,BadLocationException { int offset=this.getSelectionStart(); int end=this.getSelectionEnd(); int len=end-offset; return insertMark(id,_class,offset,len); } /** * * Insert a mark of a given color for the currently selected text * @param id The id of the mark * @param _class The class of the mark * @param offset The offset in the text * @param length The length of the mark * @return * @throws MarkExistsException * @throws NoSelectionException * @throws BadLocationException */ public boolean insertMark(String id,String _class,int offset,int length) throws MarkExistsException, NoSelectionException,BadLocationException { if (length<=0) { throw new NoSelectionException(); } return getXMLNoteDoc().insertMark(id, _class,offset,length); } /** * Remove the mark with the given id. * @param id * @return */ public boolean removeMark(String id) throws MarkNoExistException { return getXMLNoteDoc().removeMark(id); } /** * Returns all marks that are part of the position of the caret. * see {@link nl.dykema.jxmlnote.document.XMLNoteDocument#getMarksForOffset(int) XMLNoteDocument.getMarksForOffset}. * @return */ public Vector<XMLNoteMark> getMarksForCaret() { return getXMLNoteDoc().getMarksForOffset(this.getCaretPosition()); } /** * Returns all marks that are part of the selection of caret. * see {@link nl.dykema.jxmlnote.document.XMLNoteDocument#getMarksForSelection(int,int) XMLNoteDocument.getMarksForSelection}. * This member throws a NoSelectionException if nothing has been selected. * * @throws NoSelectionException * @return */ public Vector<XMLNoteMark> getMarksForSelection() throws NoSelectionException { int offset=this.getSelectionStart(); int end=this.getSelectionEnd(); int len=end-offset; if (len<=0) { throw new NoSelectionException(); } return getXMLNoteDoc().getMarksForSelection(offset,end); } /** * Returns all marks that are contained in the selection, i.e. for which <code>mark.offset()>=selectionStart</code> * and <code>mark.endOffset()<selectionEnd</code>. * * see {@link nl.dykema.jxmlnote.document.XMLNoteDocment.getMarksContainedInSelection(int,in) XMLNoteDocument.getMarksContainedInSelection). * * @throws NoSelectionException * @return */ public Vector<XMLNoteMark> getMarksContainedInSelection() throws NoSelectionException { int offset=this.getSelectionStart(); int end=this.getSelectionEnd(); int len=end-offset; if (len<=0) { throw new NoSelectionException(); } return getXMLNoteDoc().getMarksContainedInSelection(offset,end); } /** * Get the associated text pane (JTextPane) * @return */ public JTextPane textPane() { return this; //_textPane; } /******************************************************************************************** * Model to view, etc., caret positioning, etc. ********************************************************************************************/ /** * Returns the position (point) in the view for an offset in the text. * * @param offset * @return * @throws BadLocationException */ public Point getPositionForOffset(int offset) throws BadLocationException { Rectangle r; r=this.modelToView(offset); Point p=new Point(r.x,r.y); return p; } /** * Sets the caret on position 0 in the document and also clears any selection. */ public void setCaretPositionHome() { setCaretPosition(0); setSelectionStart(0); setSelectionEnd(0); } /** * Returns the size of the current selection (0 if there is no selection). * @return */ public int selectionSize() { return getSelectionEnd()-getSelectionStart(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Document Administration //////////////////////////////////////////////////////////////////////////////////////////////////// public boolean documentWillBeReset(DocumentAdminEvent e) { // does nothing return false; } public void documentHasBeenReset(DocumentAdminEvent e) { this.setCaretPosition(0); } public boolean documentWillBeCleared(DocumentAdminEvent e) { // does nothing return false; } public void documentHasBeenCleared(DocumentAdminEvent e) { // does nothing } public void documentChangedState(DocumentAdminEvent e) { // does nothing } //////////////////////////////////////////////////////////////////////////////////////////////////// // View updating //////////////////////////////////////////////////////////////////////////////////////////////////// private Set<UpdateViewListener> _updateViewListeners; /** * Adds an UpdateViewListener to this pane, which is called for every document change. */ public void addUpdateViewListener(UpdateViewListener l) { _updateViewListeners.add(l); } /** * Removes a previously added UpdateViewListener from this pane. */ public void removeUpdateViewListener(UpdateViewListener l) { _updateViewListeners.remove(l); } public void changedUpdate(DocumentEvent e) { informUpdateViewListeners(e); } public void insertUpdate(DocumentEvent e) { informUpdateViewListeners(e); } public void removeUpdate(DocumentEvent e) { informUpdateViewListeners(e); } private void informUpdateViewListeners(DocumentEvent e) { Iterator<UpdateViewListener> it=_updateViewListeners.iterator(); while (it.hasNext()) { it.next().updateView(e); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // Document handling //////////////////////////////////////////////////////////////////////////////////////////////////// /** * Returns the associated document as XMLNoteDocument. * @return */ public XMLNoteDocument getXMLNoteDoc() { return (XMLNoteDocument) super.getDocument(); } /** * Sets the document for this JXMLNotePane (please don't use textPane().setDocument(). * Note! external administrations to the current Document (e.g. with mark ids etc) may need to be cleared * as well. * @param d */ public void setDocument(XMLNoteDocument d) { if (getXMLNoteDoc()!=null) { getXMLNoteDoc().removeHighlighterForView(this.getHighlighter()); getXMLNoteDoc().removeDocumentAdminListener(this); getXMLNoteDoc().removeDocumentListener(this); } super.setDocument(d); getXMLNoteDoc().installHighlighterForView(this.getHighlighter(),_maker); getXMLNoteDoc().addDocumentAdminListener(this); getXMLNoteDoc().addDocumentListener(this); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Mark markup //////////////////////////////////////////////////////////////////////////////////////////////////// /** * Sets a new MarkMarkupProvider for this pane. Removes the previous one and reinstalls * the next one. * @param maker */ public void setMarkMarkupProviderMaker(MarkMarkupProviderMaker maker) { getXMLNoteDoc().removeHighlighterForView(this.getHighlighter()); _maker=maker; getXMLNoteDoc().installHighlighterForView(this.getHighlighter(),_maker); } /** * Returns the MarkMarkupProviderMaker associated with this pane. * * @return the current MarkMarkupProviderMaker */ public MarkMarkupProviderMaker getMarkMarkupProviderMaker() { return _maker; } //////////////////////////////////////////////////////////////////////////////////////////////////// // Scaling for real screen DPI's //////////////////////////////////////////////////////////////////////////////////////////////////// private double _zoomFactor; public double getZoomFactor() { return _zoomFactor; } public double setZoomFactor(double _new) { double n=_zoomFactor; _zoomFactor=_new; if (super.isVisible()) { this.getXMLNoteDoc().fireZoomChanged(); this.informUpdateViewListeners(new RulerRepaintEvent()); this.firePropertyChange(JXMLNotePane.PROPERTY_ZOOM, n, _new); } return n; } public void initZoomFactor() { _zoomFactor=1.0; } /*public void repaint(int x, int y, int width, int height) { super.repaint(0, 0, getWidth(), getHeight()); }*/ /*protected EditorKit createDefaultEditorKit() { return new XMLNoteEditorKit(this); }*/ public boolean print() throws PrinterException { double previousFactor=setZoomFactor(1.0); boolean b=super.print(); setZoomFactor(previousFactor); return b; } /* public void paint(Graphics g) { if (this.isVisible()) { AffineTransform at=((Graphics2D) g).getTransform(); at.concatenate(((Graphics2D) g).getDeviceConfiguration().getNormalizingTransform()); ((Graphics2D) g).setTransform(at); super.paint(g); } }*/ //////////////////////////////////////////////////////////////////////////////////////////////////// // Construction //////////////////////////////////////////////////////////////////////////////////////////////////// protected void addKeyBinding(int keyEvent,int keyMask,final String actionCommand) { KeyStroke k=KeyStroke.getKeyStroke(keyEvent,keyMask); final Action prevAction=super.getKeymap().getAction(k); super.getKeymap().addActionForKeyStroke(k, new AbstractAction() { public void actionPerformed(ActionEvent e) { ActionEvent e1=new ActionEvent(e.getSource(), e.getID(), actionCommand, e.getWhen(), e.getModifiers()); if (e.getSource() instanceof JXMLNotePane) { ((JXMLNotePane) e.getSource()).getKeyBindingActionListener().actionPerformed(e1); } else { if (prevAction!=null) { prevAction.actionPerformed(e1); } } } }); } protected void addCtrlKey(int keyEvent,String actionCommand) { addKeyBinding(keyEvent,KeyEvent.CTRL_MASK,actionCommand); } private ActionListener _keyBindingActionListener; public ActionListener getKeyBindingActionListener() { return _keyBindingActionListener; } protected void initKeyBindings(ActionListener l) { _keyBindingActionListener=l; addCtrlKey(KeyEvent.VK_V,JXMLNoteToolBar.ACTION_PASTE); addCtrlKey(KeyEvent.VK_C,JXMLNoteToolBar.ACTION_COPY); addCtrlKey(KeyEvent.VK_X,JXMLNoteToolBar.ACTION_CUT); addCtrlKey(KeyEvent.VK_A,JXMLNoteToolBar.ACTION_SELECT_ALL); addCtrlKey(KeyEvent.VK_Z,JXMLNoteToolBar.ACTION_UNDO); addCtrlKey(KeyEvent.VK_Y,JXMLNoteToolBar.ACTION_REDO); addCtrlKey(KeyEvent.VK_B,JXMLNoteToolBar.ACTION_BOLD); addCtrlKey(KeyEvent.VK_I,JXMLNoteToolBar.ACTION_ITALIC); addCtrlKey(KeyEvent.VK_U,JXMLNoteToolBar.ACTION_UNDERLINE); } /** * Constructor for JXMLNotePane. Is called with an XMLNoteDocument. * This will construct this pane with a DefaultMarkMarkerProviderMaker. * @param d */ public JXMLNotePane(XMLNoteDocument d,ActionListener l) { this(d,new DefaultMarkMarkupProviderMaker(),l); } /** * Constructor for JXMLNotePane. Is called with an XMLNoteDocument and * a MarkMarkupProviderMaker, that creates MarkMarkupProviders for XMLNoteMarks * and provides markup for XMLNoteMarks that is custom for this view. * * Once constructed, a MarkMarkupProviderMaker is cannot be changed anymore. * * @param d * @param maker */ public JXMLNotePane(XMLNoteDocument d,MarkMarkupProviderMaker maker,ActionListener l) { super(); super.setEditorKit(new XMLNoteEditorKit(this)); super.setDocument(d); initZoomFactor(); { Color c=UIManager.getColor("TextField.background"); this.setOpaque(false); this.setBackground(c); } initKeyBindings(l); DefaultCaret caret=new DefaultCaret(); caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); caret.setBlinkRate(700); this.setCaret(caret); this.putClientProperty("caretWidth",2); this.putClientProperty("caretAspectRatio", -1); this.setPreferredSize(new Dimension(300,200)); _updateViewListeners=new HashSet<UpdateViewListener>(); new XMLNoteTransferHandler(this); getXMLNoteDoc().installHighlighterForView(this.getHighlighter(),maker); _maker=maker; // add DocumentAdminListener to reposition the caret after reset and other stuff getXMLNoteDoc().addDocumentAdminListener(this); getXMLNoteDoc().addDocumentListener(this); // Track mouse movement regarding marks _markMouseListeners=new HashSet<MarkMouseListener>(); _mouseMarkTracker=new TrackMouseMark(this); this.addMouseMotionListener(_mouseMarkTracker); this.addMouseListener(_mouseMarkTracker); } }