1 /*
  2  * Copyright (C) 2008, 2009 Mihai Şucan
  3  *
  4  * This file is part of PaintWeb.
  5  *
  6  * PaintWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * PaintWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
 18  *
 19  * $URL: http://code.google.com/p/paintweb $
 20  * $Date: 2009-08-27 20:30:01 +0300 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the text tool implementation.
 26  */
 27 
 28 // TODO: make this tool nicer to use.
 29 
 30 /**
 31  * @class The text tool.
 32  *
 33  * @param {PaintWeb} app Reference to the main paint application object.
 34  */
 35 pwlib.tools.text = function (app) {
 36   var _self         = this,
 37       clearInterval = app.win.clearInterval,
 38       config        = app.config.text,
 39       context       = app.buffer.context,
 40       doc           = app.doc,
 41       gui           = app.gui,
 42       image         = app.image,
 43       lang          = app.lang,
 44       MathRound     = Math.round,
 45       mouse         = app.mouse,
 46       setInterval   = app.win.setInterval;
 47 
 48   /**
 49    * The interval ID used for invoking the drawing operation every few 
 50    * milliseconds.
 51    *
 52    * @private
 53    * @see PaintWeb.config.toolDrawDelay
 54    */
 55   var timer = null;
 56 
 57   /**
 58    * Holds the previous tool ID.
 59    *
 60    * @private
 61    * @type String
 62    */
 63   var prevTool = app.tool ? app.tool._id : null;
 64 
 65   /**
 66    * Tells if the drawing canvas needs to be updated or not.
 67    *
 68    * @private
 69    * @type Boolean
 70    * @default false
 71    */
 72   var needsRedraw = false;
 73 
 74   var inputString = null,
 75       input_fontFamily = null,
 76       ev_configChangeId = null,
 77       ns_svg = "http://www.w3.org/2000/svg",
 78       svgDoc = null,
 79       svgText = null,
 80       textWidth = 0,
 81       textHeight = 0;
 82 
 83   /**
 84    * Tool preactivation code. This method check if the browser has support for 
 85    * rendering text in Canvas.
 86    *
 87    * @returns {Boolean} True if the tool can be activated successfully, or false 
 88    * if not.
 89    */
 90   this.preActivate = function () {
 91     if (!gui.inputs.textString || !gui.inputs.text_fontFamily || 
 92         !gui.elems.viewport) {
 93       return false;
 94 
 95     }
 96 
 97     // Canvas 2D Text API
 98     if (context.fillText && context.strokeText) {
 99       return true;
100     }
101 
102     // Opera can only render text via SVG Text.
103     // Note: support for Opera has been disabled.
104     // There are severe SVG redraw issues when updating the SVG text element.
105     // Besides, there are important memory leaks.
106     // Ultimately, there's a deal breaker: security violation. The SVG document 
107     // which is rendered inside Canvas is considered "external" 
108     // - get/putImageData() and toDataURL() stop working after drawImage(svg) is 
109     // invoked. Eh.
110     /*if (pwlib.browser.opera) {
111       return true;
112     }*/
113 
114     // Gecko 1.9.0 had its own proprietary Canvas 2D Text API.
115     if (context.mozPathText) {
116       return true;
117     }
118 
119     alert(lang.errorTextUnsupported);
120     return false;
121   };
122 
123   /**
124    * The tool activation code. This sets up a few variables, starts the drawing 
125    * timer and adds event listeners as needed.
126    */
127   this.activate = function () {
128     // Reset the mouse coordinates to the scroll top/left corner such that the 
129     // text is rendered there.
130     mouse.x = Math.round(gui.elems.viewport.scrollLeft / image.canvasScale),
131     mouse.y = Math.round(gui.elems.viewport.scrollTop  / image.canvasScale),
132 
133     input_fontFamily = gui.inputs.text_fontFamily;
134     inputString = gui.inputs.textString;
135 
136     if (!context.fillText && pwlib.browser.opera) {
137       ev_configChangeId = app.events.add('configChange', ev_configChange_opera);
138       inputString.addEventListener('input',  ev_configChange_opera, false);
139       inputString.addEventListener('change', ev_configChange_opera, false);
140     } else {
141       ev_configChangeId = app.events.add('configChange', ev_configChange);
142       inputString.addEventListener('input',  ev_configChange, false);
143       inputString.addEventListener('change', ev_configChange, false);
144     }
145 
146     // Render text using the Canvas 2D context text API defined by HTML 5.
147     if (context.fillText && context.strokeText) {
148       _self.draw = _self.draw_spec;
149 
150     } else if (pwlib.browser.opera) {
151       // Render text using a SVG Text element which is copied into Canvas using 
152       // drawImage().
153       _self.draw = _self.draw_opera;
154       initOpera();
155 
156     } else if (context.mozPathText) {
157       // Render text using proprietary API available in Gecko 1.9.0.
158       _self.draw = _self.draw_moz;
159       textWidth = context.mozMeasureText(inputString.value);
160     }
161 
162     if (!timer) {
163       timer = setInterval(_self.draw, app.config.toolDrawDelay);
164     }
165     needsRedraw = true;
166   };
167 
168   /**
169    * The tool deactivation simply consists of removing the event listeners added 
170    * when the tool was constructed, and clearing the buffer canvas.
171    */
172   this.deactivate = function () {
173     if (timer) {
174       clearInterval(timer);
175       timer = null;
176     }
177     needsRedraw = false;
178 
179     if (ev_configChangeId) {
180       app.events.remove('configChange', ev_configChangeId);
181     }
182 
183     if (!context.fillText && pwlib.browser.opera) {
184       inputString.removeEventListener('input',  ev_configChange_opera, false);
185       inputString.removeEventListener('change', ev_configChange_opera, false);
186     } else {
187       inputString.removeEventListener('input',  ev_configChange, false);
188       inputString.removeEventListener('change', ev_configChange, false);
189     }
190 
191     svgText = null;
192     svgDoc = null;
193 
194     context.clearRect(0, 0, image.width, image.height);
195 
196     return true;
197   };
198 
199   /**
200    * Initialize the SVG document for Opera. This is used for rendering the text.
201    * @private
202    */
203   function initOpera () {
204     svgDoc = doc.createElementNS(ns_svg, 'svg');
205     svgDoc.setAttributeNS(ns_svg, 'version', '1.1');
206 
207     svgText = doc.createElementNS(ns_svg, 'text');
208     svgText.appendChild(doc.createTextNode(inputString.value));
209     svgDoc.appendChild(svgText);
210 
211     svgText.style.font = context.font;
212 
213     if (app.config.shapeType !== 'stroke') {
214       svgText.style.fill = context.fillStyle;
215     } else {
216       svgText.style.fill = 'none';
217     }
218 
219     if (app.config.shapeType !== 'fill') {
220       svgText.style.stroke = context.strokeStyle;
221       svgText.style.strokeWidth = context.lineWidth;
222     } else {
223       svgText.style.stroke = 'none';
224       svgText.style.strokeWidth = context.lineWidth;
225     }
226 
227     textWidth  = svgText.getComputedTextLength();
228     textHeight = svgText.getBBox().height;
229 
230     svgDoc.setAttributeNS(ns_svg, 'width',  textWidth);
231     svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10);
232     svgText.setAttributeNS(ns_svg, 'x', 0);
233     svgText.setAttributeNS(ns_svg, 'y', textHeight);
234   };
235 
236   /**
237    * The <code>configChange</code> application event handler. This is also the 
238    * <code>input</code> and <code>change</code> event handler for the text 
239    * string input element.  This method updates the Canvas text-related 
240    * properties as needed, and re-renders the text.
241    *
242    * <p>This function is not used on Opera.
243    *
244    * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event 
245    * object.
246    */
247   function ev_configChange (ev) {
248     if (ev.type === 'input' || ev.type === 'change' ||
249         (!ev.group && ev.config === 'shapeType') ||
250         (ev.group === 'line' && ev.config === 'lineWidth')) {
251       needsRedraw = true;
252 
253       // Update the text width.
254       if (!context.fillText && context.mozMeasureText) {
255         textWidth = context.mozMeasureText(inputString.value);
256       }
257       return;
258     }
259 
260     if (ev.type !== 'configChange' && ev.group !== 'text') {
261       return;
262     }
263 
264     var font = '';
265 
266     switch (ev.config) {
267       case 'fontFamily':
268         if (ev.value === '+') {
269           fontFamilyAdd(ev);
270         }
271       case 'bold':
272       case 'italic':
273       case 'fontSize':
274         if (config.bold) {
275           font += 'bold ';
276         }
277         if (config.italic) {
278           font += 'italic ';
279         }
280         font += config.fontSize + 'px ' + config.fontFamily;
281         context.font = font;
282 
283         if ('mozTextStyle' in context) {
284           context.mozTextStyle = font;
285         }
286 
287       case 'textAlign':
288       case 'textBaseline':
289         needsRedraw = true;
290     }
291 
292     // Update the text width.
293     if (ev.config !== 'textAlign' && ev.config !== 'textBaseline' && 
294         !context.fillText && context.mozMeasureText) {
295       textWidth = context.mozMeasureText(inputString.value);
296     }
297   };
298 
299   /**
300    * The <code>configChange</code> application event handler. This is also the 
301    * <code>input</code> and <code>change</code> event handler for the text 
302    * string input element.  This method updates the Canvas text-related 
303    * properties as needed, and re-renders the text.
304    *
305    * <p>This is function is specific to Opera.
306    *
307    * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event 
308    * object.
309    */
310   function ev_configChange_opera (ev) {
311     if (ev.type === 'input' || ev.type === 'change') {
312       svgText.replaceChild(doc.createTextNode(this.value), svgText.firstChild);
313       needsRedraw = true;
314     }
315 
316     if (!ev.group && ev.config === 'shapeType') {
317       if (ev.value !== 'stroke') {
318         svgText.style.fill = context.fillStyle;
319       } else {
320         svgText.style.fill = 'none';
321       }
322 
323       if (ev.value !== 'fill') {
324         svgText.style.stroke = context.strokeStyle;
325         svgText.style.strokeWidth = context.lineWidth;
326       } else {
327         svgText.style.stroke = 'none';
328         svgText.style.strokeWidth = context.lineWidth;
329       }
330       needsRedraw = true;
331     }
332 
333     if (!ev.group && ev.config === 'fillStyle') {
334       if (app.config.shapeType !== 'stroke') {
335         svgText.style.fill = ev.value;
336         needsRedraw = true;
337       }
338     }
339 
340     if ((!ev.group && ev.config === 'strokeStyle') ||
341         (ev.group === 'line' && ev.config === 'lineWidth')) {
342       if (app.config.shapeType !== 'fill') {
343         svgText.style.stroke = context.strokeStyle;
344         svgText.style.strokeWidth = context.lineWidth;
345         needsRedraw = true;
346       }
347     }
348 
349     if (ev.type === 'configChange' && ev.group === 'text') {
350       var font = '';
351       switch (ev.config) {
352         case 'fontFamily':
353           if (ev.value === '+') {
354             fontFamilyAdd(ev);
355           }
356         case 'bold':
357         case 'italic':
358         case 'fontSize':
359           if (config.bold) {
360             font += 'bold ';
361           }
362           if (config.italic) {
363             font += 'italic ';
364           }
365           font += config.fontSize + 'px ' + config.fontFamily;
366           context.font = font;
367           svgText.style.font = font;
368 
369         case 'textAlign':
370         case 'textBaseline':
371           needsRedraw = true;
372       }
373     }
374 
375     textWidth  = svgText.getComputedTextLength();
376     textHeight = svgText.getBBox().height;
377 
378     svgDoc.setAttributeNS(ns_svg, 'width',  textWidth);
379     svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10);
380     svgText.setAttributeNS(ns_svg, 'x', 0);
381     svgText.setAttributeNS(ns_svg, 'y', textHeight);
382   };
383 
384   /**
385    * Add a new font family into the font family drop down. This function is 
386    * invoked by the <code>ev_configChange()</code> function when the user 
387    * attempts to add a new font family.
388    *
389    * @private
390    *
391    * @param {pwlib.appEvent.configChange} ev The application event object.
392    */
393   function fontFamilyAdd (ev) {
394     var new_font = prompt(lang.promptTextFont) || '';
395     new_font = new_font.replace(/^\s+/, '').replace(/\s+$/, '') || 
396       ev.previousValue;
397 
398     // Check if the font name is already in the list.
399     var opt, new_font2 = new_font.toLowerCase(),
400         n = input_fontFamily.options.length;
401 
402     for (var i = 0; i < n; i++) {
403       opt = input_fontFamily.options[i];
404       if (opt.value.toLowerCase() == new_font2) {
405         config.fontFamily = opt.value;
406         input_fontFamily.selectedIndex = i;
407         input_fontFamily.value = config.fontFamily;
408         ev.value = config.fontFamily;
409 
410         return;
411       }
412     }
413 
414     // Add the new font.
415     opt = doc.createElement('option');
416     opt.value = new_font;
417     opt.appendChild(doc.createTextNode(new_font));
418     input_fontFamily.insertBefore(opt, input_fontFamily.options[n-1]);
419     input_fontFamily.selectedIndex = n-1;
420     input_fontFamily.value = new_font;
421     ev.value = new_font;
422     config.fontFamily = new_font;
423   };
424 
425   /**
426    * The <code>mousemove</code> event handler.
427    */
428   this.mousemove = function () {
429     needsRedraw = true;
430   };
431 
432   /**
433    * Perform the drawing operation using standard 2D context methods.
434    *
435    * @see PaintWeb.config.toolDrawDelay
436    */
437   this.draw_spec = function () {
438     if (!needsRedraw) {
439       return;
440     }
441 
442     context.clearRect(0, 0, image.width, image.height);
443 
444     if (app.config.shapeType != 'stroke') {
445       context.fillText(inputString.value, mouse.x, mouse.y);
446     }
447 
448     if (app.config.shapeType != 'fill') {
449       context.beginPath();
450       context.strokeText(inputString.value, mouse.x, mouse.y);
451       context.closePath();
452     }
453 
454     needsRedraw = false;
455   };
456 
457   /**
458    * Perform the drawing operation in Gecko 1.9.0.
459    */
460   this.draw_moz = function () {
461     if (!needsRedraw) {
462       return;
463     }
464 
465     context.clearRect(0, 0, image.width, image.height);
466 
467     var x = mouse.x,
468         y = mouse.y;
469 
470     if (config.textAlign === 'center') {
471       x -= MathRound(textWidth / 2);
472     } else if (config.textAlign === 'right') {
473       x -= textWidth;
474     }
475 
476     if (config.textBaseline === 'top') {
477       y += config.fontSize;
478     } else if (config.textBaseline === 'middle') {
479       y += MathRound(config.fontSize / 2);
480     }
481 
482     context.setTransform(1, 0, 0, 1, x, y);
483     context.beginPath();
484     context.mozPathText(inputString.value);
485 
486     if (app.config.shapeType != 'stroke') {
487       context.fill();
488     }
489 
490     if (app.config.shapeType != 'fill') {
491       context.stroke();
492     }
493     context.closePath();
494     context.setTransform(1, 0, 0, 1, 0, 0);
495 
496     needsRedraw = false;
497   };
498 
499   /**
500    * Perform the drawing operation in Opera using SVG.
501    */
502   this.draw_opera = function () {
503     if (!needsRedraw) {
504       return;
505     }
506 
507     context.clearRect(0, 0, image.width, image.height);
508 
509     var x = mouse.x,
510         y = mouse.y;
511 
512     if (config.textAlign === 'center') {
513       x -= MathRound(textWidth / 2);
514     } else if (config.textAlign === 'right') {
515       x -= textWidth;
516     }
517 
518     if (config.textBaseline === 'bottom') {
519       y -= textHeight;
520     } else if (config.textBaseline === 'middle') {
521       y -= MathRound(textHeight / 2);
522     }
523 
524     context.drawImage(svgDoc, x, y);
525 
526     needsRedraw = false;
527   };
528 
529   /**
530    * The <code>click</code> event handler. This method completes the drawing 
531    * operation by inserting the text into the layer canvas.
532    */
533   this.click = function () {
534     _self.draw();
535     app.layerUpdate();
536   };
537 
538   /**
539    * The <code>keydown</code> event handler allows users to press the 
540    * <kbd>Escape</kbd> key to cancel the drawing operation and return to the 
541    * previous tool.
542    *
543    * @param {Event} ev The DOM Event object.
544    * @returns {Boolean} True if the key was recognized, or false if not.
545    */
546   this.keydown = function (ev) {
547     if (!prevTool || ev.kid_ != 'Escape') {
548       return false;
549     }
550 
551     mouse.buttonDown = false;
552     app.toolActivate(prevTool, ev);
553 
554     return true;
555   };
556 };
557 
558 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
559 
560