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