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 15:37:38 +0300 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the color picker implementation.
 26  */
 27 
 28 /**
 29  * @class The color picker tool.
 30  *
 31  * @param {PaintWeb} app Reference to the main paint application object.
 32  */
 33 pwlib.tools.cpicker = function (app) {
 34   var _self        = this,
 35       colormixer   = app.extensions.colormixer,
 36       context      = app.layer.context,
 37       gui          = app.gui,
 38       lang         = app.lang,
 39       MathRound    = Math.round,
 40       mouse        = app.mouse;
 41 
 42   /**
 43    * Holds the ID of the previously active tool. Once the user completes the 
 44    * color picking operation, the previous tool is activated.
 45    *
 46    * @private
 47    * @type String
 48    */
 49   var prevTool = null;
 50 
 51   /**
 52    * Holds a reference to the target color input. This is a GUI color input 
 53    * component.
 54    *
 55    * @private
 56    * @type pwlib.guiColorInput
 57    */
 58   var targetInput = null;
 59 
 60   /**
 61    * Holds the previous color values - before the user started picking 
 62    * a different color.
 63    *
 64    * @private
 65    * @type Object
 66    */
 67   var prevColor = null;
 68 
 69   /**
 70    * Tells if the color mixer is active for the current target input.
 71    *
 72    * @private
 73    * @type Boolean
 74    */
 75   var colormixerActive = false;
 76 
 77   /**
 78    * Tells if the current color values are accepted by the user. This value is 
 79    * used by the tool deactivation code.
 80    *
 81    * @private
 82    * @type Boolean
 83    */
 84   var colorAccepted = false;
 85 
 86   /**
 87    * The <code>preActivate</code> event handler. This method checks if the 
 88    * browser implements the <code>getImageData()</code> context method. If not, 
 89    * the color picker tool cannot be used.
 90    */
 91   this.preActivate = function () {
 92     // The latest versions of all browsers which implement Canvas, also 
 93     // implement the getImageData() method. This was only a problem with some 
 94     // old versions (eg. Opera 9.2).
 95     if (!context.getImageData) {
 96       alert(lang.errorCpickerUnsupported);
 97       return false;
 98     }
 99 
100     if (app.tool && app.tool._id) {
101       prevTool = app.tool._id;
102     }
103 
104     return true;
105   };
106 
107   /**
108    * The <code>activate</code> event handler. This method determines the current 
109    * target input in the Color Mixer, if any. Canvas shadow rendering is 
110    * disallowed.
111    */
112   this.activate = function () {
113     // When the color mixer panel is active, the color picker uses the same 
114     // target input.
115     if (colormixer && colormixer.targetInput) {
116       targetInput = gui.colorInputs[colormixer.targetInput.id];
117     }
118 
119     if (targetInput) {
120       gui.statusShow('cpicker_' + targetInput.id);
121     } else {
122       gui.statusShow('cpickerNormal');
123     }
124 
125     app.shadowDisallow();
126   };
127 
128   /**
129    * The <code>deactivate</code> event handler. This method allows shadow 
130    * rendering again, and resets the color input values if the user did not 
131    * accept the new color.
132    */
133   this.deactivate = function () {
134     if (!colorAccepted && targetInput && prevColor) {
135       updateColor(null, true);
136     }
137 
138     app.shadowAllow();
139   };
140 
141   /**
142    * The <code>mousedown</code> event handler. This method starts the color 
143    * picking operation.
144    *
145    * @param {Event} ev The DOM Event object.
146    */
147   this.mousedown = function (ev) {
148     // We check again, because the user might have opened/closed the color 
149     // mixer.
150     if (colormixer && colormixer.targetInput) {
151       targetInput = gui.colorInputs[colormixer.targetInput.id];
152     }
153 
154     if (targetInput) {
155       colormixerActive = true;
156       gui.statusShow('cpicker_' + targetInput.id);
157     } else {
158       colormixerActive = false;
159       gui.statusShow('cpickerNormal');
160 
161       // The context menu (right-click). This is unsupported by Opera.
162       // Also allow Shift+Click for changing the stroke color (making it easier for Opera users).
163       if (ev.button === 2 || ev.shiftKey) {
164         targetInput = gui.colorInputs.strokeStyle;
165       } else {
166         targetInput = gui.colorInputs.fillStyle;
167       }
168     }
169 
170     updatePrevColor();
171 
172     _self.mousemove = updateColor;
173     updateColor(ev);
174 
175     return true;
176   };
177 
178   /**
179    * Perform color update. This function updates the target input or the Color 
180    * Mixer to hold the color value under the mouse - it actually performs the 
181    * color picking operation.
182    *
183    * <p>This function is also the <code>mousemove</code> event handler for this 
184    * tool.
185    *
186    * @param {Event} ev The DOM Event object.
187    * @param {Boolean} [usePrevColor=false] Tells the function to use the 
188    * previous color values we have stored. This is used when the user cancels 
189    * the color picking operation.
190    */
191   function updateColor (ev, usePrevColor) {
192     if (!targetInput) {
193       return;
194     }
195 
196     var p = usePrevColor ? prevColor :
197               context.getImageData(mouse.x, mouse.y, 1, 1),
198         color = {
199           red:    p.data[0] / 255,
200           green:  p.data[1] / 255,
201           blue:   p.data[2] / 255,
202           alpha: (p.data[3] / 255).toFixed(3)
203         };
204 
205     if (colormixerActive) {
206       colormixer.color.red   = color.red;
207       colormixer.color.green = color.green;
208       colormixer.color.blue  = color.blue;
209       colormixer.color.alpha = color.alpha;
210       colormixer.update_color('rgb');
211 
212     } else {
213       targetInput.updateColor(color);
214     }
215   };
216 
217   /**
218    * The <code>mouseup</code> event handler. This method completes the color 
219    * picking operation, and activates the previous tool.
220    *
221    * <p>The {@link pwlib.appEvent.configChange} application event is also 
222    * dispatched for the configuration property associated to the target input.
223    *
224    * @param {Event} ev The DOM Event object.
225    */
226   this.mouseup = function (ev) {
227     if (!targetInput) {
228       return false;
229     }
230 
231     delete _self.mousemove;
232     updateColor(ev);
233     colorAccepted = true;
234 
235     if (!colormixerActive) {
236       var color = targetInput.color,
237           configProperty = targetInput.configProperty,
238           configGroup    = targetInput.configGroup,
239           configGroupRef = targetInput.configGroupRef,
240           prevVal = configGroupRef[configProperty],
241           newVal  = 'rgba(' + MathRound(color.red   * 255) + ',' +
242                               MathRound(color.green * 255) + ',' +
243                               MathRound(color.blue  * 255) + ',' +
244                               color.alpha + ')';
245 
246       if (prevVal !== newVal) {
247         configGroupRef[configProperty] = newVal;
248         app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal, 
249             configProperty, configGroup, configGroupRef));
250       }
251     }
252 
253     if (prevTool) {
254       app.toolActivate(prevTool, ev);
255     }
256 
257     return true;
258   };
259 
260   /**
261    * The <code>keydown</code> event handler. This method allows the user to 
262    * press the <kbd>Escape</kbd> key to cancel the color picking operation. By 
263    * doing so, the original color values are restored.
264    *
265    * @param {Event} ev The DOM Event object.
266    * @returns {Boolean} True if the keyboard shortcut was recognized, or false 
267    * if not.
268    */
269   this.keydown = function (ev) {
270     if (!prevTool || ev.kid_ !== 'Escape') {
271       return false;
272     }
273 
274     mouse.buttonDown = false;
275     app.toolActivate(prevTool, ev);
276 
277     return true;
278   };
279 
280   /**
281    * The <code>contextmenu</code> event handler. This method only cancels the 
282    * context menu.
283    */
284   // Unfortunately, the contextmenu event is unsupported by Opera.
285   this.contextmenu = function () {
286     return true;
287   };
288 
289   /**
290    * Store the color values from the target color input, before this tool 
291    * changes the colors. The previous color values are used when the user 
292    * decides to cancel the color picking operation.
293    * @private
294    */
295   function updatePrevColor () {
296     // If the color mixer panel is visible, then we store the color values from 
297     // the color mixer, instead of those from the color input object.
298     var color = colormixerActive ? colormixer.color : targetInput.color;
299 
300     prevColor = {
301       width: 1,
302       height: 1,
303       data: [
304         MathRound(color.red   * 255),
305         MathRound(color.green * 255),
306         MathRound(color.blue  * 255),
307         color.alpha * 255
308       ]
309     };
310   };
311 };
312 
313 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
314 
315