001 /**
002 * Copyright (c) 2009, Piet Blok
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without
006 * modification, are permitted provided that the following conditions
007 * are met:
008 *
009 * * Redistributions of source code must retain the above copyright
010 * notice, this list of conditions and the following disclaimer.
011 * * Redistributions in binary form must reproduce the above
012 * copyright notice, this list of conditions and the following
013 * disclaimer in the documentation and/or other materials provided
014 * with the distribution.
015 * * Neither the name of the copyright holder nor the names of the
016 * contributors may be used to endorse or promote products derived
017 * from this software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
020 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
021 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
022 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
023 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
024 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
025 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
026 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
027 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
028 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
029 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
030 */
031
032 package org.pbjar.jxlayer.plaf.ext.transform;
033
034 import java.awt.Dimension;
035 import java.awt.geom.AffineTransform;
036 import java.awt.geom.Area;
037 import java.awt.geom.Rectangle2D;
038 import java.util.Arrays;
039 import java.util.Map;
040 import java.util.WeakHashMap;
041
042 import javax.swing.JComponent;
043 import javax.swing.event.ChangeEvent;
044 import javax.swing.event.ChangeListener;
045
046 import org.jdesktop.jxlayer.JXLayer;
047
048 /**
049 * This is an implementation of {@link TransformModel} with methods to
050 * explicitly set transformation values.
051 *
052 * @author Piet Blok
053 */
054 public class DefaultTransformModel implements TransformModel {
055
056 /**
057 * Enum for internal convenience.
058 *
059 * Describes the values that on change trigger recalculation of the
060 * transform. All have a default value, used for initializing arrays.
061 *
062 * These enums are used for two purposes:
063 *
064 * 1: To easily detect a change that requires renewed calculation of the
065 * transform (both program values and user options).
066 *
067 * 2: To generalize setters (both program values and user options) and
068 * getters (only userOptions) for the various values.
069 *
070 * There are two groups:
071 *
072 * 1: Program values that reflect the current size etc. of affected
073 * components
074 *
075 * 2: User options
076 */
077 private enum Type {
078 /*
079 * Program values
080 */
081 LayerWidth(0),
082
083 LayerHeight(0),
084
085 ViewWidth(0),
086
087 ViewHeight(0),
088
089 /*
090 * User options
091 */
092 PreferredScale(1.0),
093
094 Rotation(0.0),
095
096 ShearX(0.0),
097
098 ShearY(0.0),
099
100 QuadrantRotation(0),
101
102 PreserveAspectRatio(Boolean.TRUE),
103
104 ScaleToPreferredSize(Boolean.FALSE),
105
106 Mirror(Boolean.FALSE),
107
108 ;
109
110 public static Object[] createArray() {
111 Object[] array = new Object[values().length];
112 for (Type type : values()) {
113 array[type.ordinal()] = type.defaultValue;
114 }
115 return array;
116 }
117
118 private Object defaultValue;
119
120 private Type(Object defaultValue) {
121 this.defaultValue = defaultValue;
122 }
123 }
124
125 private final Map<ChangeListener, Object> listeners = new WeakHashMap<ChangeListener, Object>();
126
127 /**
128 * The transform object that will be recalculated upon any change.
129 */
130 private final AffineTransform transform = new AffineTransform();
131
132 /**
133 * Is populated with the current values.
134 */
135 private final Object[] values = Type.createArray();
136
137 /**
138 * Is populated with the previous values.
139 */
140 private final Object[] prevValues = Type.createArray();
141
142 @Override
143 public void addChangeListener(ChangeListener listener) {
144 listeners.put(listener, null);
145 }
146
147 /**
148 * Get the scale.
149 *
150 * @return the scale
151 * @see #setScale(double)
152 */
153 public double getScale() {
154 return getValue(Type.PreferredScale);
155 }
156
157 @Override
158 public AffineTransform getPreferredTransform(Dimension size,
159 JXLayer<?> layer) {
160 double centerX = size == null ? 0 : size.getWidth() / 2.0;
161 double centerY = size == null ? 0 : size.getHeight() / 2.0;
162 AffineTransform transform = transformNoScale(centerX, centerY);
163 double scaleX = getValue(Type.PreferredScale);
164 double scaleY = scaleX;
165 transform.translate(centerX, centerY);
166 transform.scale(getValue(Type.Mirror) ? -scaleX : scaleX, scaleY);
167 transform.translate(-centerX, -centerY);
168 return transform;
169 }
170
171 /**
172 * Get the quadrant rotation value. The default value is {@code 0}.
173 *
174 * @return the quadrant rotation value
175 * @see #setQuadrantRotation(int)
176 */
177 public int getQuadrantRotation() {
178 return getValue(Type.QuadrantRotation);
179 }
180
181 /**
182 * Get the rotation value in radians as set by {@link #setRotation(double)}.
183 * The default value is {@code 0}.
184 *
185 * @return the rotation value.
186 * @see #setRotation(double)
187 */
188 public double getRotation() {
189 return getValue(Type.Rotation);
190 }
191
192 /**
193 * Get the shearX value as set by {@link #setShearX(double)}; The default
194 * value is {@code 0}.
195 *
196 * @return the shear x value
197 * @see #setShearX(double)
198 */
199 public double getShearX() {
200 return getValue(Type.ShearX);
201 }
202
203 /**
204 * Get the shearY value as set by {@link #setShearY(double)}; The default
205 * value is {@code 0}.
206 *
207 * @return the shear y value
208 * @see #setShearY(double)
209 */
210 public double getShearY() {
211 return getValue(Type.ShearY);
212 }
213
214 /**
215 * Return the currently active {@link AffineTransform}. Recalculate if
216 * needed.
217 *
218 * @return the currently active {@link AffineTransform}
219 */
220 @Override
221 public AffineTransform getTransform(JXLayer<?> layer) {
222 JComponent view = layer.getView();
223 /*
224 * Set the current actual program values in addition to the user
225 * options.
226 */
227 setValue(Type.LayerWidth, layer == null ? 0 : layer.getWidth());
228 setValue(Type.LayerHeight, layer == null ? 0 : layer.getHeight());
229 setValue(Type.ViewWidth, view == null ? 0 : view.getWidth());
230 setValue(Type.ViewHeight, view == null ? 0 : view.getHeight());
231 /*
232 * If any change to previous values, recompute the transform.
233 */
234 if (!Arrays.equals(prevValues, values)) {
235 System.arraycopy(values, 0, prevValues, 0, values.length);
236 transform.setToIdentity();
237 if (view != null) {
238 double centerX = layer == null ? 0 : layer.getWidth() / 2.0;
239 double centerY = layer == null ? 0 : layer.getHeight() / 2.0;
240
241 AffineTransform nonScaledTransform = transformNoScale(centerX,
242 centerY);
243
244 double scaleX, scaleY;
245 if (getValue(Type.ScaleToPreferredSize)) {
246 scaleX = getValue(Type.PreferredScale);
247 scaleY = scaleX;
248 } else {
249 Area area = new Area(new Rectangle2D.Double(0, 0, view
250 .getWidth(), view.getHeight()));
251 area.transform(nonScaledTransform);
252 Rectangle2D bounds = area.getBounds2D();
253 scaleX = layer == null ? 0 : layer.getWidth()
254 / bounds.getWidth();
255 scaleY = layer == null ? 0 : layer.getHeight()
256 / bounds.getHeight();
257
258 if (getValue(Type.PreserveAspectRatio)) {
259 scaleX = Math.min(scaleX, scaleY);
260 scaleY = scaleX;
261 }
262 }
263
264 transform.translate(centerX, centerY);
265 transform.scale(getValue(Type.Mirror) ? -scaleX : scaleX,
266 scaleY);
267 transform.translate(-centerX, -centerY);
268
269 transform.concatenate(nonScaledTransform);
270 }
271 }
272 return transform;
273 }
274
275 /**
276 * Get the mirror property.
277 * <p>
278 * The default value is {@code false}.
279 * </p>
280 *
281 * @return {@code true} if the transformation will mirror the view.
282 * @see #setMirror(boolean)
283 */
284 public boolean isMirror() {
285 return getValue(Type.Mirror);
286 }
287
288 /**
289 * Get the preserve aspect ratio value.
290 * <p>
291 * The default value is {@code true}.
292 * </p>
293 *
294 * @return {@code true} if preserving aspect ratio, {@code false} otherwise
295 * @see #setPreserveAspectRatio(boolean)
296 */
297 public boolean isPreserveAspectRatio() {
298 return getValue(Type.PreserveAspectRatio);
299 }
300
301 /**
302 * Get the scale to preferred size value.
303 * <p>
304 * The default value is {@code false}.
305 * </p>
306 * <p>
307 * When {@code true}, the view is scaled according to the preferred scale,
308 * regardless of the size of the {@link JXLayer}.
309 * </p>
310 * <p>
311 * When {@code false}, the view is scaled to occupy as much as possible of
312 * the size of the {@link JXLayer}.
313 * </p>
314 *
315 * @return {@code true} if scale to preferred size, {@code false} otherwise
316 * @see #setScaleToPreferredSize(boolean)
317 */
318 public boolean isScaleToPreferredSize() {
319 return getValue(Type.ScaleToPreferredSize);
320 }
321
322 @Override
323 public void removeChangeListener(ChangeListener listener) {
324 listeners.remove(listener);
325 }
326
327 /**
328 * Set the mirror property.
329 * <p>
330 * The default value is {@code false}
331 * </p>
332 *
333 * @param newValue
334 * the new value
335 * @see #isMirror()
336 */
337 public void setMirror(boolean newValue) {
338 setValue(Type.Mirror, newValue);
339 }
340
341 /**
342 * Set a scale.
343 * <p>
344 * The scale is primarily used to calculate a preferred size. Unless {@code
345 * ScaleToPreferredSize} is set to {@code true} (see
346 * {@link #setScaleToPreferredSize(boolean)} and
347 * {@link #isScaleToPreferredSize()}), actual scaling itself is calculated
348 * such that the view occupies as much space as possible on the
349 * {@link JXLayer}.
350 * </p>
351 * <p>
352 * The default value is 1.
353 * </p>
354 *
355 * @param newValue
356 * the preferred scale
357 * @throws IllegalArgumentException
358 * when the argument value is 0
359 * @see #getScale()
360 */
361 public void setScale(double newValue) throws IllegalArgumentException {
362 if (newValue == 0.0) {
363 throw new IllegalArgumentException(
364 "Preferred scale can not be set to 0");
365 }
366 setValue(Type.PreferredScale, newValue);
367 }
368
369 /**
370 * Set preserve aspect ratio.
371 * <p>
372 * The default value is {@code true}.
373 * </p>
374 *
375 * @param newValue
376 * the new value
377 * @see #isPreserveAspectRatio()
378 */
379 public void setPreserveAspectRatio(boolean newValue) {
380 setValue(Type.PreserveAspectRatio, newValue);
381 }
382
383 /**
384 * Set the rotation in quadrants. The default value is {@code 0}.
385 *
386 * @param newValue
387 * the number of quadrants
388 * @see #getQuadrantRotation()
389 */
390 public void setQuadrantRotation(int newValue) {
391 setValue(Type.QuadrantRotation, newValue);
392 }
393
394 /**
395 * Set the rotation in radians. The default value is {@code 0}.
396 *
397 * @param newValue
398 * the rotation in radians
399 * @see #getRotation()
400 */
401 public void setRotation(double newValue) {
402 setValue(Type.Rotation, newValue);
403 }
404
405 /**
406 * Set scaleToPreferredSize.
407 * <p>
408 * The default value is {@code false}.
409 * </p>
410 * <p>
411 * When {@code true}, the view is scaled according to the preferred scale,
412 * regardless of the size of the {@link JXLayer}.
413 * </p>
414 * <p>
415 * When {@code false}, the view is scaled to occupy as much as possible of
416 * the size of the {@link JXLayer}.
417 * </p>
418 *
419 * @param newValue
420 * the new value
421 * @see #isScaleToPreferredSize()
422 */
423 public void setScaleToPreferredSize(boolean newValue) {
424 setValue(Type.ScaleToPreferredSize, newValue);
425 }
426
427 /**
428 * Set the shearX value. The default value is {@code 0}.
429 *
430 * @param newValue
431 * the shear x
432 * @see #getShearX()
433 */
434 public void setShearX(double newValue) {
435 setValue(Type.ShearX, newValue);
436 }
437
438 /**
439 * Set the shearY value. The default value is {@code 0}.
440 *
441 * @param newValue
442 * the shear y
443 * @see #getShearY()
444 */
445 public void setShearY(double newValue) {
446 setValue(Type.ShearY, newValue);
447 }
448
449 /**
450 * If {!oldValue.equals(newValue)}, a {@link ChangeEvent} will be fired.
451 *
452 * @param oldValue
453 * an old value
454 * @param newValue
455 * a new value
456 */
457 protected void fireChangeEvent(Object oldValue, Object newValue) {
458 if (!oldValue.equals(newValue)) {
459 ChangeEvent event = new ChangeEvent(this);
460 for (ChangeListener listener : listeners.keySet()) {
461 listener.stateChanged(event);
462 }
463 }
464 }
465
466 @SuppressWarnings("unchecked")
467 private <T> T getValue(Type type) {
468 return (T) values[type.ordinal()];
469 }
470
471 /**
472 * Set a value and fire a PropertyChange.
473 *
474 * @param type
475 * the value type
476 * @param newValue
477 * the new value
478 */
479 private void setValue(Type type, Object newValue) {
480 Object oldValue = values[type.ordinal()];
481 values[type.ordinal()] = newValue;
482 fireChangeEvent(oldValue, newValue);
483 }
484
485 /**
486 * Apply the prescribed transformations, excluding the scale.
487 *
488 * @param centerX
489 * a center X
490 * @param centerY
491 * a center Y
492 * @return a new {@link AffineTransform}
493 */
494 private AffineTransform transformNoScale(double centerX, double centerY) {
495 AffineTransform at = new AffineTransform();
496 at.translate(centerX, centerY);
497 at.rotate(getRotation());
498 at.quadrantRotate(getQuadrantRotation());
499 at.shear(getShearX(), getShearY());
500 at.translate(-centerX, -centerY);
501 return at;
502 }
503
504 }