Source code: jpicedt/graphic/PECanvas.java
1 /*
2 PECanvas.java - 1999 - jPicEdt 1.3.2, a picture editor for LaTeX.
3 Copyright (C) 1999-2002 Sylvain Reynal
4
5 Département de Physique
6 Ecole Nationale Supérieure de l'Electronique et de ses Applications (ENSEA)
7 6, avenue du Ponceau
8 F-95014 CERGY CEDEX
9
10 Tel : +33 130 736 245
11 Fax : +33 130 736 667
12 e-mail : reynal@ensea.fr
13 jPicEdt web page : http://www.jpicedt.org
14
15 This program is free software; you can redistribute it and/or
16 modify it under the terms of the GNU General Public License
17 as published by the Free Software Foundation; either version 2
18 of the License, or any later version.
19
20 This program is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 GNU General Public License for more details.
24
25 You should have received a copy of the GNU General Public License
26 along with this program; if not, write to the Free Software
27 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 */
29
30 package jpicedt.graphic;
31
32 import jpicedt.graphic.*;
33 import jpicedt.graphic.event.*;
34 import jpicedt.graphic.model.*;
35 import jpicedt.graphic.grid.Grid;
36 import jpicedt.graphic.toolkit.*;
37 import jpicedt.graphic.io.parser.*;
38
39 import java.awt.*;
40 import java.awt.event.*;
41 import java.awt.geom.*;
42 import javax.swing.*;
43 import javax.swing.event.*;
44 import java.util.*;
45 import java.io.*;
46 import java.beans.*;
47 import java.awt.datatransfer.*;
48 import javax.swing.undo.*;
49
50 /**
51 * This is a JComponent on which graphic elements are drawn. It's has an underlying model (a Drawing) to
52 * represent the content, an EditorKit to manipulate the content, and a View responsible for rendering
53 * the content. EditorKit, Drawing, and View's are pluggable : the EditorKit is responsible for creating
54 * 1) a default Drawing and 2) a ViewFactory that will populate the View tree associated with the Drawing,
55 * by attaching a View to each element in the model.<p>
56 * Depending on the content type this Component is loaded with, it may plug a new EditorKit on-the-fly
57 * that's suited for the given content type (e.g. LaTeX, Postscript, SVG-XML, etc...).
58 *
59 *
60 * <h1>Model to View/View to Model</h1>
61 * <p>
62 * Graphic objects are stored (e.g. in a Drawing)
63 * in natural coordinates, i.e. LaTeX/Postscript/... coordinates, using e.g. a "1 mm" unitlength :
64 * that's what we call "model coordinate". Obviously, we've to translate these coordinates to
65 * screen coordinates (e.g. JViewport-coordinate or JPanel-coordinate) before rendering,
66 * and this is done very simply by using an AffineTransform and adding it to the
67 * current Graphic2D context in the body of the "paintComponent" method. The following picture
68 * sums up the translation process that takes place b/w model- and view-coordinate.
69 * </p>
70 * <p><img src="model2view.png"></p>
71 * <p>The benefits of such an approach is to make it easy for someone willing to develop a parser/formater
72 * to handle objects coordinates with the fewest possible overhead, since objects coordinates
73 * are "natively" available in natural (i.e. from left to right and from bottom to top) coordinates, and
74 * this is perfectly suited for formatting language like LaTeX, Postscript or SVG-XML. Besides,
75 * this makes sense with the grid ticks marks.
76 * </p>
77 * <p>Margins are encapsulated in a PageFormat (an inner class) object. Contrary to previous jpicedt releases,
78 * it's no longer necessary, as of jpicedt 1.3.2, to provide formater methods with the ptOrg parameter.
79 * <p>In addition to the standard behaviour inherited from JPanel, PropertyChangeEvent's are triggered when :
80 * <ul>
81 * <li>changing the page format ;
82 * <li>changing the zoom factor ;
83 * <li>changing the editor kit ;
84 * </ul>
85 * </p>
86 * @author Sylvain Reynal
87 * @since PicEdt 1.0
88 */
89
90 public class PECanvas extends JPanel implements Scrollable {
91
92 //////////////////////////////////// PUBLIC FIELDS /////////////////////////////////
93
94 public static final String[] PREDEFINED_ZOOM_STRINGS = {"100%", "200%", "400%", "800%"}; // [pending] use java.text.NumberFormat.getPercentInstance()
95 public static final double[] PREDEFINED_ZOOMS = { 1.0, 2.0, 4.0, 8.0};
96 public static final double ZOOM_DEFAULT = 1.0;
97
98 /** key for Properties's zoom value */
99 public static final String KEY_ZOOM = "canvas.zoom";
100 /** key for Properties's content-type value */
101 public static final String KEY_CONTENT_TYPE = "canvas.content-type";
102 /** key for Properties's nb of undoable steps value */
103 public static final String KEY_UNDOABLE_STEPS = "canvas.max-undoable-steps";
104 /** default undoable events to remember */
105 public static final int MAX_UNDOABLE_STEPS_DEFAULT = 100; // same as UndoManager
106
107
108 /** property name for drawing change */
109 public static final String DRAWING_CHANGE = "drawing-change";
110
111 /** property name for editor kit change */
112 public static final String EDITOR_KIT_CHANGE = "editor-kit-change";
113
114 /** property name for content-type change */
115 public static final String CONTENT_TYPE_CHANGE = "content-type-change";
116
117 //////////////////////////////////// PROTECTED FIELDS /////////////////////////////////
118
119 /** the model for this canvas */
120 protected Drawing drawing;
121
122 /** pageFormat encapsulates board size and margin data */
123 protected PageFormat pageFormat;
124
125 /** the current content-type for this PECanvas (determines the EditorKit behaviour) */
126 protected ContentType contentType;
127
128 /** the AffineTransform used to translate from model-coordinates to view-coordinates ;
129 * gets updated each time either the zoom factor or the page format changes */
130 protected AffineTransform model2ViewTransform;
131
132 /** the AffineTransform used to translate from mouse-coordinates to model-coordinates ;
133 * gets updated each time model2ViewTransform changes */
134 protected AffineTransform view2ModelTransform;
135
136 /** the grid attached to this canvas */
137 protected Grid grid;
138
139 /** the current editor kit for this component */
140 protected EditorKit kit;
141
142 /** the UndoableEditSupport delegate for UndoableEditEvent firing */
143 protected UndoableEditSupport undoableEditSupport;
144
145 /** the UndoManager delegate for undo/redo operation */
146 protected UndoManager undoManager;
147
148 /** the UndoableEdit in progress */
149 protected StateEdit stateEdit;
150
151 /** a Map storing RenderingHints to be applied to the graphic context when rendering the drawing */
152 protected RenderingHints renderingHints = new RenderingHints(null);
153
154
155 //////////////////////////////////// PRIVATE FIELDS /////////////////////////////////
156
157 /** the zoom factor ; this determines the zoom factor to be applied to Graphics2D through an AffineTransform */
158 private double zoom;
159
160 /** the total scale factor, including DPMM and zoom ; updated each time zoom changes */
161 private double scale;
162
163 /** chained list of PEMouseInputListener's for this component */
164 private PEMouseInputListener mouseInputListener;
165
166 /** tmp. buffer used by processMouseEvent */
167 private PicPoint peMousePoint = new PicPoint();
168
169 /** tmp. buffer used by repaintFromModelRect */
170 private double[] tmpCoords = new double[2];
171 private Rectangle repaintRectangle = new Rectangle();
172
173
174
175 //////////////////////////////////// CONSTRUCTORS /////////////////////////////////
176
177 // [pending] add a default constructor and a constructor using Properties
178
179 /**
180 * Construct a new PECanvas with the default editor-kit and drawing as content storage.
181 * @param zoom initial zoom factor
182 * @param initial page format (page size + page margins)
183 * @param content-type (e.g. LaTeX, PsTricks,...) ; this will determine the EditorKit for
184 * editing the Drawing, and indirectly the ViewFactory that produces View's for the drawing,
185 * (since the ViewFactory is obtained through the currently installed EditorKit) and the
186 * FormatterFactory used to write the drawing to a writer.
187 * If null, the default editor-kit/content-type is used.
188 * @author Sylvain Reynal
189 * @since PicEdt 1.0
190 */
191 public PECanvas(double zoom, PageFormat pageFormat, Grid grid, ContentType contentType){
192
193 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"<init>","start");
194 this.zoom = zoom;
195 setPageFormat(pageFormat); // need zoom to be non-null
196 setZoomFactor(zoom);
197 // create a default drawing, and create View's for it using
198 // the ViewFactory for the given content-type
199 setContentType(contentType);
200 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"<init>","Installing grid");
201 this.grid = grid;
202 // miscellaneous...
203 // [pending] setBorder(BorderFactory.createEtchedBorder());
204 setBackground(Color.white);
205 // undo/redo
206 undoableEditSupport = new UndoableEditSupport(this); // source for event = PECanvas
207 undoManager = new UndoManager();
208 setUndoLimit(MAX_UNDOABLE_STEPS_DEFAULT);
209 undoableEditSupport.addUndoableEditListener(undoManager);
210
211 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"<init>","completed !");
212
213 // [pending] debug (press F3 to write a comment line for lisibility)
214 // if (jpicedt.Log.DEBUG)
215 //if (jpicedt.Log.DEBUG)
216 registerKeyboardAction(new ActionListener(){
217 public void actionPerformed(ActionEvent e){
218 System.out.println("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$");
219 }},KeyStroke.getKeyStroke(KeyEvent.VK_F3,0),WHEN_FOCUSED);
220
221 }
222
223
224
225 ///////////////////////////////////////////////////////////////////////////////
226 //// PAINT
227 //////////////////////////////////////////////////////////////////////////////
228
229 /**
230 * paintComponent(Graphics g) is called by AWTEventDispatchThread via "paint(g)"
231 * it's absolutely necessary to call super.paintComponent(g) so that the background gets properly painted (PECanvas is opaque)
232 */
233 public void paintComponent(Graphics g){
234
235 super.paintComponent(g); // paint background
236 Graphics2D g2 = (Graphics2D)g;
237 g2.setRenderingHints(renderingHints);
238 g2.transform(model2ViewTransform); // Note : g2.setTransform(model2ViewTransform) -> bug when partial repaint !!! use g2.transform instead
239 Rectangle2D allocation = g2.getClip().getBounds2D();
240
241 if (jpicedt.Log.DEBUG) {
242 jpicedt.Log.debug(this,"paintComponent","clip=" + allocation);
243 g2.setPaint(Color.green);
244 g2.draw(allocation);
245 }
246 if (grid != null) grid.paint(g2,allocation,scale);
247 if (drawing != null && drawing.getRootView()!=null)
248 drawing.getRootView().paint(g2,allocation); // security : if we are just installing a new drawing and its view-tree hasn't been set yet
249 if (kit != null) kit.paint(g2,allocation,scale);
250 }
251
252 /**
253 * add the given rectangle, given in model-coordinates, to the list of dirty regions.
254 */
255 public void repaintFromModelRect(Rectangle2D rect){
256
257 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"repaintFromModelRect");
258 if (jpicedt.Log.DEBUG) jpicedt.Log.debugAppendLn(this,"rect2D="+rect);
259 // fetch topleft corner :
260 tmpCoords[0] = rect.getX();
261 tmpCoords[1] = rect.getMaxY();
262 // translate it to view coord system :
263 model2ViewTransform.transform(tmpCoords,0,tmpCoords,0,1);
264 // set topleft corner for destination rectangle
265 repaintRectangle.x = (int)tmpCoords[0];
266 repaintRectangle.y = (int)tmpCoords[1];
267 // fetch double-precision width and height
268 tmpCoords[0] = rect.getWidth();
269 tmpCoords[1] = rect.getHeight();
270 // delta-translate it to view coord system :
271 model2ViewTransform.deltaTransform(tmpCoords,0,tmpCoords,0,1);
272 // set dimension for destination rectangle
273 repaintRectangle.width = (int)tmpCoords[0];
274 repaintRectangle.height = -(int)tmpCoords[1];
275 // add repaintRectangle to dirty region
276 if (jpicedt.Log.DEBUG) jpicedt.Log.debugAppendLn(this,"repaintRect="+repaintRectangle);
277 repaint(repaintRectangle);
278 }
279
280 /**
281 * Return the RenderingHints applied to the graphic context when rendering this component
282 */
283 public RenderingHints getRenderingHints(){
284 return renderingHints;
285 }
286
287
288
289
290 ////////////////////////////////////////////////////
291 //// CONTENT HANDLING
292 ////////////////////////////////////////////////////
293
294 /**
295 * @return the model, i.e. a Drawing containing only non-selected objects
296 */
297 public Drawing getDrawing(){
298 return drawing;
299 }
300
301 /**
302 * set the Drawing model for this component. The currently registered EditorKit is
303 * used to build a viewtree for the drawing. A PropertyChange event (DRAWING_CHANGE) is sent
304 * to each listener.
305 */
306 public void setDrawing(Drawing dr){
307 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"setDrawing","drawing="+dr);
308 Drawing old = this.drawing;
309 this.drawing = dr;
310 this.drawing.setViewTree(getEditorKit().getViewFactory());
311 firePropertyChange(DRAWING_CHANGE, old, this.drawing); // [pending] doesn't seem to be fired !
312 }
313
314 /**
315 * Fetches the currently installed kit for handling content.
316 * <code>createDefaultEditorKit</code> is called to set up a default, if no kit is currently installed.
317 * @return the editor kit
318 * @since jPicEdt 1.3.2
319 */
320 public EditorKit getEditorKit(){
321 if (kit == null) {
322 kit = createDefaultEditorKit();
323 }
324 return kit;
325 }
326
327 /**
328 * Creates the default editor kit (<code>EditorKit</code>) for when
329 * the component is first created.
330 * @return the editor kit
331 */
332 protected EditorKit createDefaultEditorKit() {
333 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"createDefaultEditorKit");
334 return new EditorKit();
335 }
336
337 /**
338 * @return an EditorKit that's suited for the given contentType.
339 * [pending] improve this by providing a registry of editor kits, which plug-ins designer
340 * can add their own EditorKit to.
341 * @param contentType
342 * @see JEditorPane#createEditorKitForContentType
343 * @see JEditorPane#getEditorKitForContentType
344 */
345 public static EditorKit createEditorKitForContentType(ContentType contentType){
346 return contentType.createEditorKit(); // [pending] lookup kit registry
347 }
348
349 /**
350 * Sets the currently installed kit for handling content. This is the bound property that
351 * establishes the content type of the editor. Any old kit is first deinstalled, then if kit is
352 * non-<code>null</code>, the new kit is installed. <p>
353 * A default drawing is created from it if there was no drawing set in this canvas before,
354 * otherwise the old drawing is reused : in both cases, <code>setDrawing</code> is called, but
355 * this allows the caller to change the ContentType w/o changing the Drawing if it deems it unnecessary
356 * (otherwise, it may call setDrawing() afterwards).
357 * A <code>PropertyChange</code> event (EDITOR_KIT_CHANGE) is always fired when
358 * <code>setEditorKit</code> is called.
359 * @param kit the desired editor behavior
360 * @see #getEditorKit
361 */
362 public void setEditorKit(EditorKit kit) {
363 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"setEditorKit","kit="+kit);
364 EditorKit old = this.kit;
365 if (old != null) {
366 old.deinstall(this);
367 }
368 this.kit = kit;
369 if (this.kit != null) {
370 this.kit.install(this); // add proper event listeners to PECanvas
371 if (drawing == null) setDrawing(this.kit.createDefaultDrawing());
372 else setDrawing(drawing);
373 }
374 firePropertyChange(EDITOR_KIT_CHANGE, old, kit);
375 }
376
377
378
379
380 ///////////////////////////////////
381 ///// CONTENT TYPE
382 ///////////////////////////////////
383 /**
384 * @return the current content-type
385 */
386 public ContentType getContentType(){
387 return contentType;
388 }
389
390 /**
391 * change the current content-type
392 */
393 public void setContentType(ContentType newContentType){
394 ContentType old = this.contentType;
395 this.contentType = newContentType;
396 // update editor kit
397 if (this.contentType == null) this.contentType = new DefaultContentType(); //setEditorKit(createDefaultEditorKit());
398 setEditorKit(createEditorKitForContentType(this.contentType)); // update view-tree as well
399 firePropertyChange(CONTENT_TYPE_CHANGE, old, this.contentType);
400 }
401
402
403 ///////////////////////////////////
404 ///// DRAWING BOARD SIZE
405 ///////////////////////////////////
406
407 /**
408 * Set the size of the drawing board. Length are given in mm (this should approximately
409 * represent true mm on the screen, however this might slightly depend on the underlying platform).
410 * <br>
411 * This in turn sets the preferred size of the component. .
412 * @since jPicEdt 1.3.2
413 * @author Sylvain Reynal
414 */
415 public void setPageFormat(PageFormat pageFormat){
416
417 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"setPageFormat","page-format="+pageFormat);
418 PageFormat oldFormat = this.pageFormat;
419 this.pageFormat = pageFormat;
420 model2ViewTransform = pageFormat.getModel2ViewTransform(zoom);
421 view2ModelTransform = pageFormat.getView2ModelTransform(zoom);
422 setPreferredSize(pageFormat.getSizePx(zoom));
423 //firePropertyChange(PAGE_FORMAT_CHANGE, oldFormat, this.pageFormat); //[pending] bug : not fired !
424 // give a chance
425 // to listeners to update their layout (e.g. ScrollPane)
426 invalidate();
427 fireZoomUpdate(this.zoom, this.zoom, null); // force e.g. a scrollpane to update rulers
428 //validate();
429 repaint();
430 }
431
432 /**
433 * @return the page format for this drawing board
434 * @author Sylvain Reynal
435 * @since jPicEdt 1.3.2
436 */
437 public PageFormat getPageFormat(){
438 return pageFormat;
439 }
440
441 /**
442 * @return the pixel-coordinates of the (0,0) model origin when zoom = 1.0
443 */
444 public PicPoint getSheetOrigin(){
445 return pageFormat.getOrgPx(1.0);
446 }
447
448 /**
449 * @return the grid attached to this canvas
450 */
451 public Grid getGrid() {
452 return grid;
453 }
454
455 ///////////////////////////////////
456 ///// Model <-> View
457 ///////////////////////////////////
458
459
460 /**
461 * @return an AffineTransform that represents the maping b/w the model-coordinate system
462 * and the pixel coordinate system. Guaranteed not to change over time.
463 * @author Sylvain Reynal
464 * @since jPicEdt 1.3.2
465 */
466 public AffineTransform getModelToViewTransform(){
467 return (AffineTransform)model2ViewTransform.clone();
468 }
469
470 /**
471 * @return an AffineTransform that represents the maping b/w the pixel-coordinate system
472 * and the model-coordinate system. Guaranteed not to change over time.
473 * @author Sylvain Reynal
474 * @since jPicEdt 1.3.2
475 */
476 public AffineTransform getViewToModelTransform(){
477 return (AffineTransform)view2ModelTransform.clone();
478 }
479
480 /**
481 * Converts a point from the model-coordinate system to the pixel-coordinate system.
482 * @param src the source point in model-coordinate
483 * @param dst the destination point ; if null, a new point is allocated, and returned for convenience.
484 * @return the result (same as dst if non-null)
485 * @author Sylvain Reynal
486 * @since jPicEdt 1.3.2
487 */
488 public PicPoint modelToView(PicPoint src, PicPoint dst){
489 if (dst==null) dst = new PicPoint();
490 // fetch topleft corner :
491 tmpCoords[0] = src.x;
492 tmpCoords[1] = src.y;
493 // translate it to view coord system :
494 model2ViewTransform.transform(tmpCoords,0,tmpCoords,0,1); // transform 1 point
495 dst.x = tmpCoords[0];
496 dst.y = tmpCoords[1];
497 return dst;
498 }
499
500 /**
501 * Converts a point from the pixel-coordinate system to the model-coordinate system.
502 * @param src the source point in pixel-coordinate
503 * @param dst the destination point ; if null, a new point is allocated, and returned for convenience.
504 * @return the result (same as dst if non-null)
505 * @author Sylvain Reynal
506 * @since jPicEdt 1.3.2
507 */
508 public PicPoint view2Model(PicPoint src, PicPoint dst){
509 if (dst==null) dst = new PicPoint();
510 // fetch topleft corner :
511 tmpCoords[0] = src.x;
512 tmpCoords[1] = src.y;
513 // translate it to view coord system :
514 view2ModelTransform.transform(tmpCoords,0,tmpCoords,0,1); // transform 1 point
515 dst.x = tmpCoords[0];
516 dst.y = tmpCoords[1];
517 return dst;
518 }
519
520 /**
521 * Converts a Shape from the model-coordinate system to the pixel-coordinate system.
522 * @return a new Shape corresponding to the given Shape once transformed to the pixel-coordinate system.
523 * @param src a Shape in the model-coordinate system
524 * @author Sylvain Reynal
525 * @since jPicEdt 1.3.2
526 */
527 public Shape modelToView(Shape src){
528 return model2ViewTransform.createTransformedShape(src);
529 }
530
531 /**
532 * Converts a Shape from the pixel-coordinate system to the model-coordinate system.
533 * @return a new Shape corresponding to the given Shape once transformed to the model-coordinate system.
534 * @param src a Shape in the pixel-coordinate system
535 * @author Sylvain Reynal
536 * @since jPicEdt 1.3.2
537 */
538 public Shape viewToModel(Shape src){
539 return view2ModelTransform.createTransformedShape(src);
540 }
541
542 ///////////////////////////////////
543 ///// I/O
544 ///////////////////////////////////
545
546 /**
547 * read drawing content from a reader and erase old one. Listener's interested in
548 * DrawingEvent's should register their listener anew (this can be done systematically by
549 * registering a PropertyCHangeListener to this canvas, and waiting for DRAWING_CHANGE events).<br>
550 * @param reader the reader to read content from
551 */
552 public void read(Reader reader, Parser parser) throws jpicedt.graphic.io.parser.ParserException {
553 getEditorKit().getSelectionHandler().unSelectAll(); // don't fire selection event
554 //jpicedt.graphic.io.parser.LaTeXParser parser = new jpicedt.graphic.io.parser.LaTeXParser(); // [pending] adapt parser to the type of content this EditorKit handles
555 Drawing parsed = parser.parse(reader); // takes some time...
556 setDrawing(parsed);
557 // [pending] update PageFormat and ContentType
558 repaint();
559 }
560
561 /**
562 * insert content from a reader into the current drawing<br>
563 * @param reader the reader to insert content from
564 */
565 public void insert(Reader reader, Parser parser) throws jpicedt.graphic.io.parser.ParserException {
566
567 //jpicedt.graphic.io.parser.LaTeXParser latexParser = new jpicedt.graphic.io.parser.LaTeXParser(); // adapt parser to the type of content this EditorKit handles
568 Drawing parsed = parser.parse(reader);
569 getEditorKit().getSelectionHandler().unSelectAll(); // don't fire selection event
570 // add parsed content to the current drawing
571 for (Iterator it=parsed.getRootElement().children(); it.hasNext(); ){
572 Element o = (Element)it.next();
573 o = (Element)o.clone();
574 drawing.addElement(o);
575 getEditorKit().getSelectionHandler().addToSelection(o);
576 }
577 fireSelectionUpdate(kit.getSelectionHandler().asArray(), SelectionEvent.EventType.SELECT);
578 // add more non-parsed command to current drawing, if applicable :
579 if (parsed.getNotparsedCommands().length()==0) return;
580 String s = drawing.getNotparsedCommands();
581 if (s==null || s.length()==0) drawing.setNotparsedCommands(s);
582 else {
583 s += "\n";
584 s += parsed.getNotparsedCommands();
585 drawing.setNotparsedCommands(s);
586 }
587 }
588
589 /**
590 * Write drawing content to the given stream
591 * @param writer The writer to write to
592 * @param writeSelectionOnly if true, only write selection content
593 * @exception IOException on any I/O error
594 * @since jPicEdt 1.3.2
595 */
596 public void write(Writer writer, boolean writeSelectionOnly) throws IOException {
597
598 String buffer;
599 if (writeSelectionOnly){
600 Drawing dr = new Drawing(getEditorKit().getSelectionHandler().asCollection()); // deep copy
601 // translate fragment to (0,0)
602 Rectangle2D bb = dr.getBoundingBox();
603 dr.getRootElement().translate(- bb.getX(), -bb.getY());
604 buffer = getEditorKit().getFormatterFactory().createFormatter(dr,null).format();
605 }
606 else {
607 buffer = getEditorKit().getFormatterFactory().createFormatter(drawing,null).format();
608 }
609 writer.write(buffer);
610 }
611
612
613
614
615
616
617 ///////////////////////////////////
618 ///// ZOOM
619 ///////////////////////////////////
620
621 /**
622 * Convenience call to setZoomFactor(zoom,null)
623 */
624 public void setZoomFactor(double zoom){
625 setZoomFactor(zoom,null);
626 }
627
628 /**
629 * sets the current zoom factor to the given double,
630 * then updates various properties (model <-> view transforms, dimension, preferredSize...), finally,
631 * sources a ZoomEvent to give a chance to receiver to update their state accordingly (this may
632 * be used e.g. by a parent scrollpane to update its view port location, or by a GUI widget
633 * to reflect the new zoom value).
634 * @param zoom the new zoom factor
635 * @param ptClick this only makes sense if the parent of this component is aka ScrollPane ;
636 * <br> Coordinates for this point are in the model-coordinate system.
637 */
638 public void setZoomFactor(double zoom, PicPoint ptClick){
639
640 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"setZoomFactor","zoom="+zoom);
641 double oldZoom = this.zoom; // store old value for further use by zoom event
642 this.zoom = zoom; // [pending] test if zoom really changed !
643
644 // update global variables according to new zoom value :
645 model2ViewTransform = pageFormat.getModel2ViewTransform(zoom);
646 view2ModelTransform = pageFormat.getView2ModelTransform(zoom);
647 scale = model2ViewTransform.getScaleX(); // assumption : scaleX = scaleY !
648
649 // update preferred size according to new zoom :
650 setPreferredSize(pageFormat.getSizePx(zoom));
651 invalidate(); // JDK's documentation says : "revalidate()"
652 // give a change to a scrollpane to update the view location in the viewport and ruler sizes :
653 fireZoomUpdate(oldZoom, this.zoom, ptClick);
654 repaint();
655 }
656
657 /**
658 * @return the current zoom factor
659 */
660 public double getZoomFactor(){
661 return zoom;
662 }
663
664 /**
665 * @return the current scale factor between model- and view-coordinates, as given by the current
666 * model2ViewTransform. This is usually the product of the current zoom factor, and the
667 * DotPerMilliMeter screen factor.
668 */
669 public double getScaleFactor(){
670 return scale;
671 }
672
673 /**
674 * Notify all listeners that have registered interest for notification on this event type.
675 * @param oldZoom previous zoom value
676 * @param newZoom new zoom value
677 * @param ptClick the point (in model-coordinates) that is expected to be at the center of the view-port ;
678 * can be null
679 */
680 protected void fireZoomUpdate(double oldZoom, double newZoom, PicPoint ptClick){
681 Object[] listeners = listenerList.getListenerList();
682 ZoomEvent e = null;
683 for (int i = listeners.length-2; i>=0; i-=2) {
684 // lazily create the event :
685 if (e==null) e = new ZoomEvent(this,oldZoom,newZoom,ptClick);
686 if (listeners[i]==ZoomListener.class) {
687 ((ZoomListener)listeners[i+1]).zoomUpdate(e);
688 }
689 }
690 }
691
692 /**
693 * adds a ZoomListener to the Canvas
694 */
695 public void addZoomListener(ZoomListener l){
696 listenerList.add(ZoomListener.class, l);
697 }
698
699 /**
700 * removes a ZoomListener from the Canvas
701 */
702 public void removeZoomListener(ZoomListener l){
703 listenerList.remove(ZoomListener.class, l);
704 }
705
706 /**
707 * utilities to retrieve the index of the given zoom in PREDEFINED_ZOOMS ; this may be used by GUI widgets,
708 * e.g. JComboBox,...
709 * @return index of the given zoom in array "PREDEFINED_ZOOMS" ; returns -1 if not found.
710 */
711 public static int getZoomIndex(double zoom){
712 for(int i=0; i<PREDEFINED_ZOOMS.length; i++){
713 if (PREDEFINED_ZOOMS[i] == zoom) return i;
714 }
715 return -1; // not found
716 }
717
718
719 /////////////////////////////
720 ///// UNDO/REDO
721 /////////////////////////////
722
723 /**
724 * set the number of undoable events to remember
725 */
726 public void setUndoLimit(int limit){
727 undoManager.setLimit(limit);
728 }
729
730 /**
731 * Undo last change [underway]
732 * @since PicEdt 1.1.3
733 */
734 public void undo() throws CannotUndoException {
735 selectAll(false);
736 undoManager.undo();
737 }
738
739 /**
740 * Redo last change [underway]
741 * @since PicEdt 1.1.3
742 */
743 public void redo() throws CannotRedoException {
744 selectAll(false);
745 undoManager.redo();
746 }
747
748 /**
749 * Register an UndoableEditListener for the Drawing hosted by this canvas.
750 */
751 public void addUndoableEditListener(UndoableEditListener l){
752 undoableEditSupport.addUndoableEditListener(l);
753 }
754
755 /**
756 * Register an UndoableEditListener for the Drawing hosted by this canvas.
757 */
758 public void removeUndoableEditListener(UndoableEditListener l){
759 undoableEditSupport.removeUndoableEditListener(l);
760 }
761
762 /**
763 * Create a new UndoableEdit that holds the current state of the Drawing.
764 */
765 public void beginUndoableUpdate(String presentationName){
766 if (stateEdit != null) {
767 endUndoableUpdate(); // if end() was not called !
768 }
769 //stateEdit = new StateEdit(getDrawing(),presentationName);
770 stateEdit = new PEStateEdit(getDrawing(),presentationName);
771 }
772
773 /**
774 * Ends the current UndoableEdit and fire an event to registered listeners.
775 */
776 public void endUndoableUpdate(){
777 if (stateEdit==null) {
778 return; // refuse two calls to end()
779 }
780 stateEdit.end();
781 undoableEditSupport.postEdit(stateEdit);
782 stateEdit = null; // flag for begin() to know we've ended !
783 }
784
785 /**
786 * @return true if a "redo" operation would be successfull
787 */
788 public boolean canRedo(){
789 return undoManager.canRedo();
790 }
791
792 /**
793 * @return true if a "undo" operation would be successfull
794 */
795 public boolean canUndo(){
796 return undoManager.canUndo();
797 }
798
799 /**
800 * @return the presentation name of the next edit that can be redone
801 */
802 public String getRedoPresentationName(){
803 if (!canRedo()) return "";
804 return undoManager.getRedoPresentationName();
805 }
806
807 /**
808 * @return the presentation name of the last edit that can be undone
809 */
810 public String getUndoPresentationName(){
811 if (!canUndo()) return "";
812 return undoManager.getUndoPresentationName();
813 }
814
815 /** overriden so as to display our own undo- and redo- presentation names */
816 private class PEStateEdit extends StateEdit {
817
818 PEStateEdit(StateEditable anObject, String name){
819 super(anObject, name);
820 }
821
822 public String getUndoPresentationName() {
823 return getPresentationName();
824 }
825
826 public String getRedoPresentationName() {
827 return getPresentationName();
828 }
829 }
830
831 //////////////////////////////////
832 ////// SELECTION MANAGEMENT
833 //////////////////////////////////
834
835 /**
836 * @return an Iterator over selected graphic elements
837 */
838 public Iterator selection(){ // [pending] change to "getSelection"
839 return getEditorKit().getSelectionHandler().elements();
840 }
841
842 /**
843 * @return the number of selected elements in the current selection
844 */
845 public int getSelectionSize(){
846 return getEditorKit().getSelectionHandler().size();
847 }
848
849 /**
850 * @return whether the given element is selected or not
851 */
852 public boolean isSelected(Element e){
853 return getEditorKit().getSelectionHandler().isSelected(e, true);
854 }
855
856 /**
857 * select or unselect every object in this drawing
858 * @param state if true, selectAll, otherwise unselect all.
859 */
860 public void selectAll(boolean state){
861 if (state == false){ // unselect all
862 if (getEditorKit().getSelectionHandler().size()==0) return; // already empty
863 Element[] deselected = getEditorKit().getSelectionHandler().asArray();
864 getEditorKit().getSelectionHandler().unSelectAll();
865 fireSelectionUpdate(deselected, SelectionEvent.EventType.UNSELECT);
866 }
867 else { // select all
868 kit.getSelectionHandler().selectAll(drawing);
869 fireSelectionUpdate(kit.getSelectionHandler().asArray(), SelectionEvent.EventType.SELECT);
870 }
871 }
872
873 /**
874 * select the elements in the given collection (if they belong to the drawing)
875 * @param incremental if true, add to the existing selection ; replace otherwise.
876 */
877 public void select(Collection c, boolean incremental){
878 if (incremental == false) getEditorKit().getSelectionHandler().unSelectAll();
879 ArrayList selected = new ArrayList();
880 for (Iterator it = c.iterator(); it.hasNext();){
881 Element obj = (Element)it.next();
882 if (isSelected(obj)) {
883 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"select","Already selected:elem="+obj);
884 }
885 else {
886 getEditorKit().getSelectionHandler().addToSelection(obj);
887 selected.add(obj);
888 }
889 }
890 Element[] selectedArray = (Element[])selected.toArray(new Element[0]);
891 fireSelectionUpdate(selectedArray, SelectionEvent.EventType.SELECT);
892 }
893
894 /**
895 * select the given object
896 * @param incremental if true, add to the existing selection ; replace otherwise.
897 */
898 public void select(Element obj, boolean incremental){
899 if (incremental == true) {
900 if (isSelected(obj)) {
901 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"select","Already selected:elem="+obj);
902 return; // already selected
903 }
904 getEditorKit().getSelectionHandler().addToSelection(obj);
905 fireSelectionUpdate(obj, SelectionEvent.EventType.SELECT);
906 }
907 else {
908 getEditorKit().getSelectionHandler().replaceSelection(obj);
909 fireSelectionUpdate(obj, SelectionEvent.EventType.SELECT); // don't fire "unselect"
910 }
911 }
912
913 /**
914 * unselect the given object
915 */
916 public void unSelect(Element obj){
917 if (!isSelected(obj)) {
918 if (jpicedt.Log.DEBUG) jpicedt.Log.debug(this,"unSelect","Already unselected:elem="+obj);
919 return; // already unselected
920 }
921 getEditorKit().getSelectionHandler().unSelect(obj);
922 fireSelectionUpdate(obj, SelectionEvent.EventType.UNSELECT);
923 }
924
925 /**
926 * remove all selected objects from the drawing
927 */
928 public void deleteSelection(){
929 getEditorKit().getSelectionHandler().delete(drawing);
930 }
931
932 /**
933 * Add the content of the given ClipBoard to the current drawing, then select it.
934 * If only DataFlavor.stringFlavor is provided by the Transferable,
935 * we try to parse the string and insert the parsed content. Otherwise, it's taken for granted
936 * that the ClipBoard content support the jpicedt.graphic.toolkit.TransferableGraphic.JPICEDT_DATA_FLAVOR
937 * data flavor.
938 * @param translate if true, translate the pasted content by a grid step so that it doesn't hide old one
939 */
940 public void paste(Clipboard clipbrd, boolean translate) throws ParserException, IOException, UnsupportedFlavorException {
941 getEditorKit().getSelectionHandler().unSelectAll(); // don't fire selection event
942 Transferable transferable = clipbrd.getContents(this); // according to doc. requestor is not used.
943 if (transferable==null) return; // clipboard is empty !
944
945 // first check if it's a local clipboard supporting JPICEDT_DATA_FLAVOR :
946 if (transferable.isDataFlavorSupported(TransferableGraphic.JPICEDT_DATA_FLAVOR)){
947 Element[] content = (Element[])transferable.getTransferData(TransferableGraphic.JPICEDT_DATA_FLAVOR);
948 for (int i=0; i<content.length; i++){
949 // translate clipboard so that new content doesn't hide old one
950 // Warning : this translates the source !!!
951 if (translate) content[i].translate(grid.getSnapStep(),-grid.getSnapStep());
952 // add (a copy of the) content to the current drawing
953 Element element = (Element)content[i].clone();
954 drawing.addElement(element);
955 getEditorKit().getSelectionHandler().addToSelection(element);
956 }
957 fireSelectionUpdate(kit.getSelectionHandler().asArray(), SelectionEvent.EventType.SELECT);
958 }
959
960 // otherwise that may be the System Clipboard : only text is supported :
961 else {
962 StringReader reader = new StringReader(jpicedt.MiscUtilities.getClipboardStringContent(clipbrd));
963 Parser parser = jpicedt.MiscUtilities.createParser();
964 insert(reader,parser);
965 }
966
967 // else does nothing
968 }
969
970 /**
971 * Add the content of the System's ClipBoard to the current drawing, then select it.
972 * More specifically, we try to parse the string and insert the parsed content.
973 * @param translate if true, translate the pasted content by a grid step so that it doesn't hide old one
974 */
975 public void paste(boolean translate) throws ParserException, IOException, UnsupportedFlavorException {
976 paste(Toolkit.getDefaultToolkit().getSystemClipboard(),translate);
977 }
978
979 /**
980 * Copy the content of the current selection (through a GraphicTransferable) to the
981 * System's clipboard (after a formatting to text), AND to the given clipboard if non-null
982 * (the latter can be a local clipboard supporting more data-flavors than the system clipboard)
983 * @param clipbrd the target clipboard ; can be null, in which case only the System clipboard
984 * is modified.
985 */
986 public void copy(Clipboard clipbrd) {
987 // if selection buffer is empty, don't modify clipboard's content
988 if (getSelectionSize() == 0) return;
989 // create array of selected Elements for GraphicTransferable
990 Element[] elements = new Element[getSelectionSize()];
991 int counter = 0;
992 for (Iterator it = selection(); it.hasNext();){
993 Element e = (Element)it.next();
994 elements[counter++] = e;
995 }
996 // create formatted string using "write" with selectionOnly = true :
997 StringWriter writer = new StringWriter();
998 try {
999 write(writer,true);
1000 // create GraphicTransferable :
1001 TransferableGraphic transferable = new TransferableGraphic(elements,writer.toString());
1002 writer.close();
1003 if (clipbrd!=null) clipbrd.setContents(transferable,transferable);
1004 Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1005 StringSelection ss = new StringSelection(writer.toString());
1006 systemClipboard.setContents(ss,ss);
1007 } catch (IOException ioEx){ioEx.printStackTrace();}
1008
1009 }
1010
1011 /**
1012 * Copy the content of the current selection to the
1013 * System's clipboard (after a formatting to text)
1014 */
1015 public void copy() {
1016 copy(null);
1017 }
1018
1019 /**
1020 * Cut the content of the current selection (through a GraphicTransferable) to the
1021 * System clipboard, AND to the given ClipBoard if non-null.
1022 * @param clipbrd the target clipboard ; can be null, in which case only the System clipboard
1023 * is modified.
1024 */
1025 public void cut(Clipboard clipbrd) {
1026 // if selection buffer is empty, don't modify clipboard's content
1027 if (getSelectionSize() == 0) return;
1028 copy(clipbrd);
1029 deleteSelection();
1030 }
1031
1032 /**
1033 * Cut the content of the current selection to the System clipboard, after formatting to text.
1034 */
1035 public void cut() {
1036 cut(null);
1037 }
1038 /**
1039 * group all selected objects into a new PicGroup and add it to the drawing.
1040 * @since jPicEdt 1.2.a
1041 */
1042 public void groupSelection(){
1043 PicGroup group = new PicGroup(getEditorKit().getSelectionHandler().asCollection());
1044 getEditorKit().getSelectionHandler().delete(drawing); // delete old selected elements
1045 drawing.addElement(group);
1046 getEditorKit().getSelectionHandler().addToSelection(group); // select group
1047 fireSelectionUpdate(group, SelectionEvent.EventType.SELECT);
1048 }
1049
1050 /**
1051 * fetch all Element's belonging to the given PicGroup and add them to
1052 * its parent, removing the given PicGroup from its parent afterward.
1053 */
1054 public void unGroup(PicGroup g){
1055 getEditorKit().getSelectionHandler().unSelectAll(); // otherwise the selectionHandler just gets confused
1056 BranchElement p = g.getParent();
1057 p.removeChild(g);
1058 int max = g.getChildCount();
1059 for (int i=0; i<max; i++){
1060 Element o = (Element)g.getChildAt(0); // always fetch at position 0
1061 p.addChild(o); // hence removed from group (was former parent)
1062 select(o,true); // incremental
1063 }
1064 }
1065
1066 /**
1067 * Notify all listeners that have registered interest for notification on this event type.
1068 * @param element the Element that was (un)selected
1069 * @param type the event type
1070 */
1071 protected void fireSelectionUpdate(Element element, SelectionEvent.EventType type){
1072 Object[] listeners = listenerList.getListenerList();
1073 SelectionEvent e = null;
1074 for (int i = listeners.length-2; i>=0; i-=2) {
1075 // lazily create the event :
1076 if (e==null) e = new SelectionEvent(this,element,type);
1077 if (listeners[i]==SelectionListener.class) {
1078 ((SelectionListener)listeners[i+1]).selectionUpdate(e);
1079 }
1080 }
1081 }
1082
1083 /**
1084 * Notify all listeners that have registered interest for notification on this event type.
1085 * @param elements the Element's that were (un)selected
1086 * @param type the event type
1087 */
1088 protected void fireSelectionUpdate(Element[] elements, SelectionEvent.EventType type){
1089 Object[] listeners = listenerList.getListenerList();
1090 SelectionEvent e = null;
1091 for (int i = listeners.length-2; i>=0; i-=2) {
1092 // lazily create the event :
1093 if (e==null) e = new SelectionEvent(this,elements,type);
1094 if (listeners[i]==SelectionListener.class) {
1095 ((SelectionListener)listeners[i+1]).selectionUpdate(e);
1096 }
1097 }
1098 }
1099
1100 /**
1101 * adds a SelectionListener to the Canvas
1102 */
1103 public void addSelectionListener(SelectionListener l){
1104 listenerList.add(SelectionListener.class, l);
1105 }
1106
1107 /**
1108 * removes a SelectionListener from the Canvas
1109 */
1110 public void removeSelectionListener(SelectionListener l){
1111 listenerList.remove(SelectionListener.class, l);
1112 }
1113
1114
1115
1116
1117
1118
1119
1120
1121 ////////////////////////////////////
1122 ///// SCROLLABLE INTERFACE and rel.
1123 ////////////////////////////////////
1124
1125 /**
1126 * @return the drawing board size (aka ScrollPane's View size, as opposed to ViewPort's size)
1127 */
1128 public Dimension getPreferredScrollableViewportSize() {
1129
1130 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
1131 Dimension componentSize = getPageFormat().getSizePx(zoom);
1132 // return half screen size at maximum
1133 return(new Dimension((int)Math.min(componentSize.width,screenSize.width/2),
1134 (int)Math.min(componentSize.height,screenSize.height/2)));
1135 }
1136
1137 /**
1138 * @return a grid step
1139 */
1140 public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
1141
1142 return (int)(grid.getSnapStep() * zoom);
1143 }
1144
1145 /**
1146 * @return the viewport size minus a grid step
1147 */
1148 public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
1149
1150 if (orientation == SwingConstants.HORIZONTAL)
1151 return (int)(visibleRect.width - grid.getSnapStep() * zoom);
1152 else
1153 return (int)(visibleRect.height - grid.getSnapStep() * zoom);
1154 }
1155
1156 /**
1157 * @return false for this implementation
1158 */
1159 public boolean getScrollableTracksViewportWidth() {
1160 return false;
1161 }
1162
1163 /**
1164 * @return false for this implementation
1165 */
1166 public boolean getScrollableTracksViewportHeight() {
1167 return false;
1168 }
1169
1170
1171
1172
1173
1174
1175
1176
1177 //////////////////////////
1178 //// MISC
1179 //////////////////////////
1180
1181
1182 /**
1183 * Overriden from JComponent
1184 * Signals that this component can receive focus (useful for handling keyevents)
1185 */
1186 public boolean isRequestFocusEnabled(){
1187 return true;
1188 }
1189
1190
1191
1192
1193
1194 //////////////////////////
1195 //// PEMOUSE LISTENER
1196 //////////////////////////
1197
1198 /**
1199 * Adds the specified mouse listener to receive mouse events from this component.
1200 * If l is null, no exception is thrown and no action is performed.
1201 * @param l the mouse listener.
1202 */
1203 public synchronized void addPEMouseInputListener(PEMouseInputListener l) {
1204 if (l == null) {
1205 return;
1206 }
1207 mouseInputListener = PEEventMulticaster.add(mouseInputListener,l);
1208 this.enableEvents(AWTEvent.MOUSE_EVENT_MASK);
1209 this.enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
1210 }
1211
1212 /**
1213 * Removes the specified mouse listener so that it no longer
1214 * receives mouse events from this component. This method performs
1215 * no function, nor does it throw an exception, if the listener
1216 * specified by the argument was not previously added to this component.
1217 * If l is null, no exception is thrown and no action is performed.
1218 *
1219 * @param l the mouse listener.
1220 */
1221 public synchronized void removePEMouseInputListener(PEMouseInputListener l) {
1222 if (l == null) {
1223 return;
1224 }
1225 mouseInputListener = PEEventMulticaster.remove(mouseInputListener, l);
1226 }
1227
1228 /**
1229 * Processes mouse events occurring on this component by
1230 * dispatching them to any registered
1231 * <code>PEMouseListener</code> objects.
1232 * @param e the mouse event.
1233 */
1234 protected void processMouseEvent(MouseEvent e) {
1235 super.processMouseEvent(e); // process "standar" mouse-events BEFORE (see pb with JPopupMenu for instance)
1236 if (mouseInputListener != null) {
1237 view2ModelTransform.transform(e.getPoint(),peMousePoint);
1238 PEMouseEvent me = new PEMouseEvent(e, this, peMousePoint);
1239 switch(e.getID()) {
1240 case MouseEvent.MOUSE_PRESSED:
1241 mouseInputListener.mousePressed(me);
1242 break;
1243 case MouseEvent.MOUSE_RELEASED:
1244 mouseInputListener.mouseReleased(me);
1245 break;
1246 case MouseEvent.MOUSE_CLICKED:
1247 mouseInputListener.mouseClicked(me);
1248 break;
1249 case MouseEvent.MOUSE_EXITED:
1250 mouseInputListener.mouseExited(me);
1251 break;
1252 case MouseEvent.MOUSE_ENTERED:
1253 mouseInputListener.mouseEntered(me);
1254 break;
1255 }
1256 }
1257
1258 }
1259
1260 /**
1261 * Processes mouse motion events occurring on this component by
1262 * dispatching them to any registered
1263 * <code>PEMouseInputListener</code> objects.
1264 * @param e the mouse motion event.
1265 */
1266 protected void processMouseMotionEvent(MouseEvent e) {
1267 super.processMouseMotionEvent(e); // process "standar" mouse-events
1268 if (mouseInputListener != null) {
1269 view2ModelTransform.transform(e.getPoint(),peMousePoint);
1270 PEMouseEvent me = new PEMouseEvent(e, this, peMousePoint);
1271 switch(e.getID()) {
1272 case MouseEvent.MOUSE_MOVED:
1273 mouseInputListener.mouseMoved(me);
1274 break;
1275 case MouseEvent.MOUSE_DRAGGED:
1276 mouseInputListener.mouseDragged(me);
1277 break;
1278 }
1279 }
1280
1281 }
1282} // class PECanvas
1283
1284