Worked on image save as a file in Moodle.
[moodle/mihaisucan.git] / lib / paintweb / build / paintweb.src.js
blob9b45e4d8d058d4b1584daafabf78a0d71718e6ec
1 /*
2  * © 2009 ROBO Design
3  * http://www.robodesign.ro
4  *
5  * $Date: 2009-07-26 21:29:57 +0300 $
6  */
8 /**
9  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
10  * @fileOverview Minimal JavaScript library which provides functionality for 
11  * cross-browser compatibility support.
12  */
14 /**
15  * @namespace Holds methods and properties necessary throughout the entire 
16  * application.
17  */
18 var pwlib = {};
20 /**
21  * @namespace Holds pre-packaged files.
22  * @type Object
23  */
24 pwlib.fileCache = {};
26 /**
27  * @namespace Holds the implementation of each drawing tool.
28  *
29  * @type Object
30  *
31  * @see PaintWeb#toolRegister Register a new drawing tool into a PaintWeb 
32  * instance.
33  * @see PaintWeb#toolActivate Activate a drawing tool in a PaintWeb instance.
34  * @see PaintWeb#toolUnregister Unregister a drawing tool from a PaintWeb 
35  * instance.
36  *
37  * @see PaintWeb.config.toolDefault The default tool being activated when 
38  * a PaintWeb instance is initialized.
39  * @see PaintWeb.config.tools Holds the list of tools to be loaded automatically 
40  * when a PaintWeb instance is initialized.
41  */
42 pwlib.tools = {};
44 /**
45  * @namespace Holds all the PaintWeb extensions.
46  *
47  * @type Object
48  * @see PaintWeb#extensionRegister Register a new extension into a PaintWeb 
49  * instance.
50  * @see PaintWeb#extensionUnregister Unregister an extension from a PaintWeb 
51  * instance.
52  * @see PaintWeb.config.extensions Holds the list of extensions to be loaded 
53  * automatically when a PaintWeb instance is initialized.
54  */
55 pwlib.extensions = {};
57 /**
58  * This function extends objects.
59  *
60  * @example
61  * <code>var <var>obj1</var> = {a: 'a1', b: 'b1', d: 'd1'},
62  *     <var>obj2</var> = {a: 'a2', b: 'b2', c: 'c2'};
63  * 
64  * pwlib.extend(<var>obj1</var>, <var>obj2</var>);</code>
65  * 
66  * // Now <var>obj1.c == 'c2'</var>, while <var>obj1.a</var>, <var>obj1.b</var>
67  * // and <var>obj1.d</var> remain the same.
68  *
69  * // If <code>pwlib.extend(true, <var>obj1</var>, <var>obj2</var>)</code> is
70  * // called, then <var>obj1.a</var>, <var>obj1.b</var>, <var>obj1.c</var>
71  * // become all the same as in <var>obj2</var>.
72  *
73  * @example
74  * <code>var <var>obj1</var> = {a: 'a1', b: 'b1', extend: pwlib.extend};
75  * <var>obj1</var>.extend({c: 'c1', d: 'd1'});</code>
76  *
77  * // In this case the destination object which is to be extend is
78  * // <var>obj1</var>.
79  *
80  * @param {Boolean} [overwrite=false] If the first argument is a boolean, then 
81  * it will be considered as a boolean flag for overwriting (or not) any existing 
82  * methods and properties in the destination object. Thus, any method and 
83  * property from the source object will take over those in the destination. The 
84  * argument is optional, and if it's omitted, then no method/property will be 
85  * overwritten.
86  *
87  * @param {Object} [destination=this] The second argument is the optional 
88  * destination object: the object which will be extended. By default, the 
89  * <var>this</var> object will be extended.
90  *
91  * @param {Object} source The third argument must provide list of methods and 
92  * properties which will be added to the destination object.
93  */
94 pwlib.extend = function () {
95   var name, src, sval, dval;
97   if (typeof arguments[0] === 'boolean') {
98     force = arguments[0];
99     dest  = arguments[1];
100     src   = arguments[2];
101   } else {
102     force = false;
103     dest  = arguments[0];
104     src   = arguments[1];
105   }
107   if (typeof src === 'undefined') {
108     src = dest;
109     dest = this;
110   }
112   if (typeof dest === 'undefined') {
113     return;
114   }
116   for (name in src) {
117     sval = src[name];
118     dval = dest[name];
119     if (force || typeof dval === 'undefined') {
120       dest[name] = sval;
121     }
122   }
126  * Retrieve a string formatted with the provided variables.
128  * <p>The language string must be available in the global <var>lang</var> 
129  * object.
131  * <p>The string can contain any number of variables in the form of 
132  * <code>{var_name}</code>.
134  * @example
135  * lang.table_cells = "The table {name} has {n} cells.";
137  * // later ...
138  * console.log(pwlib.strf(lang.table_cells, {'name' : 'tbl1', 'n' : 11}));
139  * // The output is 'The table tbl1 has 11 cells.'
141  * @param {String} str The string you want to output.
143  * @param {Object} [vars] The variables you want to set in the language string.
145  * @returns {String} The string updated with the variables you provided.
146  */
147 pwlib.strf = function (str, vars) {
148   if (!str) {
149     return str;
150   }
152   var re, i;
154   for (i in vars) {
155     re = new RegExp('{' + i + '}', 'g');
156     str = str.replace(re, vars[i]);
157   }
159   return str;
163  * Parse a JSON string. This method uses the global JSON parser provided by 
164  * the browser natively. The small difference is that this method allows 
165  * normal JavaScript comments in the JSON string.
167  * @param {String} str The JSON string to parse.
168  * @returns The JavaScript object that was parsed.
169  */
170 pwlib.jsonParse = function (str) {
171   str = str.replace(/\s*\/\*(\s|.)+?\*\//g, '').
172             replace(/^\s*\/\/.*$/gm,        '');
174   return JSON.parse(str);
178  * Load a file from a given URL using XMLHttpRequest.
180  * @param {String} url The URL you want to load.
182  * @param {Function} handler The <code>onreadystatechange</code> event handler 
183  * for the XMLHttpRequest object. Your event handler will always receive the 
184  * XMLHttpRequest object as the first parameter.
186  * @param {String} [method="GET"] The HTTP method to use for loading the URL.
188  * @param {String} [send] The string you want to send in an HTTP POST request.
190  * @param {Object} [headers] An object holding the header names and values you 
191  * want to set for the request.
193  * @returns {XMLHttpRequest} The XMLHttpRequest object created by this method.
194  */
195 pwlib.xhrLoad = function (url, handler, method, send, headers) {
196   if (!url) {
197     throw new TypeError('The first argument must be a string!');
198   }
200   if (!method) {
201     method = 'GET';
202   }
204   if (!headers) {
205     headers = {};
206   }
208   if (!send) {
209     send = null;
210   }
212   var xhr = new XMLHttpRequest();
213   xhr.onreadystatechange = function () { handler(xhr); };
214   xhr.open(method, url);
216   for (var header in headers) {
217     xhr.setRequestHeader(header, headers[header]);
218   }
220   xhr.send(send);
222   return xhr;
226  * Check if an URL points to a resource from the same host as the desired one.
228  * <p>Note that data URIs always return true.
230  * @param {String} url The URL you want to check.
231  * @param {String} host The host you want in the URL.
233  * @returns {Boolean} True if the <var>url</var> points to a resource from the 
234  * <var>host</var> given, or false otherwise.
235  */
236 pwlib.isSameHost = function (url, host) {
237   if (!url || !host) {
238     return false;
239   }
241   var pos = url.indexOf(':'),
242       proto = url.substr(0, pos + 1).toLowerCase();
244   if (proto === 'data:') {
245     return true;
246   }
248   if (proto !== 'http:' && proto !== 'https:') {
249     return false;
250   }
252   var urlHost = url.replace(/^https?:\/\//i, '');
253   pos  = urlHost.indexOf('/');
254   if (pos > -1) {
255     urlHost = urlHost.substr(0, pos);
256   }
258   if (urlHost !== host) {
259     return false;
260   }
262   return true;
266  * @class Custom application event.
268  * @param {String} type Event type.
269  * @param {Boolean} [cancelable=false] Tells if the event can be cancelled or 
270  * not.
272  * @throws {TypeError} If the <var>type</var> parameter is not a string.
273  * @throws {TypeError} If the <var>cancelable</var> parameter is not a string.
275  * @see pwlib.appEvents for the application events interface which allows adding 
276  * and removing event listeners.
277  */
278 pwlib.appEvent = function (type, cancelable) {
279   if (typeof type !== 'string') {
280     throw new TypeError('The first argument must be a string');
281   } else if (typeof cancelable === 'undefined') {
282     cancelable = false;
283   } else if (typeof cancelable !== 'boolean') {
284     throw new TypeError('The second argument must be a boolean');
285   }
287   /**
288    * Event target object.
289    * @type Object
290    */
291   this.target = null;
293   /**
294    * Tells if the event can be cancelled or not.
295    * @type Boolean
296    */
297   this.cancelable = cancelable;
299   /**
300    * Tells if the event has the default action prevented or not.
301    * @type Boolean
302    */
303   this.defaultPrevented = false;
305   /**
306    * Event type.
307    * @type String
308    */
309   this.type = type;
311   /**
312    * Prevent the default action of the event.
313    */
314   this.preventDefault = function () {
315     if (cancelable) {
316       this.defaultPrevented = true;
317     }
318   };
320   /**
321    * Stop the event propagation to other event handlers.
322    */
323   this.stopPropagation = function () {
324     this.propagationStopped_ = true;
325   };
329  * @class Application initialization event. This event is not cancelable.
331  * @augments pwlib.appEvent
333  * @param {Number} state The initialization state.
334  * @param {String} [errorMessage] The error message, if any.
336  * @throws {TypeError} If the <var>state</var> is not a number.
337  */
338 pwlib.appEvent.appInit = function (state, errorMessage) {
339   if (typeof state !== 'number') {
340     throw new TypeError('The first argument must be a number.');
341   }
343   /**
344    * Application initialization not started.
345    * @constant
346    */
347   this.INIT_NOT_STARTED = 0;
349   /**
350    * Application initialization started.
351    * @constant
352    */
353   this.INIT_STARTED = 1;
355   /**
356    * Application initialization completed successfully.
357    * @constant
358    */
359   this.INIT_DONE = 2;
361   /**
362    * Application initialization failed.
363    * @constant
364    */
365   this.INIT_ERROR = -1;
367   /**
368    * Initialization state.
369    * @type Number
370    */
371   this.state = state;
373   /**
374    * Initialization error message, if any.
375    * @type String|null
376    */
377   this.errorMessage = errorMessage || null;
379   pwlib.appEvent.call(this, 'appInit');
383  * @class Application destroy event. This event is not cancelable.
385  * @augments pwlib.appEvent
386  */
387 pwlib.appEvent.appDestroy = function () {
388   pwlib.appEvent.call(this, 'appDestroy');
392  * @class GUI show event. This event is not cancelable.
394  * @augments pwlib.appEvent
395  */
396 pwlib.appEvent.guiShow = function () {
397   pwlib.appEvent.call(this, 'guiShow');
401  * @class GUI hide event. This event is not cancelable.
403  * @augments pwlib.appEvent
404  */
405 pwlib.appEvent.guiHide = function () {
406   pwlib.appEvent.call(this, 'guiHide');
410  * @class Tool preactivation event. This event is cancelable.
412  * @augments pwlib.appEvent
414  * @param {String} id The ID of the new tool being activated.
415  * @param {String|null} prevId The ID of the previous tool.
417  * @throws {TypeError} If the <var>id</var> is not a string.
418  * @throws {TypeError} If the <var>prevId</var> is not a string or null.
419  */
420 pwlib.appEvent.toolPreactivate = function (id, prevId) {
421   if (typeof id !== 'string') {
422     throw new TypeError('The first argument must be a string.');
423   } else if (prevId !== null && typeof prevId !== 'string') {
424     throw new TypeError('The second argument must be a string or null.');
425   }
427   /**
428    * Tool ID.
429    * @type String
430    */
431   this.id = id;
433   /**
434    * Previous tool ID.
435    * @type String
436    */
437   this.prevId = prevId;
439   pwlib.appEvent.call(this, 'toolPreactivate', true);
443  * @class Tool activation event. This event is not cancelable.
445  * @augments pwlib.appEvent
447  * @param {String} id The ID the tool which was activated.
448  * @param {String|null} prevId The ID of the previous tool.
450  * @throws {TypeError} If the <var>id</var> is not a string.
451  * @throws {TypeError} If the <var>prevId</var> is not a string or null.
452  */
453 pwlib.appEvent.toolActivate = function (id, prevId) {
454   if (typeof id !== 'string') {
455     throw new TypeError('The first argument must be a string.');
456   } else if (prevId !== null && typeof prevId !== 'string') {
457     throw new TypeError('The second argument must be a string or null.');
458   }
460   /**
461    * Tool ID.
462    * @type String
463    */
464   this.id = id;
466   /**
467    * Previous tool ID.
468    * @type String
469    */
470   this.prevId = prevId;
472   pwlib.appEvent.call(this, 'toolActivate');
476  * @class Tool registration event. This event is not cancelable.
478  * @augments pwlib.appEvent
480  * @param {String} id The ID of the tool being registered in an active PaintWeb 
481  * instance.
483  * @throws {TypeError} If the <var>id</var> is not a string.
484  */
485 pwlib.appEvent.toolRegister = function (id) {
486   if (typeof id !== 'string') {
487     throw new TypeError('The first argument must be a string.');
488   }
490   /**
491    * Tool ID.
492    * @type String
493    */
494   this.id = id;
496   pwlib.appEvent.call(this, 'toolRegister');
500  * @class Tool removal event. This event is not cancelable.
502  * @augments pwlib.appEvent
504  * @param {String} id The ID of the tool being unregistered in an active 
505  * PaintWeb instance.
507  * @throws {TypeError} If the <var>id</var> is not a string.
508  */
509 pwlib.appEvent.toolUnregister = function (id) {
510   if (typeof id !== 'string') {
511     throw new TypeError('The first argument must be a string.');
512   }
514   /**
515    * Tool ID.
516    * @type String
517    */
518   this.id = id;
520   pwlib.appEvent.call(this, 'toolUnregister');
524  * @class Extension registration event. This event is not cancelable.
526  * @augments pwlib.appEvent
528  * @param {String} id The ID of the extension being registered in an active 
529  * PaintWeb instance.
531  * @throws {TypeError} If the <var>id</var> is not a string.
532  */
533 pwlib.appEvent.extensionRegister = function (id) {
534   if (typeof id !== 'string') {
535     throw new TypeError('The first argument must be a string.');
536   }
538   /**
539    * Extension ID.
540    * @type String
541    */
542   this.id = id;
544   pwlib.appEvent.call(this, 'extensionRegister');
548  * @class Extension removal event. This event is not cancelable.
550  * @augments pwlib.appEvent
552  * @param {String} id The ID of the extension being unregistered in an active 
553  * PaintWeb instance.
555  * @throws {TypeError} If the <var>id</var> is not a string.
556  */
557 pwlib.appEvent.extensionUnregister = function (id) {
558   if (typeof id !== 'string') {
559     throw new TypeError('The first argument must be a string.');
560   }
562   /**
563    * Extension ID.
564    * @type String
565    */
566   this.id = id;
568   pwlib.appEvent.call(this, 'extensionUnregister');
572  * @class Command registration event. This event is not cancelable.
574  * @augments pwlib.appEvent
576  * @param {String} id The ID of the command being registered in an active 
577  * PaintWeb instance.
579  * @throws {TypeError} If the <var>id</var> is not a string.
580  */
581 pwlib.appEvent.commandRegister = function (id) {
582   if (typeof id !== 'string') {
583     throw new TypeError('The first argument must be a string.');
584   }
586   /**
587    * Command ID.
588    * @type String
589    */
590   this.id = id;
592   pwlib.appEvent.call(this, 'commandRegister');
596  * @class Command removal event. This event is not cancelable.
598  * @augments pwlib.appEvent
600  * @param {String} id The ID of the command being unregistered in an active 
601  * PaintWeb instance.
603  * @throws {TypeError} If the <var>id</var> is not a string.
604  */
605 pwlib.appEvent.commandUnregister = function (id) {
606   if (typeof id !== 'string') {
607     throw new TypeError('The first argument must be a string.');
608   }
610   /**
611    * Command ID.
612    * @type String
613    */
614   this.id = id;
616   pwlib.appEvent.call(this, 'commandUnregister');
620  * @class The image save event. This event is cancelable.
622  * @augments pwlib.appEvent
624  * @param {String} dataURL The data URL generated by the browser holding the 
625  * pixels of the image being saved, in PNG format.
626  * @param {Number} width The image width.
627  * @param {Number} height The image height.
628  */
629 pwlib.appEvent.imageSave = function (dataURL, width, height) {
630   /**
631    * The image saved by the browser, using the base64 encoding.
632    * @type String
633    */
634   this.dataURL = dataURL;
636   /**
637    * Image width.
638    * @type Number
639    */
640   this.width = width;
642   /**
643    * Image height.
644    * @type Number
645    */
646   this.height = height;
648   pwlib.appEvent.call(this, 'imageSave', true);
651  * @class The image save result event. This event is not cancelable.
653  * @augments pwlib.appEvent
655  * @param {Boolean} successful Tells if the image save was successful or not.
656  * @param {String} [url] The image address.
657  * @param {String} [urlNew] The new image address. Provide this parameter, if, 
658  * for example, you allow saving images from a remote server to a local server.  
659  * In such cases the image address changes.
660  */
661 pwlib.appEvent.imageSaveResult = function (successful, url, urlNew) {
662   /**
663    * Tells if the image save was successful or not.
664    * @type String
665    */
666   this.successful = successful;
668   /**
669    * The image address.
670    * @type String|null
671    */
672   this.url = url;
674   /**
675    * The new image address.
676    * @type String|null
677    */
678   this.urlNew = urlNew;
680   pwlib.appEvent.call(this, 'imageSaveResult');
684  * @class History navigation event. This event is not cancelable.
686  * @augments pwlib.appEvent
688  * @param {Number} currentPos The new history position.
689  * @param {Number} previousPos The previous history position.
690  * @param {Number} states The number of history states available.
692  * @throws {TypeError} If any of the arguments are not numbers.
693  */
694 pwlib.appEvent.historyUpdate = function (currentPos, previousPos, states) {
695   if (typeof currentPos !== 'number' || typeof previousPos !== 'number' || 
696       typeof states !== 'number') {
697     throw new TypeError('All arguments must be numbers.');
698   }
700   /**
701    * Current history position.
702    * @type Number
703    */
704   this.currentPos = currentPos;
706   /**
707    * Previous history position.
708    * @type Number
709    */
710   this.previousPos = previousPos;
712   /**
713    * History states count.
714    * @type Number
715    */
716   this.states = states;
718   pwlib.appEvent.call(this, 'historyUpdate');
722  * @class Image size change event. This event is not cancelable.
724  * @augments pwlib.appEvent
726  * @param {Number} width The new image width.
727  * @param {Number} height The new image height.
729  * @throws {TypeError} If any of the arguments are not numbers.
730  */
731 pwlib.appEvent.imageSizeChange = function (width, height) {
732   if (typeof width !== 'number' || typeof height !== 'number') {
733     throw new TypeError('Both arguments must be numbers.');
734   }
736   /**
737    * New image width.
738    * @type Number
739    */
740   this.width  = width;
742   /**
743    * New image height.
744    * @type Number
745    */
746   this.height = height;
748   pwlib.appEvent.call(this, 'imageSizeChange');
752  * @class Canvas size change event. This event is not cancelable.
754  * <p>Note that the Canvas size is not the same as the image size. Canvas size 
755  * refers to the scaling of the Canvas elements being applied (due to image 
756  * zooming or due to browser zoom / DPI).
758  * @augments pwlib.appEvent
760  * @param {Number} width The new Canvas style width.
761  * @param {Number} height The new Canvas style height.
762  * @param {Number} scale The new Canvas scaling factor.
764  * @throws {TypeError} If any of the arguments are not numbers.
765  */
766 pwlib.appEvent.canvasSizeChange = function (width, height, scale) {
767   if (typeof width !== 'number' || typeof height !== 'number' || typeof scale 
768       !== 'number') {
769     throw new TypeError('All the arguments must be numbers.');
770   }
772   /**
773    * New Canvas style width.
774    * @type Number
775    */
776   this.width  = width;
778   /**
779    * New Canvas style height.
780    * @type Number
781    */
782   this.height = height;
784   /**
785    * The new Canvas scaling factor.
786    * @type Number
787    */
788   this.scale  = scale;
790   pwlib.appEvent.call(this, 'canvasSizeChange');
794  * @class Image zoom event. This event is cancelable.
796  * @augments pwlib.appEvent
798  * @param {Number} zoom The new image zoom level.
800  * @throws {TypeError} If the <var>zoom</var> argument is not a number.
801  */
802 pwlib.appEvent.imageZoom = function (zoom) {
803   if (typeof zoom !== 'number') {
804     throw new TypeError('The first argument must be a number.');
805   }
807   /**
808    * The new image zoom level.
809    * @type Number
810    */
811   this.zoom = zoom;
813   pwlib.appEvent.call(this, 'imageZoom', true);
817  * @class Image crop event. This event is cancelable.
819  * @augments pwlib.appEvent
821  * @param {Number} x The crop start position on the x-axis.
822  * @param {Number} y The crop start position on the y-axis.
823  * @param {Number} width The cropped image width.
824  * @param {Number} height The cropped image height.
826  * @throws {TypeError} If any of the arguments are not numbers.
827  */
828 pwlib.appEvent.imageCrop = function (x, y, width, height) {
829   if (typeof x !== 'number' || typeof y !== 'number' || typeof width !== 
830       'number' || typeof height !== 'number') {
831     throw new TypeError('All arguments must be numbers.');
832   }
834   /**
835    * The crop start position the x-axis.
836    * @type Number
837    */
838   this.x = x;
840   /**
841    * The crop start position the y-axis.
842    * @type Number
843    */
844   this.y = y;
846   /**
847    * The cropped image width.
848    * @type Number
849    */
850   this.width  = width;
852   /**
853    * The cropped image height.
854    * @type Number
855    */
856   this.height = height;
858   pwlib.appEvent.call(this, 'imageCrop', true);
862  * @class Configuration change event. This event is not cancelable.
864  * @augments pwlib.appEvent
866  * @param {String|Number|Boolean} value The new value.
867  * @param {String|Number|Boolean} previousValue The previous value.
868  * @param {String} config The configuration property that just changed.
869  * @param {String} group The configuration group where the property is found.
870  * @param {Object} groupRef The configuration group object reference.
872  * @throws {TypeError} If the <var>prop</var> argument is not a string.
873  * @throws {TypeError} If the <var>group</var> argument is not a string.
874  * @throws {TypeError} If the <var>groupRef</var> argument is not an object.
875  */
876 pwlib.appEvent.configChange = function (value, previousValue, config, group, 
877     groupRef) {
878   if (typeof config !== 'string') {
879     throw new TypeError('The third argument must be a string.');
880   } else if (typeof group !== 'string') {
881     throw new TypeError('The fourth argument must be a string.');
882   } else if (typeof groupRef !== 'object') {
883     throw new TypeError('The fifth argument must be an object.');
884   }
886   /**
887    * The new value.
888    */
889   this.value = value;
891   /**
892    * The previous value.
893    */
894   this.previousValue = previousValue;
896   /**
897    * Configuration property name.
898    * @type String
899    */
900   this.config = config;
902   /**
903    * Configuration group name.
904    * @type String
905    */
906   this.group = group;
908   /**
909    * Reference to the object holding the configuration property.
910    * @type Object
911    */
912   this.groupRef = groupRef;
914   pwlib.appEvent.call(this, 'configChange');
918  * @class Canvas shadows allowed change event. This event is not cancelable.
920  * @augments pwlib.appEvent
922  * @param {Boolean} allowed Tells the new allowance value.
924  * @throws {TypeError} If the argument is not a boolean value.
925  */
926 pwlib.appEvent.shadowAllow = function (allowed) {
927   if (typeof allowed !== 'boolean') {
928     throw new TypeError('The first argument must be a boolean.');
929   }
931   /**
932    * Tells if the Canvas shadows are allowed or not.
933    * @type Boolean
934    */
935   this.allowed = allowed;
937   pwlib.appEvent.call(this, 'shadowAllow');
941  * @class Clipboard update event. This event is not cancelable.
943  * @augments pwlib.appEvent
945  * @param {ImageData} data Holds the clipboard ImageData.
946  */
947 pwlib.appEvent.clipboardUpdate = function (data) {
948   /**
949    * The clipboard image data.
950    * @type ImageData
951    */
952   this.data = data;
954   pwlib.appEvent.call(this, 'clipboardUpdate');
958  * @class An interface for adding, removing and dispatching of custom 
959  * application events.
961  * @param {Object} target_ The target for all the events.
963  * @see pwlib.appEvent to create application event objects.
964  */
965 pwlib.appEvents = function (target_) {
966   /**
967    * Holds the list of event types and event handlers.
968    *
969    * @private
970    * @type Object
971    */
972   var events_ = {};
974   var eventID_ = 1;
976   /**
977    * Add an event listener.
978    *
979    * @param {String} type The event you want to listen for.
980    * @param {Function} handler The event handler.
981    *
982    * @returns {Number} The event ID.
983    *
984    * @throws {TypeError} If the <var>type</var> argument is not a string.
985    * @throws {TypeError} If the <var>handler</var> argument is not a function.
986    *
987    * @see pwlib.appEvents#remove to remove events.
988    * @see pwlib.appEvents#dispatch to dispatch an event.
989    */
990   this.add = function (type, handler) {
991     if (typeof type !== 'string') {
992       throw new TypeError('The first argument must be a string.');
993     } else if (typeof handler !== 'function') {
994       throw new TypeError('The second argument must be a function.');
995     }
997     var id = eventID_++;
999     if (!(type in events_)) {
1000       events_[type] = {};
1001     }
1003     events_[type][id] = handler;
1005     return id;
1006   };
1008   /**
1009    * Remove an event listener.
1010    *
1011    * @param {String} type The event type.
1012    * @param {Number} id The event ID.
1013    *
1014    * @throws {TypeError} If the <var>type</var> argument is not a string.
1015    *
1016    * @see pwlib.appEvents#add to add events.
1017    * @see pwlib.appEvents#dispatch to dispatch an event.
1018    */
1019   this.remove = function (type, id) {
1020     if (typeof type !== 'string') {
1021       throw new TypeError('The first argument must be a string.');
1022     }
1024     if (!(type in events_) || !(id in events_[type])) {
1025       return;
1026     }
1028     delete events_[type][id];
1029   };
1031   /**
1032    * Dispatch an event.
1033    *
1034    * @param {String} type The event type.
1035    * @param {pwlib.appEvent} ev The event object.
1036    *
1037    * @returns {Boolean} True if the <code>event.preventDefault()</code> has been 
1038    * invoked by one of the event handlers, or false if not.
1039    *
1040    * @throws {TypeError} If the <var>type</var> parameter is not a string.
1041    * @throws {TypeError} If the <var>ev</var> parameter is not an object.
1042    *
1043    * @see pwlib.appEvents#add to add events.
1044    * @see pwlib.appEvents#remove to remove events.
1045    * @see pwlib.appEvent the generic event object.
1046    */
1047   this.dispatch = function (ev) {
1048     if (typeof ev !== 'object') {
1049       throw new TypeError('The second argument must be an object.');
1050     } else if (typeof ev.type !== 'string') {
1051       throw new TypeError('The second argument must be an application event ' +
1052         'object.');
1053     }
1055     // No event handlers.
1056     if (!(ev.type in events_)) {
1057       return false;
1058     }
1060     ev.target = target_;
1062     var id, handlers = events_[ev.type];
1063     for (id in handlers) {
1064       handlers[id].call(target_, ev);
1066       if (ev.propagationStopped_) {
1067         break;
1068       }
1069     }
1071     return ev.defaultPrevented;
1072   };
1077  * @namespace Holds browser information.
1078  */
1079 pwlib.browser = {};
1081 (function () {
1082 var ua = '';
1084 if (window.navigator && window.navigator.userAgent) {
1085   ua = window.navigator.userAgent.toLowerCase();
1089  * @type Boolean
1090  */
1091 pwlib.browser.opera = window.opera ? true : /\bopera\b/.test(ua);
1094  * Webkit is the render engine used primarily by Safari. It's also used by 
1095  * Google Chrome and GNOME Epiphany.
1097  * @type Boolean
1098  */
1099 pwlib.browser.webkit = /\b(applewebkit|webkit)\b/.test(ua);
1102  * Firefox uses the Gecko render engine.
1104  * @type Boolean
1105  */
1106 // In some variations of the User Agent strings provided by Opera, Firefox is 
1107 // mentioned.
1108 pwlib.browser.firefox = /\bfirefox\b/.test(ua) && !pwlib.browser.opera;
1111  * Gecko is the render engine used by Firefox and related products.
1113  * @type Boolean
1114  */
1115 // Typically, the user agent string of WebKit also mentions Gecko. Additionally, 
1116 // Opera mentions Gecko for tricking some sites.
1117 pwlib.browser.gecko = /\bgecko\b/.test(ua) && !pwlib.browser.opera &&
1118                       !pwlib.browser.webkit;
1121  * Microsoft Internet Explorer. The future of computing.
1123  * @type Boolean
1124  */
1125 // Again, Opera allows users to easily fake the UA.
1126 pwlib.browser.msie = /\bmsie\b/.test(ua) && !pwlib.browser.opera;
1129  * Presto is the render engine used by Opera.
1131  * @type Boolean
1132  */
1133 // Older versions of Opera did not mention Presto in the UA string.
1134 pwlib.browser.presto = /\bpresto\b/.test(ua) || pwlib.browser.opera;
1138  * Browser operating system
1140  * @type String
1141  */
1142 pwlib.browser.os = (ua.match(/\b(windows|linux)\b/) || [])[1];
1145  * Tells if the browser is running on an OLPC XO. Typically, only the default 
1146  * Gecko-based browser includes the OLPC XO tokens in the user agent string.
1148  * @type Boolean
1149  */
1150 pwlib.browser.olpcxo = ua.match(/\bolpc\b/) && ua.match(/\bxo\b/);
1152 delete ua;
1153 })();
1157  * @namespace Holds methods and properties necessary for DOM manipulation.
1158  */
1159 pwlib.dom = {};
1162  * @namespace Holds the list of virtual key identifiers and a few characters, 
1163  * each being associated to a key code commonly used by Web browsers.
1165  * @private
1166  */
1167 pwlib.dom.keyNames = {
1168   Help:          6,
1169   Backspace:     8,
1170   Tab:           9,
1171   Clear:         12,
1172   Enter:         13,
1173   Shift:         16,
1174   Control:       17,
1175   Alt:           18,
1176   Pause:         19,
1177   CapsLock:      20,
1178   Cancel:        24,
1179   'Escape':      27,
1180   Space:         32,
1181   PageUp:        33,
1182   PageDown:      34,
1183   End:           35,
1184   Home:          36,
1185   Left:          37,
1186   Up:            38,
1187   Right:         39,
1188   Down:          40,
1189   PrintScreen:   44,
1190   Insert:        45,
1191   'Delete':      46,
1192   Win:           91,
1193   ContextMenu:   93,
1194   '*':           106,
1195   '+':           107,
1196   F1:            112,
1197   F2:            113,
1198   F3:            114,
1199   F4:            115,
1200   F5:            116,
1201   F6:            117,
1202   F7:            118,
1203   F8:            119,
1204   F9:            120,
1205   F10:           121,
1206   F11:           122,
1207   F12:           123,
1208   NumLock:       144,
1209   ';':           186,
1210   '=':           187,
1211   ',':           188,
1212   '-':           189,
1213   '.':           190,
1214   '/':           191,
1215   '`':           192,
1216   '[':           219,
1217   '\\':          220,
1218   ']':           221,
1219   "'":           222
1223  * @namespace Holds the list of codes, each being associated to a virtual key 
1224  * identifier.
1226  * @private
1227  */
1228 pwlib.dom.keyCodes = {
1229   /*
1230    * For almost each key code, these comments give the key name, the 
1231    * keyIdentifier from the DOM 3 Events spec and the Unicode character 
1232    * information (if you would use the decimal code for direct conversion to 
1233    * a character, e.g. String.fromCharCode()). Obviously, the Unicode character 
1234    * information is not to be used, since these are only virtual key codes (not 
1235    * really char codes) associated to key names.
1236    *
1237    * Each key name in here tries to follow the same style as the defined 
1238    * keyIdentifiers from the DOM 3 Events. Thus for the Page Down button, 
1239    * 'PageDown' is used (not other variations like 'pag-up'), and so on.
1240    *
1241    * Multiple key codes might be associated to the same key - it's not an error.
1242    *
1243    * Note that this list is not an exhaustive key codes list. This means that 
1244    * for key A or for key 0, the script will do String.fromCharCode(keyCode), to 
1245    * determine the key. For the case of alpha-numeric keys, this works fine.
1246    */
1248   /*
1249    * Key: Enter
1250    * Unicode: U+0003 [End of text]
1251    *
1252    * Note 1: This keyCode is only used in Safari 2 (older Webkit) for the Enter 
1253    * key.
1254    *
1255    * Note 2: In Gecko this keyCode is used for the Cancel key (see 
1256    * DOM_VK_CANCEL).
1257    */
1258   3: 'Enter',
1260   /*
1261    * Key: Help
1262    * Unicode: U+0006 [Acknowledge]
1263    *
1264    * Note: Taken from Gecko (DOM_VK_HELP).
1265    */
1266   6: 'Help',
1268   /*
1269    * Key: Backspace
1270    * Unicode: U+0008 [Backspace]
1271    * keyIdentifier: U+0008
1272    */
1273   8: 'Backspace',
1275   /*
1276    * Key: Tab
1277    * Unicode: U+0009 [Horizontal tab]
1278    * keyIdentifier: U+0009
1279    */
1280   9: 'Tab',
1282   /*
1283    * Key: Enter
1284    * Unicode: U+0010 [Line feed (LF) / New line (NL) / End of line (EOL)]
1285    *
1286    * Note: Taken from the Unicode characters list. If it ends up as a keyCode in 
1287    * some event, it's simply considered as being the Enter key.
1288    */
1289   10: 'Enter',
1291   /*
1292    * Key: NumPad_Center
1293    * Unicode: U+000C [Form feed]
1294    * keyIdentifier: Clear
1295    *
1296    * Note 1: This keyCode is used when NumLock is off, and the user pressed the 
1297    * 5 key on the numeric pad.
1298    *
1299    * Note 2: Safari 2 (older Webkit) assigns this keyCode to the NumLock key 
1300    * itself.
1301    */
1302   12: 'Clear',
1304   /*
1305    * Key: Enter
1306    * Unicode: U+000D [Carriage return (CR)]
1307    * keyIdentifier: Enter
1308    *
1309    * Note 1: This is the keyCode used by most of the Web browsers when the Enter 
1310    * key is pressed.
1311    *
1312    * Note 2: Gecko associates the DOM_VK_RETURN to this keyCode.
1313    */
1314   13: 'Enter',
1316   /*
1317    * Key: Enter
1318    * Unicode: U+000E [Shift out]
1319    *
1320    * Note: Taken from Gecko (DOM_VK_ENTER).
1321    */
1322   14: 'Enter',
1324   /*
1325    * Key: Shift
1326    * Unicode: U+0010 [Data link escape]
1327    * keyIdentifier: Shift
1328    *
1329    * Note: In older Safari (Webkit) versions Shift+Tab is assigned a different 
1330    * keyCode: keyCode 25.
1331    */
1332   16: 'Shift',
1334   /*
1335    * Key: Control
1336    * Unicode: U+0011 [Device control one]
1337    * keyIdentifier: Control
1338    */
1339   17: 'Control',
1341   /*
1342    * Key: Alt
1343    * Unicode: U+0012 [Device control two]
1344    * keyIdentifier: Alt
1345    */
1346   18: 'Alt',
1348   /*
1349    * Key: Pause
1350    * Unicode: U+0013 [Device control three]
1351    * keyIdentifier: Pause
1352    */
1353   19: 'Pause',
1355   /*
1356    * Key: CapsLock
1357    * Unicode: U+0014 [Device control four]
1358    * keyIdentifier: CapsLock
1359    */
1360   20: 'CapsLock',
1362   /*
1363    * Key: Cancel
1364    * Unicode: U+0018 [Cancel]
1365    * keyIdentifier: U+0018
1366    */
1367   24: 'Cancel',
1369   /*
1370    * Key: Escape
1371    * Unicode: U+001B [Escape]
1372    * keyIdentifier: U+001B
1373    */
1374   27: 'Escape',
1376   /*
1377    * Key: Space
1378    * Unicode: U+0020 [Space]
1379    * keyIdentifier: U+0020
1380    */
1381   32: 'Space',
1383   /*
1384    * Key: PageUp or NumPad_North_East
1385    * Unicode: U+0021 ! [Exclamation mark]
1386    * keyIdentifier: PageUp
1387    */
1388   33: 'PageUp',
1390   /*
1391    * Key: PageDown or NumPad_South_East
1392    * Unicode: U+0022 " [Quotation mark]
1393    * keyIdentifier: PageDown
1394    */
1395   34: 'PageDown',
1397   /*
1398    * Key: End or NumPad_South_West
1399    * Unicode: U+0023 # [Number sign]
1400    * keyIdentifier: PageDown
1401    */
1402   35: 'End',
1404   /*
1405    * Key: Home or NumPad_North_West
1406    * Unicode: U+0024 $ [Dollar sign]
1407    * keyIdentifier: Home
1408    */
1409   36: 'Home',
1411   /*
1412    * Key: Left or NumPad_West
1413    * Unicode: U+0025 % [Percent sign]
1414    * keyIdentifier: Left
1415    */
1416   37: 'Left',
1418   /*
1419    * Key: Up or NumPad_North
1420    * Unicode: U+0026 & [Ampersand]
1421    * keyIdentifier: Up
1422    */
1423   38: 'Up',
1425   /*
1426    * Key: Right or NumPad_East
1427    * Unicode: U+0027 ' [Apostrophe]
1428    * keyIdentifier: Right
1429    */
1430   39: 'Right',
1432   /*
1433    * Key: Down or NumPad_South
1434    * Unicode: U+0028 ( [Left parenthesis]
1435    * keyIdentifier: Down
1436    */
1437   40: 'Down',
1439   /*
1440    * Key: PrintScreen
1441    * Unicode: U+002C , [Comma]
1442    * keyIdentifier: PrintScreen
1443    */
1444   //44: 'PrintScreen',
1446   /*
1447    * Key: Insert or NumPad_Insert
1448    * Unicode: U+002D - [Hyphen-Minus]
1449    * keyIdentifier: Insert
1450    */
1451   45: 'Insert',
1453   /*
1454    * Key: Delete or NumPad_Delete
1455    * Unicode: U+002E . [Full stop / period]
1456    * keyIdentifier: U+007F
1457    */
1458   46: 'Delete',
1460   /*
1461    * Key: WinLeft
1462    * Unicode: U+005B [ [Left square bracket]
1463    * keyIdentifier: Win
1464    *
1465    * Disabled: rarely needed.
1466    */
1467   //91: 'Win',
1469   /*
1470    * Key: WinRight
1471    * Unicode: U+005C \ [Reverse solidus / Backslash]
1472    * keyIdentifier: Win
1473    */
1474   //92: 'Win',
1476   /*
1477    * Key: Menu/ContextMenu
1478    * Unicode: U+005D ] [Right square bracket]
1479    * keyIdentifier: ...
1480    *
1481    * Disabled: Is it Meta? Is it Menu, ContextMenu, what? Too much mess.
1482    */
1483   //93: 'ContextMenu',
1485   /*
1486    * Key: NumPad_0
1487    * Unicode: U+0060 ` [Grave accent]
1488    * keyIdentifier: 0
1489    */
1490   96: '0',
1492   /*
1493    * Key: NumPad_1
1494    * Unicode: U+0061 a [Latin small letter a]
1495    * keyIdentifier: 1
1496    */
1497   97: '1',
1499   /*
1500    * Key: NumPad_2
1501    * Unicode: U+0062 b [Latin small letter b]
1502    * keyIdentifier: 2
1503    */
1504   98: '2',
1506   /*
1507    * Key: NumPad_3
1508    * Unicode: U+0063 c [Latin small letter c]
1509    * keyIdentifier: 3
1510    */
1511   99: '3',
1513   /*
1514    * Key: NumPad_4
1515    * Unicode: U+0064 d [Latin small letter d]
1516    * keyIdentifier: 4
1517    */
1518   100: '4',
1520   /*
1521    * Key: NumPad_5
1522    * Unicode: U+0065 e [Latin small letter e]
1523    * keyIdentifier: 5
1524    */
1525   101: '5',
1527   /*
1528    * Key: NumPad_6
1529    * Unicode: U+0066 f [Latin small letter f]
1530    * keyIdentifier: 6
1531    */
1532   102: '6',
1534   /*
1535    * Key: NumPad_7
1536    * Unicode: U+0067 g [Latin small letter g]
1537    * keyIdentifier: 7
1538    */
1539   103: '7',
1541   /*
1542    * Key: NumPad_8
1543    * Unicode: U+0068 h [Latin small letter h]
1544    * keyIdentifier: 8
1545    */
1546   104: '8',
1548   /*
1549    * Key: NumPad_9
1550    * Unicode: U+0069 i [Latin small letter i]
1551    * keyIdentifier: 9
1552    */
1553   105: '9',
1555   /*
1556    * Key: NumPad_Multiply
1557    * Unicode: U+0070 j [Latin small letter j]
1558    * keyIdentifier: U+002A * [Asterisk / Star]
1559    */
1560   106: '*',
1562   /*
1563    * Key: NumPad_Plus
1564    * Unicode: U+0071 k [Latin small letter k]
1565    * keyIdentifier: U+002B + [Plus]
1566    */
1567   107: '+',
1569   /*
1570    * Key: NumPad_Minus
1571    * Unicode: U+0073 m [Latin small letter m]
1572    * keyIdentifier: U+002D + [Hyphen / Minus]
1573    */
1574   109: '-',
1576   /*
1577    * Key: NumPad_Period
1578    * Unicode: U+0074 n [Latin small letter n]
1579    * keyIdentifier: U+002E . [Period]
1580    */
1581   110: '.',
1583   /*
1584    * Key: NumPad_Division
1585    * Unicode: U+0075 o [Latin small letter o]
1586    * keyIdentifier: U+002F / [Solidus / Slash]
1587    */
1588   111: '/',
1590   112: 'F1',                // p
1591   113: 'F2',                // q
1592   114: 'F3',                // r
1593   115: 'F4',                // s
1594   116: 'F5',                // t
1595   117: 'F6',                // u
1596   118: 'F7',                // v
1597   119: 'F8',                // w
1598   120: 'F9',                // x
1599   121: 'F10',               // y
1600   122: 'F11',               // z
1601   123: 'F12',               // {
1603   /*
1604    * Key: Delete
1605    * Unicode: U+007F [Delete]
1606    * keyIdentifier: U+007F
1607    */
1608   127: 'Delete',
1610   /*
1611    * Key: NumLock
1612    * Unicode: U+0090 [Device control string]
1613    * keyIdentifier: NumLock
1614    */
1615   144: 'NumLock',
1617   186: ';',                 // º (Masculine ordinal indicator)
1618   187: '=',                 // »
1619   188: ',',                 // ¼
1620   189: '-',                 // ½
1621   190: '.',                 // ¾
1622   191: '/',                 // ¿
1623   192: '`',                 // À
1624   219: '[',                 // Û
1625   220: '\\',                // Ü
1626   221: ']',                 // Ý
1627   222: "'"                  // Þ (Latin capital letter thorn)
1629   //224: 'Win',               // à
1630   //229: 'WinIME',            // å or WinIME or something else in Webkit
1631   //255: 'NumLock',           // ÿ, Gecko and Chrome, Windows XP in VirtualBox
1632   //376: 'NumLock'            // Ÿ, Opera, Windows XP in VirtualBox
1635 if (pwlib.browser.gecko) {
1636   pwlib.dom.keyCodes[3] = 'Cancel'; // DOM_VK_CANCEL
1640  * @namespace Holds a list of common wrong key codes in Web browsers.
1642  * @private
1643  */
1644 pwlib.dom.keyCodes_fixes = {
1645   42:   pwlib.dom.keyNames['*'],        // char * to key *
1646   47:   pwlib.dom.keyNames['/'],        // char / to key /
1647   59:   pwlib.dom.keyNames[';'],        // char ; to key ;
1648   61:   pwlib.dom.keyNames['='],        // char = to key =
1649   96:   48,                             // NumPad_0 to char 0
1650   97:   49,                             // NumPad_1 to char 1
1651   98:   50,                             // NumPad_2 to char 2
1652   99:   51,                             // NumPad_3 to char 3
1653   100:  52,                             // NumPad_4 to char 4
1654   101:  53,                             // NumPad_5 to char 5
1655   102:  54,                             // NumPad_6 to char 6
1656   103:  55,                             // NumPad_7 to char 7
1657   104:  56,                             // NumPad_8 to char 8
1658   105:  57,                             // NumPad_9 to char 9
1659   //106:  56,                           // NumPad_Multiply to char 8
1660   //107:  187,                          // NumPad_Plus to key =
1661   109:  pwlib.dom.keyNames['-'],        // NumPad_Minus to key -
1662   110:  pwlib.dom.keyNames['.'],        // NumPad_Period to key .
1663   111:  pwlib.dom.keyNames['/']         // NumPad_Division to key /
1667  * @namespace Holds the list of broken key codes generated by older Webkit 
1668  * (Safari 2).
1670  * @private
1671  */
1672 pwlib.dom.keyCodes_Safari2 = {
1673   63232: pwlib.dom.keyNames.Up,               // 38
1674   63233: pwlib.dom.keyNames.Down,             // 40
1675   63234: pwlib.dom.keyNames.Left,             // 37
1676   63235: pwlib.dom.keyNames.Right,            // 39
1677   63236: pwlib.dom.keyNames.F1,               // 112
1678   63237: pwlib.dom.keyNames.F2,               // 113
1679   63238: pwlib.dom.keyNames.F3,               // 114
1680   63239: pwlib.dom.keyNames.F4,               // 115
1681   63240: pwlib.dom.keyNames.F5,               // 116
1682   63241: pwlib.dom.keyNames.F6,               // 117
1683   63242: pwlib.dom.keyNames.F7,               // 118
1684   63243: pwlib.dom.keyNames.F8,               // 119
1685   63244: pwlib.dom.keyNames.F9,               // 120
1686   63245: pwlib.dom.keyNames.F10,              // 121
1687   63246: pwlib.dom.keyNames.F11,              // 122
1688   63247: pwlib.dom.keyNames.F12,              // 123
1689   63248: pwlib.dom.keyNames.PrintScreen,      // 44
1690   63272: pwlib.dom.keyNames['Delete'],        // 46
1691   63273: pwlib.dom.keyNames.Home,             // 36
1692   63275: pwlib.dom.keyNames.End,              // 35
1693   63276: pwlib.dom.keyNames.PageUp,           // 33
1694   63277: pwlib.dom.keyNames.PageDown,         // 34
1695   63289: pwlib.dom.keyNames.NumLock,          // 144
1696   63302: pwlib.dom.keyNames.Insert            // 45
1701  * A complete keyboard events cross-browser compatibility layer.
1703  * <p>Unfortunately, due to the important differences across Web browsers, 
1704  * simply using the available properties in a single keyboard event is not 
1705  * enough to accurately determine the key the user pressed. Thus, one needs to 
1706  * have event handlers for all keyboard-related events <code>keydown</code>, 
1707  * <code>keypress</code> and <code>keyup</code>.
1709  * <p>This class provides a complete keyboard event compatibility layer. For any 
1710  * new instance you provide the DOM element you want to listen events for, and 
1711  * the event handlers for any of the three events <code>keydown</code> 
1712  * / <code>keypress</code> / <code>keyup</code>.
1714  * <p>Your event handlers will receive the original DOM Event object, with 
1715  * several new properties defined:
1717  * <ul>
1718  *   <li><var>event.keyCode_</var> holds the correct code for event key.
1720  *   <li><var>event.key_</var> holds the key the user pressed. It can be either 
1721  *   a key name like "PageDown", "Delete", "Enter", or it is a character like 
1722  *   "A", "1", or "[".
1724  *   <li><var>event.charCode_</var> holds the Unicode character decimal code.
1726  *   <li><var>event.char_</var> holds the character generated by the event.
1728  *   <li><var>event.repeat_</var> is a boolean property telling if the 
1729  *   <code>keypress</code> event is repeated - the user is holding down the key 
1730  *   for a long-enough period of time to generate multiple events.
1731  * </ul>
1733  * <p>The character-related properties, <var>charCode_</var> and 
1734  * <var>char_</var> are only available in the <code>keypress</code> and 
1735  * <code>keyup</code> event objects.
1737  * <p>This class will ensure that the <code>keypress</code> event is always 
1738  * fired in Webkit and MSIE for all keys, except modifiers. For modifier keys 
1739  * like <kbd>Shift</kbd>, <kbd>Control</kbd>, and <kbd>Alt</kbd>, the 
1740  * <code>keypress</code> event will not be fired, even if the Web browser does 
1741  * it.
1743  * <p>Some user agents like Webkit repeat the <code>keydown</code> event while 
1744  * the user holds down a key. This class will ensure that only the 
1745  * <code>keypress</code> event is repeated.
1747  * <p>If you want to prevent the default action for an event, you should prevent 
1748  * it on <code>keypress</code>. This class will prevent the default action for 
1749  * <code>keydown</code> if need (in MSIE).
1751  * @example
1752  * <code>var <var>klogger</var> = function (<var>ev</var>) {
1753  *   console.log(<var>ev</var>.type +
1754  *     ' keyCode_ ' + <var>ev</var>.keyCode_ +
1755  *     ' key_ ' + <var>ev</var>.key_ +
1756  *     ' charCode_ ' + <var>ev</var>.charCode_ +
1757  *     ' char_ ' + <var>ev</var>.char_ +
1758  *     ' repeat_ ' + <var>ev</var>.repeat_);
1759  * };
1761  * var <var>kbListener</var> = new pwlib.dom.KeyboardEventListener(window,
1762  *               {keydown: <var>klogger</var>,
1763  *                keypress: <var>klogger</var>,
1764  *                keyup: <var>klogger</var>});</code>
1766  * // later when you're done...
1767  * <code><var>kbListener</var>.detach();</code>
1769  * @class A complete keyboard events cross-browser compatibility layer.
1771  * @param {Element} elem_ The DOM Element you want to listen events for.
1773  * @param {Object} handlers_ The object holding the list of event handlers 
1774  * associated to the name of each keyboard event you want to listen. To listen 
1775  * for all the three keyboard events use <code>{keydown: <var>fn1</var>, 
1776  * keypress: <var>fn2</var>, keyup: <var>fn3</var>}</code>.
1778  * @throws {TypeError} If the <var>handlers_</var> object does not contain any 
1779  * event handler.
1780  */
1781 pwlib.dom.KeyboardEventListener = function (elem_, handlers_) {
1782   /*
1783     Technical details:
1785     For the keyup and keydown events the keyCode provided is that of the virtual 
1786     key irrespective of other modifiers (e.g. Shift). Generally, during the 
1787     keypress event, the keyCode holds the Unicode value of the character 
1788     resulted from the key press, say an alphabetic upper/lower-case char, 
1789     depending on the actual intent of the user and depending on the currently 
1790     active keyboard layout.
1792     Examples:
1793     * Pressing p you get keyCode 80 in keyup/keydown, and keyCode 112 in 
1794     keypress.  String.fromCharCode(80) = 'P' and String.fromCharCode(112) = 'p'.
1795     * Pressing P you get keyCode 80 in all events.
1796     * Pressing F1 you get keyCode 112 in keyup, keydown and keypress.
1797     * Pressing 9 you get keyCode 57 in all events.
1798     * Pressing Shift+9 you get keyCode 57 in keyup/keydown, and keyCode 40 in 
1799     keypress. String.fromCharCode(57) = '9' and String.fromCharCode(40) = '('.
1801     * Using the Greek layout when you press v on an US keyboard you get the 
1802     output character ω. The keyup/keydown events hold keyCode 86 which is V.  
1803     This does make sense, since it's the virtual key code we are dealing with 
1804     - not the character code, not the result of pressing the key. The keypress 
1805     event will hold keyCode 969 (ω).
1807     * Pressing NumPad_Minus you get keyCode 109 in keyup/keydown and keyCode 45 
1808     in keypress. Again, this happens because in keyup/keydown you don't get the 
1809     character code, you get the key code, as indicated above. For
1810     your information: String.fromCharCode(109) = 'm' and String.fromCharCode(45) 
1811     = '-'.
1813     Therefore, we need to map all the codes of several keys, like F1-F12, 
1814     Escape, Enter, Tab, etc. This map is held by pwlib.dom.keyCodes. It 
1815     associates, for example, code 112 to F1, or 13 to Enter. This map is used to 
1816     detect virtual keys in all events.
1818     (This is only the general story, details about browser-specific differences 
1819     follow below.)
1821     If the code given by the browser doesn't appear in keyCode maps, it's used 
1822     as is.  The key_ returned is that of String.fromCharCode(keyCode).
1824     In all browsers we consider all events having keyCode <= 32, as being events  
1825     generated by a virtual key (not a character). As such, the keyCode value is 
1826     always searched in pwlib.dom.keyCodes.
1828     As you might notice from the above description, in the keypress event we 
1829     cannot tell the difference from say F1 and p, because both give the code 
1830     112. In Gecko and Webkit we can tell the difference because these UAs also 
1831     set the charCode event property when the key generates a character. If F1 is 
1832     pressed, or some other virtual key, charCode is never set.
1834     In Opera the charCode property is never set. However, the 'which' event 
1835     property is not set for several virtual keys. This means we can tell the 
1836     difference between a character and a virtual key. However, there's a catch: 
1837     not *all* virtual keys have the 'which' property unset. Known exceptions: 
1838     Backspace (8), Tab (9), Enter (13), Shift (16), Control (17), Alt (18), 
1839     Pause (19), Escape (27), End (35), Home (36), Insert (45), Delete (46) and 
1840     NumLock (144). Given we already consider any keyCode <= 32 being one of some 
1841     virtual key, fewer exceptions remain. We only have the End, Home, Insert, 
1842     Delete and the NumLock keys which cannot be 100% properly detected in the 
1843     keypress event, in Opera. To properly detect End/Home we can check if the 
1844     Shift modifier is active or not. If the user wants # instead of End, then 
1845     Shift must be active. The same goes for $ and Home. Thus we now only have 
1846     the '-' (Insert) and the '.' (Delete) characters incorrectly detected as 
1847     being Insert/Delete.
1848     
1849     The above brings us to one of the main visible difference, when comparing 
1850     the pwlib.dom.KeyboardEventListener class and the simple 
1851     pwlib.dom.KeyboardEvent.getKey() function. In getKey(), for the keypress 
1852     event we cannot accurately determine the exact key, because it requires
1853     checking the keyCode used for the keydown event. The KeyboardEventListener
1854     class monitors all the keyboard events, ensuring a more accurate key 
1855     detection.
1857     Different keyboard layouts and international characters are generally 
1858     supported. Tested and known to work with the Cyrillic alphabet (Greek 
1859     keyboard layout) and with the US Dvorak keyboard layouts.
1861     Opera does not fire the keyup event for international characters when 
1862     running on Linux. For example, this happens with the Greek keyboard layout, 
1863     when trying Cyrillic characters.
1865     Gecko gives no keyCode/charCode/which for international characters when 
1866     running on Linux, in the keyup/keydown events. Thus, all such keys remain 
1867     unidentified for these two events. For the keypress event there are no 
1868     issues with such characters.
1870     Webkit and Konqueror 4 also implement the keyIdentifier property from the 
1871     DOM 3 Events specification. In theory, this should be great, but it's not 
1872     without problems.  Sometimes keyCode/charCode/which are all 0, but 
1873     keyIdentifier is properly set. For several virtual keys the keyIdentifier 
1874     value is simply 'U+0000'. Thus, the keyIdentifier is used only if the value 
1875     is not 'Unidentified' / 'U+0000', and only when keyCode/charCode/which are 
1876     not available.
1878     Konqueror 4 does not use the 'U+XXXX' notation for Unicode characters. It 
1879     simply gives the character, directly.
1881     Additionally, Konqueror seems to have some problems with several keyCodes in 
1882     keydown/keyup. For example, the key '[' gives keyCode 91 instead of 219.  
1883     Thus, it effectively gives the Unicode for the character, not the key code.  
1884     This issue is visible with other key as well.
1886     NumPad_Clear is unidentified on Linux in all browsers, but it works on 
1887     Windows.
1889     In MSIE the keypress event is only fired for characters and for Escape, 
1890     Space and Enter. Similarly, Webkit only fires the keypress event for 
1891     characters. However, Webkit does not fire keypress for Escape.
1893     International characters and different keyboard layouts seem to work fine in 
1894     MSIE as well.
1896     As of MSIE 4.0, the keypress event fires for the following keys:
1897       * Letters: A - Z (uppercase and lowercase)
1898       * Numerals: 0 - 9
1899       * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
1900       * System: Escape (27), Space (32), Enter (13)
1902     Documentation about the keypress event:
1903     http://msdn.microsoft.com/en-us/library/ms536939(VS.85).aspx
1905     As of MSIE 4.0, the keydown event fires for the following keys:
1906       * Editing: Delete (46), Insert (45)
1907       * Function: F1 - F12
1908       * Letters: A - Z (uppercase and lowercase)
1909       * Navigation: Home, End, Left, Right, Up, Down
1910       * Numerals: 0 - 9
1911       * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
1912       * System: Escape (27), Space (32), Shift (16), Tab (9)
1914     As of MSIE 5, the event also fires for the following keys:
1915       * Editing: Backspace (8)
1916       * Navigation: PageUp (33), PageDown (34)
1917       * System: Shift+Tab (9)
1919     Documentation about the keydown event:
1920     http://msdn.microsoft.com/en-us/library/ms536938(VS.85).aspx
1922     As of MSIE 4.0, the keyup event fires for the following keys:
1923       * Editing: Delete, Insert
1924       * Function: F1 - F12
1925       * Letters: A - Z (uppercase and lowercase)
1926       * Navigation: Home (36), End (35), Left (37), Right (39), Up (38), Down (40)
1927       * Numerals: 0 - 9
1928       * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
1929       * System: Escape (27), Space (32), Shift (16), Tab (9)
1931     As of MSIE 5, the event also fires for the following keys:
1932       * Editing: Backspace (8)
1933       * Navigation: PageUp (33), PageDown (34)
1934       * System: Shift+Tab (9)
1936     Documentation about the keyup event:
1937     http://msdn.microsoft.com/en-us/library/ms536940(VS.85).aspx
1939     For further gory details and a different implementation see:
1940     http://code.google.com/p/doctype/source/browse/trunk/goog/events/keycodes.js
1941     http://code.google.com/p/doctype/source/browse/trunk/goog/events/keyhandler.js
1943     Opera keydown/keyup:
1944       These events fire for all keys, including for modifiers.
1945       keyCode is always set.
1946       charCode is never set.
1947       which is always set.
1948       keyIdentifier is always undefined.
1950     Opera keypress:
1951       This event fires for all keys, except for modifiers themselves.
1952       keyCode is always set.
1953       charCode is never set.
1954       which is set for all characters. which = 0 for several virtual keys.
1955       which is known to be set for: Backspace (8), Tab (9), Enter (13), Shift 
1956       (16), Control (17), Alt (18), Pause (19), Escape (27), End (35), Home 
1957       (36), Insert (45), Delete (46), NumLock (144).
1958       which is known to be unset for: F1 - F12, PageUp (33), PageDown (34), Left 
1959       (37), Up (38), Right (39), Down (40).
1960       keyIdentifier is always undefined.
1962     MSIE keyup/keypress/keydown:
1963       Event firing conditions are described above.
1964       keyCode is always set.
1965       charCode is never set.
1966       which is never set.
1967       keyIdentifier is always undefined.
1969     Webkit keydown/keyup:
1970       These events fires for all keys, including for modifiers.
1971       keyCode is always set.
1972       charCode is never set.
1973       which is always set.
1974       keyIdentifier is always set.
1976     Webkit keypress:
1977       This event fires for characters keys, similarly to MSIE (see above info).
1978       keyCode is always set.
1979       charCode is always set for all characters.
1980       which is always set.
1981       keyIdentifier is null.
1983     Gecko keydown/keyup:
1984       These events fire for all keys, including for modifiers.
1985       keyCode is always set.
1986       charCode is never set.
1987       which is always set.
1988       keyIdentifier is always undefined.
1990     Gecko keypress:
1991       This event fires for all keys, except for modifiers themselves.
1992       keyCode is only set for virtual keys, not for characters.
1993       charCode is always set for all characters.
1994       which is always set for all characters and for the Enter virtual key.
1995       keyIdentifier is always undefined.
1997     Another important difference between the KeyboardEventListener class and the 
1998     getKey() function is that the class tries to ensure that the keypress event 
1999     is fired for the handler, even if the Web browser does not do it natively.  
2000     Also, the class tries to provide a consistent approach to keyboard event 
2001     repetition when the user holds down a key for longer periods of time, by 
2002     repeating only the keypress event.
2004     On Linux, Opera, Firefox and Konqueror do not repeat the keydown event, only 
2005     keypress. On Windows, Opera, Firefox and MSIE do repeat the keydown and 
2006     keypress events while the user holds down the key. Webkit  repeats the 
2007     keydown and the keypress (when it fires) events on both systems.
2009     The default action can be prevented for during keydown in MSIE, and during 
2010     keypress for the other browsers. In Webkit when keypress doesn't fire, 
2011     keydown needs to be prevented.
2013     The KeyboardEventListener class tries to bring consistency. The keydown 
2014     event never repeats, only the keypress event repeats and it always fires for 
2015     all keys. The keypress event never fires for modifiers. Events should always 
2016     be prevented during keypress - the class deals with preventing the event 
2017     during keydown or keypress as needed in Webkit and MSIE.
2019     If no code/keyIdentifier is given by the browser, the getKey() function 
2020     returns null. In the case of the KeyboardEventListener class, keyCode_ 
2021     / key_ / charCode_ / char_ will be null or undefined.
2022    */
2024   /**
2025    * During a keyboard event flow, this holds the current key code, starting 
2026    * from the <code>keydown</code> event.
2027    *
2028    * @private
2029    * @type Number
2030    */
2031   var keyCode_ = null;
2033   /**
2034    * During a keyboard event flow, this holds the current key, starting from the 
2035    * <code>keydown</code> event.
2036    *
2037    * @private
2038    * @type String
2039    */
2040   var key_ = null;
2042   /**
2043    * During a keyboard event flow, this holds the current character code, 
2044    * starting from the <code>keypress</code> event.
2045    *
2046    * @private
2047    * @type Number
2048    */
2049   var charCode_ = null;
2051   /**
2052    * During a keyboard event flow, this holds the current character, starting 
2053    * from the <code>keypress</code> event.
2054    *
2055    * @private
2056    * @type String
2057    */
2058   var char_ = null;
2060   /**
2061    * True if the current keyboard event is repeating. This happens when the user 
2062    * holds down a key for longer periods of time.
2063    *
2064    * @private
2065    * @type Boolean
2066    */
2067   var repeat_ = false;
2070   if (!handlers_) {
2071     throw new TypeError('The first argument must be of type an object.');
2072   }
2074   if (!handlers_.keydown && !handlers_.keypress && !handlers_.keyup) {
2075     throw new TypeError('The provided handlers object has no keyboard event' +
2076         'handler.');
2077   }
2079   if (handlers_.keydown && typeof handlers_.keydown !== 'function') {
2080     throw new TypeError('The keydown event handler is not a function!');
2081   }
2082   if (handlers_.keypress && typeof handlers_.keypress !== 'function') {
2083     throw new TypeError('The keypress event handler is not a function!');
2084   }
2085   if (handlers_.keyup && typeof handlers_.keyup !== 'function') {
2086     throw new TypeError('The keyup event handler is not a function!');
2087   }
2089   /**
2090    * Attach the keyboard event listeners to the current DOM element.
2091    */
2092   this.attach = function () {
2093     keyCode_ = null;
2094     key_ = null;
2095     charCode_ = null;
2096     char_ = null;
2097     repeat_ = false;
2099     // FIXME: I have some ideas for a solution to the problem of having multiple 
2100     // event handlers like these attached to the same element. Somehow, only one 
2101     // should do all the needed work.
2103     elem_.addEventListener('keydown',  keydown,  false);
2104     elem_.addEventListener('keypress', keypress, false);
2105     elem_.addEventListener('keyup',    keyup,    false);
2106   };
2108   /**
2109    * Detach the keyboard event listeners from the current DOM element.
2110    */
2111   this.detach = function () {
2112     elem_.removeEventListener('keydown',  keydown,  false);
2113     elem_.removeEventListener('keypress', keypress, false);
2114     elem_.removeEventListener('keyup',    keyup,    false);
2116     keyCode_ = null;
2117     key_ = null;
2118     charCode_ = null;
2119     char_ = null;
2120     repeat_ = false;
2121   };
2123   /**
2124    * Dispatch an event.
2125    *
2126    * <p>This function simply invokes the handler for the event of the given 
2127    * <var>type</var>. The handler receives the <var>ev</var> event.
2128    *
2129    * @private
2130    * @param {String} type The event type to dispatch.
2131    * @param {Event} ev The DOM Event object to dispatch to the handler.
2132    */
2133   function dispatch (type, ev) {
2134     if (!handlers_[type]) {
2135       return;
2136     }
2138     var handler = handlers_[type];
2140     if (type === ev.type) {
2141       handler.call(elem_, ev);
2143     } else {
2144       // This happens when the keydown event tries to dispatch a keypress event.
2146       // FIXME: I could use createEvent() ... food for thought for later
2147       var ev_new = {};
2148       pwlib.extend(ev_new, ev);
2149       ev_new.type = type;
2151       // Make sure preventDefault() is not borked...
2152       ev_new.preventDefault = function () {
2153         ev.preventDefault();
2154       };
2156       handler.call(elem_, ev_new);
2157     }
2158   };
2160   /**
2161    * The <code>keydown</code> event handler. This function determines the key 
2162    * pressed by the user, and checks if the <code>keypress</code> event will 
2163    * fire in the current Web browser, or not. If it does not, a synthetic 
2164    * <code>keypress</code> event will be fired.
2165    *
2166    * @private
2167    * @param {Event} ev The DOM Event object.
2168    */
2169   function keydown (ev) {
2170     var prevKey = key_;
2172     charCode_ = null;
2173     char_ = null;
2175     findKeyCode(ev);
2177     ev.keyCode_ = keyCode_;
2178     ev.key_ = key_;
2179     ev.repeat_ = key_ && prevKey === key_ ? true : false;
2181     repeat_ = ev.repeat_;
2183     // When the user holds down a key for a longer period of time, the keypress 
2184     // event is generally repeated. However, in Webkit keydown is repeated (and 
2185     // keypress if it fires keypress for the key). As such, we do not dispatch 
2186     // the keydown event when a key event starts to be repeated.
2187     if (!repeat_) {
2188       dispatch('keydown', ev);
2189     }
2191     // MSIE and Webkit only fire the keypress event for characters 
2192     // (alpha-numeric and symbols).
2193     if (!isModifierKey(key_) && !firesKeyPress(ev)) {
2194       ev.type_ = 'keydown';
2195       keypress(ev);
2196     }
2197   };
2199   /**
2200    * The <code>keypress</code> event handler. This function determines the 
2201    * character generated by the keyboard event.
2202    *
2203    * @private
2204    * @param {Event} ev The DOM Event object.
2205    */
2206   function keypress (ev) {
2207     // We reuse the keyCode_/key_ from the keydown event, because ev.keyCode 
2208     // generally holds the character code during the keypress event.
2209     // However, if keyCode_ is not available, try to determine the key for this 
2210     // event as well.
2211     if (!keyCode_) {
2212       findKeyCode(ev);
2213       repeat_ = false;
2214     }
2216     ev.keyCode_ = keyCode_;
2217     ev.key_ = key_;
2219     findCharCode(ev);
2221     ev.charCode_ = charCode_;
2222     ev.char_ = char_;
2224     // Any subsequent keypress event is considered a repeated keypress (the user 
2225     // is holding down the key).
2226     ev.repeat_ = repeat_;
2227     if (!repeat_) {
2228       repeat_ = true;
2229     }
2231     if (!isModifierKey(key_)) {
2232       dispatch('keypress', ev);
2233     }
2234   };
2236   /**
2237    * The <code>keyup</code> event handler.
2238    *
2239    * @private
2240    * @param {Event} ev The DOM Event object.
2241    */
2242   function keyup (ev) {
2243     /*
2244      * Try to determine the keyCode_ for keyup again, even if we might already 
2245      * have it from keydown. This is needed because the user might press some 
2246      * key which only generates the keydown and keypress events, after which 
2247      * a sudden keyup event is fired for a completely different key.
2248      *
2249      * Example: in Opera press F2 then Escape. It will first generate two 
2250      * events, keydown and keypress, for the F2 key. When you press Escape to 
2251      * close the dialog box, the script receives keyup for Escape.
2252      */
2253     findKeyCode(ev);
2255     ev.keyCode_ = keyCode_;
2256     ev.key_ = key_;
2258     // Provide the character info from the keypress event in keyup as well.
2259     ev.charCode_ = charCode_;
2260     ev.char_ = char_;
2262     dispatch('keyup', ev);
2264     keyCode_ = null;
2265     key_ = null;
2266     charCode_ = null;
2267     char_ = null;
2268     repeat_ = false;
2269   };
2271   /**
2272    * Tells if the <var>key</var> is a modifier or not.
2273    *
2274    * @private
2275    * @param {String} key The key name.
2276    * @returns {Boolean} True if the <var>key</var> is a modifier, or false if 
2277    * not.
2278    */
2279   function isModifierKey (key) {
2280     switch (key) {
2281       case 'Shift':
2282       case 'Control':
2283       case 'Alt':
2284       case 'Meta':
2285       case 'Win':
2286         return true;
2287       default:
2288         return false;
2289     }
2290   };
2292   /**
2293    * Tells if the current Web browser will fire the <code>keypress</code> event 
2294    * for the current <code>keydown</code> event object.
2295    *
2296    * @private
2297    * @param {Event} ev The DOM Event object.
2298    * @returns {Boolean} True if the Web browser will fire 
2299    * a <code>keypress</code> event, or false if not.
2300    */
2301   function firesKeyPress (ev) {
2302     // Gecko does not fire keypress for the Up/Down arrows when the target is an 
2303     // input element.
2304     if ((key_ === 'Up' || key_ === 'Down') && pwlib.browser.gecko && ev.target 
2305         && ev.target.tagName.toLowerCase() === 'input') {
2306       return false;
2307     }
2309     if (!pwlib.browser.msie && !pwlib.browser.webkit) {
2310       return true;
2311     }
2313     // Check if the key is a character key, or not.
2314     // If it's not a character, then keypress will not fire.
2315     // Known exceptions: keypress fires for Space, Enter and Escape in MSIE.
2316     if (key_ && key_ !== 'Space' && key_ !== 'Enter' && key_ !== 'Escape' && 
2317         key_.length !== 1) {
2318       return false;
2319     }
2321     // Webkit doesn't fire keypress for Escape as well ...
2322     if (pwlib.browser.webkit && key_ === 'Escape') {
2323       return false;
2324     }
2326     // MSIE does not fire keypress if you hold Control / Alt down, while Shift 
2327     // is off. Albeit, based on testing I am not completely sure if Shift needs 
2328     // to be down or not. Sometimes MSIE won't fire keypress even if I hold 
2329     // Shift down, and sometimes it does. Eh.
2330     if (pwlib.browser.msie && !ev.shiftKey && (ev.ctrlKey || ev.altKey)) {
2331       return false;
2332     }
2334     return true;
2335   };
2337   /**
2338    * Determine the key and the key code for the current DOM Event object. This 
2339    * function updates the <var>keyCode_</var> and the <var>key_</var> variables 
2340    * to hold the result.
2341    *
2342    * @private
2343    * @param {Event} ev The DOM Event object.
2344    */
2345   function findKeyCode (ev) {
2346     /*
2347      * If the event has no keyCode/which/keyIdentifier values, then simply do 
2348      * not overwrite any existing keyCode_/key_.
2349      */
2350     if (ev.type === 'keyup' && !ev.keyCode && !ev.which && (!ev.keyIdentifier || 
2351           ev.keyIdentifier === 'Unidentified' || ev.keyIdentifier === 'U+0000')) {
2352       return;
2353     }
2355     keyCode_ = null;
2356     key_ = null;
2358     // Try to use keyCode/which.
2359     if (ev.keyCode || ev.which) {
2360       keyCode_ = ev.keyCode || ev.which;
2362       // Fix Webkit quirks
2363       if (pwlib.browser.webkit) {
2364         // Old Webkit gives keyCode 25 when Shift+Tab is used.
2365         if (keyCode_ == 25 && this.shiftKey) {
2366           keyCode_ = pwlib.dom.keyNames.Tab;
2367         } else if (keyCode_ >= 63232 && keyCode_ in pwlib.dom.keyCodes_Safari2) {
2368           // Old Webkit gives wrong values for several keys.
2369           keyCode_ = pwlib.dom.keyCodes_Safari2[keyCode_];
2370         }
2371       }
2373       // Fix keyCode quirks in all browsers.
2374       if (keyCode_ in pwlib.dom.keyCodes_fixes) {
2375         keyCode_ = pwlib.dom.keyCodes_fixes[keyCode_];
2376       }
2378       key_ = pwlib.dom.keyCodes[keyCode_] || String.fromCharCode(keyCode_);
2380       return;
2381     }
2383     // Try to use ev.keyIdentifier. This is only available in Webkit and 
2384     // Konqueror 4, each having some quirks. Sometimes the property is needed, 
2385     // because keyCode/which are not always available.
2387     var key = null,
2388         keyCode = null,
2389         id = ev.keyIdentifier;
2391     if (!id || id === 'Unidentified' || id === 'U+0000') {
2392       return;
2393     }
2395     if (id.substr(0, 2) === 'U+') {
2396       // Webkit gives character codes using the 'U+XXXX' notation, as per spec.
2397       keyCode = parseInt(id.substr(2), 16);
2399     } else if (id.length === 1) {
2400       // Konqueror 4 implements keyIdentifier, and they provide the Unicode 
2401       // character directly, instead of using the 'U+XXXX' notation.
2402       keyCode = id.charCodeAt(0);
2403       key = id;
2405     } else {
2406       /*
2407        * Common keyIdentifiers like 'PageDown' are used as they are.
2408        * We determine the common keyCode used by Web browsers, from the 
2409        * pwlib.dom.keyNames object.
2410        */
2411       keyCode_ = pwlib.dom.keyNames[id] || null;
2412       key_ = id;
2414       return;
2415     }
2417     // Some keyIdentifiers like 'U+007F' (127: Delete) need to become key names.
2418     if (keyCode in pwlib.dom.keyCodes && (keyCode <= 32 || keyCode == 127 || 
2419           keyCode == 144)) {
2420       key_ = pwlib.dom.keyCodes[keyCode];
2421     } else {
2422       if (!key) {
2423         key = String.fromCharCode(keyCode);
2424       }
2426       // Konqueror gives lower-case chars
2427       key_ = key.toUpperCase();
2428       if (key !== key_) {
2429         keyCode = key_.charCodeAt(0);
2430       }
2431     }
2433     // Correct the keyCode, make sure it's a common keyCode, not the Unicode 
2434     // decimal representation of the character.
2435     if (key_ === 'Delete' || key_.length === 1 && key_ in pwlib.dom.keyNames) {
2436       keyCode = pwlib.dom.keyNames[key_];
2437     }
2439     keyCode_ = keyCode;
2440   };
2442   /**
2443    * Determine the character and the character code for the current DOM Event 
2444    * object. This function updates the <var>charCode_</var> and the 
2445    * <var>char_</var> variables to hold the result.
2446    *
2447    * @private
2448    * @param {Event} ev The DOM Event object.
2449    */
2450   function findCharCode (ev) {
2451     charCode_ = null;
2452     char_ = null;
2454     // Webkit and Gecko implement ev.charCode.
2455     if (ev.charCode) {
2456       charCode_ = ev.charCode;
2457       char_ = String.fromCharCode(ev.charCode);
2459       return;
2460     }
2462     // Try the keyCode mess.
2463     if (ev.keyCode || ev.which) {
2464       var keyCode = ev.keyCode || ev.which;
2466       var force = false;
2468       // We accept some keyCodes.
2469       switch (keyCode) {
2470         case pwlib.dom.keyNames.Tab:
2471         case pwlib.dom.keyNames.Enter:
2472         case pwlib.dom.keyNames.Space:
2473           force = true;
2474       }
2476       // Do not consider the keyCode a character code, if during the keydown 
2477       // event it was determined the key does not generate a character, unless 
2478       // it's Tab, Enter or Space.
2479       if (!force && key_ && key_.length !== 1) {
2480         return;
2481       }
2483       // If the keypress event at hand is synthetically dispatched by keydown, 
2484       // then special treatment is needed. This happens only in Webkit and MSIE.
2485       if (ev.type_ === 'keydown') {
2486         var key = pwlib.dom.keyCodes[keyCode];
2487         // Check if the keyCode points to a single character.
2488         // If it does, use it.
2489         if (key && key.length === 1) {
2490           charCode_ = key.charCodeAt(0); // keyCodes != charCodes
2491           char_ = key;
2492         }
2493       } else if (keyCode >= 32 || force) {
2494         // For normal keypress events, we are done.
2495         charCode_ = keyCode;
2496         char_ = String.fromCharCode(keyCode);
2497       }
2499       if (charCode_) {
2500         return;
2501       }
2502     }
2504     /*
2505      * Webkit and Konqueror do not provide a keyIdentifier in the keypress 
2506      * event, as per spec. However, in the unlikely case when the keyCode is 
2507      * missing, and the keyIdentifier is available, we use it.
2508      *
2509      * This property might be used when a synthetic keypress event is generated 
2510      * by the keydown event, and keyCode/charCode/which are all not available.
2511      */
2513     var c = null,
2514         charCode = null,
2515         id = ev.keyIdentifier;
2517     if (id && id !== 'Unidentified' && id !== 'U+0000' &&
2518         (id.substr(0, 2) === 'U+' || id.length === 1)) {
2520       // Characters in Konqueror...
2521       if (id.length === 1) {
2522         charCode = id.charCodeAt(0);
2523         c = id;
2525       } else {
2526         // Webkit uses the 'U+XXXX' notation as per spec.
2527         charCode = parseInt(id.substr(2), 16);
2528       }
2530       if (charCode == pwlib.dom.keyNames.Tab ||
2531           charCode == pwlib.dom.keyNames.Enter ||
2532           charCode >= 32 && charCode != 127 &&
2533           charCode != pwlib.dom.keyNames.NumLock) {
2535         charCode_ = charCode;
2536         char_ = c || String.fromCharCode(charCode);
2538         return;
2539       }
2540     }
2542     // Try to use the key determined from the previous keydown event, if it 
2543     // holds a character.
2544     if (key_ && key_.length === 1) {
2545       charCode_ = key_.charCodeAt(0);
2546       char_ = key_;
2547     }
2548   };
2550   this.attach();
2553 // Check out the libmacrame project: http://code.google.com/p/libmacrame.
2555 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
2558  * Copyright (C) 2008, 2009 Mihai Şucan
2560  * This file is part of PaintWeb.
2562  * PaintWeb is free software: you can redistribute it and/or modify
2563  * it under the terms of the GNU General Public License as published by
2564  * the Free Software Foundation, either version 3 of the License, or
2565  * (at your option) any later version.
2567  * PaintWeb is distributed in the hope that it will be useful,
2568  * but WITHOUT ANY WARRANTY; without even the implied warranty of
2569  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
2570  * GNU General Public License for more details.
2572  * You should have received a copy of the GNU General Public License
2573  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
2575  * $URL: http://code.google.com/p/paintweb $
2576  * $Date: 2009-07-02 16:07:14 +0300 $
2577  */
2580  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
2581  * @fileOverview Holds the selection tool implementation.
2582  */
2585  * @class The selection tool.
2587  * @param {PaintWeb} app Reference to the main paint application object.
2588  */
2589 pwlib.tools.selection = function (app) {
2590   var _self         = this,
2591       appEvent      = pwlib.appEvent,
2592       bufferContext = app.buffer.context,
2593       clearInterval = app.win.clearInterval,
2594       config        = app.config.selection,
2595       gui           = app.gui,
2596       image         = app.image,
2597       lang          = app.lang,
2598       layerCanvas   = app.layer.canvas,
2599       layerContext  = app.layer.context,
2600       marqueeStyle  = null,
2601       MathAbs       = Math.abs,
2602       MathMin       = Math.min,
2603       MathRound     = Math.round,
2604       mouse         = app.mouse,
2605       setInterval   = app.win.setInterval,
2606       snapXY        = app.toolSnapXY;
2608   /**
2609    * The interval ID used for invoking the drawing operation every few 
2610    * milliseconds.
2611    *
2612    * @private
2613    * @see PaintWeb.config.toolDrawDelay
2614    */
2615   var timer = null;
2617   /**
2618    * Tells if the drawing canvas needs to be updated or not.
2619    *
2620    * @private
2621    * @type Boolean
2622    * @default false
2623    */
2624   var needsRedraw = false;
2626   /**
2627    * The selection has been dropped, and the mouse button is down. The user has 
2628    * two choices: he releases the mouse button, thus the selection is dropped 
2629    * and the tool switches to STATE_NONE, or he moves the mouse in order to 
2630    * start a new selection (STATE_DRAWING).
2631    * @constant
2632    */
2633   this.STATE_PENDING = -1;
2635   /**
2636    * No selection is available.
2637    * @constant
2638    */
2639   this.STATE_NONE = 0;
2641   /**
2642    * The user is drawing a selection.
2643    * @constant
2644    */
2645   this.STATE_DRAWING = 1;
2647   /**
2648    * The selection rectangle is available.
2649    * @constant
2650    */
2651   this.STATE_SELECTED = 2;
2653   /**
2654    * The user is dragging/moving the selection rectangle.
2655    * @constant
2656    */
2657   this.STATE_DRAGGING = 3;
2659   /**
2660    * The user is resizing the selection rectangle.
2661    * @constant
2662    */
2663   this.STATE_RESIZING = 4;
2665   /**
2666    * Selection state. Known states:
2667    *
2668    * <ul>
2669    *   <li>{@link pwlib.tools.selection#STATE_PENDING} - Selection dropped after 
2670    *   the <code>mousedown</code> event is fired. The script can switch to 
2671    *   STATE_DRAWING if the mouse moves, or to STATE_NONE if it does not 
2672    *   (allowing the user to drop the selection).
2673    *
2674    *   <li>{@link pwlib.tools.selection#STATE_NONE} - No selection is available.
2675    *
2676    *   <li>{@link pwlib.tools.selection#STATE_DRAWING} - The user is drawing the 
2677    *   selection rectangle.
2678    *
2679    *   <li>{@link pwlib.tools.selection#STATE_SELECTED} - The selection 
2680    *   rectangle is available.
2681    *
2682    *   <li>{@link pwlib.tools.selection#STATE_DRAGGING} - The user is 
2683    *   dragging/moving the current selection.
2684    *
2685    *   <li>{@link pwlib.tools.selection#STATE_RESIZING} - The user is resizing 
2686    *   the current selection.
2687    * </ul>
2688    *
2689    * @type Number
2690    * @default STATE_NONE
2691    */
2692   this.state = this.STATE_NONE;
2694   /**
2695    * Holds the starting point on the <var>x</var> axis of the image, for any 
2696    * ongoing operation.
2697    *
2698    * @private
2699    * @type Number
2700    */
2701   var x0 = 0;
2703   /**
2704    * Holds the starting point on the <var>y</var> axis of the image, for the any  
2705    * ongoing operation.
2706    *
2707    * @private
2708    * @type Number
2709    */
2710   var y0 = 0;
2712   /**
2713    * Holds selection information and image.
2714    * @type Object
2715    */
2716   this.selection = {
2717     /**
2718      * Selection start point, on the <var>x</var> axis.
2719      * @type Number
2720      */
2721     x: 0,
2723     /**
2724      * Selection start point, on the <var>y</var> axis.
2725      * @type Number
2726      */
2727     y: 0,
2729     /**
2730      * Selection width.
2731      * @type Number
2732      */
2733     width: 0,
2735     /**
2736      * Selection height.
2737      * @type Number
2738      */
2739     height: 0,
2741     /**
2742      * Selection original width. The user can make a selection rectangle of 
2743      * a given width and height, but after that he/she can resize the selection.
2744      * @type Number
2745      */
2746     widthOriginal: 0,
2748     /**
2749      * Selection original height. The user can make a selection rectangle of 
2750      * a given width and height, but after that he/she can resize the selection.
2751      * @type Number
2752      */
2753     heightOriginal: 0,
2755     /**
2756      * Tells if the selected ImageData has been cut out or not from the 
2757      * layerContext.
2758      *
2759      * @type Boolean
2760      * @default false
2761      */
2762     layerCleared: false,
2764     /**
2765      * Selection marquee/border element.
2766      * @type HTMLElement
2767      */
2768     marquee: null,
2770     /**
2771      * Selection buffer context which holds the selected pixels.
2772      * @type CanvasRenderingContext2D
2773      */
2774     context: null,
2776     /**
2777      * Selection buffer canvas which holds the selected pixels.
2778      * @type HTMLCanvasElement
2779      */
2780     canvas: null
2781   };
2783   /**
2784    * The area type under the current mouse location.
2785    * 
2786    * <p>When the selection is available the mouse location can be on top/inside 
2787    * the selection rectangle, on the border of the selection, or outside the 
2788    * selection.
2789    *
2790    * <p>Possible values: 'in', 'out', 'border'.
2791    *
2792    * @private
2793    * @type String
2794    * @default 'out'
2795    */
2796   var mouseArea = 'out';
2798   /**
2799    * The resize type. If the mouse is on top of the selection border, then the 
2800    * selection can be resized. The direction of the resize operation is 
2801    * determined by the location of the mouse.
2802    * 
2803    * <p>While the user resizes the selection this variable can hold the 
2804    * following values: 'n' (North), 'ne' (North-East), 'e' (East), 'se' 
2805    * (South-East), 's' (South), 'sw' (South-West), 'w' (West), 'nw' 
2806    * (North-West).
2807    *
2808    * @private
2809    * @type String
2810    * @default null
2811    */
2812   var mouseResize = null;
2814   // shorthands / private variables
2815   var sel = this.selection,
2816       borderDouble = config.borderWidth * 2,
2817       ev_canvasSizeChangeId = null,
2818       ev_configChangeId = null,
2819       ctrlKey = false,
2820       shiftKey = false;
2822   /**
2823    * The last selection rectangle that was drawn. This is used by the selection 
2824    * drawing functions.
2825    *
2826    * @private
2827    * @type Object
2828    */
2829   // We avoid retrieving the mouse coordinates during the mouseup event, due to 
2830   // the Opera bug DSK-232264.
2831   var lastSel = null;
2833   /**
2834    * The tool preactivation code. This function prepares the selection canvas 
2835    * element.
2836    *
2837    * @returns {Boolean} True if the activation did not fail, or false otherwise.  
2838    * If false is returned, the selection tool cannot be activated.
2839    */
2840   this.preActivate = function () {
2841     if (!('canvasContainer' in gui.elems)) {
2842       alert(lang.errorToolActivate);
2843       return false;
2844     }
2846     // The selection image buffer.
2847     sel.canvas = app.doc.createElement('canvas');
2848     if (!sel.canvas) {
2849       alert(lang.errorToolActivate);
2850       return false;
2851     }
2853     sel.canvas.width  = 5;
2854     sel.canvas.height = 5;
2856     sel.context = sel.canvas.getContext('2d');
2857     if (!sel.context) {
2858       alert(lang.errorToolActivate);
2859       return false;
2860     }
2862     sel.marquee = app.doc.createElement('div');
2863     if (!sel.marquee) {
2864       alert(lang.errorToolActivate);
2865       return false;
2866     }
2867     sel.marquee.className = gui.classPrefix + 'selectionMarquee';
2868     marqueeStyle = sel.marquee.style;
2870     return true;
2871   };
2873   /**
2874    * The tool activation code. This method sets-up multiple event listeners for 
2875    * several target objects.
2876    */
2877   this.activate = function () {
2878     // Older browsers do not support get/putImageData, thus non-transparent 
2879     // selections cannot be used.
2880     if (!layerContext.putImageData || !layerContext.getImageData) {
2881       config.transparent = true;
2882     }
2884     marqueeHide();
2886     marqueeStyle.borderWidth = config.borderWidth + 'px';
2887     sel.marquee.addEventListener('mousedown', marqueeMousedown, false);
2888     sel.marquee.addEventListener('mousemove', marqueeMousemove, false);
2889     sel.marquee.addEventListener('mouseup',   marqueeMouseup,   false);
2891     gui.elems.canvasContainer.appendChild(sel.marquee);
2893     // Disable the Canvas shadow.
2894     app.shadowDisallow();
2896     // Application event listeners.
2897     ev_canvasSizeChangeId = app.events.add('canvasSizeChange', 
2898         ev_canvasSizeChange);
2899     ev_configChangeId = app.events.add('configChange', ev_configChange);
2901     // Register selection-related commands
2902     app.commandRegister('selectionCrop',   _self.selectionCrop);
2903     app.commandRegister('selectionDelete', _self.selectionDelete);
2904     app.commandRegister('selectionFill',   _self.selectionFill);
2906     if (!timer) {
2907       timer = setInterval(timerFn, app.config.toolDrawDelay);
2908     }
2910     return true;
2911   };
2913   /**
2914    * The tool deactivation code. This removes all event listeners and cleans up 
2915    * the document.
2916    */
2917   this.deactivate = function () {
2918     if (timer) {
2919       clearInterval(timer);
2920       timer = null;
2921     }
2923     _self.selectionMerge();
2925     sel.marquee.removeEventListener('mousedown', marqueeMousedown, false);
2926     sel.marquee.removeEventListener('mousemove', marqueeMousemove, false);
2927     sel.marquee.removeEventListener('mouseup',   marqueeMouseup,   false);
2929     marqueeStyle = null;
2930     gui.elems.canvasContainer.removeChild(sel.marquee);
2932     delete sel.context, sel.canvas, sel.marquee;
2934     // Re-enable canvas shadow.
2935     app.shadowAllow();
2937     // Remove the application event listeners.
2938     if (ev_canvasSizeChangeId) {
2939       app.events.remove('canvasSizeChange', ev_canvasSizeChangeId);
2940     }
2941     if (ev_configChangeId) {
2942       app.events.remove('configChange', ev_configChangeId);
2943     }
2945     // Unregister selection-related commands
2946     app.commandUnregister('selectionCrop');
2947     app.commandUnregister('selectionDelete');
2948     app.commandUnregister('selectionFill');
2950     return true;
2951   };
2953   /**
2954    * The <code>mousedown</code> event handler. Depending on the mouse location, 
2955    * this method does initiate different selection operations: drawing, 
2956    * dropping, dragging or resizing.
2957    *
2958    * <p>Hold the <kbd>Control</kbd> key down to temporarily toggle the 
2959    * transformation mode.
2960    *
2961    * @param {Event} ev The DOM Event object.
2962    */
2963   this.mousedown = function (ev) {
2964     if (_self.state !== _self.STATE_NONE &&
2965         _self.state !== _self.STATE_SELECTED) {
2966       return false;
2967     }
2969     // Update the current mouse position, this is used as the start position for most of the operations.
2970     x0 = mouse.x;
2971     y0 = mouse.y;
2973     shiftKey = ev.shiftKey;
2974     ctrlKey = ev.ctrlKey;
2975     lastSel = null;
2977     // No selection is available, then start drawing a selection.
2978     if (_self.state === _self.STATE_NONE) {
2979       _self.state = _self.STATE_DRAWING;
2980       marqueeStyle.display = '';
2981       gui.statusShow('selectionDraw');
2983       return true;
2984     }
2986     // STATE_SELECTED: selection available.
2987     mouseAreaUpdate();
2989     /*
2990      * Check if the user clicked outside the selection: drop the selection, 
2991      * switch to STATE_PENDING, clear the image buffer and put the current 
2992      * selection buffer in the image layer.
2993      *
2994      * If the user moves the mouse without taking the finger off the mouse 
2995      * button, then a new selection rectangle will start to be drawn: the script 
2996      * will switch to STATE_DRAWING.
2997      *
2998      * If the user simply takes the finger off the mouse button (mouseup), then 
2999      * the script will switch to STATE_NONE (no selection available).
3000      */
3001     switch (mouseArea) {
3002       case 'out':
3003         _self.state = _self.STATE_PENDING;
3004         marqueeHide();
3005         gui.statusShow('selectionActive');
3006         selectionMergeStrict();
3008         return true;
3010       case 'in':
3011         // The mouse area: 'in' for drag.
3012         _self.state = _self.STATE_DRAGGING;
3013         gui.statusShow('selectionDrag');
3014         break;
3016       case 'border':
3017         // 'border' for resize (the user is clicking on the borders).
3018         _self.state = _self.STATE_RESIZING;
3019         gui.statusShow('selectionResize');
3020     }
3022     // Temporarily toggle the transformation mode if the user holds the Control 
3023     // key down.
3024     if (ev.ctrlKey) {
3025       config.transform = !config.transform;
3026     }
3028     // If there's any ImageData currently in memory, which was "cut" out from 
3029     // the current layer, then put it back on the layer. This needs to be done 
3030     // only when the selection.transform mode is not active - that's when the 
3031     // drag/resize operation only changes the selection, not the pixels 
3032     // themselves.
3033     if (sel.layerCleared && !config.transform) {
3034       selectionMergeStrict();
3036     } else if (!sel.layerCleared && config.transform) {
3037       // When the user starts dragging/resizing the ImageData we must cut out 
3038       // the current selection from the image layer.
3039       selectionBufferInit();
3040     }
3042     return true;
3043   };
3045   /**
3046    * The <code>mousemove</code> event handler.
3047    *
3048    * @param {Event} ev The DOM Event object.
3049    */
3050   this.mousemove = function (ev) {
3051     shiftKey = ev.shiftKey;
3052     needsRedraw = true;
3053   };
3055   /**
3056    * The timer function. When the mouse button is down, this method performs the 
3057    * dragging/resizing operation. When the mouse button is not down, this method 
3058    * simply tracks the mouse location for the purpose of determining the area 
3059    * being pointed at: the selection, the borders, or if the mouse is outside 
3060    * the selection.
3061    * @private
3062    */
3063   function timerFn () {
3064     if (!needsRedraw) {
3065       return;
3066     }
3068     switch (_self.state) {
3069       case _self.STATE_PENDING:
3070         // selection dropped, switch to draw selection
3071         _self.state = _self.STATE_DRAWING;
3072         marqueeStyle.display = '';
3073         gui.statusShow('selectionDraw');
3075       case _self.STATE_DRAWING:
3076         selectionDraw();
3077         break;
3079       case _self.STATE_SELECTED:
3080         mouseAreaUpdate();
3081         break;
3083       case _self.STATE_DRAGGING:
3084         selectionDrag();
3085         break;
3087       case _self.STATE_RESIZING:
3088         selectionResize();
3089     }
3091     needsRedraw = false;
3092   };
3094   /**
3095    * The <code>mouseup</code> event handler. This method ends any selection 
3096    * operation.
3097    *
3098    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
3099    * application event when the selection state is changed or when the selection 
3100    * size/location is updated.
3101    *
3102    * @param {Event} ev The DOM Event object.
3103    */
3104   this.mouseup = function (ev) {
3105     // Allow click+mousemove+click, not only mousedown+move+up
3106     if (_self.state !== _self.STATE_PENDING &&
3107         mouse.x === x0 && mouse.y === y0) {
3108       return true;
3109     }
3111     needsRedraw = false;
3113     shiftKey = ev.shiftKey;
3114     if (ctrlKey) {
3115       config.transform = !config.transform;
3116     }
3118     if (_self.state === _self.STATE_PENDING) {
3119       // Selection dropped? If yes, switch to the no selection state.
3120       _self.state = _self.STATE_NONE;
3121       app.events.dispatch(new appEvent.selectionChange(_self.state));
3123       return true;
3125     } else if (!lastSel) {
3126       _self.state = _self.STATE_NONE;
3127       marqueeHide();
3128       gui.statusShow('selectionActive');
3129       app.events.dispatch(new appEvent.selectionChange(_self.state));
3131       return true;
3132     }
3134     sel.x = lastSel.x;
3135     sel.y = lastSel.y;
3137     if ('width' in lastSel) {
3138       sel.width  = lastSel.width;
3139       sel.height = lastSel.height;
3140     }
3142     _self.state = _self.STATE_SELECTED;
3144     app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 
3145           sel.width, sel.height));
3147     gui.statusShow('selectionAvailable');
3149     return true;
3150   };
3152   /**
3153    * The <code>mousedown</code> event handler for the selection marquee element.
3154    *
3155    * @private
3156    * @param {Event} ev The DOM Event object.
3157    */
3158   function marqueeMousedown (ev) {
3159     if (mouse.buttonDown) {
3160       return;
3161     }
3162     mouse.buttonDown = true;
3164     ev.preventDefault();
3166     _self.mousedown(ev);
3167   };
3169   /**
3170    * The <code>mousemove</code> event handler for the selection marquee element.
3171    *
3172    * @private
3173    * @param {Event} ev The DOM Event object.
3174    */
3175   function marqueeMousemove (ev) {
3176     if ('layerX' in ev) {
3177       mouse.x = MathRound((this.offsetLeft + ev.layerX) / image.canvasScale);
3178       mouse.y = MathRound((this.offsetTop  + ev.layerY) / image.canvasScale);
3179     } else if ('offsetX' in ev) {
3180       mouse.x = MathRound((this.offsetLeft + ev.offsetX) / image.canvasScale);
3181       mouse.y = MathRound((this.offsetTop  + ev.offsetY) / image.canvasScale);
3182     }
3184     shiftKey = ev.shiftKey;
3185     needsRedraw = true;
3186   };
3188   /**
3189    * The <code>mouseup</code> event handler for the selection marquee element.
3190    *
3191    * @private
3192    * @param {Event} ev The DOM Event object.
3193    */
3194   function marqueeMouseup (ev) {
3195     if (!mouse.buttonDown) {
3196       return;
3197     }
3198     mouse.buttonDown = false;
3200     ev.preventDefault();
3202     _self.mouseup(ev);
3203   };
3205   /**
3206    * Hide the selection marquee element.
3207    * @private
3208    */
3209   function marqueeHide () {
3210     marqueeStyle.display = 'none';
3211     marqueeStyle.top     = '-' + (borderDouble + 50) + 'px';
3212     marqueeStyle.left    = '-' + (borderDouble + 50) + 'px';
3213     marqueeStyle.width   = '1px';
3214     marqueeStyle.height  = '1px';
3215     marqueeStyle.cursor  = '';
3216   };
3218   /**
3219    * Perform the selection rectangle drawing operation.
3220    *
3221    * @private
3222    */
3223   function selectionDraw () {
3224     var x = MathMin(mouse.x,  x0),
3225         y = MathMin(mouse.y,  y0),
3226         w = MathAbs(mouse.x - x0),
3227         h = MathAbs(mouse.y - y0);
3229     // Constrain the shape to a square.
3230     if (shiftKey) {
3231       if (w > h) {
3232         if (y === mouse.y) {
3233           y -= w-h;
3234         }
3235         h = w;
3236       } else {
3237         if (x === mouse.x) {
3238           x -= h-w;
3239         }
3240         w = h;
3241       }
3242     }
3244     var mw = w * image.canvasScale - borderDouble,
3245         mh = h * image.canvasScale - borderDouble;
3247     if (mw < 1 || mh < 1) {
3248       lastSel = null;
3249       return;
3250     }
3252     marqueeStyle.top    = (y * image.canvasScale) + 'px';
3253     marqueeStyle.left   = (x * image.canvasScale) + 'px';
3254     marqueeStyle.width  = mw + 'px';
3255     marqueeStyle.height = mh + 'px';
3257     lastSel = {'x': x, 'y': y, 'width': w, 'height': h};
3258   };
3260   /**
3261    * Perform the selection drag operation.
3262    *
3263    * @private
3264    *
3265    * @returns {false|Array} False is returned if the selection is too small, 
3266    * otherwise an array of two elements is returned. The array holds the 
3267    * selection coordinates, x and y.
3268    */
3269   function selectionDrag () {
3270     // Snapping on the X/Y axis
3271     if (shiftKey) {
3272       snapXY(x0, y0);
3273     }
3275     var x = sel.x + mouse.x - x0,
3276         y = sel.y + mouse.y - y0;
3278     // Dragging the ImageData
3279     if (config.transform) {
3280       bufferContext.clearRect(0, 0, image.width, image.height);
3282       if (!config.transparent) {
3283         bufferContext.fillRect(x, y, sel.width, sel.height);
3284       }
3286       // Parameters:
3287       // source image, dest x, dest y, dest width, dest height
3288       bufferContext.drawImage(sel.canvas, x, y, sel.width, sel.height);
3289     }
3291     marqueeStyle.top  = (y * image.canvasScale) + 'px';
3292     marqueeStyle.left = (x * image.canvasScale) + 'px';
3294     lastSel = {'x': x, 'y': y};
3295   };
3297   /**
3298    * Perform the selection resize operation.
3299    *
3300    * @private
3301    *
3302    * @returns {false|Array} False is returned if the selection is too small, 
3303    * otherwise an array of four elements is returned. The array holds the 
3304    * selection information: x, y, width and height.
3305    */
3306   function selectionResize () {
3307     var diffx = mouse.x - x0,
3308         diffy = mouse.y - y0,
3309         x     = sel.x,
3310         y     = sel.y,
3311         w     = sel.width,
3312         h     = sel.height;
3314     switch (mouseResize) {
3315       case 'nw':
3316         x += diffx;
3317         y += diffy;
3318         w -= diffx;
3319         h -= diffy;
3320         break;
3321       case 'n':
3322         y += diffy;
3323         h -= diffy;
3324         break;
3325       case 'ne':
3326         y += diffy;
3327         w += diffx;
3328         h -= diffy;
3329         break;
3330       case 'e':
3331         w += diffx;
3332         break;
3333       case 'se':
3334         w += diffx;
3335         h += diffy;
3336         break;
3337       case 's':
3338         h += diffy;
3339         break;
3340       case 'sw':
3341         x += diffx;
3342         w -= diffx;
3343         h += diffy;
3344         break;
3345       case 'w':
3346         x += diffx;
3347         w -= diffx;
3348         break;
3349       default:
3350         lastSel = null;
3351         return;
3352     }
3354     if (!w || !h) {
3355       lastSel = null;
3356       return;
3357     }
3359     // Constrain the rectangle to have the same aspect ratio as the initial 
3360     // rectangle.
3361     if (shiftKey) {
3362       var p  = sel.width / sel.height,
3363           w2 = w,
3364           h2 = h;
3366       switch (mouseResize.charAt(0)) {
3367         case 'n':
3368         case 's':
3369           w2 = MathRound(h*p);
3370           break;
3371         default:
3372           h2 = MathRound(w/p);
3373       }
3375       switch (mouseResize) {
3376         case 'nw':
3377         case 'sw':
3378           x -= w2 - w;
3379           y -= h2 - h;
3380       }
3382       w = w2;
3383       h = h2;
3384     }
3386     if (w < 0) {
3387       x += w;
3388       w *= -1;
3389     }
3390     if (h < 0) {
3391       y += h;
3392       h *= -1;
3393     }
3395     var mw   = w * image.canvasScale - borderDouble,
3396         mh   = h * image.canvasScale - borderDouble;
3398     if (mw < 1 || mh < 1) {
3399       lastSel = null;
3400       return;
3401     }
3403     // Resizing the ImageData
3404     if (config.transform) {
3405       bufferContext.clearRect(0, 0, image.width, image.height);
3407       if (!config.transparent) {
3408         bufferContext.fillRect(x, y, w, h);
3409       }
3411       // Parameters:
3412       // source image, dest x, dest y, dest width, dest height
3413       bufferContext.drawImage(sel.canvas, x, y, w, h);
3414     }
3416     marqueeStyle.top    = (y * image.canvasScale) + 'px';
3417     marqueeStyle.left   = (x * image.canvasScale) + 'px';
3418     marqueeStyle.width  = mw + 'px';
3419     marqueeStyle.height = mh + 'px';
3421     lastSel = {'x': x, 'y': y, 'width': w, 'height': h};
3422   };
3424   /**
3425    * Determine the are where the mouse is located: if it is inside or outside of 
3426    * the selection rectangle, or on the selection border.
3427    * @private
3428    */
3429   function mouseAreaUpdate () {
3430     var border = config.borderWidth / image.canvasScale,
3431         cursor = '',
3432         x1_out = sel.x + sel.width,
3433         y1_out = sel.y + sel.height,
3434         x1_in  = x1_out - border,
3435         y1_in  = y1_out - border,
3436         x0_out = sel.x,
3437         y0_out = sel.y,
3438         x0_in  = sel.x + border,
3439         y0_in  = sel.y + border;
3441     mouseArea = 'out';
3443     // Inside the rectangle
3444     if (mouse.x < x1_in && mouse.y < y1_in &&
3445         mouse.x > x0_in && mouse.y > y0_in) {
3446       cursor = 'move';
3447       mouseArea = 'in';
3449     } else {
3450       // On one of the borders (north/south)
3451       if (mouse.x >= x0_out && mouse.x <= x1_out &&
3452           mouse.y >= y0_out && mouse.y <= y0_in) {
3453         cursor = 'n';
3455       } else if (mouse.x >= x0_out && mouse.x <= x1_out &&
3456                  mouse.y >= y1_in  && mouse.y <= y1_out) {
3457         cursor = 's';
3458       }
3460       // West/east
3461       if (mouse.y >= y0_out && mouse.y <= y1_out &&
3462           mouse.x >= x0_out && mouse.x <= x0_in) {
3463         cursor += 'w';
3465       } else if (mouse.y >= y0_out && mouse.y <= y1_out &&
3466                  mouse.x >= x1_in  && mouse.x <= x1_out) {
3467         cursor += 'e';
3468       }
3470       if (cursor !== '') {
3471         mouseResize = cursor;
3472         cursor += '-resize';
3473         mouseArea = 'border';
3474       }
3475     }
3477     // Due to bug 126457 Opera will not automatically update the cursor, 
3478     // therefore they will not see any visual feedback.
3479     if (cursor !== marqueeStyle.cursor) {
3480       marqueeStyle.cursor = cursor;
3481     }
3482   };
3484   /**
3485    * The <code>canvasSizeChange</code> application event handler. This method 
3486    * makes sure the selection size stays in sync.
3487    *
3488    * @private
3489    * @param {pwlib.appEvent.canvasSizeChange} ev The application event object.
3490    */
3491   function ev_canvasSizeChange (ev) {
3492     if (_self.state !== _self.STATE_SELECTED) {
3493       return;
3494     }
3496     marqueeStyle.top    = (sel.y      * ev.scale) + 'px';
3497     marqueeStyle.left   = (sel.x      * ev.scale) + 'px';
3498     marqueeStyle.width  = (sel.width  * ev.scale - borderDouble) + 'px';
3499     marqueeStyle.height = (sel.height * ev.scale - borderDouble) + 'px';
3500   };
3502   /**
3503    * The <code>configChange</code> application event handler. This method makes 
3504    * sure that changes to the selection transparency configuration option are 
3505    * applied.
3506    *
3507    * @private
3508    * @param {pwlib.appEvent.configChange} ev The application event object.
3509    */
3510   function ev_configChange (ev) {
3511     // Continue only if the selection rectangle is available.
3512     if (ev.group !== 'selection' || ev.config !== 'transparent' ||
3513         !config.transform || _self.state !== _self.STATE_SELECTED) {
3514       return;
3515     }
3517     if (!sel.layerCleared) {
3518       selectionBufferInit();
3519     }
3521     bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3523     if (!ev.value) {
3524       bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3525     }
3527     // Draw the updated selection
3528     bufferContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height);
3529   };
3531   /**
3532    * Initialize the selection buffer, when the user starts dragging or resizing 
3533    * the selected pixels.
3534    *
3535    * @private
3536    */
3537   function selectionBufferInit () {
3538     var x = sel.x,
3539         y = sel.y,
3540         w = sel.width,
3541         h = sel.height,
3542         sumX = sel.x + sel.width,
3543         sumY = sel.y + sel.height,
3544         dx = 0, dy = 0;
3546     sel.widthOriginal  = w;
3547     sel.heightOriginal = h;
3549     if (x < 0) {
3550       w += x;
3551       dx -= x;
3552       x = 0;
3553     }
3554     if (y < 0) {
3555       h += y;
3556       dy -= y;
3557       y = 0;
3558     }
3560     if (sumX > image.width) {
3561       w = image.width - sel.x;
3562     }
3563     if (sumY > image.height) {
3564       h = image.height - sel.y;
3565     }
3567     if (!config.transparent) {
3568       bufferContext.fillRect(x, y, w, h);
3569     }
3571     // Parameters:
3572     // source image, src x, src y, src w, src h, dest x, dest y, dest w, dest h
3573     bufferContext.drawImage(layerCanvas, x, y, w, h, x, y, w, h);
3575     sel.canvas.width  = sel.widthOriginal;
3576     sel.canvas.height = sel.heightOriginal;
3578     // Also put the selected pixels into the selection buffer.
3579     sel.context.drawImage(layerCanvas, x, y, w, h, dx, dy, w, h);
3581     // Clear the selected pixels from the image
3582     layerContext.clearRect(x, y, w, h);
3583     sel.layerCleared = true;
3585     app.historyAdd();
3586   };
3588   /**
3589    * Perform the selection buffer merge onto the current image layer.
3590    * @private
3591    */
3592   function selectionMergeStrict () {
3593     if (!sel.layerCleared) {
3594       return;
3595     }
3597     if (!config.transparent) {
3598       layerContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3599     }
3601     layerContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height);
3602     bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3604     sel.layerCleared  = false;
3605     sel.canvas.width  = 5;
3606     sel.canvas.height = 5;
3608     app.historyAdd();
3609   };
3611   /**
3612    * Merge the selection buffer onto the current image layer.
3613    *
3614    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
3615    * application event.
3616    *
3617    * @returns {Boolean} True if the operation was successful, or false if not.
3618    */
3619   this.selectionMerge = function () {
3620     if (_self.state !== _self.STATE_SELECTED) {
3621       return false;
3622     }
3624     selectionMergeStrict();
3626     _self.state = _self.STATE_NONE;
3627     marqueeHide();
3628     gui.statusShow('selectionActive');
3630     app.events.dispatch(new appEvent.selectionChange(_self.state));
3632     return true;
3633   };
3635   /**
3636    * Select all the entire image.
3637    *
3638    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
3639    * application event.
3640    *
3641    * @returns {Boolean} True if the operation was successful, or false if not.
3642    */
3643   this.selectAll = function () {
3644     if (_self.state !== _self.STATE_NONE && _self.state !== 
3645         _self.STATE_SELECTED) {
3646       return false;
3647     }
3649     if (_self.state === _self.STATE_SELECTED) {
3650       selectionMergeStrict();
3651     } else {
3652       _self.state = _self.STATE_SELECTED;
3653       marqueeStyle.display = '';
3654     }
3656     sel.x      = 0;
3657     sel.y      = 0;
3658     sel.width  = image.width;
3659     sel.height = image.height;
3661     marqueeStyle.top     = '0px';
3662     marqueeStyle.left    = '0px';
3663     marqueeStyle.width   = (sel.width*image.canvasScale  - borderDouble) + 'px';
3664     marqueeStyle.height  = (sel.height*image.canvasScale - borderDouble) + 'px';
3666     mouseAreaUpdate();
3668     app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 
3669           sel.width, sel.height));
3671     return true;
3672   };
3674   /**
3675    * Cut the selected pixels. The associated ImageData is stored in {@link 
3676    * PaintWeb#clipboard}.
3677    *
3678    * <p>This method dispatches two application events: {@link 
3679    * pwlib.appEvent.clipboardUpdate} and {@link pwlib.appEvent.selectionChange}.
3680    *
3681    * @returns {Boolean} True if the operation was successful, or false if not.
3682    */
3683   this.selectionCut = function () {
3684     if (!_self.selectionCopy()) {
3685       return false;
3686     }
3688     if (sel.layerCleared) {
3689       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3691       sel.canvas.width  = 5;
3692       sel.canvas.height = 5;
3693       sel.layerCleared = false;
3695     } else {
3696       layerContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3697       app.historyAdd();
3698     }
3700     _self.state = _self.STATE_NONE;
3701     marqueeHide();
3703     app.events.dispatch(new appEvent.selectionChange(_self.state));
3704     gui.statusShow('selectionActive');
3706     return true;
3707   };
3709   /**
3710    * Copy the selected pixels. The associated ImageData is stored in {@link 
3711    * PaintWeb#clipboard}.
3712    *
3713    * <p>This method dispatches the {@link pwlib.appEvent.clipboardUpdate} 
3714    * application event.
3715    *
3716    * @returns {Boolean} True if the operation was successful, or false if not.
3717    */
3718   this.selectionCopy = function () {
3719     if (_self.state !== _self.STATE_SELECTED) {
3720       return false;
3721     }
3723     if (!layerContext.getImageData || !layerContext.putImageData) {
3724       alert(lang.errorClipboardUnsupported);
3725       return false;
3726     }
3728     if (!sel.layerCleared) {
3729       var w    = sel.width,
3730           h    = sel.height,
3731           sumX = sel.width  + sel.x;
3732           sumY = sel.height + sel.y;
3734       if (sumX > image.width) {
3735         w = image.width - sel.x;
3736       }
3737       if (sumY > image.height) {
3738         h = image.height - sel.y;
3739       }
3741       try {
3742         app.clipboard = layerContext.getImageData(sel.x, sel.y, w, h);
3743       } catch (err) {
3744         alert(lang.failedSelectionCopy);
3745         return false;
3746       }
3748     } else {
3749       try {
3750         app.clipboard = sel.context.getImageData(0, 0, sel.widthOriginal, 
3751             sel.heightOriginal);
3752       } catch (err) {
3753         alert(lang.failedSelectionCopy);
3754         return false;
3755       }
3756     }
3758     app.events.dispatch(new appEvent.clipboardUpdate(app.clipboard));
3760     return true;
3761   };
3763   /**
3764    * Paste an image from the "clipboard". The {@link PaintWeb#clipboard} object 
3765    * must be an ImageData. This method will generate a new selection which will 
3766    * hold the pasted image.
3767    *
3768    * <p>The {@link pwlib.appEvent.selectionChange} application event is 
3769    * dispatched.
3770    *
3771    * <p>If the {@link PaintWeb.config.selection.transform} value is false, then 
3772    * it becomes true. The {@link pwlib.appEvent.configChange} application is 
3773    * then dispatched.
3774    *
3775    * @returns {Boolean} True if the operation was successful, or false if not.
3776    */
3777   this.clipboardPaste = function () {
3778     if (!app.clipboard || _self.state !== _self.STATE_NONE && _self.state !== 
3779         _self.STATE_SELECTED) {
3780       return false;
3781     }
3783     if (!layerContext.getImageData || !layerContext.putImageData) {
3784       alert(lang.errorClipboardUnsupported);
3785       return false;
3786     }
3788     // The default position for the pasted image is the top left corner of the 
3789     // visible area, taking into consideration the zoom level.
3790     var x = MathRound(gui.elems.viewport.scrollLeft / image.canvasScale),
3791         y = MathRound(gui.elems.viewport.scrollTop  / image.canvasScale),
3792         w = app.clipboard.width,
3793         h = app.clipboard.height;
3795     sel.canvas.width  = w;
3796     sel.canvas.height = h;
3797     sel.context.putImageData(app.clipboard, 0, 0);
3799     if (_self.state === _self.STATE_SELECTED) {
3800       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3801     } else {
3802       _self.state = _self.STATE_SELECTED;
3803     }
3805     if (!config.transparent) {
3806       bufferContext.fillRect(x, y, w, h);
3807     }
3808     bufferContext.drawImage(sel.canvas, x, y, w, h);
3810     sel.widthOriginal  = sel.width  = w;
3811     sel.heightOriginal = sel.height = h;
3812     sel.x = x;
3813     sel.y = y;
3814     sel.layerCleared = true;
3816     marqueeStyle.top     = (y * image.canvasScale) + 'px';
3817     marqueeStyle.left    = (x * image.canvasScale) + 'px';
3818     marqueeStyle.width   = (w * image.canvasScale - borderDouble) + 'px';
3819     marqueeStyle.height  = (h * image.canvasScale - borderDouble) + 'px';
3820     marqueeStyle.display = '';
3822     if (!config.transform) {
3823       config.transform = true;
3824       app.events.dispatch(new appEvent.configChange(true, false, 'transform', 
3825             'selection', config));
3826     }
3828     mouseAreaUpdate();
3830     app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 
3831           sel.width, sel.height));
3833     gui.statusShow('selectionAvailable');
3835     return true;
3836   };
3838   /**
3839    * Perform selection delete.
3840    *
3841    * <p>This method changes the {@link PaintWeb.config.selection.transform} 
3842    * value to false if the current selection has pixels that are currently being 
3843    * manipulated. In such cases, the {@link pwlib.appEvent.configChange} 
3844    * application event is also dispatched.
3845    *
3846    * @returns {Boolean} True if the operation was successful, or false if not.
3847    */
3848   this.selectionDelete = function () {
3849     // Delete the pixels from the image if they are not deleted already.
3850     if (_self.state !== _self.STATE_SELECTED) {
3851       return false;
3852     }
3854     if (!sel.layerCleared) {
3855       layerContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3856       app.historyAdd();
3858     } else {
3859       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3860       sel.layerCleared  = false;
3861       sel.canvas.width  = 5;
3862       sel.canvas.height = 5;
3864       if (config.transform) {
3865         config.transform = false;
3866         app.events.dispatch(new appEvent.configChange(false, true, 'transform', 
3867               'selection', config));
3868       }
3869     }
3871     return true;
3872   };
3874   /**
3875    * Drop the current selection.
3876    *
3877    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
3878    * application event.
3879    *
3880    * @returns {Boolean} True if the operation was successful, or false if not.
3881    */
3882   this.selectionDrop = function () {
3883     if (_self.state !== _self.STATE_SELECTED) {
3884       return false;
3885     }
3887     if (sel.layerCleared) {
3888       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
3889       sel.canvas.width  = 5;
3890       sel.canvas.height = 5;
3891       sel.layerCleared  = false;
3892     }
3894     _self.state = _self.STATE_NONE;
3896     marqueeHide();
3897     gui.statusShow('selectionActive');
3899     app.events.dispatch(new appEvent.selectionChange(_self.state));
3901     return true;
3902   };
3904   /**
3905    * Fill the available selection with the current 
3906    * <var>bufferContext.fillStyle</var>.
3907    *
3908    * @returns {Boolean} True if the operation was successful, or false if not.
3909    */
3910   this.selectionFill = function () {
3911     if (_self.state !== _self.STATE_SELECTED) {
3912       return false;
3913     }
3915     if (sel.layerCleared) {
3916       sel.context.fillStyle = bufferContext.fillStyle;
3917       sel.context.fillRect(0, 0, sel.widthOriginal, sel.heightOriginal);
3918       bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3920     } else {
3921       layerContext.fillStyle = bufferContext.fillStyle;
3922       layerContext.fillRect(sel.x, sel.y, sel.width, sel.height);
3923       app.historyAdd();
3924     }
3926     return true;
3927   };
3929   /**
3930    * Crop the image to selection width and height. The selected pixels become 
3931    * the image itself.
3932    *
3933    * <p>This method invokes the {@link this#selectionMerge} and {@link 
3934    * PaintWeb#imageCrop} methods.
3935    *
3936    * @returns {Boolean} True if the operation was successful, or false if not.
3937    */
3938   this.selectionCrop = function () {
3939     if (_self.state !== _self.STATE_SELECTED) {
3940       return false;
3941     }
3943     _self.selectionMerge();
3945     var w    = sel.width,
3946         h    = sel.height,
3947         sumX = sel.x + w,
3948         sumY = sel.y + h;
3950     if (sumX > image.width) {
3951       w -= sumX - image.width;
3952     }
3953     if (sumY > image.height) {
3954       h -= sumY - image.height;
3955     }
3957     app.imageCrop(sel.x, sel.y, w, h);
3959     return true;
3960   };
3962   /**
3963    * The <code>keydown</code> event handler. This method calls selection-related 
3964    * commands associated to keyboard shortcuts.
3965    *
3966    * @param {Event} ev The DOM Event object.
3967    *
3968    * @returns {Boolean} True if the keyboard shortcut was recognized, or false 
3969    * if not.
3970    *
3971    * @see PaintWeb.config.selection.keys holds the keyboard shortcuts 
3972    * configuration.
3973    */
3974   this.keydown = function (ev) {
3975     switch (ev.kid_) {
3976       case config.keys.transformToggle:
3977         // Toggle the selection transformation mode.
3978         config.transform = !config.transform;
3979         app.events.dispatch(new appEvent.configChange(config.transform, 
3980               !config.transform, 'transform', 'selection', config));
3981         break;
3983       case config.keys.selectionCrop:
3984         return _self.selectionCrop(ev);
3986       case config.keys.selectionDelete:
3987         return _self.selectionDelete(ev);
3989       case config.keys.selectionDrop:
3990         return _self.selectionDrop(ev);
3992       case config.keys.selectionFill:
3993         return _self.selectionFill(ev);
3995       default:
3996         return false;
3997     }
3999     return true;
4000   };
4004  * @class Selection change event. This event is not cancelable.
4006  * @augments pwlib.appEvent
4008  * @param {Number} state Tells the new state of the selection.
4009  * @param {Number} [x] Selection start position on the x-axis of the image.
4010  * @param {Number} [y] Selection start position on the y-axis of the image.
4011  * @param {Number} [width] Selection width.
4012  * @param {Number} [height] Selection height.
4013  */
4014 pwlib.appEvent.selectionChange = function (state, x, y, width, height) {
4015   /**
4016    * No selection is available.
4017    * @constant
4018    */
4019   this.STATE_NONE = 0;
4021   /**
4022    * Selection available.
4023    * @constant
4024    */
4025   this.STATE_SELECTED = 2;
4027   /**
4028    * Selection state.
4029    * @type Number
4030    */
4031   this.state = state;
4033   /**
4034    * Selection location on the x-axis of the image.
4035    * @type Number
4036    */
4037   this.x = x;
4039   /**
4040    * Selection location on the y-axis of the image.
4041    * @type Number
4042    */
4043   this.y = y;
4045   /**
4046    * Selection width.
4047    * @type Number
4048    */
4049   this.width  = width;
4051   /**
4052    * Selection height.
4053    * @type Number
4054    */
4055   this.height = height;
4057   pwlib.appEvent.call(this, 'selectionChange');
4060 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4063  * Copyright (C) 2008, 2009 Mihai Şucan
4065  * This file is part of PaintWeb.
4067  * PaintWeb is free software: you can redistribute it and/or modify
4068  * it under the terms of the GNU General Public License as published by
4069  * the Free Software Foundation, either version 3 of the License, or
4070  * (at your option) any later version.
4072  * PaintWeb is distributed in the hope that it will be useful,
4073  * but WITHOUT ANY WARRANTY; without even the implied warranty of
4074  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4075  * GNU General Public License for more details.
4077  * You should have received a copy of the GNU General Public License
4078  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
4080  * $URL: http://code.google.com/p/paintweb $
4081  * $Date: 2009-06-15 20:27:08 +0300 $
4082  */
4085  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4086  * @fileOverview Holds the hand tool implementation.
4087  */
4090  * @class The hand tool. This tool allows the user to drag the image canvas 
4091  * inside the viewport.
4093  * @param {PaintWeb} app Reference to the main paint application object.
4094  */
4095 pwlib.tools.hand = function (app) {
4096   var _self         = this,
4097       bufferCanvas  = app.buffer.canvas,
4098       bufferStyle   = bufferCanvas.style,
4099       config        = app.config;
4100       clearInterval = app.win.clearInterval,
4101       image         = app.image,
4102       MathRound     = Math.round,
4103       mouse         = app.mouse,
4104       viewport      = app.gui.elems.viewport,
4105       vheight       = 0,
4106       vwidth        = 0,
4107       setInterval   = app.win.setInterval;
4109   /**
4110    * The interval ID used for invoking the viewport drag operation every few 
4111    * milliseconds.
4112    *
4113    * @private
4114    * @see PaintWeb.config.toolDrawDelay
4115    */
4116   var timer = null;
4118   /**
4119    * Tells if the viewport needs to be scrolled.
4120    *
4121    * @private
4122    * @type Boolean
4123    * @default false
4124    */
4125   var needsScroll = false;
4127   /**
4128    * Holds the previous tool ID.
4129    *
4130    * @private
4131    * @type String
4132    */
4133   this.prevTool = null;
4135   var x0 = 0, y0 = 0,
4136       x1 = 0, y1 = 0,
4137       l0 = 0, t0 = 0;
4139   /**
4140    * Tool preactivation event handler.
4141    *
4142    * @returns {Boolean} True if the tool can become active, or false if not.
4143    */
4144   this.preActivate = function () {
4145     if (!viewport) {
4146       return false;
4147     }
4149     _self.prevTool = app.tool._id;
4151     // Check if the image canvas can be scrolled within the viewport.
4153     var cs      = app.win.getComputedStyle(viewport, null),
4154         bwidth  = parseInt(bufferStyle.width),
4155         bheight = parseInt(bufferStyle.height);
4157     vwidth  = parseInt(cs.width),
4158     vheight = parseInt(cs.height);
4160     if (vheight < bheight || vwidth < bwidth) {
4161       return true;
4162     } else {
4163       return false;
4164     }
4165   };
4167   /**
4168    * Tool activation event handler.
4169    */
4170   this.activate = function () {
4171     bufferStyle.cursor = 'move';
4172     app.shadowDisallow();
4173   };
4175   /**
4176    * Tool deactivation event handler.
4177    */
4178   this.deactivate = function (ev) {
4179     if (timer) {
4180       clearInterval(timer);
4181       timer = null;
4182       app.doc.removeEventListener('mousemove', ev_mousemove, false);
4183       app.doc.removeEventListener('mouseup',   ev_mouseup, false);
4184     }
4186     bufferStyle.cursor = '';
4187     app.shadowAllow();
4188   };
4190   /**
4191    * Initialize the canvas drag.
4192    *
4193    * @param {Event} ev The DOM event object.
4194    */
4195   this.mousedown = function (ev) {
4196     x0 = ev.clientX;
4197     y0 = ev.clientY;
4198     l0 = viewport.scrollLeft;
4199     t0 = viewport.scrollTop;
4201     needsScroll = false;
4203     app.doc.addEventListener('mousemove', ev_mousemove, false);
4204     app.doc.addEventListener('mouseup',   ev_mouseup, false);
4206     if (!timer) {
4207       timer = setInterval(viewportScroll, config.toolDrawDelay);
4208     }
4210     return true;
4211   };
4213   /**
4214    * The <code>mousemove</code> event handler. This simply stores the current 
4215    * mouse location.
4216    *
4217    * @param {Event} ev The DOM Event object.
4218    */
4219   function ev_mousemove (ev) {
4220     x1 = ev.clientX;
4221     y1 = ev.clientY;
4222     needsScroll = true;
4223   };
4225   /**
4226    * Perform the canvas drag operation. This function is called every few 
4227    * milliseconds.
4228    *
4229    * <p>Press <kbd>Escape</kbd> to stop dragging and to get back to the previous 
4230    * tool.
4231    */
4232   function viewportScroll () {
4233     if (needsScroll) {
4234       viewport.scrollTop  = t0 - y1 + y0;
4235       viewport.scrollLeft = l0 - x1 + x0;
4236       needsScroll = false;
4237     }
4238   };
4240   /**
4241    * The <code>mouseup</code> event handler.
4242    */
4243   function ev_mouseup (ev) {
4244     if (timer) {
4245       clearInterval(timer);
4246       timer = null;
4247     }
4249     ev_mousemove(ev);
4250     viewportScroll();
4252     app.doc.removeEventListener('mousemove', ev_mousemove, false);
4253     app.doc.removeEventListener('mouseup',   ev_mouseup, false);
4255     mouse.buttonDown = false;
4256   };
4258   /**
4259    * Allows the user to press <kbd>Escape</kbd> to stop dragging the canvas, and 
4260    * to return to the previous tool.
4261    *
4262    * @param {Event} ev The DOM Event object.
4263    *
4264    * @returns {Boolean} True if the key was recognized, or false if not.
4265    */
4266   this.keydown = function (ev) {
4267     if (!_self.prevTool || ev.kid_ != 'Escape') {
4268       return false;
4269     }
4271     app.toolActivate(_self.prevTool, ev);
4272     return true;
4273   };
4276 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4279  * Copyright (C) 2008, 2009 Mihai Şucan
4281  * This file is part of PaintWeb.
4283  * PaintWeb is free software: you can redistribute it and/or modify
4284  * it under the terms of the GNU General Public License as published by
4285  * the Free Software Foundation, either version 3 of the License, or
4286  * (at your option) any later version.
4288  * PaintWeb is distributed in the hope that it will be useful,
4289  * but WITHOUT ANY WARRANTY; without even the implied warranty of
4290  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4291  * GNU General Public License for more details.
4293  * You should have received a copy of the GNU General Public License
4294  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
4296  * $URL: http://code.google.com/p/paintweb $
4297  * $Date: 2009-06-11 20:21:13 +0300 $
4298  */
4301  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4302  * @fileOverview Holds the rectangle tool implementation.
4303  */
4306  * @class The rectangle tool.
4308  * @param {PaintWeb} app Reference to the main paint application object.
4309  */
4310 pwlib.tools.rectangle = function (app) {
4311   var _self         = this,
4312       clearInterval = app.win.clearInterval,
4313       config        = app.config,
4314       context       = app.buffer.context,
4315       gui           = app.gui,
4316       image         = app.image,
4317       MathAbs       = Math.abs,
4318       MathMin       = Math.min,
4319       mouse         = app.mouse,
4320       setInterval   = app.win.setInterval;
4322   /**
4323    * The interval ID used for invoking the drawing operation every few 
4324    * milliseconds.
4325    *
4326    * @private
4327    * @see PaintWeb.config.toolDrawDelay
4328    */
4329   var timer = null;
4331   /**
4332    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
4333    * drawing function.
4334    *
4335    * @private
4336    * @type Boolean
4337    * @default false
4338    */
4339   var shiftKey = false;
4341   /**
4342    * Tells if the drawing canvas needs to be updated or not.
4343    *
4344    * @private
4345    * @type Boolean
4346    * @default false
4347    */
4348   var needsRedraw = false;
4350   /**
4351    * Holds the starting point on the <var>x</var> axis of the image, for the 
4352    * current drawing operation.
4353    *
4354    * @private
4355    * @type Number
4356    */
4357   var x0 = 0;
4359   /**
4360    * Holds the starting point on the <var>y</var> axis of the image, for the 
4361    * current drawing operation.
4362    *
4363    * @private
4364    * @type Number
4365    */
4366   var y0 = 0;
4368   /**
4369    * Tool deactivation event handler.
4370    */
4371   this.deactivate = function () {
4372     if (timer) {
4373       clearInterval(timer);
4374       timer = null;
4375     }
4377     if (mouse.buttonDown) {
4378       context.clearRect(0, 0, image.width, image.height);
4379     }
4381     needsRedraw = false;
4382   };
4384   /**
4385    * Initialize the drawing operation.
4386    *
4387    * @param {Event} ev The DOM Event object.
4388    */
4389   this.mousedown = function (ev) {
4390     x0 = mouse.x;
4391     y0 = mouse.y;
4393     if (!timer) {
4394       timer = setInterval(_self.draw, config.toolDrawDelay);
4395     }
4396     shiftKey = ev.shiftKey;
4397     needsRedraw = false;
4399     gui.statusShow('rectangleMousedown');
4401     return true;
4402   };
4404   /**
4405    * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
4406    *
4407    * @param {Event} ev The DOM Event object.
4408    */
4409   this.mousemove = function (ev) {
4410     shiftKey = ev.shiftKey;
4411     needsRedraw = true;
4412   };
4414   /**
4415    * Perform the drawing operation. This function is called every few 
4416    * milliseconds.
4417    *
4418    * <p>Hold down the <kbd>Shift</kbd> key to draw a square.
4419    * <p>Press <kbd>Escape</kbd> to cancel the drawing operation.
4420    *
4421    * @see PaintWeb.config.toolDrawDelay
4422    */
4423   this.draw = function () {
4424     if (!needsRedraw) {
4425       return;
4426     }
4428     context.clearRect(0, 0, image.width, image.height);
4430     var x = MathMin(mouse.x,  x0),
4431         y = MathMin(mouse.y,  y0),
4432         w = MathAbs(mouse.x - x0),
4433         h = MathAbs(mouse.y - y0);
4435     if (!w || !h) {
4436       needsRedraw = false;
4437       return;
4438     }
4440     // Constrain the shape to a square
4441     if (shiftKey) {
4442       if (w > h) {
4443         if (y == mouse.y) {
4444           y -= w-h;
4445         }
4446         h = w;
4447       } else {
4448         if (x == mouse.x) {
4449           x -= h-w;
4450         }
4451         w = h;
4452       }
4453     }
4455     if (config.shapeType != 'stroke') {
4456       context.fillRect(x, y, w, h);
4457     }
4459     if (config.shapeType != 'fill') {
4460       context.strokeRect(x, y, w, h);
4461     }
4463     needsRedraw = false;
4464   };
4466   /**
4467    * End the drawing operation, once the user releases the mouse button.
4468    *
4469    * @param {Event} ev The DOM Event object.
4470    */
4471   this.mouseup = function (ev) {
4472     // Allow click+mousemove, not only mousedown+move+up
4473     if (mouse.x == x0 && mouse.y == y0) {
4474       mouse.buttonDown = true;
4475       return true;
4476     }
4478     if (timer) {
4479       clearInterval(timer);
4480       timer = null;
4481     }
4483     shiftKey = ev.shiftKey;
4484     _self.draw();
4485     app.layerUpdate();
4486     gui.statusShow('rectangleActive');
4488     return true;
4489   };
4491   /**
4492    * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
4493    *
4494    * @param {Event} ev The DOM Event object.
4495    *
4496    * @returns {Boolean} True if the drawing operation was cancelled, or false if 
4497    * not.
4498    */
4499   this.keydown = function (ev) {
4500     if (!mouse.buttonDown || ev.kid_ != 'Escape') {
4501       return false;
4502     }
4504     if (timer) {
4505       clearInterval(timer);
4506       timer = null;
4507     }
4509     context.clearRect(0, 0, image.width, image.height);
4510     mouse.buttonDown = false;
4511     needsRedraw = false;
4513     gui.statusShow('rectangleActive');
4515     return true;
4516   };
4519 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4523  * Copyright (C) 2008, 2009 Mihai Şucan
4525  * This file is part of PaintWeb.
4527  * PaintWeb is free software: you can redistribute it and/or modify
4528  * it under the terms of the GNU General Public License as published by
4529  * the Free Software Foundation, either version 3 of the License, or
4530  * (at your option) any later version.
4532  * PaintWeb is distributed in the hope that it will be useful,
4533  * but WITHOUT ANY WARRANTY; without even the implied warranty of
4534  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4535  * GNU General Public License for more details.
4537  * You should have received a copy of the GNU General Public License
4538  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
4540  * $URL: http://code.google.com/p/paintweb $
4541  * $Date: 2009-07-01 18:44:56 +0300 $
4542  */
4545  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4546  * @fileOverview Holds the ellipse tool implementation.
4547  */
4550  * @class The ellipse tool.
4552  * @param {PaintWeb} app Reference to the main paint application object.
4553  */
4554 pwlib.tools.ellipse = function (app) {
4555   var _self         = this,
4556       clearInterval = app.win.clearInterval,
4557       config        = app.config,
4558       context       = app.buffer.context,
4559       gui           = app.gui,
4560       image         = app.image,
4561       MathMax       = Math.max,
4562       MathMin       = Math.min,
4563       mouse         = app.mouse,
4564       setInterval   = app.win.setInterval;
4566   /**
4567    * The interval ID used for invoking the drawing operation every few 
4568    * milliseconds.
4569    *
4570    * @private
4571    * @see PaintWeb.config.toolDrawDelay
4572    */
4573   var timer = null;
4575   /**
4576    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
4577    * drawing function.
4578    *
4579    * @private
4580    * @type Boolean
4581    * @default false
4582    */
4583   var shiftKey = false;
4585   /**
4586    * Tells if the drawing canvas needs to be updated or not.
4587    *
4588    * @private
4589    * @type Boolean
4590    * @default false
4591    */
4592   var needsRedraw = false;
4594   var K = 4*((Math.SQRT2-1)/3);
4596   /**
4597    * Holds the starting point on the <var>x</var> axis of the image, for the 
4598    * current drawing operation.
4599    *
4600    * @private
4601    * @type Number
4602    */
4603   var x0 = 0;
4605   /**
4606    * Holds the starting point on the <var>y</var> axis of the image, for the 
4607    * current drawing operation.
4608    *
4609    * @private
4610    * @type Number
4611    */
4612   var y0 = 0;
4614   /**
4615    * Tool deactivation event handler.
4616    */
4617   this.deactivate = function () {
4618     if (timer) {
4619       clearInterval(timer);
4620       timer = null;
4621     }
4623     if (mouse.buttonDown) {
4624       context.clearRect(0, 0, image.width, image.height);
4625     }
4627     needsRedraw = false;
4629     return true;
4630   };
4632   /**
4633    * Initialize the drawing operation.
4634    *
4635    * @param {Event} ev The DOM Event object.
4636    */
4637   this.mousedown = function (ev) {
4638     // The mouse start position
4639     x0 = mouse.x;
4640     y0 = mouse.y;
4642     if (!timer) {
4643       timer = setInterval(_self.draw, config.toolDrawDelay);
4644     }
4645     shiftKey = ev.shiftKey;
4646     needsRedraw = false;
4648     gui.statusShow('ellipseMousedown');
4650     return true;
4651   };
4653   /**
4654    * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
4655    *
4656    * @param {Event} ev The DOM Event object.
4657    */
4658   this.mousemove = function (ev) {
4659     shiftKey = ev.shiftKey;
4660     needsRedraw = true;
4661   };
4663   /**
4664    * Perform the drawing operation. This function is called every few 
4665    * milliseconds.
4666    *
4667    * <p>Hold down the <kbd>Shift</kbd> key to draw a circle.
4668    * <p>Press <kbd>Escape</kbd> to cancel the drawing operation.
4669    *
4670    * @see PaintWeb.config.toolDrawDelay
4671    */
4672   this.draw = function () {
4673     if (!needsRedraw) {
4674       return;
4675     }
4677     context.clearRect(0, 0, image.width, image.height);
4679     var rectx0 = MathMin(mouse.x, x0),
4680         rectx1 = MathMax(mouse.x, x0),
4681         recty0 = MathMin(mouse.y, y0),
4682         recty1 = MathMax(mouse.y, y0);
4684     /*
4685       ABCD - rectangle
4686       A(rectx0, recty0), B(rectx1, recty0), C(rectx1, recty1), D(rectx0, recty1)
4687     */
4689     var w = rectx1-rectx0,
4690         h = recty1-recty0;
4692     if (!w || !h) {
4693       needsRedraw = false;
4694       return;
4695     }
4697     // Constrain the ellipse to be a circle
4698     if (shiftKey) {
4699       if (w > h) {
4700         recty1 = recty0+w;
4701         if (recty0 == mouse.y) {
4702           recty0 -= w-h;
4703           recty1 -= w-h;
4704         }
4705         h = w;
4706       } else {
4707         rectx1 = rectx0+h;
4708         if (rectx0 == mouse.x) {
4709           rectx0 -= h-w;
4710           rectx1 -= h-w;
4711         }
4712         w = h;
4713       }
4714     }
4716     // Ellipse radius
4717     var rx = w/2,
4718         ry = h/2; 
4720     // Ellipse center
4721     var cx = rectx0+rx,
4722         cy = recty0+ry;
4724     // Ellipse radius*Kappa, for the Bézier curve control points
4725     rx *= K;
4726     ry *= K;
4728     context.beginPath();
4730     // startX, startY
4731     context.moveTo(cx, recty0);
4733     // Control points: cp1x, cp1y, cp2x, cp2y, destx, desty
4734     // go clockwise: top-middle, right-middle, bottom-middle, then left-middle
4735     context.bezierCurveTo(cx + rx, recty0, rectx1, cy - ry, rectx1, cy);
4736     context.bezierCurveTo(rectx1, cy + ry, cx + rx, recty1, cx, recty1);
4737     context.bezierCurveTo(cx - rx, recty1, rectx0, cy + ry, rectx0, cy);
4738     context.bezierCurveTo(rectx0, cy - ry, cx - rx, recty0, cx, recty0);
4740     if (config.shapeType != 'stroke') {
4741       context.fill();
4742     }
4743     if (config.shapeType != 'fill') {
4744       context.stroke();
4745     }
4747     context.closePath();
4749     needsRedraw = false;
4750   };
4752   /**
4753    * End the drawing operation, once the user releases the mouse button.
4754    *
4755    * @param {Event} ev The DOM Event object.
4756    */
4757   this.mouseup = function (ev) {
4758     // Allow click+mousemove, not only mousedown+move+up
4759     if (mouse.x == x0 && mouse.y == y0) {
4760       mouse.buttonDown = true;
4761       return true;
4762     }
4764     if (timer) {
4765       clearInterval(timer);
4766       timer = null;
4767     }
4769     shiftKey = ev.shiftKey;
4770     _self.draw();
4771     app.layerUpdate();
4772     gui.statusShow('ellipseActive');
4774     return true;
4775   };
4777   /**
4778    * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
4779    *
4780    * @param {Event} ev The DOM Event object.
4781    *
4782    * @returns {Boolean} True if the drawing operation was cancelled, or false if 
4783    * not.
4784    */
4785   this.keydown = function (ev) {
4786     if (!mouse.buttonDown || ev.kid_ != 'Escape') {
4787       return false;
4788     }
4790     if (timer) {
4791       clearInterval(timer);
4792       timer = null;
4793     }
4795     context.clearRect(0, 0, image.width, image.height);
4796     mouse.buttonDown = false;
4797     needsRedraw = false;
4799     gui.statusShow('ellipseActive');
4801     return true;
4802   };
4805 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
4808  * Copyright (C) 2008, 2009 Mihai Şucan
4810  * This file is part of PaintWeb.
4812  * PaintWeb is free software: you can redistribute it and/or modify
4813  * it under the terms of the GNU General Public License as published by
4814  * the Free Software Foundation, either version 3 of the License, or
4815  * (at your option) any later version.
4817  * PaintWeb is distributed in the hope that it will be useful,
4818  * but WITHOUT ANY WARRANTY; without even the implied warranty of
4819  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4820  * GNU General Public License for more details.
4822  * You should have received a copy of the GNU General Public License
4823  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
4825  * $URL: http://code.google.com/p/paintweb $
4826  * $Date: 2009-06-11 20:28:07 +0300 $
4827  */
4830  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
4831  * @fileOverview Holds the polygon tool implementation.
4832  */
4835  * @class The polygon tool.
4837  * @param {PaintWeb} app Reference to the main paint application object.
4838  */
4839 pwlib.tools.polygon = function (app) {
4840   var _self         = this,
4841       clearInterval = app.win.clearInterval,
4842       config        = app.config,
4843       context       = app.buffer.context,
4844       gui           = app.gui,
4845       image         = app.image,
4846       MathAbs       = Math.abs,
4847       mouse         = app.mouse,
4848       setInterval   = app.win.setInterval,
4849       snapXY        = app.toolSnapXY;
4851   /**
4852    * Holds the points in the polygon being drawn.
4853    *
4854    * @private
4855    * @type Array
4856    */
4857   var points = [];
4859   /**
4860    * The interval ID used for invoking the drawing operation every few 
4861    * milliseconds.
4862    *
4863    * @private
4864    * @see PaintWeb.config.toolDrawDelay
4865    */
4866   var timer = null;
4868   /**
4869    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
4870    * drawing function.
4871    *
4872    * @private
4873    * @type Boolean
4874    * @default false
4875    */
4876   var shiftKey = false;
4878   /**
4879    * Tells if the drawing canvas needs to be updated or not.
4880    *
4881    * @private
4882    * @type Boolean
4883    * @default false
4884    */
4885   var needsRedraw = false;
4887   /**
4888    * The tool deactivation method, used for clearing the buffer.
4889    */
4890   this.deactivate = function () {
4891     if (timer) {
4892       clearInterval(timer);
4893       timer = null;
4894     }
4896     if (points.length) {
4897       context.clearRect(0, 0, image.width, image.height);
4898     }
4900     needsRedraw = false;
4901     points = [];
4903     return true;
4904   };
4906   /**
4907    * The <code>mousedown</code> event handler.
4908    *
4909    * @param {Event} ev The DOM Event object.
4910    * @returns {Boolean} True if the event handler executed, or false if not.
4911    */
4912   this.mousedown = function (ev) {
4913     if (points.length == 0) {
4914       points.push([mouse.x, mouse.y]);
4915     }
4917     if (!timer) {
4918       timer = setInterval(_self.draw, config.toolDrawDelay);
4919     }
4921     shiftKey = ev.shiftKey;
4922     needsRedraw = false;
4924     gui.statusShow('polygonMousedown');
4926     return true;
4927   };
4929   /**
4930    * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
4931    *
4932    * @param {Event} ev The DOM Event object.
4933    */
4934   this.mousemove = function (ev) {
4935     shiftKey = ev.shiftKey;
4936     needsRedraw = true;
4937   };
4939   /**
4940    * Draw the polygon.
4941    *
4942    * @see PaintWeb.config.toolDrawDelay
4943    */
4944   this.draw = function (ev) {
4945     if (!needsRedraw) {
4946       return;
4947     }
4949     var n = points.length;
4951     if (!n || (n == 1 && !mouse.buttonDown)) {
4952       needsRedraw = false;
4953       return;
4954     }
4956     // Snapping on the X/Y axis for the current point (if available).
4957     if (mouse.buttonDown && shiftKey) {
4958       snapXY(points[n-1][0], points[n-1][1]);
4959     }
4961     context.clearRect(0, 0, image.width, image.height);
4962     context.beginPath();
4963     context.moveTo(points[0][0], points[0][1]);
4965     // Draw the path of the polygon
4966     for (var i = 0; i < n; i++) {
4967       context.lineTo(points[i][0], points[i][1]);
4968     }
4970     if (mouse.buttonDown) {
4971       context.lineTo(mouse.x, mouse.y);
4972     }
4974     if (config.shapeType != 'stroke') {
4975       context.fill();
4976     }
4978     // In the case where we only have a straight line, draw a stroke even if no 
4979     // stroke should be drawn, such that the user has better visual feedback.
4980     if (config.shapeType != 'fill' || n == 1) {
4981       context.stroke();
4982     }
4984     context.closePath();
4986     needsRedraw = false;
4987   };
4989   /**
4990    * The <code>mouseup</code> event handler.
4991    *
4992    * @param {Event} ev The DOM Event object.
4993    * @returns {Boolean} True if the event handler executed, or false if not.
4994    */
4995   this.mouseup = function (ev) {
4996     var n = points.length;
4998     // Allow click+mousemove+click, not only mousedown+mousemove+mouseup.
4999     // Do this only for the first point in the polygon.
5000     if (n == 1 && mouse.x == points[n-1][0] && mouse.y == points[n-1][1]) {
5001       mouse.buttonDown = true;
5002       return true;
5003     }
5005     if (timer) {
5006       clearInterval(timer);
5007       timer = null;
5008     }
5010     shiftKey = ev.shiftKey;
5011     needsRedraw = true;
5013     if (ev.shiftKey) {
5014       snapXY(points[n-1][0], points[n-1][1]);
5015     }
5017     var diffx1 = MathAbs(mouse.x - points[0][0]),
5018         diffy1 = MathAbs(mouse.y - points[0][1]),
5019         diffx2 = MathAbs(mouse.x - points[n-1][0]),
5020         diffy2 = MathAbs(mouse.y - points[n-1][1]);
5022     // End the polygon if the new point is close enough to the first/last point.
5023     if ((diffx1 < 5 && diffy1 < 5) || (diffx2 < 5 && diffy2 < 5)) {
5024       // Add the start point to complete the polygon shape.
5025       points.push(points[0]);
5027       _self.draw();
5028       points = [];
5030       gui.statusShow('polygonActive');
5031       app.layerUpdate();
5033       return true;
5034     }
5036     if (n > 3) {
5037       gui.statusShow('polygonEnd');
5038     } else {
5039       gui.statusShow('polygonAddPoint');
5040     }
5042     points.push([mouse.x, mouse.y]);
5043     _self.draw();
5045     return true;
5046   };
5048   /**
5049    * The <code>keydown</code> event handler. This method allows the user to 
5050    * cancel drawing the current polygon, using the <kbd>Escape</kbd> key. The 
5051    * <kbd>Enter</kbd> key can be used to accept the current polygon shape, and 
5052    * end the drawing operation.
5053    *
5054    * @param {Event} ev The DOM Event object.
5055    *
5056    * @returns {Boolean} True if the keyboard shortcut was recognized, or false 
5057    * if not.
5058    */
5059   this.keydown = function (ev) {
5060     var n = points.length;
5061     if (!n || (ev.kid_ != 'Escape' && ev.kid_ != 'Enter')) {
5062       return false;
5063     }
5065     if (timer) {
5066       clearInterval(timer);
5067       timer = null;
5068     }
5069     mouse.buttonDown = false;
5071     if (ev.kid_ == 'Escape') {
5072       context.clearRect(0, 0, image.width, image.height);
5073       needsRedraw = false;
5075     } else if (ev.kid_ == 'Enter') {
5076       // Add the point of the last mousemove event, and the start point, to 
5077       // complete the polygon.
5078       points.push([mouse.x, mouse.y]);
5079       points.push(points[0]);
5080       needsRedraw = true;
5081       _self.draw();
5082       app.layerUpdate();
5083     }
5085     points = [];
5086     gui.statusShow('polygonActive');
5088     return true;
5089   };
5092 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
5095  * Copyright (C) 2008, 2009 Mihai Şucan
5097  * This file is part of PaintWeb.
5099  * PaintWeb is free software: you can redistribute it and/or modify
5100  * it under the terms of the GNU General Public License as published by
5101  * the Free Software Foundation, either version 3 of the License, or
5102  * (at your option) any later version.
5104  * PaintWeb is distributed in the hope that it will be useful,
5105  * but WITHOUT ANY WARRANTY; without even the implied warranty of
5106  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5107  * GNU General Public License for more details.
5109  * You should have received a copy of the GNU General Public License
5110  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
5112  * $URL: http://code.google.com/p/paintweb $
5113  * $Date: 2009-06-11 20:23:04 +0300 $
5114  */
5117  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
5118  * @fileOverview Holds the line tool implementation.
5119  */
5122  * @class The line tool.
5124  * @param {PaintWeb} app Reference to the main paint application object.
5125  */
5126 pwlib.tools.line = function (app) {
5127   var _self         = this,
5128       clearInterval = app.win.clearInterval,
5129       config        = app.config,
5130       context       = app.buffer.context,
5131       gui           = app.gui,
5132       image         = app.image,
5133       mouse         = app.mouse,
5134       setInterval   = app.win.setInterval,
5135       snapXY        = app.toolSnapXY;
5137   /**
5138    * The interval ID used for invoking the drawing operation every few 
5139    * milliseconds.
5140    *
5141    * @private
5142    * @see PaintWeb.config.toolDrawDelay
5143    */
5144   var timer = null;
5146   /**
5147    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
5148    * drawing function.
5149    *
5150    * @private
5151    * @type Boolean
5152    * @default false
5153    */
5154   var shiftKey = false;
5156   /**
5157    * Tells if the drawing canvas needs to be updated or not.
5158    *
5159    * @private
5160    * @type Boolean
5161    * @default false
5162    */
5163   var needsRedraw = false;
5165   /**
5166    * Holds the starting point on the <var>x</var> axis of the image, for the 
5167    * current drawing operation.
5168    *
5169    * @private
5170    * @type Number
5171    */
5172   var x0 = 0;
5174   /**
5175    * Holds the starting point on the <var>y</var> axis of the image, for the 
5176    * current drawing operation.
5177    *
5178    * @private
5179    * @type Number
5180    */
5181   var y0 = 0;
5183   /**
5184    * Tool deactivation event handler.
5185    */
5186   this.deactivate = function () {
5187     if (timer) {
5188       clearInterval(timer);
5189       timer = null;
5190     }
5192     if (mouse.buttonDown) {
5193       context.clearRect(0, 0, image.width, image.height);
5194     }
5196     needsRedraw = false;
5198     return true;
5199   };
5201   /**
5202    * Initialize the drawing operation, by storing the location of the pointer, 
5203    * the start position.
5204    *
5205    * @param {Event} ev The DOM Event object.
5206    */
5207   this.mousedown = function (ev) {
5208     x0 = mouse.x;
5209     y0 = mouse.y;
5211     if (!timer) {
5212       timer = setInterval(_self.draw, config.toolDrawDelay);
5213     }
5214     shiftKey = ev.shiftKey;
5215     needsRedraw = false;
5217     gui.statusShow('lineMousedown');
5219     return true;
5220   };
5222   /**
5223    * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
5224    *
5225    * @param {Event} ev The DOM Event object.
5226    */
5227   this.mousemove = function (ev) {
5228     shiftKey = ev.shiftKey;
5229     needsRedraw = true;
5230   };
5232   /**
5233    * Perform the drawing operation. This function is called every few 
5234    * milliseconds.
5235    *
5236    * <p>Hold down the <kbd>Shift</kbd> key to draw a straight 
5237    * horizontal/vertical line.
5238    * <p>Press <kbd>Escape</kbd> to cancel the drawing operation.
5239    *
5240    * @see PaintWeb.config.toolDrawDelay
5241    */
5242   this.draw = function () {
5243     if (!needsRedraw) {
5244       return;
5245     }
5247     context.clearRect(0, 0, image.width, image.height);
5249     // Snapping on the X/Y axis.
5250     if (shiftKey) {
5251       snapXY(x0, y0);
5252     }
5254     context.beginPath();
5255     context.moveTo(x0, y0);
5256     context.lineTo(mouse.x, mouse.y);
5257     context.stroke();
5258     context.closePath();
5260     needsRedraw = false;
5261   };
5263   /**
5264    * End the drawing operation, once the user releases the mouse button.
5265    *
5266    * @param {Event} ev The DOM Event object.
5267    */
5268   this.mouseup = function (ev) {
5269     // Allow users to click then drag, not only mousedown+drag+mouseup.
5270     if (mouse.x == x0 && mouse.y == y0) {
5271       mouse.buttonDown = true;
5272       return true;
5273     }
5275     if (timer) {
5276       clearInterval(timer);
5277       timer = null;
5278     }
5280     shiftKey = ev.shiftKey;
5281     _self.draw();
5282     gui.statusShow('lineActive');
5283     app.layerUpdate();
5285     return true;
5286   };
5288   /**
5289    * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
5290    *
5291    * @param {Event} ev The DOM Event object.
5292    *
5293    * @returns {Boolean} True if the drawing operation was cancelled, or false if 
5294    * not.
5295    */
5296   this.keydown = function (ev) {
5297     if (!mouse.buttonDown || ev.kid_ != 'Escape') {
5298       return false;
5299     }
5301     if (timer) {
5302       clearInterval(timer);
5303       timer = null;
5304     }
5306     context.clearRect(0, 0, image.width, image.height);
5307     mouse.buttonDown = false;
5308     needsRedraw = false;
5310     gui.statusShow('lineActive');
5312     return true;
5313   };
5316 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
5319  * Copyright (C) 2008, 2009 Mihai Şucan
5321  * This file is part of PaintWeb.
5323  * PaintWeb is free software: you can redistribute it and/or modify
5324  * it under the terms of the GNU General Public License as published by
5325  * the Free Software Foundation, either version 3 of the License, or
5326  * (at your option) any later version.
5328  * PaintWeb is distributed in the hope that it will be useful,
5329  * but WITHOUT ANY WARRANTY; without even the implied warranty of
5330  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5331  * GNU General Public License for more details.
5333  * You should have received a copy of the GNU General Public License
5334  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
5336  * $URL: http://code.google.com/p/paintweb $
5337  * $Date: 2009-06-22 22:31:49 +0300 $
5338  */
5341  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
5342  * @fileOverview Holds the text tool implementation.
5343  */
5345 // TODO: make this tool nicer to use.
5348  * @class The text tool.
5350  * @param {PaintWeb} app Reference to the main paint application object.
5351  */
5352 pwlib.tools.text = function (app) {
5353   var _self         = this,
5354       clearInterval = app.win.clearInterval,
5355       config        = app.config.text,
5356       context       = app.buffer.context,
5357       doc           = app.doc,
5358       gui           = app.gui,
5359       image         = app.image,
5360       lang          = app.lang,
5361       MathRound     = Math.round,
5362       mouse         = app.mouse,
5363       setInterval   = app.win.setInterval;
5365   /**
5366    * The interval ID used for invoking the drawing operation every few 
5367    * milliseconds.
5368    *
5369    * @private
5370    * @see PaintWeb.config.toolDrawDelay
5371    */
5372   var timer = null;
5374   /**
5375    * Holds the previous tool ID.
5376    *
5377    * @private
5378    * @type String
5379    */
5380   var prevTool = app.tool ? app.tool._id : null;
5382   /**
5383    * Tells if the drawing canvas needs to be updated or not.
5384    *
5385    * @private
5386    * @type Boolean
5387    * @default false
5388    */
5389   var needsRedraw = false;
5391   var inputString = null,
5392       input_fontFamily = null,
5393       ev_configChangeId = null,
5394       ns_svg = "http://www.w3.org/2000/svg",
5395       svgDoc = null,
5396       svgText = null,
5397       textWidth = 0,
5398       textHeight = 0;
5400   /**
5401    * Tool preactivation code. This method check if the browser has support for 
5402    * rendering text in Canvas.
5403    *
5404    * @returns {Boolean} True if the tool can be activated successfully, or false 
5405    * if not.
5406    */
5407   this.preActivate = function () {
5408     if (!gui.inputs.textString || !gui.inputs.text_fontFamily || 
5409         !gui.elems.viewport) {
5410       return false;
5412     }
5414     // Canvas 2D Text API
5415     if (context.fillText && context.strokeText) {
5416       return true;
5417     }
5419     // Opera can only render text via SVG Text.
5420     // Note: support for Opera has been disabled.
5421     // There are severe SVG redraw issues when updating the SVG text element.
5422     // Besides, there are important memory leaks.
5423     // Ultimately, there's a deal breaker: security violation. The SVG document 
5424     // which is rendered inside Canvas is considered "external" 
5425     // - get/putImageData() and toDataURL() stop working after drawImage(svg) is 
5426     // invoked. Eh.
5427     /*if (pwlib.browser.opera) {
5428       return true;
5429     }*/
5431     // Gecko 1.9.0 had its own proprietary Canvas 2D Text API.
5432     if (context.mozPathText) {
5433       return true;
5434     }
5436     alert(lang.errorTextUnsupported);
5437     return false;
5438   };
5440   /**
5441    * The tool activation code. This sets up a few variables, starts the drawing 
5442    * timer and adds event listeners as needed.
5443    */
5444   this.activate = function () {
5445     // Reset the mouse coordinates to the scroll top/left corner such that the 
5446     // text is rendered there.
5447     mouse.x = Math.round(gui.elems.viewport.scrollLeft / image.canvasScale),
5448     mouse.y = Math.round(gui.elems.viewport.scrollTop  / image.canvasScale),
5450     input_fontFamily = gui.inputs.text_fontFamily;
5451     inputString = gui.inputs.textString;
5453     if (!context.fillText && pwlib.browser.opera) {
5454       ev_configChangeId = app.events.add('configChange', ev_configChange_opera);
5455       inputString.addEventListener('input',  ev_configChange_opera, false);
5456       inputString.addEventListener('change', ev_configChange_opera, false);
5457     } else {
5458       ev_configChangeId = app.events.add('configChange', ev_configChange);
5459       inputString.addEventListener('input',  ev_configChange, false);
5460       inputString.addEventListener('change', ev_configChange, false);
5461     }
5463     // Render text using the Canvas 2D context text API defined by HTML 5.
5464     if (context.fillText && context.strokeText) {
5465       _self.draw = _self.draw_spec;
5467     } else if (pwlib.browser.opera) {
5468       // Render text using a SVG Text element which is copied into Canvas using 
5469       // drawImage().
5470       _self.draw = _self.draw_opera;
5471       initOpera();
5473     } else if (context.mozPathText) {
5474       // Render text using proprietary API available in Gecko 1.9.0.
5475       _self.draw = _self.draw_moz;
5476       textWidth = context.mozMeasureText(inputString.value);
5477     }
5479     if (!timer) {
5480       timer = setInterval(_self.draw, app.config.toolDrawDelay);
5481     }
5482     needsRedraw = true;
5483   };
5485   /**
5486    * The tool deactivation simply consists of removing the event listeners added 
5487    * when the tool was constructed, and clearing the buffer canvas.
5488    */
5489   this.deactivate = function () {
5490     if (timer) {
5491       clearInterval(timer);
5492       timer = null;
5493     }
5494     needsRedraw = false;
5496     if (ev_configChangeId) {
5497       app.events.remove('configChange', ev_configChangeId);
5498     }
5500     if (!context.fillText && pwlib.browser.opera) {
5501       inputString.removeEventListener('input',  ev_configChange_opera, false);
5502       inputString.removeEventListener('change', ev_configChange_opera, false);
5503     } else {
5504       inputString.removeEventListener('input',  ev_configChange, false);
5505       inputString.removeEventListener('change', ev_configChange, false);
5506     }
5508     svgText = null;
5509     svgDoc = null;
5511     context.clearRect(0, 0, image.width, image.height);
5513     return true;
5514   };
5516   /**
5517    * Initialize the SVG document for Opera. This is used for rendering the text.
5518    * @private
5519    */
5520   function initOpera () {
5521     svgDoc = doc.createElementNS(ns_svg, 'svg');
5522     svgDoc.setAttributeNS(ns_svg, 'version', '1.1');
5524     svgText = doc.createElementNS(ns_svg, 'text');
5525     svgText.appendChild(doc.createTextNode(inputString.value));
5526     svgDoc.appendChild(svgText);
5528     svgText.style.font = context.font;
5530     if (app.config.shapeType !== 'stroke') {
5531       svgText.style.fill = context.fillStyle;
5532     } else {
5533       svgText.style.fill = 'none';
5534     }
5536     if (app.config.shapeType !== 'fill') {
5537       svgText.style.stroke = context.strokeStyle;
5538       svgText.style.strokeWidth = context.lineWidth;
5539     } else {
5540       svgText.style.stroke = 'none';
5541       svgText.style.strokeWidth = context.lineWidth;
5542     }
5544     textWidth  = svgText.getComputedTextLength();
5545     textHeight = svgText.getBBox().height;
5547     svgDoc.setAttributeNS(ns_svg, 'width',  textWidth);
5548     svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10);
5549     svgText.setAttributeNS(ns_svg, 'x', 0);
5550     svgText.setAttributeNS(ns_svg, 'y', textHeight);
5551   };
5553   /**
5554    * The <code>configChange</code> application event handler. This is also the 
5555    * <code>input</code> and <code>change</code> event handler for the text 
5556    * string input element.  This method updates the Canvas text-related 
5557    * properties as needed, and re-renders the text.
5558    *
5559    * <p>This function is not used on Opera.
5560    *
5561    * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event 
5562    * object.
5563    */
5564   function ev_configChange (ev) {
5565     if (ev.type === 'input' || ev.type === 'change' ||
5566         (!ev.group && ev.config === 'shapeType') ||
5567         (ev.group === 'line' && ev.config === 'lineWidth')) {
5568       needsRedraw = true;
5570       // Update the text width.
5571       if (!context.fillText && context.mozMeasureText) {
5572         textWidth = context.mozMeasureText(inputString.value);
5573       }
5574       return;
5575     }
5577     if (ev.type !== 'configChange' && ev.group !== 'text') {
5578       return;
5579     }
5581     var font = '';
5583     switch (ev.config) {
5584       case 'fontFamily':
5585         if (ev.value === '+') {
5586           fontFamilyAdd(ev);
5587         }
5588       case 'bold':
5589       case 'italic':
5590       case 'fontSize':
5591         if (config.bold) {
5592           font += 'bold ';
5593         }
5594         if (config.italic) {
5595           font += 'italic ';
5596         }
5597         font += config.fontSize + 'px ' + config.fontFamily;
5598         context.font = font;
5600         if ('mozTextStyle' in context) {
5601           context.mozTextStyle = font;
5602         }
5604       case 'textAlign':
5605       case 'textBaseline':
5606         needsRedraw = true;
5607     }
5609     // Update the text width.
5610     if (ev.config !== 'textAlign' && ev.config !== 'textBaseline' && 
5611         !context.fillText && context.mozMeasureText) {
5612       textWidth = context.mozMeasureText(inputString.value);
5613     }
5614   };
5616   /**
5617    * The <code>configChange</code> application event handler. This is also the 
5618    * <code>input</code> and <code>change</code> event handler for the text 
5619    * string input element.  This method updates the Canvas text-related 
5620    * properties as needed, and re-renders the text.
5621    *
5622    * <p>This is function is specific to Opera.
5623    *
5624    * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event 
5625    * object.
5626    */
5627   function ev_configChange_opera (ev) {
5628     if (ev.type === 'input' || ev.type === 'change') {
5629       svgText.replaceChild(doc.createTextNode(this.value), svgText.firstChild);
5630       needsRedraw = true;
5631     }
5633     if (!ev.group && ev.config === 'shapeType') {
5634       if (ev.value !== 'stroke') {
5635         svgText.style.fill = context.fillStyle;
5636       } else {
5637         svgText.style.fill = 'none';
5638       }
5640       if (ev.value !== 'fill') {
5641         svgText.style.stroke = context.strokeStyle;
5642         svgText.style.strokeWidth = context.lineWidth;
5643       } else {
5644         svgText.style.stroke = 'none';
5645         svgText.style.strokeWidth = context.lineWidth;
5646       }
5647       needsRedraw = true;
5648     }
5650     if (!ev.group && ev.config === 'fillStyle') {
5651       if (app.config.shapeType !== 'stroke') {
5652         svgText.style.fill = ev.value;
5653         needsRedraw = true;
5654       }
5655     }
5657     if ((!ev.group && ev.config === 'strokeStyle') ||
5658         (ev.group === 'line' && ev.config === 'lineWidth')) {
5659       if (app.config.shapeType !== 'fill') {
5660         svgText.style.stroke = context.strokeStyle;
5661         svgText.style.strokeWidth = context.lineWidth;
5662         needsRedraw = true;
5663       }
5664     }
5666     if (ev.type === 'configChange' && ev.group === 'text') {
5667       var font = '';
5668       switch (ev.config) {
5669         case 'fontFamily':
5670           if (ev.value === '+') {
5671             fontFamilyAdd(ev);
5672           }
5673         case 'bold':
5674         case 'italic':
5675         case 'fontSize':
5676           if (config.bold) {
5677             font += 'bold ';
5678           }
5679           if (config.italic) {
5680             font += 'italic ';
5681           }
5682           font += config.fontSize + 'px ' + config.fontFamily;
5683           context.font = font;
5684           svgText.style.font = font;
5686         case 'textAlign':
5687         case 'textBaseline':
5688           needsRedraw = true;
5689       }
5690     }
5692     textWidth  = svgText.getComputedTextLength();
5693     textHeight = svgText.getBBox().height;
5695     svgDoc.setAttributeNS(ns_svg, 'width',  textWidth);
5696     svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10);
5697     svgText.setAttributeNS(ns_svg, 'x', 0);
5698     svgText.setAttributeNS(ns_svg, 'y', textHeight);
5699   };
5701   /**
5702    * Add a new font family into the font family drop down. This function is 
5703    * invoked by the <code>ev_configChange()</code> function when the user 
5704    * attempts to add a new font family.
5705    *
5706    * @private
5707    *
5708    * @param {pwlib.appEvent.configChange} ev The application event object.
5709    */
5710   function fontFamilyAdd (ev) {
5711     var new_font = prompt(lang.promptTextFont) || '';
5712     new_font = new_font.replace(/^\s+/, '').replace(/\s+$/, '') || 
5713       ev.previousValue;
5715     // Check if the font name is already in the list.
5716     var opt, new_font2 = new_font.toLowerCase(),
5717         n = input_fontFamily.options.length;
5719     for (var i = 0; i < n; i++) {
5720       opt = input_fontFamily.options[i];
5721       if (opt.value.toLowerCase() == new_font2) {
5722         config.fontFamily = opt.value;
5723         input_fontFamily.selectedIndex = i;
5724         input_fontFamily.value = config.fontFamily;
5725         ev.value = config.fontFamily;
5727         return;
5728       }
5729     }
5731     // Add the new font.
5732     opt = doc.createElement('option');
5733     opt.value = new_font;
5734     opt.appendChild(doc.createTextNode(new_font));
5735     input_fontFamily.insertBefore(opt, input_fontFamily.options[n-1]);
5736     input_fontFamily.selectedIndex = n-1;
5737     input_fontFamily.value = new_font;
5738     ev.value = new_font;
5739     config.fontFamily = new_font;
5740   };
5742   /**
5743    * The <code>mousemove</code> event handler.
5744    */
5745   this.mousemove = function () {
5746     needsRedraw = true;
5747   };
5749   /**
5750    * Perform the drawing operation using standard 2D context methods.
5751    *
5752    * @see PaintWeb.config.toolDrawDelay
5753    */
5754   this.draw_spec = function () {
5755     if (!needsRedraw) {
5756       return;
5757     }
5759     context.clearRect(0, 0, image.width, image.height);
5761     if (app.config.shapeType != 'stroke') {
5762       context.fillText(inputString.value, mouse.x, mouse.y);
5763     }
5765     if (app.config.shapeType != 'fill') {
5766       context.strokeText(inputString.value, mouse.x, mouse.y);
5767     }
5769     needsRedraw = false;
5770   };
5772   /**
5773    * Perform the drawing operation in Gecko 1.9.0.
5774    */
5775   this.draw_moz = function () {
5776     if (!needsRedraw) {
5777       return;
5778     }
5780     context.clearRect(0, 0, image.width, image.height);
5782     var x = mouse.x,
5783         y = mouse.y;
5785     if (config.textAlign === 'center') {
5786       x -= MathRound(textWidth / 2);
5787     } else if (config.textAlign === 'right') {
5788       x -= textWidth;
5789     }
5791     if (config.textBaseline === 'top') {
5792       y += config.fontSize;
5793     } else if (config.textBaseline === 'middle') {
5794       y += MathRound(config.fontSize / 2);
5795     }
5797     context.setTransform(1, 0, 0, 1, x, y);
5798     context.beginPath();
5799     context.mozPathText(inputString.value);
5801     if (app.config.shapeType != 'stroke') {
5802       context.fill();
5803     }
5805     if (app.config.shapeType != 'fill') {
5806       context.stroke();
5807     }
5808     context.closePath();
5809     context.setTransform(1, 0, 0, 1, 0, 0);
5811     needsRedraw = false;
5812   };
5814   /**
5815    * Perform the drawing operation in Opera using SVG.
5816    */
5817   this.draw_opera = function () {
5818     if (!needsRedraw) {
5819       return;
5820     }
5822     context.clearRect(0, 0, image.width, image.height);
5824     var x = mouse.x,
5825         y = mouse.y;
5827     if (config.textAlign === 'center') {
5828       x -= MathRound(textWidth / 2);
5829     } else if (config.textAlign === 'right') {
5830       x -= textWidth;
5831     }
5833     if (config.textBaseline === 'bottom') {
5834       y -= textHeight;
5835     } else if (config.textBaseline === 'middle') {
5836       y -= MathRound(textHeight / 2);
5837     }
5839     context.drawImage(svgDoc, x, y);
5841     needsRedraw = false;
5842   };
5844   /**
5845    * The <code>click</code> event handler. This method completes the drawing 
5846    * operation by inserting the text into the layer canvas.
5847    */
5848   this.click = function () {
5849     _self.draw();
5850     app.layerUpdate();
5851   };
5853   /**
5854    * The <code>keydown</code> event handler allows users to press the 
5855    * <kbd>Escape</kbd> key to cancel the drawing operation and return to the 
5856    * previous tool.
5857    *
5858    * @param {Event} ev The DOM Event object.
5859    * @returns {Boolean} True if the key was recognized, or false if not.
5860    */
5861   this.keydown = function (ev) {
5862     if (!prevTool || ev.kid_ != 'Escape') {
5863       return false;
5864     }
5866     mouse.buttonDown = false;
5867     app.toolActivate(prevTool, ev);
5869     return true;
5870   };
5873 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
5876  * Copyright (C) 2008, 2009 Mihai Şucan
5878  * This file is part of PaintWeb.
5880  * PaintWeb is free software: you can redistribute it and/or modify
5881  * it under the terms of the GNU General Public License as published by
5882  * the Free Software Foundation, either version 3 of the License, or
5883  * (at your option) any later version.
5885  * PaintWeb is distributed in the hope that it will be useful,
5886  * but WITHOUT ANY WARRANTY; without even the implied warranty of
5887  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5888  * GNU General Public License for more details.
5890  * You should have received a copy of the GNU General Public License
5891  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
5893  * $URL: http://code.google.com/p/paintweb $
5894  * $Date: 2009-07-01 18:43:53 +0300 $
5895  */
5898  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
5899  * @fileOverview Holds the Bézier curve tool implementation.
5900  */
5903  * @class The Bézier curve tool.
5905  * @param {PaintWeb} app Reference to the main paint application object.
5906  */
5907 pwlib.tools.bcurve = function (app) {
5908   var _self         = this,
5909       clearInterval = app.win.clearInterval,
5910       config        = app.config,
5911       context       = app.buffer.context,
5912       gui           = app.gui,
5913       image         = app.image,
5914       mouse         = app.mouse,
5915       setInterval   = app.win.setInterval,
5916       snapXY        = app.toolSnapXY;
5918   /**
5919    * Holds the points in the Bézier curve being drawn.
5920    *
5921    * @private
5922    * @type Array
5923    */
5924   var points = [];
5926   /**
5927    * The interval ID used for invoking the drawing operation every few 
5928    * milliseconds.
5929    *
5930    * @private
5931    * @see PaintWeb.config.toolDrawDelay
5932    */
5933   var timer = null;
5935   /**
5936    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
5937    * drawing function.
5938    *
5939    * @private
5940    * @type Boolean
5941    * @default false
5942    */
5943   var shiftKey = false;
5945   /**
5946    * Tells if the drawing canvas needs to be updated or not.
5947    *
5948    * @private
5949    * @type Boolean
5950    * @default false
5951    */
5952   var needsRedraw = false;
5954   /**
5955    * The tool deactivation method, used for clearing the buffer.
5956    */
5957   this.deactivate = function () {
5958     if (timer) {
5959       clearInterval(timer);
5960       timer = null;
5961     }
5963     if (points.length) {
5964       context.clearRect(0, 0, image.width, image.height);
5965     }
5967     needsRedraw = false;
5968     points = [];
5970     return true;
5971   };
5973   /**
5974    * The <code>mousedown</code> event handler.
5975    *
5976    * @param {Event} ev The DOM Event object.
5977    */
5978   this.mousedown = function (ev) {
5979     if (points.length == 0) {
5980       gui.statusShow('bcurveSnapping');
5981       points.push([mouse.x, mouse.y]);
5982     }
5984     if (!timer) {
5985       timer = setInterval(_self.draw, config.toolDrawDelay);
5986     }
5988     shiftKey = ev.shiftKey;
5989     needsRedraw = false;
5991     return true;
5992   };
5994   /**
5995    * Store the <kbd>Shift</kbd> key state which is used by the drawing function.
5996    *
5997    * @param {Event} ev The DOM Event object.
5998    */
5999   this.mousemove = function (ev) {
6000     shiftKey = ev.shiftKey;
6001     needsRedraw = true;
6002   };
6004   /**
6005    * Draw the Bézier curve, using the available points.
6006    *
6007    * @see PaintWeb.config.toolDrawDelay
6008    */
6009   this.draw = function () {
6010     if (!needsRedraw) {
6011       return;
6012     }
6014     var n = points.length;
6016     // Add the temporary point while the mouse button is down.
6017     if (mouse.buttonDown) {
6018       if (shiftKey && n == 1) {
6019         snapXY(points[0][0], points[0][1]);
6020       }
6021       points.push([mouse.x, mouse.y]);
6022       n++;
6023     }
6025     var p0          = points[0],
6026         p1          = points[1],
6027         p2          = points[2],
6028         p3          = points[3] || points[2],
6029         lineWidth   = context.lineWidth,
6030         strokeStyle = context.strokeStyle;
6032     if (mouse.buttonDown) {
6033       points.pop();
6034     }
6036     context.clearRect(0, 0, image.width, image.height);
6038     if (!n) {
6039       needsRedraw = false;
6040       return;
6041     }
6043     // Draw the main line
6044     if (n == 2) {
6045       context.beginPath();
6046       context.moveTo(p0[0], p0[1]+2);
6047       context.lineTo(p1[0], p1[1]+2);
6048       context.lineWidth = 1;
6049       context.strokeStyle = '#000000';
6050       context.stroke();
6051       context.closePath();
6053       context.lineWidth = lineWidth;
6054       context.strokeStyle = strokeStyle;
6056       needsRedraw = false;
6057       return;
6058     }
6060     // Draw the Bézier curve
6062     context.beginPath();
6063     context.moveTo(p0[0], p0[1]);
6064     context.bezierCurveTo(
6065       p2[0], p2[1],
6066       p3[0], p3[1],
6067       p1[0], p1[1]);
6069     if (config.shapeType != 'stroke') {
6070       context.fill();
6071     }
6073     if (config.shapeType != 'fill') {
6074       context.stroke();
6075     }
6077     context.closePath();
6079     needsRedraw = false;
6080   };
6082   /**
6083    * The <code>mouseup</code> event handler. This method stores the current 
6084    * mouse coordinates as a point to be used for drawing the Bézier curve.
6085    *
6086    * @param {Event} ev The DOM Event object.
6087    */
6088   this.mouseup = function (ev) {
6089     var n = points.length;
6091     // Allow click+mousemove+click, not only mousedown+mousemove+mouseup.
6092     // Do this only for the start point.
6093     if (n == 1 && mouse.x == points[0][0] && mouse.y == points[0][1]) {
6094       mouse.buttonDown = true;
6095       return true;
6096     }
6098     if (timer) {
6099       clearInterval(timer);
6100       timer = null;
6101     }
6103     if (n == 1 && ev.shiftKey) {
6104       snapXY(points[0][0], points[0][1]);
6105     }
6107     // We need 4 points to draw the Bézier curve: start, end, and two control 
6108     // points.
6109     if (n < 4) {
6110       points.push([mouse.x, mouse.y]);
6111       needsRedraw = true;
6112       n++;
6113     }
6115     // Make sure the canvas is up-to-date.
6116     shiftKey = ev.shiftKey;
6117     _self.draw();
6119     if (n == 2 || n == 3) {
6120       gui.statusShow('bcurveControlPoint' + (n-1));
6121     } else if (n == 4) {
6122       gui.statusShow('bcurveActive');
6123       app.layerUpdate();
6124       points = [];
6125     }
6127     return true;
6128   };
6130   /**
6131    * The <code>keydown</code> event handler. This method allows the user to 
6132    * press the <kbd>Escape</kbd> key to cancel the current drawing operation.
6133    *
6134    * @param {Event} ev The DOM Event object.
6135    *
6136    * @returns {Boolean} True if the keyboard shortcut was recognized, or false 
6137    * if not.
6138    */
6139   this.keydown = function (ev) {
6140     if (!points.length || ev.kid_ != 'Escape') {
6141       return false;
6142     }
6144     if (timer) {
6145       clearInterval(timer);
6146       timer = null;
6147     }
6149     context.clearRect(0, 0, image.width, image.height);
6151     points = [];
6152     needsRedraw = false;
6153     mouse.buttonDown = false;
6155     gui.statusShow('bcurveActive');
6157     return true;
6158   };
6161 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
6164  * Copyright (C) 2008, 2009 Mihai Şucan
6166  * This file is part of PaintWeb.
6168  * PaintWeb is free software: you can redistribute it and/or modify
6169  * it under the terms of the GNU General Public License as published by
6170  * the Free Software Foundation, either version 3 of the License, or
6171  * (at your option) any later version.
6173  * PaintWeb is distributed in the hope that it will be useful,
6174  * but WITHOUT ANY WARRANTY; without even the implied warranty of
6175  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
6176  * GNU General Public License for more details.
6178  * You should have received a copy of the GNU General Public License
6179  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
6181  * $URL: http://code.google.com/p/paintweb $
6182  * $Date: 2009-07-06 16:20:38 +0300 $
6183  */
6186  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
6187  * @fileOverview Holds the "Insert image" tool implementation.
6188  */
6190 // TODO: allow inserting images from a different host, using server-side magic.
6193  * @class The "Insert image" tool.
6195  * @param {PaintWeb} app Reference to the main paint application object.
6196  */
6197 pwlib.tools.insertimg = function (app) {
6198   var _self         = this,
6199       canvasImage   = app.image,
6200       clearInterval = app.win.clearInterval,
6201       config        = app.config,
6202       context       = app.buffer.context,
6203       gui           = app.gui,
6204       lang          = app.lang,
6205       MathAbs       = Math.abs,
6206       MathMin       = Math.min,
6207       MathRound     = Math.round,
6208       mouse         = app.mouse,
6209       setInterval   = app.win.setInterval;
6211   /**
6212    * Holds the previous tool ID.
6213    *
6214    * @private
6215    * @type String
6216    */
6217   var prevTool = app.tool ? app.tool._id : null;
6219   /**
6220    * The interval ID used for invoking the drawing operation every few 
6221    * milliseconds.
6222    *
6223    * @private
6224    * @see PaintWeb.config.toolDrawDelay
6225    */
6226   var timer = null;
6228   /**
6229    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
6230    * drawing function.
6231    *
6232    * @private
6233    * @type Boolean
6234    * @default false
6235    */
6236   var shiftKey = false;
6238   /**
6239    * Tells if the drawing canvas needs to be updated or not.
6240    *
6241    * @private
6242    * @type Boolean
6243    * @default false
6244    */
6245   var needsRedraw = false;
6247   /**
6248    * Holds the starting point on the <var>x</var> axis of the image, for the 
6249    * current drawing operation.
6250    *
6251    * @private
6252    * @type Number
6253    */
6254   var x0 = 0;
6256   /**
6257    * Holds the starting point on the <var>y</var> axis of the image, for the 
6258    * current drawing operation.
6259    *
6260    * @private
6261    * @type Number
6262    */
6263   var y0 = 0;
6265   /**
6266    * Tells if the image element loaded or not.
6267    *
6268    * @private
6269    * @type Boolean
6270    */
6271   var imageLoaded = false;
6273   /**
6274    * Holds the image aspect ratio, used by the resize method.
6275    *
6276    * @private
6277    * @type Number
6278    */
6279   var imageRatio = 1;
6281   /**
6282    * Holds the DOM image element.
6283    *
6284    * @private
6285    * @type Element
6286    */
6287   var imageElement = null;
6289   /**
6290    * Holds the image address.
6291    * @type String
6292    */
6293   if (!this.url) {
6294     this.url = 'http://';
6295   }
6297   /**
6298    * The tool preactivation code. This function asks the user to provide an URL 
6299    * to the image which is desired to be inserted into the canvas.
6300    *
6301    * @returns {Boolean} True if the URL provided is correct. False is returned 
6302    * if the URL is not provided or if it's incorrect. When false is returned the 
6303    * tool activation is cancelled.
6304    */
6305   this.preActivate = function () {
6306     if (!gui.elems.viewport) {
6307       return false;
6308     }
6310     _self.url = prompt(lang.promptInsertimg, _self.url);
6312     if (!_self.url || _self.url.toLowerCase() === 'http://') {
6313       return false;
6314     }
6316     // Remember the URL.
6317     pwlib.extend(true, _self.constructor.prototype, {url: _self.url});
6319     if (!pwlib.isSameHost(_self.url, app.win.location.host)) {
6320       alert(lang.errorInsertimgHost);
6321       return false;
6322     }
6324     return true;
6325   };
6327   /**
6328    * The tool activation event handler. This function is called once the 
6329    * previous tool has been deactivated.
6330    */
6331   this.activate = function () {
6332     imageElement = new Image();
6333     imageElement.addEventListener('load', ev_imageLoaded, false);
6334     imageElement.src = _self.url;
6336     return true;
6337   };
6339   /**
6340    * The tool deactivation event handler.
6341    */
6342   this.deactivate = function () {
6343     if (imageElement) {
6344       imageElement = null;
6345     }
6347     if (timer) {
6348       clearInterval(timer);
6349       timer = null;
6350     }
6351     needsRedraw = false;
6353     context.clearRect(0, 0, canvasImage.width, canvasImage.height);
6355     return true;
6356   };
6358   /**
6359    * The <code>load</code> event handler for the image element. This method 
6360    * makes sure the image dimensions are synchronized with the zoom level, and 
6361    * draws the image on the canvas.
6362    *
6363    * @private
6364    */
6365   function ev_imageLoaded () {
6366     // Did the image already load?
6367     if (imageLoaded) {
6368       return;
6369     }
6371     // The default position for the inserted image is the top left corner of the visible area, taking into consideration the zoom level.
6372     var x = MathRound(gui.elems.viewport.scrollLeft / canvasImage.canvasScale),
6373         y = MathRound(gui.elems.viewport.scrollTop  / canvasImage.canvasScale);
6375     context.clearRect(0, 0, canvasImage.width, canvasImage.height);
6377     try {
6378       context.drawImage(imageElement, x, y);
6379     } catch (err) {
6380       alert(lang.errorInsertimg);
6381       return;
6382     }
6384     imageLoaded = true;
6385     needsRedraw = false;
6387     if (!timer) {
6388       timer = setInterval(_self.draw, config.toolDrawDelay);
6389     }
6391     gui.statusShow('insertimgLoaded');
6392   };
6394   /**
6395    * The <code>mousedown</code> event handler. This method stores the current 
6396    * mouse location and the image aspect ratio for later reuse by the 
6397    * <code>draw()</code> method.
6398    *
6399    * @param {Event} ev The DOM Event object.
6400    */
6401   this.mousedown = function (ev) {
6402     if (!imageLoaded) {
6403       alert(lang.errorInsertimgNotLoaded);
6404       return false;
6405     }
6407     x0 = mouse.x;
6408     y0 = mouse.y;
6410     // The image aspect ratio - used by the draw() method when the user holds 
6411     // the Shift key down.
6412     imageRatio = imageElement.width / imageElement.height;
6413     shiftKey = ev.shiftKey;
6415     gui.statusShow('insertimgResize');
6417     if (ev.stopPropagation) {
6418       ev.stopPropagation();
6419     }
6420   };
6422   /**
6423    * The <code>mousemove</code> event handler.
6424    *
6425    * @param {Event} ev The DOM Event object.
6426    */
6427   this.mousemove = function (ev) {
6428     shiftKey = ev.shiftKey;
6429     needsRedraw = true;
6430   };
6432   /**
6433    * Perform the drawing operation. When the mouse button is not down, the user 
6434    * is allowed to pick where he/she wants to insert the image element, inside 
6435    * the canvas. Once the <code>mousedown</code> event is fired, this method 
6436    * allows the user to resize the image inside the canvas.
6437    *
6438    * @see PaintWeb.config.toolDrawDelay
6439    */
6440   this.draw = function () {
6441     if (!imageLoaded || !needsRedraw) {
6442       return;
6443     }
6445     context.clearRect(0, 0, canvasImage.width, canvasImage.height);
6447     // If the user is holding down the mouse button, then allow him/her to 
6448     // resize the image.
6449     if (mouse.buttonDown) {
6450       var w = MathAbs(mouse.x - x0),
6451           h = MathAbs(mouse.y - y0),
6452           x = MathMin(mouse.x,  x0),
6453           y = MathMin(mouse.y,  y0);
6455       if (!w || !h) {
6456         needsRedraw = false;
6457         return;
6458       }
6460       // If the Shift key is down, constrain the image to have the same aspect 
6461       // ratio as the original image element.
6462       if (shiftKey) {
6463         if (w > h) {
6464           if (y == mouse.y) {
6465             y -= w-h;
6466           }
6467           h = MathRound(w/imageRatio);
6468         } else {
6469           if (x == mouse.x) {
6470             x -= h-w;
6471           }
6472           w = MathRound(h*imageRatio);
6473         }
6474       }
6476       context.drawImage(imageElement, x, y, w, h);
6477     } else {
6478       // If the mouse button is not down, simply allow the user to pick where 
6479       // he/she wants to insert the image element.
6480       context.drawImage(imageElement, mouse.x, mouse.y);
6481     }
6483     needsRedraw = false;
6484   };
6486   /**
6487    * The <code>mouseup</code> event handler. This method completes the drawing 
6488    * operation by inserting the image in the layer canvas, and by activating the 
6489    * previous tool.
6490    *
6491    * @param {Event} ev The DOM Event object.
6492    */
6493   this.mouseup = function (ev) {
6494     if (!imageLoaded) {
6495       return false;
6496     }
6498     if (timer) {
6499       clearInterval(timer);
6500       timer = null;
6501     }
6503     app.layerUpdate();
6505     if (prevTool) {
6506       app.toolActivate(prevTool, ev);
6507     }
6509     if (ev.stopPropagation) {
6510       ev.stopPropagation();
6511     }
6512   };
6514   /**
6515    * The <code>keydown</code> event handler allows users to press the 
6516    * <kbd>Escape</kbd> key to cancel the drawing operation and return to the 
6517    * previous tool.
6518    *
6519    * @param {Event} ev The DOM Event object.
6520    * @returns {Boolean} True if the key was recognized, or false if not.
6521    */
6522   this.keydown = function (ev) {
6523     if (!prevTool || ev.kid_ != 'Escape') {
6524       return false;
6525     }
6527     if (timer) {
6528       clearInterval(timer);
6529       timer = null;
6530     }
6532     mouse.buttonDown = false;
6533     app.toolActivate(prevTool, ev);
6535     return true;
6536   };
6539 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
6542  * Copyright (C) 2008, 2009 Mihai Şucan
6544  * This file is part of PaintWeb.
6546  * PaintWeb is free software: you can redistribute it and/or modify
6547  * it under the terms of the GNU General Public License as published by
6548  * the Free Software Foundation, either version 3 of the License, or
6549  * (at your option) any later version.
6551  * PaintWeb is distributed in the hope that it will be useful,
6552  * but WITHOUT ANY WARRANTY; without even the implied warranty of
6553  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
6554  * GNU General Public License for more details.
6556  * You should have received a copy of the GNU General Public License
6557  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
6559  * $URL: http://code.google.com/p/paintweb $
6560  * $Date: 2009-06-15 15:25:29 +0300 $
6561  */
6564  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
6565  * @fileOverview Holds the pencil tool implementation.
6566  */
6569  * @class The drawing pencil.
6571  * @param {PaintWeb} app Reference to the main paint application object.
6572  */
6573 pwlib.tools.pencil = function (app) {
6574   var _self         = this,
6575       clearInterval = app.win.clearInterval,
6576       context       = app.buffer.context,
6577       image         = app.image,
6578       mouse         = app.mouse,
6579       setInterval   = app.win.setInterval;
6581   /**
6582    * The interval ID used for running the pencil drawing operation every few 
6583    * milliseconds.
6584    *
6585    * @private
6586    * @see PaintWeb.config.toolDrawDelay
6587    */
6588   var timer = null;
6590   /**
6591    * Holds the points needed to be drawn. Each point is added by the 
6592    * <code>mousemove</code> event handler.
6593    *
6594    * @private
6595    * @type Array
6596    */
6597   var points = [];
6599   /**
6600    * Holds the last point on the <var>x</var> axis of the image, for the current 
6601    * drawing operation.
6602    *
6603    * @private
6604    * @type Number
6605    */
6606   var x0 = 0;
6608   /**
6609    * Holds the last point on the <var>y</var> axis of the image, for the current 
6610    * drawing operation.
6611    *
6612    * @private
6613    * @type Number
6614    */
6615   var y0 = 0;
6617   /**
6618    * Tool deactivation event handler.
6619    */
6620   this.deactivate = function () {
6621     if (timer) {
6622       clearInterval(timer);
6623       timer = null;
6624     }
6626     if (mouse.buttonDown) {
6627       context.clearRect(0, 0, image.width, image.height);
6628     }
6630     points = [];
6631   };
6633   /**
6634    * Initialize the drawing operation.
6635    */
6636   this.mousedown = function () {
6637     x0 = mouse.x;
6638     y0 = mouse.y;
6640     points = [];
6641     if (!timer) {
6642       timer = setInterval(_self.draw, app.config.toolDrawDelay);
6643     }
6645     return true;
6646   };
6648   /**
6649    * Save the mouse coordinates in the array.
6650    */
6651   this.mousemove = function () {
6652     if (mouse.buttonDown) {
6653       points.push(mouse.x, mouse.y);
6654     }
6655   };
6657   /**
6658    * Draw the points in the stack. This function is called every few 
6659    * milliseconds.
6660    *
6661    * @see PaintWeb.config.toolDrawDelay
6662    */
6663   this.draw = function () {
6664     var i = 0, n = points.length;
6665     if (!n) {
6666       return;
6667     }
6669     context.beginPath();
6670     context.moveTo(x0, y0);
6672     while (i < n) {
6673       x0 = points[i++];
6674       y0 = points[i++];
6675       context.lineTo(x0, y0);
6676     }
6678     context.stroke();
6679     context.closePath();
6681     points = [];
6682   };
6684   /**
6685    * End the drawing operation, once the user releases the mouse button.
6686    */
6687   this.mouseup = function () {
6688     if (mouse.x == x0 && mouse.y == y0) {
6689       points.push(x0+1, y0+1);
6690     }
6692     if (timer) {
6693       clearInterval(timer);
6694       timer = null;
6695     }
6697     _self.draw();
6698     app.layerUpdate();
6700     return true;
6701   };
6703   /**
6704    * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
6705    *
6706    * @param {Event} ev The DOM Event object.
6707    *
6708    * @returns {Boolean} True if the drawing operation was cancelled, or false if 
6709    * not.
6710    */
6711   this.keydown = function (ev) {
6712     if (!mouse.buttonDown || ev.kid_ != 'Escape') {
6713       return false;
6714     }
6716     if (timer) {
6717       clearInterval(timer);
6718       timer = null;
6719     }
6721     context.clearRect(0, 0, image.width, image.height);
6722     mouse.buttonDown = false;
6723     points = [];
6725     return true;
6726   };
6729 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
6732  * Copyright (C) 2008, 2009 Mihai Şucan
6734  * This file is part of PaintWeb.
6736  * PaintWeb is free software: you can redistribute it and/or modify
6737  * it under the terms of the GNU General Public License as published by
6738  * the Free Software Foundation, either version 3 of the License, or
6739  * (at your option) any later version.
6741  * PaintWeb is distributed in the hope that it will be useful,
6742  * but WITHOUT ANY WARRANTY; without even the implied warranty of
6743  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
6744  * GNU General Public License for more details.
6746  * You should have received a copy of the GNU General Public License
6747  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
6749  * $URL: http://code.google.com/p/paintweb $
6750  * $Date: 2009-07-02 15:37:38 +0300 $
6751  */
6754  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
6755  * @fileOverview Holds the color picker implementation.
6756  */
6759  * @class The color picker tool.
6761  * @param {PaintWeb} app Reference to the main paint application object.
6762  */
6763 pwlib.tools.cpicker = function (app) {
6764   var _self        = this,
6765       colormixer   = app.extensions.colormixer,
6766       context      = app.layer.context,
6767       gui          = app.gui,
6768       lang         = app.lang,
6769       MathRound    = Math.round,
6770       mouse        = app.mouse;
6772   /**
6773    * Holds the ID of the previously active tool. Once the user completes the 
6774    * color picking operation, the previous tool is activated.
6775    *
6776    * @private
6777    * @type String
6778    */
6779   var prevTool = null;
6781   /**
6782    * Holds a reference to the target color input. This is a GUI color input 
6783    * component.
6784    *
6785    * @private
6786    * @type pwlib.guiColorInput
6787    */
6788   var targetInput = null;
6790   /**
6791    * Holds the previous color values - before the user started picking 
6792    * a different color.
6793    *
6794    * @private
6795    * @type Object
6796    */
6797   var prevColor = null;
6799   /**
6800    * Tells if the color mixer is active for the current target input.
6801    *
6802    * @private
6803    * @type Boolean
6804    */
6805   var colormixerActive = false;
6807   /**
6808    * Tells if the current color values are accepted by the user. This value is 
6809    * used by the tool deactivation code.
6810    *
6811    * @private
6812    * @type Boolean
6813    */
6814   var colorAccepted = false;
6816   /**
6817    * The <code>preActivate</code> event handler. This method checks if the 
6818    * browser implements the <code>getImageData()</code> context method. If not, 
6819    * the color picker tool cannot be used.
6820    */
6821   this.preActivate = function () {
6822     // The latest versions of all browsers which implement Canvas, also 
6823     // implement the getImageData() method. This was only a problem with some 
6824     // old versions (eg. Opera 9.2).
6825     if (!context.getImageData) {
6826       alert(lang.errorCpickerUnsupported);
6827       return false;
6828     }
6830     if (app.tool && app.tool._id) {
6831       prevTool = app.tool._id;
6832     }
6834     return true;
6835   };
6837   /**
6838    * The <code>activate</code> event handler. This method determines the current 
6839    * target input in the Color Mixer, if any. Canvas shadow rendering is 
6840    * disallowed.
6841    */
6842   this.activate = function () {
6843     // When the color mixer panel is active, the color picker uses the same 
6844     // target input.
6845     if (colormixer && colormixer.targetInput) {
6846       targetInput = gui.colorInputs[colormixer.targetInput.id];
6847     }
6849     if (targetInput) {
6850       gui.statusShow('cpicker_' + targetInput.id);
6851     } else {
6852       gui.statusShow('cpickerNormal');
6853     }
6855     app.shadowDisallow();
6856   };
6858   /**
6859    * The <code>deactivate</code> event handler. This method allows shadow 
6860    * rendering again, and resets the color input values if the user did not 
6861    * accept the new color.
6862    */
6863   this.deactivate = function () {
6864     if (!colorAccepted && targetInput && prevColor) {
6865       updateColor(null, true);
6866     }
6868     app.shadowAllow();
6869   };
6871   /**
6872    * The <code>mousedown</code> event handler. This method starts the color 
6873    * picking operation.
6874    *
6875    * @param {Event} ev The DOM Event object.
6876    */
6877   this.mousedown = function (ev) {
6878     // We check again, because the user might have opened/closed the color 
6879     // mixer.
6880     if (colormixer && colormixer.targetInput) {
6881       targetInput = gui.colorInputs[colormixer.targetInput.id];
6882     }
6884     if (targetInput) {
6885       colormixerActive = true;
6886       gui.statusShow('cpicker_' + targetInput.id);
6887     } else {
6888       colormixerActive = false;
6889       gui.statusShow('cpickerNormal');
6891       // The context menu (right-click). This is unsupported by Opera.
6892       // Also allow Shift+Click for changing the stroke color (making it easier for Opera users).
6893       if (ev.button === 2 || ev.shiftKey) {
6894         targetInput = gui.colorInputs.strokeStyle;
6895       } else {
6896         targetInput = gui.colorInputs.fillStyle;
6897       }
6898     }
6900     updatePrevColor();
6902     _self.mousemove = updateColor;
6903     updateColor(ev);
6905     return true;
6906   };
6908   /**
6909    * Perform color update. This function updates the target input or the Color 
6910    * Mixer to hold the color value under the mouse - it actually performs the 
6911    * color picking operation.
6912    *
6913    * <p>This function is also the <code>mousemove</code> event handler for this 
6914    * tool.
6915    *
6916    * @param {Event} ev The DOM Event object.
6917    * @param {Boolean} [usePrevColor=false] Tells the function to use the 
6918    * previous color values we have stored. This is used when the user cancels 
6919    * the color picking operation.
6920    */
6921   function updateColor (ev, usePrevColor) {
6922     if (!targetInput) {
6923       return;
6924     }
6926     var p = usePrevColor ? prevColor :
6927               context.getImageData(mouse.x, mouse.y, 1, 1),
6928         color = {
6929           red:    p.data[0] / 255,
6930           green:  p.data[1] / 255,
6931           blue:   p.data[2] / 255,
6932           alpha: (p.data[3] / 255).toFixed(3)
6933         };
6935     if (colormixerActive) {
6936       colormixer.color.red   = color.red;
6937       colormixer.color.green = color.green;
6938       colormixer.color.blue  = color.blue;
6939       colormixer.color.alpha = color.alpha;
6940       colormixer.update_color('rgb');
6942     } else {
6943       targetInput.updateColor(color);
6944     }
6945   };
6947   /**
6948    * The <code>mouseup</code> event handler. This method completes the color 
6949    * picking operation, and activates the previous tool.
6950    *
6951    * <p>The {@link pwlib.appEvent.configChange} application event is also 
6952    * dispatched for the configuration property associated to the target input.
6953    *
6954    * @param {Event} ev The DOM Event object.
6955    */
6956   this.mouseup = function (ev) {
6957     if (!targetInput) {
6958       return false;
6959     }
6961     delete _self.mousemove;
6962     updateColor(ev);
6963     colorAccepted = true;
6965     if (!colormixerActive) {
6966       var color = targetInput.color,
6967           configProperty = targetInput.configProperty,
6968           configGroup    = targetInput.configGroup,
6969           configGroupRef = targetInput.configGroupRef,
6970           prevVal = configGroupRef[configProperty],
6971           newVal  = 'rgba(' + MathRound(color.red   * 255) + ',' +
6972                               MathRound(color.green * 255) + ',' +
6973                               MathRound(color.blue  * 255) + ',' +
6974                               color.alpha + ')';
6976       if (prevVal !== newVal) {
6977         configGroupRef[configProperty] = newVal;
6978         app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal, 
6979             configProperty, configGroup, configGroupRef));
6980       }
6981     }
6983     if (prevTool) {
6984       app.toolActivate(prevTool, ev);
6985     }
6987     return true;
6988   };
6990   /**
6991    * The <code>keydown</code> event handler. This method allows the user to 
6992    * press the <kbd>Escape</kbd> key to cancel the color picking operation. By 
6993    * doing so, the original color values are restored.
6994    *
6995    * @param {Event} ev The DOM Event object.
6996    * @returns {Boolean} True if the keyboard shortcut was recognized, or false 
6997    * if not.
6998    */
6999   this.keydown = function (ev) {
7000     if (!prevTool || ev.kid_ !== 'Escape') {
7001       return false;
7002     }
7004     mouse.buttonDown = false;
7005     app.toolActivate(prevTool, ev);
7007     return true;
7008   };
7010   /**
7011    * The <code>contextmenu</code> event handler. This method only cancels the 
7012    * context menu.
7013    */
7014   // Unfortunately, the contextmenu event is unsupported by Opera.
7015   this.contextmenu = function () {
7016     return true;
7017   };
7019   /**
7020    * Store the color values from the target color input, before this tool 
7021    * changes the colors. The previous color values are used when the user 
7022    * decides to cancel the color picking operation.
7023    * @private
7024    */
7025   function updatePrevColor () {
7026     // If the color mixer panel is visible, then we store the color values from 
7027     // the color mixer, instead of those from the color input object.
7028     var color = colormixerActive ? colormixer.color : targetInput.color;
7030     prevColor = {
7031       width: 1,
7032       height: 1,
7033       data: [
7034         MathRound(color.red   * 255),
7035         MathRound(color.green * 255),
7036         MathRound(color.blue  * 255),
7037         color.alpha * 255
7038       ]
7039     };
7040   };
7043 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
7046  * Copyright (C) 2008, 2009 Mihai Şucan
7048  * This file is part of PaintWeb.
7050  * PaintWeb is free software: you can redistribute it and/or modify
7051  * it under the terms of the GNU General Public License as published by
7052  * the Free Software Foundation, either version 3 of the License, or
7053  * (at your option) any later version.
7055  * PaintWeb is distributed in the hope that it will be useful,
7056  * but WITHOUT ANY WARRANTY; without even the implied warranty of
7057  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
7058  * GNU General Public License for more details.
7060  * You should have received a copy of the GNU General Public License
7061  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
7063  * $URL: http://code.google.com/p/paintweb $
7064  * $Date: 2009-07-01 18:46:31 +0300 $
7065  */
7068  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
7069  * @fileOverview Holds the eraser tool implementation.
7070  */
7073  * @class The eraser tool.
7075  * @param {PaintWeb} app Reference to the main paint application object.
7076  */
7077 pwlib.tools.eraser = function (app) {
7078   var _self         = this,
7079       clearInterval = app.win.clearInterval,
7080       config        = app.config,
7081       context       = app.buffer.context,
7082       image         = app.image,
7083       layerContext  = app.layer.context,
7084       mouse         = app.mouse,
7085       setInterval   = app.win.setInterval;
7087   /**
7088    * The interval ID used for running the pencil drawing operation every few 
7089    * milliseconds.
7090    *
7091    * @private
7092    * @see PaintWeb.config.toolDrawDelay
7093    */
7094   var timer = null;
7096   /**
7097    * Holds the points needed to be drawn. Each point is added by the 
7098    * <code>mousemove</code> event handler.
7099    *
7100    * @private
7101    * @type Array
7102    */
7103   var points = [];
7105   /**
7106    * Holds the starting point on the <var>x</var> axis of the image, for the 
7107    * current drawing operation.
7108    *
7109    * @private
7110    * @type Number
7111    */
7112   var x0 = 0;
7114   /**
7115    * Holds the starting point on the <var>y</var> axis of the image, for the 
7116    * current drawing operation.
7117    *
7118    * @private
7119    * @type Number
7120    */
7121   var y0 = 0;
7123   var strokeStyle_ = null;
7125   /**
7126    * The tool deactivation event handler. This function clears timers, clears 
7127    * the canvas and allows shadows to be rendered again.
7128    */
7129   this.deactivate = function () {
7130     if (timer) {
7131       clearInterval(timer);
7132       timer = null;
7133     }
7135     if (mouse.buttonDown) {
7136       context.clearRect(0, 0, image.width, image.height);
7137     }
7139     points = [];
7141     // Allow Canvas shadows.
7142     app.shadowAllow();
7143   };
7145   /**
7146    * The tool activation event handler. This is run after the tool construction 
7147    * and after the deactivation of the previous tool. This function simply 
7148    * disallows the rendering of shadows.
7149    */
7150   this.activate = function () {
7151     // Do not allow Canvas shadows.
7152     app.shadowDisallow();
7153   };
7155   /**
7156    * Initialize the drawing operation.
7157    */
7158   this.mousedown = function () {
7159     // The mousedown event remembers the current strokeStyle and sets a white 
7160     // colored stroke (same as the background), such that the user gets live 
7161     // feedback of what he/she erases.
7163     strokeStyle_ = context.strokeStyle;
7164     context.strokeStyle = config.backgroundColor;
7166     x0 = mouse.x;
7167     y0 = mouse.y;
7169     points = [];
7170     if (!timer) {
7171       timer = setInterval(_self.draw, config.toolDrawDelay);
7172     }
7174     return true;
7175   };
7177   /**
7178    * Save the mouse coordinates in the array.
7179    */
7180   this.mousemove = function () {
7181     if (mouse.buttonDown) {
7182       points.push(mouse.x, mouse.y);
7183     }
7184   };
7186   /**
7187    * Draw the points in the stack. This function is called every few 
7188    * milliseconds.
7189    *
7190    * @see PaintWeb.config.toolDrawDelay
7191    */
7192   this.draw = function () {
7193     var i = 0, n = points.length;
7194     if (!n) {
7195       return;
7196     }
7198     context.beginPath();
7199     context.moveTo(x0, y0);
7201     while (i < n) {
7202       x0 = points[i++];
7203       y0 = points[i++];
7204       context.lineTo(x0, y0);
7205     }
7207     context.stroke();
7208     context.closePath();
7210     points = [];
7211   };
7213   /**
7214    * End the drawing operation, once the user releases the mouse button.
7215    */
7216   this.mouseup = function () {
7217     // The mouseup event handler changes the globalCompositeOperation to 
7218     // destination-out such that the white pencil path drawn by the user cuts 
7219     // out/clears the destination image
7221     if (mouse.x == x0 && mouse.y == y0) {
7222       points.push(x0+1, y0+1);
7223     }
7225     if (timer) {
7226       clearInterval(timer);
7227       timer = null;
7228     }
7229     _self.draw();
7231     var op = layerContext.globalCompositeOperation;
7232     layerContext.globalCompositeOperation = 'destination-out';
7234     app.layerUpdate();
7236     layerContext.globalCompositeOperation = op;
7237     context.strokeStyle = strokeStyle_;
7239     return true;
7240   };
7242   /**
7243    * Allows the user to press <kbd>Escape</kbd> to cancel the drawing operation.
7244    *
7245    * @param {Event} ev The DOM Event object.
7246    *
7247    * @returns {Boolean} True if the drawing operation was cancelled, or false if 
7248    * not.
7249    */
7250   this.keydown = function (ev) {
7251     if (!mouse.buttonDown || ev.kid_ != 'Escape') {
7252       return false;
7253     }
7255     if (timer) {
7256       clearInterval(timer);
7257       timer = null;
7258     }
7260     context.clearRect(0, 0, image.width, image.height);
7261     context.strokeStyle = strokeStyle_;
7262     mouse.buttonDown = false;
7263     points = [];
7265     return true;
7266   };
7269 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
7273  * Copyright (C) 2008, 2009 Mihai Şucan
7275  * This file is part of PaintWeb.
7277  * PaintWeb is free software: you can redistribute it and/or modify
7278  * it under the terms of the GNU General Public License as published by
7279  * the Free Software Foundation, either version 3 of the License, or
7280  * (at your option) any later version.
7282  * PaintWeb is distributed in the hope that it will be useful,
7283  * but WITHOUT ANY WARRANTY; without even the implied warranty of
7284  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
7285  * GNU General Public License for more details.
7287  * You should have received a copy of the GNU General Public License
7288  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
7290  * $URL: http://code.google.com/p/paintweb $
7291  * $Date: 2009-07-09 14:26:21 +0300 $
7292  */
7295  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
7296  * @fileOverview Holds the implementation of the Color Mixer dialog.
7297  */
7299 // For the implementation of this extension I used the following references:
7300 // - Wikipedia articles on each subject.
7301 // - the great brucelindbloom.com Web site - lots of information.
7304  * @class The Color Mixer extension.
7306  * @param {PaintWeb} app Reference to the main paint application object.
7307  */
7308 pwlib.extensions.colormixer = function (app) {
7309   var _self     = this,
7310       config    = app.config.colormixer,
7311       doc       = app.doc,
7312       gui       = app.gui,
7313       lang      = app.lang.colormixer,
7314       MathFloor = Math.floor,
7315       MathMax   = Math.max,
7316       MathMin   = Math.min,
7317       MathPow   = Math.pow,
7318       MathRound = Math.round,
7319       resScale  = app.resolution.scale;
7321   /**
7322    * Holds references to various DOM elements.
7323    *
7324    * @private
7325    * @type Object
7326    */
7327   this.elems = {
7328     /**
7329      * Reference to the element which holds Canvas controls (the dot on the 
7330      * Canvas, and the slider).
7331      * @type Element
7332      */
7333     'controls': null,
7335     /**
7336      * Reference to the dot element that is rendered on top of the color space 
7337      * visualisation.
7338      * @type Element
7339      */
7340     'chartDot': null,
7342     /**
7343      * Reference to the slider element.
7344      * @type Element
7345      */
7346     'slider': null,
7348     /**
7349      * Reference to the input element that allows the user to pick the color 
7350      * palette to be displayed.
7351      * @type Element
7352      */
7353     'cpaletteInput': null,
7355     /**
7356      * The container element which holds the colors of the currently selected 
7357      * palette.
7358      * @type Element
7359      */
7360     'cpaletteOutput': null,
7362     /**
7363      * Reference to the element which displays the current color.
7364      * @type Element
7365      */
7366     "colorActive": null,
7368     /**
7369      * Reference to the element which displays the old color.
7370      * @type Element
7371      */
7372     "colorOld": null
7373   };
7375   /**
7376    * Reference to the Color Mixer floating panel GUI component object.
7377    *
7378    * @private
7379    * @type pwlib.guiFloatingPanel
7380    */
7381   this.panel = null;
7383   /**
7384    * Reference to the Color Mixer tab panel GUI component object which holds the 
7385    * inputs.
7386    *
7387    * @private
7388    * @type pwlib.guiTabPanel
7389    */
7390   this.panelInputs = null;
7392   /**
7393    * Reference to the Color Mixer tab panel GUI component object which holds the 
7394    * Canvas used for color space visualisation and the color palettes selector.
7395    *
7396    * @private
7397    * @type pwlib.guiTabPanel
7398    */
7399   this.panelSelector = null;
7401   /**
7402    * Holds a reference to the 2D context of the color mixer Canvas element. This 
7403    * is where the color chart and the slider are both drawn.
7404    *
7405    * @private
7406    * @type CanvasRenderingContext2D
7407    */
7408   this.context2d = false;
7410   /**
7411    * Target input hooks. This object must hold two methods:
7412    *
7413    * <ul>
7414    *   <li><code>show()</code> which is invoked by this extension when the Color 
7415    *   Mixer panel shows up on screen.
7416    *
7417    *   <li><code>hide()</code> which is invoked when the Color Mixer panel is 
7418    *   hidden from the screen.
7419    * </ul>
7420    *
7421    * <p>The object must also hold information about the associated configuration 
7422    * property: <var>configProperty</var>, <var>configGroup</var> and 
7423    * <var>configGroupRef</var>.
7424    *
7425    * @type Object
7426    */
7427   this.targetInput = null;
7429   /**
7430    * Holds the current color in several formats: RGB, HEX, HSV, CIE Lab, and 
7431    * CMYK. Except for 'hex', all the values should be from 0 to 1.
7432    *
7433    * @type Object
7434    */
7435   this.color = {
7436     // RGB
7437     red  : 0,
7438     green: 0,
7439     blue : 0,
7441     alpha : 0,
7442     hex   : 0,
7444     // HSV
7445     hue : 0,
7446     sat : 0,
7447     val : 0,
7449     // CMYK
7450     cyan    : 0,
7451     magenta : 0,
7452     yellow  : 0,
7453     black   : 0,
7455     // CIE Lab
7456     cie_l : 0,
7457     cie_a : 0,
7458     cie_b : 0
7459   };
7461   /**
7462    * Holds references to all the DOM input fields, for each color channel.
7463    *
7464    * @private
7465    * @type Object
7466    */
7467   this.inputs = {
7468     red   : null,
7469     green : null,
7470     blue  : null,
7472     alpha : null,
7473     hex   : null,
7475     hue : null,
7476     sat : null,
7477     val : null,
7479     cyan    : null,
7480     magenta : null,
7481     yellow  : null,
7482     black   : null,
7484     cie_l : null,
7485     cie_a : null,
7486     cie_b : null
7487   };
7489   /**
7490    * The "absolute maximum" value is determined based on the min/max values.  
7491    * For min -100 and max 100, the abs_max is 200.
7492    * @private
7493    *
7494    */
7495   this.abs_max  = {};
7497   // The hue spectrum used by the HSV charts.
7498   var hueSpectrum = [
7499     [255,   0,   0], // 0, Red,       0°
7500     [255, 255,   0], // 1, Yellow,   60°
7501     [  0, 255,   0], // 2, Green,   120°
7502     [  0, 255, 255], // 3, Cyan,    180°
7503     [  0,   0, 255], // 4, Blue,    240°
7504     [255,   0, 255], // 5, Magenta, 300°
7505     [255,   0,   0]  // 6, Red,     360°
7506   ];
7508   // The active color key (input) determines how the color chart works.
7509   this.ckey_active = 'red';
7511   // Given a group of the inputs: red, green and blue, when one of them is active, the ckey_adjoint is set to an array of the other two input IDs.
7512   this.ckey_adjoint = false;
7513   this.ckey_active_group = false;
7515   this.ckey_grouping = {
7516     'red'   : 'rgb',
7517     'green' : 'rgb',
7518     'blue'  : 'rgb',
7520     'hue' : 'hsv',
7521     'sat' : 'hsv',
7522     'val' : 'hsv',
7524     'cyan'    : 'cmyk',
7525     'magenta' : 'cmyk',
7526     'yellow'  : 'cmyk',
7527     'black'   : 'cmyk',
7529     'cie_l' : 'lab',
7530     'cie_a' : 'lab',
7531     'cie_b' : 'lab'
7532   };
7534   // These values are automatically calculated when the color mixer is 
7535   // initialized.
7536   this.sliderX = 0;
7537   this.sliderWidth = 0;
7538   this.sliderHeight = 0;
7539   this.sliderSpacing = 0;
7540   this.chartWidth = 0;
7541   this.chartHeight = 0;
7543   /**
7544    * Register the Color Mixer extension.
7545    *
7546    * @returns {Boolean} True if the extension can be registered properly, or 
7547    * false if not.
7548    */
7549   this.extensionRegister = function (ev) {
7550     if (!gui.elems || !gui.elems.colormixer_canvas || !gui.floatingPanels || 
7551         !gui.floatingPanels.colormixer || !gui.tabPanels || 
7552         !gui.tabPanels.colormixer_inputs || !gui.tabPanels.colormixer_selector 
7553         || !_self.init_lab()) {
7554       return false;
7555     }
7557     _self.panel = gui.floatingPanels.colormixer;
7558     _self.panelSelector = gui.tabPanels.colormixer_selector;
7559     _self.panelInputs = gui.tabPanels.colormixer_inputs;
7561     // Initialize the color mixer Canvas element.
7562     _self.context2d = gui.elems.colormixer_canvas.getContext('2d');
7563     if (!_self.context2d) {
7564       return false;
7565     }
7567     // Setup the color mixer inputs.
7568     var elem, label, labelElem,
7569         inputValues = config.inputValues,
7570         form = _self.panelInputs.container;
7571     if (!form) {
7572       return false;
7573     }
7575     for (var i in _self.inputs) {
7576       elem = form.elements.namedItem('ckey_' + i) || gui.inputs['ckey_' + i];
7577       if (!elem) {
7578         return false;
7579       }
7581       if (i === 'hex' || i === 'alpha') {
7582         label = lang.inputs[i];
7583       } else {
7584         label = lang.inputs[_self.ckey_grouping[i] + '_' + i];
7585       }
7587       labelElem = elem.parentNode;
7588       labelElem.replaceChild(doc.createTextNode(label), labelElem.firstChild);
7590       elem.addEventListener('input',  _self.ev_input_change, false);
7591       elem.addEventListener('change', _self.ev_input_change, false);
7593       if (i !== 'hex') {
7594         elem.setAttribute('step', inputValues[i][2]);
7596         elem.setAttribute('max', MathRound(inputValues[i][1]));
7597         elem.setAttribute('min', MathRound(inputValues[i][0]));
7598         _self.abs_max[i] = inputValues[i][1] - inputValues[i][0];
7599       }
7601       // Store the color key, which is used by the event handler.
7602       elem._ckey = i;
7603       _self.inputs[i] = elem;
7604     }
7606     // Setup the ckey inputs of type=radio.
7607     var ckey = form.ckey;
7608     if (!ckey) {
7609       return false;
7610     }
7611     for (var i = 0, n = ckey.length; i < n; i++) {
7612       elem = ckey[i];
7613       if (_self.ckey_grouping[elem.value] === 'lab' && 
7614           !_self.context2d.putImageData) {
7615         elem.disabled = true;
7616         continue;
7617       }
7619       elem.addEventListener('change', _self.ev_change_ckey_active, false);
7621       if (elem.value === _self.ckey_active) {
7622         elem.checked = true;
7623         _self.update_ckey_active(_self.ckey_active, true);
7624       }
7625     }
7627     // Prepare the color preview elements.
7628     _self.elems.colorActive = gui.elems.colormixer_colorActive.firstChild;
7629     _self.elems.colorOld = gui.elems.colormixer_colorOld.firstChild;
7630     _self.elems.colorOld.addEventListener('click', _self.ev_click_color, false);
7632     // Make sure the buttons work properly.
7633     var anchor, btn, buttons = ['accept', 'cancel', 'saveColor', 'pickColor'];
7634     for (var i = 0, n = buttons.length; i < n; i++) {
7635       btn = gui.elems['colormixer_btn_' + buttons[i]];
7636       if (!btn) {
7637         continue;
7638       }
7640       anchor = doc.createElement('a');
7641       anchor.href = '#';
7642       anchor.appendChild(doc.createTextNode(lang.buttons[buttons[i]]));
7643       anchor.addEventListener('click', _self['ev_click_' + buttons[i]], false);
7645       btn.replaceChild(anchor, btn.firstChild);
7646     }
7648     // Prepare the canvas "controls" (the chart "dot" and the slider).
7649     var id, elems = ['controls', 'chartDot', 'slider'];
7650     for (var i = 0, n = elems.length; i < n; i++) {
7651       id = elems[i];
7652       elem = gui.elems['colormixer_' + id];
7653       if (!elem) {
7654         return false;
7655       }
7657       elem.addEventListener('mousedown', _self.ev_canvas, false);
7658       elem.addEventListener('mousemove', _self.ev_canvas, false);
7659       elem.addEventListener('mouseup',   _self.ev_canvas, false);
7661       _self.elems[id] = elem;
7662     }
7664     // The color palette <select>.
7665     _self.elems.cpaletteInput = gui.inputs.colormixer_cpaletteInput;
7666     _self.elems.cpaletteInput.addEventListener('change', 
7667         _self.ev_change_cpalette, false);
7669     // Add the list of color palettes into the <select>.
7670     var palette;
7671     for (var i in config.colorPalettes) {
7672       palette = config.colorPalettes[i];
7673       elem = doc.createElement('option');
7674       elem.value = i;
7675       if (i === config.paletteDefault) {
7676         elem.selected = true;
7677       }
7679       elem.appendChild( doc.createTextNode(lang.colorPalettes[i]) );
7680       _self.elems.cpaletteInput.appendChild(elem);
7681     }
7683     // This is the ordered list where we add each color (list item).
7684     _self.elems.cpaletteOutput = gui.elems.colormixer_cpaletteOutput;
7685     _self.elems.cpaletteOutput.addEventListener('click', _self.ev_click_color, 
7686         false);
7688     _self.cpalette_load(config.paletteDefault);
7690     // Make sure the Canvas element scale is in sync with the application.
7691     app.events.add('canvasSizeChange', _self.update_dimensions);
7693     _self.panelSelector.events.add('guiTabActivate', _self.ev_tabActivate);
7695     // Make sure the Color Mixer is properly closed when the floating panel is 
7696     // closed.
7697     _self.panel.events.add('guiFloatingPanelStateChange', 
7698         _self.ev_panel_stateChange);
7700     return true;
7701   };
7703   /**
7704    * This function calculates lots of values used by the other CIE Lab-related 
7705    * functions.
7706    *
7707    * @private
7708    * @returns {Boolean} True if the initialization was successful, or false if 
7709    * not.
7710    */
7711   this.init_lab = function () {
7712     var cfg = config.lab;
7713     if (!cfg) {
7714       return false;
7715     }
7717     // Chromaticity coordinates for the RGB primaries.
7718     var x0_r = cfg.x_r,
7719         y0_r = cfg.y_r,
7720         x0_g = cfg.x_g,
7721         y0_g = cfg.y_g,
7722         x0_b = cfg.x_b,
7723         y0_b = cfg.y_b,
7725         // The reference white point (xyY to XYZ).
7726         w_x = cfg.ref_x / cfg.ref_y,
7727         w_y = 1,
7728         w_z = (1 - cfg.ref_x - cfg.ref_y) / cfg.ref_y;
7730     cfg.w_x = w_x;
7731     cfg.w_y = w_y;
7732     cfg.w_z = w_z;
7734     // Again, xyY to XYZ for each RGB primary. Y=1.
7735     var x_r = x0_r / y0_r,
7736         y_r = 1,
7737         z_r = (1 - x0_r - y0_r) / y0_r,
7738         x_g = x0_g / y0_g,
7739         y_g = 1,
7740         z_g = (1 - x0_g - y0_g) / y0_g,
7741         x_b = x0_b / y0_b,
7742         y_b = 1,
7743         z_b = (1 - x0_b - y0_b) / y0_b,
7744         m   = [x_r, y_r, z_r,
7745                x_g, y_g, z_g,
7746                x_b, y_b, z_b],
7747         m_i = _self.calc_m3inv(m),
7748         s   = _self.calc_m1x3([w_x, w_y, w_z], m_i);
7750     // The 3x3 matrix used by rgb2xyz().
7751     m = [s[0] * m[0], s[0] * m[1], s[0] * m[2],
7752          s[1] * m[3], s[1] * m[4], s[1] * m[5],
7753          s[2] * m[6], s[2] * m[7], s[2] * m[8]];
7755     // The matrix inverse, used by xyz2rgb();
7756     cfg.m_i = _self.calc_m3inv(m);
7757     cfg.m   = m;
7759     // Now determine the min/max values for a and b.
7761     var xyz = _self.rgb2xyz([0, 1, 0]), // green gives the minimum value for a
7762         lab = _self.xyz2lab(xyz),
7763         values = config.inputValues;
7764     values.cie_a[0] = lab[1];
7766     xyz = _self.rgb2xyz([1, 0, 1]);     // magenta gives the maximum value for a
7767     lab = _self.xyz2lab(xyz);
7768     values.cie_a[1] = lab[1];
7770     xyz = _self.rgb2xyz([0, 0, 1]);     // blue gives the minimum value for b
7771     lab = _self.xyz2lab(xyz);
7772     values.cie_b[0] = lab[2];
7774     xyz = _self.rgb2xyz([1, 1, 0]);     // yellow gives the maximum value for b
7775     lab = _self.xyz2lab(xyz);
7776     values.cie_b[1] = lab[2];
7778     return true;
7779   };
7781   /**
7782    * The <code>guiTabActivate</code> event handler for the tab panel which holds 
7783    * the color mixer and the color palettes. When switching back to the color 
7784    * mixer, this method updates the Canvas.
7785    *
7786    * @private
7787    * @param {pwlib.appEvent.guiTabActivate} ev The application event object.
7788    */
7789   this.ev_tabActivate = function (ev) {
7790     if (ev.tabId === 'mixer' && _self.update_canvas_needed) {
7791       _self.update_canvas(null, true);
7792     }
7793   };
7795   /**
7796    * The <code>click</code> event handler for the Accept button. This method 
7797    * dispatches the {@link pwlib.appEvent.configChange} application event for 
7798    * the configuration property associated to the target input, and hides the 
7799    * Color Mixer floating panel.
7800    *
7801    * @private
7802    * @param {Event} ev The DOM Event object.
7803    */
7804   this.ev_click_accept = function (ev) {
7805     ev.preventDefault();
7807     var configProperty = _self.targetInput.configProperty,
7808         configGroup    = _self.targetInput.configGroup,
7809         configGroupRef = _self.targetInput.configGroupRef,
7810         prevVal = configGroupRef[configProperty],
7811         newVal  = 'rgba(' + MathRound(_self.color.red   * 255) + ',' +
7812                             MathRound(_self.color.green * 255) + ',' +
7813                             MathRound(_self.color.blue  * 255) + ',' +
7814                             _self.color.alpha + ')';
7816     _self.hide();
7818     if (prevVal !== newVal) {
7819       configGroupRef[configProperty] = newVal;
7820       app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal, 
7821           configProperty, configGroup, configGroupRef));
7822     }
7823   };
7825   /**
7826    * The <code>click</code> event handler for the Cancel button. This method 
7827    * hides the Color Mixer floating panel.
7828    *
7829    * @private
7830    * @param {Event} ev The DOM Event object.
7831    */
7832   this.ev_click_cancel = function (ev) {
7833     ev.preventDefault();
7834     _self.hide();
7835   };
7837   /**
7838    * The <code>click</code> event handler for the "Save color" button. This 
7839    * method adds the current color into the "_saved" color palette.
7840    *
7841    * @private
7842    * @param {Event} ev The DOM Event object.
7843    */
7844   // TODO: provide a way to save the color palette permanently. This should use 
7845   // some application event.
7846   this.ev_click_saveColor = function (ev) {
7847     ev.preventDefault();
7849     var color = [_self.color.red, _self.color.green, _self.color.blue],
7850         saved = config.colorPalettes._saved;
7852     saved.colors.push(color);
7854     _self.elems.cpaletteInput.value = '_saved';
7855     _self.cpalette_load('_saved');
7856     _self.panelSelector.tabActivate('cpalettes');
7858     return true;
7859   };
7861   /**
7862    * The <code>click</code> event handler for the "Pick color" button. This 
7863    * method activates the color picker tool.
7864    *
7865    * @private
7866    * @param {Event} ev The DOM Event object.
7867    */
7868   this.ev_click_pickColor = function (ev) {
7869     ev.preventDefault();
7870     app.toolActivate('cpicker', ev);
7871   };
7873   /**
7874    * The <code>change</code> event handler for the color palette input element.  
7875    * This loads the color palette the user selected.
7876    *
7877    * @private
7878    * @param {Event} ev The DOM Event object.
7879    */
7880   this.ev_change_cpalette = function (ev) {
7881     _self.cpalette_load(this.value);
7882   };
7884   /**
7885    * Load a color palette. Loading is performed asynchronously.
7886    *
7887    * @param {String} id The color palette ID.
7888    *
7889    * @returns {Boolean} True if the load was successful, or false if not.
7890    */
7891   this.cpalette_load = function (id) {
7892     if (!id || !(id in config.colorPalettes)) {
7893       return false;
7894     }
7896     var palette = config.colorPalettes[id];
7898     if (palette.file) {
7899       pwlib.xhrLoad(PaintWeb.baseFolder + palette.file, this.cpalette_loaded);
7901       return true;
7903     } else if (palette.colors) {
7904       return this.cpalette_show(palette.colors);
7906     } else {
7907       return false;
7908     }
7909   };
7911   /**
7912    * The <code>onreadystatechange</code> event handler for the color palette 
7913    * XMLHttpRequest object.
7914    *
7915    * @private
7916    * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
7917    */
7918   this.cpalette_loaded = function (xhr) {
7919     if (!xhr || xhr.readyState !== 4) {
7920       return;
7921     }
7923     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
7924       alert(lang.failedColorPaletteLoad);
7925       return;
7926     }
7928     var colors = JSON.parse(xhr.responseText);
7929     xhr = null;
7930     _self.cpalette_show(colors);
7931   };
7933   /**
7934    * Show a color palette. This method adds all the colors in the DOM as 
7935    * individual anchor elements which users can click on.
7936    *
7937    * @private
7938    *
7939    * @param {Array} colors The array which holds each color in the palette.
7940    *
7941    * @returns {Boolean} True if the operation was successful, or false if not.
7942    */
7943   this.cpalette_show = function (colors) {
7944     if (!colors || !(colors instanceof Array)) {
7945       return false;
7946     }
7948     var color, anchor, rgbValue,
7949         frag = doc.createDocumentFragment(),
7950         dest = this.elems.cpaletteOutput;
7952     dest.style.display = 'none';
7953     while (dest.hasChildNodes()) {
7954       dest.removeChild(dest.firstChild);
7955     }
7957     for (var i = 0, n = colors.length; i < n; i++) {
7958       color = colors[i];
7960       // Do not allow values higher than 1.
7961       color[0] = MathMin(1, color[0]);
7962       color[1] = MathMin(1, color[1]);
7963       color[2] = MathMin(1, color[2]);
7965       rgbValue = 'rgb(' + MathRound(color[0] * 255) + ',' +
7966           MathRound(color[1] * 255) + ',' +
7967           MathRound(color[2] * 255) + ')';
7969       anchor = doc.createElement('a');
7970       anchor.href = '#';
7971       anchor._color = color;
7972       anchor.style.backgroundColor = rgbValue;
7973       anchor.appendChild(doc.createTextNode(rgbValue));
7975       frag.appendChild(anchor);
7976     }
7978     dest.appendChild(frag);
7979     dest.style.display = 'block';
7981     colors = frag = null;
7983     return true;
7984   };
7986   /**
7987    * The <code>click</code> event handler for colors in the color palette list.  
7988    * This event handler is also used for the "old color" element. This method 
7989    * updates the color mixer to use the color the user picked.
7990    *
7991    * @private
7992    * @param {Event} ev The DOM Event object.
7993    */
7994   this.ev_click_color = function (ev) {
7995     var color = ev.target._color;
7996     if (!color) {
7997       return;
7998     }
8000     ev.preventDefault();
8002     _self.color.red   = color[0];
8003     _self.color.green = color[1];
8004     _self.color.blue  = color[2];
8006     if (typeof(color[3]) !== 'undefined') {
8007       _self.color.alpha = color[3];
8008     }
8010     _self.update_color('rgb');
8011   };
8013   /**
8014    * Recalculate the dimensions and coordinates for the slider and for the color 
8015    * space visualisation within the Canvas element.
8016    *
8017    * <p>This method is an event handler for the {@link 
8018    * pwlib.appEvent.canvasSizeChange} application event.
8019    *
8020    * @private
8021    */
8022   this.update_dimensions = function () {
8023     if (resScale === app.resolution.scale) {
8024       return;
8025     }
8027     resScale = app.resolution.scale;
8029     var canvas  = _self.context2d.canvas,
8030         width   = canvas.width,
8031         height  = canvas.height,
8032         sWidth  = width  / resScale,
8033         sHeight = height / resScale,
8034         style;
8036     _self.sliderWidth   = MathRound(width * config.sliderWidth);
8037     _self.sliderHeight  = height - 1;
8038     _self.sliderSpacing = MathRound(width * config.sliderSpacing);
8039     _self.sliderX       = width - _self.sliderWidth - 2;
8040     _self.chartWidth    = _self.sliderX - _self.sliderSpacing;
8041     _self.chartHeight   = height;
8043     style = _self.elems.controls.style;
8044     style.width  = sWidth  + 'px';
8045     style.height = sHeight + 'px';
8047     style = _self.elems.slider.style;
8048     style.width = (_self.sliderWidth / resScale) + 'px';
8049     style.left  = (_self.sliderX     / resScale) + 'px';
8051     style = canvas.style;
8052     style.width  = sWidth  + 'px';
8053     style.height = sHeight + 'px';
8055     if (_self.panel.state !== _self.panel.STATE_HIDDEN) {
8056       _self.update_canvas();
8057     }
8058   };
8060   /**
8061    * Calculate the product of two matrices.
8062    *
8063    * <p>Matrices are one-dimensional arrays of the form <code>[a0, a1, a2, ..., 
8064    * b0, b1, b2, ...]</code> where each element from the matrix is given in 
8065    * order, from the left to the right, row by row from the top to the bottom.
8066    *
8067    * @param {Array} a The first matrix must be one row and three columns.
8068    * @param {Array} b The second matrix must be three rows and three columns.
8069    *
8070    * @returns {Array} The matrix product, one row and three columns.
8071    */
8072   // Note: for obvious reasons, this method is not a full-fledged matrix product 
8073   // calculator. It's as simple as possible - fitting only the very specific 
8074   // needs of the color mixer.
8075   this.calc_m1x3 = function (a, b) {
8076     if (!(a instanceof Array) || !(b instanceof Array)) {
8077       return false;
8078     } else {
8079       return [
8080         a[0] * b[0] + a[1] * b[3] + a[2] * b[6], // x
8081         a[0] * b[1] + a[1] * b[4] + a[2] * b[7], // y
8082         a[0] * b[2] + a[1] * b[5] + a[2] * b[8]  // z
8083       ];
8084     }
8085   };
8087   /**
8088    * Calculate the matrix inverse.
8089    *
8090    * <p>Matrices are one-dimensional arrays of the form <code>[a0, a1, a2, ..., 
8091    * b0, b1, b2, ...]</code> where each element from the matrix is given in 
8092    * order, from the left to the right, row by row from the top to the bottom.
8093    *
8094    * @private
8095    *
8096    * @param {Array} m The square matrix which must have three rows and three 
8097    * columns.
8098    *
8099    * @returns {Array|false} The computed matrix inverse, or false if the matrix 
8100    * determinant was 0 - the given matrix is not invertible.
8101    */
8102   // Note: for obvious reasons, this method is not a full-fledged matrix inverse 
8103   // calculator. It's as simple as possible - fitting only the very specific 
8104   // needs of the color mixer.
8105   this.calc_m3inv = function (m) {
8106     if (!(m instanceof Array)) {
8107       return false;
8108     }
8110     var d = (m[0]*m[4]*m[8] + m[1]*m[5]*m[6] + m[2]*m[3]*m[7]) -
8111             (m[2]*m[4]*m[6] + m[5]*m[7]*m[0] + m[8]*m[1]*m[3]);
8113     // Matrix determinant is 0: the matrix is not invertible.
8114     if (d === 0) {
8115       return false;
8116     }
8118     var i = [
8119        m[4]*m[8] - m[5]*m[7], -m[3]*m[8] + m[5]*m[6],  m[3]*m[7] - m[4]*m[6],
8120       -m[1]*m[8] + m[2]*m[7],  m[0]*m[8] - m[2]*m[6], -m[0]*m[7] + m[1]*m[6],
8121        m[1]*m[5] - m[2]*m[4], -m[0]*m[5] + m[2]*m[3],  m[0]*m[4] - m[1]*m[3]
8122     ];
8124     i = [1/d * i[0], 1/d * i[3], 1/d * i[6],
8125          1/d * i[1], 1/d * i[4], 1/d * i[7],
8126          1/d * i[2], 1/d * i[5], 1/d * i[8]];
8128     return i;
8129   };
8131   /**
8132    * The <code>change</code> event handler for the Color Mixer inputs of 
8133    * type=radio. This method allows users to change the active color key - used 
8134    * for the color space visualisation.
8135    * @private
8136    */
8137   this.ev_change_ckey_active = function () {
8138     if (this.value && this.value !== _self.ckey_active) {
8139       _self.update_ckey_active(this.value);
8140     }
8141   };
8143   /**
8144    * Update the active color key. This method updates the Canvas accordingly.
8145    *
8146    * @private
8147    *
8148    * @param {String} ckey The color key you want to be active.
8149    * @param {Boolean} [only_vars] Tells if you want only the variables to be 
8150    * updated - no Canvas updates. This is true only during the Color Mixer 
8151    * initialization.
8152    *
8153    * @return {Boolean} True if the operation was successful, or false if not.
8154    */
8155   this.update_ckey_active = function (ckey, only_vars) {
8156     if (!_self.inputs[ckey]) {
8157       return false;
8158     }
8160     _self.ckey_active = ckey;
8162     var  adjoint = [], group = _self.ckey_grouping[ckey];
8164     // Determine the adjoint color keys. For example, if red is active, then adjoint = ['green', 'blue'].
8165     for (var i in _self.ckey_grouping) {
8166       if (_self.ckey_grouping[i] === group && i !== ckey) {
8167         adjoint.push(i);
8168       }
8169     }
8171     _self.ckey_active_group  = group;
8172     _self.ckey_adjoint       = adjoint;
8174     if (!only_vars) {
8175       if (_self.panelSelector.tabId !== 'mixer') {
8176         _self.update_canvas_needed = true;
8177         _self.panelSelector.tabActivate('mixer');
8178       } else {
8179         _self.update_canvas();
8180       }
8182       if (_self.panelInputs.tabId !== group) {
8183         _self.panelInputs.tabActivate(group);
8184       }
8185     }
8187     return true;
8188   };
8190   /**
8191    * Show the Color Mixer.
8192    *
8193    * @param {Object} target The target input object.
8194    *
8195    * @param {Object} color The color you want to set before the Color Mixer is 
8196    * shown. The object must have four properties: <var>red</var>, 
8197    * <var>green</var>, <var>blue</var> and <var>alpha</var>. All the values must 
8198    * be between 0 and 1. This color becomes the "active color" and the "old 
8199    * color".
8200    *
8201    * @see this.targetInput for more information about the <var>target</var> 
8202    * object.
8203    */
8204   this.show = function (target, color) {
8205       var styleActive = _self.elems.colorActive.style,
8206           colorOld    = _self.elems.colorOld,
8207           styleOld    = colorOld.style;
8209     if (target) {
8210       if (_self.targetInput) {
8211         _self.targetInput.hide();
8212       }
8214       _self.targetInput = target;
8215       _self.targetInput.show();
8216     }
8218     if (color) {
8219       _self.color.red   = color.red;
8220       _self.color.green = color.green;
8221       _self.color.blue  = color.blue;
8222       _self.color.alpha = color.alpha;
8224       _self.update_color('rgb');
8226       styleOld.backgroundColor = styleActive.backgroundColor;
8227       styleOld.opacity = styleActive.opacity;
8228       colorOld._color = [color.red, color.green, color.blue, color.alpha];
8229     }
8231     _self.panel.show();
8232   };
8234   /**
8235    * Hide the Color Mixer floating panel. This method invokes the 
8236    * <code>hide()</code> method provided by the target input.
8237    */
8238   this.hide = function () {
8239     _self.panel.hide();
8240     _self.ev_canvas_mode = false;
8241   };
8243   /**
8244    * The <code>guiFloatingPanelStateChange</code> event handler for the Color 
8245    * Mixer panel. This method ensures the Color Mixer is properly closed.
8246    *
8247    * @param {pwlib.appEvent.guiFloatingPanelStateChange} ev The application 
8248    * event object.
8249    */
8250   this.ev_panel_stateChange = function (ev) {
8251     if (ev.state === ev.STATE_HIDDEN) {
8252       if (_self.targetInput) {
8253         _self.targetInput.hide();
8254         _self.targetInput = null;
8255       }
8256       _self.ev_canvas_mode = false;
8257     }
8258   };
8260   /**
8261    * The <code>input</code> and <code>change</code> event handler for all the 
8262    * Color Mixer inputs.
8263    * @private
8264    */
8265   this.ev_input_change = function () {
8266     if (!this._ckey) {
8267       return;
8268     }
8270     // Validate and restrict the possible values.
8271     // If the input is unchanged, or if the new value is invalid, the function 
8272     // stops.
8273     // The hexadecimal input is checked with a simple regular expression.
8275     if ((this._ckey === 'hex' && !/^\#[a-f0-9]{6}$/i.test(this.value))) {
8276       return;
8277     }
8279     if (this.getAttribute('type') === 'number') {
8280       var val = parseInt(this.value),
8281           min = this.getAttribute('min'),
8282           max = this.getAttribute('max');
8284       if (isNaN(val)) {
8285         val = min;
8286       }
8288       if (val < min) {
8289         val = min;
8290       } else if (val > max) {
8291         val = max;
8292       }
8294       if (val != this.value) {
8295         this.value = val;
8296       }
8297     }
8299     // Update the internal color value.
8300     if (this._ckey === 'hex') {
8301       _self.color[this._ckey] = this.value;
8302     } else if (_self.ckey_grouping[this._ckey] === 'lab') {
8303       _self.color[this._ckey] = parseInt(this.value);
8304     } else {
8305       _self.color[this._ckey] = parseInt(this.value) 
8306         / config.inputValues[this._ckey][1];
8307     }
8309     _self.update_color(this._ckey);
8310   };
8312   /**
8313    * Update the current color. Once a color value is updated, this method is 
8314    * called to keep the rest of the color mixer in sync: for example, when a RGB 
8315    * value is updated, it needs to be converted to HSV, CMYK and all of the 
8316    * other formats. Additionally, this method updates the color preview, the 
8317    * controls on the Canvas and the input values.
8318    *
8319    * <p>You need to call this function whenever you update the color manually.
8320    *
8321    * @param {String} ckey The color key that was updated.
8322    */
8323   this.update_color = function (ckey) {
8324     var group = _self.ckey_grouping[ckey] || ckey;
8326     switch (group) {
8327       case 'rgb':
8328         _self.rgb2hsv();
8329         _self.rgb2hex();
8330         _self.rgb2lab();
8331         _self.rgb2cmyk();
8332         break;
8334       case 'hsv':
8335         _self.hsv2rgb();
8336         _self.rgb2hex();
8337         _self.rgb2lab();
8338         _self.rgb2cmyk();
8339         break;
8341       case 'hex':
8342         _self.hex2rgb();
8343         _self.rgb2hsv();
8344         _self.rgb2lab();
8345         _self.rgb2cmyk();
8346         break;
8348       case 'lab':
8349         _self.lab2rgb();
8350         _self.rgb2hsv();
8351         _self.rgb2hex();
8352         _self.rgb2cmyk();
8353         break;
8355       case 'cmyk':
8356         _self.cmyk2rgb();
8357         _self.rgb2lab();
8358         _self.rgb2hsv();
8359         _self.rgb2hex();
8360     }
8362     _self.update_preview();
8363     _self.update_inputs();
8365     if (ckey !== 'alpha') {
8366       _self.update_canvas(ckey);
8367     }
8368   };
8370   /**
8371    * Update the color preview.
8372    * @private
8373    */
8374   this.update_preview = function () {
8375     var red   = MathRound(_self.color.red   * 255),
8376         green = MathRound(_self.color.green * 255),
8377         blue  = MathRound(_self.color.blue  * 255),
8378         style = _self.elems.colorActive.style;
8380     style.backgroundColor = 'rgb(' + red + ',' + green + ',' + blue + ')';
8381     style.opacity = _self.color.alpha;
8382   };
8384   /**
8385    * Update the color inputs. This method takes the internal color values and 
8386    * shows them in the DOM input elements.
8387    * @private
8388    */
8389   this.update_inputs = function () {
8390     var input;
8391     for (var i in _self.inputs) {
8392       input = _self.inputs[i];
8393       input._old_value = input.value;
8394       if (input._ckey === 'hex') {
8395         input.value = _self.color[i];
8396       } else if (_self.ckey_grouping[input._ckey] === 'lab') {
8397         input.value = MathRound(_self.color[i]);
8398       } else {
8399         input.value = MathRound(_self.color[i] * config.inputValues[i][1]);
8400       }
8401     }
8402   };
8404   /**
8405    * Convert RGB to CMYK. This uses the current color RGB values and updates the 
8406    * CMYK values accordingly.
8407    * @private
8408    */
8409   // Quote from Wikipedia:
8410   // "Since RGB and CMYK spaces are both device-dependent spaces, there is no 
8411   // simple or general conversion formula that converts between them.  
8412   // Conversions are generally done through color management systems, using 
8413   // color profiles that describe the spaces being converted. Nevertheless, the 
8414   // conversions cannot be exact, since these spaces have very different 
8415   // gamuts."
8416   // Translation: this is just a simple RGB to CMYK conversion function.
8417   this.rgb2cmyk = function () {
8418     var color = _self.color,
8419         cyan, magenta, yellow, black,
8420         red   = color.red,
8421         green = color.green,
8422         blue  = color.blue;
8424     cyan    = 1 - red;
8425     magenta = 1 - green;
8426     yellow  = 1 - blue;
8428     black = MathMin(cyan, magenta, yellow, 1);
8430     if (black === 1) {
8431       cyan = magenta = yellow = 0;
8432     } else {
8433       var w = 1 - black;
8434       cyan    = (cyan    - black) / w;
8435       magenta = (magenta - black) / w;
8436       yellow  = (yellow  - black) / w;
8437     }
8439     color.cyan    = cyan;
8440     color.magenta = magenta;
8441     color.yellow  = yellow;
8442     color.black   = black;
8443   };
8445   /**
8446    * Convert CMYK to RGB (internally).
8447    * @private
8448    */
8449   this.cmyk2rgb = function () {
8450     var color = _self.color,
8451         w = 1 - color.black;
8453     color.red   = 1 - color.cyan    * w - color.black;
8454     color.green = 1 - color.magenta * w - color.black;
8455     color.blue  = 1 - color.yellow  * w - color.black;
8456   };
8458   /**
8459    * Convert RGB to HSV (internally).
8460    * @private
8461    */
8462   this.rgb2hsv = function () {
8463     var hue, sat, val, // HSV
8464         red   = _self.color.red,
8465         green = _self.color.green,
8466         blue  = _self.color.blue,
8467         min   = MathMin(red, green, blue),
8468         max   = MathMax(red, green, blue),
8469         delta = max - min,
8470         val   = max;
8472     // This is gray (red==green==blue)
8473     if (delta === 0) {
8474       hue = sat = 0;
8475     } else {
8476       sat = delta / max;
8478       if (max === red) {
8479         hue = (green -  blue) / delta;
8480       } else if (max === green) {
8481         hue = (blue  -   red) / delta + 2;
8482       } else if (max ===  blue) {
8483         hue = (red   - green) / delta + 4;
8484       }
8486       hue /= 6;
8487       if (hue < 0) {
8488         hue += 1;
8489       }
8490     }
8492     _self.color.hue = hue;
8493     _self.color.sat = sat;
8494     _self.color.val = val;
8495   };
8497   /**
8498    * Convert HSV to RGB.
8499    *
8500    * @private
8501    *
8502    * @param {Boolean} [no_update] Tells the function to not update the internal 
8503    * RGB color values.
8504    * @param {Array} [hsv] The array holding the HSV values you want to convert 
8505    * to RGB. This array must have three elements ordered as: <var>hue</var>, 
8506    * <var>saturation</var> and <var>value</var> - all between 0 and 1. If you do 
8507    * not provide the array, then the internal HSV color values are used.
8508    *
8509    * @returns {Array} The RGB values converted from HSV. The array has three 
8510    * elements ordered as: <var>red</var>, <var>green</var> and <var>blue</var> 
8511    * - all with values between 0 and 1.
8512    */
8513   this.hsv2rgb = function (no_update, hsv) {
8514     var color = _self.color,
8515         red, green, blue, hue, sat, val;
8517     // Use custom HSV values or the current color.
8518     if (hsv) {
8519       hue = hsv[0];
8520       sat = hsv[1];
8521       val = hsv[2];
8522     } else {
8523       hue = color.hue,
8524       sat = color.sat,
8525       val = color.val;
8526     }
8528     // achromatic (grey)
8529     if (sat === 0) {
8530       red = green = blue = val;
8531     } else {
8532       var h = hue * 6;
8533       var i = MathFloor(h);
8534       var t1 = val * ( 1 - sat ),
8535           t2 = val * ( 1 - sat * ( h - i ) ),
8536           t3 = val * ( 1 - sat * ( 1 - (h - i) ) );
8538       if (i === 0 || i === 6) { //   0° Red
8539         red = val;  green =  t3;  blue =  t1;
8540       } else if (i === 1) {    //  60° Yellow
8541         red =  t2;  green = val;  blue =  t1;
8542       } else if (i === 2) {    // 120° Green
8543         red =  t1;  green = val;  blue =  t3;
8544       } else if (i === 3) {    // 180° Cyan
8545         red =  t1;  green =  t2;  blue = val;
8546       } else if (i === 4) {    // 240° Blue
8547         red =  t3;  green =  t1;  blue = val;
8548       } else if (i === 5) {    // 300° Magenta
8549         red = val;  green =  t1;  blue =  t2;
8550       }
8551     }
8553     if (!no_update) {
8554       color.red   = red;
8555       color.green = green;
8556       color.blue  = blue;
8557     }
8559     return [red, green, blue];
8560   };
8562   /**
8563    * Convert RGB to hexadecimal representation (internally).
8564    * @private
8565    */
8566   this.rgb2hex = function () {
8567     var hex = '#', rgb = ['red', 'green', 'blue'], i, val,
8568         color = _self.color;
8570     for (i = 0; i < 3; i++) {
8571       val = MathRound(color[rgb[i]] * 255).toString(16);
8572       if (val.length === 1) {
8573         val = '0' + val;
8574       }
8575       hex += val;
8576     }
8578     color.hex = hex;
8579   };
8581   /**
8582    * Convert the hexadecimal representation of color to RGB values (internally).
8583    * @private
8584    */
8585   this.hex2rgb = function () {
8586     var rgb = ['red', 'green', 'blue'], i, val,
8587         color = _self.color,
8588         hex   = color.hex;
8590     hex = hex.substr(1);
8591     if (hex.length !== 6) {
8592       return;
8593     }
8595     for (i = 0; i < 3; i++) {
8596       val = hex.substr(i*2, 2);
8597       color[rgb[i]] = parseInt(val, 16)/255;
8598     }
8599   };
8601   /**
8602    * Convert RGB to CIE Lab (internally).
8603    * @private
8604    */
8605   this.rgb2lab = function () {
8606     var color = _self.color,
8607         lab   = _self.xyz2lab(_self.rgb2xyz([color.red, color.green, 
8608               color.blue]));
8610     color.cie_l = lab[0];
8611     color.cie_a = lab[1];
8612     color.cie_b = lab[2];
8613   };
8615   /**
8616    * Convert CIE Lab values to RGB values (internally).
8617    * @private
8618    */
8619   this.lab2rgb = function () {
8620     var color = _self.color,
8621         rgb   = _self.xyz2rgb(_self.lab2xyz(color.cie_l, color.cie_a, 
8622               color.cie_b));
8624     color.red   = rgb[0];
8625     color.green = rgb[1];
8626     color.blue  = rgb[2];
8627   };
8629   /**
8630    * Convert XYZ color values into CIE Lab values.
8631    *
8632    * @private
8633    *
8634    * @param {Array} xyz The array holding the XYZ color values in order: 
8635    * <var>X</var>, <var>Y</var> and <var>Z</var>.
8636    *
8637    * @returns {Array} An array holding the CIE Lab values in order: 
8638    * <var>L</var>, <var>a</var> and <var>b</var>.
8639    */
8640   this.xyz2lab = function (xyz) {
8641     var cfg = config.lab,
8643         // 216/24389 or (6/29)^3 (both = 0.008856...)
8644         e = 216/24389,
8646         // 903.296296...
8647         k = 24389/27;
8649     xyz[0] /= cfg.w_x;
8650     xyz[1] /= cfg.w_y;
8651     xyz[2] /= cfg.w_z;
8653     if (xyz[0] > e) {
8654       xyz[0] = MathPow(xyz[0], 1/3);
8655     } else {
8656       xyz[0] = (k*xyz[0] + 16)/116;
8657     }
8659     if (xyz[1] > e) {
8660       xyz[1] = MathPow(xyz[1], 1/3);
8661     } else {
8662       xyz[1] = (k*xyz[1] + 16)/116;
8663     }
8665     if (xyz[2] > e) {
8666       xyz[2] = MathPow(xyz[2], 1/3);
8667     } else {
8668       xyz[2] = (k*xyz[2] + 16)/116;
8669     }
8671     var cie_l = 116 *  xyz[1] - 16,
8672         cie_a = 500 * (xyz[0] -  xyz[1]),
8673         cie_b = 200 * (xyz[1] -  xyz[2]);
8675     return [cie_l, cie_a, cie_b];
8676   };
8678   /**
8679    * Convert CIE Lab values to XYZ color values.
8680    *
8681    * @private
8682    *
8683    * @param {Number} cie_l The color lightness value.
8684    * @param {Number} cie_a The a* color opponent.
8685    * @param {Number} cie_b The b* color opponent.
8686    *
8687    * @returns {Array} An array holding the XYZ color values in order: 
8688    * <var>X</var>, <var>Y</var> and <var>Z</var>.
8689    */
8690   this.lab2xyz = function (cie_l, cie_a, cie_b) {
8691     var y = (cie_l + 16) / 116,
8692         x = y + cie_a / 500,
8693         z = y - cie_b / 200,
8695         // 0.206896551...
8696         e = 6/29,
8698         // 7.787037...
8699         k = 1/3 * MathPow(29/6, 2),
8701         // 0.137931...
8702         t = 16/116,
8703         cfg = config.lab;
8705     if (x > e) {
8706       x = MathPow(x, 3);
8707     } else {
8708       x = (x - t) / k;
8709     }
8711     if (y > e) {
8712       y = MathPow(y, 3);
8713     } else {
8714       y = (y - t) / k;
8715     }
8717     if (z > e) {
8718       z = MathPow(z, 3);
8719     } else {
8720       z = (z - t) / k;
8721     }
8723     x *= cfg.w_x;
8724     y *= cfg.w_y;
8725     z *= cfg.w_z;
8727     return [x, y, z];
8728   };
8730   /**
8731    * Convert XYZ color values to RGB.
8732    *
8733    * @private
8734    *
8735    * @param {Array} xyz The array holding the XYZ color values in order: 
8736    * <var>X</var>, <var>Y</var> and <var>Z</var>
8737    *
8738    * @returns {Array} An array holding the RGB values in order: <var>red</var>, 
8739    * <var>green</var> and <var>blue</var>.
8740    */
8741   this.xyz2rgb = function (xyz) {
8742     var rgb = _self.calc_m1x3(xyz, config.lab.m_i);
8744     if (rgb[0] > 0.0031308) {
8745       rgb[0] = 1.055 * MathPow(rgb[0], 1 / 2.4) - 0.055;
8746     } else {
8747       rgb[0] *= 12.9232;
8748     }
8750     if (rgb[1] > 0.0031308) {
8751       rgb[1] = 1.055 * MathPow(rgb[1], 1 / 2.4) - 0.055;
8752     } else {
8753       rgb[1] *= 12.9232;
8754     }
8756     if (rgb[2] > 0.0031308) {
8757       rgb[2] = 1.055 * MathPow(rgb[2], 1 / 2.4) - 0.055;
8758     } else {
8759       rgb[2] *= 12.9232;
8760     }
8762     if (rgb[0] < 0) {
8763       rgb[0] = 0;
8764     } else if (rgb[0] > 1) {
8765       rgb[0] = 1;
8766     }
8768     if (rgb[1] < 0) {
8769       rgb[1] = 0;
8770     } else if (rgb[1] > 1) {
8771       rgb[1] = 1;
8772     }
8774     if (rgb[2] < 0) {
8775       rgb[2] = 0;
8776     } else if (rgb[2] > 1) {
8777       rgb[2] = 1;
8778     }
8780     return rgb;
8781   };
8783   /**
8784    * Convert RGB values to XYZ color values.
8785    *
8786    * @private
8787    *
8788    * @param {Array} rgb The array holding the RGB values in order: 
8789    * <var>red</var>, <var>green</var> and <var>blue</var>.
8790    *
8791    * @returns {Array} An array holding the XYZ color values in order: 
8792    * <var>X</var>, <var>Y</var> and <var>Z</var>.
8793    */
8794   this.rgb2xyz = function (rgb) {
8795     if (rgb[0] > 0.04045) {
8796       rgb[0] = MathPow(( rgb[0] + 0.055 ) / 1.055, 2.4);
8797     } else {
8798       rgb[0] /= 12.9232;
8799     }
8801     if (rgb[1] > 0.04045) {
8802       rgb[1] = MathPow(( rgb[1] + 0.055 ) / 1.055, 2.4);
8803     } else {
8804       rgb[1] /= 12.9232;
8805     }
8807     if (rgb[2] > 0.04045) {
8808       rgb[2] = MathPow(( rgb[2] + 0.055 ) / 1.055, 2.4);
8809     } else {
8810       rgb[2] /= 12.9232;
8811     }
8813     return _self.calc_m1x3(rgb, config.lab.m);
8814   };
8816   /**
8817    * Update the color space visualisation. This method updates the color chart 
8818    * and/or the color slider, and the associated controls, each as needed when 
8819    * a color key is updated.
8820    *
8821    * @private
8822    *
8823    * @param {String} updated_ckey The color key that was updated.
8824    * @param {Boolean} [force=false] Tells the function to force an update. The 
8825    * Canvas is not updated when the color mixer panel is not visible.
8826    *
8827    * @returns {Boolean} If the operation was successful, or false if not.
8828    */
8829   this.update_canvas = function (updated_ckey, force) {
8830     if (_self.panelSelector.tabId !== 'mixer' && !force) {
8831       _self.update_canvas_needed = true;
8832       return true;
8833     }
8835     _self.update_canvas_needed = false;
8837     var slider  = _self.elems.slider.style,
8838         chart   = _self.elems.chartDot.style,
8839         color   = _self.color,
8840         ckey    = _self.ckey_active,
8841         group   = _self.ckey_active_group,
8842         adjoint = _self.ckey_adjoint,
8843         width   = _self.chartWidth  / resScale,
8844         height  = _self.chartHeight / resScale,
8845         mx, my, sy;
8847     // Update the slider which shows the position of the active ckey.
8848     if (updated_ckey !== adjoint[0] && updated_ckey !== adjoint[1] && 
8849         _self.ev_canvas_mode !== 'chart') {
8850       if (group === 'lab') {
8851         sy = (color[ckey] - config.inputValues[ckey][0]) / _self.abs_max[ckey];
8852       } else {
8853         sy = color[ckey];
8854       }
8856       if (ckey !== 'hue' && group !== 'lab') {
8857         sy = 1 - sy;
8858       }
8860       slider.top = MathRound(sy * height) + 'px';
8861     }
8863     // Update the chart dot.
8864     if (updated_ckey !== ckey) {
8865       if (group === 'lab') {
8866         mx = (color[adjoint[0]] - config.inputValues[adjoint[0]][0]) 
8867           / _self.abs_max[adjoint[0]];
8868         my = (color[adjoint[1]] - config.inputValues[adjoint[1]][0]) 
8869           / _self.abs_max[adjoint[1]];
8870       } else {
8871         mx = color[adjoint[0]];
8872         my = 1 - color[adjoint[1]];
8873       }
8875       chart.top  = MathRound(my * height) + 'px';
8876       chart.left = MathRound(mx *  width) + 'px';
8877     }
8879     if (!_self.draw_chart(updated_ckey) || !_self.draw_slider(updated_ckey)) {
8880       return false;
8881     } else {
8882       return true;
8883     }
8884   };
8886   /**
8887    * The mouse events handler for the Canvas controls. This method determines 
8888    * the region the user is using, and it also updates the color values for the 
8889    * active color key. The Canvas and all the inputs in the color mixer are 
8890    * updated as needed.
8891    *
8892    * @private
8893    * @param {Event} ev The DOM Event object.
8894    */
8895   this.ev_canvas = function (ev) {
8896     ev.preventDefault();
8898     // Initialize color picking only on mousedown.
8899     if (ev.type === 'mousedown' && !_self.ev_canvas_mode) {
8900       _self.ev_canvas_mode = true;
8901       doc.addEventListener('mouseup', _self.ev_canvas, false);
8902     }
8904     if (!_self.ev_canvas_mode) {
8905       return false;
8906     }
8908     // The mouseup event stops the effect of any further mousemove events.
8909     if (ev.type === 'mouseup') {
8910       _self.ev_canvas_mode = false;
8911       doc.removeEventListener('mouseup', _self.ev_canvas, false);
8912     }
8914     var elems = _self.elems;
8916     // If the user is on top of the 'controls' element, determine the mouse coordinates and the 'mode' for this function: the user is either working with the slider, or he/she is working with the color chart itself.
8917     if (ev.target === elems.controls) {
8918       var mx, my,
8919           width  = _self.context2d.canvas.width,
8920           height = _self.context2d.canvas.height;
8922       // Get the mouse position, relative to the event target.
8923       if (ev.layerX || ev.layerX === 0) { // Firefox
8924         mx = ev.layerX * resScale;
8925         my = ev.layerY * resScale;
8926       } else if (ev.offsetX || ev.offsetX === 0) { // Opera
8927         mx = ev.offsetX * resScale;
8928         my = ev.offsetY * resScale;
8929       }
8931       if (mx >= 0 && mx <= _self.chartWidth) {
8932         mode = 'chart';
8933       } else if (mx >= _self.sliderX && mx <= width) {
8934         mode = 'slider';
8935       }
8936     } else {
8937       // The user might have clicked on the chart dot, or on the slider graphic 
8938       // itself.
8939       // If yes, then determine the mode based on this.
8940       if (ev.target === elems.chartDot) {
8941         mode = 'chart';
8942       } else if (ev.target === elems.slider) {
8943         mode = 'slider';
8944       }
8945     }
8947     // Update the ev_canvas_mode value to include the mode name, if it's simply 
8948     // the true boolean.
8949     // This ensures that the continuous mouse movements do not go from one mode 
8950     // to another when the user moves out from the slider to the chart (and 
8951     // vice-versa).
8952     if (mode && _self.ev_canvas_mode === true) {
8953       _self.ev_canvas_mode = mode;
8954     }
8956     // Do not continue if the mode wasn't determined (the mouse is not on the 
8957     // slider, nor on the chart).
8958     // Also don't continue if the mouse is not in the same place (different 
8959     // mode).
8960     if (!mode || _self.ev_canvas_mode !== mode || ev.target !== elems.controls) 
8961     {
8962       return false;
8963     }
8965     var color = _self.color,
8966         val_x = mx / _self.chartWidth,
8967         val_y = my / height;
8969     if (mode === 'slider') {
8970       if (_self.ckey_active === 'hue') {
8971         color[_self.ckey_active] = val_y;
8972       } else if (_self.ckey_active_group === 'lab') {
8973         color[_self.ckey_active] = _self.abs_max[_self.ckey_active] * val_y 
8974           + config.inputValues[_self.ckey_active][0];
8975       } else {
8976         color[_self.ckey_active] = 1 - val_y;
8977       }
8979       return _self.update_color(_self.ckey_active);
8981     } else if (mode === 'chart') {
8982       if (val_x > 1) {
8983         return false;
8984       }
8986       if (_self.ckey_active_group === 'lab') {
8987         val_x = _self.abs_max[_self.ckey_adjoint[0]] * val_x 
8988           + config.inputValues[_self.ckey_adjoint[0]][0];
8989         val_y = _self.abs_max[_self.ckey_adjoint[1]] * val_y 
8990           + config.inputValues[_self.ckey_adjoint[1]][0];
8991       } else {
8992         val_y = 1 - val_y;
8993       }
8995       color[_self.ckey_adjoint[0]] = val_x;
8996       color[_self.ckey_adjoint[1]] = val_y;
8998       return _self.update_color(_self.ckey_active_group);
8999     }
9001     return false;
9002   };
9004   /**
9005    * Draw the color space visualisation.
9006    *
9007    * @private
9008    *
9009    * @param {String} updated_ckey The color key that was updated. This is used 
9010    * to determine if the Canvas needs to be updated or not.
9011    */
9012   this.draw_chart = function (updated_ckey) {
9013     var context = _self.context2d,
9014         gradient, color, opacity, i;
9016     if (updated_ckey === _self.ckey_adjoint[0] || updated_ckey === 
9017         _self.ckey_adjoint[1] || (_self.ev_canvas_mode === 'chart' && 
9018           updated_ckey === _self.ckey_active_group)) {
9019       return true;
9020     }
9022     var w = _self.chartWidth,
9023         h = _self.chartHeight;
9025     context.clearRect(0, 0, w, h);
9027     if (_self.ckey_active === 'sat') {
9028       // In saturation mode the user has the slider which allows him/her to 
9029       // change the saturation (hSv) of the current color.
9030       // The chart shows the hue spectrum on the X axis, while the Y axis gives 
9031       // the Value (hsV).
9033       if (_self.color.sat > 0) {
9034         // Draw the hue spectrum gradient on the X axis.
9035         gradient = context.createLinearGradient(0, 0, w, 0);
9036         for (i = 0; i <= 6; i++) {
9037           color = 'rgb(' + hueSpectrum[i][0] + ', ' +
9038               hueSpectrum[i][1] + ', ' +
9039               hueSpectrum[i][2] + ')';
9040           gradient.addColorStop(i * 1/6, color);
9041         }
9042         context.fillStyle = gradient;
9043         context.fillRect(0, 0, w, h);
9045         // Draw the gradient which darkens the hue spectrum on the Y axis.
9046         gradient = context.createLinearGradient(0, 0, 0, h);
9047         gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
9048         gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
9049         context.fillStyle = gradient;
9050         context.fillRect(0, 0, w, h);
9051       }
9053       if (_self.color.sat < 1) {
9054         // Draw the white to black gradient. This is used for creating the 
9055         // saturation effect. Lowering the saturation value makes the gradient 
9056         // more visible, hence the hue colors desaturate.
9057         opacity = 1 - _self.color.sat;
9058         gradient = context.createLinearGradient(0, 0, 0, h);
9059         gradient.addColorStop(0, 'rgba(255, 255, 255, ' + opacity + ')');
9060         gradient.addColorStop(1, 'rgba(  0,   0,   0, ' + opacity + ')');
9061         context.fillStyle = gradient;
9062         context.fillRect(0, 0, w, h);
9063       }
9065     } else if (_self.ckey_active === 'val') {
9066       // In value mode the user has the slider which allows him/her to change the value (hsV) of the current color.
9067       // The chart shows the hue spectrum on the X axis, while the Y axis gives the saturation (hSv).
9069       if (_self.color.val > 0) {
9070         // Draw the hue spectrum gradient on the X axis.
9071         gradient = context.createLinearGradient(0, 0, w, 0);
9072         for (i = 0; i <= 6; i++) {
9073           color = 'rgb(' + hueSpectrum[i][0] + ', ' +
9074             hueSpectrum[i][1] + ', ' +
9075             hueSpectrum[i][2] + ')';
9076           gradient.addColorStop(i * 1/6, color);
9077         }
9078         context.fillStyle = gradient;
9079         context.fillRect(0, 0, w, h);
9081         // Draw the gradient which lightens the hue spectrum on the Y axis.
9082         gradient = context.createLinearGradient(0, 0, 0, h);
9083         gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
9084         gradient.addColorStop(1, 'rgba(255, 255, 255, 1)');
9085         context.fillStyle = gradient;
9086         context.fillRect(0, 0, w, h);
9087       }
9089       if (_self.color.val < 1) {
9090         // Draw a solid black color on top. This is used for darkening the hue colors gradient when the user reduces the Value (hsV).
9091         context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) +')';
9092         context.fillRect(0, 0, w, h);
9093       }
9095     } else if (_self.ckey_active === 'hue') {
9096       // In hue mode the user has the slider which allows him/her to change the hue (Hsv) of the current color.
9097       // The chart shows the current color in the background. The X axis gives the saturation (hSv), and the Y axis gives the value (hsV).
9099       if (_self.color.sat === 1 && _self.color.val === 1) {
9100         color = [_self.color.red, _self.color.green, _self.color.blue];
9101       } else {
9102         // Determine the RGB values for the current color which has the same hue, but maximum saturation and value (hSV).
9103         color = _self.hsv2rgb(true, [_self.color.hue, 1, 1]);
9104       }
9105       for (i = 0; i < 3; i++) {
9106         color[i] = MathRound(color[i] * 255);
9107       }
9109       context.fillStyle = 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')';
9110       context.fillRect(0, 0, w, h);
9112       // Draw the white gradient for saturation (X axis, hSv).
9113       gradient = context.createLinearGradient(0, 0, w, 0);
9114       gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
9115       gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
9116       context.fillStyle = gradient;
9117       context.fillRect(0, 0, w, h);
9119       // Draw the black gradient for value (Y axis, hsV).
9120       gradient = context.createLinearGradient(0, 0, 0, h);
9121       gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
9122       gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
9123       context.fillStyle = gradient;
9124       context.fillRect(0, 0, w, h);
9126     } else if (_self.ckey_active_group === 'rgb') {
9127       // In any red/green/blue mode the background color becomes the one of the ckey_active. Say, for ckey_active=red the background color would be the current red value (green and blue are both set to 0).
9128       // On the X/Y axes the other two colors are shown. E.g. for red the X axis gives the green gradient, and the Y axis gives the blue gradient. The two gradients are drawn on top of the red background using a global composite operation (lighter) - to create the color addition effect.
9129       var color2, color3;
9131       color = {'red' : 0, 'green' : 0, 'blue' : 0};
9132       color[_self.ckey_active] = MathRound(_self.color[_self.ckey_active] 
9133           * 255);
9135       color2 = {'red' : 0, 'green' : 0, 'blue' : 0};
9136       color2[_self.ckey_adjoint[1]] = 255;
9138       color3 = {'red' : 0, 'green' : 0, 'blue' : 0};
9139       color3[_self.ckey_adjoint[0]] = 255;
9141       // The background.
9142       context.fillStyle = 'rgb(' + color.red + ',' + color.green + ',' + color.blue + ')';
9143       context.fillRect(0, 0, w, h);
9145       // This doesn't work in Opera 9.2 and older versions.
9146       var op = context.globalCompositeOperation;
9147       context.globalCompositeOperation = 'lighter';
9149       // The Y axis gradient.
9150       gradient = context.createLinearGradient(0, 0, 0, h);
9151       gradient.addColorStop(0, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 1)');
9152       gradient.addColorStop(1, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 0)');
9153       context.fillStyle = gradient;
9154       context.fillRect(0, 0, w, h);
9156       // The X axis gradient.
9157       gradient = context.createLinearGradient(0, 0, w, 0);
9158       gradient.addColorStop(0, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 0)');
9159       gradient.addColorStop(1, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 1)');
9160       context.fillStyle = gradient;
9161       context.fillRect(0, 0, w, h);
9163       context.globalCompositeOperation = op;
9165     } else if (_self.ckey_active_group === 'lab') {
9166       // The chart plots the CIE Lab colors. The non-active color keys give the X/Y axes. For example, if cie_l (lightness) is active, then the cie_a values give the X axis, and the Y axis is given by the values of cie_b.
9167       // The chart is drawn manually, pixel-by-pixel, due to the special way CIE Lab works. This is very slow in today's UAs.
9169       var imgd = false;
9171       if (context.createImageData) {
9172         imgd = context.createImageData(w, h);
9173       } else if (context.getImageData) {
9174         imgd = context.getImageData(0, 0, w, h);
9175       } else {
9176         imgd = {
9177           'width'  : w,
9178           'height' : h,
9179           'data'   : new Array(w*h*4)
9180         };
9181       }
9183       var pix = imgd.data,
9184           n = imgd.data.length - 1,
9185           i = -1, p = 0, inc_x, inc_y, xyz = [], rgb = [], cie_x, cie_y;
9187       cie_x = _self.ckey_adjoint[0];
9188       cie_y = _self.ckey_adjoint[1];
9190       color = {
9191         'cie_l' : _self.color.cie_l,
9192         'cie_a' : _self.color.cie_a,
9193         'cie_b' : _self.color.cie_b
9194       };
9196       inc_x = _self.abs_max[cie_x] / w;
9197       inc_y = _self.abs_max[cie_y] / h;
9199       color[cie_x] = config.inputValues[cie_x][0];
9200       color[cie_y] = config.inputValues[cie_y][0];
9202       while (i < n) {
9203         xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b);
9204         rgb = _self.xyz2rgb(xyz);
9206         pix[++i] = MathRound(rgb[0]*255);
9207         pix[++i] = MathRound(rgb[1]*255);
9208         pix[++i] = MathRound(rgb[2]*255);
9209         pix[++i] = 255;
9211         p++;
9212         color[cie_x] += inc_x;
9214         if ((p % w) === 0) {
9215           color[cie_x] = config.inputValues[cie_x][0];
9216           color[cie_y] += inc_y;
9217         }
9218       }
9220       context.putImageData(imgd, 0, 0);
9221     }
9223     return true;
9224   };
9226   /**
9227    * Draw the color slider on the Canvas element.
9228    *
9229    * @private
9230    *
9231    * @param {String} updated_ckey The color key that was updated. This is used 
9232    * to determine if the Canvas needs to be updated or not.
9233    */
9234   this.draw_slider = function (updated_ckey) {
9235     if (_self.ckey_active === updated_ckey) {
9236       return true;
9237     }
9239     var context  = _self.context2d,
9240         slider_w = _self.sliderWidth,
9241         slider_h = _self.sliderHeight,
9242         slider_x = _self.sliderX,
9243         slider_y = 0,
9244         gradient, color, i;
9246     gradient = context.createLinearGradient(slider_x, slider_y, slider_x, slider_h);
9248     if (_self.ckey_active === 'hue') {
9249       // Draw the hue spectrum gradient.
9250       for (i = 0; i <= 6; i++) {
9251         color = 'rgb(' + hueSpectrum[i][0] + ', ' +
9252             hueSpectrum[i][1] + ', ' +
9253             hueSpectrum[i][2] + ')';
9254         gradient.addColorStop(i * 1/6, color);
9255       }
9256       context.fillStyle = gradient;
9257       context.fillRect(slider_x, slider_y, slider_w, slider_h);
9259       if (_self.color.sat < 1) {
9260         context.fillStyle = 'rgba(255, 255, 255, ' +
9261           (1 - _self.color.sat) + ')';
9262         context.fillRect(slider_x, slider_y, slider_w, slider_h);
9263       }
9264       if (_self.color.val < 1) {
9265         context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) + ')';
9266         context.fillRect(slider_x, slider_y, slider_w, slider_h);
9267       }
9269     } else if (_self.ckey_active === 'sat') {
9270       // Draw the saturation gradient for the slider.
9271       // The start color is the current color with maximum saturation. The bottom gradient color is the same "color" without saturation.
9272       // The slider allows you to desaturate the current color.
9274       // Determine the RGB values for the current color which has the same hue and value (HsV), but maximum saturation (hSv).
9275       if (_self.color.sat === 1) {
9276         color = [_self.color.red, _self.color.green, _self.color.blue];
9277       } else {
9278         color = _self.hsv2rgb(true, [_self.color.hue, 1, _self.color.val]);
9279       }
9281       for (i = 0; i < 3; i++) {
9282         color[i] = MathRound(color[i] * 255);
9283       }
9285       var gray = MathRound(_self.color.val * 255);
9286       gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')');
9287       gradient.addColorStop(1, 'rgb(' + gray     + ', ' + gray     + ', ' + gray     + ')');
9288       context.fillStyle = gradient;
9289       context.fillRect(slider_x, slider_y, slider_w, slider_h);
9291     } else if (_self.ckey_active === 'val') {
9292       // Determine the RGB values for the current color which has the same hue and saturation, but maximum value (hsV).
9293       if (_self.color.val === 1) {
9294         color = [_self.color.red, _self.color.green, _self.color.blue];
9295       } else {
9296         color = _self.hsv2rgb(true, [_self.color.hue, _self.color.sat, 1]);
9297       }
9299       for (i = 0; i < 3; i++) {
9300         color[i] = MathRound(color[i] * 255);
9301       }
9303       gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')');
9304       gradient.addColorStop(1, 'rgb(0, 0, 0)');
9305       context.fillStyle = gradient;
9306       context.fillRect(slider_x, slider_y, slider_w, slider_h);
9308     } else if (_self.ckey_active_group === 'rgb') {
9309       var red   = MathRound(_self.color.red   * 255),
9310           green = MathRound(_self.color.green * 255),
9311           blue  = MathRound(_self.color.blue  * 255);
9313       color = {
9314         'red'   : red,
9315         'green' : green,
9316         'blue'  : blue
9317       };
9318       color[_self.ckey_active] = 255;
9320       var color2 = {
9321         'red'   : red,
9322         'green' : green,
9323         'blue'  : blue
9324       };
9325       color2[_self.ckey_active] = 0;
9327       gradient.addColorStop(0, 'rgb(' + color.red  + ',' + color.green  + ',' + color.blue  + ')');
9328       gradient.addColorStop(1, 'rgb(' + color2.red + ',' + color2.green + ',' + color2.blue + ')');
9329       context.fillStyle = gradient;
9330       context.fillRect(slider_x, slider_y, slider_w, slider_h);
9332     } else if (_self.ckey_active_group === 'lab') {
9333       // The slider shows a gradient with the current color key going from the minimum to the maximum value. The gradient is calculated pixel by pixel, due to the special way CIE Lab is defined.
9335       var imgd = false;
9337       if (context.createImageData) {
9338         imgd = context.createImageData(1, slider_h);
9339       } else if (context.getImageData) {
9340         imgd = context.getImageData(0, 0, 1, slider_h);
9341       } else {
9342         imgd = {
9343           'width'  : 1,
9344           'height' : slider_h,
9345           'data'   : new Array(slider_h*4)
9346         };
9347       }
9349       var pix = imgd.data,
9350           n = imgd.data.length - 1,
9351           ckey = _self.ckey_active,
9352           i = -1, inc, xyz, rgb;
9354       color = {
9355         'cie_l' : _self.color.cie_l,
9356         'cie_a' : _self.color.cie_a,
9357         'cie_b' : _self.color.cie_b
9358       };
9360       color[ckey] = config.inputValues[ckey][0];
9361       inc = _self.abs_max[ckey] / slider_h;
9363       while (i < n) {
9364         xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b);
9365         rgb = _self.xyz2rgb(xyz);
9366         pix[++i] = MathRound(rgb[0]*255);
9367         pix[++i] = MathRound(rgb[1]*255);
9368         pix[++i] = MathRound(rgb[2]*255);
9369         pix[++i] = 255;
9371         color[ckey] += inc;
9372       }
9374       for (i = 0; i <= slider_w; i++) {
9375         context.putImageData(imgd, slider_x+i, slider_y);
9376       }
9377     }
9379     context.strokeStyle = '#6d6d6d';
9380     context.strokeRect(slider_x, slider_y, slider_w, slider_h);
9382     return true;
9383   };
9386 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
9389  * Copyright (C) 2009 Mihai Şucan
9391  * This file is part of PaintWeb.
9393  * PaintWeb is free software: you can redistribute it and/or modify
9394  * it under the terms of the GNU General Public License as published by
9395  * the Free Software Foundation, either version 3 of the License, or
9396  * (at your option) any later version.
9398  * PaintWeb is distributed in the hope that it will be useful,
9399  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9400  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9401  * GNU General Public License for more details.
9403  * You should have received a copy of the GNU General Public License
9404  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
9406  * $URL: http://code.google.com/p/paintweb $
9407  * $Date: 2009-07-28 18:49:37 +0300 $
9408  */
9411  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
9412  * @fileOverview Holds the integration code for PaintWeb inside <a 
9413  * href="http://www.moodle.org">Moodle</a>.
9414  */
9417  * @class The Moodle extension for PaintWeb. This extension handles the Moodle 
9418  * integration inside the PaintWeb code.
9420  * @param {PaintWeb} app Reference to the main paint application object.
9421  */
9422 pwlib.extensions.moodle = function (app) {
9423   var _self    = this,
9424       appEvent = pwlib.appEvent,
9425       config   = app.config,
9426       gui      = app.gui,
9427       lang     = app.lang.moodle;
9429   // Holds properties related to Moodle.
9430   var moodle = {
9431     // Holds the URL of the image the user is saving.
9432     imageURL: null,
9434     // The class name for the element which holds the textarea buttons (toggle 
9435     // on/off).
9436     textareaButtons: 'textareaicons',
9438     // The image save handler script on the server-side. The path is relative to 
9439     // the PaintWeb base folder.
9440     imageSaveHandler: '../ext/moodle/imagesave.php'
9441   };
9443   /**
9444    * The <code>extensionRegister</code> event handler.
9445    *
9446    * @returns {Boolean} True if the extension initialized successfully, or false 
9447    * if not.
9448    */
9449   this.extensionRegister = function () {
9450     // Register application events.
9451     app.events.add('guiShow',   this.guiShow);
9452     app.events.add('guiHide',   this.guiHide);
9453     app.events.add('imageSave', this.imageSave);
9455     return true;
9456   };
9458   /**
9459    * The <code>extensionUnregister</code> event handler.
9460    */
9461   this.extensionUnregister = function () {
9462     return;
9463   };
9465   /**
9466    * The <code>imageSave</code> application event handler. When the user 
9467    * attempts to save an image, this extension handles the event by sending the 
9468    * image data to the Moodle server, to perform the actual save operation.
9469    *
9470    * @private
9471    * @param {pwlib.appEvent.imageSave} ev The application event object.
9472    */
9473   this.imageSave = function (ev) {
9474     if (!ev.dataURL) {
9475       return;
9476     }
9478     ev.preventDefault();
9480     moodle.imageURL = config.imageLoad.src;
9481     if (!moodle.imageURL || moodle.imageURL.substr(0, 5) === 'data:') {
9482       moodle.imageURL = '-';
9483     }
9485     if (!moodle.imageSaveHandler || config.moodleSaveMethod === 'dataURL') {
9486       app.events.dispatch(new appEvent.imageSaveResult(true, moodle.imageURL, 
9487             ev.dataURL));
9489     } else {
9490       var handlerURL = PaintWeb.baseFolder + moodle.imageSaveHandler,
9491           send       = 'url=' + encodeURIComponent(moodle.imageURL) +
9492                        '&dataURL=' + encodeURIComponent(ev.dataURL),
9493           headers    = {'Content-Type': 'application/x-www-form-urlencoded'};
9495       pwlib.xhrLoad(handlerURL, imageSaveReady, 'POST', send, headers);
9496     }
9497   };
9499   /**
9500    * The image save <code>onreadystatechange</code> event handler for the 
9501    * <code>XMLHttpRequest</code> which performs the image save. This function 
9502    * uses the reply to determine if the image save operation is successful or 
9503    * not.
9504    *
9505    * <p>The {@link pwlib.appEvent.imageSaveResult} application event is 
9506    * dispatched.
9507    *
9508    * <p>The server-side script must reply with a JSON object with the following 
9509    * properties:
9510    *
9511    * <ul>
9512    *   <li><var>successful</var> which tells if the image save operation was 
9513    *   successful or not;
9514    *
9515    *   <li><var>url</var> which must tell the same URL as the image we just 
9516    *   saved (sanity/security check);
9517    *
9518    *   <li><var>urlNew</var> is optional. This allows the server-side script to 
9519    *   change the image URL;
9520    *
9521    *   <li><var>errorMessage</var> is optional. When the image save was not 
9522    *   successful, an error message can be displayed.
9523    * </ul>
9524    *
9525    * @private
9526    * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
9527    */
9528   function imageSaveReady (xhr) {
9529     if (!xhr || xhr.readyState !== 4) {
9530       return;
9531     }
9533     var result = {successful: false, url: moodle.imageURL};
9535     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
9536       alert(lang.xhrRequestFailed);
9538       app.events.dispatch(new appEvent.imageSaveResult(false, result.url, null, 
9539             lang.xhrRequestFailed));
9541       return;
9542     }
9544     try {
9545       result = JSON.parse(xhr.responseText);
9546     } catch (err) {
9547       result.errorMessage = lang.jsonParseFailed + "\n" + err;
9548       alert(result.errorMessage);
9549     }
9551     if (result.successful) {
9552       if (result.url !== moodle.imageURL) {
9553         alert(pwlib.strf(lang.urlMismatch, {
9554                 url: moodle.imageURL,
9555                 urlServer: result.url || 'null'}));
9556       }
9557     } else {
9558       if (result.errorMessage) {
9559         alert(lang.imageSaveFailed + "\n" + result.errorMessage);
9560       } else {
9561         alert(lang.imageSaveFailed);
9562       }
9563     }
9565     app.events.dispatch(new appEvent.imageSaveResult(result.successful, 
9566           result.url, result.urlNew, result.errorMessage));
9567   };
9569   /**
9570    * The <code>guiShow</code> application event handler. When the PaintWeb GUI 
9571    * is shown, we must hide the textarea icons for the current textarea element, 
9572    * inside a Moodle page.
9573    * @private
9574    */
9575   this.guiShow = function () {
9576     var pNode = config.guiPlaceholder.parentNode,
9577         elem = pNode.getElementsByClassName(moodle.textareaButtons)[0];
9579     if (elem) {
9580       elem.style.display = 'none';
9581     }
9582   };
9584   /**
9585    * The <code>guiHide</code> application event handler. When the PaintWeb GUI 
9586    * is hidden, we must show again the textarea icons for the current textarea 
9587    * element, inside a Moodle page.
9588    * @private
9589    */
9590   this.guiHide = function () {
9591     var pNode = config.guiPlaceholder.parentNode,
9592         elem = pNode.getElementsByClassName(moodle.textareaButtons)[0];
9594     if (elem) {
9595       elem.style.display = '';
9596     }
9597   };
9600 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
9603  * Copyright (C) 2008, 2009 Mihai Şucan
9605  * This file is part of PaintWeb.
9607  * PaintWeb is free software: you can redistribute it and/or modify
9608  * it under the terms of the GNU General Public License as published by
9609  * the Free Software Foundation, either version 3 of the License, or
9610  * (at your option) any later version.
9612  * PaintWeb is distributed in the hope that it will be useful,
9613  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9614  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9615  * GNU General Public License for more details.
9617  * You should have received a copy of the GNU General Public License
9618  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
9620  * $URL: http://code.google.com/p/paintweb $
9621  * $Date: 2009-07-26 17:56:47 +0300 $
9622  */
9625  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
9626  * @fileOverview The default PaintWeb interface code.
9627  */
9630  * @class The default PaintWeb interface.
9632  * @param {PaintWeb} app Reference to the main paint application object.
9633  */
9634 pwlib.gui = function (app) {
9635   var _self     = this,
9636       config    = app.config,
9637       doc       = app.doc,
9638       lang      = app.lang,
9639       MathRound = Math.round,
9640       pwlib     = window.pwlib,
9641       appEvent  = pwlib.appEvent,
9642       win       = app.win;
9644   this.app = app;
9645   this.idPrefix = 'paintweb' + app.UID + '_',
9646   this.classPrefix = 'paintweb_';
9648   /**
9649    * Holds references to DOM elements.
9650    * @type Object
9651    */
9652   this.elems = {};
9654   /**
9655    * Holds references to input elements associated to the PaintWeb configuration 
9656    * properties.
9657    * @type Object
9658    */
9659   this.inputs = {};
9661   /**
9662    * Holds references to DOM elements associated to configuration values.
9663    * @type Object
9664    */
9665   this.inputValues = {};
9667   /**
9668    * Holds references to DOM elements associated to color configuration 
9669    * properties.
9670    *
9671    * @type Object
9672    * @see pwlib.guiColorInput
9673    */
9674   this.colorInputs = {};
9676   /**
9677    * Holds references to DOM elements associated to each tool registered in the 
9678    * current PaintWeb application instance.
9679    *
9680    * @private
9681    * @type Object
9682    */
9683   this.tools = {};
9685   /**
9686    * Holds references to DOM elements associated to PaintWeb commands.
9687    *
9688    * @private
9689    * @type Object
9690    */
9691   this.commands = {};
9693   /**
9694    * Holds references to floating panels GUI components.
9695    *
9696    * @type Object
9697    * @see pwlib.guiFloatingPanel
9698    */
9699   this.floatingPanels = {zIndex_: 0};
9701   /**
9702    * Holds references to tab panel GUI components.
9703    *
9704    * @type Object
9705    * @see pwlib.guiTabPanel
9706    */
9707   this.tabPanels = {};
9709   /**
9710    * Holds an instance of the guiResizer object attached to the Canvas.
9711    *
9712    * @private
9713    * @type pwlib.guiResizer
9714    */
9715   this.canvasResizer = null;
9717   /**
9718    * Holds tab configuration information for most drawing tools.
9719    *
9720    * @private
9721    * @type Object
9722    */
9723   this.toolTabConfig = {
9724     bcurve: {
9725       lineTab: true,
9726       shapeType: true,
9727       lineWidth: true,
9728       lineWidthLabel: lang.inputs.borderWidth,
9729       lineCap: true
9730     },
9731     ellipse: {
9732       lineTab: true,
9733       shapeType: true,
9734       lineWidth: true,
9735       lineWidthLabel: lang.inputs.borderWidth
9736     },
9737     rectangle: {
9738       lineTab: true,
9739       shapeType: true,
9740       lineWidth: true,
9741       lineWidthLabel: lang.inputs.borderWidth,
9742       lineJoin: true
9743     },
9744     polygon: {
9745       lineTab: true,
9746       shapeType: true,
9747       lineWidth: true,
9748       lineWidthLabel: lang.inputs.borderWidth,
9749       lineJoin: true,
9750       lineCap: true,
9751       miterLimit: true
9752     },
9753     eraser: {
9754       lineTab: true,
9755       lineWidth: true,
9756       lineWidthLabel: lang.inputs.eraserSize,
9757       lineJoin: true,
9758       lineCap: true,
9759       miterLimit: true
9760     },
9761     pencil: {
9762       lineTab: true,
9763       lineWidth: true,
9764       lineWidthLabel: lang.inputs.pencilSize,
9765       lineJoin: true,
9766       lineCap: true,
9767       miterLimit: true
9768     },
9769     line: {
9770       lineTab: true,
9771       lineWidth: true,
9772       lineWidthLabel: lang.inputs.line.lineWidth,
9773       lineJoin: true,
9774       lineCap: true,
9775       miterLimit: true
9776     },
9777     text: {
9778       lineTab: true,
9779       lineTabLabel: lang.tabs.main.textBorder,
9780       shapeType: true,
9781       lineWidth: true,
9782       lineWidthLabel: lang.inputs.borderWidth
9783     }
9784   };
9786   /**
9787    * Initialize the PaintWeb interface.
9788    *
9789    * @param {Document|String} markup The interface markup loaded and parsed as 
9790    * DOM Document object. Optionally, the value can be a string holding the 
9791    * interface markup (this is used when PaintWeb is packaged).
9792    *
9793    * @returns {Boolean} True if the initialization was successful, or false if 
9794    * not.
9795    */
9796   this.init = function (markup) {
9797     // Make sure the user nicely waits for PaintWeb to load, without seeing 
9798     // much.
9799     var placeholder = config.guiPlaceholder,
9800         placeholderStyle = placeholder.style;
9802     placeholderStyle.display = 'none';
9803     placeholderStyle.height = '1px';
9804     placeholderStyle.overflow = 'hidden';
9805     placeholderStyle.position = 'absolute';
9806     placeholderStyle.visibility = 'hidden';
9808     placeholder.className += ' ' + this.classPrefix + 'placeholder';
9809     if (!placeholder.tabIndex || placeholder.tabIndex == -1) {
9810       placeholder.tabIndex = 1;
9811     }
9813     if (!this.initImportDoc(markup)) {
9814       app.initError(lang.guiMarkupImportFailed);
9815       return false;
9816     }
9817     markup = null;
9819     if (!this.initParseMarkup()) {
9820       app.initError(lang.guiMarkupParseFailed);
9821       return false;
9822     }
9824     if (!this.initCanvas() ||
9825         !this.initImageZoom() ||
9826         !this.initSelectionTool() ||
9827         !this.initTextTool() ||
9828         !this.initKeyboardShortcuts()) {
9829       return false;
9830     }
9832     // Setup the main tabbed panel.
9833     var panel = this.tabPanels.main;
9834     if (!panel) {
9835       app.initError(lang.noMainTabPanel);
9836       return false;
9837     }
9839     // Hide the "Shadow" tab if the drawing of shadows is not supported.
9840     if (!app.shadowSupported && 'shadow' in panel.tabs) {
9841       panel.tabHide('shadow');
9842     }
9844     // Setup the viewport height.
9845     if ('viewport' in this.elems) {
9846       this.elems.viewport.style.height = config.viewportHeight + 'px';
9847     }
9849     // Setup the Canvas resizer.
9850     var resizeHandle = this.elems.canvasResizer;
9851     if (!resizeHandle) {
9852       app.initError(lang.missingCanvasResizer);
9853       return false;
9854     }
9855     resizeHandle.title = lang.guiCanvasResizer;
9856     resizeHandle.replaceChild(doc.createTextNode(lang.guiCanvasResizer), 
9857         resizeHandle.firstChild);
9858     resizeHandle.addEventListener('mouseover', this.item_mouseover, false);
9859     resizeHandle.addEventListener('mouseout',  this.item_mouseout,  false);
9861     this.canvasResizer = new pwlib.guiResizer(this, resizeHandle, 
9862         this.elems.canvasContainer);
9864     this.canvasResizer.events.add('guiResizeStart', this.canvasResizeStart);
9865     this.canvasResizer.events.add('guiResizeEnd',   this.canvasResizeEnd);
9867     if ('statusMessage' in this.elems) {
9868       this.elems.statusMessage._prevText = false;
9869     }
9871     // Update the version string in Help.
9872     if ('version' in this.elems) {
9873       this.elems.version.appendChild(doc.createTextNode(app.toString()));
9874     }
9876     // Update the image dimensions in the GUI.
9877     var imageSize = this.elems.imageSize;
9878     if (imageSize) {
9879       imageSize.replaceChild(doc.createTextNode(app.image.width + 'x' 
9880             + app.image.height), imageSize.firstChild);
9881     }
9883     // Add application-wide event listeners.
9884     app.events.add('canvasSizeChange',  this.canvasSizeChange);
9885     app.events.add('commandRegister',   this.commandRegister);
9886     app.events.add('commandUnregister', this.commandUnregister);
9887     app.events.add('configChange',      this.configChangeHandler);
9888     app.events.add('imageSizeChange',   this.imageSizeChange);
9889     app.events.add('imageZoom',         this.imageZoom);
9890     app.events.add('appInit',           this.appInit);
9891     app.events.add('shadowAllow',       this.shadowAllow);
9892     app.events.add('toolActivate',      this.toolActivate);
9893     app.events.add('toolRegister',      this.toolRegister);
9894     app.events.add('toolUnregister',    this.toolUnregister);
9896     // Make sure the historyUndo and historyRedo command elements are 
9897     // synchronized with the application history state.
9898     if ('historyUndo' in this.commands && 'historyRedo' in this.commands) {
9899       app.events.add('historyUpdate', this.historyUpdate);
9900     }
9902     app.commandRegister('about', this.commandAbout);
9904     return true;
9905   };
9907   /**
9908    * Initialize the Canvas elements.
9909    *
9910    * @private
9911    * @returns {Boolean} True if the initialization was successful, or false if 
9912    * not.
9913    */
9914   this.initCanvas = function () {
9915     var canvasContainer = this.elems.canvasContainer,
9916         layerCanvas     = app.layer.canvas,
9917         layerContext    = app.layer.context,
9918         layerStyle      = layerCanvas.style,
9919         bufferCanvas    = app.buffer.canvas;
9921     if (!canvasContainer) {
9922       app.initError(lang.missingCanvasContainer);
9923       return false;
9924     }
9926     var containerStyle  = canvasContainer.style;
9928     canvasContainer.className = this.classPrefix + 'canvasContainer';
9929     layerCanvas.className     = this.classPrefix + 'layerCanvas';
9930     bufferCanvas.className    = this.classPrefix + 'bufferCanvas';
9932     containerStyle.width  = layerStyle.width;
9933     containerStyle.height = layerStyle.height;
9935     canvasContainer.appendChild(layerCanvas);
9936     canvasContainer.appendChild(bufferCanvas);
9937     canvasContainer.style.backgroundColor = config.backgroundColor;
9939     // Make sure the selection transparency input checkbox is disabled if the 
9940     // putImageData and getImageData methods are unsupported.
9941     if ('selection_transparent' in this.inputs && (!layerContext.putImageData || 
9942           !layerContext.getImageData)) {
9943       this.inputs.selection_transparent.disabled = true;
9944       this.inputs.selection_transparent.checked = true;
9945     }
9947     return true;
9948   };
9950   /**
9951    * Import the DOM nodes from the interface DOM document. All the nodes are 
9952    * inserted into the {@link PaintWeb.config.guiPlaceholder} element.
9953    *
9954    * <p>Elements which have the ID attribute will have the attribute renamed to 
9955    * <code>data-pwId</code>.
9956    *
9957    * <p>Input elements which have the ID attribute will have their attribute 
9958    * updated to be unique for the current PaintWeb instance.
9959    *
9960    * @private
9961    *
9962    * @param {Document|String} markup The source DOM document to import the nodes 
9963    * from. Optionally, this parameter can be a string which holds the interface 
9964    * markup.
9965    *
9966    * @returns {Boolean} True if the initialization was successful, or false if 
9967    * not.
9968    */
9969   this.initImportDoc = function (markup) {
9970     // I could use some XPath here, but for the sake of compatibility I don't.
9971     var destElem = config.guiPlaceholder,
9972         elType = Node.ELEMENT_NODE,
9973         elem, root, nodes, n, tag, isInput;
9975     if (typeof markup === 'string') {
9976       elem = doc.createElement('div');
9977       elem.innerHTML = markup;
9978       root = elem.firstChild;
9979     } else {
9980       root = markup.documentElement;
9981     }
9982     markup = null;
9984     nodes = root.getElementsByTagName('*');
9985     n = nodes.length;
9987     // Change all the id attributes to be data-pwId attributes.
9988     // Input elements have their ID updated to be unique for the current 
9989     // PaintWeb instance.
9990     for (var i = 0; i < n; i++) {
9991       elem = nodes[i];
9992       if (elem.nodeType !== elType) {
9993         continue;
9994       }
9995       tag = elem.tagName.toLowerCase();
9996       isInput = tag === 'input' || tag === 'select' || tag === 'textarea';
9998       if (elem.id) {
9999         elem.setAttribute('data-pwId', elem.id);
10001         if (isInput) {
10002           elem.id = this.idPrefix + elem.id;
10003         } else {
10004           elem.removeAttribute('id');
10005         }
10006       }
10008       // label elements have their "for" attribute updated as well.
10009       if (tag === 'label' && elem.htmlFor) {
10010         elem.htmlFor = this.idPrefix + elem.htmlFor;
10011       }
10012     }
10014     // Import all the nodes.
10015     n = root.childNodes.length;
10016     for (var i = 0; i < n; i++) {
10017       destElem.appendChild(doc.importNode(root.childNodes[i], true));
10018     }
10020     return true;
10021   };
10023   /**
10024    * Parse the interface markup. The layout file can have custom 
10025    * PaintWeb-specific attributes.
10026    *
10027    * <p>Elements with the <code>data-pwId</code> attribute are added to the 
10028    * {@link pwlib.gui#elems} object.
10029    *
10030    * <p>Elements having the <code>data-pwCommand</code> attribute are added to 
10031    * the {@link pwlib.gui#commands} object.
10032    *
10033    * <p>Elements having the <code>data-pwTool</code> attribute are added to the 
10034    * {@link pwlib.gui#tools} object.
10035    *
10036    * <p>Elements having the <code>data-pwTabPanel</code> attribute are added to 
10037    * the {@link pwlib.gui#tabPanels} object. These become interactive GUI 
10038    * components (see {@link pwlib.guiTabPanel}).
10039    *
10040    * <p>Elements having the <code>data-pwFloatingPanel</code> attribute are 
10041    * added to the {@link pwlib.gui#floatingPanels} object. These become 
10042    * interactive GUI components (see {@link pwlib.guiFloatingPanel}).
10043    *
10044    * <p>Elements having the <code>data-pwConfig</code> attribute are added to 
10045    * the {@link pwlib.gui#inputs} object. These become interactive GUI 
10046    * components which allow users to change configuration options.
10047    *
10048    * <p>Elements having the <code>data-pwConfigValue</code> attribute are added 
10049    * to the {@link pwlib.gui#inputValues} object. These can only be child nodes 
10050    * of elements which have the <code>data-pwConfig</code> attribute. Each such 
10051    * element is considered an icon. Anchor elements are appended to ensure 
10052    * keyboard accessibility.
10053    *
10054    * <p>Elements having the <code>data-pwConfigToggle</code> attribute are added 
10055    * to the {@link pwlib.gui#inputs} object. These become interactive GUI 
10056    * components which toggle the boolean value of the configuration property 
10057    * they are associated to.
10058    *
10059    * <p>Elements having the <code>data-pwColorInput</code> attribute are added 
10060    * to the {@link pwlib.gui#colorInputs} object. These become color picker 
10061    * inputs which are associated to the configuration property given as the 
10062    * attribute value. (see {@link pwlib.guiColorInput})
10063    *
10064    * @returns {Boolean} True if the parsing was successful, or false if not.
10065    */
10066   this.initParseMarkup = function () {
10067     var nodes = config.guiPlaceholder.getElementsByTagName('*'),
10068         elType = Node.ELEMENT_NODE,
10069         elem, tag, isInput, tool, tabPanel, floatingPanel, cmd, id, cfgAttr, 
10070         colorInput;
10072     // Store references to important elements and parse PaintWeb-specific 
10073     // attributes.
10074     for (var i = 0; i < nodes.length; i++) {
10075       elem = nodes[i];
10076       if (elem.nodeType !== elType) {
10077         continue;
10078       }
10079       tag = elem.tagName.toLowerCase();
10080       isInput = tag === 'input' || tag === 'select' || tag === 'textarea';
10082       // Store references to commands.
10083       cmd = elem.getAttribute('data-pwCommand');
10084       if (cmd && !(cmd in this.commands)) {
10085         elem.className += ' ' + this.classPrefix + 'command';
10086         this.commands[cmd] = elem;
10087       }
10089       // Store references to tools.
10090       tool = elem.getAttribute('data-pwTool');
10091       if (tool && !(tool in this.tools)) {
10092         elem.className += ' ' + this.classPrefix + 'tool';
10093         this.tools[tool] = elem;
10094       }
10096       // Create tab panels.
10097       tabPanel = elem.getAttribute('data-pwTabPanel');
10098       if (tabPanel) {
10099         this.tabPanels[tabPanel] = new pwlib.guiTabPanel(this, elem);
10100       }
10102       // Create floating panels.
10103       floatingPanel = elem.getAttribute('data-pwFloatingPanel');
10104       if (floatingPanel) {
10105         this.floatingPanels[floatingPanel] = new pwlib.guiFloatingPanel(this, 
10106             elem);
10107       }
10109       cfgAttr = elem.getAttribute('data-pwConfig');
10110       if (cfgAttr) {
10111         if (isInput) {
10112           this.initConfigInput(elem, cfgAttr);
10113         } else {
10114           this.initConfigIcons(elem, cfgAttr);
10115         }
10116       }
10118       cfgAttr = elem.getAttribute('data-pwConfigToggle');
10119       if (cfgAttr) {
10120         this.initConfigToggle(elem, cfgAttr);
10121       }
10123       // elem.hasAttribute() fails in webkit (tested with chrome and safari 4)
10124       if (elem.getAttribute('data-pwColorInput')) {
10125         colorInput = new pwlib.guiColorInput(this, elem);
10126         this.colorInputs[colorInput.id] = colorInput;
10127       }
10129       id = elem.getAttribute('data-pwId');
10130       if (id) {
10131         elem.className += ' ' + this.classPrefix + id;
10133         // Store a reference to the element.
10134         if (isInput && !cfgAttr) {
10135           this.inputs[id] = elem;
10136         } else if (!isInput) {
10137           this.elems[id] = elem;
10138         }
10139       }
10140     }
10142     return true;
10143   };
10145   /**
10146    * Initialize an input element associated to a configuration property.
10147    *
10148    * @private
10149    *
10150    * @param {Element} elem The DOM element which is associated to the 
10151    * configuration property.
10152    *
10153    * @param {String} cfgAttr The configuration attribute. This tells the 
10154    * configuration group and property to which the DOM element is attached to.
10155    */
10156   this.initConfigInput = function (input, cfgAttr) {
10157     var cfgNoDots   = cfgAttr.replace('.', '_'),
10158         cfgArray    = cfgAttr.split('.'),
10159         cfgProp     = cfgArray.pop(),
10160         cfgGroup    = cfgArray.join('.'),
10161         cfgGroupRef = config,
10162         langGroup   = lang.inputs,
10163         labelElem   = input.parentNode;
10165     for (var i = 0, n = cfgArray.length; i < n; i++) {
10166       cfgGroupRef = cfgGroupRef[cfgArray[i]];
10167       langGroup = langGroup[cfgArray[i]];
10168     }
10170     input._pwConfigProperty = cfgProp;
10171     input._pwConfigGroup = cfgGroup;
10172     input._pwConfigGroupRef = cfgGroupRef;
10173     input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
10174     input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots;
10176     this.inputs[cfgNoDots] = input;
10178     if (labelElem.tagName.toLowerCase() !== 'label') {
10179       labelElem = labelElem.getElementsByTagName('label')[0];
10180     }
10182     if (input.type === 'checkbox' || labelElem.htmlFor) {
10183       labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
10184           labelElem.lastChild);
10185     } else {
10186       labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
10187           labelElem.firstChild);
10188     }
10190     if (input.type === 'checkbox') {
10191       input.checked = cfgGroupRef[cfgProp];
10192     } else {
10193       input.value = cfgGroupRef[cfgProp];
10194     }
10196     input.addEventListener('input',  this.configInputChange, false);
10197     input.addEventListener('change', this.configInputChange, false);
10198   };
10200   /**
10201    * Initialize an HTML element associated to a configuration property, and all 
10202    * of its own sub-elements associated to configuration values. Each element 
10203    * that has the <var>data-pwConfigValue</var> attribute is considered an icon.
10204    *
10205    * @private
10206    *
10207    * @param {Element} elem The DOM element which is associated to the 
10208    * configuration property.
10209    *
10210    * @param {String} cfgAttr The configuration attribute. This tells the 
10211    * configuration group and property to which the DOM element is attached to.
10212    */
10213   this.initConfigIcons = function (input, cfgAttr) {
10214     var cfgNoDots   = cfgAttr.replace('.', '_'),
10215         cfgArray    = cfgAttr.split('.'),
10216         cfgProp     = cfgArray.pop(),
10217         cfgGroup    = cfgArray.join('.'),
10218         cfgGroupRef = config,
10219         langGroup   = lang.inputs;
10221     for (var i = 0, n = cfgArray.length; i < n; i++) {
10222       cfgGroupRef = cfgGroupRef[cfgArray[i]];
10223       langGroup = langGroup[cfgArray[i]];
10224     }
10226     input._pwConfigProperty = cfgProp;
10227     input._pwConfigGroup = cfgGroup;
10228     input._pwConfigGroupRef = cfgGroupRef;
10229     input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
10230     input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots;
10232     this.inputs[cfgNoDots] = input;
10234     var labelElem = input.getElementsByTagName('p')[0];
10235     labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
10236         labelElem.firstChild);
10238     var elem, anchor, val,
10239         className = ' ' + this.classPrefix + 'configActive';
10240         nodes = input.getElementsByTagName('*'),
10241         elType = Node.ELEMENT_NODE;
10243     for (var i = 0; i < nodes.length; i++) {
10244       elem = nodes[i];
10245       if (elem.nodeType !== elType) {
10246         continue;
10247       }
10249       val = elem.getAttribute('data-pwConfigValue');
10250       if (!val) {
10251         continue;
10252       }
10254       anchor = doc.createElement('a');
10255       anchor.href = '#';
10256       anchor.title = langGroup[cfgProp + '_' + val];
10257       anchor.appendChild(doc.createTextNode(anchor.title));
10259       elem.className += ' ' + this.classPrefix + cfgProp + '_' + val 
10260         + ' ' + this.classPrefix + 'icon';
10261       elem._pwConfigParent = input;
10263       if (cfgGroupRef[cfgProp] == val) {
10264         elem.className += className;
10265       }
10267       anchor.addEventListener('click',     this.configValueClick, false);
10268       anchor.addEventListener('mouseover', this.item_mouseover,   false);
10269       anchor.addEventListener('mouseout',  this.item_mouseout,    false);
10271       elem.replaceChild(anchor, elem.firstChild);
10273       this.inputValues[cfgGroup + '_' + cfgProp + '_' + val] = elem;
10274     }
10275   };
10277   /**
10278    * Initialize an HTML element associated to a boolean configuration property.
10279    *
10280    * @private
10281    *
10282    * @param {Element} elem The DOM element which is associated to the 
10283    * configuration property.
10284    *
10285    * @param {String} cfgAttr The configuration attribute. This tells the 
10286    * configuration group and property to which the DOM element is attached to.
10287    */
10288   this.initConfigToggle = function (input, cfgAttr) {
10289     var cfgNoDots   = cfgAttr.replace('.', '_'),
10290         cfgArray    = cfgAttr.split('.'),
10291         cfgProp     = cfgArray.pop(),
10292         cfgGroup    = cfgArray.join('.'),
10293         cfgGroupRef = config,
10294         langGroup   = lang.inputs;
10296     for (var i = 0, n = cfgArray.length; i < n; i++) {
10297       cfgGroupRef = cfgGroupRef[cfgArray[i]];
10298       langGroup = langGroup[cfgArray[i]];
10299     }
10301     input._pwConfigProperty = cfgProp;
10302     input._pwConfigGroup = cfgGroup;
10303     input._pwConfigGroupRef = cfgGroupRef;
10304     input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots 
10305       + ' ' + this.classPrefix + 'icon';
10307     if (cfgGroupRef[cfgProp]) {
10308       input.className += ' ' + this.classPrefix + 'configActive';
10309     }
10311     var anchor = doc.createElement('a');
10312     anchor.href = '#';
10313     anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
10314     anchor.appendChild(doc.createTextNode(langGroup[cfgProp]));
10316     anchor.addEventListener('click',     this.configToggleClick, false);
10317     anchor.addEventListener('mouseover', this.item_mouseover,    false);
10318     anchor.addEventListener('mouseout',  this.item_mouseout,     false);
10320     input.replaceChild(anchor, input.firstChild);
10322     this.inputs[cfgNoDots] = input;
10323   };
10325   /**
10326    * Initialize the image zoom input.
10327    *
10328    * @private
10329    * @returns {Boolean} True if the initialization was successful, or false if 
10330    * not.
10331    */
10332   this.initImageZoom = function () {
10333     var input = this.inputs.imageZoom;
10334     if (!input) {
10335       return true; // allow layouts without the zoom input
10336     }
10338     input.value = 100;
10339     input._old_value = 100;
10341     // Override the attributes, based on the settings.
10342     input.setAttribute('step', config.imageZoomStep * 100);
10343     input.setAttribute('max',  config.imageZoomMax  * 100);
10344     input.setAttribute('min',  config.imageZoomMin  * 100);
10346     var changeFn = function () {
10347       app.imageZoomTo(parseInt(this.value) / 100);
10348     };
10350     input.addEventListener('change', changeFn, false);
10351     input.addEventListener('input',  changeFn, false);
10353     // Update some language strings
10355     var label = input.parentNode;
10356     if (label.tagName.toLowerCase() === 'label') {
10357       label.replaceChild(doc.createTextNode(lang.imageZoomLabel), 
10358           label.firstChild);
10359     }
10361     var elem = this.elems.statusZoom;
10362     if (!elem) {
10363       return true;
10364     }
10366     elem.title = lang.imageZoomTitle;
10368     return true;
10369   };
10371   /**
10372    * Initialize GUI elements associated to selection tool options and commands.
10373    *
10374    * @private
10375    * @returns {Boolean} True if the initialization was successful, or false if 
10376    * not.
10377    */
10378   this.initSelectionTool = function () {
10379     var classDisabled = ' ' + this.classPrefix + 'disabled',
10380         cut   = this.commands.selectionCut,
10381         copy  = this.commands.selectionCopy,
10382         paste = this.commands.clipboardPaste;
10384     if (paste) {
10385       app.events.add('clipboardUpdate', this.clipboardUpdate);
10386       paste.className += classDisabled;
10388     }
10390     if (cut && copy) {
10391       app.events.add('selectionChange', this.selectionChange);
10392       cut.className  += classDisabled;
10393       copy.className += classDisabled;
10394     }
10396     var selTab_cmds = ['selectionCut', 'selectionCopy', 'clipboardPaste'],
10397         anchor, elem, cmd;
10399     for (var i = 0, n = selTab_cmds.length; i < n; i++) {
10400       cmd = selTab_cmds[i];
10401       elem = this.elems['selTab_' + cmd];
10402       if (!elem) {
10403         continue;
10404       }
10406       anchor = doc.createElement('a');
10407       anchor.title = lang.commands[cmd];
10408       anchor.href = '#';
10409       anchor.appendChild(doc.createTextNode(anchor.title));
10410       anchor.addEventListener('click', this.commandClick, false);
10412       elem.className += classDisabled + ' ' + this.classPrefix + 'command' 
10413         + ' ' + this.classPrefix + 'cmd_' + cmd;
10414       elem.setAttribute('data-pwCommand', cmd);
10415       elem.replaceChild(anchor, elem.firstChild);
10416     }
10418     var selCrop   = this.commands.selectionCrop,
10419         selFill   = this.commands.selectionFill,
10420         selDelete = this.commands.selectionDelete;
10422     selCrop.className   += classDisabled;
10423     selFill.className   += classDisabled;
10424     selDelete.className += classDisabled;
10426     return true;
10427   };
10429   /**
10430    * Initialize GUI elements associated to text tool options.
10431    *
10432    * @private
10433    * @returns {Boolean} True if the initialization was successful, or false if 
10434    * not.
10435    */
10436   this.initTextTool = function () {
10437     if ('textString' in this.inputs) {
10438       this.inputs.textString.value = lang.inputs.text.textString_value;
10439     }
10441     if (!('text_fontFamily' in this.inputs) || !('text' in config) || 
10442         !('fontFamilies' in config.text)) {
10443       return true;
10444     }
10446     var option, input = this.inputs.text_fontFamily;
10447     for (var i = 0, n = config.text.fontFamilies.length; i < n; i++) {
10448       option = doc.createElement('option');
10449       option.value = config.text.fontFamilies[i];
10450       option.appendChild(doc.createTextNode(option.value));
10451       input.appendChild(option);
10453       if (option.value === config.text.fontFamily) {
10454         input.selectedIndex = i;
10455         input.value = option.value;
10456       }
10457     }
10459     option = doc.createElement('option');
10460     option.value = '+';
10461     option.appendChild(doc.createTextNode(lang.inputs.text.fontFamily_add));
10462     input.appendChild(option);
10464     return true;
10465   };
10467   /**
10468    * Initialize the keyboard shortcuts. Basically, this updates various strings 
10469    * to ensure the user interface is informational.
10470    *
10471    * @private
10472    * @returns {Boolean} True if the initialization was successful, or false if 
10473    * not.
10474    */
10475   this.initKeyboardShortcuts = function () {
10476     var kid = null, kobj = null;
10478     for (kid in config.keys) {
10479       kobj = config.keys[kid];
10481       if ('toolActivate' in kobj && kobj.toolActivate in lang.tools) {
10482         lang.tools[kobj.toolActivate] += ' [ ' + kid + ' ]';
10483       }
10485       if ('command' in kobj && kobj.command in lang.commands) {
10486         lang.commands[kobj.command] += ' [ ' + kid + ' ]';
10487       }
10488     }
10490     return true;
10491   };
10493   /**
10494    * The <code>appInit</code> event handler. This method is invoked once 
10495    * PaintWeb completes all the loading.
10496    *
10497    * <p>This method dispatches the {@link pwlib.appEvent.guiShow} application 
10498    * event.
10499    *
10500    * @private
10501    * @param {pwlib.appEvent.appInit} ev The application event object.
10502    */
10503   this.appInit = function (ev) {
10504     // Initialization was not successful ...
10505     if (ev.state !== PaintWeb.INIT_DONE) {
10506       return;
10507     }
10509     // Make PaintWeb visible.
10510     var placeholder = config.guiPlaceholder,
10511         placeholderStyle = placeholder.style,
10512         cs = win.getComputedStyle(placeholder, null);
10514     // We do not reset the display property. We leave this for the stylesheet.
10515     placeholderStyle.height = '';
10516     placeholderStyle.overflow = '';
10517     placeholderStyle.position = '';
10518     placeholderStyle.visibility = '';
10520     // Do not allow the static positioning for the PaintWeb placeholder.  
10521     // Usually, the GUI requires absolute/relative positioning.
10522     if (cs.position === 'static') {
10523       placeholderStyle.position = 'relative';
10524     }
10526     placeholder.focus();
10528     app.events.dispatch(new appEvent.guiShow());
10529   };
10531   /**
10532    * The <code>guiResizeStart</code> event handler for the Canvas resize 
10533    * operation.
10534    * @private
10535    */
10536   this.canvasResizeStart = function () {
10537     this.resizeHandle.style.visibility = 'hidden';
10539     // ugly...
10540     this.timeout_ = setTimeout(function () {
10541       _self.statusShow('guiCanvasResizerActive', true);
10542       clearTimeout(_self.canvasResizer.timeout_);
10543       delete _self.canvasResizer.timeout_;
10544     }, 400);
10545   };
10547   /**
10548    * The <code>guiResizeEnd</code> event handler for the Canvas resize 
10549    * operation.
10550    *
10551    * @private
10552    * @param {pwlib.appEvent.guiResizeEnd} ev The application event object.
10553    */
10554   this.canvasResizeEnd = function (ev) {
10555     this.resizeHandle.style.visibility = '';
10557     app.imageCrop(0, 0, MathRound(ev.width / app.image.canvasScale),
10558         MathRound(ev.height / app.image.canvasScale));
10560     if (this.timeout_) {
10561       clearTimeout(this.timeout_);
10562       delete this.timeout_;
10563     } else {
10564       _self.statusShow(-1);
10565     }
10566   };
10568   /**
10569    * The <code>mouseover</code> event handler for all tools, commands and icons.  
10570    * This simply shows the title / text content of the element in the GUI status 
10571    * bar.
10572    *
10573    * @see pwlib.gui#statusShow The method used for displaying the message in the 
10574    * GUI status bar.
10575    */
10576   this.item_mouseover = function () {
10577     if (this.title || this.textConent) {
10578       _self.statusShow(this.title || this.textContent, true);
10579     }
10580   };
10582   /**
10583    * The <code>mouseout</code> event handler for all tools, commands and icons.  
10584    * This method simply resets the GUI status bar to the previous message it was 
10585    * displaying before the user hovered the current element.
10586    *
10587    * @see pwlib.gui#statusShow The method used for displaying the message in the 
10588    * GUI status bar.
10589    */
10590   this.item_mouseout = function () {
10591     _self.statusShow(-1);
10592   };
10594   /**
10595    * Show a message in the status bar.
10596    *
10597    * @param {String|Number} msg The message ID you want to display. The ID 
10598    * should be available in the {@link PaintWeb.lang.status} object. If the 
10599    * value is -1 then the previous non-temporary message will be displayed. If 
10600    * the ID is not available in the language file, then the string is shown 
10601    * as-is.
10602    *
10603    * @param {Boolean} [temporary=false] Tells if the message is temporary or 
10604    * not.
10605    */
10606   this.statusShow = function (msg, temporary) {
10607     var elem = this.elems.statusMessage;
10608     if (msg === -1 && elem._prevText === false) {
10609       return false;
10610     }
10612     if (msg === -1) {
10613       msg = elem._prevText;
10614     }
10616     if (msg in lang.status) {
10617       msg = lang.status[msg];
10618     }
10620     if (!temporary) {
10621       elem._prevText = msg;
10622     }
10624     if (elem.firstChild) {
10625       elem.removeChild(elem.firstChild);
10626     }
10628     win.status = msg;
10630     if (msg) {
10631       elem.appendChild(doc.createTextNode(msg));
10632     }
10633   };
10635   /**
10636    * The "About" command. This method displays the "About" panel.
10637    */
10638   this.commandAbout = function () {
10639     _self.floatingPanels.about.toggle();
10640   };
10642   /**
10643    * The <code>click</code> event handler for the tool DOM elements.
10644    *
10645    * @private
10646    *
10647    * @param {Event} ev The DOM Event object.
10648    *
10649    * @see PaintWeb#toolActivate to activate a drawing tool.
10650    */
10651   this.toolClick = function (ev) {
10652     app.toolActivate(this.parentNode.getAttribute('data-pwTool'), ev);
10653     ev.preventDefault();
10654   };
10656   /**
10657    * The <code>toolActivate</code> application event handler. This method 
10658    * provides visual feedback for the activation of a new drawing tool.
10659    *
10660    * @private
10661    *
10662    * @param {pwlib.appEvent.toolActivate} ev The application event object.
10663    *
10664    * @see PaintWeb#toolActivate the method which allows you to activate 
10665    * a drawing tool.
10666    */
10667   this.toolActivate = function (ev) {
10668     var tabAnchor,
10669         tabActive = _self.tools[ev.id],
10670         tabConfig = _self.toolTabConfig[ev.id] || {},
10671         tabPanel = _self.tabPanels.main,
10672         lineTab = tabPanel.tabs.line,
10673         shapeType = _self.inputs.shapeType,
10674         lineWidth = _self.inputs.line_lineWidth,
10675         lineCap = _self.inputs.line_lineCap,
10676         lineJoin = _self.inputs.line_lineJoin,
10677         miterLimit = _self.inputs.line_miterLimit,
10678         lineWidthLabel = null;
10680     tabActive.className += ' ' + _self.classPrefix + 'toolActive';
10681     tabActive.firstChild.focus();
10683     if ((ev.id + 'Active') in lang.status) {
10684       _self.statusShow(ev.id + 'Active');
10685     }
10687     // show/hide the shapeType input config.
10688     if (shapeType) {
10689       if (tabConfig.shapeType) {
10690         shapeType.style.display = '';
10691       } else {
10692         shapeType.style.display = 'none';
10693       }
10694     }
10696     if (ev.prevId) {
10697       var prevTab = _self.tools[ev.prevId],
10698           prevTabConfig = _self.toolTabConfig[ev.prevId] || {};
10700       prevTab.className = prevTab.className.
10701         replace(' ' + _self.classPrefix + 'toolActive', '');
10703       // hide the line tab
10704       if (prevTabConfig.lineTab && lineTab) {
10705         tabPanel.tabHide('line');
10706         lineTab.container.className = lineTab.container.className.
10707           replace(' ' + _self.classPrefix + 'main_line_' + ev.prevId, 
10708               ' ' + _self.classPrefix + 'main_line');
10709       }
10711       // hide the tab for the current tool.
10712       if (ev.prevId in tabPanel.tabs) {
10713         tabPanel.tabHide(ev.prevId);
10714       }
10715     }
10717     // Change the label of the lineWidth input element.
10718     if (tabConfig.lineWidthLabel) {
10719       lineWidthLabel = lineWidth.parentNode;
10720       lineWidthLabel.replaceChild(doc.createTextNode(tabConfig.lineWidthLabel), 
10721           lineWidthLabel.firstChild);
10723     }
10725     if (lineJoin) {
10726       if (tabConfig.lineJoin) {
10727         lineJoin.style.display = '';
10728       } else {
10729         lineJoin.style.display = 'none';
10730       }
10731     }
10733     if (lineCap) {
10734       if (tabConfig.lineCap) {
10735         lineCap.style.display = '';
10736       } else {
10737         lineCap.style.display = 'none';
10738       }
10739     }
10741     if (miterLimit) {
10742       if (tabConfig.miterLimit) {
10743         miterLimit.parentNode.parentNode.style.display = '';
10744       } else {
10745         miterLimit.parentNode.parentNode.style.display = 'none';
10746       }
10747     }
10749     if (lineWidth) {
10750       if (tabConfig.lineWidth) {
10751         lineWidth.parentNode.parentNode.style.display = '';
10752       } else {
10753         lineWidth.parentNode.parentNode.style.display = 'none';
10754       }
10755     }
10757     // show the line tab, if configured
10758     if (tabConfig.lineTab && 'line' in tabPanel.tabs) {
10759       tabAnchor = lineTab.button.firstChild;
10760       tabAnchor.title = tabConfig.lineTabLabel || lang.tabs.main[ev.id];
10761       tabAnchor.replaceChild(doc.createTextNode(tabAnchor.title), 
10762           tabAnchor.firstChild);
10764       if (ev.id !== 'line') {
10765         lineTab.container.className = lineTab.container.className.
10766             replace(' ' + _self.classPrefix + 'main_line', ' ' + _self.classPrefix 
10767                 + 'main_line_' + ev.id);
10768       }
10770       tabPanel.tabShow('line');
10771     }
10773     // show the tab for the current tool, if there's one.
10774     if (ev.id in tabPanel.tabs) {
10775       tabPanel.tabShow(ev.id);
10776     }
10777   };
10779   /**
10780    * The <code>toolRegister</code> application event handler. This method adds 
10781    * the new tool into the GUI.
10782    *
10783    * @private
10784    *
10785    * @param {pwlib.appEvent.toolRegister} ev The application event object.
10786    *
10787    * @see PaintWeb#toolRegister the method which allows you to register new 
10788    * tools.
10789    */
10790   this.toolRegister = function (ev) {
10791     var attr = null, elem = null, anchor = null;
10793     if (ev.id in _self.tools) {
10794       elem = _self.tools[ev.id];
10795       attr = elem.getAttribute('data-pwTool');
10796       if (attr && attr !== ev.id) {
10797         attr = null;
10798         elem = null;
10799         delete _self.tools[ev.id];
10800       }
10801     }
10803     // Create a new element if there's none already associated to the tool ID.
10804     if (!elem) {
10805       elem = doc.createElement('li');
10806     }
10808     if (!attr) {
10809       elem.setAttribute('data-pwTool', ev.id);
10810     }
10812     elem.className += ' ' + _self.classPrefix + 'tool_' + ev.id;
10814     // Append an anchor element which holds the locale string.
10815     anchor = doc.createElement('a');
10816     anchor.title = lang.tools[ev.id];
10817     anchor.href = '#';
10818     anchor.appendChild(doc.createTextNode(anchor.title));
10820     elem.replaceChild(anchor, elem.firstChild);
10822     anchor.addEventListener('click',     _self.toolClick,      false);
10823     anchor.addEventListener('mouseover', _self.item_mouseover, false);
10824     anchor.addEventListener('mouseout',  _self.item_mouseout,  false);
10826     if (!(ev.id in _self.tools)) {
10827       _self.tools[ev.id] = elem;
10828       _self.elems.tools.appendChild(elem);
10829     }
10831     // Disable the text tool icon if the Canvas Text API is not supported.
10832     if (ev.id === 'text' && !app.layer.context.fillText && 
10833         !app.layer.context.mozPathText && elem) {
10834       elem.className += ' ' + _self.classPrefix + 'disabled';
10835       anchor.title = lang.tools.textUnsupported;
10837       anchor.removeEventListener('click', _self.toolClick, false);
10838       anchor.addEventListener('click', function (ev) {
10839         ev.preventDefault();
10840       }, false);
10841     }
10842   };
10844   /**
10845    * The <code>toolUnregister</code> application event handler. This method the 
10846    * tool element from the GUI.
10847    *
10848    * @param {pwlib.appEvent.toolUnregister} ev The application event object.
10849    *
10850    * @see PaintWeb#toolUnregister the method which allows you to unregister 
10851    * tools.
10852    */
10853   this.toolUnregister = function (ev) {
10854     if (ev.id in _self.tools) {
10855       _self.elems.tools.removeChild(_self.tools[ev.id]);
10856       delete _self.tools[ev.id];
10857     } else {
10858       return;
10859     }
10860   };
10862   /**
10863    * The <code>click</code> event handler for the command DOM elements.
10864    *
10865    * @private
10866    *
10867    * @param {Event} ev The DOM Event object.
10868    *
10869    * @see PaintWeb#commandRegister to register a new command.
10870    */
10871   this.commandClick = function (ev) {
10872     var cmd = this.parentNode.getAttribute('data-pwCommand');
10873     if (cmd && cmd in app.commands) {
10874       app.commands[cmd].call(this, ev);
10875     }
10876     ev.preventDefault();
10877     this.focus();
10878   };
10880   /**
10881    * The <code>commandRegister</code> application event handler. GUI elements 
10882    * associated to commands are updated to ensure proper user interaction.
10883    *
10884    * @private
10885    *
10886    * @param {pwlib.appEvent.commandRegister} ev The application event object.
10887    *
10888    * @see PaintWeb#commandRegister the method which allows you to register new 
10889    * commands.
10890    */
10891   this.commandRegister = function (ev) {
10892     var elem   = _self.commands[ev.id],
10893         anchor = null;
10894     if (!elem) {
10895       return;
10896     }
10898     elem.className += ' ' + _self.classPrefix + 'cmd_' + ev.id;
10900     anchor = doc.createElement('a');
10901     anchor.title = lang.commands[ev.id];
10902     anchor.href = '#';
10903     anchor.appendChild(doc.createTextNode(anchor.title));
10905     // Remove the text content and append the locale string associated to 
10906     // current command inside an anchor element (for better keyboard 
10907     // accessibility).
10908     if (elem.firstChild) {
10909       elem.removeChild(elem.firstChild);
10910     }
10911     elem.appendChild(anchor);
10913     anchor.addEventListener('click',     _self.commandClick,   false);
10914     anchor.addEventListener('mouseover', _self.item_mouseover, false);
10915     anchor.addEventListener('mouseout',  _self.item_mouseout,  false);
10916   };
10918   /**
10919    * The <code>commandUnregister</code> application event handler. This method 
10920    * simply removes all the user interactivity from the GUI element associated 
10921    * to the command being unregistered.
10922    *
10923    * @param {pwlib.appEvent.commandUnregister} ev The application event object.
10924    *
10925    * @see PaintWeb#commandUnregister the method which allows you to unregister 
10926    * commands.
10927    */
10928   this.commandUnregister = function (ev) {
10929     var elem   = _self.commands[ev.id],
10930         anchor = null;
10931     if (!elem) {
10932       return;
10933     }
10935     elem.className = elem.className.replace(' ' + _self.classPrefix + 'cmd_' 
10936         + ev.id, '');
10938     anchor = elem.firstChild;
10939     anchor.removeEventListener('click',     this.commands[ev.id], false);
10940     anchor.removeEventListener('mouseover', _self.item_mouseover, false);
10941     anchor.removeEventListener('mouseout',  _self.item_mouseout,  false);
10943     elem.removeChild(anchor);
10944   };
10946   /**
10947    * The <code>historyUpdate</code> application event handler. GUI elements 
10948    * associated to the <code>historyUndo</code> and to the 
10949    * <code>historyRedo</code> commands are updated such that they are either 
10950    * enabled or disabled, depending on the current history position.
10951    *
10952    * @param {pwlib.appEvent.historyUpdate} ev The application event object.
10953    * @see PaintWeb#historyGoto the method which allows you to go to different 
10954    * history states.
10955    */
10956   this.historyUpdate = function (ev) {
10957     var undoElem  = _self.commands.historyUndo,
10958         undoState = false,
10959         redoElem  = _self.commands.historyRedo,
10960         redoState = false,
10961         className = ' ' + _self.classPrefix + 'disabled',
10962         undoElemState = undoElem.className.indexOf(className) === -1,
10963         redoElemState = redoElem.className.indexOf(className) === -1;
10965     if (ev.currentPos > 1) {
10966       undoState = true;
10967     }
10968     if (ev.currentPos < ev.states) {
10969       redoState = true;
10970     }
10972     if (undoElemState !== undoState) {
10973       if (undoState) {
10974         undoElem.className = undoElem.className.replace(className, '');
10975       } else {
10976         undoElem.className += className;
10977       }
10978     }
10980     if (redoElemState !== redoState) {
10981       if (redoState) {
10982         redoElem.className = redoElem.className.replace(className, '');
10983       } else {
10984         redoElem.className += className;
10985       }
10986     }
10987   };
10989   /**
10990    * The <code>imageSizeChange</code> application event handler. The GUI element 
10991    * which displays the image dimensions is updated to display the new image 
10992    * size.
10993    *
10994    * <p>Image size refers strictly to the dimensions of the image being edited 
10995    * by the user, that's width and height.
10996    *
10997    * @param {pwlib.appEvent.imageSizeChange} ev The application event object.
10998    */
10999   this.imageSizeChange = function (ev) {
11000     var imageSize  = _self.elems.imageSize;
11001     if (imageSize) {
11002       imageSize.replaceChild(doc.createTextNode(ev.width + 'x' + ev.height), 
11003           imageSize.firstChild);
11004     }
11005   };
11007   /**
11008    * The <code>canvasSizeChange</code> application event handler. The Canvas 
11009    * container element dimensions are updated to the new values and the Hand 
11010    * tool is enabled/disabled as necessary.
11011    *
11012    * <p>Canvas size refers strictly to the dimensions of the Canvas elements in 
11013    * the browser, changed with CSS style properties, width and height. Scaling 
11014    * of the Canvas elements is applied when the user zooms the image or when the 
11015    * browser changes the render DPI / zoom.
11016    *
11017    * @param {pwlib.appEvent.canvasSizeChange} ev The application event object.
11018    */
11019   this.canvasSizeChange = function (ev) {
11020     var canvasContainer = _self.elems.canvasContainer,
11021         canvasResizer   = _self.canvasResizer,
11022         className       = ' ' + _self.classPrefix + 'disabled',
11023         hand            = _self.tools.hand,
11024         resizeHandle    = canvasResizer.resizeHandle,
11025         viewport        = _self.elems.viewport;
11027     // Update the Canvas container to be the same size as the Canvas elements.
11028     canvasContainer.style.width  = ev.width  + 'px';
11029     canvasContainer.style.height = ev.height + 'px';
11031     if (resizeHandle) {
11032       resizeHandle.style.top  = ev.height + 'px';
11033       resizeHandle.style.left = ev.width  + 'px';
11034     }
11036     if (!hand || !viewport) {
11037       return;
11038     }
11040     // Update Hand tool state.
11041     var cs         = win.getComputedStyle(viewport, null),
11042         vwidth     = parseInt(cs.width),
11043         vheight    = parseInt(cs.height),
11044         enableHand = false,
11045         handState  = hand.className.indexOf(className) === -1;
11047     if (vheight < ev.height || vwidth < ev.width) {
11048       enableHand = true;
11049     }
11051     if (enableHand && !handState) {
11052       hand.className = hand.className.replace(className, '');
11053     } else if (!enableHand && handState) {
11054       hand.className += className;
11055     }
11057     if (!enableHand && app.tool && app.tool._id === 'hand' && 'prevTool' in 
11058         app.tool) {
11059       app.toolActivate(app.tool.prevTool);
11060     }
11061   };
11063   /**
11064    * The <code>imageZoom</code> application event handler. The GUI input element 
11065    * which displays the image zoom level is updated to display the new value.
11066    *
11067    * @param {pwlib.appEvent.imageZoom} ev The application event object.
11068    */
11069   this.imageZoom = function (ev) {
11070     var elem  = _self.inputs.imageZoom,
11071         val   = MathRound(ev.zoom * 100);
11072     if (elem && elem.value != val) {
11073       elem.value = val;
11074     }
11075   };
11077   /**
11078    * The <code>configChange</code> application event handler. This method 
11079    * ensures the GUI input elements stay up-to-date when some PaintWeb 
11080    * configuration is modified.
11081    *
11082    * @param {pwlib.appEvent.configChange} ev The application event object.
11083    */
11084   this.configChangeHandler = function (ev) {
11085     var cfg = '', input;
11086     if (ev.group) {
11087       cfg = ev.group.replace('.', '_') + '_';
11088     }
11089     cfg += ev.config;
11090     input = _self.inputs[cfg];
11092     // Handle changes for color inputs.
11093     if (!input && (input = _self.colorInputs[cfg])) {
11094       var color = ev.value.replace(/\s+/g, '').
11095                     replace(/^rgba\(/, '').replace(/\)$/, '');
11097       color = color.split(',');
11098       input.updateColor({
11099         red:   color[0] / 255,
11100         green: color[1] / 255,
11101         blue:  color[2] / 255,
11102         alpha: color[3]
11103       });
11105       return;
11106     }
11108     if (!input) {
11109       return;
11110     }
11112     var tag = input.tagName.toLowerCase(),
11113         isInput = tag === 'select' || tag === 'input' || tag === 'textarea';
11115     if (isInput) {
11116       if (input.type === 'checkbox' && input.checked !== ev.value) {
11117         input.checked = ev.value;
11118       }
11119       if (input.type !== 'checkbox' && input.value !== ev.value) {
11120         input.value = ev.value;
11121       }
11123       return;
11124     }
11126     var classActive = ' ' + _self.className + 'configActive';
11128     if (input.hasAttribute('data-pwConfigToggle')) {
11129       var inputActive = input.className.indexOf(classActive) !== -1;
11131       if (ev.value && !inputActive) {
11132         input.className += classActive;
11133       } else if (!ev.value && inputActive) {
11134         input.className = input.className.replace(classActive, '');
11135       }
11136     }
11138     var classActive = ' ' + _self.className + 'configActive',
11139         prevValElem = _self.inputValues[cfg + '_' + ev.previousValue],
11140         valElem = _self.inputValues[cfg + '_' + ev.value];
11142     if (prevValElem && prevValElem.className.indexOf(classActive) !== -1) {
11143       prevValElem.className = prevValElem.className.replace(classActive, '');
11144     }
11146     if (valElem && valElem.className.indexOf(classActive) === -1) {
11147       valElem.className += classActive;
11148     }
11149   };
11151   /**
11152    * The <code>click</code> event handler for DOM elements associated to 
11153    * PaintWeb configuration values. These elements rely on parent elements which 
11154    * are associated to configuration properties.
11155    *
11156    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
11157    *
11158    * @param {Event} ev The DOM Event object.
11159    */
11160   this.configValueClick = function (ev) {
11161     var pNode = this.parentNode,
11162         input = pNode._pwConfigParent,
11163         val = pNode.getAttribute('data-pwConfigValue');
11165     if (!input || !input._pwConfigProperty) {
11166       return;
11167     }
11169     ev.preventDefault();
11171     var className = ' ' + _self.classPrefix + 'configActive',
11172         groupRef = input._pwConfigGroupRef,
11173         group = input._pwConfigGroup,
11174         prop = input._pwConfigProperty,
11175         prevVal = groupRef[prop],
11176         prevValElem = _self.inputValues[group.replace('.', '_') + '_' + prop 
11177           + '_' + prevVal];
11179     if (prevVal == val) {
11180       return;
11181     }
11183     if (prevValElem && prevValElem.className.indexOf(className) !== -1) {
11184       prevValElem.className = prevValElem.className.replace(className, '');
11185     }
11187     groupRef[prop] = val;
11189     if (pNode.className.indexOf(className) === -1) {
11190       pNode.className += className;
11191     }
11193     app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group, 
11194           groupRef));
11195   };
11197   /**
11198    * The <code>change</code> event handler for input elements associated to 
11199    * PaintWeb configuration properties.
11200    *
11201    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
11202    */
11203   this.configInputChange = function () {
11204     if (!this._pwConfigProperty) {
11205       return;
11206     }
11208     var val = this.type === 'checkbox' ? this.checked : this.value,
11209         groupRef = this._pwConfigGroupRef,
11210         group = this._pwConfigGroup,
11211         prop = this._pwConfigProperty,
11212         prevVal = groupRef[prop];
11214     if (this.getAttribute('type') === 'number') {
11215       val = parseInt(val);
11216       if (val != this.value) {
11217         this.value = val;
11218       }
11219     }
11221     if (val == prevVal) {
11222       return;
11223     }
11225     groupRef[prop] = val;
11227     app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group, 
11228           groupRef));
11229   };
11231   /**
11232    * The <code>click</code> event handler for DOM elements associated to boolean 
11233    * configuration properties. These elements only toggle the true/false value 
11234    * of the configuration property.
11235    *
11236    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
11237    *
11238    * @param {Event} ev The DOM Event object.
11239    */
11240   this.configToggleClick = function (ev) {
11241     var className = ' ' + _self.classPrefix + 'configActive',
11242         pNode = this.parentNode,
11243         groupRef = pNode._pwConfigGroupRef,
11244         group = pNode._pwConfigGroup,
11245         prop = pNode._pwConfigProperty,
11246         elemActive = pNode.className.indexOf(className) !== -1;
11248     ev.preventDefault();
11250     groupRef[prop] = !groupRef[prop];
11252     if (groupRef[prop] && !elemActive) {
11253       pNode.className += className;
11254     } else if (!groupRef[prop] && elemActive) {
11255       pNode.className = pNode.className.replace(className, '');
11256     }
11258     app.events.dispatch(new appEvent.configChange(groupRef[prop], 
11259           !groupRef[prop], prop, group, groupRef));
11260   };
11262   /**
11263    * The <code>shadowAllow</code> application event handler. This method 
11264    * shows/hide the shadow tab when shadows are allowed/disallowed.
11265    *
11266    * @param {pwlib.appEvent.shadowAllow} ev The application event object.
11267    */
11268   this.shadowAllow = function (ev) {
11269     if ('shadow' in _self.tabPanels.main.tabs) {
11270       if (ev.allowed) {
11271         _self.tabPanels.main.tabShow('shadow');
11272       } else {
11273         _self.tabPanels.main.tabHide('shadow');
11274       }
11275     }
11276   };
11278   /**
11279    * The <code>clipboardUpdate</code> application event handler. The GUI element 
11280    * associated to the <code>clipboardPaste</code> command is updated to be 
11281    * disabled/enabled depending on the event.
11282    *
11283    * @param {pwlib.appEvent.clipboardUpdate} ev The application event object.
11284    */
11285   this.clipboardUpdate = function (ev) {
11286     var classDisabled = ' ' + _self.classPrefix + 'disabled',
11287         elem, elemEnabled,
11288         elems = [_self.commands.clipboardPaste, 
11289         _self.elems.selTab_clipboardPaste];
11291     for (var i = 0, n = elems.length; i < n; i++) {
11292       elem = elems[i];
11293       if (!elem) {
11294         continue;
11295       }
11297       elemEnabled = elem.className.indexOf(classDisabled) === -1;
11299       if (!ev.data && elemEnabled) {
11300         elem.className += classDisabled;
11301       } else if (ev.data && !elemEnabled) {
11302         elem.className = elem.className.replace(classDisabled, '');
11303       }
11304     }
11305   };
11307   /**
11308    * The <code>selectionChange</code> application event handler. The GUI 
11309    * elements associated to the <code>selectionCut</code> and 
11310    * <code>selectionCopy</code> commands are updated to be disabled/enabled 
11311    * depending on the event.
11312    *
11313    * @param {pwlib.appEvent.selectionChange} ev The application event object.
11314    */
11315   this.selectionChange = function (ev) {
11316     var classDisabled  = ' ' + _self.classPrefix + 'disabled',
11317         elem, elemEnabled,
11318         elems = [_self.commands.selectionCut, _self.commands.selectionCopy, 
11319         _self.elems.selTab_selectionCut, _self.elems.selTab_selectionCopy, 
11320         _self.commands.selectionDelete, _self.commands.selectionFill, 
11321         _self.commands.selectionCrop];
11323     for (var i = 0, n = elems.length; i < n; i++) {
11324       elem = elems[i];
11325       if (!elem) {
11326         continue;
11327       }
11329       elemEnabled = elem.className.indexOf(classDisabled) === -1;
11331       if (ev.state === ev.STATE_NONE && elemEnabled) {
11332         elem.className += classDisabled;
11333       } else if (ev.state === ev.STATE_SELECTED && !elemEnabled) {
11334         elem.className = elem.className.replace(classDisabled, '');
11335       }
11336     }
11337   };
11339   /**
11340    * Show the graphical user interface.
11341    *
11342    * <p>This method dispatches the {@link pwlib.appEvent.guiShow} application 
11343    * event.
11344    */
11345   this.show = function () {
11346     var placeholder = config.guiPlaceholder,
11347         className   = this.classPrefix + 'placeholder',
11348         re          = new RegExp('\\b' + className);
11350     if (!re.test(placeholder.className)) {
11351       placeholder.className += ' ' + className;
11352     }
11354     placeholder.focus();
11356     app.events.dispatch(new appEvent.guiShow());
11357   };
11359   /**
11360    * Hide the graphical user interface.
11361    *
11362    * <p>This method dispatches the {@link pwlib.appEvent.guiHide} application 
11363    * event.
11364    */
11365   this.hide = function () {
11366     var placeholder = config.guiPlaceholder,
11367         re = new RegExp('\\b' + this.classPrefix + 'placeholder', 'g');
11369     placeholder.className = placeholder.className.replace(re, '');
11371     app.events.dispatch(new appEvent.guiHide());
11372   };
11374   /**
11375    * The application destroy event handler. This method is invoked by the main 
11376    * PaintWeb application when the instance is destroyed, for the purpose of 
11377    * cleaning-up the GUI-related things from the document add by the current 
11378    * instance.
11379    *
11380    * @private
11381    */
11382   this.destroy = function () {
11383     var placeholder = config.guiPlaceholder;
11385     while(placeholder.hasChildNodes()) {
11386       placeholder.removeChild(placeholder.firstChild);
11387     }
11388   };
11392  * @class A floating panel GUI element.
11394  * @private
11396  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
11398  * @param {Element} container Reference to the DOM element you want to transform 
11399  * into a floating panel.
11400  */
11401 pwlib.guiFloatingPanel = function (gui, container) {
11402   var _self          = this,
11403       appEvent       = pwlib.appEvent,
11404       cStyle         = container.style,
11405       doc            = gui.app.doc,
11406       guiPlaceholder = gui.app.config.guiPlaceholder,
11407       lang           = gui.app.lang,
11408       panels         = gui.floatingPanels,
11409       win            = gui.app.win,
11410       zIndex_step    = 200;
11412   // These hold the mouse starting location during the drag operation.
11413   var mx, my;
11415   // These hold the panel starting location during the drag operation.
11416   var ptop, pleft;
11418   /**
11419    * Panel state: hidden.
11420    * @constant
11421    */
11422   this.STATE_HIDDEN    = 0;
11424   /**
11425    * Panel state: visible.
11426    * @constant
11427    */
11428   this.STATE_VISIBLE   = 1;
11430   /**
11431    * Panel state: minimized.
11432    * @constant
11433    */
11434   this.STATE_MINIMIZED = 3;
11436   /**
11437    * Panel state: the user is dragging the floating panel.
11438    * @constant
11439    */
11440   this.STATE_DRAGGING  = 4;
11442   /**
11443    * Tells the state of the floating panel: hidden/minimized/visible or if it's 
11444    * being dragged.
11445    * @type Number
11446    */
11447   this.state = -1;
11449   /**
11450    * Floating panel ID. This is the ID used in the 
11451    * <var>data-pwFloatingPanel</var> element attribute.
11452    * @type String
11453    */
11454   this.id = null;
11456   /**
11457    * Reference to the floating panel element.
11458    * @type Element
11459    */
11460   this.container = container;
11462   /**
11463    * The viewport element. This element is the first parent element which has 
11464    * the style.overflow set to "auto" or "scroll".
11465    * @type Element
11466    */
11467   this.viewport = null;
11469   /**
11470    * Custom application events interface.
11471    * @type pwlib.appEvents
11472    */
11473   this.events = null;
11475   /**
11476    * The panel content element.
11477    * @type Element
11478    */
11479   this.content = null;
11481   // The initial viewport scroll position.
11482   var vScrollLeft = 0, vScrollTop = 0,
11483       btn_close = null, btn_minimize = null;
11485   /**
11486    * Initialize the floating panel.
11487    * @private
11488    */
11489   function init () {
11490     _self.events = new pwlib.appEvents(_self);
11492     _self.id = _self.container.getAttribute('data-pwFloatingPanel');
11494     var ttl = _self.container.getElementsByTagName('h1')[0],
11495         content = _self.container.getElementsByTagName('div')[0],
11496         cs = win.getComputedStyle(_self.container, null),
11497         zIndex = parseInt(cs.zIndex);
11499     cStyle.zIndex = cs.zIndex;
11501     if (zIndex > panels.zIndex_) {
11502       panels.zIndex_ = zIndex;
11503     }
11505     _self.container.className += ' ' + gui.classPrefix + 'floatingPanel ' +
11506       gui.classPrefix + 'floatingPanel_' + _self.id;
11508     // the content
11509     content.className += ' ' + gui.classPrefix + 'floatingPanel_content';
11510     _self.content = content;
11512     // setup the title element
11513     ttl.className += ' ' + gui.classPrefix + 'floatingPanel_title';
11514     ttl.replaceChild(doc.createTextNode(lang.floatingPanels[_self.id]), 
11515         ttl.firstChild);
11517     ttl.addEventListener('mousedown', ev_mousedown, false);
11519     // allow auto-hide for the panel
11520     if (_self.container.getAttribute('data-pwPanelHide') === 'true') {
11521       _self.hide();
11522     } else {
11523       _self.state = _self.STATE_VISIBLE;
11524     }
11526     // Find the viewport parent element.
11527     var pNode = _self.container.parentNode,
11528         found = null;
11530     while (!found && pNode) {
11531       if (pNode.nodeName.toLowerCase() === 'html') {
11532         found = pNode;
11533         break;
11534       }
11536       cs = win.getComputedStyle(pNode, null);
11537       if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) {
11538         found = pNode;
11539       } else {
11540         pNode = pNode.parentNode;
11541       }
11542     }
11544     _self.viewport = found;
11546     // add the panel minimize button.
11547     btn_minimize = doc.createElement('a');
11548     btn_minimize.href = '#';
11549     btn_minimize.title = lang.floatingPanelMinimize;
11550     btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize';
11551     btn_minimize.addEventListener('click', ev_minimize, false);
11552     btn_minimize.appendChild(doc.createTextNode(btn_minimize.title));
11554     _self.container.insertBefore(btn_minimize, content);
11556     // add the panel close button.
11557     btn_close = doc.createElement('a');
11558     btn_close.href = '#';
11559     btn_close.title = lang.floatingPanelClose;
11560     btn_close.className = gui.classPrefix + 'floatingPanel_close';
11561     btn_close.addEventListener('click', ev_close, false);
11562     btn_close.appendChild(doc.createTextNode(btn_close.title));
11564     _self.container.insertBefore(btn_close, content);
11566     // setup the panel resize handle.
11567     if (_self.container.getAttribute('data-pwPanelResizable') === 'true') {
11568       var resizeHandle = doc.createElement('div');
11569       resizeHandle.className = gui.classPrefix + 'floatingPanel_resizer';
11570       _self.container.appendChild(resizeHandle);
11571       _self.resizer = new pwlib.guiResizer(gui, resizeHandle, _self.container);
11572     }
11573   };
11575   /**
11576    * The <code>click</code> event handler for the panel Minimize button element.
11577    *
11578    * <p>This method dispatches the {@link 
11579    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11580    *
11581    * @private
11582    * @param {Event} ev The DOM Event object.
11583    */
11584   function ev_minimize (ev) {
11585     ev.preventDefault();
11586     this.focus();
11588     var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized';
11590     if (_self.state === _self.STATE_MINIMIZED) {
11591       _self.state = _self.STATE_VISIBLE;
11593       this.title = lang.floatingPanelMinimize;
11594       this.className = gui.classPrefix + 'floatingPanel_minimize';
11595       this.replaceChild(doc.createTextNode(this.title), this.firstChild);
11597       if (_self.container.className.indexOf(classMinimized) !== -1) {
11598         _self.container.className 
11599           = _self.container.className.replace(classMinimized, '');
11600       }
11602     } else if (_self.state === _self.STATE_VISIBLE) {
11603       _self.state = _self.STATE_MINIMIZED;
11605       this.title = lang.floatingPanelRestore;
11606       this.className = gui.classPrefix + 'floatingPanel_restore';
11607       this.replaceChild(doc.createTextNode(this.title), this.firstChild);
11609       if (_self.container.className.indexOf(classMinimized) === -1) {
11610         _self.container.className += classMinimized;
11611       }
11612     }
11614     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11616     _self.bringOnTop();
11617   };
11619   /**
11620    * The <code>click</code> event handler for the panel Close button element.  
11621    * This hides the floating panel.
11622    *
11623    * <p>This method dispatches the {@link 
11624    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11625    *
11626    * @private
11627    * @param {Event} ev The DOM Event object.
11628    */
11629   function ev_close (ev) {
11630     ev.preventDefault();
11631     _self.hide();
11632     guiPlaceholder.focus();
11633   };
11635   /**
11636    * The <code>mousedown</code> event handler. This is invoked when you start 
11637    * dragging the floating panel.
11638    *
11639    * <p>This method dispatches the {@link 
11640    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11641    *
11642    * @private
11643    * @param {Event} ev The DOM Event object.
11644    */
11645   function ev_mousedown (ev) {
11646     _self.state = _self.STATE_DRAGGING;
11648     mx = ev.clientX;
11649     my = ev.clientY;
11651     var cs = win.getComputedStyle(_self.container, null);
11653     ptop  = parseInt(cs.top);
11654     pleft = parseInt(cs.left);
11656     if (_self.viewport) {
11657       vScrollLeft = _self.viewport.scrollLeft;
11658       vScrollTop  = _self.viewport.scrollTop;
11659     }
11661     _self.bringOnTop();
11663     doc.addEventListener('mousemove', ev_mousemove, false);
11664     doc.addEventListener('mouseup',   ev_mouseup,   false);
11666     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11668     if (ev.preventDefault) {
11669       ev.preventDefault();
11670     }
11671   };
11673   /**
11674    * The <code>mousemove</code> event handler. This performs the actual move of 
11675    * the floating panel.
11676    *
11677    * @private
11678    * @param {Event} ev The DOM Event object.
11679    */
11680   function ev_mousemove (ev) {
11681     var x = pleft + ev.clientX - mx,
11682         y = ptop  + ev.clientY - my;
11684     if (_self.viewport) {
11685       if (_self.viewport.scrollLeft !== vScrollLeft) {
11686         x += _self.viewport.scrollLeft - vScrollLeft;
11687       }
11688       if (_self.viewport.scrollTop !== vScrollTop) {
11689         y += _self.viewport.scrollTop - vScrollTop;
11690       }
11691     }
11693     cStyle.left = x + 'px';
11694     cStyle.top  = y + 'px';
11695   };
11697   /**
11698    * The <code>mouseup</code> event handler. This ends the panel drag operation.
11699    *
11700    * <p>This method dispatches the {@link 
11701    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11702    *
11703    * @private
11704    * @param {Event} ev The DOM Event object.
11705    */
11706   function ev_mouseup (ev) {
11707     if (_self.container.className.indexOf(' ' + gui.classPrefix 
11708           + 'floatingPanel_minimized') !== -1) {
11709       _self.state = _self.STATE_MINIMIZED;
11710     } else {
11711       _self.state = _self.STATE_VISIBLE;
11712     }
11714     doc.removeEventListener('mousemove', ev_mousemove, false);
11715     doc.removeEventListener('mouseup',   ev_mouseup,   false);
11717     guiPlaceholder.focus();
11718     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11719   };
11721   /**
11722    * Bring the panel to the top. This method makes sure the current floating 
11723    * panel is visible.
11724    */
11725   this.bringOnTop = function () {
11726     panels.zIndex_ += zIndex_step;
11727     cStyle.zIndex = panels.zIndex_;
11728   };
11730   /**
11731    * Hide the panel.
11732    *
11733    * <p>This method dispatches the {@link 
11734    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11735    */
11736   this.hide = function () {
11737     cStyle.display = 'none';
11738     _self.state = _self.STATE_HIDDEN;
11739     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11740   };
11742   /**
11743    * Show the panel.
11744    *
11745    * <p>This method dispatches the {@link 
11746    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11747    */
11748   this.show = function () {
11749     if (_self.state === _self.STATE_VISIBLE) {
11750       return;
11751     }
11753     cStyle.display = 'block';
11754     _self.state = _self.STATE_VISIBLE;
11756     var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized';
11758     if (_self.container.className.indexOf(classMinimized) !== -1) {
11759       _self.container.className 
11760         = _self.container.className.replace(classMinimized, '');
11762       btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize';
11763       btn_minimize.title = lang.floatingPanelMinimize;
11764       btn_minimize.replaceChild(doc.createTextNode(btn_minimize.title), 
11765           btn_minimize.firstChild);
11766     }
11768     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
11770     _self.bringOnTop();
11771   };
11773   /**
11774    * Toggle the panel visibility.
11775    *
11776    * <p>This method dispatches the {@link 
11777    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
11778    */
11779   this.toggle = function () {
11780     if (_self.state === _self.STATE_VISIBLE || _self.state === 
11781         _self.STATE_MINIMIZED) {
11782       _self.hide();
11783     } else {
11784       _self.show();
11785     }
11786   };
11788   init();
11792  * @class The state change event for the floating panel. This event is fired 
11793  * when the floating panel changes its state. This event is not cancelable.
11795  * @augments pwlib.appEvent
11797  * @param {Number} state The floating panel state.
11798  */
11799 pwlib.appEvent.guiFloatingPanelStateChange = function (state) {
11800   /**
11801    * Panel state: hidden.
11802    * @constant
11803    */
11804   this.STATE_HIDDEN    = 0;
11806   /**
11807    * Panel state: visible.
11808    * @constant
11809    */
11810   this.STATE_VISIBLE   = 1;
11812   /**
11813    * Panel state: minimized.
11814    * @constant
11815    */
11816   this.STATE_MINIMIZED = 3;
11818   /**
11819    * Panel state: the user is dragging the floating panel.
11820    * @constant
11821    */
11822   this.STATE_DRAGGING  = 4;
11824   /**
11825    * The current floating panel state.
11826    * @type Number
11827    */
11828   this.state = state;
11830   pwlib.appEvent.call(this, 'guiFloatingPanelStateChange');
11834  * @class Resize handler.
11836  * @private
11838  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
11840  * @param {Element} resizeHandle Reference to the resize handle DOM element.  
11841  * This is the element users will be able to drag to achieve the resize effect 
11842  * on the <var>container</var> element.
11844  * @param {Element} container Reference to the container DOM element. This is 
11845  * the element users will be able to resize using the <var>resizeHandle</var> 
11846  * element.
11847  */
11848 pwlib.guiResizer = function (gui, resizeHandle, container) {
11849   var _self          = this,
11850       cStyle         = container.style,
11851       doc            = gui.app.doc,
11852       guiResizeEnd   = pwlib.appEvent.guiResizeEnd,
11853       guiResizeStart = pwlib.appEvent.guiResizeStart,
11854       win            = gui.app.win;
11856   /**
11857    * Custom application events interface.
11858    * @type pwlib.appEvents
11859    */
11860   this.events = null;
11862   /**
11863    * The resize handle DOM element.
11864    * @type Element
11865    */
11866   this.resizeHandle = resizeHandle;
11868   /**
11869    * The container DOM element. This is the element that's resized by the user 
11870    * when he/she drags the resize handle.
11871    * @type Element
11872    */
11873   this.container = container;
11875   /**
11876    * The viewport element. This element is the first parent element which has 
11877    * the style.overflow set to "auto" or "scroll".
11878    * @type Element
11879    */
11880   this.viewport = null;
11882   /**
11883    * Tells if the user resizing the container now.
11884    * @type Boolean
11885    */
11886   this.resizing = false;
11888   // The initial position of the mouse.
11889   var mx = 0, my = 0;
11891   // The initial container dimensions.
11892   var cWidth = 0, cHeight = 0;
11894   // The initial viewport scroll position.
11895   var vScrollLeft = 0, vScrollTop = 0;
11897   /**
11898    * Initialize the resize functionality.
11899    * @private
11900    */
11901   function init () {
11902     _self.events = new pwlib.appEvents(_self);
11903     resizeHandle.addEventListener('mousedown', ev_mousedown, false);
11905     // Find the viewport parent element.
11906     var cs, pNode = _self.container.parentNode,
11907         found = null;
11908     while (!found && pNode) {
11909       if (pNode.nodeName.toLowerCase() === 'html') {
11910         found = pNode;
11911         break;
11912       }
11914       cs = win.getComputedStyle(pNode, null);
11915       if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) {
11916         found = pNode;
11917       } else {
11918         pNode = pNode.parentNode;
11919       }
11920     }
11922     _self.viewport = found;
11923   };
11925   /**
11926    * The <code>mousedown</code> event handler. This starts the resize operation.
11927    *
11928    * <p>This function dispatches the {@link pwlib.appEvent.guiResizeStart} 
11929    * event.
11930    *
11931    * @private
11932    * @param {Event} ev The DOM Event object.
11933    */
11934   function ev_mousedown (ev) {
11935     mx = ev.clientX;
11936     my = ev.clientY;
11938     var cs = win.getComputedStyle(_self.container, null);
11939     cWidth  = parseInt(cs.width);
11940     cHeight = parseInt(cs.height);
11942     var cancel = _self.events.dispatch(new guiResizeStart(mx, my, cWidth, 
11943           cHeight));
11945     if (cancel) {
11946       return;
11947     }
11949     if (_self.viewport) {
11950       vScrollLeft = _self.viewport.scrollLeft;
11951       vScrollTop  = _self.viewport.scrollTop;
11952     }
11954     _self.resizing = true;
11955     doc.addEventListener('mousemove', ev_mousemove, false);
11956     doc.addEventListener('mouseup',   ev_mouseup,   false);
11958     if (ev.preventDefault) {
11959       ev.preventDefault();
11960     }
11962     if (ev.stopPropagation) {
11963       ev.stopPropagation();
11964     }
11965   };
11967   /**
11968    * The <code>mousemove</code> event handler. This performs the actual resizing 
11969    * of the <var>container</var> element.
11970    *
11971    * @private
11972    * @param {Event} ev The DOM Event object.
11973    */
11974   function ev_mousemove (ev) {
11975     var w = cWidth  + ev.clientX - mx,
11976         h = cHeight + ev.clientY - my;
11978     if (_self.viewport) {
11979       if (_self.viewport.scrollLeft !== vScrollLeft) {
11980         w += _self.viewport.scrollLeft - vScrollLeft;
11981       }
11982       if (_self.viewport.scrollTop !== vScrollTop) {
11983         h += _self.viewport.scrollTop - vScrollTop;
11984       }
11985     }
11987     cStyle.width  = w + 'px';
11988     cStyle.height = h + 'px';
11989   };
11991   /**
11992    * The <code>mouseup</code> event handler. This ends the resize operation.
11993    *
11994    * <p>This function dispatches the {@link pwlib.appEvent.guiResizeEnd} event.
11995    *
11996    * @private
11997    * @param {Event} ev The DOM Event object.
11998    */
11999   function ev_mouseup (ev) {
12000     var cancel = _self.events.dispatch(new guiResizeEnd(ev.clientX, ev.clientY, 
12001           parseInt(cStyle.width), parseInt(cStyle.height)));
12003     if (cancel) {
12004       return;
12005     }
12007     _self.resizing = false;
12008     doc.removeEventListener('mousemove', ev_mousemove, false);
12009     doc.removeEventListener('mouseup',   ev_mouseup,   false);
12010   };
12012   init();
12016  * @class The GUI element resize start event. This event is cancelable.
12018  * @augments pwlib.appEvent
12020  * @param {Number} x The mouse location on the x-axis.
12021  * @param {Number} y The mouse location on the y-axis.
12022  * @param {Number} width The element width.
12023  * @param {Number} height The element height.
12024  */
12025 pwlib.appEvent.guiResizeStart = function (x, y, width, height) {
12026   /**
12027    * The mouse location on the x-axis.
12028    * @type Number
12029    */
12030   this.x = x;
12032   /**
12033    * The mouse location on the y-axis.
12034    * @type Number
12035    */
12036   this.y = y;
12038   /**
12039    * The element width.
12040    * @type Number
12041    */
12042   this.width = width;
12044   /**
12045    * The element height.
12046    * @type Number
12047    */
12048   this.height = height;
12050   pwlib.appEvent.call(this, 'guiResizeStart', true);
12054  * @class The GUI element resize end event. This event is cancelable.
12056  * @augments pwlib.appEvent
12058  * @param {Number} x The mouse location on the x-axis.
12059  * @param {Number} y The mouse location on the y-axis.
12060  * @param {Number} width The element width.
12061  * @param {Number} height The element height.
12062  */
12063 pwlib.appEvent.guiResizeEnd = function (x, y, width, height) {
12064   /**
12065    * The mouse location on the x-axis.
12066    * @type Number
12067    */
12068   this.x = x;
12070   /**
12071    * The mouse location on the y-axis.
12072    * @type Number
12073    */
12074   this.y = y;
12076   /**
12077    * The element width.
12078    * @type Number
12079    */
12080   this.width = width;
12082   /**
12083    * The element height.
12084    * @type Number
12085    */
12086   this.height = height;
12088   pwlib.appEvent.call(this, 'guiResizeEnd', true);
12092  * @class The tabbed panel GUI component.
12094  * @private
12096  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
12098  * @param {Element} panel Reference to the panel DOM element.
12099  */
12100 pwlib.guiTabPanel = function (gui, panel) {
12101   var _self    = this,
12102       appEvent = pwlib.appEvent,
12103       doc      = gui.app.doc,
12104       lang     = gui.app.lang;
12106   /**
12107    * Custom application events interface.
12108    * @type pwlib.appEvents
12109    */
12110   this.events = null;
12112   /**
12113    * Panel ID. The ID is the same as the data-pwTabPanel attribute value of the 
12114    * panel DOM element .
12115    *
12116    * @type String.
12117    */
12118   this.id = null;
12120   /**
12121    * Holds references to the DOM element of each tab and tab button.
12122    * @type Object
12123    */
12124   this.tabs = {};
12126   /**
12127    * Reference to the tab buttons DOM element.
12128    * @type Element
12129    */
12130   this.tabButtons = null;
12132   /**
12133    * The panel container DOM element.
12134    * @type Element
12135    */
12136   this.container = panel;
12138   /**
12139    * Holds the ID of the currently active tab.
12140    * @type String
12141    */
12142   this.tabId = null;
12144   /**
12145    * Holds the ID of the previously active tab.
12146    *
12147    * @private
12148    * @type String
12149    */
12150   var prevTabId_ = null;
12152   /**
12153    * Initialize the toolbar functionality.
12154    * @private
12155    */
12156   function init () {
12157     _self.events = new pwlib.appEvents(_self);
12158     _self.id = _self.container.getAttribute('data-pwTabPanel');
12160     // Add two class names, the generic .paintweb_tabPanel and another class 
12161     // name specific to the current tab panel: .paintweb_tabPanel_id. 
12162     _self.container.className += ' ' + gui.classPrefix + 'tabPanel' 
12163       + ' ' + gui.classPrefix + 'tabPanel_' + _self.id;
12165     var tabButtons = doc.createElement('ul'),
12166         tabButton = null,
12167         tabDefault = _self.container.getAttribute('data-pwTabDefault') || null,
12168         childNodes = _self.container.childNodes,
12169         type = Node.ELEMENT_NODE,
12170         elem = null,
12171         tabId = null,
12172         anchor = null;
12174     tabButtons.className = gui.classPrefix + 'tabsList';
12176     // Find all the tabs in the current panel container element.
12177     for (var i = 0; elem = childNodes[i]; i++) {
12178       if (elem.nodeType !== type) {
12179         continue;
12180       }
12182       // A tab is any element with a given data-pwTab attribute.
12183       tabId = elem.getAttribute('data-pwTab');
12184       if (!tabId) {
12185         continue;
12186       }
12188       // two class names, the generic .paintweb_tab and the tab-specific class 
12189       // name .paintweb_tabPanelId_tabId.
12190       elem.className += ' ' + gui.classPrefix + 'tab ' + gui.classPrefix 
12191         + _self.id + '_' + tabId;
12193       tabButton = doc.createElement('li');
12194       tabButton._pwTab = tabId;
12196       anchor = doc.createElement('a');
12197       anchor.href = '#';
12198       anchor.addEventListener('click', ev_tabClick, false);
12200       if (_self.id in lang.tabs) {
12201         anchor.title = lang.tabs[_self.id][tabId + 'Title'] || 
12202           lang.tabs[_self.id][tabId];
12203         anchor.appendChild(doc.createTextNode(lang.tabs[_self.id][tabId]));
12204       }
12206       if ((tabDefault && tabId === tabDefault) ||
12207           (!tabDefault && !_self.tabId)) {
12208         _self.tabId = tabId;
12209         tabButton.className = gui.classPrefix + 'tabActive';
12210       } else {
12211         prevTabId_ = tabId;
12212         elem.style.display = 'none';
12213       }
12215       // automatically hide the tab
12216       if (elem.getAttribute('data-pwTabHide') === 'true') {
12217         tabButton.style.display = 'none';
12218       }
12220       _self.tabs[tabId] = {container: elem, button: tabButton};
12222       tabButton.appendChild(anchor);
12223       tabButtons.appendChild(tabButton);
12224     }
12226     _self.tabButtons = tabButtons;
12227     _self.container.appendChild(tabButtons);
12228   };
12230   /**
12231    * The <code>click</code> event handler for tab buttons. This function simply 
12232    * activates the tab the user clicked.
12233    *
12234    * @private
12235    * @param {Event} ev The DOM Event object.
12236    */
12237   function ev_tabClick (ev) {
12238     ev.preventDefault();
12239     _self.tabActivate(this.parentNode._pwTab);
12240   };
12242   /**
12243    * Activate a tab by ID.
12244    *
12245    * <p>This method dispatches the {@link pwlib.appEvent.guiTabActivate} event.
12246    *
12247    * @param {String} tabId The ID of the tab you want to activate.
12248    * @returns {Boolean} True if the tab has been activated successfully, or 
12249    * false if not.
12250    */
12251   this.tabActivate = function (tabId) {
12252     if (!tabId || !(tabId in this.tabs)) {
12253       return false;
12254     } else if (tabId === this.tabId) {
12255       return true;
12256     }
12258     var ev = new appEvent.guiTabActivate(tabId, this.tabId),
12259         cancel = this.events.dispatch(ev),
12260         elem = null,
12261         tabButton = null;
12263     if (cancel) {
12264       return false;
12265     }
12267     // Deactivate the currently active tab.
12268     if (this.tabId in this.tabs) {
12269       elem = this.tabs[this.tabId].container;
12270       elem.style.display = 'none';
12271       tabButton = this.tabs[this.tabId].button;
12272       tabButton.className = '';
12273       prevTabId_ = this.tabId;
12274     }
12276     // Activate the new tab.
12277     elem = this.tabs[tabId].container;
12278     elem.style.display = '';
12279     tabButton = this.tabs[tabId].button;
12280     tabButton.className = gui.classPrefix + 'tabActive';
12281     tabButton.style.display = ''; // make sure the tab is not hidden
12282     tabButton.firstChild.focus();
12283     this.tabId = tabId;
12285     return true;
12286   };
12288   /**
12289    * Hide a tab by ID.
12290    *
12291    * @param {String} tabId The ID of the tab you want to hide.
12292    * @returns {Boolean} True if the tab has been hidden successfully, or false 
12293    * if not.
12294    */
12295   this.tabHide = function (tabId) {
12296     if (!(tabId in this.tabs)) {
12297       return false;
12298     }
12300     if (this.tabId === tabId) {
12301       this.tabActivate(prevTabId_);
12302     }
12304     this.tabs[tabId].button.style.display = 'none';
12306     return true;
12307   };
12309   /**
12310    * Show a tab by ID.
12311    *
12312    * @param {String} tabId The ID of the tab you want to show.
12313    * @returns {Boolean} True if the tab has been displayed successfully, or 
12314    * false if not.
12315    */
12316   this.tabShow = function (tabId) {
12317     if (!(tabId in this.tabs)) {
12318       return false;
12319     }
12321     this.tabs[tabId].button.style.display = '';
12323     return true;
12324   };
12326   init();
12330  * @class The GUI tab activation event. This event is cancelable.
12332  * @augments pwlib.appEvent
12334  * @param {String} tabId The ID of the tab being activated.
12335  * @param {String} prevTabId The ID of the previously active tab.
12336  */
12337 pwlib.appEvent.guiTabActivate = function (tabId, prevTabId) {
12338   /**
12339    * The ID of the tab being activated.
12340    * @type String
12341    */
12342   this.tabId = tabId;
12344   /**
12345    * The ID of the previously active tab.
12346    * @type String
12347    */
12348   this.prevTabId = prevTabId;
12350   pwlib.appEvent.call(this, 'guiTabActivate', true);
12354  * @class The color input GUI component.
12356  * @private
12358  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
12360  * @param {Element} input Reference to the DOM input element. This can be 
12361  * a span, a div, or any other tag.
12362  */
12363 pwlib.guiColorInput = function (gui, input) {
12364   var _self      = this,
12365       colormixer = null,
12366       config     = gui.app.config,
12367       doc        = gui.app.doc,
12368       MathRound  = Math.round,
12369       lang       = gui.app.lang;
12371   /**
12372    * Color input ID. The ID is the same as the data-pwColorInput attribute value 
12373    * of the DOM input element .
12374    *
12375    * @type String.
12376    */
12377   this.id = null;
12379   /**
12380    * The color input element DOM reference.
12381    *
12382    * @type Element
12383    */
12384   this.input = input;
12386   /**
12387    * The configuration property to which this color input is attached to.
12388    * @type String
12389    */
12390   this.configProperty = null;
12392   /**
12393    * The configuration group to which this color input is attached to.
12394    * @type String
12395    */
12396   this.configGroup = null;
12398   /**
12399    * Reference to the configuration object which holds the color input value.
12400    * @type String
12401    */
12402   this.configGroupRef = null;
12404   /**
12405    * Holds the current color displayed by the input.
12406    *
12407    * @type Object
12408    */
12409   this.color = {red: 0, green: 0, blue: 0, alpha: 0};
12411   /**
12412    * Initialize the color input functionality.
12413    * @private
12414    */
12415   function init () {
12416     var cfgAttr     = _self.input.getAttribute('data-pwColorInput'),
12417         cfgNoDots   = cfgAttr.replace('.', '_'),
12418         cfgArray    = cfgAttr.split('.'),
12419         cfgProp     = cfgArray.pop(),
12420         cfgGroup    = cfgArray.join('.'),
12421         cfgGroupRef = config,
12422         langGroup   = lang.inputs,
12423         labelElem   = _self.input.parentNode,
12424         anchor      = doc.createElement('a'),
12425         color;
12427     for (var i = 0, n = cfgArray.length; i < n; i++) {
12428       cfgGroupRef = cfgGroupRef[cfgArray[i]];
12429       langGroup = langGroup[cfgArray[i]];
12430     }
12432     _self.configProperty = cfgProp;
12433     _self.configGroup = cfgGroup;
12434     _self.configGroupRef = cfgGroupRef;
12436     _self.id = cfgNoDots;
12438     _self.input.className += ' ' + gui.classPrefix + 'colorInput' 
12439       + ' ' + gui.classPrefix + _self.id;
12441     labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
12442         labelElem.firstChild);
12444     color = _self.configGroupRef[_self.configProperty];
12445     color = color.replace(/\s+/g, '').replace(/^rgba\(/, '').replace(/\)$/, '');
12446     color = color.split(',');
12447     _self.color.red   = color[0] / 255;
12448     _self.color.green = color[1] / 255;
12449     _self.color.blue  = color[2] / 255;
12450     _self.color.alpha = color[3];
12452     anchor.style.backgroundColor = 'rgb(' + color[0] + ',' + color[1] + ',' 
12453         + color[2] + ')';
12454     anchor.style.opacity = color[3];
12456     anchor.href = '#';
12457     anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
12458     anchor.appendChild(doc.createTextNode(lang.inputs.colorInputAnchorContent));
12459     anchor.addEventListener('click', ev_input_click, false);
12461     _self.input.replaceChild(anchor, _self.input.firstChild);
12462   };
12464   /**
12465    * The <code>click</code> event handler for the color input element. This 
12466    * function shows/hides the Color Mixer panel.
12467    *
12468    * @private
12469    * @param {Event} ev The DOM Event object.
12470    */
12471   function ev_input_click (ev) {
12472     ev.preventDefault();
12474     if (!colormixer) {
12475       colormixer = gui.app.extensions.colormixer;
12476     }
12478     if (!colormixer.targetInput || colormixer.targetInput.id !== _self.id) {
12479       colormixer.show({
12480           id: _self.id,
12481           configProperty: _self.configProperty,
12482           configGroup: _self.configGroup,
12483           configGroupRef: _self.configGroupRef,
12484           show: colormixer_show,
12485           hide: colormixer_hide
12486         }, _self.color);
12488     } else {
12489       colormixer.hide();
12490     }
12491   };
12493   /**
12494    * The color mixer <code>show</code> event handler. This function is invoked 
12495    * when the color mixer is shown.
12496    * @private
12497    */
12498   function colormixer_show () {
12499     var classActive = ' ' + gui.classPrefix + 'colorInputActive',
12500         elemActive = _self.input.className.indexOf(classActive) !== -1;
12502     if (!elemActive) {
12503       _self.input.className += classActive;
12504     }
12505   };
12507   /**
12508    * The color mixer <code>hide</code> event handler. This function is invoked 
12509    * when the color mixer is hidden.
12510    * @private
12511    */
12512   function colormixer_hide () {
12513     var classActive = ' ' + gui.classPrefix + 'colorInputActive',
12514         elemActive = _self.input.className.indexOf(classActive) !== -1;
12516     if (elemActive) {
12517       _self.input.className = _self.input.className.replace(classActive, '');
12518     }
12519   };
12521   /**
12522    * Update color. This method allows the change of the color values associated 
12523    * to the current color input.
12524    *
12525    * <p>This method is used by the color picker tool and by the global GUI 
12526    * <code>configChange</code> application event handler.
12527    *
12528    * @param {Object} color The new color values. The object must have four 
12529    * properties: <var>red</var>, <var>green</var>, <var>blue</var> and 
12530    * <var>alpha</var>. All values must be between 0 and 1.
12531    */
12532   this.updateColor = function (color) {
12533     var anchor = _self.input.firstChild.style;
12535     anchor.opacity         = color.alpha;
12536     anchor.backgroundColor = 'rgb(' + MathRound(color.red   * 255) + ',' +
12537                                       MathRound(color.green * 255) + ',' +
12538                                       MathRound(color.blue  * 255) + ')';
12539     _self.color.red   = color.red;
12540     _self.color.green = color.green;
12541     _self.color.blue  = color.blue;
12542     _self.color.alpha = color.alpha;
12543   };
12545   init();
12548 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
12550 pwlib.fileCache['interfaces/default/layout.xhtml'] = 
12551 "<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">  <h1 class=\"paintweb_appTitle\">PaintWeb<\/h1>  <div data-pwTabPanel=\"main\" data-pwTabDefault=\"main\">  <div data-pwTab=\"main\">  <ul id=\"tools\">  <li data-pwCommand=\"historyUndo\">Undo<\/li>  <li data-pwCommand=\"historyRedo\">Redo<\/li>  <li class=\"paintweb_toolSeparator\">&#160;<\/li>  <li data-pwCommand=\"imageClear\">Clear image<\/li>  <li data-pwCommand=\"imageSave\">Save image<\/li>  <li data-pwTool=\"insertimg\">Insert image<\/li>  <li class=\"paintweb_toolSeparator\">&#160;<\/li>  <li data-pwCommand=\"selectionCut\">Cut selection<\/li>  <li data-pwCommand=\"selectionCopy\">Copy selection<\/li>  <li data-pwCommand=\"clipboardPaste\">Clipboard paste<\/li>  <li class=\"paintweb_toolSeparator\">&#160;<\/li>  <li data-pwTool=\"cpicker\">Color picker<\/li>  <li class=\"paintweb_toolsWrap\">&#160;<\/li>  <li data-pwTool=\"selection\">Selection<\/li>  <li data-pwTool=\"hand\">Hand<\/li>  <li class=\"paintweb_toolSeparator\">&#160;<\/li>  <li data-pwTool=\"rectangle\">Rectangle<\/li>  <li data-pwTool=\"ellipse\">Ellipse<\/li>  <li data-pwTool=\"polygon\">Polygon<\/li>  <li data-pwTool=\"line\">Line<\/li>  <li data-pwTool=\"bcurve\">B\u00e9zier curve<\/li>  <li data-pwTool=\"text\">Text<\/li>  <li data-pwTool=\"pencil\">Pencil<\/li>  <li class=\"paintweb_toolSeparator\">&#160;<\/li>  <li data-pwTool=\"eraser\">Eraser<\/li>  <\/ul>  <div class=\"paintweb_strokeFillStyles\">  <p class=\"paintweb_opt_fillStyle\">Fill <span   data-pwColorInput=\"fillStyle\">&#160;<\/span>  <\/p>  <p class=\"paintweb_opt_strokeStyle\">Stroke <span   data-pwColorInput=\"strokeStyle\">&#160;<\/span>  <\/p>  <\/div>  <\/div>  <div data-pwTab=\"line\" data-pwTabHide=\"true\">  <p class=\"paintweb_opt_lineWidth\"><label>Line width <input   data-pwConfig=\"line.lineWidth\" type=\"number\" min=\"1\" value=\"1\"   \/><\/label><\/p>  <p class=\"paintweb_opt_miterLimit\"><label>Miter limit <input    data-pwConfig=\"line.miterLimit\" type=\"number\" min=\"1\" value=\"10\"   \/><\/label><\/p>  <div data-pwConfig=\"line.lineCap\">  <p>Line cap<\/p>  <div data-pwConfigValue=\"butt\">Butt<\/div>  <div data-pwConfigValue=\"square\">Square<\/div>  <div data-pwConfigValue=\"round\">Round<\/div>  <\/div>  <div data-pwConfig=\"line.lineJoin\">  <p>Line join<\/p>  <div data-pwConfigValue=\"miter\">Miter<\/div>  <div data-pwConfigValue=\"round\">Round<\/div>  <div data-pwConfigValue=\"bevel\">Bevel<\/div>  <\/div>  <div data-pwConfig=\"shapeType\">  <p>Shape type<\/p>  <div data-pwConfigValue=\"both\">Both<\/div>  <div data-pwConfigValue=\"fill\">Fill<\/div>  <div data-pwConfigValue=\"stroke\">Stroke<\/div>  <\/div>  <\/div>  <div data-pwTab=\"selection\" data-pwTabHide=\"true\">  <p data-pwId=\"selTab_selectionCut\">Cut selection<\/p>  <p data-pwId=\"selTab_selectionCopy\">Copy selection<\/p>  <p data-pwId=\"selTab_clipboardPaste\">Clipboard paste<\/p>  <p data-pwCommand=\"selectionCrop\">Crop selection<\/p>  <p data-pwCommand=\"selectionDelete\">Delete selection<\/p>  <p data-pwCommand=\"selectionFill\">Fill selection<\/p>  <p class=\"paintweb_opt_selectionTransparent\">  <label><input data-pwConfig=\"selection.transparent\" type=\"checkbox\"   value=\"1\" checked=\"checked\" \/> Transparent background<\/label>  <\/p>  <p class=\"paintweb_opt_selectionTransform\">  <label><input data-pwConfig=\"selection.transform\" type=\"checkbox\"   value=\"1\" \/> Transformation mode<\/label>  <\/p>  <\/div>  <div data-pwTab=\"text\" data-pwTabHide=\"true\">  <p class=\"paintweb_opt_fontFamily\">  <label for=\"fontFamily\">Font family:<\/label>  <select id=\"fontFamily\" data-pwConfig=\"text.fontFamily\"><\/select>  <\/p>  <p class=\"paintweb_opt_fontSize\">  <label for=\"fontSize\">Font size:<\/label>  <input id=\"fontSize\" data-pwConfig=\"text.fontSize\" type=\"number\" min=\"6\"   value=\"12\" \/>  <\/p>  <div data-pwConfigToggle=\"text.bold\">Bold<\/div>  <div data-pwConfigToggle=\"text.italic\">Italic<\/div>  <div data-pwConfig=\"text.textAlign\">  <p>Text alignment<\/p>  <div data-pwConfigValue=\"left\">Left<\/div>  <div data-pwConfigValue=\"center\">Center<\/div>  <div data-pwConfigValue=\"right\">Right<\/div>  <\/div>  <p class=\"paintweb_opt_textString\">  <label>String <textarea id=\"textString\" rows=\"2\" cols=\"4\">Hello   world!<\/textarea><\/label>  <\/p>  <\/div>  <div data-pwTab=\"shadow\">  <p class=\"paintweb_opt_shadowEnable\"><label><input   data-pwConfig=\"shadow.enable\" type=\"checkbox\" value=\"1\" \/> Draw   shadows<\/label><\/p>  <p class=\"paintweb_opt_shadowColor\">Color <span   data-pwColorInput=\"shadow.shadowColor\">&#160;<\/span>  <\/p>  <p class=\"paintweb_opt_shadowOffsetX\">  <label>Offset X  <input data-pwConfig=\"shadow.shadowOffsetX\" type=\"number\" value=\"5\" \/>  <\/label>  <\/p>  <p class=\"paintweb_opt_shadowOffsetY\">  <label>Offset Y  <input data-pwConfig=\"shadow.shadowOffsetY\" type=\"number\" value=\"5\" \/>  <\/label>  <\/p>  <p class=\"paintweb_opt_shadowBlur\">  <label>Blur  <input data-pwConfig=\"shadow.shadowBlur\" type=\"number\" value=\"5\"   min=\"0\" \/>  <\/label>  <\/p>  <\/div>  <p data-pwCommand=\"about\">About<\/p>  <\/div>   <div id=\"viewport\">  <div id=\"canvasContainer\">  <\/div>  <div id=\"canvasResizer\">Resize the image Canvas<\/div>  <\/div>   <div class=\"paintweb_statusbar\">  <p id=\"imageSize\">WxH<\/p>  <p id=\"statusZoom\" title=\"Zoom image\">  <label>Zoom: <input id=\"imageZoom\" type=\"number\" min=\"20\" max=\"400\"   value=\"100\" step=\"10\" \/><\/label>  <\/p>  <p id=\"statusMessage\">Status<\/p>  <\/div>   <div data-pwFloatingPanel=\"colormixer\" data-pwPanelHide=\"true\">  <h1>Color mixer<\/h1>  <div>  <ol class=\"paintweb_colormixer_preview\">  <li id=\"colormixer_colorActive\"><span>&#160;<\/span> Active<\/li>  <li id=\"colormixer_colorOld\"><span>&#160;<\/span> Old<\/li>  <\/ol>  <ol class=\"paintweb_colormixer_actions\">  <li id=\"colormixer_btn_accept\">Close<\/li>  <li id=\"colormixer_btn_cancel\">Cancel<\/li>  <li id=\"colormixer_btn_saveColor\">Save color<\/li>  <li id=\"colormixer_btn_pickColor\">Pick color<\/li>  <\/ol>  <div data-pwTabPanel=\"colormixer_selector\" data-pwTabDefault=\"mixer\">  <div data-pwTab=\"mixer\">  <canvas id=\"colormixer_canvas\" width=\"200\" height=\"195\">Your browser   does not support Canvas.<\/canvas>  <div id=\"colormixer_controls\">  <span id=\"colormixer_chartDot\"><\/span>  <span id=\"colormixer_slider\"><\/span>  <\/div>  <\/div>  <div data-pwTab=\"cpalettes\">  <select id=\"colormixer_cpaletteInput\"><\/select>  <div id=\"colormixer_cpaletteOutput\"><\/div>  <\/div>  <\/div>  <ol class=\"paintweb_colormixer_hexalpha\">  <li><label>HEX  <input id=\"ckey_hex\" value=\"#RRGGBB\" type=\"text\" maxlength=\"7\"   pattern=\"#[a-f0-9]{6}\" \/><\/label>  <\/li>  <li><label>Alpha  <input id=\"ckey_alpha\" value=\"100\" type=\"number\" min=\"0\" max=\"100\"   step=\"1\" \/><\/label>  <\/li>  <\/ol>  <form data-pwTabPanel=\"colormixer_inputs\" data-pwTabDefault=\"rgb\">  <ol data-pwTab=\"rgb\">  <li>  <input name=\"ckey\" value=\"red\" type=\"radio\" \/>  <label>Red  <input name=\"ckey_red\" value=\"0\" type=\"number\" min=\"0\" max=\"255\"   step=\"1\" \/><\/label>  <\/li>  <li>  <input name=\"ckey\" value=\"green\" type=\"radio\" \/>  <label>Green  <input name=\"ckey_green\" value=\"0\" type=\"number\" min=\"0\" max=\"255\"   step=\"1\" \/><\/label>  <\/li>  <li>  <input name=\"ckey\" value=\"blue\" type=\"radio\" \/>  <label>Blue  <input name=\"ckey_blue\" value=\"0\" type=\"number\" min=\"0\" max=\"255\"   step=\"1\" \/><\/label>  <\/li>  <\/ol>  <ol data-pwTab=\"hsv\">  <li>  <input name=\"ckey\" value=\"hue\" type=\"radio\" \/>  <label>Hue  <input name=\"ckey_hue\" value=\"0\" type=\"number\" min=\"0\" max=\"360\"   step=\"1\" \/><\/label>  <\/li>  <li>  <input name=\"ckey\" value=\"sat\" type=\"radio\" \/>  <label>Saturation  <input name=\"ckey_sat\" value=\"0\" type=\"number\" min=\"0\" max=\"255\"   step=\"1\" \/><\/label>  <\/li>  <li>  <input name=\"ckey\" value=\"val\" type=\"radio\" \/>  <label>Value  <input name=\"ckey_val\" value=\"0\" type=\"number\" min=\"0\" max=\"255\"   step=\"1\" \/><\/label>  <\/li>  <\/ol>  <ol data-pwTab=\"lab\">  <li>  <input name=\"ckey\" value=\"cie_l\" type=\"radio\" \/>  <label>Lightness  <input name=\"ckey_cie_l\" value=\"0\" type=\"number\" min=\"0\"      max=\"100\" step=\"1\" \/><\/label>  <\/li>  <li>  <input name=\"ckey\" value=\"cie_a\" type=\"radio\" \/>  <label>a*  <input name=\"ckey_cie_a\" value=\"0\" type=\"number\" min=\"-85\"    max=\"94\" step=\"1\" \/><\/label>  <\/li>  <li>  <input name=\"ckey\" value=\"cie_b\" type=\"radio\" \/>  <label>b*  <input name=\"ckey_cie_b\" value=\"0\" type=\"number\" min=\"-109\"   max=\"95\" step=\"1\" \/><\/label>  <\/li>  <\/ol>  <ol data-pwTab=\"cmyk\">  <li>  <label>Cyan  <input name=\"ckey_cyan\" value=\"0\" type=\"number\" min=\"0\" max=\"100\"   step=\"1\" \/><\/label>  <\/li>  <li>  <label>Magenta  <input name=\"ckey_magenta\" value=\"0\" type=\"number\" min=\"0\"   max=\"100\" step=\"1\" \/><\/label>  <\/li>  <li>  <label>Yellow  <input name=\"ckey_yellow\" value=\"0\" type=\"number\" min=\"0\" max=\"100\"   step=\"1\" \/><\/label>  <\/li>  <li>  <label>Key (Black)  <input name=\"ckey_black\" value=\"0\" type=\"number\" min=\"0\" max=\"100\"   step=\"1\" \/><\/label>  <\/li>  <\/ol>  <\/form>   <\/div>   <\/div>   <div data-pwFloatingPanel=\"about\" data-pwPanelHide=\"true\">  <h1>About<\/h1>  <div>  <ul>  <li id=\"version\"><strong>Version:<\/strong> <\/li>  <li><strong>Authors:<\/strong> <a href=\"http:\/\/www.robodesign.ro\">Marius   and Mihai \u015eucan (ROBO Design)<\/a><\/li>  <li><strong>Project site:<\/strong> <a   href=\"http:\/\/code.google.com\/p\/paintweb\">code.google.com\/p\/paintweb<\/a><\/li>  <li><strong>Code license:<\/strong> <a   href=\"http:\/\/www.gnu.org\/licenses\/gpl-3.0.html\" title=\"GNU General   Public License, version 3\">GPLv3<\/a><\/li>  <\/ul>  <p>The project is currently undergoing heavy development for the purpose of   integrating it into <a href=\"http:\/\/www.moodle.org\">Moodle<\/a>.<\/p>  <p>For user and developer documentation please check out the <a   href=\"http:\/\/code.google.com\/p\/paintweb\">project site<\/a>.<\/p>  <\/div>  <\/div>  <\/div>";
12553  * Copyright (C) 2008, 2009 Mihai Şucan
12555  * This file is part of PaintWeb.
12557  * PaintWeb is free software: you can redistribute it and/or modify
12558  * it under the terms of the GNU General Public License as published by
12559  * the Free Software Foundation, either version 3 of the License, or
12560  * (at your option) any later version.
12562  * PaintWeb is distributed in the hope that it will be useful,
12563  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12564  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12565  * GNU General Public License for more details.
12567  * You should have received a copy of the GNU General Public License
12568  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
12570  * $URL: http://code.google.com/p/paintweb $
12571  * $Date: 2009-07-28 21:43:08 +0300 $
12572  */
12575  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
12576  * @fileOverview The main PaintWeb application code.
12577  */
12580  * @class The PaintWeb application object.
12582  * @param {Window} [win=window] The window object to use.
12583  * @param {Document} [doc=document] The document object to use.
12584  */
12585 function PaintWeb (win, doc) {
12586   var _self = this;
12588   if (!win) {
12589     win = window;
12590   }
12591   if (!doc) {
12592     doc = document;
12593   }
12595   /**
12596    * PaintWeb version.
12597    * @type Number
12598    */
12599   this.version = 0.9; //!
12601   /**
12602    * PaintWeb build date (YYYYMMDD).
12603    * @type Number
12604    */
12605   this.build = 20090728;
12607   /**
12608    * Holds all the PaintWeb configuration.
12609    * @type Object
12610    */
12611   this.config = {
12612     showErrors: true
12613   };
12615   /**
12616    * Holds all language strings used within PaintWeb.
12617    */
12618   // Here we include a minimal set of strings, used in case the language file will 
12619   // not load.
12620   this.lang = {
12621     "noComputedStyle": "Error: window.getComputedStyle is not available.",
12622     "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.",
12623     "noCanvasSupport": "Error: Your browser does not support Canvas.",
12624     "guiPlaceholderWrong": "Error: The config.guiPlaceholder property must " +
12625       "reference a DOM element!",
12626     "initHandlerMustBeFunction": "The first argument must be a function.",
12627     "noConfigFile": "Error: You must point to a configuration file by " +
12628       "setting the config.configFile property!",
12629     "failedConfigLoad": "Error: Failed loading the configuration file.",
12630     "failedLangLoad": "Error: Failed loading the language file."
12631   };
12633   /**
12634    * Holds the buffer canvas and context references.
12635    * @type Object
12636    */
12637   this.buffer = {canvas: null, context: null};
12639   /**
12640    * Holds the current layer ID, canvas and context references.
12641    * @type Object
12642    */
12643   this.layer = {id: null, canvas: null, context: null};
12645   /**
12646    * The instance of the active tool object.
12647    *
12648    * @type Object
12649    *
12650    * @see PaintWeb.config.toolDefault holds the ID of the tool which is 
12651    * activated when the application loads.
12652    * @see PaintWeb#toolActivate Activate a drawing tool by ID.
12653    * @see PaintWeb#toolRegister Register a new drawing tool.
12654    * @see PaintWeb#toolUnregister Unregister a drawing tool.
12655    * @see pwlib.tools holds the drawing tools.
12656    */
12657   this.tool = null;
12659   /**
12660    * Holds references to DOM elements.
12661    *
12662    * @private
12663    * @type Object
12664    */
12665   this.elems = {};
12667   /**
12668    * Holds the last recorded mouse coordinates and the button state (if it's 
12669    * down or not).
12670    *
12671    * @private
12672    * @type Object
12673    */
12674   this.mouse = {x: 0, y: 0, buttonDown: false};
12676   /**
12677    * Holds all the PaintWeb extensions.
12678    *
12679    * @type Object
12680    * @see PaintWeb#extensionRegister Register a new extension.
12681    * @see PaintWeb#extensionUnregister Unregister an extension.
12682    * @see PaintWeb.config.extensions Holds the list of extensions to be loaded 
12683    * automatically when PaintWeb is initialized.
12684    */
12685   this.extensions = {};
12687   /**
12688    * Holds all the PaintWeb commands. Each property in this object must 
12689    * reference a simple function which can be executed by keyboard shortcuts 
12690    * and/or GUI elements.
12691    *
12692    * @type Object
12693    * @see PaintWeb#commandRegister Register a new command.
12694    * @see PaintWeb#commandUnregister Unregister a command.
12695    */
12696   this.commands = {};
12698   /**
12699    * The graphical user interface object instance.
12700    * @type pwlib.gui
12701    */
12702   this.gui = null;
12704   /**
12705    * The document element PaintWeb is working with.
12706    *
12707    * @private
12708    * @type Document
12709    * @default document
12710    */
12711   this.doc = doc;
12713   /**
12714    * The window object PaintWeb is working with.
12715    *
12716    * @private
12717    * @type Window
12718    * @default window
12719    */
12720   this.win = win;
12722   /**
12723    * Holds image information: width and height.
12724    *
12725    * @type Object
12726    */
12727   this.image = {
12728     /**
12729      * Image width.
12730      *
12731      * @type Number
12732      */
12733     width: 0,
12735     /**
12736      * Image height.
12737      *
12738      * @type Number
12739      */
12740     height: 0,
12742     /**
12743      * Image zoom level. This property holds the current image zoom level used 
12744      * by the user for viewing the image.
12745      *
12746      * @type Number
12747      * @default 1
12748      */
12749     zoom: 1,
12751     /**
12752      * Image scaling. The canvas elements are scaled from CSS using this value 
12753      * as the scaling factor. This value is dependant on the browser rendering 
12754      * resolution and on the user-defined image zoom level.
12755      *
12756      * @type Number
12757      * @default 1
12758      */
12759     canvasScale: 1
12760   };
12762   /**
12763    * Resolution information.
12764    *
12765    * @type Object
12766    */
12767   this.resolution = {
12768     /**
12769      * The DOM element holding information about the current browser rendering 
12770      * settings (zoom / DPI).
12771      *
12772      * @private
12773      * @type Element
12774      */
12775     elem: null,
12777     /**
12778      * The ID of the DOM element holding information about the current browser 
12779      * rendering settings (zoom / DPI).
12780      *
12781      * @private
12782      * @type String
12783      * @default 'paintweb_resInfo'
12784      */
12785     elemId: 'paintweb_resInfo',
12787     /**
12788      * The styling necessary for the DOM element.
12789      *
12790      * @private
12791      * @type String
12792      */
12793     cssText: '@media screen and (resolution:96dpi){' +
12794              '#paintweb_resInfo{width:96px}}' +
12795              '@media screen and (resolution:134dpi){' +
12796              '#paintweb_resInfo{width:134px}}' +
12797              '@media screen and (resolution:200dpi){' +
12798              '#paintweb_resInfo{width:200px}}' +
12799              '@media screen and (resolution:300dpi){' +
12800              '#paintweb_resInfo{width:300px}}' +
12801              '#paintweb_resInfo{' +
12802              'display:block;' +
12803              'height:100%;' +
12804              'left:-3000px;' +
12805              'position:fixed;' +
12806              'top:0;' +
12807              'visibility:hidden;' +
12808              'z-index:-32}',
12810     /**
12811      * Optimal DPI for the canvas elements.
12812      *
12813      * @private
12814      * @type Number
12815      * @default 96
12816      */
12817     dpiOptimal: 96,
12819     /**
12820      * The current DPI used by the browser for rendering the entire page.
12821      *
12822      * @type Number
12823      * @default 96
12824      */
12825     dpiLocal: 96,
12827     /**
12828      * The current zoom level used by the browser for rendering the entire page.
12829      *
12830      * @type Number
12831      * @default 1
12832      */
12833     browserZoom: 1,
12835     /**
12836      * The scaling factor used by the browser for rendering the entire page. For 
12837      * example, on Gecko using DPI 200 the scale factor is 2.
12838      *
12839      * @private
12840      * @type Number
12841      * @default -1
12842      */
12843     scale: -1
12844   };
12846   /**
12847    * The image history.
12848    *
12849    * @private
12850    * @type Object
12851    */
12852   this.history = {
12853     /**
12854      * History position.
12855      *
12856      * @type Number
12857      * @default 0
12858      */
12859     pos: 0,
12861     /**
12862      * The ImageDatas for each history state.
12863      *
12864      * @private
12865      * @type Array
12866      */
12867     states: []
12868   };
12870   /**
12871    * Tells if the browser supports the Canvas Shadows API.
12872    *
12873    * @type Boolean
12874    * @default true
12875    */
12876   this.shadowSupported = true;
12878   /**
12879    * Tells if the current tool allows the drawing of shadows.
12880    *
12881    * @type Boolean
12882    * @default true
12883    */
12884   this.shadowAllowed = true;
12886   /**
12887    * Image in the clipboard. This is used when some selection is copy/pasted.  
12888    * 
12889    * @type ImageData
12890    */
12891   this.clipboard = false;
12893   /**
12894    * Application initialization state. This property can be in one of the 
12895    * following states:
12896    *
12897    * <ul>
12898    *   <li>{@link PaintWeb.INIT_NOT_STARTED} - The initialization is not 
12899    *   started.
12900    *
12901    *   <li>{@link PaintWeb.INIT_STARTED} - The initialization process is 
12902    *   running.
12903    *
12904    *   <li>{@link PaintWeb.INIT_DONE} - The initialization process has completed 
12905    *   successfully.
12906    *
12907    *   <li>{@link PaintWeb.INIT_ERROR} - The initialization process has failed.
12908    * </ul>
12909    *
12910    * @type Number
12911    * @default PaintWeb.INIT_NOT_STARTED
12912    */
12913   this.initialized = PaintWeb.INIT_NOT_STARTED;
12915   /**
12916    * Custom application events object.
12917    *
12918    * @type pwlib.appEvents
12919    */
12920   this.events = null;
12922   /**
12923    * Unique ID for the current PaintWeb instance.
12924    *
12925    * @type Number
12926    */
12927   this.UID = 0;
12929   /**
12930    * List of Canvas context properties to save and restore.
12931    *
12932    * <p>When the Canvas is resized the state is lost. Using context.save/restore 
12933    * state does work only in Opera. In Firefox/Gecko and WebKit saved states are 
12934    * lost after resize, so there's no state to restore. As such, PaintWeb has 
12935    * its own simple state save/restore mechanism. The property values are saved 
12936    * into a JavaScript object.
12937    *
12938    * @private
12939    * @type Array
12940    *
12941    * @see PaintWeb#stateSave to save the canvas context state.
12942    * @see PaintWeb#stateRestore to restore a canvas context state.
12943    */
12944   this.stateProperties = ['strokeStyle', 'fillStyle', 'globalAlpha', 
12945     'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'shadowOffsetX', 
12946     'shadowOffsetY', 'shadowBlur', 'shadowColor', 'globalCompositeOperation', 
12947     'font', 'textAlign', 'textBaseline'];
12949   /**
12950    * Holds the keyboard event listener object.
12951    *
12952    * @private
12953    * @type pwlib.dom.KeyboardEventListener
12954    * @see pwlib.dom.KeyboardEventListener The class dealing with the 
12955    * cross-browser differences in the DOM keyboard events.
12956    */
12957   var kbListener_ = null;
12959   /**
12960    * Holds temporary state information during PaintWeb initialization.
12961    *
12962    * @private
12963    * @type Object
12964    */
12965   var temp_ = {onInit: null, toolsLoadQueue: 0, extensionsLoadQueue: 0};
12967   // Avoid global scope lookup.
12968   var MathAbs   = Math.abs,
12969       MathFloor = Math.floor,
12970       MathMax   = Math.max,
12971       MathMin   = Math.min,
12972       MathRound = Math.round,
12973       pwlib     = null,
12974       appEvent  = null,
12975       lang      = this.lang;
12977   /**
12978    * PaintWeb pre-initialization code. This runs when the PaintWeb instance is 
12979    * constructed.
12980    * @private
12981    */
12982   function preInit() {
12983     var d = new Date();
12985     // If PaintWeb is running directly from the source code, then the build date 
12986     // is always today.
12987     if (_self.build === -1) {
12988       var dateArr = [d.getFullYear(), d.getMonth()+1, d.getDate()];
12990       if (dateArr[1] < 10) {
12991         dateArr[1] = '0' + dateArr[1];
12992       }
12993       if (dateArr[2] < 10) {
12994         dateArr[2] = '0' + dateArr[2];
12995       }
12997       _self.build = dateArr.join('');
12998     }
13000     _self.UID = d.getMilliseconds() * MathRound(Math.random() * 100);
13001     _self.elems.head = doc.getElementsByTagName('head')[0] || doc.body;
13002   };
13004   /**
13005    * Initialize PaintWeb.
13006    *
13007    * <p>This method is asynchronous, meaning that it will return much sooner 
13008    * before the application initialization is completed.
13009    *
13010    * @param {Function} [handler] The <code>appInit</code> event handler. Your 
13011    * event handler will be invoked automatically when PaintWeb completes 
13012    * loading, or when an error occurs.
13013    *
13014    * @returns {Boolean} True if the initialization has been started 
13015    * successfully, or false if not.
13016    */
13017   this.init = function (handler) {
13018     if (this.initialized === PaintWeb.INIT_DONE) {
13019       return true;
13020     }
13022     this.initialized = PaintWeb.INIT_STARTED;
13024     if (handler && typeof handler !== 'function') {
13025       throw new TypeError(lang.initHandlerMustBeFunction);
13026     }
13028     temp_.onInit = handler;
13030     // Check Canvas support.
13031     if (!doc.createElement('canvas').getContext) {
13032       this.initError(lang.noCanvasSupport);
13033       return false;
13034     }
13036     // Basic functionality used within the Web application.
13037     if (!window.getComputedStyle) {
13038       this.initError(lang.noComputedStyle);
13039       return false;
13040     }
13042     if (!window.XMLHttpRequest) {
13043       this.initError(lang.noXMLHttpRequest);
13044       return false;
13045     }
13047     if (!this.config.configFile) {
13048       this.initError(lang.noConfigFile);
13049       return false;
13050     }
13052     if (typeof this.config.guiPlaceholder !== 'object' || 
13053         this.config.guiPlaceholder.nodeType !== Node.ELEMENT_NODE) {
13054       this.initError(lang.guiPlaceholderWrong);
13055       return false;
13056     }
13058     // Silently ignore any wrong value for the config.imageLoad property.
13059     if (typeof this.config.imageLoad !== 'object' || 
13060         this.config.imageLoad.nodeType !== Node.ELEMENT_NODE) {
13061       this.config.imageLoad = null;
13062     }
13064     // JSON parser and serializer.
13065     if (!window.JSON) {
13066       this.scriptLoad(PaintWeb.baseFolder + 'includes/json2.js', 
13067           this.jsonlibReady);
13068     } else {
13069       this.jsonlibReady();
13070     }
13072     return true;
13073   };
13075   /**
13076    * The <code>load</code> event handler for the JSON library script.
13077    * @private
13078    */
13079   this.jsonlibReady = function () {
13080     if (window.pwlib) {
13081       _self.pwlibReady();
13082     } else {
13083       _self.scriptLoad(PaintWeb.baseFolder + 'includes/lib.js', 
13084           _self.pwlibReady);
13085     }
13086   };
13088   /**
13089    * The <code>load</code> event handler for the PaintWeb library script.
13090    * @private
13091    */
13092   this.pwlibReady = function () {
13093     pwlib = window.pwlib;
13094     appEvent = pwlib.appEvent;
13096     // Create the custom application events object.
13097     _self.events = new pwlib.appEvents(_self);
13099     // Add the init event handler.
13100     if (typeof temp_.onInit === 'function') {
13101       _self.events.add('appInit', temp_.onInit);
13102       delete temp_.onInit;
13103     }
13105     _self.configLoad();
13106   };
13108   /**
13109    * Report an initialization error.
13110    *
13111    * <p>This method dispatches the {@link pwlib.appEvent.appInit} event.
13112    *
13113    * @private
13114    *
13115    * @param {String} msg The error message.
13116    *
13117    * @see pwlib.appEvent.appInit
13118    */
13119   this.initError = function (msg) {
13120     switch (this.initialized) {
13121       case PaintWeb.INIT_ERROR:
13122       case PaintWeb.INIT_DONE:
13123       case PaintWeb.INIT_NOT_STARTED:
13124         return;
13125     }
13127     this.initialized = PaintWeb.INIT_ERROR;
13129     var ev = null;
13131     if (this.events && 'dispatch' in this.events &&
13132         appEvent    && 'appInit'  in appEvent) {
13134       ev = new appEvent.appInit(this.initialized, msg);
13135       this.events.dispatch(ev);
13137     } else if (typeof temp_.onInit === 'function') {
13138       // fake an event dispatch.
13139       ev = {type: 'appInit', state: this.initialized, errorMessage: msg};
13140       temp_.onInit.call(this, ev);
13141     }
13143     if (this.config.showErrors) {
13144       alert(msg);
13145     } else if (window.console && console.log) {
13146       console.log(msg);
13147     }
13148   };
13150   /**
13151    * Asynchronously load the configuration file. This method issues an 
13152    * XMLHttpRequest to load the JSON file.
13153    *
13154    * @private
13155    *
13156    * @see PaintWeb.config.configFile The configuration file.
13157    * @see pwlib.xhrLoad The library function being used for creating the 
13158    * XMLHttpRequest object.
13159    */
13160   this.configLoad = function () {
13161     pwlib.xhrLoad(PaintWeb.baseFolder + this.config.configFile, 
13162         this.configReady);
13163   };
13165   /**
13166    * The configuration reader. This is the event handler for the XMLHttpRequest 
13167    * object, for the <code>onreadystatechange</code> event.
13168    *
13169    * @private
13170    *
13171    * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
13172    *
13173    * @see PaintWeb#configLoad The method which issues the XMLHttpRequest request 
13174    * for loading the configuration file.
13175    */
13176   this.configReady = function (xhr) {
13177     /*
13178      * readyState values:
13179      *   0 UNINITIALIZED open() has not been called yet.
13180      *   1 LOADING send() has not been called yet.
13181      *   2 LOADED send() has been called, headers and status are available.
13182      *   3 INTERACTIVE Downloading, responseText holds the partial data.
13183      *   4 COMPLETED Finished with all operations.
13184      */
13185     if (!xhr || xhr.readyState !== 4) {
13186       return;
13187     }
13189     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
13190       _self.initError(lang.failedConfigLoad);
13191       return;
13192     }
13194     var config = pwlib.jsonParse(xhr.responseText);
13195     pwlib.extend(_self.config, config);
13197     _self.langLoad();
13198   };
13200   /**
13201    * Asynchronously load the language file. This method issues an XMLHttpRequest 
13202    * to load the JSON file.
13203    *
13204    * @private
13205    *
13206    * @see PaintWeb.config.lang The language you want for the PaintWeb user 
13207    * interface.
13208    * @see pwlib.xhrLoad The library function being used for creating the 
13209    * XMLHttpRequest object.
13210    */
13211   this.langLoad = function () {
13212     var id   = this.config.lang,
13213         file = PaintWeb.baseFolder;
13215     // If the language is not available, always fallback to English.
13216     if (!(id in this.config.languages)) {
13217       id = this.config.lang = 'en';
13218     }
13220     if ('file' in this.config.languages[id]) {
13221       file += this.config.languages[id].file;
13222     } else {
13223       file += this.config.langFolder + '/' + id + '.json';
13224     }
13226     pwlib.xhrLoad(file, this.langReady);
13227   };
13229   /**
13230    * The language file reader. This is the event handler for the XMLHttpRequest 
13231    * object, for the <code>onreadystatechange</code> event.
13232    *
13233    * @private
13234    *
13235    * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
13236    *
13237    * @see PaintWeb#langLoad The method which issues the XMLHttpRequest request 
13238    * for loading the language file.
13239    */
13240   this.langReady = function (xhr) {
13241     if (!xhr || xhr.readyState !== 4) {
13242       return;
13243     }
13245     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
13246       _self.initError(lang.failedLangLoad);
13247       return;
13248     }
13250     pwlib.extend(_self.lang, pwlib.jsonParse(xhr.responseText));
13252     if (_self.initCanvas() && _self.initContext()) {
13253       // Start GUI load now.
13254       _self.guiLoad();
13255     } else {
13256       _self.initError(lang.errorInitCanvas);
13257     }
13258   };
13260   /**
13261    * Initialize the PaintWeb commands.
13262    *
13263    * @private
13264    * @returns {Boolean} True if the initialization was successful, or false if 
13265    * not.
13266    */
13267   this.initCommands = function () {
13268     if (this.commandRegister('historyUndo',    this.historyUndo) &&
13269         this.commandRegister('historyRedo',    this.historyRedo) &&
13270         this.commandRegister('selectAll',      this.selectAll) &&
13271         this.commandRegister('selectionCut',   this.selectionCut) &&
13272         this.commandRegister('selectionCopy',  this.selectionCopy) &&
13273         this.commandRegister('clipboardPaste', this.clipboardPaste) &&
13274         this.commandRegister('imageSave',      this.imageSave) &&
13275         this.commandRegister('imageClear',     this.imageClear) &&
13276         this.commandRegister('swapFillStroke', this.swapFillStroke) &&
13277         this.commandRegister('imageZoomIn',    this.imageZoomIn) &&
13278         this.commandRegister('imageZoomOut',   this.imageZoomOut) &&
13279         this.commandRegister('imageZoomReset', this.imageZoomReset)) {
13280       return true;
13281     } else {
13282       this.initError(lang.errorInitCommands);
13283       return false;
13284     }
13285   };
13287   /**
13288    * Load th PaintWeb GUI. This method loads the GUI markup file, the stylesheet 
13289    * and the script.
13290    *
13291    * @private
13292    *
13293    * @see PaintWeb.config.guiStyle The interface style file.
13294    * @see PaintWeb.config.guiScript The interface script file.
13295    * @see pwlib.gui The interface object.
13296    */
13297   this.guiLoad = function () {
13298     var cfg    = this.config,
13299         gui    = this.config.gui,
13300         base   = PaintWeb.baseFolder + cfg.interfacesFolder + '/' + gui + '/',
13301         style  = base + cfg.guiStyle,
13302         script = base + cfg.guiScript;
13304     this.styleLoad(gui + 'style', style);
13306     if (pwlib.gui) {
13307       this.guiScriptReady();
13308     } else {
13309       this.scriptLoad(script, this.guiScriptReady);
13310     }
13311   };
13313   /**
13314    * The <code>load</code> event handler for the PaintWeb GUI script. This 
13315    * method creates an instance of the GUI object that just loaded and starts 
13316    * loading the GUI markup.
13317    *
13318    * @private
13319    *
13320    * @see PaintWeb.config.guiScript The interface script file.
13321    * @see PaintWeb.config.guiMarkup The interface markup file.
13322    * @see pwlib.gui The interface object.
13323    * @see pwlib.xhrLoad The library function being used for creating the 
13324    * XMLHttpRequest object.
13325    */
13326   this.guiScriptReady = function () {
13327     var cfg    = _self.config,
13328         gui    = _self.config.gui,
13329         base   = cfg.interfacesFolder + '/' + gui + '/',
13330         markup = base + cfg.guiMarkup;
13332     _self.gui = new pwlib.gui(_self);
13334     // Check if the interface markup is cached already.
13335     if (markup in pwlib.fileCache) {
13336       if (_self.gui.init(pwlib.fileCache[markup])) {
13337         _self.initTools();
13338       } else {
13339         _self.initError(lang.errorInitGUI);
13340       }
13342     } else {
13343       pwlib.xhrLoad(PaintWeb.baseFolder + markup, _self.guiMarkupReady);
13344     }
13345   };
13347   /**
13348    * The GUI markup reader. This is the event handler for the XMLHttpRequest 
13349    * object, for the <code>onreadystatechange</code> event.
13350    *
13351    * @private
13352    *
13353    * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
13354    *
13355    * @see PaintWeb#guiScriptReady The method which issues the XMLHttpRequest 
13356    * request for loading the interface markup file.
13357    */
13358   this.guiMarkupReady = function (xhr) {
13359     if (!xhr || xhr.readyState !== 4) {
13360       return;
13361     }
13363     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseXML) {
13364       _self.initError(lang.failedMarkupLoad);
13365       return;
13366     }
13368     if (_self.gui.init(xhr.responseXML)) {
13369       _self.initTools();
13370     } else {
13371       _self.initError(lang.errorInitGUI);
13372     }
13373   };
13375   /**
13376    * Initialize the Canvas elements. This method creates the elements and 
13377    * sets-up their dimensions.
13378    * 
13379    * <p>If {@link PaintWeb.config.imageLoad} is defined, then the image element 
13380    * is inserted into the Canvas image.
13381    *
13382    * <p>All the Canvas event listeners are also attached to the buffer Canvas 
13383    * element.
13384    *
13385    * @private
13386    * @returns {Boolean} True if the initialization was successful, or false if 
13387    * not.
13388    *
13389    * @see PaintWeb#ev_canvas The global Canvas events handler.
13390    */
13391   this.initCanvas = function () {
13392     var cfg           = this.config,
13393         res           = this.resolution,
13394         resInfo       = doc.getElementById(res.elemId),
13395         layerCanvas   = doc.createElement('canvas'),
13396         bufferCanvas  = doc.createElement('canvas'),
13397         layerContext  = layerCanvas.getContext('2d'),
13398         bufferContext = bufferCanvas.getContext('2d'),
13399         width         = cfg.imageWidth,
13400         height        = cfg.imageHeight,
13401         imageLoad     = cfg.imageLoad;
13403     if (!resInfo) {
13404       var style = doc.createElement('style');
13405       style.type = 'text/css';
13406       style.appendChild(doc.createTextNode(res.cssText));
13407       _self.elems.head.appendChild(style);
13409       resInfo = doc.createElement('div');
13410       resInfo.id = res.elemId;
13411       doc.body.appendChild(resInfo);
13412     }
13414     if (!resInfo) {
13415       this.initError(lang.errorInitCanvas);
13416       return false;
13417     }
13418     if (!layerCanvas || !bufferCanvas || !layerContext || !bufferContext) {
13419       this.initError(lang.noCanvasSupport);
13420       return false;
13421     }
13423     if (!pwlib.isSameHost(imageLoad.src, win.location.host)) {
13424       cfg.imageLoad = imageLoad = null;
13425       alert(lang.imageLoadDifferentHost);
13426     }
13428     if (imageLoad) {
13429       width  = parseInt(imageLoad.width);
13430       height = parseInt(imageLoad.height);
13431     }
13433     res.elem = resInfo;
13435     this.image.width  = layerCanvas.width  = bufferCanvas.width  = width;
13436     this.image.height = layerCanvas.height = bufferCanvas.height = height;
13438     this.layer.canvas   = layerCanvas;
13439     this.layer.context  = layerContext;
13440     this.buffer.canvas  = bufferCanvas;
13441     this.buffer.context = bufferContext;
13443     if (imageLoad) {
13444       layerContext.drawImage(imageLoad, 0, 0);
13445     }
13447     /*
13448      * Setup the event listeners for the canvas element.
13449      *
13450      * The event handler (ev_canvas) calls the event handlers associated with 
13451      * the active tool (e.g. tool.mousemove).
13452      */
13453     var events = ['dblclick', 'click', 'mousedown', 'mouseup', 'mousemove', 
13454         'contextmenu'],
13455         n = events.length;
13457     for (var i = 0; i < n; i++) {
13458       bufferCanvas.addEventListener(events[i], this.ev_canvas, false);
13459     }
13461     return true;
13462   };
13464   /**
13465    * Initialize the Canvas buffer context. This method updates the context 
13466    * properties to reflect the values defined in the PaintWeb configuration 
13467    * file.
13468    * 
13469    * <p>Shadows support is also determined. The {@link PaintWeb#shadowSupported} 
13470    * value is updated accordingly.
13471    *
13472    * @private
13473    * @returns {Boolean} True if the initialization was successful, or false if 
13474    * not.
13475    */
13476   this.initContext = function () {
13477     var bufferContext = this.buffer.context;
13479     // Opera does not render shadows, at the moment.
13480     if (!pwlib.browser.opera && bufferContext.shadowColor && 'shadowOffsetX' in 
13481         bufferContext && 'shadowOffsetY' in bufferContext && 'shadowBlur' in 
13482         bufferContext) {
13483       this.shadowSupported = true;
13484     } else {
13485       this.shadowSupported = false;
13486     }
13488     var cfg = this.config,
13489         props = {
13490           fillStyle:    cfg.fillStyle,
13491           font:         cfg.text.fontSize + 'px ' + cfg.text.fontFamily,
13492           lineCap:      cfg.line.lineCap,
13493           lineJoin:     cfg.line.lineJoin,
13494           lineWidth:    cfg.line.lineWidth,
13495           miterLimit:   cfg.line.miterLimit,
13496           strokeStyle:  cfg.strokeStyle,
13497           textAlign:    cfg.text.textAlign,
13498           textBaseline: cfg.text.textBaseline
13499         };
13501     if (cfg.text.bold) {
13502       props.font = 'bold ' + props.font;
13503     }
13505     if (cfg.text.italic) {
13506       props.font = 'italic ' + props.font;
13507     }
13509     // Support Gecko 1.9.0
13510     if (!bufferContext.fillText && 'mozTextStyle' in bufferContext) {
13511       props.mozTextStyle = props.font;
13512     }
13514     for (var prop in props) {
13515       bufferContext[prop] = props[prop];
13516     }
13518     // shadows are only for the layer context.
13519     if (cfg.shadow.enable && this.shadowSupported) {
13520       var layerContext = this.layer.context;
13521       layerContext.shadowColor   = cfg.shadow.shadowColor;
13522       layerContext.shadowBlur    = cfg.shadow.shadowBlur;
13523       layerContext.shadowOffsetX = cfg.shadow.shadowOffsetX;
13524       layerContext.shadowOffsetY = cfg.shadow.shadowOffsetY;
13525     }
13527     return true;
13528   };
13530   /**
13531    * Initialization procedure which runs after the configuration, language and 
13532    * GUI files have loaded.
13533    *
13534    * <p>This method dispatches the {@link pwlib.appEvent.appInit} event.
13535    *
13536    * @private
13537    *
13538    * @see pwlib.appEvent.appInit
13539    */
13540   this.initComplete = function () {
13541     if (!this.initCommands()) {
13542       this.initError(lang.errorInitCommands);
13543       return;
13544     }
13546     // The initial blank state of the image
13547     this.historyAdd();
13549     // The global keyboard events handler implements everything needed for 
13550     // switching between tools and for accessing any other functionality of the 
13551     // Web application.
13552     kbListener_ = new pwlib.dom.KeyboardEventListener(this.config.guiPlaceholder,
13553         {keydown:  this.ev_keyboard,
13554          keypress: this.ev_keyboard,
13555          keyup:    this.ev_keyboard});
13557     this.updateCanvasScaling();
13558     this.win.addEventListener('resize', this.updateCanvasScaling, false);
13560     this.events.add('configChange', this.configChangeHandler);
13562     this.initialized = PaintWeb.INIT_DONE;
13564     this.events.dispatch(new appEvent.appInit(this.initialized));
13565   };
13567   /**
13568    * Load all the configured drawing tools.
13569    * @private
13570    */
13571   this.initTools = function () {
13572     var id   = '',
13573         cfg  = this.config,
13574         n    = cfg.tools.length,
13575         base = PaintWeb.baseFolder + cfg.toolsFolder + '/';
13577     if (n < 1) {
13578       this.initError(lang.noToolConfigured);
13579       return;
13580     }
13582     temp_.toolsLoadQueue = n;
13584     for (var i = 0; i < n; i++) {
13585       id = cfg.tools[i];
13586       if (id in pwlib.tools) {
13587         this.toolLoaded();
13588       } else {
13589         this.scriptLoad(base + id + '.js' , this.toolLoaded);
13590       }
13591     }
13592   };
13594   /**
13595    * The <code>load</code> event handler for each tool script.
13596    * @private
13597    */
13598   this.toolLoaded = function () {
13599     temp_.toolsLoadQueue--;
13601     if (temp_.toolsLoadQueue === 0) {
13602       var t = _self.config.tools,
13603           n = t.length;
13605       for (var i = 0; i < n; i++) {
13606         if (!_self.toolRegister(t[i])) {
13607           _self.initError(pwlib.strf(lang.toolRegisterFailed, {id: t[i]}));
13608           return;
13609         }
13610       }
13612       _self.initExtensions();
13613     }
13614   };
13616   /**
13617    * Load all the extensions.
13618    * @private
13619    */
13620   this.initExtensions = function () {
13621     var id   = '',
13622         cfg  = this.config,
13623         n    = cfg.extensions.length,
13624         base = PaintWeb.baseFolder + cfg.extensionsFolder + '/';
13626     if (n < 1) {
13627       this.initComplete();
13628       return;
13629     }
13631     temp_.extensionsLoadQueue = n;
13633     for (var i = 0; i < n; i++) {
13634       id = cfg.extensions[i];
13635       if (id in pwlib.extensions) {
13636         this.extensionLoaded();
13637       } else {
13638         this.scriptLoad(base + id + '.js', this.extensionLoaded);
13639       }
13640     }
13641   };
13643   /**
13644    * The <code>load</code> event handler for each extension script.
13645    * @private
13646    */
13647   this.extensionLoaded = function () {
13648     temp_.extensionsLoadQueue--;
13650     if (temp_.extensionsLoadQueue === 0) {
13651       var e = _self.config.extensions,
13652           n = e.length;
13654       for (var i = 0; i < n; i++) {
13655         if (!_self.extensionRegister(e[i])) {
13656           _self.initError(pwlib.strf(lang.extensionRegisterFailed, {id: e[i]}));
13657           return;
13658         }
13659       }
13661       _self.initComplete();
13662     }
13663   };
13665   /**
13666    * Update the canvas scaling. This method determines the DPI and/or zoom level 
13667    * used by the browser to render the application. Based on these values, the 
13668    * canvas elements are scaled down to cancel any upscaling performed by the 
13669    * browser.
13670    *
13671    * <p>The {@link pwlib.appEvent.canvasSizeChange} application event is 
13672    * dispatched.
13673    */
13674   this.updateCanvasScaling = function () {
13675     var res            = _self.resolution,
13676         cs             = win.getComputedStyle(res.elem, null),
13677         image          = _self.image;
13678         bufferStyle    = _self.buffer.canvas.style,
13679         layerStyle     = _self.layer.canvas.style,
13680         scaleNew       = 1;
13682     var width  = parseInt(cs.width),
13683         height = parseInt(cs.height);
13685     if (pwlib.browser.opera) {
13686       // Opera zoom level detection.
13687       // The scaling factor is sufficiently accurate for zoom levels between 
13688       // 100% and 200% (in steps of 10%).
13690       scaleNew = win.innerHeight / height;
13691       scaleNew = MathRound(scaleNew * 10) / 10;
13693     } else if (width && !isNaN(width) && width !== res.dpiOptimal) {
13694       // Page DPI detection. This only works in Gecko 1.9.1.
13696       res.dpiLocal = width;
13698       // The scaling factor is the same as in Gecko.
13699       scaleNew = MathFloor(res.dpiLocal / res.dpiOptimal);
13701     } else if (pwlib.browser.olpcxo) {
13702       // Support for the default Gecko included on the OLPC XO-1 system.
13703       //
13704       // See:
13705       // http://mxr.mozilla.org/mozilla-central/source/gfx/src/thebes/nsThebesDeviceContext.cpp#725
13706       // dotsArePixels = false on the XO due to a hard-coded patch.
13707       // Thanks go to roc from Mozilla for his feedback on making this work.
13709       res.dpiLocal = 134; // hard-coded value, we cannot determine it
13711       var appUnitsPerCSSPixel  = 60, // hard-coded internally in Gecko
13712           devPixelsPerCSSPixel = res.dpiLocal / res.dpiOptimal; // 1.3958333333
13713           appUnitsPerDevPixel  = appUnitsPerCSSPixel / devPixelsPerCSSPixel; // 42.9850746278...
13715       scaleNew = appUnitsPerCSSPixel / MathFloor(appUnitsPerDevPixel); // 1.4285714285...
13716     }
13718     if (scaleNew === res.scale) {
13719       return;
13720     }
13722     res.scale = scaleNew;
13724     var styleWidth  = image.width  / res.scale * image.zoom,
13725         styleHeight = image.height / res.scale * image.zoom;
13727     image.canvasScale = styleWidth / image.width;
13729     bufferStyle.width  = layerStyle.width  = styleWidth  + 'px';
13730     bufferStyle.height = layerStyle.height = styleHeight + 'px';
13732     _self.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 
13733           image.canvasScale));
13734   };
13736   /**
13737    * The Canvas events handler.
13738    * 
13739    * <p>This method determines the mouse position relative to the canvas 
13740    * element, after which it invokes the method of the currently active tool 
13741    * with the same name as the current event type. For example, for the 
13742    * <code>mousedown</code> event the <code><var>tool</var>.mousedown()</code> 
13743    * method is invoked.
13744    *
13745    * <p>The mouse coordinates are stored in the {@link PaintWeb#mouse} object.  
13746    * These properties take into account the current zoom level and the image 
13747    * scroll.
13748    *
13749    * @private
13750    *
13751    * @param {Event} ev The DOM Event object.
13752    *
13753    * @returns {Boolean} True if the tool event handler executed, or false 
13754    * otherwise.
13755    */
13756   this.ev_canvas = function (ev) {
13757     if (!_self.tool) {
13758       return false;
13759     }
13761     switch (ev.type) {
13762       case 'mousedown':
13763         /*
13764          * If the mouse is down already, skip the event.
13765          * This is needed to allow the user to go out of the drawing canvas, 
13766          * release the mouse button, then come back and click to end the drawing 
13767          * operation.
13768          * Additionally, this is needed to allow extensions like MouseKeys to 
13769          * perform their actions during a drawing operation, even when a real 
13770          * mouse is used. For example, allow the user to start drawing with the 
13771          * keyboard (press 0) then use the real mouse to move and click to end 
13772          * the drawing operation.
13773          */
13774         if (_self.mouse.buttonDown) {
13775           return false;
13776         }
13777         _self.mouse.buttonDown = true;
13778         break;
13780       case 'mouseup':
13781         // Skip the event if the mouse button was not down.
13782         if (!_self.mouse.buttonDown) {
13783           return false;
13784         }
13785         _self.mouse.buttonDown = false;
13786     }
13788     /*
13789      * Update the event, to include the mouse position, relative to the canvas 
13790      * element.
13791      */
13792     if ('layerX' in ev) {
13793       if (_self.image.canvasScale === 1) {
13794         _self.mouse.x = ev.layerX;
13795         _self.mouse.y = ev.layerY;
13796       } else {
13797         _self.mouse.x = MathRound(ev.layerX / _self.image.canvasScale);
13798         _self.mouse.y = MathRound(ev.layerY / _self.image.canvasScale);
13799       }
13800     } else if ('offsetX' in ev) {
13801       if (_self.image.canvasScale === 1) {
13802         _self.mouse.x = ev.offsetX;
13803         _self.mouse.y = ev.offsetY;
13804       } else {
13805         _self.mouse.x = MathRound(ev.offsetX / _self.image.canvasScale);
13806         _self.mouse.y = MathRound(ev.offsetY / _self.image.canvasScale);
13807       }
13808     }
13810     // The event handler of the current tool.
13811     if (ev.type in _self.tool && _self.tool[ev.type](ev)) {
13812       ev.preventDefault();
13813       return true;
13814     } else {
13815       return false;
13816     }
13817   };
13819   /**
13820    * The global keyboard events handler. This makes all the keyboard shortcuts 
13821    * work in the web application.
13822    *
13823    * <p>This method determines the key the user pressed, based on the 
13824    * <var>ev</var> DOM Event object, taking into consideration any browser 
13825    * differences. Two new properties are added to the <var>ev</var> object:
13826    *
13827    * <ul>
13828    *   <li><var>ev.kid_</var> is a string holding the key and the modifiers list 
13829    *   (<kbd>Control</kbd>, <kbd>Alt</kbd> and/or <kbd>Shift</kbd>). For 
13830    *   example, if the user would press the key <kbd>A</kbd> while holding down 
13831    *   <kbd>Control</kbd>, then <var>ev.kid_</var> would be "Control A". If the 
13832    *   user would press "9" while holding down <kbd>Shift</kbd>, then 
13833    *   <var>ev.kid_</var> would be "Shift 9".
13834    *
13835    *   <li><var>ev.kobj_</var> holds a reference to the keyboard shortcut 
13836    *   definition object from the configuration. This is useful for reuse, for 
13837    *   passing parameters from the keyboard shortcut configuration object to the 
13838    *   event handler.
13839    * </ul>
13840    *
13841    * <p>In {@link PaintWeb.config.keys} one can setup the keyboard shortcuts.  
13842    * If the keyboard combination is found in that list, then the associated tool 
13843    * is activated.
13844    *
13845    * <p>Note: this method includes some work-around for making the image zoom 
13846    * keys work well both in Opera and Firefox.
13847    *
13848    * @private
13849    *
13850    * @param {Event} ev The DOM Event object.
13851    *
13852    * @see PaintWeb.config.keys The keyboard shortcuts configuration.
13853    * @see pwlib.dom.KeyboardEventListener The class dealing with the 
13854    * cross-browser differences in the DOM keyboard events.
13855    */
13856   this.ev_keyboard = function (ev) {
13857     // Do not continue if the key was not recognized by the lib.
13858     if (!ev.key_) {
13859       return;
13860     }
13862     if (ev.target && ev.target.nodeName) {
13863       switch (ev.target.nodeName.toLowerCase()) {
13864         case 'input':
13865           if (ev.type === 'keypress' && (ev.key_ === 'Up' || ev.key_ === 'Down') 
13866               && ev.target.getAttribute('type') === 'number') {
13867             _self.ev_numberInput(ev);
13868           }
13869         case 'select':
13870         case 'textarea':
13871         case 'button':
13872           return;
13873       }
13874     }
13876     // Rather ugly, but the only way, at the moment, to detect these keys in 
13877     // Opera and Firefox.
13878     if (ev.type === 'keypress' && ev.char_) {
13879       var isZoomKey = true,
13880           imageZoomKeys = _self.config.imageZoomKeys;
13882       // Check if this is a zoom key and execute the commands as needed.
13883       switch (ev.char_) {
13884         case imageZoomKeys['in']:
13885           _self.imageZoomIn(ev);
13886           break;
13888         case imageZoomKeys['out']:
13889           _self.imageZoomOut(ev);
13890           break;
13891         case imageZoomKeys['reset']:
13892           _self.imageZoomReset(ev);
13893           break;
13894         default:
13895           isZoomKey = false;
13896       }
13898       if (isZoomKey) {
13899         ev.preventDefault();
13900         return;
13901       }
13902     }
13904     // Determine the key ID.
13905     ev.kid_ = '';
13906     var i, kmods = {altKey: 'Alt', ctrlKey: 'Control', shiftKey: 'Shift'};
13907     for (i in kmods) {
13908       if (ev[i] && ev.key_ !== kmods[i]) {
13909         ev.kid_ += kmods[i] + ' ';
13910       }
13911     }
13912     ev.kid_ += ev.key_;
13914     // Send the keyboard event to the event handler of the active tool. If it 
13915     // returns true, we consider it recognized the keyboard shortcut.
13916     if (_self.tool && ev.type in _self.tool && _self.tool[ev.type](ev)) {
13917       return true;
13918     }
13920     // If there's no event handler within the active tool, or if the event 
13921     // handler does otherwise return false, then we continue with the global 
13922     // keyboard shortcuts.
13924     var gkey = _self.config.keys[ev.kid_];
13925     if (!gkey) {
13926       return false;
13927     }
13929     ev.kobj_ = gkey;
13931     // Check if the keyboard shortcut has some extension associated.
13932     if ('extension' in gkey) {
13933       var extension = _self.extensions[gkey.extension],
13934           method    = gkey.method || ev.type;
13936       // Call the extension method.
13937       if (method in extension) {
13938         extension[method].call(this, ev);
13939       }
13941     } else if ('command' in gkey && gkey.command in _self.commands) {
13942       // Invoke the command associated with the key.
13943       _self.commands[gkey.command].call(this, ev);
13945     } else if (ev.type === 'keydown' && 'toolActivate' in gkey) {
13947       // Active the tool associated to the key.
13948       _self.toolActivate(gkey.toolActivate, ev);
13950     }
13952     if (ev.type === 'keypress') {
13953       ev.preventDefault();
13954     }
13955   };
13957   /**
13958    * This is the <code>keypress</code> event handler for inputs of type=number.  
13959    * This function only handles cases when the key is <kbd>Up</kbd> or 
13960    * <kbd>Down</kbd>. For the <kbd>Up</kbd> key the input value is increased, 
13961    * and for the <kbd>Down</kbd> the value is decreased.
13962    *
13963    * @private
13964    * @param {Event} ev The DOM Event object.
13965    * @see PaintWeb#ev_keyboard
13966    */
13967   this.ev_numberInput = function (ev) {
13968     var target = ev.target;
13970     // Process the value.
13971     var val,
13972         max  = parseFloat(target.getAttribute('max')),
13973         min  = parseFloat(target.getAttribute('min')),
13974         step = parseFloat(target.getAttribute('step'));
13976     if (target.value === '' || target.value === null) {
13977       val = !isNaN(min) ? min : 0;
13978     } else {
13979       val = target.value.replace(/[,.]+/g, '.').replace(/[^0-9.\-]/g, '');
13980       val = parseFloat(val);
13981     }
13983     // If target is not a number, then set the old value, or the minimum value. If all fails, set 0.
13984     if (isNaN(val)) {
13985       val = min || 0;
13986     }
13988     if (isNaN(step)) {
13989       step = 1;
13990     }
13992     if (ev.shiftKey) {
13993       step *= 2;
13994     }
13996     if (ev.key_ === 'Down') {
13997       step *= -1;
13998     }
14000     val += step;
14002     if (!isNaN(max) && val > max) {
14003       val = max;
14004     } else if (!isNaN(min) && val < min) {
14005       val = min;
14006     }
14008     if (val == target.value) {
14009       return;
14010     }
14012     target.value = val;
14014     // Dispatch the 'change' events to make sure that any associated event 
14015     // handlers pick up the changes.
14016     if (doc.createEvent && target.dispatchEvent) {
14017       var ev_change = doc.createEvent('HTMLEvents');
14018       ev_change.initEvent('change', true, true);
14019       target.dispatchEvent(ev_change);
14020     }
14021   };
14023   /**
14024    * Zoom into the image.
14025    *
14026    * @param {mixed} ev An event object which might have the <var>shiftKey</var> 
14027    * property. If the property evaluates to true, then the zoom level will 
14028    * increase twice more than normal.
14029    *
14030    * @returns {Boolean} True if the operation was successful, or false if not.
14031    *
14032    * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
14033    * @see PaintWeb.config.zoomStep The value used for increasing the zoom level.
14034    */
14035   this.imageZoomIn = function (ev) {
14036     if (ev && ev.shiftKey) {
14037       _self.config.imageZoomStep *= 2;
14038     }
14040     var res = _self.imageZoomTo('+');
14042     if (ev && ev.shiftKey) {
14043       _self.config.imageZoomStep /= 2;
14044     }
14046     return res;
14047   };
14049   /**
14050    * Zoom out of the image.
14051    *
14052    * @param {mixed} ev An event object which might have the <var>shiftKey</var> 
14053    * property. If the property evaluates to true, then the zoom level will 
14054    * decrease twice more than normal.
14055    *
14056    * @returns {Boolean} True if the operation was successful, or false if not.
14057    *
14058    * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
14059    * @see PaintWeb.config.zoomStep The value used for decreasing the zoom level.
14060    */
14061   this.imageZoomOut = function (ev) {
14062     if (ev && ev.shiftKey) {
14063       _self.config.imageZoomStep *= 2;
14064     }
14066     var res = _self.imageZoomTo('-');
14068     if (ev && ev.shiftKey) {
14069       _self.config.imageZoomStep /= 2;
14070     }
14072     return res;
14073   };
14075   /**
14076    * Reset the image zoom level to normal.
14077    *
14078    * @returns {Boolean} True if the operation was successful, or false if not.
14079    *
14080    * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
14081    */
14082   this.imageZoomReset = function (ev) {
14083     return _self.imageZoomTo(1);
14084   };
14086   /**
14087    * Change the image zoom level.
14088    *
14089    * <p>This method dispatches the {@link pwlib.appEvent.imageZoom} application 
14090    * event before zooming the image. Once the image zoom is applied, the {@link 
14091    * pwlib.appEvent.canvasSizeChange} event is dispatched.
14092    *
14093    * @param {Number|String} level The level you want to zoom the image to.
14094    * 
14095    * <p>If the value is a number, it must be a floating point positive number, 
14096    * where 0.5 means 50%, 1 means 100% (normal) zoom, 4 means 400% and so on.
14097    *
14098    * <p>If the value is a string it must be "+" or "-". This means that the zoom 
14099    * level will increase/decrease using the configured {@link 
14100    * PaintWeb.config.zoomStep}.
14101    *
14102    * @returns {Boolean} True if the image zoom level changed successfully, or 
14103    * false if not.
14104    */
14105   this.imageZoomTo = function (level) {
14106     var image  = this.image,
14107         config = this.config,
14108         res    = this.resolution;
14110     if (!level) {
14111       return false;
14112     } else if (level === '+') {
14113       level = image.zoom + config.imageZoomStep;
14114     } else if (level === '-') {
14115       level = image.zoom - config.imageZoomStep;
14116     } else if (typeof level !== 'number') {
14117       return false;
14118     }
14120     if (level > config.imageZoomMax) {
14121       level = config.imageZoomMax;
14122     } else if (level < config.imageZoomMin) {
14123       level = config.imageZoomMin;
14124     }
14126     if (level === image.zoom) {
14127       return true;
14128     }
14130     var cancel = this.events.dispatch(new appEvent.imageZoom(level));
14131     if (cancel) {
14132       return false;
14133     }
14135     var styleWidth  = image.width  / res.scale * level,
14136         styleHeight = image.height / res.scale * level,
14137         bufferStyle = this.buffer.canvas.style,
14138         layerStyle  = this.layer.canvas.style;
14140     image.canvasScale = styleWidth / image.width;
14142     bufferStyle.width  = layerStyle.width  = styleWidth  + 'px';
14143     bufferStyle.height = layerStyle.height = styleHeight + 'px';
14145     image.zoom = level;
14147     this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 
14148           image.canvasScale));
14150     return true;
14151   };
14153   /**
14154    * Crop the image.
14155    *
14156    * <p>The content of the image is retained only if the browser implements the 
14157    * <code>getImageData</code> and <code>putImageData</code> methods.
14158    *
14159    * <p>This method dispatches three application events: {@link 
14160    * pwlib.appEvent.imageSizeChange}, {@link pwlib.appEvent.canvasSizeChange} 
14161    * and {@link pwlib.appEvent.imageCrop}. The <code>imageCrop</code> event is 
14162    * dispatched before the image is cropped. The <code>imageSizeChange</code> 
14163    * and <code>canvasSizeChange</code> events are dispatched after the image is 
14164    * cropped.
14165    *
14166    * @param {Number} cropX Image cropping start position on the x-axis.
14167    * @param {Number} cropY Image cropping start position on the y-axis.
14168    * @param {Number} cropWidth Image crop width.
14169    * @param {Number} cropHeight Image crop height.
14170    *
14171    * @returns {Boolean} True if the image was cropped successfully, or false if 
14172    * not.
14173    */
14174   this.imageCrop = function (cropX, cropY, cropWidth, cropHeight) {
14175     var bufferCanvas  = this.buffer.canvas,
14176         bufferContext = this.buffer.context,
14177         image         = this.image,
14178         layerCanvas   = this.layer.canvas,
14179         layerContext  = this.layer.context;
14181     cropX      = parseInt(cropX);
14182     cropY      = parseInt(cropY);
14183     cropWidth  = parseInt(cropWidth);
14184     cropHeight = parseInt(cropHeight);
14186     if (!cropWidth || !cropHeight || isNaN(cropX) || isNaN(cropY) || 
14187         isNaN(cropWidth) || isNaN(cropHeight) || cropX >= image.width || cropY 
14188         >= image.height) {
14189       return false;
14190     }
14192     var cancel = this.events.dispatch(new appEvent.imageCrop(cropX, cropY, 
14193           cropWidth, cropHeight));
14194     if (cancel) {
14195       return false;
14196     }
14198     if (cropWidth > this.config.imageWidthMax) {
14199       cropWidth = this.config.imageWidthMax;
14200     }
14202     if (cropHeight > this.config.imageHeightMax) {
14203       cropHeight = this.config.imageHeightMax;
14204     }
14206     if (cropX === 0 && cropY === 0 && image.width === cropWidth && image.height 
14207         === cropHeight) {
14208       return true;
14209     }
14211     var scaledWidth  = cropWidth  * image.canvasScale,
14212         scaledHeight = cropHeight * image.canvasScale;
14214     bufferCanvas.style.width  = layerCanvas.style.width  = scaledWidth  + 'px';
14215     bufferCanvas.style.height = layerCanvas.style.height = scaledHeight + 'px';
14217     // The canvas state is reset once the dimensions change.
14218     var state      = this.stateSave(layerContext),
14219         dataWidth  = MathMin(image.width,  cropWidth),
14220         dataHeight = MathMin(image.height, cropHeight),
14221         sumX       = cropX + dataWidth,
14222         sumY       = cropY + dataHeight;
14224     if (sumX > image.width) {
14225       dataWidth -= sumX - image.width;
14226     }
14227     if (sumY > image.height) {
14228       dataHeight -= sumY - image.height;
14229     }
14231     // The image is cleared once the dimensions change. We need to restore the image.
14232     var idata = null;
14234     if (layerContext.getImageData) {
14235       // TODO: handle "out of memory" errors.
14236       try {
14237         idata = layerContext.getImageData(cropX, cropY, dataWidth, dataHeight);
14238       } catch (err) {
14239         // do not continue if we can't store the image in memory.
14240         return false;
14241       }
14242     }
14244     layerCanvas.width  = cropWidth;
14245     layerCanvas.height = cropHeight;
14247     if (idata && layerContext.putImageData) {
14248       layerContext.putImageData(idata, 0, 0);
14249     }
14251     this.stateRestore(layerContext, state);
14252     state = this.stateSave(bufferContext);
14254     idata = null;
14255     if (bufferContext.getImageData) {
14256       try {
14257         idata = bufferContext.getImageData(cropX, cropY, dataWidth, dataHeight);
14258       } catch (err) { }
14259     }
14261     bufferCanvas.width  = cropWidth;
14262     bufferCanvas.height = cropHeight;
14264     if (idata && bufferContext.putImageData) {
14265       bufferContext.putImageData(idata, 0, 0);
14266     }
14268     this.stateRestore(bufferContext, state);
14270     image.width  = cropWidth;
14271     image.height = cropHeight;
14273     this.events.dispatch(new appEvent.imageSizeChange(cropWidth, cropHeight));
14274     this.events.dispatch(new appEvent.canvasSizeChange(scaledWidth, 
14275           scaledHeight, image.canvasScale));
14277     return true;
14278   };
14280   /**
14281    * Save the state of a Canvas context.
14282    *
14283    * @param {CanvasRenderingContext2D} context The 2D context of the Canvas 
14284    * element you want to save the state.
14285    *
14286    * @returns {Object} The object has all the state properties and values.
14287    */
14288   this.stateSave = function (context) {
14289     if (!context || !context.canvas || !this.stateProperties) {
14290       return false;
14291     }
14293     var stateObj = {},
14294         prop = null,
14295         n = this.stateProperties.length;
14297     for (var i = 0; i < n; i++) {
14298       prop = this.stateProperties[i];
14299       stateObj[prop] = context[prop];
14300     }
14302     return stateObj;
14303   };
14305   /**
14306    * Restore the state of a Canvas context.
14307    *
14308    * @param {CanvasRenderingContext2D} context The 2D context where you want to 
14309    * restore the state.
14310    *
14311    * @param {Object} stateObj The state object saved by the {@link 
14312    * PaintWeb#stateSave} method.
14313    *
14314    * @returns {Boolean} True if the operation was successful, or false if not.
14315    */
14316   this.stateRestore = function (context, stateObj) {
14317     if (!context || !context.canvas) {
14318       return false;
14319     }
14321     for (var state in stateObj) {
14322       context[state] = stateObj[state];
14323     }
14325     return true;
14326   };
14328   /**
14329    * Allow shadows. This method re-enabled shadow rendering, if it was enabled 
14330    * before shadows were disallowed.
14331    *
14332    * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched.
14333    */
14334   this.shadowAllow = function () {
14335     if (this.shadowAllowed || !this.shadowSupported) {
14336       return;
14337     }
14339     // Note that some daily builds of Webkit in Chromium fail to render the 
14340     // shadow when context.drawImage() is used (see the this.layerUpdate()).
14341     var context = this.layer.context,
14342         cfg = this.config.shadow;
14344     if (cfg.enable) {
14345       context.shadowColor   = cfg.shadowColor;
14346       context.shadowOffsetX = cfg.shadowOffsetX;
14347       context.shadowOffsetY = cfg.shadowOffsetY;
14348       context.shadowBlur    = cfg.shadowBlur;
14349     }
14351     this.shadowAllowed = true;
14353     this.events.dispatch(new appEvent.shadowAllow(true));
14354   };
14356   /**
14357    * Disallow shadows. This method disables shadow rendering, if it is enabled.
14358    *
14359    * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched.
14360    */
14361   this.shadowDisallow = function () {
14362     if (!this.shadowAllowed || !this.shadowSupported) {
14363       return;
14364     }
14366     if (this.config.shadow.enable) {
14367       var context = this.layer.context;
14368       context.shadowColor   = 'rgba(0,0,0,0)';
14369       context.shadowOffsetX = 0;
14370       context.shadowOffsetY = 0;
14371       context.shadowBlur    = 0;
14372     }
14374     this.shadowAllowed = false;
14376     this.events.dispatch(new appEvent.shadowAllow(false));
14377   };
14379   /**
14380    * Update the current image layer by moving the pixels from the buffer onto 
14381    * the layer. This method also adds a point into the history.
14382    *
14383    * @returns {Boolean} True if the operation was successful, or false if not.
14384    */
14385   this.layerUpdate = function () {
14386     this.layer.context.drawImage(this.buffer.canvas, 0, 0);
14387     this.buffer.context.clearRect(0, 0, this.image.width, this.image.height);
14388     this.historyAdd();
14390     return true;
14391   };
14393   /**
14394    * Add the current image layer to the history.
14395    *
14396    * <p>Once the history state has been updated, this method dispatches the 
14397    * {@link pwlib.appEvent.historyUpdate} event.
14398    *
14399    * @returns {Boolean} True if the operation was successful, or false if not.
14400    */
14401   // TODO: some day it would be nice to implement a hybrid history system.
14402   this.historyAdd = function () {
14403     var layerContext = this.layer.context,
14404         history      = this.history,
14405         prevPos      = history.pos;
14407     if (!layerContext.getImageData) {
14408       return false;
14409     }
14411     // We are in an undo-step, trim until the end, eliminating any possible redo-steps.
14412     if (prevPos < history.states.length) {
14413       history.states.splice(prevPos, history.states.length);
14414     }
14416     // TODO: in case of "out of memory" errors... I should show up some error.
14417     try {
14418       history.states.push(layerContext.getImageData(0, 0, this.image.width, 
14419             this.image.height));
14420     } catch (err) {
14421       return false;
14422     }
14424     // If we have too many history ImageDatas, remove the oldest ones
14425     if ('historyLimit' in this.config &&
14426         history.states.length > this.config.historyLimit) {
14428       history.states.splice(0, history.states.length 
14429           - this.config.historyLimit);
14430     }
14431     history.pos = history.states.length;
14433     this.events.dispatch(new appEvent.historyUpdate(history.pos, prevPos, 
14434           history.pos));
14436     return true;
14437   };
14439   /**
14440    * Jump to any ImageData/position in the history.
14441    *
14442    * <p>Once the history state has been updated, this method dispatches the 
14443    * {@link pwlib.appEvent.historyUpdate} event.
14444    *
14445    * @param {Number|String} pos The history position to jump to.
14446    * 
14447    * <p>If the value is a number, then it must point to an existing index in the  
14448    * <var>{@link PaintWeb#history}.states</var> array.
14449    *
14450    * <p>If the value is a string, it must be "undo" or "redo".
14451    *
14452    * @returns {Boolean} True if the operation was successful, or false if not.
14453    */
14454   this.historyGoto = function (pos) {
14455     var layerContext = this.layer.context,
14456         image        = this.image,
14457         history      = this.history;
14459     if (!history.states.length || !layerContext.putImageData) {
14460       return false;
14461     }
14463     var cpos = history.pos;
14465     if (pos === 'undo') {
14466       pos = cpos-1;
14467     } else if (pos === 'redo') {
14468       pos = cpos+1;
14469     }
14471     if (pos === cpos || pos < 1 || pos > history.states.length) {
14472       return false;
14473     }
14475     var himg = history.states[pos-1];
14476     if (!himg) {
14477       return false;
14478     }
14480     // Each image in the history can have a different size. As such, the script 
14481     // must take this into consideration.
14482     var w = MathMin(image.width,  himg.width),
14483         h = MathMin(image.height, himg.height);
14485     layerContext.clearRect(0, 0, image.width, image.height);
14487     try {
14488       // Firefox 3 does not clip the image, if needed.
14489       layerContext.putImageData(himg, 0, 0, 0, 0, w, h);
14491     } catch (err) {
14492       // The workaround is to use a new canvas from which we can copy the 
14493       // history image without causing any exceptions.
14494       var tmp    = doc.createElement('canvas');
14495       tmp.width  = himg.width;
14496       tmp.height = himg.height;
14498       var tmp2 = tmp.getContext('2d');
14499       tmp2.putImageData(himg, 0, 0);
14501       layerContext.drawImage(tmp, 0, 0);
14502       delete tmp2, tmp;
14503     }
14505     history.pos = pos;
14507     this.events.dispatch(new appEvent.historyUpdate(pos, cpos, 
14508           history.states.length));
14510     return true;
14511   };
14513   /**
14514    * Clear the image history.
14515    *
14516    * <p>This method dispatches the {@link pwlib.appEvent.historyUpdate} event.
14517    *
14518    * @private
14519    */
14520   this.historyReset = function () {
14521     this.history.pos = 0;
14522     this.history.states = [];
14524     this.events.dispatch(new appEvent.historyUpdate(0, 0, 0));
14525   };
14527   /**
14528    * Perform horizontal/vertical line snapping. This method updates the mouse 
14529    * coordinates to "snap" with the given coordinates.
14530    *
14531    * @param {Number} x The x-axis location.
14532    * @param {Number} y The y-axis location.
14533    */
14534   this.toolSnapXY = function (x, y) {
14535     var diffx = MathAbs(_self.mouse.x - x),
14536         diffy = MathAbs(_self.mouse.y - y);
14538     if (diffx > diffy) {
14539       _self.mouse.y = y;
14540     } else {
14541       _self.mouse.x = x;
14542     }
14543   };
14545   /**
14546    * Activate a drawing tool by ID.
14547    *
14548    * <p>The <var>id</var> provided must be of an existing drawing tool, one that  
14549    * has been installed.
14550    *
14551    * <p>The <var>ev</var> argument is an optional DOM Event object which is 
14552    * useful when dealing with different types of tool activation, either by 
14553    * keyboard or by mouse events. Tool-specific code can implement different 
14554    * functionality based on events.
14555    *
14556    * <p>This method dispatches the {@link pwlib.appEvent.toolPreactivate} event 
14557    * before creating the new tool instance. Once the new tool is successfully 
14558    * activated, the {@link pwlib.appEvent.toolActivate} event is also 
14559    * dispatched.
14560    *
14561    * @param {String} id The ID of the drawing tool to be activated.
14562    * @param {Event} [ev] The DOM Event object.
14563    *
14564    * @returns {Boolean} True if the tool has been activated, or false if not.
14565    *
14566    * @see PaintWeb#toolRegister Register a new drawing tool.
14567    * @see PaintWeb#toolUnregister Unregister a drawing tool.
14568    *
14569    * @see pwlib.tools The object holding all the drawing tools.
14570    * @see pwlib.appEvent.toolPreactivate
14571    * @see pwlib.appEvent.toolActivate
14572    */
14573   this.toolActivate = function (id, ev) {
14574     if (!id || !(id in pwlib.tools) || typeof pwlib.tools[id] !== 'function') {
14575       return false;
14576     }
14578     var tool = pwlib.tools[id],
14579         prevId = this.tool ? this.tool._id : null;
14581     if (prevId && this.tool instanceof pwlib.tools[id]) {
14582       return true;
14583     }
14585     var cancel = this.events.dispatch(new appEvent.toolPreactivate(id, prevId));
14586     if (cancel) {
14587       return false;
14588     }
14590     var tool_obj = new tool(this, ev);
14591     if (!tool_obj) {
14592       return false;
14593     }
14595     /*
14596      * Each tool can implement its own mouse and keyboard events handler.
14597      * Additionally, tool objects can implement handlers for the deactivation 
14598      * and activation events.
14599      * Given tool1 is active and tool2 is going to be activated, then the 
14600      * following event handlers will be called:
14601      *
14602      * tool2.preActivate
14603      * tool1.deactivate
14604      * tool2.activate
14605      *
14606      * In the "preActivate" event handler you can cancel the tool activation by 
14607      * returning a value which evaluates to false.
14608      */
14610     if ('preActivate' in tool_obj && !tool_obj.preActivate(ev)) {
14611       tool_obj = null;
14612       return false;
14613     }
14615     // Deactivate the previously active tool
14616     if (this.tool && 'deactivate' in this.tool) {
14617       this.tool.deactivate(ev);
14618     }
14620     this.tool = tool_obj;
14622     this.mouse.buttonDown = false;
14624     // Besides the "constructor", each tool can also have code which is run 
14625     // after the deactivation of the previous tool.
14626     if ('activate' in this.tool) {
14627       this.tool.activate(ev);
14628     }
14630     this.events.dispatch(new appEvent.toolActivate(id, prevId));
14632     return true;
14633   };
14635   /**
14636    * Register a new drawing tool into PaintWeb.
14637    *
14638    * <p>This method dispatches the {@link pwlib.appEvent.toolRegister} 
14639    * application event.
14640    *
14641    * @param {String} id The ID of the new tool. The tool object must exist in 
14642    * {@link pwlib.tools}.
14643    *
14644    * @returns {Boolean} True if the tool was successfully registered, or false 
14645    * if not.
14646    *
14647    * @see PaintWeb#toolUnregister allows you to unregister tools.
14648    * @see pwlib.tools Holds all the drawing tools.
14649    * @see pwlib.appEvent.toolRegister
14650    */
14651   this.toolRegister = function (id) {
14652     if (typeof id !== 'string' || !id) {
14653       return false;
14654     }
14656     // TODO: it would be very nice to create the tool instance on register, for 
14657     // further extensibility.
14659     var tool = pwlib.tools[id];
14660     if (typeof tool !== 'function') {
14661       return false;
14662     }
14664     tool.prototype._id = id;
14666     this.events.dispatch(new appEvent.toolRegister(id));
14668     if (!this.tool && id === this.config.toolDefault) {
14669       return this.toolActivate(id);
14670     } else {
14671       return true;
14672     }
14673   };
14675   /**
14676    * Unregister a drawing tool from PaintWeb.
14677    *
14678    * <p>This method dispatches the {@link pwlib.appEvent.toolUnregister} 
14679    * application event.
14680    *
14681    * @param {String} id The ID of the tool you want to unregister.
14682    *
14683    * @returns {Boolean} True if the tool was unregistered, or false if it does 
14684    * not exist or some error occurred.
14685    *
14686    * @see PaintWeb#toolRegister allows you to register new drawing tools.
14687    * @see pwlib.tools Holds all the drawing tools.
14688    * @see pwlib.appEvent.toolUnregister
14689    */
14690   this.toolUnregister = function (id) {
14691     if (typeof id !== 'string' || !id || !(id in pwlib.tools)) {
14692       return false;
14693     }
14695     this.events.dispatch(new appEvent.toolUnregister(id));
14697     return true;
14698   };
14700   /**
14701    * Register a new extension into PaintWeb.
14702    *
14703    * <p>If the extension object being constructed has the 
14704    * <code>extensionRegister()</code> method, then it will be invoked, allowing 
14705    * any custom extension registration code to run. If the method returns false, 
14706    * then the extension will not be registered.
14707    *
14708    * <p>Once the extension is successfully registered, this method dispatches 
14709    * the {@link pwlib.appEvent.extensionRegister} application event.
14710    *
14711    * @param {String} id The ID of the new extension. The extension object 
14712    * constructor must exist in {@link pwlib.extensions}.
14713    *
14714    * @returns {Boolean} True if the extension was successfully registered, or 
14715    * false if not.
14716    *
14717    * @see PaintWeb#extensionUnregister allows you to unregister extensions.
14718    * @see PaintWeb#extensions Holds all the instances of registered extensions.
14719    * @see pwlib.extensions Holds all the extension classes.
14720    */
14721   this.extensionRegister = function (id) {
14722     if (typeof id !== 'string' || !id) {
14723       return false;
14724     }
14726     var func = pwlib.extensions[id];
14727     if (typeof func !== 'function') {
14728       return false;
14729     }
14731     func.prototype._id = id;
14733     var obj = new func(_self);
14735     if ('extensionRegister' in obj && !obj.extensionRegister()) {
14736       return false;
14737     }
14739     this.extensions[id] = obj;
14740     this.events.dispatch(new appEvent.extensionRegister(id));
14742     return true;
14743   };
14745   /**
14746    * Unregister an extension from PaintWeb.
14747    *
14748    * <p>If the extension object being destructed has the 
14749    * <code>extensionUnregister()</code> method, then it will be invoked, 
14750    * allowing any custom extension removal code to run.
14751    *
14752    * <p>Before the extension is unregistered, this method dispatches the {@link 
14753    * pwlib.appEvent.extensionUnregister} application event.
14754    *
14755    * @param {String} id The ID of the extension object you want to unregister.
14756    *
14757    * @returns {Boolean} True if the extension was removed, or false if it does 
14758    * not exist or some error occurred.
14759    *
14760    * @see PaintWeb#extensionRegister allows you to register new extensions.
14761    * @see PaintWeb#extensions Holds all the instances of registered extensions.
14762    * @see pwlib.extensions Holds all the extension classes.
14763    */
14764   this.extensionUnregister = function (id) {
14765     if (typeof id !== 'string' || !id || !(id in this.extensions)) {
14766       return false;
14767     }
14769     this.events.dispatch(new appEvent.extensionUnregister(id));
14771     if ('extensionUnregister' in this.extensions[id]) {
14772       this.extensions[id].extensionUnregister();
14773     }
14774     delete this.extensions[id];
14776     return true;
14777   };
14779   /**
14780    * Register a new command in PaintWeb. Commands are simple function objects 
14781    * which can be invoked by keyboard shortcuts or by GUI elements.
14782    *
14783    * <p>Once the command is successfully registered, this method dispatches the 
14784    * {@link pwlib.appEvent.commandRegister} application event.
14785    *
14786    * @param {String} id The ID of the new command.
14787    * @param {Function} func The command function.
14788    *
14789    * @returns {Boolean} True if the command was successfully registered, or 
14790    * false if not.
14791    *
14792    * @see PaintWeb#commandUnregister allows you to unregister commands.
14793    * @see PaintWeb#commands Holds all the registered commands.
14794    */
14795   this.commandRegister = function (id, func) {
14796     if (typeof id !== 'string' || !id || typeof func !== 'function' || id in 
14797         this.commands) {
14798       return false;
14799     }
14801     this.commands[id] = func;
14802     this.events.dispatch(new appEvent.commandRegister(id));
14804     return true;
14805   };
14807   /**
14808    * Unregister a command from PaintWeb.
14809    *
14810    * <p>Before the command is unregistered, this method dispatches the {@link 
14811    * pwlib.appEvent.commandUnregister} application event.
14812    *
14813    * @param {String} id The ID of the command you want to unregister.
14814    *
14815    * @returns {Boolean} True if the command was removed successfully, or false 
14816    * if not.
14817    *
14818    * @see PaintWeb#commandRegister allows you to register new commands.
14819    * @see PaintWeb#commands Holds all the registered commands.
14820    */
14821   this.commandUnregister = function (id) {
14822     if (typeof id !== 'string' || !id || !(id in this.commands)) {
14823       return false;
14824     }
14826     this.events.dispatch(new appEvent.commandUnregister(id));
14828     delete this.commands[id];
14830     return true;
14831   };
14833   /**
14834    * Load a script into the document.
14835    *
14836    * @param {String} url The script URL you want to insert.
14837    * @param {Function} [handler] The <code>load</code> event handler you want.
14838    */
14839   this.scriptLoad = function (url, handler) {
14840     if (!handler) {
14841       var elem = doc.createElement('script');
14842       elem.type = 'text/javascript';
14843       elem.src = url;
14844       this.elems.head.appendChild(elem);
14845       return;
14846     }
14848     // huh, use XHR then eval() the code.
14849     // browsers do not dispatch the 'load' event reliably for script elements.
14850     var xhr = new XMLHttpRequest();
14852     xhr.onreadystatechange = function () {
14853       if (!xhr || xhr.readyState !== 4) {
14854         return;
14856       } else if ((xhr.status !== 304 && xhr.status !== 200) || 
14857           !xhr.responseText) {
14858         handler(false, xhr);
14860       } else {
14861         try {
14862           eval.call(win, xhr.responseText);
14863         } catch (err) {
14864           eval(xhr.responseText, win);
14865         }
14866         handler(true, xhr);
14867       }
14869       xhr = null;
14870     };
14872     xhr.open('GET', url);
14873     xhr.send('');
14874   };
14876   /**
14877    * Insert a stylesheet into the document.
14878    *
14879    * @param {String} id The stylesheet ID. This is used to avoid inserting the 
14880    * same style in the document.
14881    * @param {String} url The URL of the stylesheet you want to insert.
14882    * @param {String} [media='screen, projection'] The media attribute.
14883    * @param {Function} [handler] The <code>load</code> event handler.
14884    */
14885   this.styleLoad = function (id, url, media, handler) {
14886     id = 'paintweb_style_' + id;
14888     var elem = doc.getElementById(id);
14889     if (elem) {
14890       return;
14891     }
14893     if (!media) {
14894       media = 'screen, projection';
14895     }
14897     elem = doc.createElement('link');
14899     if (handler) {
14900       elem.addEventListener('load', handler, false);
14901     }
14903     elem.id = id;
14904     elem.rel = 'stylesheet';
14905     elem.type = 'text/css';
14906     elem.media = media;
14907     elem.href = url;
14909     this.elems.head.appendChild(elem);
14910   };
14912   /**
14913    * Perform action undo.
14914    *
14915    * @returns {Boolean} True if the operation was successful, or false if not.
14916    *
14917    * @see PaintWeb#historyGoto The method invoked by this command.
14918    */
14919   this.historyUndo = function () {
14920     return _self.historyGoto('undo');
14921   };
14923   /**
14924    * Perform action redo.
14925    *
14926    * @returns {Boolean} True if the operation was successful, or false if not.
14927    *
14928    * @see PaintWeb#historyGoto The method invoked by this command.
14929    */
14930   this.historyRedo = function () {
14931     return _self.historyGoto('redo');
14932   };
14934   /**
14935    * Load an image. By loading an image the history is cleared and the Canvas 
14936    * dimensions are updated to fit the new image.
14937    *
14938    * <p>This method dispatches two application events: {@link 
14939    * pwlib.appEvent.imageSizeChange} and {@link 
14940    * pwlib.appEvent.canvasSizeChange}.
14941    *
14942    * @param {Element} importImage The image element you want to load into the 
14943    * Canvas.
14944    *
14945    * @returns {Boolean} True if the operation was successful, or false if not.
14946    */
14947   this.imageLoad = function (importImage) {
14948     if (!importImage || !importImage.width || !importImage.height || 
14949         importImage.nodeType !== Node.ELEMENT_NODE || 
14950         !pwlib.isSameHost(importImage.src, win.location.host)) {
14951       return false;
14952     }
14954     this.historyReset();
14956     var layerContext = this.layer.context,
14957         layerCanvas  = this.layer.canvas,
14958         layerStyle   = layerCanvas.style,
14959         bufferCanvas = this.buffer.canvas,
14960         bufferStyle  = bufferCanvas.style,
14961         image        = this.image,
14962         styleWidth   = importImage.width  * image.canvasScale,
14963         styleHeight  = importImage.height * image.canvasScale,
14964         result       = true;
14966     bufferCanvas.width  = layerCanvas.width  = importImage.width;
14967     bufferCanvas.height = layerCanvas.height = importImage.height;
14969     try {
14970       layerContext.drawImage(importImage, 0, 0);
14971     } catch (err) {
14972       result = false;
14973       bufferCanvas.width  = layerCanvas.width  = image.width;
14974       bufferCanvas.height = layerCanvas.height = image.height;
14975     }
14977     if (result) {
14978       image.width  = importImage.width;
14979       image.height = importImage.height;
14980       bufferStyle.width  = layerStyle.width  = styleWidth  + 'px';
14981       bufferStyle.height = layerStyle.height = styleHeight + 'px';
14982       _self.config.imageLoad = importImage;
14984       this.events.dispatch(new appEvent.imageSizeChange(image.width, 
14985             image.height));
14987       this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 
14988             image.canvasScale));
14989     }
14991     this.historyAdd();
14993     return result;
14994   };
14996   /**
14997    * Clear the image.
14998    */
14999   this.imageClear = function (ev) {
15000     _self.layer.context.clearRect(0, 0, _self.image.width, _self.image.height);
15001     _self.historyAdd();
15002   };
15004   /**
15005    * Save the image.
15006    *
15007    * <p>This method dispatches the {@link pwlib.appEvent.imageSave} event.
15008    *
15009    * <p><strong>Note:</strong> the "Save image" operation relies on integration 
15010    * extensions. A vanilla configuration of PaintWeb will simply open the the 
15011    * image in a new tab using a data: URL. You must have some event listener for 
15012    * the <code>imageSave</code> event and you must prevent the default action.
15013    *
15014    * <p>If the default action for the <code>imageSave</code> application event 
15015    * is not prevented, then this method will also dispatch the {@link 
15016    * pwlib.appEvent.imageSaveResult} application event.
15017    *
15018    * <p>Your event handler for the <code>imageSave</code> event must dispatch 
15019    * the <code>imageSaveResult</code> event.
15020    *
15021    * @param {String} [type="auto"] Image MIME type. This tells the browser which 
15022    * format to use when saving the image. If the image format type is not 
15023    * supported, then the image is saved as PNG.
15024    *
15025    * <p>You can use the resulting data URL to check which is the actual image 
15026    * format.
15027    *
15028    * <p>When <var>type</var> is "auto" then PaintWeb checks the type of the 
15029    * image currently loaded ({@link PaintWeb.config.imageLoad}). If the format 
15030    * is recognized, then the same format is used to save the image.
15031    *
15032    * @returns {Boolean} True if the operation was successful, or false if not.
15033    */
15034   this.imageSave = function (type) {
15035     var canvas = _self.layer.canvas,
15036         imageLoad = _self.config.imageLoad,
15037         ext = 'png', idata = null, src = null, pos;
15039     if (!canvas.toDataURL) {
15040       return false;
15041     }
15043     var extMap = {'jpg' : 'image/jpeg', 'jpeg' : 'image/jpeg', 'png' 
15044       : 'image/png', 'gif' : 'image/gif'};
15046     // Detect the MIME type of the image currently loaded.
15047     if (!type) {
15048       if (imageLoad && imageLoad.src && imageLoad.src.substr(0, 5) !== 'data:') {
15049         src = imageLoad.src;
15050         pos = src.indexOf('?');
15051         if (pos !== -1) {
15052           src = src.substr(0, pos);
15053         }
15054         ext = src.substr(src.lastIndexOf('.') + 1).toLowerCase();
15055       }
15057       type = extMap[ext] || 'image/png';
15058     }
15060     try {
15061       if (type === 'image/jpeg') {
15062         idata = canvas.toDataURL(type, _self.config.jpegSaveQuality);
15063       } else {
15064         idata = canvas.toDataURL(type);
15065       }
15066     } catch (err) {
15067       alert(lang.errorImageSave + "\n" + err);
15068       return false;
15069     }
15071     if (!idata || idata === 'data:,') {
15072       return false;
15073     }
15075     var img = _self.image,
15076         ev = new appEvent.imageSave(idata, img.width, img.height),
15077         cancel = _self.events.dispatch(ev);
15079     if (cancel) {
15080       return true;
15081     }
15083     var imgwin = _self.win.open();
15084     if (!imgwin) {
15085       return false;
15086     }
15088     imgwin.location = idata;
15089     idata = null;
15091     _self.events.dispatch(new appEvent.imageSaveResult(true));
15093     return true;
15094   };
15096   /**
15097    * Swap the fill and stroke styles. This is just like in Photoshop, if the 
15098    * user presses X, the fill/stroke colors are swapped.
15099    *
15100    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event 
15101    * twice for each color (strokeStyle and fillStyle).
15102    */
15103   this.swapFillStroke = function () {
15104     var fillStyle     = _self.config.fillStyle,
15105         strokeStyle   = _self.config.strokeStyle;
15107     _self.config.fillStyle   = strokeStyle;
15108     _self.config.strokeStyle = fillStyle;
15110     var ev = new appEvent.configChange(strokeStyle, fillStyle, 'fillStyle', '', 
15111         _self.config);
15113     _self.events.dispatch(ev);
15115     ev = new appEvent.configChange(fillStyle, strokeStyle, 'strokeStyle', '', 
15116         _self.config);
15118     _self.events.dispatch(ev);
15119   };
15121   /**
15122    * Select all the pixels. This activates the selection tool, and selects the 
15123    * entire image.
15124    *
15125    * @param {Event} [ev] The DOM Event object which generated the request.
15126    * @returns {Boolean} True if the operation was successful, or false if not.
15127    *
15128    * @see {pwlib.tools.selection.selectAll} The command implementation.
15129    */
15130   this.selectAll = function (ev) {
15131     if (_self.toolActivate('selection', ev)) {
15132       return _self.tool.selectAll(ev);
15133     } else {
15134       return false;
15135     }
15136   };
15138   /**
15139    * Cut the available selection. This only works when the selection tool is 
15140    * active and when some selection is available.
15141    *
15142    * @param {Event} [ev] The DOM Event object which generated the request.
15143    * @returns {Boolean} True if the operation was successful, or false if not.
15144    *
15145    * @see {pwlib.tools.selection.selectionCut} The command implementation.
15146    */
15147   this.selectionCut = function (ev) {
15148     if (!_self.tool || _self.tool._id !== 'selection') {
15149       return false;
15150     } else {
15151       return _self.tool.selectionCut(ev);
15152     }
15153   };
15155   /**
15156    * Copy the available selection. This only works when the selection tool is 
15157    * active and when some selection is available.
15158    *
15159    * @param {Event} [ev] The DOM Event object which generated the request.
15160    * @returns {Boolean} True if the operation was successful, or false if not.
15161    *
15162    * @see {pwlib.tools.selection.selectionCopy} The command implementation.
15163    */
15164   this.selectionCopy = function (ev) {
15165     if (!_self.tool || _self.tool._id !== 'selection') {
15166       return false;
15167     } else {
15168       return _self.tool.selectionCopy(ev);
15169     }
15170   };
15172   /**
15173    * Paste the current clipboard image. This only works when some ImageData is 
15174    * available in {@link PaintWeb#clipboard}.
15175    *
15176    * @param {Event} [ev] The DOM Event object which generated the request.
15177    * @returns {Boolean} True if the operation was successful, or false if not.
15178    *
15179    * @see {pwlib.tools.selection.clipboardPaste} The command implementation.
15180    */
15181   this.clipboardPaste = function (ev) {
15182     if (!_self.clipboard || !_self.toolActivate('selection', ev)) {
15183       return false;
15184     } else {
15185       return _self.tool.clipboardPaste(ev);
15186     }
15187   };
15189   /**
15190    * The <code>configChange</code> application event handler. This method 
15191    * updates the Canvas context properties depending on which configuration 
15192    * property changed.
15193    *
15194    * @private
15195    * @param {pwlib.appEvent.configChange} ev The application event object.
15196    */
15197   this.configChangeHandler = function (ev) {
15198     if (ev.group === 'shadow' && _self.shadowSupported && _self.shadowAllowed) {
15199       var context = _self.layer.context,
15200           cfg = ev.groupRef;
15202       // Enable/disable shadows
15203       if (ev.config === 'enable') {
15204         if (ev.value) {
15205           context.shadowColor   = cfg.shadowColor;
15206           context.shadowOffsetX = cfg.shadowOffsetX;
15207           context.shadowOffsetY = cfg.shadowOffsetY;
15208           context.shadowBlur    = cfg.shadowBlur;
15209         } else {
15210           context.shadowColor   = 'rgba(0,0,0,0)';
15211           context.shadowOffsetX = 0;
15212           context.shadowOffsetY = 0;
15213           context.shadowBlur    = 0;
15214         }
15215         return;
15216       }
15218       // Do not update any context properties if shadows are not enabled.
15219       if (!cfg.enable) {
15220         return;
15221       }
15223       switch (ev.config) {
15224         case 'shadowBlur':
15225         case 'shadowOffsetX':
15226         case 'shadowOffsetY':
15227           ev.value = parseInt(ev.value);
15228         case 'shadowColor':
15229           context[ev.config] = ev.value;
15230       }
15232     } else if (ev.group === 'line') {
15233       switch (ev.config) {
15234         case 'lineWidth':
15235         case 'miterLimit':
15236           ev.value = parseInt(ev.value);
15237         case 'lineJoin':
15238         case 'lineCap':
15239           _self.buffer.context[ev.config] = ev.value;
15240       }
15242     } else if (ev.group === 'text') {
15243       switch (ev.config) {
15244         case 'textAlign':
15245         case 'textBaseline':
15246           _self.buffer.context[ev.config] = ev.value;
15247       }
15249     } else if (!ev.group) {
15250       switch (ev.config) {
15251         case 'fillStyle':
15252         case 'strokeStyle':
15253           _self.buffer.context[ev.config] = ev.value;
15254       }
15255     }
15256   };
15258   /**
15259    * Destroy a PaintWeb instance. This method allows you to unload a PaintWeb 
15260    * instance. Extensions, tools and commands are unregistered, and the GUI 
15261    * elements are removed.
15262    *
15263    * <p>The scripts and styles loaded are not removed, since they might be used 
15264    * by other PaintWeb instances.
15265    *
15266    * <p>The {@link pwlib.appEvent.appDestroy} application event is dispatched 
15267    * before the current instance is destroyed.
15268    */
15269   this.destroy = function () {
15270     this.events.dispatch(new appEvent.appDestroy());
15272     for (var cmd in this.commands) {
15273       this.commandUnregister(cmd);
15274     }
15276     for (var ext in this.extensions) {
15277       this.extensionUnregister(ext);
15278     }
15280     for (var tool in this.gui.tools) {
15281       this.toolUnregister(tool);
15282     }
15284     this.gui.destroy();
15286     this.initialized = PaintWeb.INIT_NOT_STARTED;
15287   };
15289   this.toString = function () {
15290     return 'PaintWeb v' + this.version + ' (build ' + this.build + ')';
15291   };
15294   preInit();
15298  * Application initialization not started.
15299  * @constant
15300  */
15301 PaintWeb.INIT_NOT_STARTED = 0;
15304  * Application initialization started.
15305  * @constant
15306  */
15307 PaintWeb.INIT_STARTED = 1;
15310  * Application initialization completed successfully.
15311  * @constant
15312  */
15313 PaintWeb.INIT_DONE = 2;
15316  * Application initialization failed.
15317  * @constant
15318  */
15319 PaintWeb.INIT_ERROR = -1;
15322  * PaintWeb base folder. This is determined automatically when the PaintWeb 
15323  * script is added in a page.
15324  * @type String
15325  */
15326 PaintWeb.baseFolder = '';
15328 (function () {
15329   var scripts = document.getElementsByTagName('script'),
15330       n = scripts.length,
15331       pos, src;
15333   // Determine the baseFolder.
15335   for (var i = 0; i < n; i++) {
15336     src = scripts[i].src;
15337     if (!src || !/paintweb(\.dev|\.src)?\.js/.test(src)) {
15338       continue;
15339     }
15341     pos = src.lastIndexOf('/');
15342     if (pos !== -1) {
15343       PaintWeb.baseFolder = src.substr(0, pos + 1);
15344     }
15346     break;
15347   }
15348 })();
15350 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: