OSDN Git Service

version bump
[mypaint-anime/master.git] / gui / pixbuflist.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2008 by Martin Renold <martinxyz@gmx.ch>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import gtk
10 gdk = gtk.gdk
11 from lib import helpers
12 from math import ceil
13 from warnings import warn
14
15 DRAG_ITEM_NAME = 103
16 ITEM_SIZE_DEFAULT = 48
17
18 class PixbufList(gtk.DrawingArea):
19     # interface to be implemented by children
20     def on_select(self, item):
21         pass
22     def on_drag_data(self, copy, source_widget, brush_name, target_idx):
23         return False
24     def drag_begin_cb(self, widget, context):
25         widget.drag_insertion_index = None
26     def drag_end_cb(self, widget, context):
27         widget.drag_insertion_index = None
28
29     # GType naming, for GtkBuilder
30     __gtype_name__ = 'PixbufList'
31
32     def __init__(self, itemlist=None,
33                  item_w=ITEM_SIZE_DEFAULT,
34                  item_h=ITEM_SIZE_DEFAULT,
35                  namefunc=None, pixbuffunc=lambda x: x):
36         gtk.DrawingArea.__init__(self)
37
38         if itemlist is not None:
39             self.itemlist = itemlist
40         else:
41             warn("Creating standalone, empty itemlist for testing", RuntimeWarning, 2)
42             self.itemlist = []
43         self.pixbuffunc = pixbuffunc
44         self.namefunc = namefunc
45         self.dragging_allowed = True
46
47         self.pixbuf = None
48         self.spacing_outside = 0
49         self.border_visible = 2
50         self.border_visible_outside_cell = 1
51         self.spacing_inside = 0
52         self.set_size(item_w, item_h)
53
54         self.selected = None
55         self.tooltip_text = None
56         self.in_potential_drag = False
57
58         self.connect("expose-event", self.expose_cb)
59         self.connect("button-press-event", self.button_press_cb)
60         self.connect("button-release-event", self.button_release_cb)
61         self.connect("configure-event", self.configure_event_cb)
62         self.connect("motion-notify-event", self.motion_notify_cb)
63         self.set_events(gdk.EXPOSURE_MASK |
64                         gdk.BUTTON_PRESS_MASK |
65                         gdk.BUTTON_RELEASE_MASK |
66                         gdk.POINTER_MOTION_MASK |
67                         # Allow switching between mouse and pen inside the widget
68                         gdk.PROXIMITY_OUT_MASK |
69                         gdk.PROXIMITY_IN_MASK)
70
71         self.get_settings().set_property("gtk-dnd-drag-threshold",
72             int(min(item_w, item_h) * 0.75))
73
74         self.realized_once = False
75         self.connect("realize", self.on_realize)
76
77         self.drag_highlighted = False
78         self.drag_insertion_index = None
79         self.update()
80
81     def on_realize(self, widget):
82         if self.realized_once:
83             return
84         self.realized_once = True
85         if self.dragging_allowed:
86             # DnD setup.
87             self.connect('drag-data-get', self.drag_data_get_cb)
88             self.connect('drag-motion', self.drag_motion_cb)
89             self.connect('drag-leave', self.drag_leave_cb)
90             self.connect('drag-begin', self.drag_begin_cb)
91             self.connect('drag-end', self.drag_end_cb)
92             self.connect('drag-data-received', self.drag_data_received_cb)
93             # Users can drag pixbufs *to* anywhere on a pixbuflist at all times.
94             self.drag_dest_set(gtk.DEST_DEFAULT_ALL,
95                     [('LIST_ITEM', gtk.TARGET_SAME_APP, DRAG_ITEM_NAME)],
96                     gdk.ACTION_MOVE | gdk.ACTION_COPY)
97             # Dragging *from* a list can only happen over a pixbuf: see motion_notify_cb
98             self.drag_source_sensitive = False
99
100     def set_size(self, item_w, item_h):
101         self.item_w = item_w
102         self.item_h = item_h
103         self.thumbnails = {}
104
105     def motion_notify_cb(self, widget, event):
106         over_item = False
107         if self.point_is_inside(event.x, event.y):
108             i = self.index(event.x, event.y)
109             over_item = i < len(self.itemlist)
110         if over_item:
111             if self.namefunc is not None:
112                 item = self.itemlist[i]
113                 item_name = self.namefunc(item)
114                 # Tooltip changing has to happen over two motion-notifys
115                 # because we want to force the tooltip box to move with the
116                 # mouse pointer.
117                 if self.tooltip_text != item_name:
118                     self.tooltip_text = item_name
119                     self.set_has_tooltip(False)
120                     # pop down on the 1st event with this name
121                 else:
122                     self.set_tooltip_text(item_name)
123                     # pop up on the 2nd
124             if self.dragging_allowed:
125                 if not self.drag_source_sensitive:
126                     self.drag_source_set(gtk.gdk.BUTTON1_MASK,
127                         [('LIST_ITEM', gtk.TARGET_SAME_APP, DRAG_ITEM_NAME)],
128                         gdk.ACTION_COPY|gdk.ACTION_MOVE)
129                     self.drag_source_sensitive = True
130         else:
131             if self.tooltip_text is not None:
132                 self.set_has_tooltip(False)
133                 self.tooltip_text = None
134             if self.dragging_allowed and self.drag_source_sensitive:
135                 if not self.in_potential_drag:
136                     # If we haven't crossed the drag threshold yet, don't kill
137                     # the potential drag before it starts.
138                     self.drag_source_unset()
139                     self.drag_source_sensitive = False
140
141     def drag_motion_cb(self, widget, context, x, y, time):
142         if not self.dragging_allowed:
143             return False
144         action = None
145         source_widget = context.get_source_widget()
146         if self is source_widget:
147             # Only moves are possible
148             action = gdk.ACTION_MOVE
149         else:
150             # Dragging from another widget, default action is copy
151             action = gdk.ACTION_COPY
152             # However, if the item already exists here, it's a move
153             sel = source_widget.selected
154             if sel in self.itemlist:
155                 action = gdk.ACTION_MOVE
156             else:
157                 # the user can force a move by pressing shift
158                 px, py, kbmods = self.get_window().get_pointer()
159                 if kbmods & gdk.SHIFT_MASK:
160                     action = gdk.ACTION_MOVE
161         context.drag_status(action, time)
162         if not self.drag_highlighted:
163             #self.drag_highlight()   # XXX nonfunctional
164             self.drag_highlighted = True
165             self.queue_draw()
166         if self.drag_highlighted:
167             i = self.index(x, y)
168             if i != self.drag_insertion_index:
169                 self.queue_draw()
170                 self.drag_insertion_index = i
171
172     def drag_leave_cb(self, widget, context, time):
173         if widget.drag_highlighted:
174             #widget.drag_unhighlight()   # XXX nonfunctional
175             widget.drag_highlighted = False
176             widget.drag_insertion_index = None
177             widget.queue_draw()
178
179     def drag_data_get_cb(self, widget, context, selection, targetType, time):
180         item = self.selected
181         assert item in self.itemlist
182         assert targetType == DRAG_ITEM_NAME
183         name = self.namefunc(item)
184         selection.set(selection.target, 8, name)
185
186     def drag_data_received_cb(self, widget, context, x,y, selection, targetType, time):
187         item_name = selection.data
188         target_item_idx = self.index(x, y) # idx always valid, we reject drops at invalid idx
189         w = context.get_source_widget()
190         copy = context.action==gdk.ACTION_COPY
191         success = self.on_drag_data(copy, w, item_name, target_item_idx)
192         context.finish(success, False, time)
193
194     def update(self, width = None, height = None):
195         """
196         Redraws the widget from scratch.
197         """
198         self.total_border = self.border_visible + self.spacing_inside + self.spacing_outside
199         self.total_w = self.item_w + 2*self.total_border
200         self.total_h = self.item_h + 2*self.total_border
201
202         if width is None:
203             if not self.pixbuf: return
204             width = self.pixbuf.get_width()
205             height = self.pixbuf.get_height()
206         width = max(width, self.total_w)
207         self.tiles_w = max(1, int( width / self.total_w ))
208         self.tiles_h = max(1, int( ceil( float(len(self.itemlist)) / self.tiles_w ) ))
209
210         height = self.tiles_h * self.total_h
211         self.set_size_request(self.total_w, height)
212
213         self.pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, True, 8, width, height)
214         self.pixbuf.fill(0xffffff00) # transparent
215         for i, item in enumerate(self.itemlist):
216             x = (i % self.tiles_w) * self.total_w
217             y = (i / self.tiles_w) * self.total_h
218             x += self.total_border
219             y += self.total_border
220
221             pixbuf = self.pixbuffunc(item)
222             if pixbuf not in self.thumbnails:
223                 self.thumbnails[pixbuf] = helpers.pixbuf_thumbnail(pixbuf, self.item_w, self.item_h)
224             pixbuf = self.thumbnails[pixbuf]
225             pixbuf.copy_area(0, 0, self.item_w, self.item_h, self.pixbuf, x, y)
226         self.queue_draw()
227
228     def set_selected(self, item):
229         self.selected = item
230         self.queue_draw()
231
232     def index(self, x,y):
233         x, y = int(x), int(y)
234         i = x / self.total_w
235         if i >= self.tiles_w: i = self.tiles_w - 1
236         if i < 0: i = 0
237         i = i + self.tiles_w * (y / self.total_h)
238         if i < 0: i = 0
239         return i
240
241     def point_is_inside(self, x, y):
242         w = self.allocation.width
243         h = self.allocation.height
244         return x >= 0 and y >= 0 and x < w and y < h
245
246     def button_press_cb(self, widget, event):
247         ex, ey = int(event.x), int(event.y)
248         if not self.point_is_inside(ex, ey):
249             return False
250         i = self.index(ex, ey)
251         if i >= len(self.itemlist): return
252         item = self.itemlist[i]
253         self.set_selected(item)
254         self.on_select(item)
255         self.in_potential_drag = True
256
257     def button_release_cb(self, widget, event):
258         self.in_potential_drag = False
259
260     def configure_event_cb(self, widget, size):
261         if self.pixbuf and self.pixbuf.get_width() == size.width:
262             if self.pixbuf.get_height() == size.height:
263                 return
264         self.update(size.width, size.height)
265
266     def expose_cb(self, widget, event):
267         # cut to maximal size
268         p_w, p_h = self.pixbuf.get_width(), self.pixbuf.get_height()
269
270         self.window.draw_rectangle(widget.style.base_gc[gtk.STATE_NORMAL],
271                                    True, 0, 0, p_w, p_h)
272
273         if self.drag_highlighted:
274             self.window.draw_rectangle(widget.style.black_gc, False, 0, 0, p_w-1, p_h-1)
275
276         widget.window.draw_pixbuf(widget.style.black_gc,
277                                   self.pixbuf,
278                                   0, 0, 0, 0) 
279
280         # draw borders
281         i = 0
282         last_i = len(self.itemlist) - 1
283         for b in self.itemlist:
284             rect_gc = None
285             if b is self.selected:
286                 rect_gc = widget.style.bg_gc[gtk.STATE_SELECTED]
287             elif  i == self.drag_insertion_index \
288               or (i == last_i and self.drag_insertion_index > i):
289                 rect_gc = widget.style.fg_gc[gtk.STATE_NORMAL]
290             x = (i % self.tiles_w) * self.total_w
291             y = (i / self.tiles_w) * self.total_h
292             w = self.total_w
293             h = self.total_h
294             def shrink(pixels, x, y, w, h):
295                 x += pixels
296                 y += pixels
297                 w -= 2*pixels
298                 h -= 2*pixels
299                 return (x, y, w, h)
300             x, y, w, h = shrink(self.spacing_outside, x, y, w, h)
301             for j in range(self.border_visible_outside_cell):
302                 x, y, w, h = shrink(-1, x, y, w, h)
303             for j in range(self.border_visible + self.border_visible_outside_cell):
304                 if rect_gc:
305                     widget.window.draw_rectangle(rect_gc, False, x, y, w-1, h-1)
306                 x, y, w, h = shrink(1, x, y, w, h)
307             i += 1
308
309         return True
310
311
312 if __name__ == '__main__':
313     win = gtk.Window()
314     win.set_title("pixbuflist test")
315     test_list = PixbufList()
316     win.add(test_list)
317     test_list.set_size_request(256, 128)
318     win.connect("destroy", lambda *a: gtk.main_quit())
319     win.show_all()
320     gtk.main()