Consistent naming: items are actually nodes
[pysize.git] / ui / gtk / pysize_widget.py
blobc9b4285fee839957977ca118944f28a5723ccde3
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)
9 from core.pysize_fs_tree import pysize_tree
10 from ui.utils import human_unit
12 RADIUS = 20.0
13 LINE_WIDTH = 4.0
14 CAIRO_IS_FAST = True
16 class PysizeWidget(gtk.DrawingArea):
17 def __init__(self, options, args):
18 super(PysizeWidget, self).__init__()
19 self.connect('expose-event', self._expose)
20 self.connect('realize', self._realize)
21 self.connect('motion-notify-event', self._motion)
22 self.connect('configure-event', self._configure)
23 self.connect('key-press-event', self._key_press)
24 self.connect('button-press-event', self._button_press)
25 self.connect('button-release-event', self._button_release)
26 self.set_flags(gtk.CAN_FOCUS)
27 self.modify_font(pango.FontDescription('Monospace 12'))
28 self.options = options
29 self.args = args
30 self.needs_tree = True
31 self.tree = pysize_tree(args[0], options.max_depth, 1.0)
32 self.selected_nodes = []
33 self.cursor_node = None
34 self.button_press_node = None
36 def _get_node_here(self, event):
37 for node in self.tree.root:
38 if node.contains_point(event):
39 return node
41 def _queue_node_redraw(self, node):
42 if node:
43 (x0, x1, y0, y1) = map(int, node.rectangle)
44 self.queue_draw_area(x0, y0, x1 - x0, y1 - y0)
46 def _button_press(self, widget, event):
47 node = self._get_node_here(event)
48 if not node is self.button_press_node:
49 self._queue_node_redraw(self.button_press_node)
50 self._queue_node_redraw(node)
51 self.button_press_node = node
53 def _button_release(self, widget, event):
54 node = self._get_node_here(event)
55 self._queue_node_redraw(self.button_press_node)
56 self._queue_node_redraw(node)
57 same_as_press = node is self.button_press_node
58 self.button_press_node = None
59 if same_as_press:
60 for n in self.selected_nodes:
61 self._queue_node_redraw(n)
62 self._queue_node_redraw(node)
63 if event.state & gtk.gdk.CONTROL_MASK:
64 self.selected_nodes.append(node)
65 else:
66 self.selected_nodes = [node]
68 def _key_press(self, widget, event):
69 directions = {
70 gtk.keysyms.Left: self.tree.get_parent,
71 gtk.keysyms.Up: self.tree.get_previous_sibling,
72 gtk.keysyms.Right: self.tree.get_first_child,
73 gtk.keysyms.Down: self.tree.get_next_sibling
76 action = directions.get(event.keyval, None)
77 if action and not self.cursor_node is None:
78 new_selection = action(self.cursor_node)
79 if not new_selection in (None, self.tree.root):
80 self._queue_node_redraw(self.cursor_node)
81 self._queue_node_redraw(new_selection)
82 self.cursor_node = new_selection
84 def _realize(self, widget):
85 events = self.window.get_events()
86 events |= gtk.gdk.POINTER_MOTION_MASK
87 events |= gtk.gdk.POINTER_MOTION_HINT_MASK
88 events |= gtk.gdk.KEY_PRESS_MASK
89 events |= gtk.gdk.BUTTON_PRESS_MASK
90 events |= gtk.gdk.BUTTON_RELEASE_MASK
91 self.window.set_events(events)
92 self.window.get_pointer()
94 def _configure(self, widget, event):
95 self.needs_tree = True
97 def _motion(self, widget, event):
98 prev_selection = new_selection = self.cursor_node
99 for node in self.tree.root:
100 was_selected = node is prev_selection
101 if node.contains_point(event) != (node is prev_selection):
102 if node is prev_selection:
103 new_selection = None
104 else:
105 new_selection = node
106 self._queue_node_redraw(self.cursor_node)
107 self._queue_node_redraw(new_selection)
108 self.cursor_node = new_selection
109 if prev_selection is new_selection:
110 self.window.get_pointer()
112 def _expose(self, widget, event):
113 context = widget.window.cairo_create()
115 # set a clip region for the expose event
116 context.rectangle(event.area.x, event.area.y,
117 event.area.width, event.area.height)
118 context.clip()
120 self._draw(context)
121 self.window.get_pointer()
122 return False
124 def _draw_text(self, context, text, x0, x1, y0, y1):
125 pl = self.create_pango_layout(text)
126 w = x1 - x0
127 h = y1 - y0
128 pl.set_width(int(w*pango.SCALE))
129 pl.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
130 (real_w, real_h) = pl.get_pixel_size()
131 if real_w > w or real_h > h:
132 return False
133 if real_w+w/20.0 < w:
134 x0 += w/20.0
135 context.move_to(x0, y0+(h-real_h)/2.0)
136 context.set_source_rgb(0, 0, 0)
137 context.show_layout(pl)
138 return True
140 def _get_node_colors(self, node, colors):
141 if node is self.cursor_node:
142 colors = map(lambda c: c+0.2, colors)
143 if node in self.selected_nodes:
144 colors.reverse()
145 if node is self.button_press_node:
146 colors = map(lambda c: c+0.2, colors)
147 return colors
149 def _draw_box(self, context, x0, x1, y0, y1, node):
150 x0 += LINE_WIDTH/4.0
151 x1 -= LINE_WIDTH/4.0
152 y0 += LINE_WIDTH/4.0
153 y1 -= LINE_WIDTH/4.0
154 node.rectangle = (x0, x1, y0, y1)
156 if CAIRO_IS_FAST:
157 context.new_path()
158 context.arc(x0 + RADIUS, y0 + RADIUS, RADIUS,
159 - math.pi, - math.pi / 2.0)
160 context.rel_line_to(x1 - x0 - 2*RADIUS, 0)
161 context.arc(x1 - RADIUS, y0 + RADIUS, RADIUS,
162 - math.pi / 2.0, 0)
163 context.rel_line_to(0, y1 - y0 - 2*RADIUS)
164 context.arc(x1 - RADIUS, y1 - RADIUS, RADIUS,
165 0, math.pi / 2.0)
166 context.rel_line_to(- x1 + x0 + 2*RADIUS, 0)
167 context.arc(x0 + RADIUS, y1 - RADIUS, RADIUS,
168 math.pi / 2.0, math.pi)
169 context.close_path()
171 context.set_source_rgb(0, 0, 0)
172 context.stroke_preserve()
174 gradient = cairo.LinearGradient(0, y0, 0, y1)
175 colors = self._get_node_colors(node, [0.5, 0.5, 1.0, 0.0, 0.5, 1.0])
176 gradient.add_color_stop_rgb(0.0, *colors[:3])
177 gradient.add_color_stop_rgb(1.0, *colors[3:])
178 context.set_source(gradient)
179 context.fill()
180 else:
181 context.rectangle(x0, y0, x1 - x0, y1 - y0)
182 context.set_source_rgb(0, 0, 0)
183 context.stroke_preserve()
185 colors = self._get_node_colors(node, [0, 0.5, 1.0])
186 context.set_source_rgb(*colors)
187 context.fill()
189 name = node.get_name()
190 size = human_unit(node.size)
191 if not self._draw_text(context, name + '\n' + size, x0, x1, y0, y1):
192 first, second = name, size
193 if not node.is_real():
194 first, second = second, first
195 if not self._draw_text(context, first, x0, x1, y0, y1):
196 self._draw_text(context, second, x0, x1, y0, y1)
198 def _draw_boxes(self, context, node, depth, offset):
199 w = self.allocation.width
200 h = self.allocation.height
201 x0 = depth * (w - 1.0) / self.tree.height
202 x1 = (depth + 1.0) * (w - 1.0) / self.tree.height
203 y0 = (h - 1.0) * offset / self.tree.root.size
204 y1 = (h - 1.0) * (offset + node.size) / self.tree.root.size
206 self._draw_box(context, x0, x1, y0, y1, node)
207 depth += 1
208 for child in node.get_children():
209 self._draw_boxes(context, child, depth, offset)
210 offset += child.size
212 def _draw(self, context):
213 if self.needs_tree:
214 self.tree = pysize_tree(self.args[0], self.options.max_depth,
215 2*RADIUS /self.allocation.height)
216 self.needs_tree = False
217 if not self.tree.root:
218 return
219 context.set_line_width(LINE_WIDTH)
220 offset = 0
221 for child in self.tree.root.get_children():
222 self._draw_boxes(context, child, 0, offset)
223 offset += child.size