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-02 16:07:14 +0300 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the selection tool implementation.
 26  */
 27 
 28 /**
 29  * @class The selection tool.
 30  *
 31  * @param {PaintWeb} app Reference to the main paint application object.
 32  */
 33 pwlib.tools.selection = function (app) {
 34   var _self         = this,
 35       appEvent      = pwlib.appEvent,
 36       bufferContext = app.buffer.context,
 37       clearInterval = app.win.clearInterval,
 38       config        = app.config.selection,
 39       gui           = app.gui,
 40       image         = app.image,
 41       lang          = app.lang,
 42       layerCanvas   = app.layer.canvas,
 43       layerContext  = app.layer.context,
 44       marqueeStyle  = null,
 45       MathAbs       = Math.abs,
 46       MathMin       = Math.min,
 47       MathRound     = Math.round,
 48       mouse         = app.mouse,
 49       setInterval   = app.win.setInterval,
 50       snapXY        = app.toolSnapXY;
 51 
 52   /**
 53    * The interval ID used for invoking the drawing operation every few 
 54    * milliseconds.
 55    *
 56    * @private
 57    * @see PaintWeb.config.toolDrawDelay
 58    */
 59   var timer = null;
 60 
 61   /**
 62    * Tells if the drawing canvas needs to be updated or not.
 63    *
 64    * @private
 65    * @type Boolean
 66    * @default false
 67    */
 68   var needsRedraw = false;
 69 
 70   /**
 71    * The selection has been dropped, and the mouse button is down. The user has 
 72    * two choices: he releases the mouse button, thus the selection is dropped 
 73    * and the tool switches to STATE_NONE, or he moves the mouse in order to 
 74    * start a new selection (STATE_DRAWING).
 75    * @constant
 76    */
 77   this.STATE_PENDING = -1;
 78 
 79   /**
 80    * No selection is available.
 81    * @constant
 82    */
 83   this.STATE_NONE = 0;
 84 
 85   /**
 86    * The user is drawing a selection.
 87    * @constant
 88    */
 89   this.STATE_DRAWING = 1;
 90 
 91   /**
 92    * The selection rectangle is available.
 93    * @constant
 94    */
 95   this.STATE_SELECTED = 2;
 96 
 97   /**
 98    * The user is dragging/moving the selection rectangle.
 99    * @constant
100    */
101   this.STATE_DRAGGING = 3;
102 
103   /**
104    * The user is resizing the selection rectangle.
105    * @constant
106    */
107   this.STATE_RESIZING = 4;
108 
109   /**
110    * Selection state. Known states:
111    *
112    * <ul>
113    *   <li>{@link pwlib.tools.selection#STATE_PENDING} - Selection dropped after 
114    *   the <code>mousedown</code> event is fired. The script can switch to 
115    *   STATE_DRAWING if the mouse moves, or to STATE_NONE if it does not 
116    *   (allowing the user to drop the selection).
117    *
118    *   <li>{@link pwlib.tools.selection#STATE_NONE} - No selection is available.
119    *
120    *   <li>{@link pwlib.tools.selection#STATE_DRAWING} - The user is drawing the 
121    *   selection rectangle.
122    *
123    *   <li>{@link pwlib.tools.selection#STATE_SELECTED} - The selection 
124    *   rectangle is available.
125    *
126    *   <li>{@link pwlib.tools.selection#STATE_DRAGGING} - The user is 
127    *   dragging/moving the current selection.
128    *
129    *   <li>{@link pwlib.tools.selection#STATE_RESIZING} - The user is resizing 
130    *   the current selection.
131    * </ul>
132    *
133    * @type Number
134    * @default STATE_NONE
135    */
136   this.state = this.STATE_NONE;
137 
138   /**
139    * Holds the starting point on the <var>x</var> axis of the image, for any 
140    * ongoing operation.
141    *
142    * @private
143    * @type Number
144    */
145   var x0 = 0;
146 
147   /**
148    * Holds the starting point on the <var>y</var> axis of the image, for the any  
149    * ongoing operation.
150    *
151    * @private
152    * @type Number
153    */
154   var y0 = 0;
155 
156   /**
157    * Holds selection information and image.
158    * @type Object
159    */
160   this.selection = {
161     /**
162      * Selection start point, on the <var>x</var> axis.
163      * @type Number
164      */
165     x: 0,
166 
167     /**
168      * Selection start point, on the <var>y</var> axis.
169      * @type Number
170      */
171     y: 0,
172 
173     /**
174      * Selection width.
175      * @type Number
176      */
177     width: 0,
178 
179     /**
180      * Selection height.
181      * @type Number
182      */
183     height: 0,
184 
185     /**
186      * Selection original width. The user can make a selection rectangle of 
187      * a given width and height, but after that he/she can resize the selection.
188      * @type Number
189      */
190     widthOriginal: 0,
191 
192     /**
193      * Selection original height. The user can make a selection rectangle of 
194      * a given width and height, but after that he/she can resize the selection.
195      * @type Number
196      */
197     heightOriginal: 0,
198 
199     /**
200      * Tells if the selected ImageData has been cut out or not from the 
201      * layerContext.
202      *
203      * @type Boolean
204      * @default false
205      */
206     layerCleared: false,
207 
208     /**
209      * Selection marquee/border element.
210      * @type HTMLElement
211      */
212     marquee: null,
213 
214     /**
215      * Selection buffer context which holds the selected pixels.
216      * @type CanvasRenderingContext2D
217      */
218     context: null,
219 
220     /**
221      * Selection buffer canvas which holds the selected pixels.
222      * @type HTMLCanvasElement
223      */
224     canvas: null
225   };
226 
227   /**
228    * The area type under the current mouse location.
229    * 
230    * <p>When the selection is available the mouse location can be on top/inside 
231    * the selection rectangle, on the border of the selection, or outside the 
232    * selection.
233    *
234    * <p>Possible values: 'in', 'out', 'border'.
235    *
236    * @private
237    * @type String
238    * @default 'out'
239    */
240   var mouseArea = 'out';
241 
242   /**
243    * The resize type. If the mouse is on top of the selection border, then the 
244    * selection can be resized. The direction of the resize operation is 
245    * determined by the location of the mouse.
246    * 
247    * <p>While the user resizes the selection this variable can hold the 
248    * following values: 'n' (North), 'ne' (North-East), 'e' (East), 'se' 
249    * (South-East), 's' (South), 'sw' (South-West), 'w' (West), 'nw' 
250    * (North-West).
251    *
252    * @private
253    * @type String
254    * @default null
255    */
256   var mouseResize = null;
257 
258   // shorthands / private variables
259   var sel = this.selection,
260       borderDouble = config.borderWidth * 2,
261       ev_canvasSizeChangeId = null,
262       ev_configChangeId = null,
263       ctrlKey = false,
264       shiftKey = false;
265 
266   /**
267    * The last selection rectangle that was drawn. This is used by the selection 
268    * drawing functions.
269    *
270    * @private
271    * @type Object
272    */
273   // We avoid retrieving the mouse coordinates during the mouseup event, due to 
274   // the Opera bug DSK-232264.
275   var lastSel = null;
276 
277   /**
278    * The tool preactivation code. This function prepares the selection canvas 
279    * element.
280    *
281    * @returns {Boolean} True if the activation did not fail, or false otherwise.  
282    * If false is returned, the selection tool cannot be activated.
283    */
284   this.preActivate = function () {
285     if (!('canvasContainer' in gui.elems)) {
286       alert(lang.errorToolActivate);
287       return false;
288     }
289 
290     // The selection image buffer.
291     sel.canvas = app.doc.createElement('canvas');
292     if (!sel.canvas) {
293       alert(lang.errorToolActivate);
294       return false;
295     }
296 
297     sel.canvas.width  = 5;
298     sel.canvas.height = 5;
299 
300     sel.context = sel.canvas.getContext('2d');
301     if (!sel.context) {
302       alert(lang.errorToolActivate);
303       return false;
304     }
305 
306     sel.marquee = app.doc.createElement('div');
307     if (!sel.marquee) {
308       alert(lang.errorToolActivate);
309       return false;
310     }
311     sel.marquee.className = gui.classPrefix + 'selectionMarquee';
312     marqueeStyle = sel.marquee.style;
313 
314     return true;
315   };
316 
317   /**
318    * The tool activation code. This method sets-up multiple event listeners for 
319    * several target objects.
320    */
321   this.activate = function () {
322     // Older browsers do not support get/putImageData, thus non-transparent 
323     // selections cannot be used.
324     if (!layerContext.putImageData || !layerContext.getImageData) {
325       config.transparent = true;
326     }
327 
328     marqueeHide();
329 
330     marqueeStyle.borderWidth = config.borderWidth + 'px';
331     sel.marquee.addEventListener('mousedown', marqueeMousedown, false);
332     sel.marquee.addEventListener('mousemove', marqueeMousemove, false);
333     sel.marquee.addEventListener('mouseup',   marqueeMouseup,   false);
334 
335     gui.elems.canvasContainer.appendChild(sel.marquee);
336 
337     // Disable the Canvas shadow.
338     app.shadowDisallow();
339 
340     // Application event listeners.
341     ev_canvasSizeChangeId = app.events.add('canvasSizeChange', 
342         ev_canvasSizeChange);
343     ev_configChangeId = app.events.add('configChange', ev_configChange);
344 
345     // Register selection-related commands
346     app.commandRegister('selectionCrop',   _self.selectionCrop);
347     app.commandRegister('selectionDelete', _self.selectionDelete);
348     app.commandRegister('selectionFill',   _self.selectionFill);
349 
350     if (!timer) {
351       timer = setInterval(timerFn, app.config.toolDrawDelay);
352     }
353 
354     return true;
355   };
356 
357   /**
358    * The tool deactivation code. This removes all event listeners and cleans up 
359    * the document.
360    */
361   this.deactivate = function () {
362     if (timer) {
363       clearInterval(timer);
364       timer = null;
365     }
366 
367     _self.selectionMerge();
368 
369     sel.marquee.removeEventListener('mousedown', marqueeMousedown, false);
370     sel.marquee.removeEventListener('mousemove', marqueeMousemove, false);
371     sel.marquee.removeEventListener('mouseup',   marqueeMouseup,   false);
372 
373     marqueeStyle = null;
374     gui.elems.canvasContainer.removeChild(sel.marquee);
375 
376     delete sel.context, sel.canvas, sel.marquee;
377 
378     // Re-enable canvas shadow.
379     app.shadowAllow();
380 
381     // Remove the application event listeners.
382     if (ev_canvasSizeChangeId) {
383       app.events.remove('canvasSizeChange', ev_canvasSizeChangeId);
384     }
385     if (ev_configChangeId) {
386       app.events.remove('configChange', ev_configChangeId);
387     }
388 
389     // Unregister selection-related commands
390     app.commandUnregister('selectionCrop');
391     app.commandUnregister('selectionDelete');
392     app.commandUnregister('selectionFill');
393 
394     return true;
395   };
396 
397   /**
398    * The <code>mousedown</code> event handler. Depending on the mouse location, 
399    * this method does initiate different selection operations: drawing, 
400    * dropping, dragging or resizing.
401    *
402    * <p>Hold the <kbd>Control</kbd> key down to temporarily toggle the 
403    * transformation mode.
404    *
405    * @param {Event} ev The DOM Event object.
406    */
407   this.mousedown = function (ev) {
408     if (_self.state !== _self.STATE_NONE &&
409         _self.state !== _self.STATE_SELECTED) {
410       return false;
411     }
412 
413     // Update the current mouse position, this is used as the start position for most of the operations.
414     x0 = mouse.x;
415     y0 = mouse.y;
416 
417     shiftKey = ev.shiftKey;
418     ctrlKey = ev.ctrlKey;
419     lastSel = null;
420 
421     // No selection is available, then start drawing a selection.
422     if (_self.state === _self.STATE_NONE) {
423       _self.state = _self.STATE_DRAWING;
424       marqueeStyle.display = '';
425       gui.statusShow('selectionDraw');
426 
427       return true;
428     }
429 
430     // STATE_SELECTED: selection available.
431     mouseAreaUpdate();
432 
433     /*
434      * Check if the user clicked outside the selection: drop the selection, 
435      * switch to STATE_PENDING, clear the image buffer and put the current 
436      * selection buffer in the image layer.
437      *
438      * If the user moves the mouse without taking the finger off the mouse 
439      * button, then a new selection rectangle will start to be drawn: the script 
440      * will switch to STATE_DRAWING.
441      *
442      * If the user simply takes the finger off the mouse button (mouseup), then 
443      * the script will switch to STATE_NONE (no selection available).
444      */
445     switch (mouseArea) {
446       case 'out':
447         _self.state = _self.STATE_PENDING;
448         marqueeHide();
449         gui.statusShow('selectionActive');
450         selectionMergeStrict();
451 
452         return true;
453 
454       case 'in':
455         // The mouse area: 'in' for drag.
456         _self.state = _self.STATE_DRAGGING;
457         gui.statusShow('selectionDrag');
458         break;
459 
460       case 'border':
461         // 'border' for resize (the user is clicking on the borders).
462         _self.state = _self.STATE_RESIZING;
463         gui.statusShow('selectionResize');
464     }
465 
466     // Temporarily toggle the transformation mode if the user holds the Control 
467     // key down.
468     if (ev.ctrlKey) {
469       config.transform = !config.transform;
470     }
471 
472     // If there's any ImageData currently in memory, which was "cut" out from 
473     // the current layer, then put it back on the layer. This needs to be done 
474     // only when the selection.transform mode is not active - that's when the 
475     // drag/resize operation only changes the selection, not the pixels 
476     // themselves.
477     if (sel.layerCleared && !config.transform) {
478       selectionMergeStrict();
479 
480     } else if (!sel.layerCleared && config.transform) {
481       // When the user starts dragging/resizing the ImageData we must cut out 
482       // the current selection from the image layer.
483       selectionBufferInit();
484     }
485 
486     return true;
487   };
488 
489   /**
490    * The <code>mousemove</code> event handler.
491    *
492    * @param {Event} ev The DOM Event object.
493    */
494   this.mousemove = function (ev) {
495     shiftKey = ev.shiftKey;
496     needsRedraw = true;
497   };
498 
499   /**
500    * The timer function. When the mouse button is down, this method performs the 
501    * dragging/resizing operation. When the mouse button is not down, this method 
502    * simply tracks the mouse location for the purpose of determining the area 
503    * being pointed at: the selection, the borders, or if the mouse is outside 
504    * the selection.
505    * @private
506    */
507   function timerFn () {
508     if (!needsRedraw) {
509       return;
510     }
511 
512     switch (_self.state) {
513       case _self.STATE_PENDING:
514         // selection dropped, switch to draw selection
515         _self.state = _self.STATE_DRAWING;
516         marqueeStyle.display = '';
517         gui.statusShow('selectionDraw');
518 
519       case _self.STATE_DRAWING:
520         selectionDraw();
521         break;
522 
523       case _self.STATE_SELECTED:
524         mouseAreaUpdate();
525         break;
526 
527       case _self.STATE_DRAGGING:
528         selectionDrag();
529         break;
530 
531       case _self.STATE_RESIZING:
532         selectionResize();
533     }
534 
535     needsRedraw = false;
536   };
537 
538   /**
539    * The <code>mouseup</code> event handler. This method ends any selection 
540    * operation.
541    *
542    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
543    * application event when the selection state is changed or when the selection 
544    * size/location is updated.
545    *
546    * @param {Event} ev The DOM Event object.
547    */
548   this.mouseup = function (ev) {
549     // Allow click+mousemove+click, not only mousedown+move+up
550     if (_self.state !== _self.STATE_PENDING &&
551         mouse.x === x0 && mouse.y === y0) {
552       return true;
553     }
554 
555     needsRedraw = false;
556 
557     shiftKey = ev.shiftKey;
558     if (ctrlKey) {
559       config.transform = !config.transform;
560     }
561 
562     if (_self.state === _self.STATE_PENDING) {
563       // Selection dropped? If yes, switch to the no selection state.
564       _self.state = _self.STATE_NONE;
565       app.events.dispatch(new appEvent.selectionChange(_self.state));
566 
567       return true;
568 
569     } else if (!lastSel) {
570       _self.state = _self.STATE_NONE;
571       marqueeHide();
572       gui.statusShow('selectionActive');
573       app.events.dispatch(new appEvent.selectionChange(_self.state));
574 
575       return true;
576     }
577 
578     sel.x = lastSel.x;
579     sel.y = lastSel.y;
580 
581     if ('width' in lastSel) {
582       sel.width  = lastSel.width;
583       sel.height = lastSel.height;
584     }
585 
586     _self.state = _self.STATE_SELECTED;
587 
588     app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 
589           sel.width, sel.height));
590 
591     gui.statusShow('selectionAvailable');
592 
593     return true;
594   };
595 
596   /**
597    * The <code>mousedown</code> event handler for the selection marquee element.
598    *
599    * @private
600    * @param {Event} ev The DOM Event object.
601    */
602   function marqueeMousedown (ev) {
603     if (mouse.buttonDown) {
604       return;
605     }
606     mouse.buttonDown = true;
607 
608     ev.preventDefault();
609 
610     _self.mousedown(ev);
611   };
612 
613   /**
614    * The <code>mousemove</code> event handler for the selection marquee element.
615    *
616    * @private
617    * @param {Event} ev The DOM Event object.
618    */
619   function marqueeMousemove (ev) {
620     if ('layerX' in ev) {
621       mouse.x = MathRound((this.offsetLeft + ev.layerX) / image.canvasScale);
622       mouse.y = MathRound((this.offsetTop  + ev.layerY) / image.canvasScale);
623     } else if ('offsetX' in ev) {
624       mouse.x = MathRound((this.offsetLeft + ev.offsetX) / image.canvasScale);
625       mouse.y = MathRound((this.offsetTop  + ev.offsetY) / image.canvasScale);
626     }
627 
628     shiftKey = ev.shiftKey;
629     needsRedraw = true;
630   };
631 
632   /**
633    * The <code>mouseup</code> event handler for the selection marquee element.
634    *
635    * @private
636    * @param {Event} ev The DOM Event object.
637    */
638   function marqueeMouseup (ev) {
639     if (!mouse.buttonDown) {
640       return;
641     }
642     mouse.buttonDown = false;
643 
644     ev.preventDefault();
645 
646     _self.mouseup(ev);
647   };
648 
649   /**
650    * Hide the selection marquee element.
651    * @private
652    */
653   function marqueeHide () {
654     marqueeStyle.display = 'none';
655     marqueeStyle.top     = '-' + (borderDouble + 50) + 'px';
656     marqueeStyle.left    = '-' + (borderDouble + 50) + 'px';
657     marqueeStyle.width   = '1px';
658     marqueeStyle.height  = '1px';
659     marqueeStyle.cursor  = '';
660   };
661 
662   /**
663    * Perform the selection rectangle drawing operation.
664    *
665    * @private
666    */
667   function selectionDraw () {
668     var x = MathMin(mouse.x,  x0),
669         y = MathMin(mouse.y,  y0),
670         w = MathAbs(mouse.x - x0),
671         h = MathAbs(mouse.y - y0);
672 
673     // Constrain the shape to a square.
674     if (shiftKey) {
675       if (w > h) {
676         if (y === mouse.y) {
677           y -= w-h;
678         }
679         h = w;
680       } else {
681         if (x === mouse.x) {
682           x -= h-w;
683         }
684         w = h;
685       }
686     }
687 
688     var mw = w * image.canvasScale - borderDouble,
689         mh = h * image.canvasScale - borderDouble;
690 
691     if (mw < 1 || mh < 1) {
692       lastSel = null;
693       return;
694     }
695 
696     marqueeStyle.top    = (y * image.canvasScale) + 'px';
697     marqueeStyle.left   = (x * image.canvasScale) + 'px';
698     marqueeStyle.width  = mw + 'px';
699     marqueeStyle.height = mh + 'px';
700 
701     lastSel = {'x': x, 'y': y, 'width': w, 'height': h};
702   };
703 
704   /**
705    * Perform the selection drag operation.
706    *
707    * @private
708    *
709    * @returns {false|Array} False is returned if the selection is too small, 
710    * otherwise an array of two elements is returned. The array holds the 
711    * selection coordinates, x and y.
712    */
713   function selectionDrag () {
714     // Snapping on the X/Y axis
715     if (shiftKey) {
716       snapXY(x0, y0);
717     }
718 
719     var x = sel.x + mouse.x - x0,
720         y = sel.y + mouse.y - y0;
721 
722     // Dragging the ImageData
723     if (config.transform) {
724       bufferContext.clearRect(0, 0, image.width, image.height);
725 
726       if (!config.transparent) {
727         bufferContext.fillRect(x, y, sel.width, sel.height);
728       }
729 
730       // Parameters:
731       // source image, dest x, dest y, dest width, dest height
732       bufferContext.drawImage(sel.canvas, x, y, sel.width, sel.height);
733     }
734 
735     marqueeStyle.top  = (y * image.canvasScale) + 'px';
736     marqueeStyle.left = (x * image.canvasScale) + 'px';
737 
738     lastSel = {'x': x, 'y': y};
739   };
740 
741   /**
742    * Perform the selection resize operation.
743    *
744    * @private
745    *
746    * @returns {false|Array} False is returned if the selection is too small, 
747    * otherwise an array of four elements is returned. The array holds the 
748    * selection information: x, y, width and height.
749    */
750   function selectionResize () {
751     var diffx = mouse.x - x0,
752         diffy = mouse.y - y0,
753         x     = sel.x,
754         y     = sel.y,
755         w     = sel.width,
756         h     = sel.height;
757 
758     switch (mouseResize) {
759       case 'nw':
760         x += diffx;
761         y += diffy;
762         w -= diffx;
763         h -= diffy;
764         break;
765       case 'n':
766         y += diffy;
767         h -= diffy;
768         break;
769       case 'ne':
770         y += diffy;
771         w += diffx;
772         h -= diffy;
773         break;
774       case 'e':
775         w += diffx;
776         break;
777       case 'se':
778         w += diffx;
779         h += diffy;
780         break;
781       case 's':
782         h += diffy;
783         break;
784       case 'sw':
785         x += diffx;
786         w -= diffx;
787         h += diffy;
788         break;
789       case 'w':
790         x += diffx;
791         w -= diffx;
792         break;
793       default:
794         lastSel = null;
795         return;
796     }
797 
798     if (!w || !h) {
799       lastSel = null;
800       return;
801     }
802 
803     // Constrain the rectangle to have the same aspect ratio as the initial 
804     // rectangle.
805     if (shiftKey) {
806       var p  = sel.width / sel.height,
807           w2 = w,
808           h2 = h;
809 
810       switch (mouseResize.charAt(0)) {
811         case 'n':
812         case 's':
813           w2 = MathRound(h*p);
814           break;
815         default:
816           h2 = MathRound(w/p);
817       }
818 
819       switch (mouseResize) {
820         case 'nw':
821         case 'sw':
822           x -= w2 - w;
823           y -= h2 - h;
824       }
825 
826       w = w2;
827       h = h2;
828     }
829 
830     if (w < 0) {
831       x += w;
832       w *= -1;
833     }
834     if (h < 0) {
835       y += h;
836       h *= -1;
837     }
838 
839     var mw   = w * image.canvasScale - borderDouble,
840         mh   = h * image.canvasScale - borderDouble;
841 
842     if (mw < 1 || mh < 1) {
843       lastSel = null;
844       return;
845     }
846 
847     // Resizing the ImageData
848     if (config.transform) {
849       bufferContext.clearRect(0, 0, image.width, image.height);
850 
851       if (!config.transparent) {
852         bufferContext.fillRect(x, y, w, h);
853       }
854 
855       // Parameters:
856       // source image, dest x, dest y, dest width, dest height
857       bufferContext.drawImage(sel.canvas, x, y, w, h);
858     }
859 
860     marqueeStyle.top    = (y * image.canvasScale) + 'px';
861     marqueeStyle.left   = (x * image.canvasScale) + 'px';
862     marqueeStyle.width  = mw + 'px';
863     marqueeStyle.height = mh + 'px';
864 
865     lastSel = {'x': x, 'y': y, 'width': w, 'height': h};
866   };
867 
868   /**
869    * Determine the are where the mouse is located: if it is inside or outside of 
870    * the selection rectangle, or on the selection border.
871    * @private
872    */
873   function mouseAreaUpdate () {
874     var border = config.borderWidth / image.canvasScale,
875         cursor = '',
876         x1_out = sel.x + sel.width,
877         y1_out = sel.y + sel.height,
878         x1_in  = x1_out - border,
879         y1_in  = y1_out - border,
880         x0_out = sel.x,
881         y0_out = sel.y,
882         x0_in  = sel.x + border,
883         y0_in  = sel.y + border;
884 
885     mouseArea = 'out';
886 
887     // Inside the rectangle
888     if (mouse.x < x1_in && mouse.y < y1_in &&
889         mouse.x > x0_in && mouse.y > y0_in) {
890       cursor = 'move';
891       mouseArea = 'in';
892 
893     } else {
894       // On one of the borders (north/south)
895       if (mouse.x >= x0_out && mouse.x <= x1_out &&
896           mouse.y >= y0_out && mouse.y <= y0_in) {
897         cursor = 'n';
898 
899       } else if (mouse.x >= x0_out && mouse.x <= x1_out &&
900                  mouse.y >= y1_in  && mouse.y <= y1_out) {
901         cursor = 's';
902       }
903 
904       // West/east
905       if (mouse.y >= y0_out && mouse.y <= y1_out &&
906           mouse.x >= x0_out && mouse.x <= x0_in) {
907         cursor += 'w';
908 
909       } else if (mouse.y >= y0_out && mouse.y <= y1_out &&
910                  mouse.x >= x1_in  && mouse.x <= x1_out) {
911         cursor += 'e';
912       }
913 
914       if (cursor !== '') {
915         mouseResize = cursor;
916         cursor += '-resize';
917         mouseArea = 'border';
918       }
919     }
920 
921     // Due to bug 126457 Opera will not automatically update the cursor, 
922     // therefore they will not see any visual feedback.
923     if (cursor !== marqueeStyle.cursor) {
924       marqueeStyle.cursor = cursor;
925     }
926   };
927 
928   /**
929    * The <code>canvasSizeChange</code> application event handler. This method 
930    * makes sure the selection size stays in sync.
931    *
932    * @private
933    * @param {pwlib.appEvent.canvasSizeChange} ev The application event object.
934    */
935   function ev_canvasSizeChange (ev) {
936     if (_self.state !== _self.STATE_SELECTED) {
937       return;
938     }
939 
940     marqueeStyle.top    = (sel.y      * ev.scale) + 'px';
941     marqueeStyle.left   = (sel.x      * ev.scale) + 'px';
942     marqueeStyle.width  = (sel.width  * ev.scale - borderDouble) + 'px';
943     marqueeStyle.height = (sel.height * ev.scale - borderDouble) + 'px';
944   };
945 
946   /**
947    * The <code>configChange</code> application event handler. This method makes 
948    * sure that changes to the selection transparency configuration option are 
949    * applied.
950    *
951    * @private
952    * @param {pwlib.appEvent.configChange} ev The application event object.
953    */
954   function ev_configChange (ev) {
955     // Continue only if the selection rectangle is available.
956     if (ev.group !== 'selection' || ev.config !== 'transparent' ||
957         !config.transform || _self.state !== _self.STATE_SELECTED) {
958       return;
959     }
960 
961     if (!sel.layerCleared) {
962       selectionBufferInit();
963     }
964 
965     bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
966 
967     if (!ev.value) {
968       bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height);
969     }
970 
971     // Draw the updated selection
972     bufferContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height);
973   };
974 
975   /**
976    * Initialize the selection buffer, when the user starts dragging or resizing 
977    * the selected pixels.
978    *
979    * @private
980    */
981   function selectionBufferInit () {
982     var x = sel.x,
983         y = sel.y,
984         w = sel.width,
985         h = sel.height,
986         sumX = sel.x + sel.width,
987         sumY = sel.y + sel.height,
988         dx = 0, dy = 0;
989 
990     sel.widthOriginal  = w;
991     sel.heightOriginal = h;
992 
993     if (x < 0) {
994       w += x;
995       dx -= x;
996       x = 0;
997     }
998     if (y < 0) {
999       h += y;
1000       dy -= y;
1001       y = 0;
1002     }
1003 
1004     if (sumX > image.width) {
1005       w = image.width - sel.x;
1006     }
1007     if (sumY > image.height) {
1008       h = image.height - sel.y;
1009     }
1010 
1011     if (!config.transparent) {
1012       bufferContext.fillRect(x, y, w, h);
1013     }
1014 
1015     // Parameters:
1016     // source image, src x, src y, src w, src h, dest x, dest y, dest w, dest h
1017     bufferContext.drawImage(layerCanvas, x, y, w, h, x, y, w, h);
1018 
1019     sel.canvas.width  = sel.widthOriginal;
1020     sel.canvas.height = sel.heightOriginal;
1021 
1022     // Also put the selected pixels into the selection buffer.
1023     sel.context.drawImage(layerCanvas, x, y, w, h, dx, dy, w, h);
1024 
1025     // Clear the selected pixels from the image
1026     layerContext.clearRect(x, y, w, h);
1027     sel.layerCleared = true;
1028 
1029     app.historyAdd();
1030   };
1031 
1032   /**
1033    * Perform the selection buffer merge onto the current image layer.
1034    * @private
1035    */
1036   function selectionMergeStrict () {
1037     if (!sel.layerCleared) {
1038       return;
1039     }
1040 
1041     if (!config.transparent) {
1042       layerContext.fillRect(sel.x, sel.y, sel.width, sel.height);
1043     }
1044 
1045     layerContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height);
1046     bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1047 
1048     sel.layerCleared  = false;
1049     sel.canvas.width  = 5;
1050     sel.canvas.height = 5;
1051 
1052     app.historyAdd();
1053   };
1054 
1055   /**
1056    * Merge the selection buffer onto the current image layer.
1057    *
1058    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
1059    * application event.
1060    *
1061    * @returns {Boolean} True if the operation was successful, or false if not.
1062    */
1063   this.selectionMerge = function () {
1064     if (_self.state !== _self.STATE_SELECTED) {
1065       return false;
1066     }
1067 
1068     selectionMergeStrict();
1069 
1070     _self.state = _self.STATE_NONE;
1071     marqueeHide();
1072     gui.statusShow('selectionActive');
1073 
1074     app.events.dispatch(new appEvent.selectionChange(_self.state));
1075 
1076     return true;
1077   };
1078 
1079   /**
1080    * Select all the entire image.
1081    *
1082    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
1083    * application event.
1084    *
1085    * @returns {Boolean} True if the operation was successful, or false if not.
1086    */
1087   this.selectAll = function () {
1088     if (_self.state !== _self.STATE_NONE && _self.state !== 
1089         _self.STATE_SELECTED) {
1090       return false;
1091     }
1092 
1093     if (_self.state === _self.STATE_SELECTED) {
1094       selectionMergeStrict();
1095     } else {
1096       _self.state = _self.STATE_SELECTED;
1097       marqueeStyle.display = '';
1098     }
1099 
1100     sel.x      = 0;
1101     sel.y      = 0;
1102     sel.width  = image.width;
1103     sel.height = image.height;
1104 
1105     marqueeStyle.top     = '0px';
1106     marqueeStyle.left    = '0px';
1107     marqueeStyle.width   = (sel.width*image.canvasScale  - borderDouble) + 'px';
1108     marqueeStyle.height  = (sel.height*image.canvasScale - borderDouble) + 'px';
1109 
1110     mouseAreaUpdate();
1111 
1112     app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 
1113           sel.width, sel.height));
1114 
1115     return true;
1116   };
1117 
1118   /**
1119    * Cut the selected pixels. The associated ImageData is stored in {@link 
1120    * PaintWeb#clipboard}.
1121    *
1122    * <p>This method dispatches two application events: {@link 
1123    * pwlib.appEvent.clipboardUpdate} and {@link pwlib.appEvent.selectionChange}.
1124    *
1125    * @returns {Boolean} True if the operation was successful, or false if not.
1126    */
1127   this.selectionCut = function () {
1128     if (!_self.selectionCopy()) {
1129       return false;
1130     }
1131 
1132     if (sel.layerCleared) {
1133       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1134 
1135       sel.canvas.width  = 5;
1136       sel.canvas.height = 5;
1137       sel.layerCleared = false;
1138 
1139     } else {
1140       layerContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1141       app.historyAdd();
1142     }
1143 
1144     _self.state = _self.STATE_NONE;
1145     marqueeHide();
1146 
1147     app.events.dispatch(new appEvent.selectionChange(_self.state));
1148     gui.statusShow('selectionActive');
1149 
1150     return true;
1151   };
1152 
1153   /**
1154    * Copy the selected pixels. The associated ImageData is stored in {@link 
1155    * PaintWeb#clipboard}.
1156    *
1157    * <p>This method dispatches the {@link pwlib.appEvent.clipboardUpdate} 
1158    * application event.
1159    *
1160    * @returns {Boolean} True if the operation was successful, or false if not.
1161    */
1162   this.selectionCopy = function () {
1163     if (_self.state !== _self.STATE_SELECTED) {
1164       return false;
1165     }
1166 
1167     if (!layerContext.getImageData || !layerContext.putImageData) {
1168       alert(lang.errorClipboardUnsupported);
1169       return false;
1170     }
1171 
1172     if (!sel.layerCleared) {
1173       var w    = sel.width,
1174           h    = sel.height,
1175           sumX = sel.width  + sel.x;
1176           sumY = sel.height + sel.y;
1177 
1178       if (sumX > image.width) {
1179         w = image.width - sel.x;
1180       }
1181       if (sumY > image.height) {
1182         h = image.height - sel.y;
1183       }
1184 
1185       try {
1186         app.clipboard = layerContext.getImageData(sel.x, sel.y, w, h);
1187       } catch (err) {
1188         alert(lang.failedSelectionCopy);
1189         return false;
1190       }
1191 
1192     } else {
1193       try {
1194         app.clipboard = sel.context.getImageData(0, 0, sel.widthOriginal, 
1195             sel.heightOriginal);
1196       } catch (err) {
1197         alert(lang.failedSelectionCopy);
1198         return false;
1199       }
1200     }
1201 
1202     app.events.dispatch(new appEvent.clipboardUpdate(app.clipboard));
1203 
1204     return true;
1205   };
1206 
1207   /**
1208    * Paste an image from the "clipboard". The {@link PaintWeb#clipboard} object 
1209    * must be an ImageData. This method will generate a new selection which will 
1210    * hold the pasted image.
1211    *
1212    * <p>The {@link pwlib.appEvent.selectionChange} application event is 
1213    * dispatched.
1214    *
1215    * <p>If the {@link PaintWeb.config.selection.transform} value is false, then 
1216    * it becomes true. The {@link pwlib.appEvent.configChange} application is 
1217    * then dispatched.
1218    *
1219    * @returns {Boolean} True if the operation was successful, or false if not.
1220    */
1221   this.clipboardPaste = function () {
1222     if (!app.clipboard || _self.state !== _self.STATE_NONE && _self.state !== 
1223         _self.STATE_SELECTED) {
1224       return false;
1225     }
1226 
1227     if (!layerContext.getImageData || !layerContext.putImageData) {
1228       alert(lang.errorClipboardUnsupported);
1229       return false;
1230     }
1231 
1232     // The default position for the pasted image is the top left corner of the 
1233     // visible area, taking into consideration the zoom level.
1234     var x = MathRound(gui.elems.viewport.scrollLeft / image.canvasScale),
1235         y = MathRound(gui.elems.viewport.scrollTop  / image.canvasScale),
1236         w = app.clipboard.width,
1237         h = app.clipboard.height;
1238 
1239     sel.canvas.width  = w;
1240     sel.canvas.height = h;
1241     sel.context.putImageData(app.clipboard, 0, 0);
1242 
1243     if (_self.state === _self.STATE_SELECTED) {
1244       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1245     } else {
1246       _self.state = _self.STATE_SELECTED;
1247     }
1248 
1249     if (!config.transparent) {
1250       bufferContext.fillRect(x, y, w, h);
1251     }
1252     bufferContext.drawImage(sel.canvas, x, y, w, h);
1253 
1254     sel.widthOriginal  = sel.width  = w;
1255     sel.heightOriginal = sel.height = h;
1256     sel.x = x;
1257     sel.y = y;
1258     sel.layerCleared = true;
1259 
1260     marqueeStyle.top     = (y * image.canvasScale) + 'px';
1261     marqueeStyle.left    = (x * image.canvasScale) + 'px';
1262     marqueeStyle.width   = (w * image.canvasScale - borderDouble) + 'px';
1263     marqueeStyle.height  = (h * image.canvasScale - borderDouble) + 'px';
1264     marqueeStyle.display = '';
1265 
1266     if (!config.transform) {
1267       config.transform = true;
1268       app.events.dispatch(new appEvent.configChange(true, false, 'transform', 
1269             'selection', config));
1270     }
1271 
1272     mouseAreaUpdate();
1273 
1274     app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, 
1275           sel.width, sel.height));
1276 
1277     gui.statusShow('selectionAvailable');
1278 
1279     return true;
1280   };
1281 
1282   /**
1283    * Perform selection delete.
1284    *
1285    * <p>This method changes the {@link PaintWeb.config.selection.transform} 
1286    * value to false if the current selection has pixels that are currently being 
1287    * manipulated. In such cases, the {@link pwlib.appEvent.configChange} 
1288    * application event is also dispatched.
1289    *
1290    * @returns {Boolean} True if the operation was successful, or false if not.
1291    */
1292   this.selectionDelete = function () {
1293     // Delete the pixels from the image if they are not deleted already.
1294     if (_self.state !== _self.STATE_SELECTED) {
1295       return false;
1296     }
1297 
1298     if (!sel.layerCleared) {
1299       layerContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1300       app.historyAdd();
1301 
1302     } else {
1303       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1304       sel.layerCleared  = false;
1305       sel.canvas.width  = 5;
1306       sel.canvas.height = 5;
1307 
1308       if (config.transform) {
1309         config.transform = false;
1310         app.events.dispatch(new appEvent.configChange(false, true, 'transform', 
1311               'selection', config));
1312       }
1313     }
1314 
1315     return true;
1316   };
1317 
1318   /**
1319    * Drop the current selection.
1320    *
1321    * <p>This method dispatches the {@link pwlib.appEvent.selectionChange} 
1322    * application event.
1323    *
1324    * @returns {Boolean} True if the operation was successful, or false if not.
1325    */
1326   this.selectionDrop = function () {
1327     if (_self.state !== _self.STATE_SELECTED) {
1328       return false;
1329     }
1330 
1331     if (sel.layerCleared) {
1332       bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height);
1333       sel.canvas.width  = 5;
1334       sel.canvas.height = 5;
1335       sel.layerCleared  = false;
1336     }
1337 
1338     _self.state = _self.STATE_NONE;
1339 
1340     marqueeHide();
1341     gui.statusShow('selectionActive');
1342 
1343     app.events.dispatch(new appEvent.selectionChange(_self.state));
1344 
1345     return true;
1346   };
1347 
1348   /**
1349    * Fill the available selection with the current 
1350    * <var>bufferContext.fillStyle</var>.
1351    *
1352    * @returns {Boolean} True if the operation was successful, or false if not.
1353    */
1354   this.selectionFill = function () {
1355     if (_self.state !== _self.STATE_SELECTED) {
1356       return false;
1357     }
1358 
1359     if (sel.layerCleared) {
1360       sel.context.fillStyle = bufferContext.fillStyle;
1361       sel.context.fillRect(0, 0, sel.widthOriginal, sel.heightOriginal);
1362       bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height);
1363 
1364     } else {
1365       layerContext.fillStyle = bufferContext.fillStyle;
1366       layerContext.fillRect(sel.x, sel.y, sel.width, sel.height);
1367       app.historyAdd();
1368     }
1369 
1370     return true;
1371   };
1372 
1373   /**
1374    * Crop the image to selection width and height. The selected pixels become 
1375    * the image itself.
1376    *
1377    * <p>This method invokes the {@link this#selectionMerge} and {@link 
1378    * PaintWeb#imageCrop} methods.
1379    *
1380    * @returns {Boolean} True if the operation was successful, or false if not.
1381    */
1382   this.selectionCrop = function () {
1383     if (_self.state !== _self.STATE_SELECTED) {
1384       return false;
1385     }
1386 
1387     _self.selectionMerge();
1388 
1389     var w    = sel.width,
1390         h    = sel.height,
1391         sumX = sel.x + w,
1392         sumY = sel.y + h;
1393 
1394     if (sumX > image.width) {
1395       w -= sumX - image.width;
1396     }
1397     if (sumY > image.height) {
1398       h -= sumY - image.height;
1399     }
1400 
1401     app.imageCrop(sel.x, sel.y, w, h);
1402 
1403     return true;
1404   };
1405 
1406   /**
1407    * The <code>keydown</code> event handler. This method calls selection-related 
1408    * commands associated to keyboard shortcuts.
1409    *
1410    * @param {Event} ev The DOM Event object.
1411    *
1412    * @returns {Boolean} True if the keyboard shortcut was recognized, or false 
1413    * if not.
1414    *
1415    * @see PaintWeb.config.selection.keys holds the keyboard shortcuts 
1416    * configuration.
1417    */
1418   this.keydown = function (ev) {
1419     switch (ev.kid_) {
1420       case config.keys.transformToggle:
1421         // Toggle the selection transformation mode.
1422         config.transform = !config.transform;
1423         app.events.dispatch(new appEvent.configChange(config.transform, 
1424               !config.transform, 'transform', 'selection', config));
1425         break;
1426 
1427       case config.keys.selectionCrop:
1428         return _self.selectionCrop(ev);
1429 
1430       case config.keys.selectionDelete:
1431         return _self.selectionDelete(ev);
1432 
1433       case config.keys.selectionDrop:
1434         return _self.selectionDrop(ev);
1435 
1436       case config.keys.selectionFill:
1437         return _self.selectionFill(ev);
1438 
1439       default:
1440         return false;
1441     }
1442 
1443     return true;
1444   };
1445 };
1446 
1447 /**
1448  * @class Selection change event. This event is not cancelable.
1449  *
1450  * @augments pwlib.appEvent
1451  *
1452  * @param {Number} state Tells the new state of the selection.
1453  * @param {Number} [x] Selection start position on the x-axis of the image.
1454  * @param {Number} [y] Selection start position on the y-axis of the image.
1455  * @param {Number} [width] Selection width.
1456  * @param {Number} [height] Selection height.
1457  */
1458 pwlib.appEvent.selectionChange = function (state, x, y, width, height) {
1459   /**
1460    * No selection is available.
1461    * @constant
1462    */
1463   this.STATE_NONE = 0;
1464 
1465   /**
1466    * Selection available.
1467    * @constant
1468    */
1469   this.STATE_SELECTED = 2;
1470 
1471   /**
1472    * Selection state.
1473    * @type Number
1474    */
1475   this.state = state;
1476 
1477   /**
1478    * Selection location on the x-axis of the image.
1479    * @type Number
1480    */
1481   this.x = x;
1482 
1483   /**
1484    * Selection location on the y-axis of the image.
1485    * @type Number
1486    */
1487   this.y = y;
1488 
1489   /**
1490    * Selection width.
1491    * @type Number
1492    */
1493   this.width  = width;
1494 
1495   /**
1496    * Selection height.
1497    * @type Number
1498    */
1499   this.height = height;
1500 
1501   pwlib.appEvent.call(this, 'selectionChange');
1502 };
1503 
1504 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
1505 
1506