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