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-07-06 16:20:38 +0300 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the "Insert image" tool implementation.
 26  */
 27 
 28 // TODO: allow inserting images from a different host, using server-side magic.
 29 
 30 /**
 31  * @class The "Insert image" tool.
 32  *
 33  * @param {PaintWeb} app Reference to the main paint application object.
 34  */
 35 pwlib.tools.insertimg = function (app) {
 36   var _self         = this,
 37       canvasImage   = app.image,
 38       clearInterval = app.win.clearInterval,
 39       config        = app.config,
 40       context       = app.buffer.context,
 41       gui           = app.gui,
 42       lang          = app.lang,
 43       MathAbs       = Math.abs,
 44       MathMin       = Math.min,
 45       MathRound     = Math.round,
 46       mouse         = app.mouse,
 47       setInterval   = app.win.setInterval;
 48 
 49   /**
 50    * Holds the previous tool ID.
 51    *
 52    * @private
 53    * @type String
 54    */
 55   var prevTool = app.tool ? app.tool._id : null;
 56 
 57   /**
 58    * The interval ID used for invoking the drawing operation every few 
 59    * milliseconds.
 60    *
 61    * @private
 62    * @see PaintWeb.config.toolDrawDelay
 63    */
 64   var timer = null;
 65 
 66   /**
 67    * Tells if the <kbd>Shift</kbd> key is down or not. This is used by the 
 68    * drawing function.
 69    *
 70    * @private
 71    * @type Boolean
 72    * @default false
 73    */
 74   var shiftKey = false;
 75 
 76   /**
 77    * Tells if the drawing canvas needs to be updated or not.
 78    *
 79    * @private
 80    * @type Boolean
 81    * @default false
 82    */
 83   var needsRedraw = false;
 84 
 85   /**
 86    * Holds the starting point on the <var>x</var> axis of the image, for the 
 87    * current drawing operation.
 88    *
 89    * @private
 90    * @type Number
 91    */
 92   var x0 = 0;
 93 
 94   /**
 95    * Holds the starting point on the <var>y</var> axis of the image, for the 
 96    * current drawing operation.
 97    *
 98    * @private
 99    * @type Number
100    */
101   var y0 = 0;
102 
103   /**
104    * Tells if the image element loaded or not.
105    *
106    * @private
107    * @type Boolean
108    */
109   var imageLoaded = false;
110 
111   /**
112    * Holds the image aspect ratio, used by the resize method.
113    *
114    * @private
115    * @type Number
116    */
117   var imageRatio = 1;
118 
119   /**
120    * Holds the DOM image element.
121    *
122    * @private
123    * @type Element
124    */
125   var imageElement = null;
126 
127   /**
128    * Holds the image address.
129    * @type String
130    */
131   if (!this.url) {
132     this.url = 'http://';
133   }
134 
135   /**
136    * The tool preactivation code. This function asks the user to provide an URL 
137    * to the image which is desired to be inserted into the canvas.
138    *
139    * @returns {Boolean} True if the URL provided is correct. False is returned 
140    * if the URL is not provided or if it's incorrect. When false is returned the 
141    * tool activation is cancelled.
142    */
143   this.preActivate = function () {
144     if (!gui.elems.viewport) {
145       return false;
146     }
147 
148     _self.url = prompt(lang.promptInsertimg, _self.url);
149 
150     if (!_self.url || _self.url.toLowerCase() === 'http://') {
151       return false;
152     }
153 
154     // Remember the URL.
155     pwlib.extend(true, _self.constructor.prototype, {url: _self.url});
156 
157     if (!pwlib.isSameHost(_self.url, app.win.location.host)) {
158       alert(lang.errorInsertimgHost);
159       return false;
160     }
161 
162     return true;
163   };
164 
165   /**
166    * The tool activation event handler. This function is called once the 
167    * previous tool has been deactivated.
168    */
169   this.activate = function () {
170     imageElement = new Image();
171     imageElement.addEventListener('load', ev_imageLoaded, false);
172     imageElement.src = _self.url;
173 
174     return true;
175   };
176 
177   /**
178    * The tool deactivation event handler.
179    */
180   this.deactivate = function () {
181     if (imageElement) {
182       imageElement = null;
183     }
184 
185     if (timer) {
186       clearInterval(timer);
187       timer = null;
188     }
189     needsRedraw = false;
190 
191     context.clearRect(0, 0, canvasImage.width, canvasImage.height);
192 
193     return true;
194   };
195 
196   /**
197    * The <code>load</code> event handler for the image element. This method 
198    * makes sure the image dimensions are synchronized with the zoom level, and 
199    * draws the image on the canvas.
200    *
201    * @private
202    */
203   function ev_imageLoaded () {
204     // Did the image already load?
205     if (imageLoaded) {
206       return;
207     }
208 
209     // The default position for the inserted image is the top left corner of the visible area, taking into consideration the zoom level.
210     var x = MathRound(gui.elems.viewport.scrollLeft / canvasImage.canvasScale),
211         y = MathRound(gui.elems.viewport.scrollTop  / canvasImage.canvasScale);
212 
213     context.clearRect(0, 0, canvasImage.width, canvasImage.height);
214 
215     try {
216       context.drawImage(imageElement, x, y);
217     } catch (err) {
218       alert(lang.errorInsertimg);
219       return;
220     }
221 
222     imageLoaded = true;
223     needsRedraw = false;
224 
225     if (!timer) {
226       timer = setInterval(_self.draw, config.toolDrawDelay);
227     }
228 
229     gui.statusShow('insertimgLoaded');
230   };
231 
232   /**
233    * The <code>mousedown</code> event handler. This method stores the current 
234    * mouse location and the image aspect ratio for later reuse by the 
235    * <code>draw()</code> method.
236    *
237    * @param {Event} ev The DOM Event object.
238    */
239   this.mousedown = function (ev) {
240     if (!imageLoaded) {
241       alert(lang.errorInsertimgNotLoaded);
242       return false;
243     }
244 
245     x0 = mouse.x;
246     y0 = mouse.y;
247 
248     // The image aspect ratio - used by the draw() method when the user holds 
249     // the Shift key down.
250     imageRatio = imageElement.width / imageElement.height;
251     shiftKey = ev.shiftKey;
252 
253     gui.statusShow('insertimgResize');
254 
255     if (ev.stopPropagation) {
256       ev.stopPropagation();
257     }
258   };
259 
260   /**
261    * The <code>mousemove</code> event handler.
262    *
263    * @param {Event} ev The DOM Event object.
264    */
265   this.mousemove = function (ev) {
266     shiftKey = ev.shiftKey;
267     needsRedraw = true;
268   };
269 
270   /**
271    * Perform the drawing operation. When the mouse button is not down, the user 
272    * is allowed to pick where he/she wants to insert the image element, inside 
273    * the canvas. Once the <code>mousedown</code> event is fired, this method 
274    * allows the user to resize the image inside the canvas.
275    *
276    * @see PaintWeb.config.toolDrawDelay
277    */
278   this.draw = function () {
279     if (!imageLoaded || !needsRedraw) {
280       return;
281     }
282 
283     context.clearRect(0, 0, canvasImage.width, canvasImage.height);
284 
285     // If the user is holding down the mouse button, then allow him/her to 
286     // resize the image.
287     if (mouse.buttonDown) {
288       var w = MathAbs(mouse.x - x0),
289           h = MathAbs(mouse.y - y0),
290           x = MathMin(mouse.x,  x0),
291           y = MathMin(mouse.y,  y0);
292 
293       if (!w || !h) {
294         needsRedraw = false;
295         return;
296       }
297 
298       // If the Shift key is down, constrain the image to have the same aspect 
299       // ratio as the original image element.
300       if (shiftKey) {
301         if (w > h) {
302           if (y == mouse.y) {
303             y -= w-h;
304           }
305           h = MathRound(w/imageRatio);
306         } else {
307           if (x == mouse.x) {
308             x -= h-w;
309           }
310           w = MathRound(h*imageRatio);
311         }
312       }
313 
314       context.drawImage(imageElement, x, y, w, h);
315     } else {
316       // If the mouse button is not down, simply allow the user to pick where 
317       // he/she wants to insert the image element.
318       context.drawImage(imageElement, mouse.x, mouse.y);
319     }
320 
321     needsRedraw = false;
322   };
323 
324   /**
325    * The <code>mouseup</code> event handler. This method completes the drawing 
326    * operation by inserting the image in the layer canvas, and by activating the 
327    * previous tool.
328    *
329    * @param {Event} ev The DOM Event object.
330    */
331   this.mouseup = function (ev) {
332     if (!imageLoaded) {
333       return false;
334     }
335 
336     if (timer) {
337       clearInterval(timer);
338       timer = null;
339     }
340 
341     app.layerUpdate();
342 
343     if (prevTool) {
344       app.toolActivate(prevTool, ev);
345     }
346 
347     if (ev.stopPropagation) {
348       ev.stopPropagation();
349     }
350   };
351 
352   /**
353    * The <code>keydown</code> event handler allows users to press the 
354    * <kbd>Escape</kbd> key to cancel the drawing operation and return to the 
355    * previous tool.
356    *
357    * @param {Event} ev The DOM Event object.
358    * @returns {Boolean} True if the key was recognized, or false if not.
359    */
360   this.keydown = function (ev) {
361     if (!prevTool || ev.kid_ != 'Escape') {
362       return false;
363     }
364 
365     if (timer) {
366       clearInterval(timer);
367       timer = null;
368     }
369 
370     mouse.buttonDown = false;
371     app.toolActivate(prevTool, ev);
372 
373     return true;
374   };
375 };
376 
377 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
378 
379