1 /*
  2  * graphics2d.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]>
  5  *
  6  * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  7  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  8  *
  9  * This code is free software; you can redistribute it and/or modify it
 10  * under the terms of the GNU General Public License version 2 only, as
 11  * published by the Free Software Foundation.  Oracle designates this
 12  * particular file as subject to the "Classpath" exception as provided
 13  * by Oracle in the LICENSE file that accompanied OpenJDK 8 source code.
 14  *
 15  * This code is distributed in the hope that it will be useful, but WITHOUT
 16  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 17  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 18  * version 2 for more details (a copy is included in the LICENSE file that
 19  * accompanied this code).
 20  *
 21  * You should have received a copy of the GNU General Public License
 22  * along with this program; if not, write to the Free Software
 23  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 24  */
 25 
 26 // Graphics classes of OpenJDK 8 translated to Javascript
 27 
 28 /**
 29  * This class is a wrapper that implements 2D drawing functions on a canvas.
 30  * Creates a new instance wrapping the given HTML canvas.
 31  * @constructor
 32  * @author Renaud Pawlak
 33  * @author Emmanuel Puybaret
 34  */
 35 function Graphics2D(canvas) {
 36   this.context = canvas.getContext("2d");
 37   this.context.imageSmoothingEnabled = true;
 38   this.context.setTransform(1, 0, 0, 1, 0, 0);
 39   // Need to store also the current transform in Graphics2D 
 40   // because context.currentTransform isn't supported under all browsers
 41   this.currentTransform = new java.awt.geom.AffineTransform(1., 0., 0., 1., 0., 0.);
 42   
 43   var computedStyle = window.getComputedStyle(canvas);
 44   this.context.font = computedStyle.font;
 45   this.color = computedStyle.color;
 46   this.background = computedStyle.background;
 47 }
 48 Graphics2D.prototype.constructor = Graphics2D;
 49 
 50 /**
 51  * Clears the canvas.
 52  */
 53 Graphics2D.prototype.clear = function() {
 54   this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
 55 }
 56 
 57 /**
 58  * Gets the wrapped canvas context.
 59  */
 60 Graphics2D.prototype.getContext = function() {
 61   return this.context;
 62 }
 63 
 64 /**
 65  * Draws a shape on the canvas using the current stroke.
 66  * @param {java.awt.Shape} shape the shape to be drawn
 67  */
 68 Graphics2D.prototype.draw = function(shape) {
 69   this.createPathFromShape(shape);
 70   this.context.stroke();
 71 }
 72 
 73 /**
 74  * @param {java.awt.Shape} shape the shape to create a path from
 75  * @private
 76  */
 77 Graphics2D.prototype.createPathFromShape = function(s) {
 78   this.context.beginPath();
 79   var it = s.getPathIterator(java.awt.geom.AffineTransform.getTranslateInstance(0, 0));
 80   var coords = new Array(6);
 81   while (!it.isDone()) {
 82     switch (it.currentSegment(coords)) {
 83       case java.awt.geom.PathIterator.SEG_MOVETO:
 84         this.context.moveTo(coords[0], coords[1]);
 85         break;
 86       case java.awt.geom.PathIterator.SEG_LINETO:
 87         this.context.lineTo(coords[0], coords[1]);
 88         break;
 89       case java.awt.geom.PathIterator.SEG_QUADTO:
 90         this.context.lineTo(coords[0], coords[1]);
 91         break;
 92       case java.awt.geom.PathIterator.SEG_CUBICTO:
 93         this.context.bezierCurveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
 94         break;
 95       case java.awt.geom.PathIterator.SEG_CLOSE:
 96         this.context.closePath();
 97         break;
 98       default:
 99         break;
100     }
101     it.next();
102   }
103 }
104 
105 /**
106  * Fills a shape on the canvas using the current paint.
107  * @param {java.awt.Shape} shape the shape to be filled
108  */
109 Graphics2D.prototype.fill = function(s) {
110   this.createPathFromShape(s);
111   this.context.fill();
112 }
113 
114 /**
115  * Draws an image on the canvas.
116  * @param {HTMLImageElement} img the image to be drawn
117  * @param {number} x
118  * @param {number} y
119  * @param {string} [bgcolor]
120  */
121 Graphics2D.prototype.drawImage = function(img, x, y, bgcolor) {
122   this.context.drawImage(img, x, y);
123 }
124 
125 /**
126  * Draws an image on the canvas.
127  * @param {HTMLImageElement} img the image to be drawn
128  * @param {number} x
129  * @param {number} y
130  * @param {number} width
131  * @param {number} height
132  * @param {string} [bgcolor]
133  */
134 Graphics2D.prototype.drawImageWithSize = function(img, x, y, width, height, bgcolor) {
135   this.context.drawImage(img, x, y, width, height);
136 }
137 
138 /**
139  * Gets the current clip.
140  * @return {java.awt.Shape} the clip as a shape
141  */
142 Graphics2D.prototype.getClip = function() {
143   return this.clipZone !== undefined ? this.clipZone : null;
144 }
145 
146 /**
147  * Sets the current clip.
148  * @param {java.awt.Shape} clip the clip as a shape
149  */
150 Graphics2D.prototype.setClip = function(clip) {
151   if (this.clipZone !== undefined) {
152     this.context.restore();
153     delete this.clipZone;
154   }
155   if (clip != null) {
156     this.clip(clip);
157   }
158 }
159 
160 /**
161  * Adds the given clip to the current clip.
162  * @param {java.awt.Shape} clip the added clip as a shape
163  */
164 Graphics2D.prototype.clip = function(clip) {
165   if (this.clipZone === undefined) {
166     // Save current clipping zone
167     this.context.save();
168   }
169   this.clipZone = clip;
170   if (clip != null) {
171     this.createPathFromShape(clip);
172     this.context.clip();
173   }
174 }
175 
176 /**
177  * Sets the current clip as a rectangle region.
178  * @param {number} x
179  * @param {number} y
180  * @param {number} width
181  * @param {number} height
182  */
183 Graphics2D.prototype.clipRect = function(x, y, width, height) {
184   this.setClip(new java.awt.geom.Rectangle2D.Double(x, y, width, height));
185 }
186 
187 /**
188  * Translates the canvas transform matrix.
189  * @param {number} x
190  * @param {number} y
191  */
192 Graphics2D.prototype.translate = function(x, y) {
193   this.currentTransform.translate(x, y);
194   this.context.translate(x, y);
195 }
196 
197 /**
198  * Draws a string outline with the current stroke.
199  * @param {string} str
200  * @param {number} x
201  * @param {number} y
202  */
203 Graphics2D.prototype.drawStringOutline = function(str, x, y) {
204   this.context.strokeText(str, x, y);
205 }
206 
207 /**
208  * Draws a string with the current paint.
209  * @param {string} str
210  * @param {number} x
211  * @param {number} y
212  */
213 Graphics2D.prototype.drawString = function(str, x, y) {
214   this.context.fillText(str, x, y);
215 }
216 
217 /**
218  * Fills the given rectangular region with the current paint.
219  * @param {number} x
220  * @param {number} y
221  * @param {number} width
222  * @param {number} height
223  */
224 Graphics2D.prototype.fillRect = function(x, y, width, height) {
225   this.context.fillRect(x, y, width, height);
226 }
227 
228 /**
229  * Sets the current stroke and fill style as a CSS style.
230  * @param {string} color a CSS style
231  */
232 Graphics2D.prototype.setColor = function(color) {
233   this.color = color;
234   this.context.strokeStyle = color;
235   this.context.fillStyle = color;
236 }
237 
238 /**
239  * Gets the current color.
240  */
241 Graphics2D.prototype.getColor = function() {
242   return this.color;
243 }
244 
245 Graphics2D.prototype.setComposite = function(c) {
246   this.setColor(c);
247 }
248 
249 /**
250  * Sets the alpha component for all subsequent drawing and fill operations.
251  * @param {number} alpha
252  */
253 Graphics2D.prototype.setAlpha = function(alpha) {
254   this.context.globalAlpha = alpha;
255 }
256 
257 /**
258  * Gets the alpha component of the canvas.
259  * @return {number}
260  */
261 Graphics2D.prototype.getAlpha = function() {
262   return this.context.globalAlpha;
263 }
264 
265 /**
266  * Rotates the canvas current transform matrix.
267  * @param {number} theta the rotation angle
268  * @param {number} [x] the rotation origin (x)
269  * @param {number} [y] the rotation origin (y)
270  */
271 Graphics2D.prototype.rotate = function(theta, x, y) {
272   if (typeof x === 'number' && typeof y === 'number') {
273     this.currentTransform.rotate(theta, x, y);
274     this.context.translate(-x, -y);
275     this.context.rotate(theta);
276     this.context.translate(x, y);
277   } else {
278     this.currentTransform.rotate(theta);
279     this.context.rotate(theta);
280   }
281 }
282 
283 /**
284  * Scales the canvas current transform matrix.
285  * @param {number} sx the x scale factor
286  * @param {number} sy the y scale factor
287  */
288 Graphics2D.prototype.scale = function(sx, sy) {
289   this.currentTransform.scale(sx, sy);
290   this.context.scale(sx, sy);
291 }
292 
293 /**
294  * Shears the canvas current transform matrix.
295  * @param {number} shx the x shear factor
296  * @param {number} shy the y shear factor
297  */
298 Graphics2D.prototype.shear = function(shx, shy) {
299   this.currentTransform.shear(shx, shy);
300   this.context.transform(0, shx, shy, 0, 0, 0);
301 }
302 
303 /**
304  * @ignore
305  */
306 Graphics2D.prototype.dispose = function() {
307 }
308 
309 /**
310  * Sets the current font.
311  * @param {string} font a CSS font descriptor
312  */
313 Graphics2D.prototype.setFont = function(font) {
314   this.context.font = font;
315 }
316 
317 /**
318  * Gets the current font.
319  * @return {string} a CSS font descriptor
320  */
321 Graphics2D.prototype.getFont = function() {
322   return this.context.font;
323 }
324 
325 /**
326  * Sets the fill style as a color.
327  * @param {string} color a CSS color descriptor
328  */
329 Graphics2D.prototype.setBackground = function(color) {
330   this.background = color;
331   this.context.fillStyle = color;
332 }
333 
334 /**
335  * Gets the fill style.
336  * @return {string} a CSS color descriptor
337  */
338 Graphics2D.prototype.getBackground = function() {
339   return this.background;
340 }
341 
342 /**
343  * Sets (overrides) the current transform matrix.
344  * @param {java.awt.geom.AffineTransform} transform the new transform matrix
345  */
346 Graphics2D.prototype.setTransform = function(transform) {
347   this.currentTransform.setTransform(transform);
348   this.context.setTransform(transform.getScaleX(), transform.getShearY(), transform.getShearX(), transform.getScaleY(), transform.getTranslateX(), transform.getTranslateY());
349 }
350 
351 /**
352  * Gets the current transform matrix.
353  * @return {java.awt.geom.AffineTransform} the current transform matrix
354  */
355 Graphics2D.prototype.getTransform = function() {
356   return new java.awt.geom.AffineTransform(this.currentTransform);
357 }
358 
359 /**
360  * Applies the given transform matrix to the current transform matrix.
361  * @param {java.awt.geom.AffineTransform} transform the transform matrix to be applied
362  */
363 Graphics2D.prototype.transform = function(transform) {
364   this.currentTransform.concatenate(transform);
365   this.context.transform(transform.getScaleX(), transform.getShearX(), transform.getShearY(), transform.getScaleY(), transform.getTranslateX(), transform.getTranslateY());
366 }
367 
368 Graphics2D.prototype.setPaintMode = function() {
369 }
370 
371 /**
372  * Gets the current paint.
373  * @return {string|CanvasPattern}
374  */
375 Graphics2D.prototype.getPaint = function() {
376   return this.color;
377 }
378 
379 /**
380  * Sets the current paint.
381  * @param {string|CanvasPattern} paint
382  */
383 Graphics2D.prototype.setPaint = function(paint) {
384   if (typeof paint === "string") {
385     this.setColor(paint);
386   } else {
387     this.context.strokeStyle = paint;
388     this.context.fillStyle = paint;
389   }
390 }
391 
392 /**
393  * Sets the current stroke.
394  */
395 Graphics2D.prototype.setStroke = function(s) {
396   this.context.lineWidth = s.getLineWidth();
397   if (s.getDashArray() != null) {
398     this.context.setLineDash(s.getDashArray());
399     this.context.lineDashOffset = s.getDashPhase();
400   } else {
401     this.context.setLineDash([]);
402   }
403   switch (s.getLineJoin()) {
404     case java.awt.BasicStroke.JOIN_BEVEL:
405       this.context.lineJoin = "bevel";
406       break;
407     case java.awt.BasicStroke.JOIN_MITER:
408       this.context.lineJoin = "miter";
409       break;
410     case java.awt.BasicStroke.JOIN_ROUND:
411       this.context.lineJoin = "round";
412       break;
413   }
414   switch (s.getEndCap()) {
415     case java.awt.BasicStroke.CAP_BUTT:
416       this.context.lineCap = "butt";
417       break;
418     case java.awt.BasicStroke.CAP_ROUND:
419       this.context.lineCap = "round";
420       break;
421     case java.awt.BasicStroke.CAP_SQUARE:
422       this.context.lineCap = "square";
423       break;
424   }
425   this.context.miterLimit = s.getMiterLimit();
426 }
427 
428 /**
429  * Creates a pattern from an image.
430  * @param {HTMLImageElement} image
431  * @return CanvasPattern
432  */
433 Graphics2D.prototype.createPattern = function(image) {
434   return this.context.createPattern(image, 'repeat');
435 }
436 
437 
438 /**
439  * This utility class allows to get the metrics of a given font. Note that this class will approximate
440  * the metrics on older browsers where CanvasRenderingContext2D.measureText() is only partially implemented.
441  * Builds a font metrics instance for the given font.
442  * @param {string} font the given font, in a CSS canvas-compatible representation
443  * @constructor
444  * @author Renaud Pawlak
445  * @author Emmanuel Puybaret
446  */
447 function FontMetrics(font) {
448   this.approximated = false;
449   this.font = font;
450   this.cached = false;
451 }
452 FontMetrics.prototype.constructor = FontMetrics;
453 
454 /**
455  * Gets the bounds of the given string for this font metrics.
456  * @param {string} aString the string to get the bounds of
457  * @return {java.awt.geom.Rectangle2D} the bounds as an instance of java.awt.geom.Rectangle2D
458  */
459 FontMetrics.prototype.getStringBounds = function(aString) {
460   this.compute(aString);
461   this.cached = false;
462   return new java.awt.geom.Rectangle2D.Double(0, -this.ascent, this.width, this.height);
463 }
464 
465 /**
466  * Gets the font ascent.
467  * @return {number} the font ascent
468  */
469 FontMetrics.prototype.getAscent = function() {
470   if (!this.cached) {
471     this.compute("Llp");
472   }
473   return this.ascent;
474 }
475 
476 /**
477  * Gets the font descent.
478  * @return {number} the font descent
479  */
480 FontMetrics.prototype.getDescent = function() {
481   if (!this.cached) {
482     this.compute("Llp");
483   }
484   return this.descent;
485 }
486 
487 /**
488  * Gets the font height.
489  * @return {number} the font height
490  */
491 FontMetrics.prototype.getHeight = function() {
492   if (!this.cached) {
493     this.compute("Llp");
494   }
495   return this.height;
496 }
497 
498 /**
499  * Computes the various dimensions of the given string, for the current canvas and font.
500  * This function caches the results so that it can be fast accessed in other functions.
501  * @param {string} aString the string to compute the dimensions of
502  * @private
503  */
504 FontMetrics.prototype.compute = function(aString) {
505   if (!FontMetrics.context) {
506     FontMetrics.context = document.createElement("canvas").getContext("2d");
507   }
508   FontMetrics.context.font = this.font;
509   var textMetrics = FontMetrics.context.measureText(aString);
510   if (textMetrics.fontBoundingBoxAscent) {
511     this.cached = true;
512     this.ascent = textMetrics.fontBoundingBoxAscent;
513     this.descent = textMetrics.fontBoundingBoxDescent;
514     this.height = this.ascent + this.descent;
515     this.width = textMetrics.width;
516   } else {
517     // height info is not available on old browsers, so we build an approx.
518     if (!this.approximated) {
519       this.approximated = true;
520       var font = new Font(this.font);
521       this.height = parseInt(font.size);
522       if (["Times", "Serif", "Helvetica"].indexOf(font.family) === -1) {
523         this.height *= 1.18;
524       }
525       this.descent = 0.23 * this.height;
526       this.ascent = this.height - this.descent;
527       this.cached = true;
528     }
529     this.width = textMetrics.width;
530   }
531 }
532 
533 
534 /**
535  * A font utility class.
536  * Creates a new font from a CSS font descriptor.
537  * @param cssFontDecriptor {string|Object} the font descriptor as a CSS string or an object {style, size, family, weight}
538  * @constructor
539  * @author Renaud Pawlak
540  */
541 function Font(cssFontDecriptor) {
542   // font desciptors are normalized by the browser using the getComputedStyle function
543   if (!Font.element) {
544     Font.element = document.createElement('span');
545     Font.element.style.display = 'none';
546     document.body.appendChild(Font.element);
547   }
548   if (typeof cssFontDecriptor === 'string') {
549     Font.element.style.font = cssFontDecriptor;
550   } else {
551     if (cssFontDecriptor.style) {
552       Font.element.style.fontStyle = cssFontDecriptor.style;
553     }
554     if (cssFontDecriptor.size) {
555       Font.element.style.fontSize = cssFontDecriptor.size;
556     }
557     if (cssFontDecriptor.family) {
558       Font.element.style.fontFamily = cssFontDecriptor.family;
559     }
560     if (cssFontDecriptor.weight) {
561       Font.element.style.fontWeight = cssFontDecriptor.weight;
562     }
563   }
564   this.computedStyle = window.getComputedStyle(Font.element);
565   this.size = this.computedStyle.fontSize;
566   this.family = this.computedStyle.fontFamily;
567   this.style = this.computedStyle.fontStyle;
568   this.weight = this.computedStyle.fontWeight;
569 }
570 Font.prototype.constructor = Font;
571 
572 /**
573  * Returns the font as a browser-normalized CSS string.
574  * @return {string}
575  */
576 Font.prototype.toString = function() {
577   var font = '';
578   if (this.weight != 'normal') {
579     font = this.weight + ' ';
580   }
581   if (this.style != 'normal') {
582     font += this.style + ' ';
583   }
584   font += this.size + ' ' + this.family;
585   return font;
586 }
587 
588 
589 /**
590  * Creates an empty action.
591  * Adapted from javax.swing.AbstractAction
592  * @constructor
593  * @author Georges Saab
594  * @ignore
595  */
596 function AbstractAction() {
597   this.enabled = true;      
598 }
599 
600 /**
601  * Useful constants that can be used as the storage-retrieval key
602  * when setting or getting one of this object's properties (text
603  * or icon).
604  */
605 /**
606  * Not currently used.
607  */
608 AbstractAction.DEFAULT = "Default";
609 
610 /**
611  * The key used for storing the <code>String</code> name
612  * for the action, used for a menu or button.
613  */
614 AbstractAction.NAME = "Name";
615 
616 /**
617  * The key used for storing a short <code>String</code>
618  * description for the action, used for tooltip text.
619  */
620 AbstractAction.SHORT_DESCRIPTION = "ShortDescription";
621 
622 /**
623  * The key used for storing a longer <code>String</code>
624  * description for the action, could be used for context-sensitive help.
625  */
626 AbstractAction.LONG_DESCRIPTION = "LongDescription";
627 
628 /**
629  * The key used for storing a small <code>Icon</code>, such
630  * as <code>ImageIcon</code>.  This is typically used with
631  * menus such as <code>JMenuItem</code>.
632  * <p>If the same <code>Action</code> is used with menus and buttons you'll
633  * typically specify both a <code>SMALL_ICON</code> and a
634  * <code>LARGE_ICON_KEY</code>.  The menu will use the
635  * <code>SMALL_ICON</code> and the button will use the
636  * <code>LARGE_ICON_KEY</code>.
637  */
638 AbstractAction.SMALL_ICON = "SmallIcon";
639 
640 /**
641  * The key used to determine the command <code>String</code> for the
642  * <code>ActionEvent</code> that will be created when an
643  * <code>Action</code> is going to be notified as the result of
644  * residing in a <code>Keymap</code> associated with a
645  * <code>JComponent</code>.
646  */
647 AbstractAction.ACTION_COMMAND_KEY = "ActionCommandKey";
648 
649 /**
650  * The key used for storing a <code>KeyStroke</code> to be used as the
651  * accelerator for the action.
652  */
653 AbstractAction.ACCELERATOR_KEY = "AcceleratorKey";
654 
655 /**
656  * The key used for storing an <code>Integer</code> that corresponds to
657  * one of the <code>KeyEvent</code> key codes.  The value is
658  * commonly used to specify a mnemonic.  For example:
659  * <code>myAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_A)</code>
660  * sets the mnemonic of <code>myAction</code> to 'a', while
661  * <code>myAction.putValue(Action.MNEMONIC_KEY, KeyEvent.getExtendedKeyCodeForChar('\u0444'))</code>
662  * sets the mnemonic of <code>myAction</code> to Cyrillic letter "Ef".
663  */
664 AbstractAction.MNEMONIC_KEY = "MnemonicKey";
665 
666 /**
667  * The key used for storing a <code>Boolean</code> that corresponds
668  * to the selected state.  This is typically used only for components
669  * that have a meaningful selection state.  For example,
670  * <code>JRadioButton</code> and <code>JCheckBox</code> make use of
671  * this but instances of <code>JMenu</code> don't.
672  * <p>This property differs from the others in that it is both read
673  * by the component and set by the component.  For example,
674  * if an <code>Action</code> is attached to a <code>JCheckBox</code>
675  * the selected state of the <code>JCheckBox</code> will be set from
676  * that of the <code>Action</code>.  If the user clicks on the
677  * <code>JCheckBox</code> the selected state of the <code>JCheckBox</code>
678  * <b>and</b> the <code>Action</code> will <b>both</b> be updated.
679  */
680 AbstractAction.SELECTED_KEY = "SwingSelectedKey";
681 
682 /**
683  * The key used for storing an <code>Integer</code> that corresponds
684  * to the index in the text (identified by the <code>NAME</code>
685  * property) that the decoration for a mnemonic should be rendered at.  If
686  * the value of this property is greater than or equal to the length of
687  * the text, it will treated as -1.
688  */
689 AbstractAction.DISPLAYED_MNEMONIC_INDEX_KEY = "SwingDisplayedMnemonicIndexKey";
690 
691 /**
692  * The key used for storing an <code>Icon</code>.  This is typically
693  * used by buttons, such as <code>JButton</code> and
694  * <code>JToggleButton</code>.
695  * <p>If the same <code>Action</code> is used with menus and buttons you'll
696  * typically specify both a <code>SMALL_ICON</code> and a
697  * <code>LARGE_ICON_KEY</code>.  The menu will use the
698  * <code>SMALL_ICON</code> and the button the <code>LARGE_ICON_KEY</code>.
699  */
700 AbstractAction.LARGE_ICON_KEY = "SwingLargeIconKey";
701 
702 /**
703  * Unsupported operation. Subclasses should override this method if they want
704  * to associate a real action to this class.
705  * @param {java.awt.event.ActionEvent} ev
706  */
707 AbstractAction.prototype.actionPerformed = function(ev) {
708   throw new UnsupportedOperationException();
709 }
710 
711 /**
712  * Gets the <code>Object</code> associated with the specified key.
713  * @param {string} key a string containing the specified <code>key</code>
714  * @return {Object} the binding <code>Object</code> stored with this key; if there
715  *          are no keys, it will return <code>null</code>
716  */
717 AbstractAction.prototype.getValue = function(key) {
718   if (key == "enabled") {
719     return this.enabled;
720   }
721   if (this.arrayTable == null) {
722     return null;
723   }
724   return this.arrayTable[key];
725 }
726 
727 /**
728  * Sets the <code>Value</code> associated with the specified key.
729  * @param {string} key  the <code>String</code> that identifies the stored object
730  * @param {Object} newValue the <code>Object</code> to store using this key
731  */
732 AbstractAction.prototype.putValue = function(key, newValue) {
733   var oldValue = null;
734   if (key == "enabled") {
735     if (newValue == null || !(newValue instanceof Boolean)) {
736       newValue = false;
737     }
738     oldValue = enabled;
739     this.enabled = newValue;
740   } else {
741     if (this.arrayTable == null) {
742       this.arrayTable = {};
743     }
744     if (this.arrayTable[key] != null)
745       oldValue = this.arrayTable[key];
746     // Remove the entry for key if newValue is null
747     // else put in the newValue for key.
748     if (newValue == null) {
749       delete this.arrayTable.key;
750     } else {
751       this.arrayTable[key] = newValue;
752     }
753   }
754   if (this.changeSupport != null) {
755     this.firePropertyChange(key, oldValue, newValue);
756   }
757 }
758 
759 /**
760  * Returns true if the action is enabled.
761  * @return {boolean} true if the action is enabled, false otherwise
762  */
763 AbstractAction.prototype.isEnabled = function() {
764   return this.enabled;
765 }
766 
767 /**
768  * Sets whether the Action is enabled.
769  * @param {boolean} newValue true to enable the action, false to disable it
770  */
771 AbstractAction.prototype.setEnabled = function(enabled) {
772   if (this.enabled != enabled) {
773     this.enabled = enabled;
774     if (this.changeSupport != null) {
775       this.firePropertyChange("enabled", !enabled, enabled);
776     }
777   }
778 }
779 
780 /**
781  * Returns an array of <code>Object</code> which are keys for
782  * which values have been set for this <code>AbstractAction</code>,
783  * or <code>null</code> if no keys have values set.
784  * @return an array of key objects, or <code>null</code> if no keys have values set
785  */
786 AbstractAction.prototype.getKeys = function() {
787   if (this.arrayTable == null) {
788     return null;
789   }
790   return this.arrayTable.getOwnPropertyNames();
791 }
792 
793 /**
794  * Supports reporting bound property changes.  This method can be called
795  * when a bound property has changed and it will send the appropriate
796  * <code>PropertyChangeEvent</code> to any registered listener.
797  * @protected
798  */
799 AbstractAction.prototype.firePropertyChange = function(propertyName, oldValue, newValue) {
800   if (this.changeSupport == null 
801       || (oldValue != null && newValue != null && oldValue == newValue)) {
802     return;
803   }
804   this.changeSupport.firePropertyChange(propertyName, oldValue, newValue);
805 }  
806 
807 /**
808  * Adds a <code>PropertyChangeListener</code> to the listener list.
809  * The listener is registered for all properties.
810  * <p>A <code>PropertyChangeEvent</code> will get fired in response to setting
811  * a bound property, e.g. <code>setFont</code>, <code>setBackground</code>,
812  * or <code>setForeground</code>.
813  * Note that if the current component is inheriting its foreground,
814  * background, or font from its container, then no event will be
815  * fired in response to a change in the inherited property.
816  * @param {PropertyChangeListener} listener The <code>PropertyChangeListener</code> to be added
817  */
818 AbstractAction.prototype.addPropertyChangeListener = function(listener) {
819   if (this.changeSupport == null) {
820     this.changeSupport = new PropertyChangeSupport(this);
821   }
822   this.changeSupport.addPropertyChangeListener(listener);
823 }
824 
825 /**
826  * Removes a <code>PropertyChangeListener</code> from the listener list.
827  * This removes a <code>PropertyChangeListener</code> that was registered
828  * for all properties.
829  * @param {PropertyChangeListener} listener  the <code>PropertyChangeListener</code> to be removed
830  */
831 AbstractAction.prototype.removePropertyChangeListener = function(listener) {
832   if (this.changeSupport == null) {
833     return;
834   }
835   this.changeSupport.removePropertyChangeListener(listener);
836 }
837 
838 /**
839  * Returns an array of all the <code>PropertyChangeListener</code>s added
840  * to this AbstractAction with addPropertyChangeListener().
841  * @return {PropertyChangeListener[]} all of the <code>PropertyChangeListener</code>s added or an empty
842  *         array if no listeners have been added
843  */
844 AbstractAction.prototype.getPropertyChangeListeners = function() {
845   if (this.changeSupport == null) {
846     return new PropertyChangeListener[0];
847   }
848   return this.changeSupport.getPropertyChangeListeners();
849 }
850