-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcolors.py
More file actions
executable file
·2168 lines (1767 loc) · 87.7 KB
/
colors.py
File metadata and controls
executable file
·2168 lines (1767 loc) · 87.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2008 by Jens Andersson and Wade Brainerd.
# This file is part of Colors! XO.
#
# Colors is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Colors is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Colors. If not, see <http://www.gnu.org/licenses/>.
#!/usr/bin/env python
"""Colors! XO painting activity. Based on Colors! by Collecting Smiles."""
# Note- I had to rename the self.canvas object *self.easel* for now, because the stupid sugar.window.Window
# super class of Activity has its own "canvas" member. If anyone has a better name, feel free to change it.
# Reference links:
#
# Colors! - http://wiki.laptop.org/go/Colors!
# PyGTK - http://www.pygtk.org/pygtk2reference/
# Sugar - http://dev.laptop.org/~cscott/joyride-1477-api/
# GStreamer - http://pygstdocs.berlios.de/pygst-reference/index.html
# Activities - http://wiki.laptop.org/go/Sugar_Activity_Tutorial
# Sharing - http://wiki.laptop.org/go/Shared_Sugar_Activities
# Import standard Python modules.
import logging, os, sys, math, time, copy, json, tempfile
from gettext import gettext as _
# Prefer local modules.
from sugar.activity import activity
sys.path.insert(0, activity.get_bundle_path())
try:
import json
json.dumps
except (ImportError, AttributeError):
import simplejson as json
# Import the C++ component of the activity.
from colorsc import *
# Import PyGTK.
import gobject, pygtk, gtk, pango
# Needed to avoid thread crashes with GStreamer
gobject.threads_init()
# Import PyGame. Used for camera and sound.
try:
from pygame import camera, transform, surface, mask
except ImportError:
print "No pygame available."
# Import DBUS and mesh networking modules.
import dbus, telepathy, telepathy.client
from dbus import Interface
from dbus.service import method, signal
from dbus.gobject_service import ExportedGObject
from sugar.presence.tubeconn import TubeConnection
from sugar.presence import presenceservice
from sugar.datastore import datastore
# Import Sugar UI modules.
from sugar import graphics
from sugar.graphics import *
from sugar.graphics import toggletoolbutton
from sugar.graphics.menuitem import MenuItem
# Import GStreamer (for camera access).
#import pygst, gst
# Initialize logging.
log = logging.getLogger('Colors')
log.setLevel(logging.DEBUG)
logging.basicConfig()
# Track memory leaks.
#import gc
#gc.set_debug(gc.DEBUG_LEAK)
# DBUS identifiers are used to uniquely identify the activity for network communcations.
DBUS_IFACE = "org.laptop.community.Colors"
DBUS_PATH = "/org/laptop/community/Colors"
DBUS_SERVICE = DBUS_IFACE
# This is the overlay that appears when the user presses the Palette toobar button. It covers the entire screen,
# and offers controls for brush type, size, opacity, and color.
#
# The color wheel and triangle are rendered into GdkImage objects by C++ code in palette.h / palette.cpp which
# is compiled into the colorsc module.
class BrushControlsPanel(gtk.HBox):
PALETTE_SIZE = int(7.0 * style.GRID_CELL_SIZE) & ~1
PREVIEW_SIZE = int(2.5 * style.GRID_CELL_SIZE) & ~1
BRUSHTYPE_SIZE = int(1.0 * style.GRID_CELL_SIZE) & ~1
def __init__ (self):
gtk.HBox.__init__(self)
self.set_property("spacing", 5)
self.set_border_width(30)
# Locally managed Brush object.
self.brush = Brush()
# Palette wheel widget.
palbox = gtk.VBox()
self.palette = Palette(BrushControlsPanel.PALETTE_SIZE)
self.paletteimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), BrushControlsPanel.PALETTE_SIZE, BrushControlsPanel.PALETTE_SIZE)
self.palette.render_wheel(self.paletteimage)
self.palette.render_triangle(self.paletteimage)
self.palettearea = gtk.DrawingArea()
self.palettearea.set_size_request(BrushControlsPanel.PALETTE_SIZE, BrushControlsPanel.PALETTE_SIZE)
self.palettearea.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_RELEASE_MASK)
self.palettearea.connect('expose-event', self.on_palette_expose)
self.palettearea.connect('motion-notify-event', self.on_palette_mouse)
self.palettearea.connect('button-press-event', self.on_palette_mouse)
self.palettearea.connect('button-release-event', self.on_palette_mouse)
palbox.pack_start(self.palettearea, False, False)
# Brush size scrollbar, label and pressure sensitivity checkbox.
sizebox = gtk.VBox()
sizelabel = gtk.Label(_('Size'))
sizebox.pack_end(sizelabel, False)
self.size = gtk.Adjustment(50, 1, 130, 1, 10, 10)
self.sizebar = gtk.VScale(self.size)
self.sizebar.set_property("draw-value", False)
self.sizebar.set_property("inverted", True)
self.sizebar.connect('value-changed', self.on_size_change)
sizebox.pack_end(self.sizebar)
self.sizecheck = gtk.CheckButton(_('Sensitive'))
self.sizecheck.connect('toggled', self.on_variable_size_toggle)
sizebox.pack_end(self.sizecheck, False)
# Brush opacity scrollbar, label and pressure sensitivity checkbox.
opacitybox = gtk.VBox()
opacitylabel = gtk.Label(_('Opacity'))
opacitybox.pack_end(opacitylabel, False)
self.opacity = gtk.Adjustment(0, 0, 1.1, 0.001, 0.1, 0.1)
self.opacitybar = gtk.VScale(self.opacity)
self.opacitybar.set_property("draw-value", False)
self.opacitybar.set_property("inverted", True)
self.opacitybar.connect('value-changed', self.on_opacity_change)
opacitybox.pack_end(self.opacitybar)
self.opacitycheck = gtk.CheckButton(_('Sensitive'))
self.opacitycheck.connect('toggled', self.on_variable_opacity_toggle)
opacitybox.pack_end(self.opacitycheck, False)
# Force column scrollbars to be equal width.
group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
group.add_widget(sizebox)
group.add_widget(opacitybox)
# Brush preview widget.
brushbox = gtk.VBox()
brushbox.set_property("spacing", 20)
self.preview = BrushPreview(BrushControlsPanel.PREVIEW_SIZE)
self.previewimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), BrushControlsPanel.PREVIEW_SIZE, BrushControlsPanel.PREVIEW_SIZE)
self.previewarea = gtk.DrawingArea()
self.previewarea.set_size_request(BrushControlsPanel.PREVIEW_SIZE, BrushControlsPanel.PREVIEW_SIZE)
self.previewarea.connect('expose-event', self.on_preview_expose)
brushbox.pack_start(self.previewarea, False)
# Brush type selection widgets.
self.brushbtns = []
brushtbl = gtk.Table(1, 2)
brushtbl.set_col_spacings(5)
brushtbl.attach(self.create_brushtype_widget(BrushType.BRUSHTYPE_SOFT), 0, 1, 0, 1)
brushtbl.attach(self.create_brushtype_widget(BrushType.BRUSHTYPE_HARD), 1, 2, 0, 1)
brushbox.pack_start(brushtbl, False)
self.pack_start(sizebox, False, False)
self.pack_start(opacitybox, False, False)
self.pack_start(palbox, True, False)
self.pack_start(brushbox, False)
self.in_toggle_cb = False
def create_brushtype_widget (self, type):
brusharea = gtk.DrawingArea()
brusharea.set_size_request(BrushControlsPanel.BRUSHTYPE_SIZE, BrushControlsPanel.BRUSHTYPE_SIZE)
brusharea.connect('expose-event', self.on_brushtype_expose)
brusharea.preview = BrushPreview(BrushControlsPanel.BRUSHTYPE_SIZE)
brusharea.preview.brush.size = int(BrushControlsPanel.BRUSHTYPE_SIZE*0.75)
brusharea.preview.brush.type = type
brusharea.preview.brush.color = Color(0,0,0,0)
brusharea.previewimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), BrushControlsPanel.BRUSHTYPE_SIZE, BrushControlsPanel.BRUSHTYPE_SIZE)
brusharea.preview.render(brusharea.previewimage)
brushbtn = gtk.ToggleButton()
brushbtn.set_image(brusharea)
brushbtn.connect('toggled', self.on_brushtype_toggle)
brushbtn.brushtype = type
self.brushbtns.append(brushbtn)
return brushbtn
def set_brush (self, brush):
self.brush = brush
self.opacity.set_value(brush.opacity)
self.size.set_value(brush.size)
self.palette.set_color(brush.color)
self.opacitycheck.set_active((brush.control & Brush.BRUSHCONTROL_VARIABLEOPACITY) != 0)
self.sizecheck.set_active((brush.control & Brush.BRUSHCONTROL_VARIABLESIZE) != 0)
self.update_brushtype_btns()
def on_size_change (self, event):
self.brush.size = int(self.size.get_value())
self.previewarea.queue_draw()
def on_opacity_change (self, event):
self.brush.opacity = self.opacity.get_value()
self.previewarea.queue_draw()
def on_variable_size_toggle (self, event):
if self.sizecheck.get_active():
self.brush.control |= Brush.BRUSHCONTROL_VARIABLESIZE
else:
self.brush.control &= ~Brush.BRUSHCONTROL_VARIABLESIZE
def on_variable_opacity_toggle (self, event):
if self.opacitycheck.get_active():
self.brush.control |= Brush.BRUSHCONTROL_VARIABLEOPACITY
else:
self.brush.control &= ~Brush.BRUSHCONTROL_VARIABLEOPACITY
def on_palette_expose (self, widget, event):
gc = self.palettearea.get_style().fg_gc[gtk.STATE_NORMAL]
old_foreground = gc.foreground
old_line_width = gc.line_width
# Draw palette image.
self.palette.render_triangle(self.paletteimage)
self.palettearea.window.draw_image(gc, self.paletteimage, 0, 0, 0, 0, -1, -1)
# Draw circles to indicate selected color.
# todo- Better looking circles.
r = int(self.palette.WHEEL_WIDTH*0.75)
gc.foreground = self.palettearea.get_colormap().alloc_color(16384,16384,16384)
gc.line_width = 2
wheel_pos = self.palette.get_wheel_pos()
tri_pos = self.palette.get_triangle_pos()
self.palettearea.window.draw_arc(gc, False, int(wheel_pos.x-r/2+2), int(wheel_pos.y-r/2+2), r-4, r-4, 0, 360*64)
self.palettearea.window.draw_arc(gc, False, int(tri_pos.x-r/2+2), int(tri_pos.y-r/2+2), r-4, r-4, 0, 360*64)
gc.foreground = self.palettearea.get_colormap().alloc_color(65535,65535,65535)
gc.line_width = 2
self.palettearea.window.draw_arc(gc, False, int(wheel_pos.x-r/2), int(wheel_pos.y-r/2), r, r, 0, 360*64)
self.palettearea.window.draw_arc(gc, False, int(tri_pos.x-r/2), int(tri_pos.y-r/2), r, r, 0, 360*64)
gc.foreground = old_foreground
gc.line_width = old_line_width
def on_palette_mouse (self, widget, event):
if event.state & gtk.gdk.BUTTON1_MASK:
widget.grab_focus()
self.palette.process_mouse(int(event.x), int(event.y))
self.palettearea.queue_draw()
self.brush.color = self.palette.get_color()
self.previewarea.queue_draw()
if event.type == gtk.gdk.BUTTON_RELEASE:
self.palette.process_mouse_release()
def on_preview_expose (self, widget, event):
self.preview.brush = self.brush
self.preview.brush.size = self.brush.size*2 # Mimic 2x canvas scaling.
self.preview.render(self.previewimage)
self.previewarea.window.draw_image(widget.get_style().fg_gc[gtk.STATE_NORMAL], self.previewimage, 0, 0, 0, 0, -1, -1)
def on_brushtype_expose (self, widget, event):
widget.window.draw_image(widget.get_style().fg_gc[gtk.STATE_NORMAL], widget.previewimage, 0, 0, 0, 0, -1, -1)
# Manually implemented radio button using ToggleButtons.
def update_brushtype_btns (self):
for b in self.brushbtns:
b.set_active(b.brushtype == self.brush.type)
def on_brushtype_toggle (self, widget):
if self.in_toggle_cb:
return
self.in_toggle_cb = True
self.brush.type = widget.brushtype
self.update_brushtype_btns()
self.previewarea.queue_draw()
self.in_toggle_cb = False
# This is the overlay that appears when playing back large numbers of drawing commands.
# It simply shows how much work is left to do and that progress is taking place.
class ProgressPanel(gtk.VBox):
def __init__ (self):
gtk.VBox.__init__(self)
self.set_border_width(50)
self.label = gtk.Label()
self.label.set_markup("<span foreground='white' size='xx-large'>"+_("Working...")+"</span>")
self.progress = gtk.ProgressBar()
self.progress.set_fraction(0.5)
self.progress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
vbox = gtk.VBox()
vbox.set_property("spacing", 20)
vbox.pack_start(self.label, False)
vbox.pack_start(self.progress, False)
self.pack_start(vbox, True, False)
# This is the overlay that appears when the Help button is pressed.
class HelpPanel(gtk.VBox):
def __init__ (self):
gtk.VBox.__init__(self)
# Add the context sensitive help.
self.helplabel = gtk.Label()
self.helplabel.set_padding(10, 10)
self.helplabel.set_markup(
'''
<span font_family="monospace" size="large" color="#ffffff">
Keyboard controls: Gamepad controls:
Space - Open palette Up - Zoom in
v - Video paint Down - Zoom in
Hand drag - Scroll drag Left - Center canvas
Arrows - Scroll Right - Scroll drag
Alt click - Pick up color Square - Open palette
Ctrl Up - Zoom in Check - Undo
Ctrl Down - Zoom in Circle - Pick
Ctrl A - Center canvas X - Paint
Ctrl Z - Undo
Ctrl C - Copy to clipboard
Ctrl E - Erase image
Alt Enter - Full screen
Alt + letter - Save brush
letter - Restore brush
</span>
'''
)
self.helpbox = gtk.EventBox()
self.helpbox.modify_bg(gtk.STATE_NORMAL, self.helpbox.get_colormap().alloc_color('#000000'))
self.helpbox.add(self.helplabel)
vbox = gtk.VBox()
vbox.set_property("spacing", 20)
vbox.pack_start(self.helpbox, True, True)
self.pack_start(vbox, True, True)
# This is the main Colors! activity class.
#
# It owns the main application window, the painting canvas, and all the various toolbars and options.
class Colors(activity.Activity, ExportedGObject):
# Application mode definitions.
MODE_INTRO = 0
MODE_PLAYBACK = 1
MODE_CANVAS = 2
MODE_PICK = 3
MODE_SCROLL = 4
MODE_PALETTE = 5
MODE_REFERENCE = 6
# Button definitions
BUTTON_PALETTE = 1<<0
#BUTTON_REFERENCE = 1<<1
BUTTON_VIDEOPAINT = 1<<2
BUTTON_SCROLL = 1<<3
BUTTON_PICK = 1<<4
BUTTON_ZOOM_IN = 1<<5
BUTTON_ZOOM_OUT = 1<<6
BUTTON_CENTER = 1<<7
BUTTON_TOUCH = 1<<8
BUTTON_CONTROL = 1<<9
BUTTON_UNDO = 1<<10
# Number of drawing steps to execute between progress bar updates. More updates means faster overall drawing
# but a less responsive UI.
PROGRESS_DELTA = 50
def __init__ (self, handle):
activity.Activity.__init__(self, handle)
self.set_title(_("Colors!"))
# Uncomment to test out a bunch of the C++ heavy lifting APIs. Takes awhile on the XO though.
#self.benchmark()
# Get activity size. What we really need is the size of the canvasarea, not including the toolbox.
# This will be figured out on the first paint event, once everything is resized.
self.width = gtk.gdk.screen_width()
self.height = gtk.gdk.screen_height()
# Set the initial mode to None, it will be set to Intro on the first update.
self.mode = None
# Set up various systems.
self.init_input()
self.init_zoom()
self.init_scroll()
# Build the toolbar.
self.build_toolbar()
# Set up drawing canvas (which is also the parent for any popup widgets like the brush controls).
self.build_canvas()
# Build the brush control popup window.
self.build_brush_controls()
# Build the progress display popup window.
self.build_progress()
# Build the help popup window.
self.build_help()
# Start camera processing.
self.init_camera()
# Set up mesh networking.
self.init_mesh()
# Scan for input devices.
self.init_input_devices()
# This has to happen last, because it calls the read_file method when restoring from the Journal.
self.set_canvas(self.easelarea)
# Reveal the main window (but not the panels).
self.show_all()
self.brush_controls.hide()
self.progress.hide()
self.help.hide()
self.overlay_active = False
# Start it running.
self.update_timer = None
self.update()
# store event.get_axis() of last event to ignore fake pressure
# when system doesnt support gtk.gdk.AXIS_PRESSURE but
# event.get_axis(gtk.gdk.AXIS_PRESSURE) returns 0.0 value
self._prev_AXIS_PRESSURE = None
#-----------------------------------------------------------------------------------------------------------------
# User interface construction
def build_canvas (self):
# The canvasarea is the main window which covers the entire screen below the toolbar.
self.easelarea = gtk.Layout()
self.easelarea.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height())
self.easelarea.set_flags(gtk.CAN_FOCUS)
self.set_double_buffered(False)
self.easelarea.set_double_buffered(False)
# Set up GTK events for the canvasarea.
self.easelarea.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK)
self.easelarea.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_RELEASE_MASK)
self.easelarea.add_events(gtk.gdk.KEY_PRESS_MASK|gtk.gdk.KEY_RELEASE_MASK)
# The actual drawing canvas is at 1/2 resolution, which improves performance by 4x and still leaves a decent
# painting resolution of 600x400 on the XO.
self.easel = Canvas(gtk.gdk.screen_width()/2, gtk.gdk.screen_height()/2)
self.set_brush(self.easel.brush)
# Map of keyboard keys to brushes.
self.brush_map = {}
# The Canvas internally stores the image as 32bit. When rendering, it scales up and blits into canvasimage,
# which is in the native 565 resolution of the XO. Then canvasimage is drawn into the canvasarea DrawingArea.
self.easelimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), self.width, self.height)
# Now that we have a canvas, connect the rest of the events.
self.easelarea.connect('expose-event', self.on_easelarea_expose)
self.connect('key-press-event', self.on_key_event)
self.connect('key-release-event', self.on_key_event)
self.easelarea.connect('button-press-event', self.on_mouse_event)
self.easelarea.connect('button-release-event', self.on_mouse_event)
self.easelarea.connect('motion-notify-event', self.on_mouse_event)
def build_brush_controls (self):
self.brush_controls = BrushControlsPanel()
self.brush_controls.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height())
self.easelarea.put(self.brush_controls, 0, 0)
def build_progress (self):
self.progress = ProgressPanel()
self.progress.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height())
self.easelarea.put(self.progress, 0, 0)
def build_help (self):
self.help = HelpPanel()
self.help.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height())
self.easelarea.put(self.help, 0, 0)
def build_toolbar (self):
self.add_accel_group(gtk.AccelGroup())
# Painting controls (palette, zoom, etc)
self.palettebtn = toggletoolbutton.ToggleToolButton('palette')
self.palettebtn.set_tooltip(_("Palette"))
self.palettebtn.connect('clicked', self.on_palette)
self.brushpreview = BrushPreview(Canvas.VIDEO_HEIGHT)
self.brushpreviewimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), Canvas.VIDEO_HEIGHT, Canvas.VIDEO_HEIGHT)
self.brushpreviewarea = gtk.DrawingArea()
self.brushpreviewarea.set_size_request(Canvas.VIDEO_HEIGHT, Canvas.VIDEO_HEIGHT)
self.brushpreviewarea.connect('expose-event', self.on_brushpreview_expose)
self.brushpreviewitem = gtk.ToolItem()
self.brushpreviewitem.add(self.brushpreviewarea)
# todo- Color picker button, similar semantics to scroll button.
self.zoomsep = gtk.SeparatorToolItem()
self.zoomsep.set_expand(True)
self.zoomsep.set_draw(False)
self.zoomoutbtn = toolbutton.ToolButton('zoom-out')
self.zoomoutbtn.set_tooltip(_("Zoom Out"))
self.zoomoutbtn.connect('clicked', self.on_zoom_out)
self.zoomoutbtn.props.accelerator = '<Ctrl>Down'
self.zoominbtn = toolbutton.ToolButton('zoom-in')
self.zoominbtn.set_tooltip(_("Zoom In"))
self.zoominbtn.props.accelerator = '<Ctrl>Up'
self.zoominbtn.connect('clicked', self.on_zoom_in)
self.centerbtn = toolbutton.ToolButton('zoom-original')
self.centerbtn.set_tooltip(_("Center Image"))
self.centerbtn.connect('clicked', self.on_center)
self.centerbtn.props.accelerator = '<Ctrl>A'
self.fullscreenbtn = toolbutton.ToolButton('view-fullscreen')
self.fullscreenbtn.set_tooltip(_("Fullscreen"))
self.fullscreenbtn.connect('clicked', self.on_fullscreen)
self.fullscreenbtn.props.accelerator = '<Alt>Enter'
self.editsep = gtk.SeparatorToolItem()
self.editsep.set_expand(True)
self.editsep.set_draw(False)
self.undobtn = toolbutton.ToolButton('edit-undo')
self.undobtn.set_tooltip(_("Undo"))
self.undobtn.connect('clicked', self.on_undo)
self.undobtn.props.accelerator = '<Ctrl>Z'
self.copybtn = toolbutton.ToolButton('edit-copy')
self.copybtn.set_tooltip(_("Copy"))
self.copybtn.connect('clicked', self.on_copy)
self.copybtn.props.accelerator = '<Ctrl>C'
#self.refsep = gtk.SeparatorToolItem()
#
#self.takerefbtn = toolbutton.ToolButton('take-reference')
#self.takerefbtn.set_tooltip(_("Take Reference Picture"))
#self.takerefbtn.connect('clicked', self.on_take_reference)
#self.take_reference = False
#
#self.showrefbtn = toggletoolbutton.ToggleToolButton('show-reference')
#self.showrefbtn.set_tooltip(_("Show Reference Picture"))
#self.showrefbtn.connect('clicked', self.on_show_reference)
#
self.videopaintsep = gtk.SeparatorToolItem()
#
self.videopaintbtn = toggletoolbutton.ToggleToolButton('video-paint')
self.videopaintbtn.set_tooltip(_("Video Paint"))
self.videopaintbtn.connect('clicked', self.on_videopaint)
#self.videopaintpreview = gtk.DrawingArea()
#self.videopaintpreview.set_size_request(Canvas.VIDEO_WIDTH, Canvas.VIDEO_HEIGHT)
#self.videopaintpreview.connect('expose-event', self.on_videopaintpreview_expose)
#self.videopaintitem = gtk.ToolItem()
#self.videopaintitem.add(self.videopaintpreview)
#self.videopaintimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), Canvas.VIDEO_WIDTH, Canvas.VIDEO_HEIGHT)
self.videopaint_enabled = False
self.clearsep = gtk.SeparatorToolItem()
self.clearsep.set_expand(True)
self.clearsep.set_draw(False)
self.clearbtn = toolbutton.ToolButton('erase')
self.clearbtn.set_tooltip(_("Erase Image"))
self.clearbtn.connect('clicked', self.on_clear)
self.clearbtn.props.accelerator = '<Ctrl>E'
self.helpbtn = toggletoolbutton.ToggleToolButton('help')
self.helpbtn.set_active(False)
self.helpbtn.set_tooltip(_("Show Help"))
#self.helpbtn.props.accelerator = '<Ctrl>H'
self.helpbtn.connect('clicked', self.on_help)
#editbox = activity.EditToolbar()
#editbox.undo.props.visible = False
#editbox.redo.props.visible = False
#editbox.separator.props.visible = False
#editbox.copy.connect('clicked', self.on_copy)
#editbox.paste.connect('clicked', self.on_paste)
paintbox = gtk.Toolbar()
paintbox.insert(self.palettebtn, -1)
paintbox.insert(self.brushpreviewitem, -1)
paintbox.insert(self.zoomsep, -1)
paintbox.insert(self.zoomoutbtn, -1)
paintbox.insert(self.zoominbtn, -1)
paintbox.insert(self.centerbtn, -1)
paintbox.insert(self.fullscreenbtn, -1)
paintbox.insert(self.editsep, -1)
paintbox.insert(self.undobtn, -1)
paintbox.insert(self.copybtn, -1)
#paintbox.insert(self.refsep, -1)
#paintbox.insert(self.takerefbtn, -1)
#paintbox.insert(self.showrefbtn, -1)
paintbox.insert(self.videopaintsep, -1)
paintbox.insert(self.videopaintbtn, -1)
paintbox.insert(self.helpbtn, -1)
#paintbox.insert(self.videopaintitem, -1)
paintbox.insert(self.clearsep, -1)
paintbox.insert(self.clearbtn, -1)
# Playback controls
self.startbtn = toolbutton.ToolButton('media-playback-start')
self.startbtn.set_tooltip(_("Start Playback"))
self.startbtn.connect('clicked', self.on_play)
self.pausebtn = toolbutton.ToolButton('media-playback-pause')
self.pausebtn.set_tooltip(_("Pause Playback"))
self.pausebtn.connect('clicked', self.on_pause)
#self.backonebtn = toolbutton.ToolButton('media-seek-backward')
#self.backonebtn.set_tooltip(_("Back One Stroke"))
#self.backonebtn.connect('clicked', self.on_back_one)
#self.backonebtn.props.accelerator = '<Ctrl>Left'
#self.forwardonebtn = toolbutton.ToolButton('media-seek-forward')
#self.forwardonebtn.set_tooltip(_("Forward One Stroke"))
#self.forwardonebtn.connect('clicked', self.on_forward_one)
#self.forwardonebtn.props.accelerator = '<Ctrl>Right'
# Position bar
self.playbackpossep = gtk.SeparatorToolItem()
self.playbackpossep.set_draw(True)
self.beginbtn = toolbutton.ToolButton('media-seek-backward')
self.beginbtn.set_tooltip(_("Skip To Beginning"))
self.beginbtn.connect('clicked', self.on_skip_begin)
self.playbackpos = gtk.Adjustment(0, 0, 110, 1, 10, 10)
self.playbackposbar = gtk.HScale(self.playbackpos)
self.playbackposbar.connect('value-changed', self.on_playbackposbar_change)
self.playbackposbar.ignore_change = 0
self.playbackpositem = gtk.ToolItem()
self.playbackpositem.set_expand(True)
self.playbackpositem.add(self.playbackposbar)
self.endbtn = toolbutton.ToolButton('media-seek-forward')
self.endbtn.set_tooltip(_("Skip To End"))
self.endbtn.connect('clicked', self.on_skip_end)
playbox = gtk.Toolbar()
playbox.insert(self.startbtn, -1)
playbox.insert(self.pausebtn, -1)
playbox.insert(self.beginbtn, -1)
playbox.insert(self.endbtn, -1)
playbox.insert(self.playbackpossep, -1)
playbox.insert(self.playbackpositem, -1)
# Sample files to learn from. Reads the list from an INDEX file in the data folder.
samplebox = gtk.Toolbar()
self.samplebtns = []
samples = []
fd = open(activity.get_bundle_path() + '/data/INDEX', 'r')
try:
samples = json.loads(fd.read())
finally:
fd.close()
log.debug("Samples: %r", samples)
for s in samples:
btn = toolbutton.ToolButton('media-playback-start')
btn.filename = activity.get_bundle_path() + '/data/' + s['drw']
btn.set_tooltip(s['title'])
img = gtk.Image()
img.set_from_file(activity.get_bundle_path() + '/data/' + s['icon'])
btn.set_icon_widget(img)
btn.connect('clicked', self.on_sample)
samplebox.insert(btn, -1)
self.samplebtns.append(btn)
self.webbtn = toolbutton.ToolButton('web')
self.webbtn.set_tooltip(_("Colors! Gallery"))
self.webbtn.connect('clicked', self.on_web)
self.samplesep = gtk.SeparatorToolItem()
self.samplesep.set_draw(False)
self.samplesep.set_expand(True)
samplebox.insert(self.samplesep, -1)
samplebox.insert(self.webbtn, -1)
toolbar = activity.ActivityToolbox(self)
toolbar.add_toolbar(_("Paint"),paintbox)
#toolbar.add_toolbar(_("Edit"),editbox)
toolbar.add_toolbar(_("Watch"),playbox)
toolbar.add_toolbar(_("Learn"),samplebox)
toolbar.show_all()
self.set_toolbox(toolbar)
# Add Keep As button to activity toolbar.
activity_toolbar = toolbar.get_activity_toolbar()
keep_palette = activity_toolbar.keep.get_palette()
menu_item = MenuItem(_('Keep to PNG'))
menu_item.connect('activate', self.on_export_png)
keep_palette.menu.append(menu_item)
menu_item.show()
#-----------------------------------------------------------------------------------------------------------------
# Camera access
#
# The new camera module from Pygame, by Nirav Patel, is used for camera access.
# It was only recently added, so we have to handle the case where the module doesn't exist.
def init_camera (self):
self.camera_enabled = False
self.videopaintbtn.set_sensitive(False)
try:
camera_list = camera.list_cameras()
if len(camera_list):
self.cam = camera.Camera(camera_list[0],(320,240),"RGB")
self.camcapture = surface.Surface((320,240),0,16,(63488,2016,31,0))
self.camsmall = surface.Surface((240,180),0,self.camcapture)
self.camhsv = surface.Surface((240,180),0,self.camcapture)
self.camera_enabled = True
self.videopaintbtn.set_sensitive(True)
else:
log.debug('No cameras found, videopaint disabled.')
except NameError:
log.debug('Pygame camera module not found, videopaint disabled.')
pass
#-----------------------------------------------------------------------------------------------------------------
# Mesh networking
#
# The mesh networking system is a little bit wacky, but works reasonably well at the moment. It might need to
# be redone in the future for less stable networking environments.
#
# Each user maintains the current state of the canvas, as well as a 'shared image' state which represents the
# 'master' state of the canvas that is shared by all the users. The command index in the drawing command list
# that corresponds to the master state is also recorded.
#
# Each user is allowed to paint 'ahead' of the master state, by appending commands to their local command list.
# After each stroke, the commands of the stroke are broadcast as the new master state to the other users.
#
# Whenever a new master state is received, it will contain a list of commands that follow the old master state
# to reach the new one. The user simply rewinds their canvas to the old master state, replaces their local image
# with the shared one, and appends the received commands to reach the new master state.
#
# The net effect is that when the user paints something, they broadcast their own commands, and then *receive*
# their own commands, rewinding and then playing them back immediately. So, every users canvas state is simply the
# sum of all the broadcasts from themselves and the other users. Since the broadcasts are serialized, every
# user has the same state all the time.
def init_mesh (self):
self.connected = False # If True, the activity is shared with other users.
self.initiating = False # If True, this instance started the activity. Otherwise, we joined it.
# Set up the drawing send and receive state.
# See send_and_receive_draw_commands for more information.
self.draw_command_sent = 0
self.draw_command_received = 0
self.draw_command_queue = DrawCommandBuffer()
# Get the presence server and self handle.
self.pservice = presenceservice.get_instance()
self.owner = self.pservice.get_owner()
self.connect('shared', self.on_shared) # Called when the user clicks the Share button in the toolbar.
self.connect('joined', self.on_join) # Called when the activity joins a remote activity.
def on_shared (self, activity):
self.initiating = True
self.setup_sharing()
# Offer a DBus tube that everyone else can connect to.
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(DBUS_SERVICE, {})
# Cancel the intro if playing.
self.set_mode(Colors.MODE_CANVAS)
def on_list_tubes_reply(self, tubes): # Called by on_join.
for tube_info in tubes:
self.on_tube(*tube_info)
def on_list_tubes_error(self, e): # Called by on_join.
pass
def on_join (self, activity):
self.initiating = False
self.setup_sharing()
# List existing tubes. There should only be one, which will invoke the on_new_tube callback.
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
reply_handler=self.on_list_tubes_reply, error_handler=self.on_list_tubes_error)
# Cancel the intro if playing.
self.set_mode(Colors.MODE_CANVAS)
def setup_sharing (self):
"""Called to initialize mesh networking objects when the activity becomes shared (on_shared) or joins an
existing shared activity (on_join)."""
# Cache connection related objects.
self.conn = self._shared_activity.telepathy_conn
self.tubes_chan = self._shared_activity.telepathy_tubes_chan
self.text_chan = self._shared_activity.telepathy_text_chan
# This will get called as soon as the connection is established.
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', self.on_tube)
# Called when a buddy joins us or leaves (does nothing right now).
self._shared_activity.connect('buddy-joined', self.on_buddy_joined)
self._shared_activity.connect('buddy-left', self.on_buddy_left)
def on_tube (self, id, initiator, type, service, params, state):
"""Called by the NewTube callback or the ListTubes enumeration, when a real connection finally exists."""
if (type == telepathy.TUBE_TYPE_DBUS and service == DBUS_SERVICE):
# If the new tube is waiting for us to finalize it, do so.
if state == telepathy.TUBE_STATE_LOCAL_PENDING:
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id)
if not self.connected:
# Create the TubeConnection object to manage the connection.
self.tube = TubeConnection(self.conn,
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
ExportedGObject.__init__(self, self.tube, DBUS_PATH)
# Set up DBUS Signal receiviers.
self.tube.add_signal_receiver(self.ReceiveHello, 'BroadcastHello', DBUS_IFACE, path=DBUS_PATH)
self.tube.add_signal_receiver(self.ReceiveCanvasMode, 'BroadcastCanvasMode', DBUS_IFACE, path=DBUS_PATH)
self.tube.add_signal_receiver(self.ReceiveClear, 'BroadcastClear', DBUS_IFACE, path=DBUS_PATH)
self.tube.add_signal_receiver(self.ReceiveDrawCommands, 'BroadcastDrawCommands', DBUS_IFACE, path=DBUS_PATH)
self.tube.add_signal_receiver(self.ReceivePlayback, 'BroadcastPlayback', DBUS_IFACE, path=DBUS_PATH)
log.debug("Connected.")
self.connected = True
# Limit UI choices when sharing.
self.disable_shared_commands()
# Announce our presence to the server.
if not self.initiating:
self.BroadcastHello()
# Notes about DBUS signals:
# - When you call a @signal function, its implementation is invoked, and the registered callback is invoked on all
# the peers (including the one who invoked the signal!). So it's usually best for the @signal function to do
# nothing at all.
# - The 'signature' describes the parameters to the function.
@signal(dbus_interface=DBUS_IFACE, signature='')
def BroadcastHello (self):
"""Broadcast signal sent when a client joins the shared activity."""
pass
def ReceiveHello (self):
if not self.initiating: return # Only the initiating peer responds to Hello commands.
log.debug("Received Hello. Responding with canvas state (%d commands).", self.easel.playback_length())
self.BroadcastCanvasMode()
self.BroadcastClear()
buf = self.easel.send_drw_commands(0, self.easel.get_num_commands())
self.BroadcastDrawCommands(buf.get_bytes(), buf.ncommands)
self.update()
@signal(dbus_interface=DBUS_IFACE, signature='')
def BroadcastCanvasMode (self):
"""Broadcast signal for forcing clients into Canvas mode."""
pass
def ReceiveCanvasMode (self):
log.debug("ReceiveCanvasMode")
if self.mode != Colors.MODE_CANVAS:
self.set_mode(Colors.MODE_CANVAS)
self.update()
@signal(dbus_interface=DBUS_IFACE, signature='')
def BroadcastClear (self):
"""Broadcast signal for clearing the canvas."""
pass
def ReceiveClear (self):
log.debug("ReceiveClear")
self.easel.clear()
self.easel.save_shared_image()
self.update()
@signal(dbus_interface=DBUS_IFACE, signature='ayi')
def BroadcastDrawCommands (self, cmds, ncommands):
"""Broadcast signal for drawing commands."""
pass
def ReceiveDrawCommands (self, cmds, ncommands):
log.debug("ReceiveDrawCommands")
s = "".join(chr(b) for b in cmds) # Convert dbus.ByteArray to Python string.
self.draw_command_queue.append(DrawCommandBuffer(s, ncommands))
self.update()
@signal(dbus_interface=DBUS_IFACE, signature='bii')
def BroadcastPlayback (self, playing, playback_pos, playback_speed):
"""Broadcast signal controlling playback. Not yet used."""
pass
def ReceivePlayback (self):
log.debug("ReceivePlayback")
if playing:
if self.mode != Colors.MODE_PLAYBACK:
self.set_mode(Colors.MODE_PLAYBACK)
else:
if self.mode == Colors.MODE_PLAYBACK:
self.set_mode(Colors.MODE_CANVAS)
self.easel.playback_to(playback_pos)
self.easel.set_playback_speed(playback_speed)
self.update()
def on_buddy_joined (self, activity, buddy):
log.debug('Buddy %s joined', buddy.props.nick)
def on_buddy_left (self, activity, buddy):
log.debug('Buddy %s left', buddy.props.nick)
def send_and_receive_draw_commands (self):
if self.connected:
# Broadcast drawing commands that were generated by this user since the last call to this function.
if self.draw_command_sent < self.easel.get_num_commands():
# TODO: Always prepend the current brush here.
buf = self.easel.send_drw_commands(self.draw_command_sent, self.easel.get_num_commands()-self.draw_command_sent)
self.BroadcastDrawCommands(buf.get_bytes(), buf.ncommands)
# Play any queued draw commands that were received from the host. If there are any, we first reset the
# canvas contents back to the last received state and then play them back.
if self.draw_command_queue.ncommands:
# Also, we have to save and restore the brush around the queued commands.
saved_brush = self.easel.brush
self.easel.receive_drw_commands(self.draw_command_queue, self.draw_command_sent)
self.easel.restore_shared_image()
self.easel.play_range(self.draw_command_sent, self.easel.get_num_commands())
self.easel.save_shared_image()
self.draw_command_queue.clear()
self.set_brush(saved_brush)
self.flush_dirty_canvas()
# Note that resetting the state above means "undoing" the commands we just broadcast. We will receive them
# again by our ReceiveDrawCommands callback immediately, and will play them back so the user shouldn't notice.
self.draw_command_sent = self.easel.get_num_commands()
def disable_shared_commands (self):
"""Disables UI controls which cannot be activated by non-host peers."""
# Cannot control playback.
self.startbtn.set_sensitive(False)
self.pausebtn.set_sensitive(False)
self.beginbtn.set_sensitive(False)
self.endbtn.set_sensitive(False)
self.playbackposbar.set_sensitive(False)
self.undobtn.set_sensitive(False)
# Cannot activate sample drawings.
for s in self.samplebtns:
s.set_sensitive(False)
#-----------------------------------------------------------------------------------------------------------------
# Input device (Wacom etc.) code
def init_input_devices(self):
self.easelarea.set_extension_events(gtk.gdk.EXTENSION_EVENTS_CURSOR)
self.devices = gtk.gdk.devices_list()
for d in self.devices:
log.debug('Input Device: name=\'%s\'' % (d.name))
d.set_mode(gtk.gdk.MODE_SCREEN)
#-----------------------------------------------------------------------------------------------------------------
# Input code
def init_input (self):
self.cur_buttons = 0
self.pending_press = 0
self.pending_release = 0
self.mx = 0
self.my = 0
self.pressure = 255
self.lastmx = 0
self.lastmy = 0
self.lastr = 0
def on_key_event (self, widget, event):