|
1 # Copyright (c) 2009 Nokia Corporation |
|
2 # |
|
3 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
4 # you may not use this file except in compliance with the License. |
|
5 # You may obtain a copy of the License at |
|
6 # |
|
7 # http://www.apache.org/licenses/LICENSE-2.0 |
|
8 # |
|
9 # Unless required by applicable law or agreed to in writing, software |
|
10 # distributed under the License is distributed on an "AS IS" BASIS, |
|
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
12 # See the License for the specific language governing permissions and |
|
13 # limitations under the License. |
|
14 |
|
15 import appuifw |
|
16 import e32 |
|
17 import os |
|
18 import time |
|
19 import operator |
|
20 from graphics import * |
|
21 from key_codes import * |
|
22 |
|
23 if not appuifw.touch_enabled(): |
|
24 appuifw.note(u"This application only works on devices that support " + |
|
25 u"touch input") |
|
26 |
|
27 |
|
28 class PaintApp(): |
|
29 BG_COLOR = 0xffffff # white |
|
30 BORDER_COLOR = 0x000000 # black |
|
31 BRUSH_COLOR = 0x000000 # black |
|
32 ERASER_ACTIVE = 0xFF0000 # Red |
|
33 |
|
34 def __init__(self): |
|
35 appuifw.app.exit_key_handler = self.quit |
|
36 self.running = True |
|
37 appuifw.app.directional_pad = False |
|
38 self.erase_mode = False |
|
39 self.saving_file = False |
|
40 self.orientation_changed = False |
|
41 self.bind_palette = True |
|
42 self.about_active = False |
|
43 self.is_about_active = False |
|
44 self.about_timer = None |
|
45 self.pen_width = 4 |
|
46 self.x_max = 0 |
|
47 self.y_max = 0 |
|
48 self.canvas = appuifw.Canvas(event_callback=self.event_callback, |
|
49 redraw_callback=self.redraw_callback) |
|
50 if self.canvas.size[0] > self.canvas.size[1]: |
|
51 self.orientation = 'landscape' |
|
52 else: |
|
53 self.orientation = 'portrait' |
|
54 self.drive = unicode(os.getcwd()[0]) |
|
55 self.old_body = appuifw.app.body |
|
56 appuifw.app.body = self.canvas |
|
57 appuifw.app.screen = 'full' |
|
58 appuifw.app.focus = self.focus_monitor |
|
59 self.canvas.clear() |
|
60 self.draw = self.canvas |
|
61 self.draw_img = Image.new((self.canvas.size[0], self.canvas.size[1])) |
|
62 self.draw_buttons() |
|
63 self.bind_buttons() |
|
64 |
|
65 def draw_buttons(self): |
|
66 self.x_max = self.canvas.size[0] |
|
67 self.y_max = self.canvas.size[1] |
|
68 self.pointer_advance = min(self.x_max, self.y_max) / 4 |
|
69 self.toolbar_size = max(self.x_max, self.y_max) / 4 |
|
70 if self.orientation == 'landscape': |
|
71 box_width = self.pointer_advance - 10 |
|
72 self.menu_bar_size = self.toolbar_size / 2 |
|
73 self.color_palette = self.toolbar_size - self.menu_bar_size |
|
74 draw_position = self.x_max - self.menu_bar_size |
|
75 y_displacement = (self.menu_bar_size / 2) + 10 |
|
76 self.options_button = ((draw_position, 0), |
|
77 (self.x_max, self.pointer_advance)) |
|
78 self.clear_button = ((draw_position, self.pointer_advance), |
|
79 (self.x_max, 2 * self.pointer_advance)) |
|
80 self.eraser = ((draw_position, 2 * self.pointer_advance), |
|
81 (self.x_max, 3 * self.pointer_advance)) |
|
82 self.quit_button = ((draw_position, 3 * self.pointer_advance), |
|
83 (self.x_max, 4 * self.pointer_advance)) |
|
84 self.draw.rectangle((draw_position, 0, self.x_max, self.y_max), |
|
85 fill=self.BG_COLOR) |
|
86 self.draw.rectangle((self.x_max - self.toolbar_size, 0, |
|
87 self.x_max - self.menu_bar_size, self.y_max), |
|
88 outline=self.BORDER_COLOR, width=5) |
|
89 else: |
|
90 box_width = self.pointer_advance |
|
91 self.menu_bar_size = self.toolbar_size / 4 |
|
92 self.color_palette = self.toolbar_size - self.menu_bar_size |
|
93 draw_position = self.y_max - self.menu_bar_size |
|
94 y_displacement = (self.menu_bar_size / 2) + 5 |
|
95 self.options_button = ((0, draw_position), |
|
96 (self.pointer_advance, self.y_max)) |
|
97 self.clear_button = ((self.pointer_advance, draw_position), |
|
98 (2 * self.pointer_advance, self.y_max)) |
|
99 self.eraser = ((self.pointer_advance * 2, draw_position), |
|
100 (self.pointer_advance * 3, self.y_max)) |
|
101 self.quit_button = ((self.pointer_advance * 3, draw_position), |
|
102 (self.pointer_advance * 4, self.y_max)) |
|
103 self.draw.rectangle((0, draw_position, self.x_max, self.y_max), |
|
104 fill=self.BG_COLOR) |
|
105 self.draw.rectangle((0, self.y_max - self.toolbar_size, |
|
106 self.x_max, self.y_max - self.menu_bar_size), |
|
107 outline=self.BORDER_COLOR, width=5) |
|
108 # Draw the buttons at the bottom and the respective text at an offset |
|
109 # specified by x_displacement and y_displacement |
|
110 buttons = [self.options_button, self.clear_button, self.eraser, |
|
111 self.quit_button] |
|
112 options = [u'Options', u'Clear', u'Erase', u'Quit'] |
|
113 for button, text in zip(buttons, options): |
|
114 if self.erase_mode and text == u'Erase': |
|
115 self.draw.rectangle(self.eraser, fill=self.ERASER_ACTIVE, |
|
116 outline=self.BORDER_COLOR, width=5) |
|
117 else: |
|
118 self.draw.rectangle(button, outline=self.BORDER_COLOR, width=5) |
|
119 text_dimensions = self.draw.measure_text(text) |
|
120 x_displacement = (box_width - text_dimensions[0][2]) / 2 |
|
121 self.draw.text((button[0][0] + x_displacement, |
|
122 button[0][1] + y_displacement), text, |
|
123 font=u'Sans MT TC Big5HK_S60C', |
|
124 fill=self.BORDER_COLOR) |
|
125 self.draw_palette() |
|
126 |
|
127 def bind_buttons(self): |
|
128 self.canvas.bind(EButton1Down, self.reset_canvas, self.clear_button) |
|
129 self.canvas.bind(EButton1Down, self.options_callback, |
|
130 self.options_button) |
|
131 self.canvas.bind(EButton1Down, self.set_exit, self.quit_button) |
|
132 self.canvas.bind(EButton1Down, self.eraser_callback, self.eraser) |
|
133 |
|
134 def clear_button_bindings(self): |
|
135 self.canvas.bind(EButton1Down, None, self.clear_button) |
|
136 self.canvas.bind(EButton1Down, None, self.options_button) |
|
137 self.canvas.bind(EButton1Down, None, self.quit_button) |
|
138 self.canvas.bind(EButton1Down, None, self.eraser) |
|
139 self.canvas.bind(EButton1Down, None) |
|
140 |
|
141 def focus_monitor(self, value): |
|
142 if value: |
|
143 self.canvas.blit(self.draw_img) |
|
144 self.draw_buttons() |
|
145 |
|
146 def set_exit(self, pos): |
|
147 appuifw.app.body = self.old_body |
|
148 self.canvas.bind(EButton1Down, None) |
|
149 self.canvas = None |
|
150 self.draw_img = None |
|
151 appuifw.app.focus = None |
|
152 self.running = False |
|
153 |
|
154 def options_callback(self, pos): |
|
155 option = appuifw.popup_menu([u'Save', u'Point/Line Width', u'About'], |
|
156 u'Options') |
|
157 pen_width_options = [u'1', u'2', u'3', u'4', u'5', u'6'] |
|
158 if option == 0: |
|
159 self.save_callback() |
|
160 elif option == 1: |
|
161 pen_width_choice = appuifw.popup_menu(pen_width_options) |
|
162 if pen_width_choice is not None: |
|
163 self.pen_width = int(pen_width_options[pen_width_choice]) |
|
164 elif option == 2: |
|
165 self.is_about_active = True |
|
166 self.show_about() |
|
167 return |
|
168 self.canvas.blit(self.draw_img) |
|
169 self.draw_buttons() |
|
170 |
|
171 def show_about(self): |
|
172 img_path = self.drive + u':\\data\\python\\about.png' |
|
173 if self.orientation == 'landscape' or not os.path.exists(img_path): |
|
174 appuifw.note(u"Scribble is Copyright (c) 2009 Nokia Corporation") |
|
175 self.canvas.blit(self.draw_img) |
|
176 self.draw_buttons() |
|
177 else: |
|
178 self.about_window = Image.open(img_path) |
|
179 self.about_active = True |
|
180 self.clear_button_bindings() |
|
181 self.canvas.blit(self.about_window) |
|
182 self.canvas.bind(EButton1Up, self.clear_about_screen, ((0, 0), |
|
183 (self.x_max, self.y_max))) |
|
184 self.about_timer = e32.Ao_timer() |
|
185 self.about_timer.after(5, self.clear_about_screen) |
|
186 |
|
187 def clear_about_screen(self, pos=(0, 0)): |
|
188 if self.about_timer is not None: |
|
189 self.about_timer.cancel() |
|
190 self.about_timer = None |
|
191 self.canvas.bind(EButton1Up, None, ((0, 0), (self.x_max, self.y_max))) |
|
192 self.canvas.blit(self.draw_img) |
|
193 self.bind_palette = True |
|
194 self.draw_buttons() |
|
195 self.bind_buttons() |
|
196 self.about_active = False |
|
197 |
|
198 def eraser_callback(self, pos): |
|
199 # The pen_width and fill_color change in event_callback when erase_mode |
|
200 # changes |
|
201 self.erase_mode = not self.erase_mode |
|
202 self.draw_buttons() |
|
203 |
|
204 def save_callback(self): |
|
205 if not self.saving_file: |
|
206 self.saving_file = True |
|
207 save_dir = self.drive + u":\\data\\python\\" |
|
208 if not os.path.exists(save_dir): |
|
209 os.mkdir(save_dir) |
|
210 filename = save_dir + \ |
|
211 unicode(time.strftime("%d%m%Y%H%M%S", time.localtime())) + \ |
|
212 u".jpg" |
|
213 self.draw_img.save(filename, quality=100) |
|
214 appuifw.note(u"Saved :" + unicode(filename)) |
|
215 self.canvas.blit(self.draw_img) |
|
216 self.saving_file = False |
|
217 self.draw_buttons() |
|
218 |
|
219 def set_brush_color(self, pos, color): |
|
220 self.BRUSH_COLOR = color |
|
221 |
|
222 def draw_and_bind_color(self, color): |
|
223 if self.orientation == 'portrait': |
|
224 self.top_left_x = (self.no_of_colors % (len(self.colors) / 2)) * \ |
|
225 self.color_box_width |
|
226 self.bottom_right_x = self.top_left_x + self.color_box_width |
|
227 self.top_left_y = self.y_max - self.toolbar_size |
|
228 self.bottom_right_y = self.y_max - self.menu_bar_size - \ |
|
229 self.color_palette / 2 |
|
230 if self.no_of_colors >= (len(self.colors) / 2): |
|
231 self.top_left_y = self.bottom_right_y |
|
232 self.bottom_right_y = self.y_max - self.menu_bar_size |
|
233 else: |
|
234 self.top_left_x = self.x_max - self.toolbar_size |
|
235 self.bottom_right_x = self.x_max - self.menu_bar_size - \ |
|
236 self.color_palette / 2 |
|
237 if self.no_of_colors >= (len(self.colors) / 2): |
|
238 self.top_left_x = self.bottom_right_x |
|
239 self.bottom_right_x = self.x_max - self.menu_bar_size |
|
240 self.top_left_y = (self.no_of_colors % (len(self.colors) / 2)) * \ |
|
241 self.color_box_width |
|
242 self.bottom_right_y = self.top_left_y + self.color_box_width |
|
243 |
|
244 self.top_left = (self.top_left_x, self.top_left_y) |
|
245 self.bottom_right = (self.bottom_right_x, self.bottom_right_y) |
|
246 # Draw the color rectangle and bind a function which sets the brush |
|
247 # color |
|
248 self.draw.rectangle((self.top_left, self.bottom_right), |
|
249 fill=self.colors[color]) |
|
250 if self.bind_palette: |
|
251 self.canvas.bind(EButton1Down, |
|
252 lambda pos: self.set_brush_color(pos, self.colors[color]), |
|
253 (self.top_left, self.bottom_right)) |
|
254 self.no_of_colors += 1 |
|
255 |
|
256 def draw_palette(self): |
|
257 self.colors = {'Black': 0x000000, 'Blue': 0x0000FF, 'Brown': 0xA52A2A, |
|
258 'Gray': 0x808080, 'Green': 0x008000, 'Maroon': 0x800000, |
|
259 'Orange': 0xFFA500, 'Pink': 0xFFC0CB, 'Purple': 0x800080, |
|
260 'Silver': 0xC0C0C0, 'Violet': 0xEE82EE, 'Yellow': 0xFFFF00, |
|
261 'Red': 0xFF0000, 'Lime': 0x00FF00} |
|
262 self.color_box_width = min(self.x_max, self.y_max) / (len(self.colors) |
|
263 / 2) |
|
264 self.no_of_colors = 0 |
|
265 map(self.draw_and_bind_color, sorted(self.colors)) |
|
266 if self.bind_palette: |
|
267 self.bind_palette = False |
|
268 |
|
269 def reset_canvas(self, pos=(0, 0)): |
|
270 self.draw_img.clear(self.BG_COLOR) |
|
271 self.prev_x = 0 |
|
272 self.prev_y = 0 |
|
273 self.erase_mode = False |
|
274 self.canvas.clear(self.BG_COLOR) |
|
275 self.draw_buttons() |
|
276 |
|
277 def check_orientation(self): |
|
278 if not self.orientation_changed: |
|
279 self.orientation_changed = True |
|
280 else: |
|
281 self.orientation_changed = False |
|
282 self.x_max = self.canvas.size[0] |
|
283 self.y_max = self.canvas.size[1] |
|
284 |
|
285 def redraw_callback(self, rect): |
|
286 if self.about_active: |
|
287 self.canvas.blit(self.about_window) |
|
288 if rect == (0, 0, self.y_max, self.x_max) and \ |
|
289 self.orientation == 'portrait': |
|
290 self.orientation = 'landscape' |
|
291 self.check_orientation() |
|
292 elif rect == (0, 0, self.y_max, self.x_max) and \ |
|
293 self.orientation == 'landscape': |
|
294 self.orientation = 'portrait' |
|
295 self.check_orientation() |
|
296 |
|
297 def event_callback(self, event): |
|
298 if not event['type'] in [EButton1Up, EButton1Down, EDrag]: |
|
299 return |
|
300 |
|
301 if event['type'] == EButton1Up and self.is_about_active: |
|
302 # This check is for ignoring button up event generated when exiting |
|
303 # `About` menu option. The flag `is_about_active` is set when |
|
304 # `About` menu is active. |
|
305 self.is_about_active = False |
|
306 return |
|
307 |
|
308 if self.erase_mode: |
|
309 pen_size = self.pen_width * 2 |
|
310 outline_color = self.BG_COLOR |
|
311 fill_color = self.BG_COLOR |
|
312 else: |
|
313 pen_size = self.pen_width |
|
314 outline_color = self.BRUSH_COLOR |
|
315 fill_color = self.BRUSH_COLOR |
|
316 |
|
317 # Ignore the touch events in the region where buttons are drawn or if |
|
318 # about screen is active |
|
319 if (self.orientation == 'portrait' and event['pos'][1] > \ |
|
320 (self.y_max - self.toolbar_size - 3 - (pen_size/2))) or \ |
|
321 self.about_active: |
|
322 self.prev_x = event['pos'][0] |
|
323 self.prev_y = self.y_max - self.toolbar_size - 3 - (pen_size / 2) |
|
324 return |
|
325 elif (self.orientation == 'landscape' and event['pos'][0] > \ |
|
326 (self.x_max - self.toolbar_size - 3 - (pen_size/2))) or \ |
|
327 self.about_active: |
|
328 self.prev_x = self.x_max - self.toolbar_size - 3 - (pen_size / 2) |
|
329 self.prev_y = event['pos'][1] |
|
330 return |
|
331 |
|
332 if event['type'] in [EButton1Down, EButton1Up]: |
|
333 self.draw.point((event['pos'][0], event['pos'][1]), |
|
334 outline=outline_color, width=pen_size, fill=fill_color) |
|
335 self.draw_img.point((event['pos'][0], event['pos'][1]), |
|
336 outline=outline_color, width=pen_size, fill=fill_color) |
|
337 elif event['type'] == EDrag: |
|
338 rect = (self.prev_x, self.prev_y, event['pos'][0], event['pos'][1]) |
|
339 redraw_rect = list(rect) |
|
340 # Ensure that the prev_x and prev_y co-ordinates are above the |
|
341 # current co-ordinates. This way we can use prev_x and prev_y as |
|
342 # the top left corner and the current co-ordinates as the bottom |
|
343 # right corner of the rect to be passed to begin_redraw. |
|
344 if redraw_rect[0] > redraw_rect[2]: |
|
345 redraw_rect[0], redraw_rect[2] = redraw_rect[2], redraw_rect[0] |
|
346 if redraw_rect[1] > redraw_rect[3]: |
|
347 redraw_rect[1], redraw_rect[3] = redraw_rect[3], redraw_rect[1] |
|
348 self.canvas.begin_redraw((redraw_rect[0] - pen_size, |
|
349 redraw_rect[1] - pen_size, |
|
350 redraw_rect[2] + pen_size, |
|
351 redraw_rect[3] + pen_size)) |
|
352 self.draw.line(rect, outline=outline_color, width=pen_size, |
|
353 fill=fill_color) |
|
354 self.draw_img.line(rect, outline=outline_color, width=pen_size, |
|
355 fill=fill_color) |
|
356 self.canvas.end_redraw() |
|
357 self.prev_x = event['pos'][0] |
|
358 self.prev_y = event['pos'][1] |
|
359 |
|
360 def run(self): |
|
361 while self.running: |
|
362 e32.ao_sleep(0.01) |
|
363 if self.orientation_changed: |
|
364 if self.orientation == 'landscape': |
|
365 self.new_draw_img = self.draw_img.transpose(ROTATE_90) |
|
366 elif self.orientation == 'portrait': |
|
367 self.new_draw_img = self.draw_img.transpose(ROTATE_270) |
|
368 self.draw_img = None |
|
369 self.draw_img = Image.new((self.canvas.size[0], |
|
370 self.canvas.size[1])) |
|
371 self.draw_img.blit(self.new_draw_img) |
|
372 self.new_draw_img = None |
|
373 self.canvas.blit(self.draw_img) |
|
374 self.clear_button_bindings() |
|
375 if self.about_active: |
|
376 self.clear_about_screen() |
|
377 self.bind_palette = True |
|
378 self.draw_buttons() |
|
379 self.bind_buttons() |
|
380 self.orientation_changed = False |
|
381 |
|
382 self.quit() |
|
383 |
|
384 def quit(self): |
|
385 appuifw.app.exit_key_handler = None |
|
386 self.running = False |
|
387 |
|
388 |
|
389 if __name__ == '__main__': |
|
390 d = PaintApp() |
|
391 d.run() |