1 /*
  2  * Copyright (C) 2008, 2009 Mihai Şucan
  3  *
  4  * This file is part of PaintWeb.
  5  *
  6  * PaintWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * PaintWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
 18  *
 19  * $URL: http://code.google.com/p/paintweb $
 20  * $Date: 2009-11-07 18:13:21 +0200 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview The default PaintWeb interface code.
 26  */
 27 
 28 /**
 29  * @class The default PaintWeb interface.
 30  *
 31  * @param {PaintWeb} app Reference to the main paint application object.
 32  */
 33 pwlib.gui = function (app) {
 34   var _self     = this,
 35       config    = app.config,
 36       doc       = app.doc,
 37       lang      = app.lang,
 38       MathRound = Math.round,
 39       pwlib     = window.pwlib,
 40       appEvent  = pwlib.appEvent,
 41       win       = app.win;
 42 
 43   this.app = app;
 44   this.idPrefix = 'paintweb' + app.UID + '_',
 45   this.classPrefix = 'paintweb_';
 46 
 47   /**
 48    * Holds references to DOM elements.
 49    * @type Object
 50    */
 51   this.elems = {};
 52 
 53   /**
 54    * Holds references to input elements associated to the PaintWeb configuration 
 55    * properties.
 56    * @type Object
 57    */
 58   this.inputs = {};
 59 
 60   /**
 61    * Holds references to DOM elements associated to configuration values.
 62    * @type Object
 63    */
 64   this.inputValues = {};
 65 
 66   /**
 67    * Holds references to DOM elements associated to color configuration 
 68    * properties.
 69    *
 70    * @type Object
 71    * @see pwlib.guiColorInput
 72    */
 73   this.colorInputs = {};
 74 
 75   /**
 76    * Holds references to DOM elements associated to each tool registered in the 
 77    * current PaintWeb application instance.
 78    *
 79    * @private
 80    * @type Object
 81    */
 82   this.tools = {};
 83 
 84   /**
 85    * Holds references to DOM elements associated to PaintWeb commands.
 86    *
 87    * @private
 88    * @type Object
 89    */
 90   this.commands = {};
 91 
 92   /**
 93    * Holds references to floating panels GUI components.
 94    *
 95    * @type Object
 96    * @see pwlib.guiFloatingPanel
 97    */
 98   this.floatingPanels = {zIndex_: 0};
 99 
100   /**
101    * Holds references to tab panel GUI components.
102    *
103    * @type Object
104    * @see pwlib.guiTabPanel
105    */
106   this.tabPanels = {};
107 
108   /**
109    * Holds an instance of the guiResizer object attached to the Canvas.
110    *
111    * @private
112    * @type pwlib.guiResizer
113    */
114   this.canvasResizer = null;
115 
116   /**
117    * Holds an instance of the guiResizer object attached to the viewport 
118    * element.
119    *
120    * @private
121    * @type pwlib.guiResizer
122    */
123   this.viewportResizer = null;
124 
125   /**
126    * Holds tab configuration information for most drawing tools.
127    *
128    * @private
129    * @type Object
130    */
131   this.toolTabConfig = {
132     bcurve: {
133       lineTab: true,
134       shapeType: true,
135       lineWidth: true,
136       lineWidthLabel: lang.inputs.borderWidth,
137       lineCap: true
138     },
139     ellipse: {
140       lineTab: true,
141       shapeType: true,
142       lineWidth: true,
143       lineWidthLabel: lang.inputs.borderWidth
144     },
145     rectangle: {
146       lineTab: true,
147       shapeType: true,
148       lineWidth: true,
149       lineWidthLabel: lang.inputs.borderWidth,
150       lineJoin: true
151     },
152     polygon: {
153       lineTab: true,
154       shapeType: true,
155       lineWidth: true,
156       lineWidthLabel: lang.inputs.borderWidth,
157       lineJoin: true,
158       lineCap: true,
159       miterLimit: true
160     },
161     eraser: {
162       lineTab: true,
163       lineWidth: true,
164       lineWidthLabel: lang.inputs.eraserSize,
165       lineJoin: true,
166       lineCap: true,
167       miterLimit: true
168     },
169     pencil: {
170       lineTab: true,
171       lineWidth: true,
172       lineWidthLabel: lang.inputs.pencilSize,
173       lineJoin: true,
174       lineCap: true,
175       miterLimit: true
176     },
177     line: {
178       lineTab: true,
179       lineWidth: true,
180       lineWidthLabel: lang.inputs.line.lineWidth,
181       lineJoin: true,
182       lineCap: true,
183       miterLimit: true
184     },
185     text: {
186       lineTab: true,
187       lineTabLabel: lang.tabs.main.textBorder,
188       shapeType: true,
189       lineWidth: true,
190       lineWidthLabel: lang.inputs.borderWidth
191     }
192   };
193 
194   /**
195    * Initialize the PaintWeb interface.
196    *
197    * @param {Document|String} markup The interface markup loaded and parsed as 
198    * DOM Document object. Optionally, the value can be a string holding the 
199    * interface markup (this is used when PaintWeb is packaged).
200    *
201    * @returns {Boolean} True if the initialization was successful, or false if 
202    * not.
203    */
204   this.init = function (markup) {
205     // Make sure the user nicely waits for PaintWeb to load, without seeing 
206     // much.
207     var placeholder = config.guiPlaceholder,
208         placeholderStyle = placeholder.style;
209 
210     placeholderStyle.display = 'none';
211     placeholderStyle.height = '1px';
212     placeholderStyle.overflow = 'hidden';
213     placeholderStyle.position = 'absolute';
214     placeholderStyle.visibility = 'hidden';
215 
216     placeholder.className += ' ' + this.classPrefix + 'placeholder';
217     if (!placeholder.tabIndex || placeholder.tabIndex == -1) {
218       placeholder.tabIndex = 1;
219     }
220 
221     if (!this.initImportDoc(markup)) {
222       app.initError(lang.guiMarkupImportFailed);
223       return false;
224     }
225     markup = null;
226 
227     if (!this.initParseMarkup()) {
228       app.initError(lang.guiMarkupParseFailed);
229       return false;
230     }
231 
232     if (!this.initCanvas() ||
233         !this.initImageZoom() ||
234         !this.initSelectionTool() ||
235         !this.initTextTool() ||
236         !this.initKeyboardShortcuts()) {
237       return false;
238     }
239 
240     // Setup the main tabbed panel.
241     var panel = this.tabPanels.main;
242     if (!panel) {
243       app.initError(lang.noMainTabPanel);
244       return false;
245     }
246 
247     // Hide the "Shadow" tab if the drawing of shadows is not supported.
248     if (!app.shadowSupported && 'shadow' in panel.tabs) {
249       panel.tabHide('shadow');
250     }
251 
252     if (!('viewport' in this.elems)) {
253       app.initError(lang.missingViewport);
254       return false;
255     }
256 
257     // Setup the GUI dimensions .
258     this.elems.viewport.style.height = config.viewportHeight;
259     placeholderStyle.width = config.viewportWidth;
260 
261     // Setup the Canvas resizer.
262     var resizeHandle = this.elems.canvasResizer;
263     if (!resizeHandle) {
264       app.initError(lang.missingCanvasResizer);
265       return false;
266     }
267     resizeHandle.title = lang.guiCanvasResizer;
268     resizeHandle.replaceChild(doc.createTextNode(resizeHandle.title), 
269         resizeHandle.firstChild);
270     resizeHandle.addEventListener('mouseover', this.item_mouseover, false);
271     resizeHandle.addEventListener('mouseout',  this.item_mouseout,  false);
272 
273     this.canvasResizer = new pwlib.guiResizer(this, resizeHandle, 
274         this.elems.canvasContainer);
275 
276     this.canvasResizer.events.add('guiResizeStart', this.canvasResizeStart);
277     this.canvasResizer.events.add('guiResizeEnd',   this.canvasResizeEnd);
278 
279     // Setup the viewport resizer.
280     var resizeHandle = this.elems.viewportResizer;
281     if (!resizeHandle) {
282       app.initError(lang.missingViewportResizer);
283       return false;
284     }
285     resizeHandle.title = lang.guiViewportResizer;
286     resizeHandle.replaceChild(doc.createTextNode(resizeHandle.title), 
287         resizeHandle.firstChild);
288     resizeHandle.addEventListener('mouseover', this.item_mouseover, false);
289     resizeHandle.addEventListener('mouseout',  this.item_mouseout,  false);
290 
291     this.viewportResizer = new pwlib.guiResizer(this, resizeHandle, 
292         this.elems.viewport);
293 
294     this.viewportResizer.dispatchMouseMove = true;
295     this.viewportResizer.events.add('guiResizeMouseMove', 
296         this.viewportResizeMouseMove);
297     this.viewportResizer.events.add('guiResizeEnd', this.viewportResizeEnd);
298 
299     if ('statusMessage' in this.elems) {
300       this.elems.statusMessage._prevText = false;
301     }
302 
303     // Update the version string in Help.
304     if ('version' in this.elems) {
305       this.elems.version.appendChild(doc.createTextNode(app.toString()));
306     }
307 
308     // Update the image dimensions in the GUI.
309     var imageSize = this.elems.imageSize;
310     if (imageSize) {
311       imageSize.replaceChild(doc.createTextNode(app.image.width + 'x' 
312             + app.image.height), imageSize.firstChild);
313     }
314 
315     // Add application-wide event listeners.
316     app.events.add('canvasSizeChange',  this.canvasSizeChange);
317     app.events.add('commandRegister',   this.commandRegister);
318     app.events.add('commandUnregister', this.commandUnregister);
319     app.events.add('configChange',      this.configChangeHandler);
320     app.events.add('imageSizeChange',   this.imageSizeChange);
321     app.events.add('imageZoom',         this.imageZoom);
322     app.events.add('appInit',           this.appInit);
323     app.events.add('shadowAllow',       this.shadowAllow);
324     app.events.add('toolActivate',      this.toolActivate);
325     app.events.add('toolRegister',      this.toolRegister);
326     app.events.add('toolUnregister',    this.toolUnregister);
327 
328     // Make sure the historyUndo and historyRedo command elements are 
329     // synchronized with the application history state.
330     if ('historyUndo' in this.commands && 'historyRedo' in this.commands) {
331       app.events.add('historyUpdate', this.historyUpdate);
332     }
333 
334     app.commandRegister('about', this.commandAbout);
335 
336     return true;
337   };
338 
339   /**
340    * Initialize the Canvas elements.
341    *
342    * @private
343    * @returns {Boolean} True if the initialization was successful, or false if 
344    * not.
345    */
346   this.initCanvas = function () {
347     var canvasContainer = this.elems.canvasContainer,
348         layerCanvas     = app.layer.canvas,
349         layerContext    = app.layer.context,
350         layerStyle      = layerCanvas.style,
351         bufferCanvas    = app.buffer.canvas;
352 
353     if (!canvasContainer) {
354       app.initError(lang.missingCanvasContainer);
355       return false;
356     }
357 
358     var containerStyle  = canvasContainer.style;
359 
360     canvasContainer.className = this.classPrefix + 'canvasContainer';
361     layerCanvas.className     = this.classPrefix + 'layerCanvas';
362     bufferCanvas.className    = this.classPrefix + 'bufferCanvas';
363 
364     containerStyle.width  = layerStyle.width;
365     containerStyle.height = layerStyle.height;
366     if (!config.checkersBackground || pwlib.browser.olpcxo) {
367       containerStyle.backgroundImage = 'none';
368     }
369 
370     canvasContainer.appendChild(layerCanvas);
371     canvasContainer.appendChild(bufferCanvas);
372 
373     // Make sure the selection transparency input checkbox is disabled if the 
374     // putImageData and getImageData methods are unsupported.
375     if ('selection_transparent' in this.inputs && (!layerContext.putImageData || 
376           !layerContext.getImageData)) {
377       this.inputs.selection_transparent.disabled = true;
378       this.inputs.selection_transparent.checked = true;
379     }
380 
381     return true;
382   };
383 
384   /**
385    * Import the DOM nodes from the interface DOM document. All the nodes are 
386    * inserted into the {@link PaintWeb.config.guiPlaceholder} element.
387    *
388    * <p>Elements which have the ID attribute will have the attribute renamed to 
389    * <code>data-pwId</code>.
390    *
391    * <p>Input elements which have the ID attribute will have their attribute 
392    * updated to be unique for the current PaintWeb instance.
393    *
394    * @private
395    *
396    * @param {Document|String} markup The source DOM document to import the nodes 
397    * from. Optionally, this parameter can be a string which holds the interface 
398    * markup.
399    *
400    * @returns {Boolean} True if the initialization was successful, or false if 
401    * not.
402    */
403   this.initImportDoc = function (markup) {
404     // I could use some XPath here, but for the sake of compatibility I don't.
405     var destElem = config.guiPlaceholder,
406         elType = Node.ELEMENT_NODE,
407         elem, root, nodes, n, tag, isInput;
408 
409     if (typeof markup === 'string') {
410       elem = doc.createElement('div');
411       elem.innerHTML = markup;
412       root = elem.firstChild;
413     } else {
414       root = markup.documentElement;
415     }
416     markup = null;
417 
418     nodes = root.getElementsByTagName('*');
419     n = nodes.length;
420 
421     // Change all the id attributes to be data-pwId attributes.
422     // Input elements have their ID updated to be unique for the current 
423     // PaintWeb instance.
424     for (var i = 0; i < n; i++) {
425       elem = nodes[i];
426       if (elem.nodeType !== elType) {
427         continue;
428       }
429       tag = elem.tagName.toLowerCase();
430       isInput = tag === 'input' || tag === 'select' || tag === 'textarea';
431 
432       if (elem.id) {
433         elem.setAttribute('data-pwId', elem.id);
434 
435         if (isInput) {
436           elem.id = this.idPrefix + elem.id;
437         } else {
438           elem.removeAttribute('id');
439         }
440       }
441 
442       // label elements have their "for" attribute updated as well.
443       if (tag === 'label' && elem.htmlFor) {
444         elem.htmlFor = this.idPrefix + elem.htmlFor;
445       }
446     }
447 
448     // Import all the nodes.
449     n = root.childNodes.length;
450     for (var i = 0; i < n; i++) {
451       destElem.appendChild(doc.importNode(root.childNodes[i], true));
452     }
453 
454     return true;
455   };
456 
457   /**
458    * Parse the interface markup. The layout file can have custom 
459    * PaintWeb-specific attributes.
460    *
461    * <p>Elements with the <code>data-pwId</code> attribute are added to the 
462    * {@link pwlib.gui#elems} object.
463    *
464    * <p>Elements having the <code>data-pwCommand</code> attribute are added to 
465    * the {@link pwlib.gui#commands} object.
466    *
467    * <p>Elements having the <code>data-pwTool</code> attribute are added to the 
468    * {@link pwlib.gui#tools} object.
469    *
470    * <p>Elements having the <code>data-pwTabPanel</code> attribute are added to 
471    * the {@link pwlib.gui#tabPanels} object. These become interactive GUI 
472    * components (see {@link pwlib.guiTabPanel}).
473    *
474    * <p>Elements having the <code>data-pwFloatingPanel</code> attribute are 
475    * added to the {@link pwlib.gui#floatingPanels} object. These become 
476    * interactive GUI components (see {@link pwlib.guiFloatingPanel}).
477    *
478    * <p>Elements having the <code>data-pwConfig</code> attribute are added to 
479    * the {@link pwlib.gui#inputs} object. These become interactive GUI 
480    * components which allow users to change configuration options.
481    *
482    * <p>Elements having the <code>data-pwConfigValue</code> attribute are added 
483    * to the {@link pwlib.gui#inputValues} object. These can only be child nodes 
484    * of elements which have the <code>data-pwConfig</code> attribute. Each such 
485    * element is considered an icon. Anchor elements are appended to ensure 
486    * keyboard accessibility.
487    *
488    * <p>Elements having the <code>data-pwConfigToggle</code> attribute are added 
489    * to the {@link pwlib.gui#inputs} object. These become interactive GUI 
490    * components which toggle the boolean value of the configuration property 
491    * they are associated to.
492    *
493    * <p>Elements having the <code>data-pwColorInput</code> attribute are added 
494    * to the {@link pwlib.gui#colorInputs} object. These become color picker 
495    * inputs which are associated to the configuration property given as the 
496    * attribute value. (see {@link pwlib.guiColorInput})
497    *
498    * @returns {Boolean} True if the parsing was successful, or false if not.
499    */
500   this.initParseMarkup = function () {
501     var nodes = config.guiPlaceholder.getElementsByTagName('*'),
502         elType = Node.ELEMENT_NODE,
503         elem, tag, isInput, tool, tabPanel, floatingPanel, cmd, id, cfgAttr, 
504         colorInput;
505 
506     // Store references to important elements and parse PaintWeb-specific 
507     // attributes.
508     for (var i = 0; i < nodes.length; i++) {
509       elem = nodes[i];
510       if (elem.nodeType !== elType) {
511         continue;
512       }
513       tag = elem.tagName.toLowerCase();
514       isInput = tag === 'input' || tag === 'select' || tag === 'textarea';
515 
516       // Store references to commands.
517       cmd = elem.getAttribute('data-pwCommand');
518       if (cmd && !(cmd in this.commands)) {
519         elem.className += ' ' + this.classPrefix + 'command';
520         this.commands[cmd] = elem;
521       }
522 
523       // Store references to tools.
524       tool = elem.getAttribute('data-pwTool');
525       if (tool && !(tool in this.tools)) {
526         elem.className += ' ' + this.classPrefix + 'tool';
527         this.tools[tool] = elem;
528       }
529 
530       // Create tab panels.
531       tabPanel = elem.getAttribute('data-pwTabPanel');
532       if (tabPanel) {
533         this.tabPanels[tabPanel] = new pwlib.guiTabPanel(this, elem);
534       }
535 
536       // Create floating panels.
537       floatingPanel = elem.getAttribute('data-pwFloatingPanel');
538       if (floatingPanel) {
539         this.floatingPanels[floatingPanel] = new pwlib.guiFloatingPanel(this, 
540             elem);
541       }
542 
543       cfgAttr = elem.getAttribute('data-pwConfig');
544       if (cfgAttr) {
545         if (isInput) {
546           this.initConfigInput(elem, cfgAttr);
547         } else {
548           this.initConfigIcons(elem, cfgAttr);
549         }
550       }
551 
552       cfgAttr = elem.getAttribute('data-pwConfigToggle');
553       if (cfgAttr) {
554         this.initConfigToggle(elem, cfgAttr);
555       }
556 
557       // elem.hasAttribute() fails in webkit (tested with chrome and safari 4)
558       if (elem.getAttribute('data-pwColorInput')) {
559         colorInput = new pwlib.guiColorInput(this, elem);
560         this.colorInputs[colorInput.id] = colorInput;
561       }
562 
563       id = elem.getAttribute('data-pwId');
564       if (id) {
565         elem.className += ' ' + this.classPrefix + id;
566 
567         // Store a reference to the element.
568         if (isInput && !cfgAttr) {
569           this.inputs[id] = elem;
570         } else if (!isInput) {
571           this.elems[id] = elem;
572         }
573       }
574     }
575 
576     return true;
577   };
578 
579   /**
580    * Initialize an input element associated to a configuration property.
581    *
582    * @private
583    *
584    * @param {Element} elem The DOM element which is associated to the 
585    * configuration property.
586    *
587    * @param {String} cfgAttr The configuration attribute. This tells the 
588    * configuration group and property to which the DOM element is attached to.
589    */
590   this.initConfigInput = function (input, cfgAttr) {
591     var cfgNoDots   = cfgAttr.replace('.', '_'),
592         cfgArray    = cfgAttr.split('.'),
593         cfgProp     = cfgArray.pop(),
594         cfgGroup    = cfgArray.join('.'),
595         cfgGroupRef = config,
596         langGroup   = lang.inputs,
597         labelElem   = input.parentNode;
598 
599     for (var i = 0, n = cfgArray.length; i < n; i++) {
600       cfgGroupRef = cfgGroupRef[cfgArray[i]];
601       langGroup = langGroup[cfgArray[i]];
602     }
603 
604     input._pwConfigProperty = cfgProp;
605     input._pwConfigGroup = cfgGroup;
606     input._pwConfigGroupRef = cfgGroupRef;
607     input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
608     input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots;
609 
610     this.inputs[cfgNoDots] = input;
611 
612     if (labelElem.tagName.toLowerCase() !== 'label') {
613       labelElem = labelElem.getElementsByTagName('label')[0];
614     }
615 
616     if (input.type === 'checkbox' || labelElem.htmlFor) {
617       labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
618           labelElem.lastChild);
619     } else {
620       labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
621           labelElem.firstChild);
622     }
623 
624     if (input.type === 'checkbox') {
625       input.checked = cfgGroupRef[cfgProp];
626     } else {
627       input.value = cfgGroupRef[cfgProp];
628     }
629 
630     input.addEventListener('input',  this.configInputChange, false);
631     input.addEventListener('change', this.configInputChange, false);
632   };
633 
634   /**
635    * Initialize an HTML element associated to a configuration property, and all 
636    * of its own sub-elements associated to configuration values. Each element 
637    * that has the <var>data-pwConfigValue</var> attribute is considered an icon.
638    *
639    * @private
640    *
641    * @param {Element} elem The DOM element which is associated to the 
642    * configuration property.
643    *
644    * @param {String} cfgAttr The configuration attribute. This tells the 
645    * configuration group and property to which the DOM element is attached to.
646    */
647   this.initConfigIcons = function (input, cfgAttr) {
648     var cfgNoDots   = cfgAttr.replace('.', '_'),
649         cfgArray    = cfgAttr.split('.'),
650         cfgProp     = cfgArray.pop(),
651         cfgGroup    = cfgArray.join('.'),
652         cfgGroupRef = config,
653         langGroup   = lang.inputs;
654 
655     for (var i = 0, n = cfgArray.length; i < n; i++) {
656       cfgGroupRef = cfgGroupRef[cfgArray[i]];
657       langGroup = langGroup[cfgArray[i]];
658     }
659 
660     input._pwConfigProperty = cfgProp;
661     input._pwConfigGroup = cfgGroup;
662     input._pwConfigGroupRef = cfgGroupRef;
663     input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
664     input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots;
665 
666     this.inputs[cfgNoDots] = input;
667 
668     var labelElem = input.getElementsByTagName('p')[0];
669     labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
670         labelElem.firstChild);
671 
672     var elem, anchor, val,
673         className = ' ' + this.classPrefix + 'configActive';
674         nodes = input.getElementsByTagName('*'),
675         elType = Node.ELEMENT_NODE;
676 
677     for (var i = 0; i < nodes.length; i++) {
678       elem = nodes[i];
679       if (elem.nodeType !== elType) {
680         continue;
681       }
682 
683       val = elem.getAttribute('data-pwConfigValue');
684       if (!val) {
685         continue;
686       }
687 
688       anchor = doc.createElement('a');
689       anchor.href = '#';
690       anchor.title = langGroup[cfgProp + '_' + val];
691       anchor.appendChild(doc.createTextNode(anchor.title));
692 
693       elem.className += ' ' + this.classPrefix + cfgProp + '_' + val 
694         + ' ' + this.classPrefix + 'icon';
695       elem._pwConfigParent = input;
696 
697       if (cfgGroupRef[cfgProp] == val) {
698         elem.className += className;
699       }
700 
701       anchor.addEventListener('click',     this.configValueClick, false);
702       anchor.addEventListener('mouseover', this.item_mouseover,   false);
703       anchor.addEventListener('mouseout',  this.item_mouseout,    false);
704 
705       elem.replaceChild(anchor, elem.firstChild);
706 
707       this.inputValues[cfgGroup + '_' + cfgProp + '_' + val] = elem;
708     }
709   };
710 
711   /**
712    * Initialize an HTML element associated to a boolean configuration property.
713    *
714    * @private
715    *
716    * @param {Element} elem The DOM element which is associated to the 
717    * configuration property.
718    *
719    * @param {String} cfgAttr The configuration attribute. This tells the 
720    * configuration group and property to which the DOM element is attached to.
721    */
722   this.initConfigToggle = function (input, cfgAttr) {
723     var cfgNoDots   = cfgAttr.replace('.', '_'),
724         cfgArray    = cfgAttr.split('.'),
725         cfgProp     = cfgArray.pop(),
726         cfgGroup    = cfgArray.join('.'),
727         cfgGroupRef = config,
728         langGroup   = lang.inputs;
729 
730     for (var i = 0, n = cfgArray.length; i < n; i++) {
731       cfgGroupRef = cfgGroupRef[cfgArray[i]];
732       langGroup = langGroup[cfgArray[i]];
733     }
734 
735     input._pwConfigProperty = cfgProp;
736     input._pwConfigGroup = cfgGroup;
737     input._pwConfigGroupRef = cfgGroupRef;
738     input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots 
739       + ' ' + this.classPrefix + 'icon';
740 
741     if (cfgGroupRef[cfgProp]) {
742       input.className += ' ' + this.classPrefix + 'configActive';
743     }
744 
745     var anchor = doc.createElement('a');
746     anchor.href = '#';
747     anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
748     anchor.appendChild(doc.createTextNode(langGroup[cfgProp]));
749 
750     anchor.addEventListener('click',     this.configToggleClick, false);
751     anchor.addEventListener('mouseover', this.item_mouseover,    false);
752     anchor.addEventListener('mouseout',  this.item_mouseout,     false);
753 
754     input.replaceChild(anchor, input.firstChild);
755 
756     this.inputs[cfgNoDots] = input;
757   };
758 
759   /**
760    * Initialize the image zoom input.
761    *
762    * @private
763    * @returns {Boolean} True if the initialization was successful, or false if 
764    * not.
765    */
766   this.initImageZoom = function () {
767     var input = this.inputs.imageZoom;
768     if (!input) {
769       return true; // allow layouts without the zoom input
770     }
771 
772     input.value = 100;
773     input._old_value = 100;
774 
775     // Override the attributes, based on the settings.
776     input.setAttribute('step', config.imageZoomStep * 100);
777     input.setAttribute('max',  config.imageZoomMax  * 100);
778     input.setAttribute('min',  config.imageZoomMin  * 100);
779 
780     var changeFn = function () {
781       app.imageZoomTo(parseInt(this.value) / 100);
782     };
783 
784     input.addEventListener('change', changeFn, false);
785     input.addEventListener('input',  changeFn, false);
786 
787     // Update some language strings
788 
789     var label = input.parentNode;
790     if (label.tagName.toLowerCase() === 'label') {
791       label.replaceChild(doc.createTextNode(lang.imageZoomLabel), 
792           label.firstChild);
793     }
794 
795     var elem = this.elems.statusZoom;
796     if (!elem) {
797       return true;
798     }
799 
800     elem.title = lang.imageZoomTitle;
801 
802     return true;
803   };
804 
805   /**
806    * Initialize GUI elements associated to selection tool options and commands.
807    *
808    * @private
809    * @returns {Boolean} True if the initialization was successful, or false if 
810    * not.
811    */
812   this.initSelectionTool = function () {
813     var classDisabled = ' ' + this.classPrefix + 'disabled',
814         cut   = this.commands.selectionCut,
815         copy  = this.commands.selectionCopy,
816         paste = this.commands.clipboardPaste;
817 
818     if (paste) {
819       app.events.add('clipboardUpdate', this.clipboardUpdate);
820       paste.className += classDisabled;
821 
822     }
823 
824     if (cut && copy) {
825       app.events.add('selectionChange', this.selectionChange);
826       cut.className  += classDisabled;
827       copy.className += classDisabled;
828     }
829 
830     var selTab_cmds = ['selectionCut', 'selectionCopy', 'clipboardPaste'],
831         anchor, elem, cmd;
832 
833     for (var i = 0, n = selTab_cmds.length; i < n; i++) {
834       cmd = selTab_cmds[i];
835       elem = this.elems['selTab_' + cmd];
836       if (!elem) {
837         continue;
838       }
839 
840       anchor = doc.createElement('a');
841       anchor.title = lang.commands[cmd];
842       anchor.href = '#';
843       anchor.appendChild(doc.createTextNode(anchor.title));
844       anchor.addEventListener('click', this.commandClick, false);
845 
846       elem.className += classDisabled + ' ' + this.classPrefix + 'command' 
847         + ' ' + this.classPrefix + 'cmd_' + cmd;
848       elem.setAttribute('data-pwCommand', cmd);
849       elem.replaceChild(anchor, elem.firstChild);
850     }
851 
852     var selCrop   = this.commands.selectionCrop,
853         selFill   = this.commands.selectionFill,
854         selDelete = this.commands.selectionDelete;
855 
856     selCrop.className   += classDisabled;
857     selFill.className   += classDisabled;
858     selDelete.className += classDisabled;
859 
860     return true;
861   };
862 
863   /**
864    * Initialize GUI elements associated to text tool options.
865    *
866    * @private
867    * @returns {Boolean} True if the initialization was successful, or false if 
868    * not.
869    */
870   this.initTextTool = function () {
871     if ('textString' in this.inputs) {
872       this.inputs.textString.value = lang.inputs.text.textString_value;
873     }
874 
875     if (!('text_fontFamily' in this.inputs) || !('text' in config) || 
876         !('fontFamilies' in config.text)) {
877       return true;
878     }
879 
880     var option, input = this.inputs.text_fontFamily;
881     for (var i = 0, n = config.text.fontFamilies.length; i < n; i++) {
882       option = doc.createElement('option');
883       option.value = config.text.fontFamilies[i];
884       option.appendChild(doc.createTextNode(option.value));
885       input.appendChild(option);
886 
887       if (option.value === config.text.fontFamily) {
888         input.selectedIndex = i;
889         input.value = option.value;
890       }
891     }
892 
893     option = doc.createElement('option');
894     option.value = '+';
895     option.appendChild(doc.createTextNode(lang.inputs.text.fontFamily_add));
896     input.appendChild(option);
897 
898     return true;
899   };
900 
901   /**
902    * Initialize the keyboard shortcuts. Basically, this updates various strings 
903    * to ensure the user interface is informational.
904    *
905    * @private
906    * @returns {Boolean} True if the initialization was successful, or false if 
907    * not.
908    */
909   this.initKeyboardShortcuts = function () {
910     var kid = null, kobj = null;
911 
912     for (kid in config.keys) {
913       kobj = config.keys[kid];
914 
915       if ('toolActivate' in kobj && kobj.toolActivate in lang.tools) {
916         lang.tools[kobj.toolActivate] += ' [ ' + kid + ' ]';
917       }
918 
919       if ('command' in kobj && kobj.command in lang.commands) {
920         lang.commands[kobj.command] += ' [ ' + kid + ' ]';
921       }
922     }
923 
924     return true;
925   };
926 
927   /**
928    * The <code>appInit</code> event handler. This method is invoked once 
929    * PaintWeb completes all the loading.
930    *
931    * <p>This method dispatches the {@link pwlib.appEvent.guiShow} application 
932    * event.
933    *
934    * @private
935    * @param {pwlib.appEvent.appInit} ev The application event object.
936    */
937   this.appInit = function (ev) {
938     // Initialization was not successful ...
939     if (ev.state !== PaintWeb.INIT_DONE) {
940       return;
941     }
942 
943     // Make sure the Hand tool is enabled/disabled as needed.
944     if ('hand' in _self.tools) {
945       app.events.add('canvasSizeChange',   _self.toolHandStateChange);
946       app.events.add('viewportSizeChange', _self.toolHandStateChange);
947       _self.toolHandStateChange(ev);
948     }
949 
950     // Make PaintWeb visible.
951     var placeholder = config.guiPlaceholder,
952         placeholderStyle = placeholder.style;
953 
954     // We do not reset the display property. We leave this for the stylesheet.
955     placeholderStyle.height = '';
956     placeholderStyle.overflow = '';
957     placeholderStyle.position = '';
958     placeholderStyle.visibility = '';
959 
960     var cs = win.getComputedStyle(placeholder, null);
961 
962     // Do not allow the static positioning for the PaintWeb placeholder.  
963     // Usually, the GUI requires absolute/relative positioning.
964     if (cs.position === 'static') {
965       placeholderStyle.position = 'relative';
966     }
967 
968     placeholder.focus();
969 
970     app.events.dispatch(new appEvent.guiShow());
971   };
972 
973   /**
974    * The <code>guiResizeStart</code> event handler for the Canvas resize 
975    * operation.
976    * @private
977    */
978   this.canvasResizeStart = function () {
979     this.resizeHandle.style.visibility = 'hidden';
980 
981     // ugly...
982     this.timeout_ = setTimeout(function () {
983       _self.statusShow('guiCanvasResizerActive', true);
984       clearTimeout(_self.canvasResizer.timeout_);
985       delete _self.canvasResizer.timeout_;
986     }, 400);
987   };
988 
989   /**
990    * The <code>guiResizeEnd</code> event handler for the Canvas resize 
991    * operation.
992    *
993    * @private
994    * @param {pwlib.appEvent.guiResizeEnd} ev The application event object.
995    */
996   this.canvasResizeEnd = function (ev) {
997     this.resizeHandle.style.visibility = '';
998 
999     app.imageCrop(0, 0, MathRound(ev.width / app.image.canvasScale),
1000         MathRound(ev.height / app.image.canvasScale));
1001 
1002     if (this.timeout_) {
1003       clearTimeout(this.timeout_);
1004       delete this.timeout_;
1005     } else {
1006       _self.statusShow(-1);
1007     }
1008   };
1009 
1010   /**
1011    * The <code>guiResizeMouseMove</code> event handler for the viewport resize 
1012    * operation.
1013    *
1014    * @private
1015    * @param {pwlib.appEvent.guiResizeMouseMove} ev The application event object.
1016    */
1017   this.viewportResizeMouseMove = function (ev) {
1018     config.guiPlaceholder.style.width = ev.width + 'px';
1019   };
1020 
1021   /**
1022    * The <code>guiResizeEnd</code> event handler for the viewport resize 
1023    * operation.
1024    *
1025    * @private
1026    * @param {pwlib.appEvent.guiResizeEnd} ev The application event object.
1027    */
1028   this.viewportResizeEnd = function (ev) {
1029     _self.elems.viewport.style.width = '';
1030     _self.resizeTo(ev.width + 'px', ev.height + 'px');
1031     config.guiPlaceholder.focus();
1032   };
1033 
1034   /**
1035    * The <code>mouseover</code> event handler for all tools, commands and icons.  
1036    * This simply shows the title / text content of the element in the GUI status 
1037    * bar.
1038    *
1039    * @see pwlib.gui#statusShow The method used for displaying the message in the 
1040    * GUI status bar.
1041    */
1042   this.item_mouseover = function () {
1043     if (this.title || this.textConent) {
1044       _self.statusShow(this.title || this.textContent, true);
1045     }
1046   };
1047 
1048   /**
1049    * The <code>mouseout</code> event handler for all tools, commands and icons.  
1050    * This method simply resets the GUI status bar to the previous message it was 
1051    * displaying before the user hovered the current element.
1052    *
1053    * @see pwlib.gui#statusShow The method used for displaying the message in the 
1054    * GUI status bar.
1055    */
1056   this.item_mouseout = function () {
1057     _self.statusShow(-1);
1058   };
1059 
1060   /**
1061    * Show a message in the status bar.
1062    *
1063    * @param {String|Number} msg The message ID you want to display. The ID 
1064    * should be available in the {@link PaintWeb.lang.status} object. If the 
1065    * value is -1 then the previous non-temporary message will be displayed. If 
1066    * the ID is not available in the language file, then the string is shown 
1067    * as-is.
1068    *
1069    * @param {Boolean} [temporary=false] Tells if the message is temporary or 
1070    * not.
1071    */
1072   this.statusShow = function (msg, temporary) {
1073     var elem = this.elems.statusMessage;
1074     if (msg === -1 && elem._prevText === false) {
1075       return false;
1076     }
1077 
1078     if (msg === -1) {
1079       msg = elem._prevText;
1080     }
1081 
1082     if (msg in lang.status) {
1083       msg = lang.status[msg];
1084     }
1085 
1086     if (!temporary) {
1087       elem._prevText = msg;
1088     }
1089 
1090     if (elem.firstChild) {
1091       elem.removeChild(elem.firstChild);
1092     }
1093 
1094     win.status = msg;
1095 
1096     if (msg) {
1097       elem.appendChild(doc.createTextNode(msg));
1098     }
1099   };
1100 
1101   /**
1102    * The "About" command. This method displays the "About" panel.
1103    */
1104   this.commandAbout = function () {
1105     _self.floatingPanels.about.toggle();
1106   };
1107 
1108   /**
1109    * The <code>click</code> event handler for the tool DOM elements.
1110    *
1111    * @private
1112    *
1113    * @param {Event} ev The DOM Event object.
1114    *
1115    * @see PaintWeb#toolActivate to activate a drawing tool.
1116    */
1117   this.toolClick = function (ev) {
1118     app.toolActivate(this.parentNode.getAttribute('data-pwTool'), ev);
1119     ev.preventDefault();
1120   };
1121 
1122   /**
1123    * The <code>toolActivate</code> application event handler. This method 
1124    * provides visual feedback for the activation of a new drawing tool.
1125    *
1126    * @private
1127    *
1128    * @param {pwlib.appEvent.toolActivate} ev The application event object.
1129    *
1130    * @see PaintWeb#toolActivate the method which allows you to activate 
1131    * a drawing tool.
1132    */
1133   this.toolActivate = function (ev) {
1134     var tabAnchor,
1135         tabActive = _self.tools[ev.id],
1136         tabConfig = _self.toolTabConfig[ev.id] || {},
1137         tabPanel = _self.tabPanels.main,
1138         lineTab = tabPanel.tabs.line,
1139         shapeType = _self.inputs.shapeType,
1140         lineWidth = _self.inputs.line_lineWidth,
1141         lineCap = _self.inputs.line_lineCap,
1142         lineJoin = _self.inputs.line_lineJoin,
1143         miterLimit = _self.inputs.line_miterLimit,
1144         lineWidthLabel = null;
1145 
1146     tabActive.className += ' ' + _self.classPrefix + 'toolActive';
1147     tabActive.firstChild.focus();
1148 
1149     if ((ev.id + 'Active') in lang.status) {
1150       _self.statusShow(ev.id + 'Active');
1151     }
1152 
1153     // show/hide the shapeType input config.
1154     if (shapeType) {
1155       if (tabConfig.shapeType) {
1156         shapeType.style.display = '';
1157       } else {
1158         shapeType.style.display = 'none';
1159       }
1160     }
1161 
1162     if (ev.prevId) {
1163       var prevTab = _self.tools[ev.prevId],
1164           prevTabConfig = _self.toolTabConfig[ev.prevId] || {};
1165 
1166       prevTab.className = prevTab.className.
1167         replace(' ' + _self.classPrefix + 'toolActive', '');
1168 
1169       // hide the line tab
1170       if (prevTabConfig.lineTab && lineTab) {
1171         tabPanel.tabHide('line');
1172         lineTab.container.className = lineTab.container.className.
1173           replace(' ' + _self.classPrefix + 'main_line_' + ev.prevId, 
1174               ' ' + _self.classPrefix + 'main_line');
1175       }
1176 
1177       // hide the tab for the current tool.
1178       if (ev.prevId in tabPanel.tabs) {
1179         tabPanel.tabHide(ev.prevId);
1180       }
1181     }
1182 
1183     // Change the label of the lineWidth input element.
1184     if (tabConfig.lineWidthLabel) {
1185       lineWidthLabel = lineWidth.parentNode;
1186       lineWidthLabel.replaceChild(doc.createTextNode(tabConfig.lineWidthLabel), 
1187           lineWidthLabel.firstChild);
1188 
1189     }
1190 
1191     if (lineJoin) {
1192       if (tabConfig.lineJoin) {
1193         lineJoin.style.display = '';
1194       } else {
1195         lineJoin.style.display = 'none';
1196       }
1197     }
1198 
1199     if (lineCap) {
1200       if (tabConfig.lineCap) {
1201         lineCap.style.display = '';
1202       } else {
1203         lineCap.style.display = 'none';
1204       }
1205     }
1206 
1207     if (miterLimit) {
1208       if (tabConfig.miterLimit) {
1209         miterLimit.parentNode.parentNode.style.display = '';
1210       } else {
1211         miterLimit.parentNode.parentNode.style.display = 'none';
1212       }
1213     }
1214 
1215     if (lineWidth) {
1216       if (tabConfig.lineWidth) {
1217         lineWidth.parentNode.parentNode.style.display = '';
1218       } else {
1219         lineWidth.parentNode.parentNode.style.display = 'none';
1220       }
1221     }
1222 
1223     // show the line tab, if configured
1224     if (tabConfig.lineTab && 'line' in tabPanel.tabs) {
1225       tabAnchor = lineTab.button.firstChild;
1226       tabAnchor.title = tabConfig.lineTabLabel || lang.tabs.main[ev.id];
1227       tabAnchor.replaceChild(doc.createTextNode(tabAnchor.title), 
1228           tabAnchor.firstChild);
1229 
1230       if (ev.id !== 'line') {
1231         lineTab.container.className = lineTab.container.className.
1232             replace(' ' + _self.classPrefix + 'main_line', ' ' + _self.classPrefix 
1233                 + 'main_line_' + ev.id);
1234       }
1235 
1236       tabPanel.tabShow('line');
1237     }
1238 
1239     // show the tab for the current tool, if there's one.
1240     if (ev.id in tabPanel.tabs) {
1241       tabPanel.tabShow(ev.id);
1242     }
1243   };
1244 
1245   /**
1246    * The <code>toolRegister</code> application event handler. This method adds 
1247    * the new tool into the GUI.
1248    *
1249    * @private
1250    *
1251    * @param {pwlib.appEvent.toolRegister} ev The application event object.
1252    *
1253    * @see PaintWeb#toolRegister the method which allows you to register new 
1254    * tools.
1255    */
1256   this.toolRegister = function (ev) {
1257     var attr = null, elem = null, anchor = null;
1258 
1259     if (ev.id in _self.tools) {
1260       elem = _self.tools[ev.id];
1261       attr = elem.getAttribute('data-pwTool');
1262       if (attr && attr !== ev.id) {
1263         attr = null;
1264         elem = null;
1265         delete _self.tools[ev.id];
1266       }
1267     }
1268 
1269     // Create a new element if there's none already associated to the tool ID.
1270     if (!elem) {
1271       elem = doc.createElement('li');
1272     }
1273 
1274     if (!attr) {
1275       elem.setAttribute('data-pwTool', ev.id);
1276     }
1277 
1278     elem.className += ' ' + _self.classPrefix + 'tool_' + ev.id;
1279 
1280     // Append an anchor element which holds the locale string.
1281     anchor = doc.createElement('a');
1282     anchor.title = lang.tools[ev.id];
1283     anchor.href = '#';
1284     anchor.appendChild(doc.createTextNode(anchor.title));
1285 
1286     if (elem.firstChild) {
1287       elem.replaceChild(anchor, elem.firstChild);
1288     } else {
1289       elem.appendChild(anchor);
1290     }
1291 
1292     anchor.addEventListener('click',     _self.toolClick,      false);
1293     anchor.addEventListener('mouseover', _self.item_mouseover, false);
1294     anchor.addEventListener('mouseout',  _self.item_mouseout,  false);
1295 
1296     if (!(ev.id in _self.tools)) {
1297       _self.tools[ev.id] = elem;
1298       _self.elems.tools.appendChild(elem);
1299     }
1300 
1301     // Disable the text tool icon if the Canvas Text API is not supported.
1302     if (ev.id === 'text' && !app.layer.context.fillText && 
1303         !app.layer.context.mozPathText && elem) {
1304       elem.className += ' ' + _self.classPrefix + 'disabled';
1305       anchor.title = lang.tools.textUnsupported;
1306 
1307       anchor.removeEventListener('click', _self.toolClick, false);
1308       anchor.addEventListener('click', function (ev) {
1309         ev.preventDefault();
1310       }, false);
1311     }
1312   };
1313 
1314   /**
1315    * The <code>toolUnregister</code> application event handler. This method the 
1316    * tool element from the GUI.
1317    *
1318    * @param {pwlib.appEvent.toolUnregister} ev The application event object.
1319    *
1320    * @see PaintWeb#toolUnregister the method which allows you to unregister 
1321    * tools.
1322    */
1323   this.toolUnregister = function (ev) {
1324     if (ev.id in _self.tools) {
1325       _self.elems.tools.removeChild(_self.tools[ev.id]);
1326       delete _self.tools[ev.id];
1327     } else {
1328       return;
1329     }
1330   };
1331 
1332   /**
1333    * The <code>click</code> event handler for the command DOM elements.
1334    *
1335    * @private
1336    *
1337    * @param {Event} ev The DOM Event object.
1338    *
1339    * @see PaintWeb#commandRegister to register a new command.
1340    */
1341   this.commandClick = function (ev) {
1342     var cmd = this.parentNode.getAttribute('data-pwCommand');
1343     if (cmd && cmd in app.commands) {
1344       app.commands[cmd].call(this, ev);
1345     }
1346     ev.preventDefault();
1347     this.focus();
1348   };
1349 
1350   /**
1351    * The <code>commandRegister</code> application event handler. GUI elements 
1352    * associated to commands are updated to ensure proper user interaction.
1353    *
1354    * @private
1355    *
1356    * @param {pwlib.appEvent.commandRegister} ev The application event object.
1357    *
1358    * @see PaintWeb#commandRegister the method which allows you to register new 
1359    * commands.
1360    */
1361   this.commandRegister = function (ev) {
1362     var elem   = _self.commands[ev.id],
1363         anchor = null;
1364     if (!elem) {
1365       return;
1366     }
1367 
1368     elem.className += ' ' + _self.classPrefix + 'cmd_' + ev.id;
1369 
1370     anchor = doc.createElement('a');
1371     anchor.title = lang.commands[ev.id];
1372     anchor.href = '#';
1373     anchor.appendChild(doc.createTextNode(anchor.title));
1374 
1375     // Remove the text content and append the locale string associated to 
1376     // current command inside an anchor element (for better keyboard 
1377     // accessibility).
1378     if (elem.firstChild) {
1379       elem.removeChild(elem.firstChild);
1380     }
1381     elem.appendChild(anchor);
1382 
1383     anchor.addEventListener('click',     _self.commandClick,   false);
1384     anchor.addEventListener('mouseover', _self.item_mouseover, false);
1385     anchor.addEventListener('mouseout',  _self.item_mouseout,  false);
1386   };
1387 
1388   /**
1389    * The <code>commandUnregister</code> application event handler. This method 
1390    * simply removes all the user interactivity from the GUI element associated 
1391    * to the command being unregistered.
1392    *
1393    * @private
1394    *
1395    * @param {pwlib.appEvent.commandUnregister} ev The application event object.
1396    *
1397    * @see PaintWeb#commandUnregister the method which allows you to unregister 
1398    * commands.
1399    */
1400   this.commandUnregister = function (ev) {
1401     var elem   = _self.commands[ev.id],
1402         anchor = null;
1403     if (!elem) {
1404       return;
1405     }
1406 
1407     elem.className = elem.className.replace(' ' + _self.classPrefix + 'cmd_' 
1408         + ev.id, '');
1409 
1410     anchor = elem.firstChild;
1411     anchor.removeEventListener('click',     this.commands[ev.id], false);
1412     anchor.removeEventListener('mouseover', _self.item_mouseover, false);
1413     anchor.removeEventListener('mouseout',  _self.item_mouseout,  false);
1414 
1415     elem.removeChild(anchor);
1416   };
1417 
1418   /**
1419    * The <code>historyUpdate</code> application event handler. GUI elements 
1420    * associated to the <code>historyUndo</code> and to the 
1421    * <code>historyRedo</code> commands are updated such that they are either 
1422    * enabled or disabled, depending on the current history position.
1423    *
1424    * @private
1425    *
1426    * @param {pwlib.appEvent.historyUpdate} ev The application event object.
1427    *
1428    * @see PaintWeb#historyGoto the method which allows you to go to different 
1429    * history states.
1430    */
1431   this.historyUpdate = function (ev) {
1432     var undoElem  = _self.commands.historyUndo,
1433         undoState = false,
1434         redoElem  = _self.commands.historyRedo,
1435         redoState = false,
1436         className = ' ' + _self.classPrefix + 'disabled',
1437         undoElemState = undoElem.className.indexOf(className) === -1,
1438         redoElemState = redoElem.className.indexOf(className) === -1;
1439 
1440     if (ev.currentPos > 1) {
1441       undoState = true;
1442     }
1443     if (ev.currentPos < ev.states) {
1444       redoState = true;
1445     }
1446 
1447     if (undoElemState !== undoState) {
1448       if (undoState) {
1449         undoElem.className = undoElem.className.replace(className, '');
1450       } else {
1451         undoElem.className += className;
1452       }
1453     }
1454 
1455     if (redoElemState !== redoState) {
1456       if (redoState) {
1457         redoElem.className = redoElem.className.replace(className, '');
1458       } else {
1459         redoElem.className += className;
1460       }
1461     }
1462   };
1463 
1464   /**
1465    * The <code>imageSizeChange</code> application event handler. The GUI element 
1466    * which displays the image dimensions is updated to display the new image 
1467    * size.
1468    *
1469    * <p>Image size refers strictly to the dimensions of the image being edited 
1470    * by the user, that's width and height.
1471    *
1472    * @private
1473    * @param {pwlib.appEvent.imageSizeChange} ev The application event object.
1474    */
1475   this.imageSizeChange = function (ev) {
1476     var imageSize  = _self.elems.imageSize;
1477     if (imageSize) {
1478       imageSize.replaceChild(doc.createTextNode(ev.width + 'x' + ev.height), 
1479           imageSize.firstChild);
1480     }
1481   };
1482 
1483   /**
1484    * The <code>canvasSizeChange</code> application event handler. The Canvas 
1485    * container element dimensions are updated to the new values, and the image 
1486    * resize handle is positioned accordingly.
1487    *
1488    * <p>Canvas size refers strictly to the dimensions of the Canvas elements in 
1489    * the browser, changed with CSS style properties, width and height. Scaling 
1490    * of the Canvas elements is applied when the user zooms the image or when the 
1491    * browser changes the render DPI / zoom.
1492    *
1493    * @private
1494    * @param {pwlib.appEvent.canvasSizeChange} ev The application event object.
1495    */
1496   this.canvasSizeChange = function (ev) {
1497     var canvasContainer = _self.elems.canvasContainer,
1498         canvasResizer   = _self.canvasResizer,
1499         className       = ' ' + _self.classPrefix + 'disabled',
1500         resizeHandle    = canvasResizer.resizeHandle;
1501 
1502     // Update the Canvas container to be the same size as the Canvas elements.
1503     canvasContainer.style.width  = ev.width  + 'px';
1504     canvasContainer.style.height = ev.height + 'px';
1505 
1506     resizeHandle.style.top  = ev.height + 'px';
1507     resizeHandle.style.left = ev.width  + 'px';
1508   };
1509 
1510   /**
1511    * The <code>imageZoom</code> application event handler. The GUI input element 
1512    * which displays the image zoom level is updated to display the new value.
1513    *
1514    * @private
1515    * @param {pwlib.appEvent.imageZoom} ev The application event object.
1516    */
1517   this.imageZoom = function (ev) {
1518     var elem  = _self.inputs.imageZoom,
1519         val   = MathRound(ev.zoom * 100);
1520     if (elem && elem.value != val) {
1521       elem.value = val;
1522     }
1523   };
1524 
1525   /**
1526    * The <code>configChange</code> application event handler. This method 
1527    * ensures the GUI input elements stay up-to-date when some PaintWeb 
1528    * configuration is modified.
1529    *
1530    * @private
1531    * @param {pwlib.appEvent.configChange} ev The application event object.
1532    */
1533   this.configChangeHandler = function (ev) {
1534     var cfg = '', input;
1535     if (ev.group) {
1536       cfg = ev.group.replace('.', '_') + '_';
1537     }
1538     cfg += ev.config;
1539     input = _self.inputs[cfg];
1540 
1541     // Handle changes for color inputs.
1542     if (!input && (input = _self.colorInputs[cfg])) {
1543       var color = ev.value.replace(/\s+/g, '').
1544                     replace(/^rgba\(/, '').replace(/\)$/, '');
1545 
1546       color = color.split(',');
1547       input.updateColor({
1548         red:   color[0] / 255,
1549         green: color[1] / 255,
1550         blue:  color[2] / 255,
1551         alpha: color[3]
1552       });
1553 
1554       return;
1555     }
1556 
1557     if (!input) {
1558       return;
1559     }
1560 
1561     var tag = input.tagName.toLowerCase(),
1562         isInput = tag === 'select' || tag === 'input' || tag === 'textarea';
1563 
1564     if (isInput) {
1565       if (input.type === 'checkbox' && input.checked !== ev.value) {
1566         input.checked = ev.value;
1567       }
1568       if (input.type !== 'checkbox' && input.value !== ev.value) {
1569         input.value = ev.value;
1570       }
1571 
1572       return;
1573     }
1574 
1575     var classActive = ' ' + _self.className + 'configActive';
1576 
1577     if (input.hasAttribute('data-pwConfigToggle')) {
1578       var inputActive = input.className.indexOf(classActive) !== -1;
1579 
1580       if (ev.value && !inputActive) {
1581         input.className += classActive;
1582       } else if (!ev.value && inputActive) {
1583         input.className = input.className.replace(classActive, '');
1584       }
1585     }
1586 
1587     var classActive = ' ' + _self.className + 'configActive',
1588         prevValElem = _self.inputValues[cfg + '_' + ev.previousValue],
1589         valElem = _self.inputValues[cfg + '_' + ev.value];
1590 
1591     if (prevValElem && prevValElem.className.indexOf(classActive) !== -1) {
1592       prevValElem.className = prevValElem.className.replace(classActive, '');
1593     }
1594 
1595     if (valElem && valElem.className.indexOf(classActive) === -1) {
1596       valElem.className += classActive;
1597     }
1598   };
1599 
1600   /**
1601    * The <code>click</code> event handler for DOM elements associated to 
1602    * PaintWeb configuration values. These elements rely on parent elements which 
1603    * are associated to configuration properties.
1604    *
1605    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
1606    *
1607    * @private
1608    * @param {Event} ev The DOM Event object.
1609    */
1610   this.configValueClick = function (ev) {
1611     var pNode = this.parentNode,
1612         input = pNode._pwConfigParent,
1613         val = pNode.getAttribute('data-pwConfigValue');
1614 
1615     if (!input || !input._pwConfigProperty) {
1616       return;
1617     }
1618 
1619     ev.preventDefault();
1620 
1621     var className = ' ' + _self.classPrefix + 'configActive',
1622         groupRef = input._pwConfigGroupRef,
1623         group = input._pwConfigGroup,
1624         prop = input._pwConfigProperty,
1625         prevVal = groupRef[prop],
1626         prevValElem = _self.inputValues[group.replace('.', '_') + '_' + prop 
1627           + '_' + prevVal];
1628 
1629     if (prevVal == val) {
1630       return;
1631     }
1632 
1633     if (prevValElem && prevValElem.className.indexOf(className) !== -1) {
1634       prevValElem.className = prevValElem.className.replace(className, '');
1635     }
1636 
1637     groupRef[prop] = val;
1638 
1639     if (pNode.className.indexOf(className) === -1) {
1640       pNode.className += className;
1641     }
1642 
1643     app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group, 
1644           groupRef));
1645   };
1646 
1647   /**
1648    * The <code>change</code> event handler for input elements associated to 
1649    * PaintWeb configuration properties.
1650    *
1651    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
1652    *
1653    * @private
1654    */
1655   this.configInputChange = function () {
1656     if (!this._pwConfigProperty) {
1657       return;
1658     }
1659 
1660     var val = this.type === 'checkbox' ? this.checked : this.value,
1661         groupRef = this._pwConfigGroupRef,
1662         group = this._pwConfigGroup,
1663         prop = this._pwConfigProperty,
1664         prevVal = groupRef[prop];
1665 
1666     if (this.getAttribute('type') === 'number') {
1667       val = parseInt(val);
1668       if (val != this.value) {
1669         this.value = val;
1670       }
1671     }
1672 
1673     if (val == prevVal) {
1674       return;
1675     }
1676 
1677     groupRef[prop] = val;
1678 
1679     app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group, 
1680           groupRef));
1681   };
1682 
1683   /**
1684    * The <code>click</code> event handler for DOM elements associated to boolean 
1685    * configuration properties. These elements only toggle the true/false value 
1686    * of the configuration property.
1687    *
1688    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event.
1689    *
1690    * @private
1691    * @param {Event} ev The DOM Event object.
1692    */
1693   this.configToggleClick = function (ev) {
1694     var className = ' ' + _self.classPrefix + 'configActive',
1695         pNode = this.parentNode,
1696         groupRef = pNode._pwConfigGroupRef,
1697         group = pNode._pwConfigGroup,
1698         prop = pNode._pwConfigProperty,
1699         elemActive = pNode.className.indexOf(className) !== -1;
1700 
1701     ev.preventDefault();
1702 
1703     groupRef[prop] = !groupRef[prop];
1704 
1705     if (groupRef[prop] && !elemActive) {
1706       pNode.className += className;
1707     } else if (!groupRef[prop] && elemActive) {
1708       pNode.className = pNode.className.replace(className, '');
1709     }
1710 
1711     app.events.dispatch(new appEvent.configChange(groupRef[prop], 
1712           !groupRef[prop], prop, group, groupRef));
1713   };
1714 
1715   /**
1716    * The <code>shadowAllow</code> application event handler. This method 
1717    * shows/hide the shadow tab when shadows are allowed/disallowed.
1718    *
1719    * @private
1720    * @param {pwlib.appEvent.shadowAllow} ev The application event object.
1721    */
1722   this.shadowAllow = function (ev) {
1723     if ('shadow' in _self.tabPanels.main.tabs) {
1724       if (ev.allowed) {
1725         _self.tabPanels.main.tabShow('shadow');
1726       } else {
1727         _self.tabPanels.main.tabHide('shadow');
1728       }
1729     }
1730   };
1731 
1732   /**
1733    * The <code>clipboardUpdate</code> application event handler. The GUI element 
1734    * associated to the <code>clipboardPaste</code> command is updated to be 
1735    * disabled/enabled depending on the event.
1736    *
1737    * @private
1738    * @param {pwlib.appEvent.clipboardUpdate} ev The application event object.
1739    */
1740   this.clipboardUpdate = function (ev) {
1741     var classDisabled = ' ' + _self.classPrefix + 'disabled',
1742         elem, elemEnabled,
1743         elems = [_self.commands.clipboardPaste, 
1744         _self.elems.selTab_clipboardPaste];
1745 
1746     for (var i = 0, n = elems.length; i < n; i++) {
1747       elem = elems[i];
1748       if (!elem) {
1749         continue;
1750       }
1751 
1752       elemEnabled = elem.className.indexOf(classDisabled) === -1;
1753 
1754       if (!ev.data && elemEnabled) {
1755         elem.className += classDisabled;
1756       } else if (ev.data && !elemEnabled) {
1757         elem.className = elem.className.replace(classDisabled, '');
1758       }
1759     }
1760   };
1761 
1762   /**
1763    * The <code>selectionChange</code> application event handler. The GUI 
1764    * elements associated to the <code>selectionCut</code> and 
1765    * <code>selectionCopy</code> commands are updated to be disabled/enabled 
1766    * depending on the event.
1767    *
1768    * @private
1769    * @param {pwlib.appEvent.selectionChange} ev The application event object.
1770    */
1771   this.selectionChange = function (ev) {
1772     var classDisabled  = ' ' + _self.classPrefix + 'disabled',
1773         elem, elemEnabled,
1774         elems = [_self.commands.selectionCut, _self.commands.selectionCopy, 
1775         _self.elems.selTab_selectionCut, _self.elems.selTab_selectionCopy, 
1776         _self.commands.selectionDelete, _self.commands.selectionFill, 
1777         _self.commands.selectionCrop];
1778 
1779     for (var i = 0, n = elems.length; i < n; i++) {
1780       elem = elems[i];
1781       if (!elem) {
1782         continue;
1783       }
1784 
1785       elemEnabled = elem.className.indexOf(classDisabled) === -1;
1786 
1787       if (ev.state === ev.STATE_NONE && elemEnabled) {
1788         elem.className += classDisabled;
1789       } else if (ev.state === ev.STATE_SELECTED && !elemEnabled) {
1790         elem.className = elem.className.replace(classDisabled, '');
1791       }
1792     }
1793   };
1794 
1795   /**
1796    * Show the graphical user interface.
1797    *
1798    * <p>This method dispatches the {@link pwlib.appEvent.guiShow} application 
1799    * event.
1800    */
1801   this.show = function () {
1802     var placeholder = config.guiPlaceholder,
1803         className   = this.classPrefix + 'placeholder',
1804         re          = new RegExp('\\b' + className);
1805 
1806     if (!re.test(placeholder.className)) {
1807       placeholder.className += ' ' + className;
1808     }
1809 
1810     placeholder.focus();
1811 
1812     app.events.dispatch(new appEvent.guiShow());
1813   };
1814 
1815   /**
1816    * Hide the graphical user interface.
1817    *
1818    * <p>This method dispatches the {@link pwlib.appEvent.guiHide} application 
1819    * event.
1820    */
1821   this.hide = function () {
1822     var placeholder = config.guiPlaceholder,
1823         re = new RegExp('\\b' + this.classPrefix + 'placeholder', 'g');
1824 
1825     placeholder.className = placeholder.className.replace(re, '');
1826 
1827     app.events.dispatch(new appEvent.guiHide());
1828   };
1829 
1830   /**
1831    * The application destroy event handler. This method is invoked by the main 
1832    * PaintWeb application when the instance is destroyed, for the purpose of 
1833    * cleaning-up the GUI-related things from the document add by the current 
1834    * instance.
1835    *
1836    * @private
1837    */
1838   this.destroy = function () {
1839     var placeholder = config.guiPlaceholder;
1840 
1841     while(placeholder.hasChildNodes()) {
1842       placeholder.removeChild(placeholder.firstChild);
1843     }
1844   };
1845 
1846   /**
1847    * Resize the PaintWeb graphical user interface.
1848    *
1849    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event for 
1850    * the "viewportWidth" and "viewportHeight" configuration properties. Both 
1851    * properties are updated to hold the new values you give.
1852    *
1853    * <p>Once the GUI is resized, the {@link pwlib.appEvent.viewportSizeChange} 
1854    * event is also dispatched.
1855    *
1856    * @param {String} width The new width you want. Make sure the value is a CSS 
1857    * length, like "50%", "450px" or "30em".
1858    *
1859    * @param {String} height The new height you want.
1860    */
1861   this.resizeTo = function (width, height) {
1862     if (!width || !height) {
1863       return;
1864     }
1865 
1866     var width_old  = config.viewportWidth,
1867         height_old = config.viewportHeight;
1868 
1869     config.viewportWidth  = width;
1870     config.viewportHeight = height;
1871 
1872     app.events.dispatch(new appEvent.configChange(width, width_old, 
1873           'viewportWidth', '', config));
1874 
1875     app.events.dispatch(new appEvent.configChange(height, height_old, 
1876           'viewportHeight', '', config));
1877 
1878     config.guiPlaceholder.style.width = config.viewportWidth;
1879     this.elems.viewport.style.height  = config.viewportHeight;
1880 
1881     app.events.dispatch(new appEvent.viewportSizeChange(width, height));
1882   };
1883 
1884   /**
1885    * The state change event handler for the Hand tool. This function 
1886    * enables/disables the Hand tool by checking if the current image fits into 
1887    * the viewport or not.
1888    *
1889    * <p>This function is invoked when one of the following application events is  
1890    * dispatched: <code>viewportSizeChange</code>, <code>canvasSizeChange</code> 
1891    * or <code>appInit</code.
1892    *
1893    * @private
1894    * @param 
1895    * {pwlib.appEvent.viewportSizeChange|pwlib.appEvent.canvasSizeChange|pwlib.appEvent.appInit} 
1896    * [ev] The application event object.
1897    */
1898   this.toolHandStateChange = function (ev) {
1899     var cwidth    = 0,
1900         cheight   = 0,
1901         className = ' ' + _self.classPrefix + 'disabled',
1902         hand      = _self.tools.hand,
1903         viewport  = _self.elems.viewport;
1904 
1905     if (!hand) {
1906       return;
1907     }
1908 
1909     if (ev.type === 'canvasSizeChange') {
1910       cwidth  = ev.width;
1911       cheight = ev.height;
1912     } else {
1913       var containerStyle = _self.elems.canvasContainer.style;
1914       cwidth  = parseInt(containerStyle.width);
1915       cheight = parseInt(containerStyle.height);
1916     }
1917 
1918     // FIXME: it should be noted that when PaintWeb loads, the entire GUI is 
1919     // hidden, and win.getComputedStyle() style tells that the viewport 
1920     // width/height is 0.
1921     cs = win.getComputedStyle(viewport, null);
1922 
1923     var vwidth     = parseInt(cs.width),
1924         vheight    = parseInt(cs.height),
1925         enableHand = false,
1926         handState  = hand.className.indexOf(className) === -1;
1927 
1928     if (vheight < cheight || vwidth < cwidth) {
1929       enableHand = true;
1930     }
1931 
1932     if (enableHand && !handState) {
1933       hand.className = hand.className.replace(className, '');
1934     } else if (!enableHand && handState) {
1935       hand.className += className;
1936     }
1937 
1938     if (!enableHand && app.tool && app.tool._id === 'hand' && 'prevTool' in 
1939         app.tool) {
1940       app.toolActivate(app.tool.prevTool, ev);
1941     }
1942   };
1943 };
1944 
1945 /**
1946  * @class A floating panel GUI element.
1947  *
1948  * @private
1949  *
1950  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
1951  *
1952  * @param {Element} container Reference to the DOM element you want to transform 
1953  * into a floating panel.
1954  */
1955 pwlib.guiFloatingPanel = function (gui, container) {
1956   var _self          = this,
1957       appEvent       = pwlib.appEvent,
1958       cStyle         = container.style,
1959       doc            = gui.app.doc,
1960       guiPlaceholder = gui.app.config.guiPlaceholder,
1961       lang           = gui.app.lang,
1962       panels         = gui.floatingPanels,
1963       win            = gui.app.win,
1964       zIndex_step    = 200;
1965 
1966   // These hold the mouse starting location during the drag operation.
1967   var mx, my;
1968 
1969   // These hold the panel starting location during the drag operation.
1970   var ptop, pleft;
1971 
1972   /**
1973    * Panel state: hidden.
1974    * @constant
1975    */
1976   this.STATE_HIDDEN    = 0;
1977 
1978   /**
1979    * Panel state: visible.
1980    * @constant
1981    */
1982   this.STATE_VISIBLE   = 1;
1983 
1984   /**
1985    * Panel state: minimized.
1986    * @constant
1987    */
1988   this.STATE_MINIMIZED = 3;
1989 
1990   /**
1991    * Panel state: the user is dragging the floating panel.
1992    * @constant
1993    */
1994   this.STATE_DRAGGING  = 4;
1995 
1996   /**
1997    * Tells the state of the floating panel: hidden/minimized/visible or if it's 
1998    * being dragged.
1999    * @type Number
2000    */
2001   this.state = -1;
2002 
2003   /**
2004    * Floating panel ID. This is the ID used in the 
2005    * <var>data-pwFloatingPanel</var> element attribute.
2006    * @type String
2007    */
2008   this.id = null;
2009 
2010   /**
2011    * Reference to the floating panel element.
2012    * @type Element
2013    */
2014   this.container = container;
2015 
2016   /**
2017    * The viewport element. This element is the first parent element which has 
2018    * the style.overflow set to "auto" or "scroll".
2019    * @type Element
2020    */
2021   this.viewport = null;
2022 
2023   /**
2024    * Custom application events interface.
2025    * @type pwlib.appEvents
2026    */
2027   this.events = null;
2028 
2029   /**
2030    * The panel content element.
2031    * @type Element
2032    */
2033   this.content = null;
2034 
2035   // The initial viewport scroll position.
2036   var vScrollLeft = 0, vScrollTop = 0,
2037       btn_close = null, btn_minimize = null;
2038 
2039   /**
2040    * Initialize the floating panel.
2041    * @private
2042    */
2043   function init () {
2044     _self.events = new pwlib.appEvents(_self);
2045 
2046     _self.id = _self.container.getAttribute('data-pwFloatingPanel');
2047 
2048     var ttl = _self.container.getElementsByTagName('h1')[0],
2049         content = _self.container.getElementsByTagName('div')[0],
2050         cs = win.getComputedStyle(_self.container, null),
2051         zIndex = parseInt(cs.zIndex);
2052 
2053     cStyle.zIndex = cs.zIndex;
2054 
2055     if (zIndex > panels.zIndex_) {
2056       panels.zIndex_ = zIndex;
2057     }
2058 
2059     _self.container.className += ' ' + gui.classPrefix + 'floatingPanel ' +
2060       gui.classPrefix + 'floatingPanel_' + _self.id;
2061 
2062     // the content
2063     content.className += ' ' + gui.classPrefix + 'floatingPanel_content';
2064     _self.content = content;
2065 
2066     // setup the title element
2067     ttl.className += ' ' + gui.classPrefix + 'floatingPanel_title';
2068     ttl.replaceChild(doc.createTextNode(lang.floatingPanels[_self.id]), 
2069         ttl.firstChild);
2070 
2071     ttl.addEventListener('mousedown', ev_mousedown, false);
2072 
2073     // allow auto-hide for the panel
2074     if (_self.container.getAttribute('data-pwPanelHide') === 'true') {
2075       _self.hide();
2076     } else {
2077       _self.state = _self.STATE_VISIBLE;
2078     }
2079 
2080     // Find the viewport parent element.
2081     var pNode = _self.container.parentNode,
2082         found = null;
2083 
2084     while (!found && pNode) {
2085       if (pNode.nodeName.toLowerCase() === 'html') {
2086         found = pNode;
2087         break;
2088       }
2089 
2090       cs = win.getComputedStyle(pNode, null);
2091       if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) {
2092         found = pNode;
2093       } else {
2094         pNode = pNode.parentNode;
2095       }
2096     }
2097 
2098     _self.viewport = found;
2099 
2100     // add the panel minimize button.
2101     btn_minimize = doc.createElement('a');
2102     btn_minimize.href = '#';
2103     btn_minimize.title = lang.floatingPanelMinimize;
2104     btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize';
2105     btn_minimize.addEventListener('click', ev_minimize, false);
2106     btn_minimize.appendChild(doc.createTextNode(btn_minimize.title));
2107 
2108     _self.container.insertBefore(btn_minimize, content);
2109 
2110     // add the panel close button.
2111     btn_close = doc.createElement('a');
2112     btn_close.href = '#';
2113     btn_close.title = lang.floatingPanelClose;
2114     btn_close.className = gui.classPrefix + 'floatingPanel_close';
2115     btn_close.addEventListener('click', ev_close, false);
2116     btn_close.appendChild(doc.createTextNode(btn_close.title));
2117 
2118     _self.container.insertBefore(btn_close, content);
2119 
2120     // setup the panel resize handle.
2121     if (_self.container.getAttribute('data-pwPanelResizable') === 'true') {
2122       var resizeHandle = doc.createElement('div');
2123       resizeHandle.className = gui.classPrefix + 'floatingPanel_resizer';
2124       _self.container.appendChild(resizeHandle);
2125       _self.resizer = new pwlib.guiResizer(gui, resizeHandle, _self.container);
2126     }
2127   };
2128 
2129   /**
2130    * The <code>click</code> event handler for the panel Minimize button element.
2131    *
2132    * <p>This method dispatches the {@link 
2133    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2134    *
2135    * @private
2136    * @param {Event} ev The DOM Event object.
2137    */
2138   function ev_minimize (ev) {
2139     ev.preventDefault();
2140     this.focus();
2141 
2142     var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized';
2143 
2144     if (_self.state === _self.STATE_MINIMIZED) {
2145       _self.state = _self.STATE_VISIBLE;
2146 
2147       this.title = lang.floatingPanelMinimize;
2148       this.className = gui.classPrefix + 'floatingPanel_minimize';
2149       this.replaceChild(doc.createTextNode(this.title), this.firstChild);
2150 
2151       if (_self.container.className.indexOf(classMinimized) !== -1) {
2152         _self.container.className 
2153           = _self.container.className.replace(classMinimized, '');
2154       }
2155 
2156     } else if (_self.state === _self.STATE_VISIBLE) {
2157       _self.state = _self.STATE_MINIMIZED;
2158 
2159       this.title = lang.floatingPanelRestore;
2160       this.className = gui.classPrefix + 'floatingPanel_restore';
2161       this.replaceChild(doc.createTextNode(this.title), this.firstChild);
2162 
2163       if (_self.container.className.indexOf(classMinimized) === -1) {
2164         _self.container.className += classMinimized;
2165       }
2166     }
2167 
2168     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
2169 
2170     _self.bringOnTop();
2171   };
2172 
2173   /**
2174    * The <code>click</code> event handler for the panel Close button element.  
2175    * This hides the floating panel.
2176    *
2177    * <p>This method dispatches the {@link 
2178    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2179    *
2180    * @private
2181    * @param {Event} ev The DOM Event object.
2182    */
2183   function ev_close (ev) {
2184     ev.preventDefault();
2185     _self.hide();
2186     guiPlaceholder.focus();
2187   };
2188 
2189   /**
2190    * The <code>mousedown</code> event handler. This is invoked when you start 
2191    * dragging the floating panel.
2192    *
2193    * <p>This method dispatches the {@link 
2194    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2195    *
2196    * @private
2197    * @param {Event} ev The DOM Event object.
2198    */
2199   function ev_mousedown (ev) {
2200     _self.state = _self.STATE_DRAGGING;
2201 
2202     mx = ev.clientX;
2203     my = ev.clientY;
2204 
2205     var cs = win.getComputedStyle(_self.container, null);
2206 
2207     ptop  = parseInt(cs.top);
2208     pleft = parseInt(cs.left);
2209 
2210     if (_self.viewport) {
2211       vScrollLeft = _self.viewport.scrollLeft;
2212       vScrollTop  = _self.viewport.scrollTop;
2213     }
2214 
2215     _self.bringOnTop();
2216 
2217     doc.addEventListener('mousemove', ev_mousemove, false);
2218     doc.addEventListener('mouseup',   ev_mouseup,   false);
2219 
2220     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
2221 
2222     if (ev.preventDefault) {
2223       ev.preventDefault();
2224     }
2225   };
2226 
2227   /**
2228    * The <code>mousemove</code> event handler. This performs the actual move of 
2229    * the floating panel.
2230    *
2231    * @private
2232    * @param {Event} ev The DOM Event object.
2233    */
2234   function ev_mousemove (ev) {
2235     var x = pleft + ev.clientX - mx,
2236         y = ptop  + ev.clientY - my;
2237 
2238     if (_self.viewport) {
2239       if (_self.viewport.scrollLeft !== vScrollLeft) {
2240         x += _self.viewport.scrollLeft - vScrollLeft;
2241       }
2242       if (_self.viewport.scrollTop !== vScrollTop) {
2243         y += _self.viewport.scrollTop - vScrollTop;
2244       }
2245     }
2246 
2247     cStyle.left = x + 'px';
2248     cStyle.top  = y + 'px';
2249   };
2250 
2251   /**
2252    * The <code>mouseup</code> event handler. This ends the panel drag operation.
2253    *
2254    * <p>This method dispatches the {@link 
2255    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2256    *
2257    * @private
2258    * @param {Event} ev The DOM Event object.
2259    */
2260   function ev_mouseup (ev) {
2261     if (_self.container.className.indexOf(' ' + gui.classPrefix 
2262           + 'floatingPanel_minimized') !== -1) {
2263       _self.state = _self.STATE_MINIMIZED;
2264     } else {
2265       _self.state = _self.STATE_VISIBLE;
2266     }
2267 
2268     doc.removeEventListener('mousemove', ev_mousemove, false);
2269     doc.removeEventListener('mouseup',   ev_mouseup,   false);
2270 
2271     guiPlaceholder.focus();
2272     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
2273   };
2274 
2275   /**
2276    * Bring the panel to the top. This method makes sure the current floating 
2277    * panel is visible.
2278    */
2279   this.bringOnTop = function () {
2280     panels.zIndex_ += zIndex_step;
2281     cStyle.zIndex = panels.zIndex_;
2282   };
2283 
2284   /**
2285    * Hide the panel.
2286    *
2287    * <p>This method dispatches the {@link 
2288    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2289    */
2290   this.hide = function () {
2291     cStyle.display = 'none';
2292     _self.state = _self.STATE_HIDDEN;
2293     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
2294   };
2295 
2296   /**
2297    * Show the panel.
2298    *
2299    * <p>This method dispatches the {@link 
2300    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2301    */
2302   this.show = function () {
2303     if (_self.state === _self.STATE_VISIBLE) {
2304       return;
2305     }
2306 
2307     cStyle.display = 'block';
2308     _self.state = _self.STATE_VISIBLE;
2309 
2310     var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized';
2311 
2312     if (_self.container.className.indexOf(classMinimized) !== -1) {
2313       _self.container.className 
2314         = _self.container.className.replace(classMinimized, '');
2315 
2316       btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize';
2317       btn_minimize.title = lang.floatingPanelMinimize;
2318       btn_minimize.replaceChild(doc.createTextNode(btn_minimize.title), 
2319           btn_minimize.firstChild);
2320     }
2321 
2322     _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state));
2323 
2324     _self.bringOnTop();
2325   };
2326 
2327   /**
2328    * Toggle the panel visibility.
2329    *
2330    * <p>This method dispatches the {@link 
2331    * pwlib.appEvent.guiFloatingPanelStateChange} application event.
2332    */
2333   this.toggle = function () {
2334     if (_self.state === _self.STATE_VISIBLE || _self.state === 
2335         _self.STATE_MINIMIZED) {
2336       _self.hide();
2337     } else {
2338       _self.show();
2339     }
2340   };
2341 
2342   init();
2343 };
2344 
2345 /**
2346  * @class The state change event for the floating panel. This event is fired 
2347  * when the floating panel changes its state. This event is not cancelable.
2348  *
2349  * @augments pwlib.appEvent
2350  *
2351  * @param {Number} state The floating panel state.
2352  */
2353 pwlib.appEvent.guiFloatingPanelStateChange = function (state) {
2354   /**
2355    * Panel state: hidden.
2356    * @constant
2357    */
2358   this.STATE_HIDDEN    = 0;
2359 
2360   /**
2361    * Panel state: visible.
2362    * @constant
2363    */
2364   this.STATE_VISIBLE   = 1;
2365 
2366   /**
2367    * Panel state: minimized.
2368    * @constant
2369    */
2370   this.STATE_MINIMIZED = 3;
2371 
2372   /**
2373    * Panel state: the user is dragging the floating panel.
2374    * @constant
2375    */
2376   this.STATE_DRAGGING  = 4;
2377 
2378   /**
2379    * The current floating panel state.
2380    * @type Number
2381    */
2382   this.state = state;
2383 
2384   pwlib.appEvent.call(this, 'guiFloatingPanelStateChange');
2385 };
2386 
2387 /**
2388  * @class Resize handler.
2389  *
2390  * @private
2391  *
2392  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
2393  *
2394  * @param {Element} resizeHandle Reference to the resize handle DOM element.  
2395  * This is the element users will be able to drag to achieve the resize effect 
2396  * on the <var>container</var> element.
2397  *
2398  * @param {Element} container Reference to the container DOM element. This is 
2399  * the element users will be able to resize using the <var>resizeHandle</var> 
2400  * element.
2401  */
2402 pwlib.guiResizer = function (gui, resizeHandle, container) {
2403   var _self              = this,
2404       cStyle             = container.style,
2405       doc                = gui.app.doc,
2406       guiResizeEnd       = pwlib.appEvent.guiResizeEnd,
2407       guiResizeMouseMove = pwlib.appEvent.guiResizeMouseMove,
2408       guiResizeStart     = pwlib.appEvent.guiResizeStart,
2409       win                = gui.app.win;
2410 
2411   /**
2412    * Custom application events interface.
2413    * @type pwlib.appEvents
2414    */
2415   this.events = null;
2416 
2417   /**
2418    * The resize handle DOM element.
2419    * @type Element
2420    */
2421   this.resizeHandle = resizeHandle;
2422 
2423   /**
2424    * The container DOM element. This is the element that's resized by the user 
2425    * when he/she drags the resize handle.
2426    * @type Element
2427    */
2428   this.container = container;
2429 
2430   /**
2431    * The viewport element. This element is the first parent element which has 
2432    * the style.overflow set to "auto" or "scroll".
2433    * @type Element
2434    */
2435   this.viewport = null;
2436 
2437   /**
2438    * Tells if the GUI resizer should dispatch the {@link 
2439    * pwlib.appEvent.guiResizeMouseMove} application event when the user moves 
2440    * the mouse during the resize operation.
2441    *
2442    * @type Boolean
2443    * @default false
2444    */
2445   this.dispatchMouseMove = false;
2446 
2447   /**
2448    * Tells if the user resizing the container now.
2449    *
2450    * @type Boolean
2451    * @default false
2452    */
2453   this.resizing = false;
2454 
2455   // The initial position of the mouse.
2456   var mx = 0, my = 0;
2457 
2458   // The initial container dimensions.
2459   var cWidth = 0, cHeight = 0;
2460 
2461   // The initial viewport scroll position.
2462   var vScrollLeft = 0, vScrollTop = 0;
2463 
2464   /**
2465    * Initialize the resize functionality.
2466    * @private
2467    */
2468   function init () {
2469     _self.events = new pwlib.appEvents(_self);
2470     resizeHandle.addEventListener('mousedown', ev_mousedown, false);
2471 
2472     // Find the viewport parent element.
2473     var cs, pNode = _self.container.parentNode,
2474         found = null;
2475     while (!found && pNode) {
2476       if (pNode.nodeName.toLowerCase() === 'html') {
2477         found = pNode;
2478         break;
2479       }
2480 
2481       cs = win.getComputedStyle(pNode, null);
2482       if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) {
2483         found = pNode;
2484       } else {
2485         pNode = pNode.parentNode;
2486       }
2487     }
2488 
2489     _self.viewport = found;
2490   };
2491 
2492   /**
2493    * The <code>mousedown</code> event handler. This starts the resize operation.
2494    *
2495    * <p>This function dispatches the {@link pwlib.appEvent.guiResizeStart} 
2496    * event.
2497    *
2498    * @private
2499    * @param {Event} ev The DOM Event object.
2500    */
2501   function ev_mousedown (ev) {
2502     mx = ev.clientX;
2503     my = ev.clientY;
2504 
2505     var cs = win.getComputedStyle(_self.container, null);
2506     cWidth  = parseInt(cs.width);
2507     cHeight = parseInt(cs.height);
2508 
2509     var cancel = _self.events.dispatch(new guiResizeStart(mx, my, cWidth, 
2510           cHeight));
2511 
2512     if (cancel) {
2513       return;
2514     }
2515 
2516     if (_self.viewport) {
2517       vScrollLeft = _self.viewport.scrollLeft;
2518       vScrollTop  = _self.viewport.scrollTop;
2519     }
2520 
2521     _self.resizing = true;
2522     doc.addEventListener('mousemove', ev_mousemove, false);
2523     doc.addEventListener('mouseup',   ev_mouseup,   false);
2524 
2525     if (ev.preventDefault) {
2526       ev.preventDefault();
2527     }
2528 
2529     if (ev.stopPropagation) {
2530       ev.stopPropagation();
2531     }
2532   };
2533 
2534   /**
2535    * The <code>mousemove</code> event handler. This performs the actual resizing 
2536    * of the <var>container</var> element.
2537    *
2538    * @private
2539    * @param {Event} ev The DOM Event object.
2540    */
2541   function ev_mousemove (ev) {
2542     var w = cWidth  + ev.clientX - mx,
2543         h = cHeight + ev.clientY - my;
2544 
2545     if (_self.viewport) {
2546       if (_self.viewport.scrollLeft !== vScrollLeft) {
2547         w += _self.viewport.scrollLeft - vScrollLeft;
2548       }
2549       if (_self.viewport.scrollTop !== vScrollTop) {
2550         h += _self.viewport.scrollTop - vScrollTop;
2551       }
2552     }
2553 
2554     cStyle.width  = w + 'px';
2555     cStyle.height = h + 'px';
2556 
2557     if (_self.dispatchMouseMove) {
2558       _self.events.dispatch(new guiResizeMouseMove(ev.clientX, ev.clientY, w, 
2559             h));
2560     }
2561   };
2562 
2563   /**
2564    * The <code>mouseup</code> event handler. This ends the resize operation.
2565    *
2566    * <p>This function dispatches the {@link pwlib.appEvent.guiResizeEnd} event.
2567    *
2568    * @private
2569    * @param {Event} ev The DOM Event object.
2570    */
2571   function ev_mouseup (ev) {
2572     var cancel = _self.events.dispatch(new guiResizeEnd(ev.clientX, ev.clientY, 
2573           parseInt(cStyle.width), parseInt(cStyle.height)));
2574 
2575     if (cancel) {
2576       return;
2577     }
2578 
2579     _self.resizing = false;
2580     doc.removeEventListener('mousemove', ev_mousemove, false);
2581     doc.removeEventListener('mouseup',   ev_mouseup,   false);
2582   };
2583 
2584   init();
2585 };
2586 
2587 /**
2588  * @class The GUI element resize start event. This event is cancelable.
2589  *
2590  * @augments pwlib.appEvent
2591  *
2592  * @param {Number} x The mouse location on the x-axis.
2593  * @param {Number} y The mouse location on the y-axis.
2594  * @param {Number} width The element width.
2595  * @param {Number} height The element height.
2596  */
2597 pwlib.appEvent.guiResizeStart = function (x, y, width, height) {
2598   /**
2599    * The mouse location on the x-axis.
2600    * @type Number
2601    */
2602   this.x = x;
2603 
2604   /**
2605    * The mouse location on the y-axis.
2606    * @type Number
2607    */
2608   this.y = y;
2609 
2610   /**
2611    * The element width.
2612    * @type Number
2613    */
2614   this.width = width;
2615 
2616   /**
2617    * The element height.
2618    * @type Number
2619    */
2620   this.height = height;
2621 
2622   pwlib.appEvent.call(this, 'guiResizeStart', true);
2623 };
2624 
2625 /**
2626  * @class The GUI element resize end event. This event is cancelable.
2627  *
2628  * @augments pwlib.appEvent
2629  *
2630  * @param {Number} x The mouse location on the x-axis.
2631  * @param {Number} y The mouse location on the y-axis.
2632  * @param {Number} width The element width.
2633  * @param {Number} height The element height.
2634  */
2635 pwlib.appEvent.guiResizeEnd = function (x, y, width, height) {
2636   /**
2637    * The mouse location on the x-axis.
2638    * @type Number
2639    */
2640   this.x = x;
2641 
2642   /**
2643    * The mouse location on the y-axis.
2644    * @type Number
2645    */
2646   this.y = y;
2647 
2648   /**
2649    * The element width.
2650    * @type Number
2651    */
2652   this.width = width;
2653 
2654   /**
2655    * The element height.
2656    * @type Number
2657    */
2658   this.height = height;
2659 
2660   pwlib.appEvent.call(this, 'guiResizeEnd', true);
2661 };
2662 
2663 /**
2664  * @class The GUI element resize mouse move event. This event is not cancelable.
2665  *
2666  * @augments pwlib.appEvent
2667  *
2668  * @param {Number} x The mouse location on the x-axis.
2669  * @param {Number} y The mouse location on the y-axis.
2670  * @param {Number} width The element width.
2671  * @param {Number} height The element height.
2672  */
2673 pwlib.appEvent.guiResizeMouseMove = function (x, y, width, height) {
2674   /**
2675    * The mouse location on the x-axis.
2676    * @type Number
2677    */
2678   this.x = x;
2679 
2680   /**
2681    * The mouse location on the y-axis.
2682    * @type Number
2683    */
2684   this.y = y;
2685 
2686   /**
2687    * The element width.
2688    * @type Number
2689    */
2690   this.width = width;
2691 
2692   /**
2693    * The element height.
2694    * @type Number
2695    */
2696   this.height = height;
2697 
2698   pwlib.appEvent.call(this, 'guiResizeMouseMove');
2699 };
2700 
2701 /**
2702  * @class The tabbed panel GUI component.
2703  *
2704  * @private
2705  *
2706  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
2707  *
2708  * @param {Element} panel Reference to the panel DOM element.
2709  */
2710 pwlib.guiTabPanel = function (gui, panel) {
2711   var _self    = this,
2712       appEvent = pwlib.appEvent,
2713       doc      = gui.app.doc,
2714       lang     = gui.app.lang;
2715 
2716   /**
2717    * Custom application events interface.
2718    * @type pwlib.appEvents
2719    */
2720   this.events = null;
2721 
2722   /**
2723    * Panel ID. The ID is the same as the data-pwTabPanel attribute value of the 
2724    * panel DOM element .
2725    *
2726    * @type String.
2727    */
2728   this.id = null;
2729 
2730   /**
2731    * Holds references to the DOM element of each tab and tab button.
2732    * @type Object
2733    */
2734   this.tabs = {};
2735 
2736   /**
2737    * Reference to the tab buttons DOM element.
2738    * @type Element
2739    */
2740   this.tabButtons = null;
2741 
2742   /**
2743    * The panel container DOM element.
2744    * @type Element
2745    */
2746   this.container = panel;
2747 
2748   /**
2749    * Holds the ID of the currently active tab.
2750    * @type String
2751    */
2752   this.tabId = null;
2753 
2754   /**
2755    * Holds the ID of the previously active tab.
2756    *
2757    * @private
2758    * @type String
2759    */
2760   var prevTabId_ = null;
2761 
2762   /**
2763    * Initialize the toolbar functionality.
2764    * @private
2765    */
2766   function init () {
2767     _self.events = new pwlib.appEvents(_self);
2768     _self.id = _self.container.getAttribute('data-pwTabPanel');
2769 
2770     // Add two class names, the generic .paintweb_tabPanel and another class 
2771     // name specific to the current tab panel: .paintweb_tabPanel_id. 
2772     _self.container.className += ' ' + gui.classPrefix + 'tabPanel' 
2773       + ' ' + gui.classPrefix + 'tabPanel_' + _self.id;
2774 
2775     var tabButtons = doc.createElement('ul'),
2776         tabButton = null,
2777         tabDefault = _self.container.getAttribute('data-pwTabDefault') || null,
2778         childNodes = _self.container.childNodes,
2779         type = Node.ELEMENT_NODE,
2780         elem = null,
2781         tabId = null,
2782         anchor = null;
2783 
2784     tabButtons.className = gui.classPrefix + 'tabsList';
2785 
2786     // Find all the tabs in the current panel container element.
2787     for (var i = 0; elem = childNodes[i]; i++) {
2788       if (elem.nodeType !== type) {
2789         continue;
2790       }
2791 
2792       // A tab is any element with a given data-pwTab attribute.
2793       tabId = elem.getAttribute('data-pwTab');
2794       if (!tabId) {
2795         continue;
2796       }
2797 
2798       // two class names, the generic .paintweb_tab and the tab-specific class 
2799       // name .paintweb_tabPanelId_tabId.
2800       elem.className += ' ' + gui.classPrefix + 'tab ' + gui.classPrefix 
2801         + _self.id + '_' + tabId;
2802 
2803       tabButton = doc.createElement('li');
2804       tabButton._pwTab = tabId;
2805 
2806       anchor = doc.createElement('a');
2807       anchor.href = '#';
2808       anchor.addEventListener('click', ev_tabClick, false);
2809 
2810       if (_self.id in lang.tabs) {
2811         anchor.title = lang.tabs[_self.id][tabId + 'Title'] || 
2812           lang.tabs[_self.id][tabId];
2813         anchor.appendChild(doc.createTextNode(lang.tabs[_self.id][tabId]));
2814       }
2815 
2816       if ((tabDefault && tabId === tabDefault) ||
2817           (!tabDefault && !_self.tabId)) {
2818         _self.tabId = tabId;
2819         tabButton.className = gui.classPrefix + 'tabActive';
2820       } else {
2821         prevTabId_ = tabId;
2822         elem.style.display = 'none';
2823       }
2824 
2825       // automatically hide the tab
2826       if (elem.getAttribute('data-pwTabHide') === 'true') {
2827         tabButton.style.display = 'none';
2828       }
2829 
2830       _self.tabs[tabId] = {container: elem, button: tabButton};
2831 
2832       tabButton.appendChild(anchor);
2833       tabButtons.appendChild(tabButton);
2834     }
2835 
2836     _self.tabButtons = tabButtons;
2837     _self.container.appendChild(tabButtons);
2838   };
2839 
2840   /**
2841    * The <code>click</code> event handler for tab buttons. This function simply 
2842    * activates the tab the user clicked.
2843    *
2844    * @private
2845    * @param {Event} ev The DOM Event object.
2846    */
2847   function ev_tabClick (ev) {
2848     ev.preventDefault();
2849     _self.tabActivate(this.parentNode._pwTab);
2850   };
2851 
2852   /**
2853    * Activate a tab by ID.
2854    *
2855    * <p>This method dispatches the {@link pwlib.appEvent.guiTabActivate} event.
2856    *
2857    * @param {String} tabId The ID of the tab you want to activate.
2858    * @returns {Boolean} True if the tab has been activated successfully, or 
2859    * false if not.
2860    */
2861   this.tabActivate = function (tabId) {
2862     if (!tabId || !(tabId in this.tabs)) {
2863       return false;
2864     } else if (tabId === this.tabId) {
2865       return true;
2866     }
2867 
2868     var ev = new appEvent.guiTabActivate(tabId, this.tabId),
2869         cancel = this.events.dispatch(ev),
2870         elem = null,
2871         tabButton = null;
2872 
2873     if (cancel) {
2874       return false;
2875     }
2876 
2877     // Deactivate the currently active tab.
2878     if (this.tabId in this.tabs) {
2879       elem = this.tabs[this.tabId].container;
2880       elem.style.display = 'none';
2881       tabButton = this.tabs[this.tabId].button;
2882       tabButton.className = '';
2883       prevTabId_ = this.tabId;
2884     }
2885 
2886     // Activate the new tab.
2887     elem = this.tabs[tabId].container;
2888     elem.style.display = '';
2889     tabButton = this.tabs[tabId].button;
2890     tabButton.className = gui.classPrefix + 'tabActive';
2891     tabButton.style.display = ''; // make sure the tab is not hidden
2892     tabButton.firstChild.focus();
2893     this.tabId = tabId;
2894 
2895     return true;
2896   };
2897 
2898   /**
2899    * Hide a tab by ID.
2900    *
2901    * @param {String} tabId The ID of the tab you want to hide.
2902    * @returns {Boolean} True if the tab has been hidden successfully, or false 
2903    * if not.
2904    */
2905   this.tabHide = function (tabId) {
2906     if (!(tabId in this.tabs)) {
2907       return false;
2908     }
2909 
2910     if (this.tabId === tabId) {
2911       this.tabActivate(prevTabId_);
2912     }
2913 
2914     this.tabs[tabId].button.style.display = 'none';
2915 
2916     return true;
2917   };
2918 
2919   /**
2920    * Show a tab by ID.
2921    *
2922    * @param {String} tabId The ID of the tab you want to show.
2923    * @returns {Boolean} True if the tab has been displayed successfully, or 
2924    * false if not.
2925    */
2926   this.tabShow = function (tabId) {
2927     if (!(tabId in this.tabs)) {
2928       return false;
2929     }
2930 
2931     this.tabs[tabId].button.style.display = '';
2932 
2933     return true;
2934   };
2935 
2936   init();
2937 };
2938 
2939 /**
2940  * @class The GUI tab activation event. This event is cancelable.
2941  *
2942  * @augments pwlib.appEvent
2943  *
2944  * @param {String} tabId The ID of the tab being activated.
2945  * @param {String} prevTabId The ID of the previously active tab.
2946  */
2947 pwlib.appEvent.guiTabActivate = function (tabId, prevTabId) {
2948   /**
2949    * The ID of the tab being activated.
2950    * @type String
2951    */
2952   this.tabId = tabId;
2953 
2954   /**
2955    * The ID of the previously active tab.
2956    * @type String
2957    */
2958   this.prevTabId = prevTabId;
2959 
2960   pwlib.appEvent.call(this, 'guiTabActivate', true);
2961 };
2962 
2963 /**
2964  * @class The color input GUI component.
2965  *
2966  * @private
2967  *
2968  * @param {pwlib.gui} gui Reference to the PaintWeb GUI object.
2969  *
2970  * @param {Element} input Reference to the DOM input element. This can be 
2971  * a span, a div, or any other tag.
2972  */
2973 pwlib.guiColorInput = function (gui, input) {
2974   var _self      = this,
2975       colormixer = null,
2976       config     = gui.app.config,
2977       doc        = gui.app.doc,
2978       MathRound  = Math.round,
2979       lang       = gui.app.lang;
2980 
2981   /**
2982    * Color input ID. The ID is the same as the data-pwColorInput attribute value 
2983    * of the DOM input element .
2984    *
2985    * @type String.
2986    */
2987   this.id = null;
2988 
2989   /**
2990    * The color input element DOM reference.
2991    *
2992    * @type Element
2993    */
2994   this.input = input;
2995 
2996   /**
2997    * The configuration property to which this color input is attached to.
2998    * @type String
2999    */
3000   this.configProperty = null;
3001 
3002   /**
3003    * The configuration group to which this color input is attached to.
3004    * @type String
3005    */
3006   this.configGroup = null;
3007 
3008   /**
3009    * Reference to the configuration object which holds the color input value.
3010    * @type String
3011    */
3012   this.configGroupRef = null;
3013 
3014   /**
3015    * Holds the current color displayed by the input.
3016    *
3017    * @type Object
3018    */
3019   this.color = {red: 0, green: 0, blue: 0, alpha: 0};
3020 
3021   /**
3022    * Initialize the color input functionality.
3023    * @private
3024    */
3025   function init () {
3026     var cfgAttr     = _self.input.getAttribute('data-pwColorInput'),
3027         cfgNoDots   = cfgAttr.replace('.', '_'),
3028         cfgArray    = cfgAttr.split('.'),
3029         cfgProp     = cfgArray.pop(),
3030         cfgGroup    = cfgArray.join('.'),
3031         cfgGroupRef = config,
3032         langGroup   = lang.inputs,
3033         labelElem   = _self.input.parentNode,
3034         anchor      = doc.createElement('a'),
3035         color;
3036 
3037     for (var i = 0, n = cfgArray.length; i < n; i++) {
3038       cfgGroupRef = cfgGroupRef[cfgArray[i]];
3039       langGroup = langGroup[cfgArray[i]];
3040     }
3041 
3042     _self.configProperty = cfgProp;
3043     _self.configGroup = cfgGroup;
3044     _self.configGroupRef = cfgGroupRef;
3045 
3046     _self.id = cfgNoDots;
3047 
3048     _self.input.className += ' ' + gui.classPrefix + 'colorInput' 
3049       + ' ' + gui.classPrefix + _self.id;
3050 
3051     labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), 
3052         labelElem.firstChild);
3053 
3054     color = _self.configGroupRef[_self.configProperty];
3055     color = color.replace(/\s+/g, '').replace(/^rgba\(/, '').replace(/\)$/, '');
3056     color = color.split(',');
3057     _self.color.red   = color[0] / 255;
3058     _self.color.green = color[1] / 255;
3059     _self.color.blue  = color[2] / 255;
3060     _self.color.alpha = color[3];
3061 
3062     anchor.style.backgroundColor = 'rgb(' + color[0] + ',' + color[1] + ',' 
3063         + color[2] + ')';
3064     anchor.style.opacity = color[3];
3065 
3066     anchor.href = '#';
3067     anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp];
3068     anchor.appendChild(doc.createTextNode(lang.inputs.colorInputAnchorContent));
3069     anchor.addEventListener('click', ev_input_click, false);
3070 
3071     _self.input.replaceChild(anchor, _self.input.firstChild);
3072   };
3073 
3074   /**
3075    * The <code>click</code> event handler for the color input element. This 
3076    * function shows/hides the Color Mixer panel.
3077    *
3078    * @private
3079    * @param {Event} ev The DOM Event object.
3080    */
3081   function ev_input_click (ev) {
3082     ev.preventDefault();
3083 
3084     if (!colormixer) {
3085       colormixer = gui.app.extensions.colormixer;
3086     }
3087 
3088     if (!colormixer.targetInput || colormixer.targetInput.id !== _self.id) {
3089       colormixer.show({
3090           id: _self.id,
3091           configProperty: _self.configProperty,
3092           configGroup: _self.configGroup,
3093           configGroupRef: _self.configGroupRef,
3094           show: colormixer_show,
3095           hide: colormixer_hide
3096         }, _self.color);
3097 
3098     } else {
3099       colormixer.hide();
3100     }
3101   };
3102 
3103   /**
3104    * The color mixer <code>show</code> event handler. This function is invoked 
3105    * when the color mixer is shown.
3106    * @private
3107    */
3108   function colormixer_show () {
3109     var classActive = ' ' + gui.classPrefix + 'colorInputActive',
3110         elemActive = _self.input.className.indexOf(classActive) !== -1;
3111 
3112     if (!elemActive) {
3113       _self.input.className += classActive;
3114     }
3115   };
3116 
3117   /**
3118    * The color mixer <code>hide</code> event handler. This function is invoked 
3119    * when the color mixer is hidden.
3120    * @private
3121    */
3122   function colormixer_hide () {
3123     var classActive = ' ' + gui.classPrefix + 'colorInputActive',
3124         elemActive = _self.input.className.indexOf(classActive) !== -1;
3125 
3126     if (elemActive) {
3127       _self.input.className = _self.input.className.replace(classActive, '');
3128     }
3129   };
3130 
3131   /**
3132    * Update color. This method allows the change of the color values associated 
3133    * to the current color input.
3134    *
3135    * <p>This method is used by the color picker tool and by the global GUI 
3136    * <code>configChange</code> application event handler.
3137    *
3138    * @param {Object} color The new color values. The object must have four 
3139    * properties: <var>red</var>, <var>green</var>, <var>blue</var> and 
3140    * <var>alpha</var>. All values must be between 0 and 1.
3141    */
3142   this.updateColor = function (color) {
3143     var anchor = _self.input.firstChild.style;
3144 
3145     anchor.opacity         = color.alpha;
3146     anchor.backgroundColor = 'rgb(' + MathRound(color.red   * 255) + ',' +
3147                                       MathRound(color.green * 255) + ',' +
3148                                       MathRound(color.blue  * 255) + ')';
3149     _self.color.red   = color.red;
3150     _self.color.green = color.green;
3151     _self.color.blue  = color.blue;
3152     _self.color.alpha = color.alpha;
3153   };
3154 
3155   init();
3156 };
3157 
3158 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
3159 
3160