1"""Strip viewer and related widgets.
2
3The classes in this file implement the StripViewer shown in the top two thirds
4of the main Pynche window.  It consists of three StripWidgets which display
5the variations in red, green, and blue respectively of the currently selected
6r/g/b color value.
7
8Each StripWidget shows the color variations that are reachable by varying an
9axis of the currently selected color.  So for example, if the color is
10
11  (R,G,B)=(127,163,196)
12
13then the Red variations show colors from (0,163,196) to (255,163,196), the
14Green variations show colors from (127,0,196) to (127,255,196), and the Blue
15variations show colors from (127,163,0) to (127,163,255).
16
17The selected color is always visible in all three StripWidgets, and in fact
18each StripWidget highlights the selected color, and has an arrow pointing to
19the selected chip, which includes the value along that particular axis.
20
21Clicking on any chip in any StripWidget selects that color, and updates all
22arrows and other windows.  By toggling on Update while dragging, Pynche will
23select the color under the cursor while you drag it, but be forewarned that
24this can be slow.
25"""
26
27from Tkinter import *
28import ColorDB
29
30# Load this script into the Tcl interpreter and call it in
31# StripWidget.set_color().  This is about as fast as it can be with the
32# current _tkinter.c interface, which doesn't support Tcl Objects.
33TCLPROC = '''\
34proc setcolor {canv colors} {
35    set i 1
36    foreach c $colors {
37        $canv itemconfigure $i -fill $c -outline $c
38        incr i
39    }
40}
41'''
42
43# Tcl event types
44BTNDOWN = 4
45BTNUP = 5
46BTNDRAG = 6
47
48SPACE = ' '
49
50
51
52def constant(numchips):
53    step = 255.0 / (numchips - 1)
54    start = 0.0
55    seq = []
56    while numchips > 0:
57        seq.append(int(start))
58        start = start + step
59        numchips = numchips - 1
60    return seq
61
62# red variations, green+blue = cyan constant
63def constant_red_generator(numchips, red, green, blue):
64    seq = constant(numchips)
65    return map(None, [red] * numchips, seq, seq)
66
67# green variations, red+blue = magenta constant
68def constant_green_generator(numchips, red, green, blue):
69    seq = constant(numchips)
70    return map(None, seq, [green] * numchips, seq)
71
72# blue variations, red+green = yellow constant
73def constant_blue_generator(numchips, red, green, blue):
74    seq = constant(numchips)
75    return map(None, seq, seq, [blue] * numchips)
76
77# red variations, green+blue = cyan constant
78def constant_cyan_generator(numchips, red, green, blue):
79    seq = constant(numchips)
80    return map(None, seq, [green] * numchips, [blue] * numchips)
81
82# green variations, red+blue = magenta constant
83def constant_magenta_generator(numchips, red, green, blue):
84    seq = constant(numchips)
85    return map(None, [red] * numchips, seq, [blue] * numchips)
86
87# blue variations, red+green = yellow constant
88def constant_yellow_generator(numchips, red, green, blue):
89    seq = constant(numchips)
90    return map(None, [red] * numchips, [green] * numchips, seq)
91
92
93
94class LeftArrow:
95    _ARROWWIDTH = 30
96    _ARROWHEIGHT = 15
97    _YOFFSET = 13
98    _TEXTYOFFSET = 1
99    _TAG = ('leftarrow',)
100
101    def __init__(self, canvas, x):
102        self._canvas = canvas
103        self.__arrow, self.__text = self._create(x)
104        self.move_to(x)
105
106    def _create(self, x):
107        arrow = self._canvas.create_line(
108            x, self._ARROWHEIGHT + self._YOFFSET,
109            x, self._YOFFSET,
110            x + self._ARROWWIDTH, self._YOFFSET,
111            arrow='first',
112            width=3.0,
113            tags=self._TAG)
114        text = self._canvas.create_text(
115            x + self._ARROWWIDTH + 13,
116            self._ARROWHEIGHT - self._TEXTYOFFSET,
117            tags=self._TAG,
118            text='128')
119        return arrow, text
120
121    def _x(self):
122        coords = self._canvas.coords(self._TAG)
123        assert coords
124        return coords[0]
125
126    def move_to(self, x):
127        deltax = x - self._x()
128        self._canvas.move(self._TAG, deltax, 0)
129
130    def set_text(self, text):
131        self._canvas.itemconfigure(self.__text, text=text)
132
133
134class RightArrow(LeftArrow):
135    _TAG = ('rightarrow',)
136
137    def _create(self, x):
138        arrow = self._canvas.create_line(
139            x, self._YOFFSET,
140            x + self._ARROWWIDTH, self._YOFFSET,
141            x + self._ARROWWIDTH, self._ARROWHEIGHT + self._YOFFSET,
142            arrow='last',
143            width=3.0,
144            tags=self._TAG)
145        text = self._canvas.create_text(
146            x - self._ARROWWIDTH + 15,            # BAW: kludge
147            self._ARROWHEIGHT - self._TEXTYOFFSET,
148            justify=RIGHT,
149            text='128',
150            tags=self._TAG)
151        return arrow, text
152
153    def _x(self):
154        coords = self._canvas.coords(self._TAG)
155        assert coords
156        return coords[0] + self._ARROWWIDTH
157
158
159
160class StripWidget:
161    _CHIPHEIGHT = 50
162    _CHIPWIDTH = 10
163    _NUMCHIPS = 40
164
165    def __init__(self, switchboard,
166                 master     = None,
167                 chipwidth  = _CHIPWIDTH,
168                 chipheight = _CHIPHEIGHT,
169                 numchips   = _NUMCHIPS,
170                 generator  = None,
171                 axis       = None,
172                 label      = '',
173                 uwdvar     = None,
174                 hexvar     = None):
175        # instance variables
176        self.__generator = generator
177        self.__axis = axis
178        self.__numchips = numchips
179        assert self.__axis in (0, 1, 2)
180        self.__uwd = uwdvar
181        self.__hexp = hexvar
182        # the last chip selected
183        self.__lastchip = None
184        self.__sb = switchboard
185
186        canvaswidth = numchips * (chipwidth + 1)
187        canvasheight = chipheight + 43            # BAW: Kludge
188
189        # create the canvas and pack it
190        canvas = self.__canvas = Canvas(master,
191                                        width=canvaswidth,
192                                        height=canvasheight,
193##                                        borderwidth=2,
194##                                        relief=GROOVE
195                                        )
196
197        canvas.pack()
198        canvas.bind('<ButtonPress-1>', self.__select_chip)
199        canvas.bind('<ButtonRelease-1>', self.__select_chip)
200        canvas.bind('<B1-Motion>', self.__select_chip)
201
202        # Load a proc into the Tcl interpreter.  This is used in the
203        # set_color() method to speed up setting the chip colors.
204        canvas.tk.eval(TCLPROC)
205
206        # create the color strip
207        chips = self.__chips = []
208        x = 1
209        y = 30
210        tags = ('chip',)
211        for c in range(self.__numchips):
212            color = 'grey'
213            canvas.create_rectangle(
214                x, y, x+chipwidth, y+chipheight,
215                fill=color, outline=color,
216                tags=tags)
217            x = x + chipwidth + 1                 # for outline
218            chips.append(color)
219
220        # create the strip label
221        self.__label = canvas.create_text(
222            3, y + chipheight + 8,
223            text=label,
224            anchor=W)
225
226        # create the arrow and text item
227        chipx = self.__arrow_x(0)
228        self.__leftarrow = LeftArrow(canvas, chipx)
229
230        chipx = self.__arrow_x(len(chips) - 1)
231        self.__rightarrow = RightArrow(canvas, chipx)
232
233    def __arrow_x(self, chipnum):
234        coords = self.__canvas.coords(chipnum+1)
235        assert coords
236        x0, y0, x1, y1 = coords
237        return (x1 + x0) / 2.0
238
239    # Invoked when one of the chips is clicked.  This should just tell the
240    # switchboard to set the color on all the output components
241    def __select_chip(self, event=None):
242        x = event.x
243        y = event.y
244        canvas = self.__canvas
245        chip = canvas.find_overlapping(x, y, x, y)
246        if chip and (1 <= chip[0] <= self.__numchips):
247            color = self.__chips[chip[0]-1]
248            red, green, blue = ColorDB.rrggbb_to_triplet(color)
249            etype = int(event.type)
250            if (etype == BTNUP or self.__uwd.get()):
251                # update everyone
252                self.__sb.update_views(red, green, blue)
253            else:
254                # just track the arrows
255                self.__trackarrow(chip[0], (red, green, blue))
256
257    def __trackarrow(self, chip, rgbtuple):
258        # invert the last chip
259        if self.__lastchip is not None:
260            color = self.__canvas.itemcget(self.__lastchip, 'fill')
261            self.__canvas.itemconfigure(self.__lastchip, outline=color)
262        self.__lastchip = chip
263        # get the arrow's text
264        coloraxis = rgbtuple[self.__axis]
265        if self.__hexp.get():
266            # hex
267            text = hex(coloraxis)
268        else:
269            # decimal
270            text = repr(coloraxis)
271        # move the arrow, and set its text
272        if coloraxis <= 128:
273            # use the left arrow
274            self.__leftarrow.set_text(text)
275            self.__leftarrow.move_to(self.__arrow_x(chip-1))
276            self.__rightarrow.move_to(-100)
277        else:
278            # use the right arrow
279            self.__rightarrow.set_text(text)
280            self.__rightarrow.move_to(self.__arrow_x(chip-1))
281            self.__leftarrow.move_to(-100)
282        # and set the chip's outline
283        brightness = ColorDB.triplet_to_brightness(rgbtuple)
284        if brightness <= 128:
285            outline = 'white'
286        else:
287            outline = 'black'
288        self.__canvas.itemconfigure(chip, outline=outline)
289
290
291    def update_yourself(self, red, green, blue):
292        assert self.__generator
293        i = 1
294        chip = 0
295        chips = self.__chips = []
296        tk = self.__canvas.tk
297        # get the red, green, and blue components for all chips
298        for t in self.__generator(self.__numchips, red, green, blue):
299            rrggbb = ColorDB.triplet_to_rrggbb(t)
300            chips.append(rrggbb)
301            tred, tgreen, tblue = t
302            if tred <= red and tgreen <= green and tblue <= blue:
303                chip = i
304            i = i + 1
305        # call the raw tcl script
306        colors = SPACE.join(chips)
307        tk.eval('setcolor %s {%s}' % (self.__canvas._w, colors))
308        # move the arrows around
309        self.__trackarrow(chip, (red, green, blue))
310
311    def set(self, label, generator):
312        self.__canvas.itemconfigure(self.__label, text=label)
313        self.__generator = generator
314
315
316class StripViewer:
317    def __init__(self, switchboard, master=None):
318        self.__sb = switchboard
319        optiondb = switchboard.optiondb()
320        # create a frame inside the master.
321        frame = Frame(master, relief=RAISED, borderwidth=1)
322        frame.grid(row=1, column=0, columnspan=2, sticky='NSEW')
323        # create the options to be used later
324        uwd = self.__uwdvar = BooleanVar()
325        uwd.set(optiondb.get('UPWHILEDRAG', 0))
326        hexp = self.__hexpvar = BooleanVar()
327        hexp.set(optiondb.get('HEXSTRIP', 0))
328        # create the red, green, blue strips inside their own frame
329        frame1 = Frame(frame)
330        frame1.pack(expand=YES, fill=BOTH)
331        self.__reds = StripWidget(switchboard, frame1,
332                                  generator=constant_cyan_generator,
333                                  axis=0,
334                                  label='Red Variations',
335                                  uwdvar=uwd, hexvar=hexp)
336
337        self.__greens = StripWidget(switchboard, frame1,
338                                    generator=constant_magenta_generator,
339                                    axis=1,
340                                    label='Green Variations',
341                                    uwdvar=uwd, hexvar=hexp)
342
343        self.__blues = StripWidget(switchboard, frame1,
344                                   generator=constant_yellow_generator,
345                                   axis=2,
346                                   label='Blue Variations',
347                                   uwdvar=uwd, hexvar=hexp)
348
349        # create a frame to contain the controls
350        frame2 = Frame(frame)
351        frame2.pack(expand=YES, fill=BOTH)
352        frame2.columnconfigure(0, weight=20)
353        frame2.columnconfigure(2, weight=20)
354
355        padx = 8
356
357        # create the black button
358        blackbtn = Button(frame2,
359                          text='Black',
360                          command=self.__toblack)
361        blackbtn.grid(row=0, column=0, rowspan=2, sticky=W, padx=padx)
362
363        # create the controls
364        uwdbtn = Checkbutton(frame2,
365                             text='Update while dragging',
366                             variable=uwd)
367        uwdbtn.grid(row=0, column=1, sticky=W)
368        hexbtn = Checkbutton(frame2,
369                             text='Hexadecimal',
370                             variable=hexp,
371                             command=self.__togglehex)
372        hexbtn.grid(row=1, column=1, sticky=W)
373
374        # XXX: ignore this feature for now; it doesn't work quite right yet
375
376##        gentypevar = self.__gentypevar = IntVar()
377##        self.__variations = Radiobutton(frame,
378##                                        text='Variations',
379##                                        variable=gentypevar,
380##                                        value=0,
381##                                        command=self.__togglegentype)
382##        self.__variations.grid(row=0, column=1, sticky=W)
383##        self.__constants = Radiobutton(frame,
384##                                       text='Constants',
385##                                       variable=gentypevar,
386##                                       value=1,
387##                                       command=self.__togglegentype)
388##        self.__constants.grid(row=1, column=1, sticky=W)
389
390        # create the white button
391        whitebtn = Button(frame2,
392                          text='White',
393                          command=self.__towhite)
394        whitebtn.grid(row=0, column=2, rowspan=2, sticky=E, padx=padx)
395
396    def update_yourself(self, red, green, blue):
397        self.__reds.update_yourself(red, green, blue)
398        self.__greens.update_yourself(red, green, blue)
399        self.__blues.update_yourself(red, green, blue)
400
401    def __togglehex(self, event=None):
402        red, green, blue = self.__sb.current_rgb()
403        self.update_yourself(red, green, blue)
404
405##    def __togglegentype(self, event=None):
406##        which = self.__gentypevar.get()
407##        if which == 0:
408##            self.__reds.set(label='Red Variations',
409##                            generator=constant_cyan_generator)
410##            self.__greens.set(label='Green Variations',
411##                              generator=constant_magenta_generator)
412##            self.__blues.set(label='Blue Variations',
413##                             generator=constant_yellow_generator)
414##        elif which == 1:
415##            self.__reds.set(label='Red Constant',
416##                            generator=constant_red_generator)
417##            self.__greens.set(label='Green Constant',
418##                              generator=constant_green_generator)
419##            self.__blues.set(label='Blue Constant',
420##                             generator=constant_blue_generator)
421##        else:
422##            assert 0
423##        self.__sb.update_views_current()
424
425    def __toblack(self, event=None):
426        self.__sb.update_views(0, 0, 0)
427
428    def __towhite(self, event=None):
429        self.__sb.update_views(255, 255, 255)
430
431    def save_options(self, optiondb):
432        optiondb['UPWHILEDRAG'] = self.__uwdvar.get()
433        optiondb['HEXSTRIP'] = self.__hexpvar.get()
434