Fix the point class. event.is_hint is set only for motion-notify events.
[pysize.git] / ui / gtk / pysize_widget.py
blob33fd30e924fbb152f410f1e672900227ac86f888
1 import math
2 import cairo
3 import pygtk
4 import pango
5 pygtk.require('2.0')
6 import gtk
7 assert gtk.pygtk_version >= (2, 8)
8 import gobject
10 from core.pysize_global_fs_cache import drop_caches
11 from core.pysize_fs_tree import pysize_tree
12 from ui.utils import human_unit, min_size_to_consider
13 from threaded_pysize_tree import threaded_pysize_tree
15 RADIUS = 20.0
16 LINE_WIDTH = 4.0
17 CAIRO_IS_FAST = True
19 class point_class:
20 def __init__(self, x, y):
21 self.x = x
22 self.y = y
24 class PysizeWidget(gtk.DrawingArea):
25 __gsignals__ = {
26 'hover-changed':
27 (gobject.SIGNAL_RUN_LAST,
28 gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
29 'building-tree-state-changed':
30 (gobject.SIGNAL_RUN_LAST,
31 gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,))
34 def __init__(self, options, args):
35 super(PysizeWidget, self).__init__()
36 self.connect('expose-event', self._expose)
37 self.connect('realize', self._realize)
38 self.connect('motion-notify-event', self._motion)
39 self.connect('configure-event', self._configure)
40 self.connect('key-press-event', self._key_press)
41 self.connect('button-press-event', self._button_press)
42 self.connect('button-release-event', self._button_release)
43 self.set_flags(gtk.CAN_FOCUS)
44 self.modify_font(pango.FontDescription('Monospace 12'))
45 self.options = options
46 self.args = args
47 self.selected_nodes = []
48 self._set_cursor_node(None)
49 self.button_press_node = None
50 self.tree = pysize_tree(None)
51 self.tree_builder = threaded_pysize_tree()
52 self.tree_builder.completion.add_observer(self._new_tree_callback)
54 # An entry needs at least the equivalent of 2*RADIUS in height
55 def _get_requested_height(self):
56 return 2 * RADIUS * self.tree.root.size / \
57 min_size_to_consider(self.options.min_size)
59 # Called when the threaded_pysize_tree built a new tree for us
60 def _new_tree_callback(self, tree):
61 if tree:
62 self.tree = tree
63 self.queue_draw()
64 emit_sig = lambda: self.emit('building-tree-state-changed', not tree)
65 gobject.idle_add(emit_sig)
66 if self.options.min_size != 'auto' and tree:
67 height = self._get_requested_height()
68 self.set_size_request(-1, int(height))
70 def _set_cursor_node(self, cursor_node):
71 self.cursor_node = cursor_node
72 self.emit('hover-changed', cursor_node)
74 def _event_point(self, event):
75 if event.is_hint:
76 x, y, state = event.window.get_pointer()
77 point = point_class(x, y)
78 else:
79 point = point_class(event.x, event.y)
80 return point
82 def _get_node_here(self, event):
83 point = point_class(event.x, event.y)
84 for node in self.tree.root:
85 if node.contains_point(point):
86 return node
88 def _queue_node_redraw(self, node):
89 if node:
90 (x0, x1, y0, y1) = map(int, node.rectangle)
91 self.queue_draw_area(x0, y0, x1 - x0, y1 - y0)
93 def _button_press(self, widget, event):
94 node = self._get_node_here(event)
95 if node != self.button_press_node:
96 self._queue_node_redraw(self.button_press_node)
97 self._queue_node_redraw(node)
98 self.button_press_node = node
100 def _button_release(self, widget, event):
101 node = self._get_node_here(event)
102 self._queue_node_redraw(self.button_press_node)
103 self._queue_node_redraw(node)
104 same_as_press = node == self.button_press_node
105 self.button_press_node = None
106 if same_as_press:
107 for n in self.selected_nodes:
108 self._queue_node_redraw(n)
109 self._queue_node_redraw(node)
110 if event.state & gtk.gdk.CONTROL_MASK:
111 self.selected_nodes.append(node)
112 else:
113 self.selected_nodes = [node]
115 def _key_press(self, widget, event):
116 directions = {
117 gtk.keysyms.Left: self.tree.get_parent,
118 gtk.keysyms.Up: self.tree.get_previous_sibling,
119 gtk.keysyms.Right: self.tree.get_first_child,
120 gtk.keysyms.Down: self.tree.get_next_sibling
123 action = directions.get(event.keyval, None)
124 if action and not self.cursor_node is None:
125 new_selection = action(self.cursor_node)
126 if new_selection not in (None, self.tree.root):
127 self._queue_node_redraw(self.cursor_node)
128 self._queue_node_redraw(new_selection)
129 self._set_cursor_node(new_selection)
131 def _realize(self, widget):
132 events = self.window.get_events()
133 events |= gtk.gdk.POINTER_MOTION_MASK
134 events |= gtk.gdk.POINTER_MOTION_HINT_MASK
135 events |= gtk.gdk.KEY_PRESS_MASK
136 events |= gtk.gdk.BUTTON_PRESS_MASK
137 events |= gtk.gdk.BUTTON_RELEASE_MASK
138 self.window.set_events(events)
139 self.window.get_pointer()
141 def refresh_tree(self):
142 drop_caches()
143 self._schedule_new_tree()
145 def _schedule_new_tree(self):
146 self.tree_builder.schedule(self.args[0], self.options.max_depth,
147 min_size_to_consider(self.options.min_size,
148 self.allocation.height/
149 (2*RADIUS)))
151 def _configure(self, widget, event):
152 self._schedule_new_tree()
154 def _motion(self, widget, event):
155 point = self._event_point(event)
156 prev_selection = new_selection = self.cursor_node
157 if not (self.cursor_node and self.cursor_node.contains_point(point)):
158 for node in self.tree.root:
159 was_selected = node == prev_selection
160 if node.contains_point(point) != (node == prev_selection):
161 if node == prev_selection:
162 new_selection = None
163 else:
164 new_selection = node
165 self._queue_node_redraw(prev_selection)
166 self._queue_node_redraw(new_selection)
167 if prev_selection == new_selection:
168 self.window.get_pointer()
169 else:
170 self._set_cursor_node(new_selection)
172 def _expose(self, widget, event):
173 context = widget.window.cairo_create()
175 # set a clip region for the expose event
176 context.rectangle(event.area.x, event.area.y,
177 event.area.width, event.area.height)
178 context.clip()
180 self._draw(context)
181 self.window.get_pointer()
182 return False
184 def _draw_text(self, context, text, x0, x1, y0, y1):
185 pl = self.create_pango_layout(text)
186 w = x1 - x0
187 h = y1 - y0
188 pl.set_width(int(w*pango.SCALE))
189 pl.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
190 (real_w, real_h) = pl.get_pixel_size()
191 if real_w > w or real_h > h:
192 return False
193 if real_w+w/20.0 < w:
194 x0 += w/20.0
195 context.move_to(x0, y0+(h-real_h)/2.0)
196 context.set_source_rgb(0, 0, 0)
197 context.show_layout(pl)
198 return True
200 def _get_node_colors(self, node, colors):
201 if node == self.cursor_node:
202 colors = map(lambda c: c+0.1, colors)
203 if node in self.selected_nodes + [self.button_press_node]:
204 colors = map(lambda c: c-0.3, colors)
205 return colors
207 def _draw_box(self, context, x0, x1, y0, y1, node):
208 if x0 == 0.0:
209 x0 += LINE_WIDTH/2.0
210 else:
211 x0 += LINE_WIDTH/4.0
212 x1 -= LINE_WIDTH/4.0
213 if y0 == 0.0:
214 y0 += LINE_WIDTH/2.0
215 else:
216 y0 += LINE_WIDTH/4.0
217 y1 -= LINE_WIDTH/4.0
218 node.rectangle = (x0, x1, y0, y1)
220 if CAIRO_IS_FAST:
221 context.new_path()
222 context.arc(x0 + RADIUS, y0 + RADIUS, RADIUS,
223 - math.pi, - math.pi / 2.0)
224 context.rel_line_to(x1 - x0 - 2*RADIUS, 0)
225 context.arc(x1 - RADIUS, y0 + RADIUS, RADIUS,
226 - math.pi / 2.0, 0)
227 context.rel_line_to(0, y1 - y0 - 2*RADIUS)
228 context.arc(x1 - RADIUS, y1 - RADIUS, RADIUS,
229 0, math.pi / 2.0)
230 context.rel_line_to(- x1 + x0 + 2*RADIUS, 0)
231 context.arc(x0 + RADIUS, y1 - RADIUS, RADIUS,
232 math.pi / 2.0, math.pi)
233 context.close_path()
235 context.set_source_rgb(0, 0, 0)
236 context.stroke_preserve()
238 gradient = cairo.LinearGradient(0, y0, 0, y1)
239 colors = self._get_node_colors(node, [0.5, 0.5, 1.0, 0.0, 0.5, 1.0])
240 gradient.add_color_stop_rgb(0.0, *colors[:3])
241 gradient.add_color_stop_rgb(1.0, *colors[3:])
242 context.set_source(gradient)
243 context.fill()
244 else:
245 context.rectangle(x0, y0, x1 - x0, y1 - y0)
246 context.set_source_rgb(0, 0, 0)
247 context.stroke_preserve()
249 colors = self._get_node_colors(node, [0, 0.5, 1.0])
250 context.set_source_rgb(*colors)
251 context.fill()
253 name = node.get_name()
254 size = human_unit(node.size)
255 if not self._draw_text(context, name + '\n' + size, x0, x1, y0, y1):
256 first, second = name, size
257 if not node.is_real():
258 first, second = second, first
259 if not self._draw_text(context, first, x0, x1, y0, y1):
260 self._draw_text(context, second, x0, x1, y0, y1)
262 def _draw_boxes(self, context, node, depth, offset):
263 w = self.allocation.width
264 if (self.options.min_size == 'auto'):
265 h = self.allocation.height
266 else:
267 h = self._get_requested_height()
268 x0 = depth * (w - 1.0) / self.tree.height
269 x1 = (depth + 1.0) * (w - 1.0) / self.tree.height
270 y0 = (h - 1.0) * offset / self.tree.root.size
271 y1 = (h - 1.0) * (offset + node.size) / self.tree.root.size
273 self._draw_box(context, x0, x1, y0, y1, node)
274 depth += 1
275 for child in node.get_children():
276 self._draw_boxes(context, child, depth, offset)
277 offset += child.size
279 def _draw(self, context):
280 context.set_line_width(LINE_WIDTH)
281 offset = 0
282 for child in self.tree.root.get_children():
283 self._draw_boxes(context, child, 0, offset)
284 offset += child.size