1 # This file is part of MyPaint.
2 # Copyright (C) 2008 by Martin Renold <martinxyz@gmx.ch>
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.
11 from lib import helpers
13 from warnings import warn
16 ITEM_SIZE_DEFAULT = 48
18 class PixbufList(gtk.DrawingArea):
19 # interface to be implemented by children
20 def on_select(self, item):
22 def on_drag_data(self, copy, source_widget, brush_name, target_idx):
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
29 # GType naming, for GtkBuilder
30 __gtype_name__ = 'PixbufList'
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)
38 if itemlist is not None:
39 self.itemlist = itemlist
41 warn("Creating standalone, empty itemlist for testing", RuntimeWarning, 2)
43 self.pixbuffunc = pixbuffunc
44 self.namefunc = namefunc
45 self.dragging_allowed = True
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)
55 self.tooltip_text = None
56 self.in_potential_drag = False
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)
71 self.get_settings().set_property("gtk-dnd-drag-threshold",
72 int(min(item_w, item_h) * 0.75))
74 self.realized_once = False
75 self.connect("realize", self.on_realize)
77 self.drag_highlighted = False
78 self.drag_insertion_index = None
81 def on_realize(self, widget):
82 if self.realized_once:
84 self.realized_once = True
85 if self.dragging_allowed:
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
100 def set_size(self, item_w, item_h):
105 def motion_notify_cb(self, widget, event):
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)
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
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
122 self.set_tooltip_text(item_name)
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
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
141 def drag_motion_cb(self, widget, context, x, y, time):
142 if not self.dragging_allowed:
145 source_widget = context.get_source_widget()
146 if self is source_widget:
147 # Only moves are possible
148 action = gdk.ACTION_MOVE
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
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
166 if self.drag_highlighted:
168 if i != self.drag_insertion_index:
170 self.drag_insertion_index = i
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
179 def drag_data_get_cb(self, widget, context, selection, targetType, time):
181 assert item in self.itemlist
182 assert targetType == DRAG_ITEM_NAME
183 name = self.namefunc(item)
184 selection.set(selection.target, 8, name)
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)
194 def update(self, width = None, height = None):
196 Redraws the widget from scratch.
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
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 ) ))
210 height = self.tiles_h * self.total_h
211 self.set_size_request(self.total_w, height)
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
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)
228 def set_selected(self, item):
232 def index(self, x,y):
233 x, y = int(x), int(y)
235 if i >= self.tiles_w: i = self.tiles_w - 1
237 i = i + self.tiles_w * (y / self.total_h)
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
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):
250 i = self.index(ex, ey)
251 if i >= len(self.itemlist): return
252 item = self.itemlist[i]
253 self.set_selected(item)
255 self.in_potential_drag = True
257 def button_release_cb(self, widget, event):
258 self.in_potential_drag = False
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:
264 self.update(size.width, size.height)
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()
270 self.window.draw_rectangle(widget.style.base_gc[gtk.STATE_NORMAL],
271 True, 0, 0, p_w, p_h)
273 if self.drag_highlighted:
274 self.window.draw_rectangle(widget.style.black_gc, False, 0, 0, p_w-1, p_h-1)
276 widget.window.draw_pixbuf(widget.style.black_gc,
282 last_i = len(self.itemlist) - 1
283 for b in self.itemlist:
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
294 def shrink(pixels, 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):
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)
312 if __name__ == '__main__':
314 win.set_title("pixbuflist test")
315 test_list = PixbufList()
317 test_list.set_size_request(256, 128)
318 win.connect("destroy", lambda *a: gtk.main_quit())