diff options
Diffstat (limited to 'bitbake/lib/bb/ui/crumbs/runningbuild.py')
-rw-r--r-- | bitbake/lib/bb/ui/crumbs/runningbuild.py | 533 |
1 files changed, 533 insertions, 0 deletions
diff --git a/bitbake/lib/bb/ui/crumbs/runningbuild.py b/bitbake/lib/bb/ui/crumbs/runningbuild.py new file mode 100644 index 0000000000..abd3300149 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/runningbuild.py | |||
@@ -0,0 +1,533 @@ | |||
1 | |||
2 | # | ||
3 | # BitBake Graphical GTK User Interface | ||
4 | # | ||
5 | # Copyright (C) 2008 Intel Corporation | ||
6 | # | ||
7 | # Authored by Rob Bradford <rob@linux.intel.com> | ||
8 | # | ||
9 | # This program is free software; you can redistribute it and/or modify | ||
10 | # it under the terms of the GNU General Public License version 2 as | ||
11 | # published by the Free Software Foundation. | ||
12 | # | ||
13 | # This program is distributed in the hope that it will be useful, | ||
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
16 | # GNU General Public License for more details. | ||
17 | # | ||
18 | # You should have received a copy of the GNU General Public License along | ||
19 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
21 | |||
22 | import gtk | ||
23 | import gobject | ||
24 | import logging | ||
25 | import time | ||
26 | import urllib | ||
27 | import urllib2 | ||
28 | import pango | ||
29 | from bb.ui.crumbs.hobcolor import HobColors | ||
30 | from bb.ui.crumbs.hobwidget import HobWarpCellRendererText, HobCellRendererPixbuf | ||
31 | |||
32 | class RunningBuildModel (gtk.TreeStore): | ||
33 | (COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = range(7) | ||
34 | |||
35 | def __init__ (self): | ||
36 | gtk.TreeStore.__init__ (self, | ||
37 | gobject.TYPE_STRING, | ||
38 | gobject.TYPE_STRING, | ||
39 | gobject.TYPE_STRING, | ||
40 | gobject.TYPE_STRING, | ||
41 | gobject.TYPE_STRING, | ||
42 | gobject.TYPE_STRING, | ||
43 | gobject.TYPE_INT) | ||
44 | |||
45 | def failure_model_filter(self, model, it): | ||
46 | color = model.get(it, self.COL_COLOR)[0] | ||
47 | if not color: | ||
48 | return False | ||
49 | if color == HobColors.ERROR or color == HobColors.WARNING: | ||
50 | return True | ||
51 | return False | ||
52 | |||
53 | def failure_model(self): | ||
54 | model = self.filter_new() | ||
55 | model.set_visible_func(self.failure_model_filter) | ||
56 | return model | ||
57 | |||
58 | def foreach_cell_func(self, model, path, iter, usr_data=None): | ||
59 | if model.get_value(iter, self.COL_ICON) == "gtk-execute": | ||
60 | model.set(iter, self.COL_ICON, "") | ||
61 | |||
62 | def close_task_refresh(self): | ||
63 | self.foreach(self.foreach_cell_func, None) | ||
64 | |||
65 | class RunningBuild (gobject.GObject): | ||
66 | __gsignals__ = { | ||
67 | 'build-started' : (gobject.SIGNAL_RUN_LAST, | ||
68 | gobject.TYPE_NONE, | ||
69 | ()), | ||
70 | 'build-succeeded' : (gobject.SIGNAL_RUN_LAST, | ||
71 | gobject.TYPE_NONE, | ||
72 | ()), | ||
73 | 'build-failed' : (gobject.SIGNAL_RUN_LAST, | ||
74 | gobject.TYPE_NONE, | ||
75 | ()), | ||
76 | 'build-complete' : (gobject.SIGNAL_RUN_LAST, | ||
77 | gobject.TYPE_NONE, | ||
78 | ()), | ||
79 | 'build-aborted' : (gobject.SIGNAL_RUN_LAST, | ||
80 | gobject.TYPE_NONE, | ||
81 | ()), | ||
82 | 'task-started' : (gobject.SIGNAL_RUN_LAST, | ||
83 | gobject.TYPE_NONE, | ||
84 | (gobject.TYPE_PYOBJECT,)), | ||
85 | 'log-error' : (gobject.SIGNAL_RUN_LAST, | ||
86 | gobject.TYPE_NONE, | ||
87 | ()), | ||
88 | 'log-warning' : (gobject.SIGNAL_RUN_LAST, | ||
89 | gobject.TYPE_NONE, | ||
90 | ()), | ||
91 | 'disk-full' : (gobject.SIGNAL_RUN_LAST, | ||
92 | gobject.TYPE_NONE, | ||
93 | ()), | ||
94 | 'no-provider' : (gobject.SIGNAL_RUN_LAST, | ||
95 | gobject.TYPE_NONE, | ||
96 | (gobject.TYPE_PYOBJECT,)), | ||
97 | 'log' : (gobject.SIGNAL_RUN_LAST, | ||
98 | gobject.TYPE_NONE, | ||
99 | (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)), | ||
100 | } | ||
101 | pids_to_task = {} | ||
102 | tasks_to_iter = {} | ||
103 | |||
104 | def __init__ (self, sequential=False): | ||
105 | gobject.GObject.__init__ (self) | ||
106 | self.model = RunningBuildModel() | ||
107 | self.sequential = sequential | ||
108 | self.buildaborted = False | ||
109 | |||
110 | def reset (self): | ||
111 | self.pids_to_task.clear() | ||
112 | self.tasks_to_iter.clear() | ||
113 | self.model.clear() | ||
114 | |||
115 | def handle_event (self, event, pbar=None): | ||
116 | # Handle an event from the event queue, this may result in updating | ||
117 | # the model and thus the UI. Or it may be to tell us that the build | ||
118 | # has finished successfully (or not, as the case may be.) | ||
119 | |||
120 | parent = None | ||
121 | pid = 0 | ||
122 | package = None | ||
123 | task = None | ||
124 | |||
125 | # If we have a pid attached to this message/event try and get the | ||
126 | # (package, task) pair for it. If we get that then get the parent iter | ||
127 | # for the message. | ||
128 | if hasattr(event, 'pid'): | ||
129 | pid = event.pid | ||
130 | if hasattr(event, 'process'): | ||
131 | pid = event.process | ||
132 | |||
133 | if pid and pid in self.pids_to_task: | ||
134 | (package, task) = self.pids_to_task[pid] | ||
135 | parent = self.tasks_to_iter[(package, task)] | ||
136 | |||
137 | if(isinstance(event, logging.LogRecord)): | ||
138 | if event.taskpid == 0 or event.levelno > logging.INFO: | ||
139 | self.emit("log", "handle", event) | ||
140 | # FIXME: this is a hack! More info in Yocto #1433 | ||
141 | # http://bugzilla.pokylinux.org/show_bug.cgi?id=1433, temporarily | ||
142 | # mask the error message as it's not informative for the user. | ||
143 | if event.msg.startswith("Execution of event handler 'run_buildstats' failed"): | ||
144 | return | ||
145 | |||
146 | if (event.levelno < logging.INFO or | ||
147 | event.msg.startswith("Running task")): | ||
148 | return # don't add these to the list | ||
149 | |||
150 | if event.levelno >= logging.ERROR: | ||
151 | icon = "dialog-error" | ||
152 | color = HobColors.ERROR | ||
153 | self.emit("log-error") | ||
154 | elif event.levelno >= logging.WARNING: | ||
155 | icon = "dialog-warning" | ||
156 | color = HobColors.WARNING | ||
157 | self.emit("log-warning") | ||
158 | else: | ||
159 | icon = None | ||
160 | color = HobColors.OK | ||
161 | |||
162 | # if we know which package we belong to, we'll append onto its list. | ||
163 | # otherwise, we'll jump to the top of the master list | ||
164 | if self.sequential or not parent: | ||
165 | tree_add = self.model.append | ||
166 | else: | ||
167 | tree_add = self.model.prepend | ||
168 | tree_add(parent, | ||
169 | (None, | ||
170 | package, | ||
171 | task, | ||
172 | event.getMessage(), | ||
173 | icon, | ||
174 | color, | ||
175 | 0)) | ||
176 | |||
177 | elif isinstance(event, bb.build.TaskStarted): | ||
178 | (package, task) = (event._package, event._task) | ||
179 | |||
180 | # Save out this PID. | ||
181 | self.pids_to_task[pid] = (package, task) | ||
182 | |||
183 | # Check if we already have this package in our model. If so then | ||
184 | # that can be the parent for the task. Otherwise we create a new | ||
185 | # top level for the package. | ||
186 | if ((package, None) in self.tasks_to_iter): | ||
187 | parent = self.tasks_to_iter[(package, None)] | ||
188 | else: | ||
189 | if self.sequential: | ||
190 | add = self.model.append | ||
191 | else: | ||
192 | add = self.model.prepend | ||
193 | parent = add(None, (None, | ||
194 | package, | ||
195 | None, | ||
196 | "Package: %s" % (package), | ||
197 | None, | ||
198 | HobColors.OK, | ||
199 | 0)) | ||
200 | self.tasks_to_iter[(package, None)] = parent | ||
201 | |||
202 | # Because this parent package now has an active child mark it as | ||
203 | # such. | ||
204 | # @todo if parent is already in error, don't mark it green | ||
205 | self.model.set(parent, self.model.COL_ICON, "gtk-execute", | ||
206 | self.model.COL_COLOR, HobColors.RUNNING) | ||
207 | |||
208 | # Add an entry in the model for this task | ||
209 | i = self.model.append (parent, (None, | ||
210 | package, | ||
211 | task, | ||
212 | "Task: %s" % (task), | ||
213 | "gtk-execute", | ||
214 | HobColors.RUNNING, | ||
215 | 0)) | ||
216 | |||
217 | # update the parent's active task count | ||
218 | num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1 | ||
219 | self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active) | ||
220 | |||
221 | # Save out the iter so that we can find it when we have a message | ||
222 | # that we need to attach to a task. | ||
223 | self.tasks_to_iter[(package, task)] = i | ||
224 | |||
225 | elif isinstance(event, bb.build.TaskBase): | ||
226 | self.emit("log", "info", event._message) | ||
227 | current = self.tasks_to_iter[(package, task)] | ||
228 | parent = self.tasks_to_iter[(package, None)] | ||
229 | |||
230 | # remove this task from the parent's active count | ||
231 | num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1 | ||
232 | self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active) | ||
233 | |||
234 | if isinstance(event, bb.build.TaskFailed): | ||
235 | # Mark the task and parent as failed | ||
236 | icon = "dialog-error" | ||
237 | color = HobColors.ERROR | ||
238 | |||
239 | logfile = event.logfile | ||
240 | if logfile and os.path.exists(logfile): | ||
241 | with open(logfile) as f: | ||
242 | logdata = f.read() | ||
243 | self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', HobColors.OK, 0)) | ||
244 | |||
245 | for i in (current, parent): | ||
246 | self.model.set(i, self.model.COL_ICON, icon, | ||
247 | self.model.COL_COLOR, color) | ||
248 | else: | ||
249 | icon = None | ||
250 | color = HobColors.OK | ||
251 | |||
252 | # Mark the task as inactive | ||
253 | self.model.set(current, self.model.COL_ICON, icon, | ||
254 | self.model.COL_COLOR, color) | ||
255 | |||
256 | # Mark the parent package as inactive, but make sure to | ||
257 | # preserve error and active states | ||
258 | i = self.tasks_to_iter[(package, None)] | ||
259 | if self.model.get(parent, self.model.COL_ICON) != 'dialog-error': | ||
260 | self.model.set(parent, self.model.COL_ICON, icon) | ||
261 | if num_active == 0: | ||
262 | self.model.set(parent, self.model.COL_COLOR, HobColors.OK) | ||
263 | |||
264 | # Clear the iters and the pids since when the task goes away the | ||
265 | # pid will no longer be used for messages | ||
266 | del self.tasks_to_iter[(package, task)] | ||
267 | del self.pids_to_task[pid] | ||
268 | |||
269 | elif isinstance(event, bb.event.BuildStarted): | ||
270 | |||
271 | self.emit("build-started") | ||
272 | self.model.prepend(None, (None, | ||
273 | None, | ||
274 | None, | ||
275 | "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), | ||
276 | None, | ||
277 | HobColors.OK, | ||
278 | 0)) | ||
279 | if pbar: | ||
280 | pbar.update(0, self.progress_total) | ||
281 | pbar.set_title(bb.event.getName(event)) | ||
282 | |||
283 | elif isinstance(event, bb.event.BuildCompleted): | ||
284 | failures = int (event._failures) | ||
285 | self.model.prepend(None, (None, | ||
286 | None, | ||
287 | None, | ||
288 | "Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), | ||
289 | None, | ||
290 | HobColors.OK, | ||
291 | 0)) | ||
292 | |||
293 | # Emit the appropriate signal depending on the number of failures | ||
294 | if self.buildaborted: | ||
295 | self.emit ("build-aborted") | ||
296 | self.buildaborted = False | ||
297 | elif (failures >= 1): | ||
298 | self.emit ("build-failed") | ||
299 | else: | ||
300 | self.emit ("build-succeeded") | ||
301 | # Emit a generic "build-complete" signal for things wishing to | ||
302 | # handle when the build is finished | ||
303 | self.emit("build-complete") | ||
304 | # reset the all cell's icon indicator | ||
305 | self.model.close_task_refresh() | ||
306 | if pbar: | ||
307 | pbar.set_text(event.msg) | ||
308 | |||
309 | elif isinstance(event, bb.event.DiskFull): | ||
310 | self.buildaborted = True | ||
311 | self.emit("disk-full") | ||
312 | |||
313 | elif isinstance(event, bb.command.CommandFailed): | ||
314 | self.emit("log", "error", "Command execution failed: %s" % (event.error)) | ||
315 | if event.error.startswith("Exited with"): | ||
316 | # If the command fails with an exit code we're done, emit the | ||
317 | # generic signal for the UI to notify the user | ||
318 | self.emit("build-complete") | ||
319 | # reset the all cell's icon indicator | ||
320 | self.model.close_task_refresh() | ||
321 | |||
322 | elif isinstance(event, bb.event.CacheLoadStarted) and pbar: | ||
323 | pbar.set_title("Loading cache") | ||
324 | self.progress_total = event.total | ||
325 | pbar.update(0, self.progress_total) | ||
326 | elif isinstance(event, bb.event.CacheLoadProgress) and pbar: | ||
327 | pbar.update(event.current, self.progress_total) | ||
328 | elif isinstance(event, bb.event.CacheLoadCompleted) and pbar: | ||
329 | pbar.update(self.progress_total, self.progress_total) | ||
330 | pbar.hide() | ||
331 | elif isinstance(event, bb.event.ParseStarted) and pbar: | ||
332 | if event.total == 0: | ||
333 | return | ||
334 | pbar.set_title("Processing recipes") | ||
335 | self.progress_total = event.total | ||
336 | pbar.update(0, self.progress_total) | ||
337 | elif isinstance(event, bb.event.ParseProgress) and pbar: | ||
338 | pbar.update(event.current, self.progress_total) | ||
339 | elif isinstance(event, bb.event.ParseCompleted) and pbar: | ||
340 | pbar.hide() | ||
341 | #using runqueue events as many as possible to update the progress bar | ||
342 | elif isinstance(event, bb.runqueue.runQueueTaskFailed): | ||
343 | self.emit("log", "error", "Task %s (%s) failed with exit code '%s'" % (event.taskid, event.taskstring, event.exitcode)) | ||
344 | elif isinstance(event, bb.runqueue.sceneQueueTaskFailed): | ||
345 | self.emit("log", "warn", "Setscene task %s (%s) failed with exit code '%s' - real task will be run instead" \ | ||
346 | % (event.taskid, event.taskstring, event.exitcode)) | ||
347 | elif isinstance(event, (bb.runqueue.runQueueTaskStarted, bb.runqueue.sceneQueueTaskStarted)): | ||
348 | if isinstance(event, bb.runqueue.sceneQueueTaskStarted): | ||
349 | self.emit("log", "info", "Running setscene task %d of %d (%s)" % \ | ||
350 | (event.stats.completed + event.stats.active + event.stats.failed + 1, | ||
351 | event.stats.total, event.taskstring)) | ||
352 | else: | ||
353 | if event.noexec: | ||
354 | tasktype = 'noexec task' | ||
355 | else: | ||
356 | tasktype = 'task' | ||
357 | self.emit("log", "info", "Running %s %s of %s (ID: %s, %s)" % \ | ||
358 | (tasktype, event.stats.completed + event.stats.active + event.stats.failed + 1, | ||
359 | event.stats.total, event.taskid, event.taskstring)) | ||
360 | message = {} | ||
361 | message["eventname"] = bb.event.getName(event) | ||
362 | num_of_completed = event.stats.completed + event.stats.failed | ||
363 | message["current"] = num_of_completed | ||
364 | message["total"] = event.stats.total | ||
365 | message["title"] = "" | ||
366 | message["task"] = event.taskstring | ||
367 | self.emit("task-started", message) | ||
368 | elif isinstance(event, bb.event.MultipleProviders): | ||
369 | self.emit("log", "info", "multiple providers are available for %s%s (%s)" \ | ||
370 | % (event._is_runtime and "runtime " or "", event._item, ", ".join(event._candidates))) | ||
371 | self.emit("log", "info", "consider defining a PREFERRED_PROVIDER entry to match %s" % (event._item)) | ||
372 | elif isinstance(event, bb.event.NoProvider): | ||
373 | msg = "" | ||
374 | if event._runtime: | ||
375 | r = "R" | ||
376 | else: | ||
377 | r = "" | ||
378 | |||
379 | extra = '' | ||
380 | if not event._reasons: | ||
381 | if event._close_matches: | ||
382 | extra = ". Close matches:\n %s" % '\n '.join(event._close_matches) | ||
383 | |||
384 | if event._dependees: | ||
385 | msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s\n" % (r, event._item, ", ".join(event._dependees), r, extra) | ||
386 | else: | ||
387 | msg = "Nothing %sPROVIDES '%s'%s\n" % (r, event._item, extra) | ||
388 | if event._reasons: | ||
389 | for reason in event._reasons: | ||
390 | msg += ("%s\n" % reason) | ||
391 | self.emit("no-provider", msg) | ||
392 | self.emit("log", "error", msg) | ||
393 | elif isinstance(event, bb.event.LogExecTTY): | ||
394 | icon = "dialog-warning" | ||
395 | color = HobColors.WARNING | ||
396 | if self.sequential or not parent: | ||
397 | tree_add = self.model.append | ||
398 | else: | ||
399 | tree_add = self.model.prepend | ||
400 | tree_add(parent, | ||
401 | (None, | ||
402 | package, | ||
403 | task, | ||
404 | event.msg, | ||
405 | icon, | ||
406 | color, | ||
407 | 0)) | ||
408 | else: | ||
409 | if not isinstance(event, (bb.event.BuildBase, | ||
410 | bb.event.StampUpdate, | ||
411 | bb.event.ConfigParsed, | ||
412 | bb.event.RecipeParsed, | ||
413 | bb.event.RecipePreFinalise, | ||
414 | bb.runqueue.runQueueEvent, | ||
415 | bb.runqueue.runQueueExitWait, | ||
416 | bb.event.OperationStarted, | ||
417 | bb.event.OperationCompleted, | ||
418 | bb.event.OperationProgress)): | ||
419 | self.emit("log", "error", "Unknown event: %s" % (event.error if hasattr(event, 'error') else 'error')) | ||
420 | |||
421 | return | ||
422 | |||
423 | |||
424 | def do_pastebin(text): | ||
425 | url = 'http://pastebin.com/api_public.php' | ||
426 | params = {'paste_code': text, 'paste_format': 'text'} | ||
427 | |||
428 | req = urllib2.Request(url, urllib.urlencode(params)) | ||
429 | response = urllib2.urlopen(req) | ||
430 | paste_url = response.read() | ||
431 | |||
432 | return paste_url | ||
433 | |||
434 | |||
435 | class RunningBuildTreeView (gtk.TreeView): | ||
436 | __gsignals__ = { | ||
437 | "button_press_event" : "override" | ||
438 | } | ||
439 | def __init__ (self, readonly=False, hob=False): | ||
440 | gtk.TreeView.__init__ (self) | ||
441 | self.readonly = readonly | ||
442 | |||
443 | # The icon that indicates whether we're building or failed. | ||
444 | # add 'hob' flag because there has not only hob to share this code | ||
445 | if hob: | ||
446 | renderer = HobCellRendererPixbuf () | ||
447 | else: | ||
448 | renderer = gtk.CellRendererPixbuf() | ||
449 | col = gtk.TreeViewColumn ("Status", renderer) | ||
450 | col.add_attribute (renderer, "icon-name", 4) | ||
451 | self.append_column (col) | ||
452 | |||
453 | # The message of the build. | ||
454 | # add 'hob' flag because there has not only hob to share this code | ||
455 | if hob: | ||
456 | self.message_renderer = HobWarpCellRendererText (col_number=1) | ||
457 | else: | ||
458 | self.message_renderer = gtk.CellRendererText () | ||
459 | self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3) | ||
460 | self.message_column.add_attribute(self.message_renderer, 'background', 5) | ||
461 | self.message_renderer.set_property('editable', (not self.readonly)) | ||
462 | self.append_column (self.message_column) | ||
463 | |||
464 | def do_button_press_event(self, event): | ||
465 | gtk.TreeView.do_button_press_event(self, event) | ||
466 | |||
467 | if event.button == 3: | ||
468 | selection = super(RunningBuildTreeView, self).get_selection() | ||
469 | (model, it) = selection.get_selected() | ||
470 | if it is not None: | ||
471 | can_paste = model.get(it, model.COL_LOG)[0] | ||
472 | if can_paste == 'pastebin': | ||
473 | # build a simple menu with a pastebin option | ||
474 | menu = gtk.Menu() | ||
475 | menuitem = gtk.MenuItem("Copy") | ||
476 | menu.append(menuitem) | ||
477 | menuitem.connect("activate", self.clipboard_handler, (model, it)) | ||
478 | menuitem.show() | ||
479 | menuitem = gtk.MenuItem("Send log to pastebin") | ||
480 | menu.append(menuitem) | ||
481 | menuitem.connect("activate", self.pastebin_handler, (model, it)) | ||
482 | menuitem.show() | ||
483 | menu.show() | ||
484 | menu.popup(None, None, None, event.button, event.time) | ||
485 | |||
486 | def _add_to_clipboard(self, clipping): | ||
487 | """ | ||
488 | Add the contents of clipping to the system clipboard. | ||
489 | """ | ||
490 | clipboard = gtk.clipboard_get() | ||
491 | clipboard.set_text(clipping) | ||
492 | clipboard.store() | ||
493 | |||
494 | def pastebin_handler(self, widget, data): | ||
495 | """ | ||
496 | Send the log data to pastebin, then add the new paste url to the | ||
497 | clipboard. | ||
498 | """ | ||
499 | (model, it) = data | ||
500 | paste_url = do_pastebin(model.get(it, model.COL_MESSAGE)[0]) | ||
501 | |||
502 | # @todo Provide visual feedback to the user that it is done and that | ||
503 | # it worked. | ||
504 | print paste_url | ||
505 | |||
506 | self._add_to_clipboard(paste_url) | ||
507 | |||
508 | def clipboard_handler(self, widget, data): | ||
509 | """ | ||
510 | """ | ||
511 | (model, it) = data | ||
512 | message = model.get(it, model.COL_MESSAGE)[0] | ||
513 | |||
514 | self._add_to_clipboard(message) | ||
515 | |||
516 | class BuildFailureTreeView(gtk.TreeView): | ||
517 | |||
518 | def __init__ (self): | ||
519 | gtk.TreeView.__init__(self) | ||
520 | self.set_rules_hint(False) | ||
521 | self.set_headers_visible(False) | ||
522 | self.get_selection().set_mode(gtk.SELECTION_SINGLE) | ||
523 | |||
524 | # The icon that indicates whether we're building or failed. | ||
525 | renderer = HobCellRendererPixbuf () | ||
526 | col = gtk.TreeViewColumn ("Status", renderer) | ||
527 | col.add_attribute (renderer, "icon-name", RunningBuildModel.COL_ICON) | ||
528 | self.append_column (col) | ||
529 | |||
530 | # The message of the build. | ||
531 | self.message_renderer = HobWarpCellRendererText (col_number=1) | ||
532 | self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=RunningBuildModel.COL_MESSAGE, background=RunningBuildModel.COL_COLOR) | ||
533 | self.append_column (self.message_column) | ||