Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 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 | # if there are warnings while processing a package |
| 178 | # (parent), mark the task with warning color; |
| 179 | # in case there are errors, the updates will be |
| 180 | # handled on TaskFailed. |
| 181 | if color == HobColors.WARNING and parent: |
| 182 | self.model.set(parent, self.model.COL_COLOR, color) |
| 183 | if task: #then we have a parent (package), and update it's color |
| 184 | self.model.set(self.tasks_to_iter[(package, None)], self.model.COL_COLOR, color) |
| 185 | |
| 186 | elif isinstance(event, bb.build.TaskStarted): |
| 187 | (package, task) = (event._package, event._task) |
| 188 | |
| 189 | # Save out this PID. |
| 190 | self.pids_to_task[pid] = (package, task) |
| 191 | |
| 192 | # Check if we already have this package in our model. If so then |
| 193 | # that can be the parent for the task. Otherwise we create a new |
| 194 | # top level for the package. |
| 195 | if ((package, None) in self.tasks_to_iter): |
| 196 | parent = self.tasks_to_iter[(package, None)] |
| 197 | else: |
| 198 | if self.sequential: |
| 199 | add = self.model.append |
| 200 | else: |
| 201 | add = self.model.prepend |
| 202 | parent = add(None, (None, |
| 203 | package, |
| 204 | None, |
| 205 | "Package: %s" % (package), |
| 206 | None, |
| 207 | HobColors.OK, |
| 208 | 0)) |
| 209 | self.tasks_to_iter[(package, None)] = parent |
| 210 | |
| 211 | # Because this parent package now has an active child mark it as |
| 212 | # such. |
| 213 | self.model.set(parent, self.model.COL_ICON, "gtk-execute") |
| 214 | parent_color = self.model.get(parent, self.model.COL_COLOR)[0] |
| 215 | if parent_color != HobColors.ERROR and parent_color != HobColors.WARNING: |
| 216 | self.model.set(parent, self.model.COL_COLOR, HobColors.RUNNING) |
| 217 | |
| 218 | # Add an entry in the model for this task |
| 219 | i = self.model.append (parent, (None, |
| 220 | package, |
| 221 | task, |
| 222 | "Task: %s" % (task), |
| 223 | "gtk-execute", |
| 224 | HobColors.RUNNING, |
| 225 | 0)) |
| 226 | |
| 227 | # update the parent's active task count |
| 228 | num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1 |
| 229 | self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active) |
| 230 | |
| 231 | # Save out the iter so that we can find it when we have a message |
| 232 | # that we need to attach to a task. |
| 233 | self.tasks_to_iter[(package, task)] = i |
| 234 | |
| 235 | elif isinstance(event, bb.build.TaskBase): |
| 236 | self.emit("log", "info", event._message) |
| 237 | current = self.tasks_to_iter[(package, task)] |
| 238 | parent = self.tasks_to_iter[(package, None)] |
| 239 | |
| 240 | # remove this task from the parent's active count |
| 241 | num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1 |
| 242 | self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active) |
| 243 | |
| 244 | if isinstance(event, bb.build.TaskFailed): |
| 245 | # Mark the task and parent as failed |
| 246 | icon = "dialog-error" |
| 247 | color = HobColors.ERROR |
| 248 | |
| 249 | logfile = event.logfile |
| 250 | if logfile and os.path.exists(logfile): |
| 251 | with open(logfile) as f: |
| 252 | logdata = f.read() |
| 253 | self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', HobColors.OK, 0)) |
| 254 | |
| 255 | for i in (current, parent): |
| 256 | self.model.set(i, self.model.COL_ICON, icon, |
| 257 | self.model.COL_COLOR, color) |
| 258 | else: |
| 259 | # Mark the parent package and the task as inactive, |
| 260 | # but make sure to preserve error, warnings and active |
| 261 | # states |
| 262 | parent_color = self.model.get(parent, self.model.COL_COLOR)[0] |
| 263 | task_color = self.model.get(current, self.model.COL_COLOR)[0] |
| 264 | |
| 265 | # Mark the task as inactive |
| 266 | self.model.set(current, self.model.COL_ICON, None) |
| 267 | if task_color != HobColors.ERROR: |
| 268 | if task_color == HobColors.WARNING: |
| 269 | self.model.set(current, self.model.COL_ICON, 'dialog-warning') |
| 270 | else: |
| 271 | self.model.set(current, self.model.COL_COLOR, HobColors.OK) |
| 272 | |
| 273 | # Mark the parent as inactive |
| 274 | if parent_color != HobColors.ERROR: |
| 275 | if parent_color == HobColors.WARNING: |
| 276 | self.model.set(parent, self.model.COL_ICON, "dialog-warning") |
| 277 | else: |
| 278 | self.model.set(parent, self.model.COL_ICON, None) |
| 279 | if num_active == 0: |
| 280 | self.model.set(parent, self.model.COL_COLOR, HobColors.OK) |
| 281 | |
| 282 | # Clear the iters and the pids since when the task goes away the |
| 283 | # pid will no longer be used for messages |
| 284 | del self.tasks_to_iter[(package, task)] |
| 285 | del self.pids_to_task[pid] |
| 286 | |
| 287 | elif isinstance(event, bb.event.BuildStarted): |
| 288 | |
| 289 | self.emit("build-started") |
| 290 | self.model.prepend(None, (None, |
| 291 | None, |
| 292 | None, |
| 293 | "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), |
| 294 | None, |
| 295 | HobColors.OK, |
| 296 | 0)) |
| 297 | if pbar: |
| 298 | pbar.update(0, self.progress_total) |
| 299 | pbar.set_title(bb.event.getName(event)) |
| 300 | |
| 301 | elif isinstance(event, bb.event.BuildCompleted): |
| 302 | failures = int (event._failures) |
| 303 | self.model.prepend(None, (None, |
| 304 | None, |
| 305 | None, |
| 306 | "Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), |
| 307 | None, |
| 308 | HobColors.OK, |
| 309 | 0)) |
| 310 | |
| 311 | # Emit the appropriate signal depending on the number of failures |
| 312 | if self.buildaborted: |
| 313 | self.emit ("build-aborted") |
| 314 | self.buildaborted = False |
| 315 | elif (failures >= 1): |
| 316 | self.emit ("build-failed") |
| 317 | else: |
| 318 | self.emit ("build-succeeded") |
| 319 | # Emit a generic "build-complete" signal for things wishing to |
| 320 | # handle when the build is finished |
| 321 | self.emit("build-complete") |
| 322 | # reset the all cell's icon indicator |
| 323 | self.model.close_task_refresh() |
| 324 | if pbar: |
| 325 | pbar.set_text(event.msg) |
| 326 | |
| 327 | elif isinstance(event, bb.event.DiskFull): |
| 328 | self.buildaborted = True |
| 329 | self.emit("disk-full") |
| 330 | |
| 331 | elif isinstance(event, bb.command.CommandFailed): |
| 332 | self.emit("log", "error", "Command execution failed: %s" % (event.error)) |
| 333 | if event.error.startswith("Exited with"): |
| 334 | # If the command fails with an exit code we're done, emit the |
| 335 | # generic signal for the UI to notify the user |
| 336 | self.emit("build-complete") |
| 337 | # reset the all cell's icon indicator |
| 338 | self.model.close_task_refresh() |
| 339 | |
| 340 | elif isinstance(event, bb.event.CacheLoadStarted) and pbar: |
| 341 | pbar.set_title("Loading cache") |
| 342 | self.progress_total = event.total |
| 343 | pbar.update(0, self.progress_total) |
| 344 | elif isinstance(event, bb.event.CacheLoadProgress) and pbar: |
| 345 | pbar.update(event.current, self.progress_total) |
| 346 | elif isinstance(event, bb.event.CacheLoadCompleted) and pbar: |
| 347 | pbar.update(self.progress_total, self.progress_total) |
| 348 | pbar.hide() |
| 349 | elif isinstance(event, bb.event.ParseStarted) and pbar: |
| 350 | if event.total == 0: |
| 351 | return |
| 352 | pbar.set_title("Processing recipes") |
| 353 | self.progress_total = event.total |
| 354 | pbar.update(0, self.progress_total) |
| 355 | elif isinstance(event, bb.event.ParseProgress) and pbar: |
| 356 | pbar.update(event.current, self.progress_total) |
| 357 | elif isinstance(event, bb.event.ParseCompleted) and pbar: |
| 358 | pbar.hide() |
| 359 | #using runqueue events as many as possible to update the progress bar |
| 360 | elif isinstance(event, bb.runqueue.runQueueTaskFailed): |
| 361 | self.emit("log", "error", "Task %s (%s) failed with exit code '%s'" % (event.taskid, event.taskstring, event.exitcode)) |
| 362 | elif isinstance(event, bb.runqueue.sceneQueueTaskFailed): |
| 363 | self.emit("log", "warn", "Setscene task %s (%s) failed with exit code '%s' - real task will be run instead" \ |
| 364 | % (event.taskid, event.taskstring, event.exitcode)) |
| 365 | elif isinstance(event, (bb.runqueue.runQueueTaskStarted, bb.runqueue.sceneQueueTaskStarted)): |
| 366 | if isinstance(event, bb.runqueue.sceneQueueTaskStarted): |
| 367 | self.emit("log", "info", "Running setscene task %d of %d (%s)" % \ |
| 368 | (event.stats.completed + event.stats.active + event.stats.failed + 1, |
| 369 | event.stats.total, event.taskstring)) |
| 370 | else: |
| 371 | if event.noexec: |
| 372 | tasktype = 'noexec task' |
| 373 | else: |
| 374 | tasktype = 'task' |
| 375 | self.emit("log", "info", "Running %s %s of %s (ID: %s, %s)" % \ |
| 376 | (tasktype, event.stats.completed + event.stats.active + event.stats.failed + 1, |
| 377 | event.stats.total, event.taskid, event.taskstring)) |
| 378 | message = {} |
| 379 | message["eventname"] = bb.event.getName(event) |
| 380 | num_of_completed = event.stats.completed + event.stats.failed |
| 381 | message["current"] = num_of_completed |
| 382 | message["total"] = event.stats.total |
| 383 | message["title"] = "" |
| 384 | message["task"] = event.taskstring |
| 385 | self.emit("task-started", message) |
| 386 | elif isinstance(event, bb.event.MultipleProviders): |
| 387 | self.emit("log", "info", "multiple providers are available for %s%s (%s)" \ |
| 388 | % (event._is_runtime and "runtime " or "", event._item, ", ".join(event._candidates))) |
| 389 | self.emit("log", "info", "consider defining a PREFERRED_PROVIDER entry to match %s" % (event._item)) |
| 390 | elif isinstance(event, bb.event.NoProvider): |
| 391 | msg = "" |
| 392 | if event._runtime: |
| 393 | r = "R" |
| 394 | else: |
| 395 | r = "" |
| 396 | |
| 397 | extra = '' |
| 398 | if not event._reasons: |
| 399 | if event._close_matches: |
| 400 | extra = ". Close matches:\n %s" % '\n '.join(event._close_matches) |
| 401 | |
| 402 | if event._dependees: |
| 403 | msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s\n" % (r, event._item, ", ".join(event._dependees), r, extra) |
| 404 | else: |
| 405 | msg = "Nothing %sPROVIDES '%s'%s\n" % (r, event._item, extra) |
| 406 | if event._reasons: |
| 407 | for reason in event._reasons: |
| 408 | msg += ("%s\n" % reason) |
| 409 | self.emit("no-provider", msg) |
| 410 | self.emit("log", "error", msg) |
| 411 | elif isinstance(event, bb.event.LogExecTTY): |
| 412 | icon = "dialog-warning" |
| 413 | color = HobColors.WARNING |
| 414 | if self.sequential or not parent: |
| 415 | tree_add = self.model.append |
| 416 | else: |
| 417 | tree_add = self.model.prepend |
| 418 | tree_add(parent, |
| 419 | (None, |
| 420 | package, |
| 421 | task, |
| 422 | event.msg, |
| 423 | icon, |
| 424 | color, |
| 425 | 0)) |
| 426 | else: |
| 427 | if not isinstance(event, (bb.event.BuildBase, |
| 428 | bb.event.StampUpdate, |
| 429 | bb.event.ConfigParsed, |
| 430 | bb.event.RecipeParsed, |
| 431 | bb.event.RecipePreFinalise, |
| 432 | bb.runqueue.runQueueEvent, |
| 433 | bb.runqueue.runQueueExitWait, |
| 434 | bb.event.OperationStarted, |
| 435 | bb.event.OperationCompleted, |
| 436 | bb.event.OperationProgress)): |
| 437 | self.emit("log", "error", "Unknown event: %s" % (event.error if hasattr(event, 'error') else 'error')) |
| 438 | |
| 439 | return |
| 440 | |
| 441 | |
| 442 | def do_pastebin(text): |
| 443 | url = 'http://pastebin.com/api_public.php' |
| 444 | params = {'paste_code': text, 'paste_format': 'text'} |
| 445 | |
| 446 | req = urllib2.Request(url, urllib.urlencode(params)) |
| 447 | response = urllib2.urlopen(req) |
| 448 | paste_url = response.read() |
| 449 | |
| 450 | return paste_url |
| 451 | |
| 452 | |
| 453 | class RunningBuildTreeView (gtk.TreeView): |
| 454 | __gsignals__ = { |
| 455 | "button_press_event" : "override" |
| 456 | } |
| 457 | def __init__ (self, readonly=False, hob=False): |
| 458 | gtk.TreeView.__init__ (self) |
| 459 | self.readonly = readonly |
| 460 | |
| 461 | # The icon that indicates whether we're building or failed. |
| 462 | # add 'hob' flag because there has not only hob to share this code |
| 463 | if hob: |
| 464 | renderer = HobCellRendererPixbuf () |
| 465 | else: |
| 466 | renderer = gtk.CellRendererPixbuf() |
| 467 | col = gtk.TreeViewColumn ("Status", renderer) |
| 468 | col.add_attribute (renderer, "icon-name", 4) |
| 469 | self.append_column (col) |
| 470 | |
| 471 | # The message of the build. |
| 472 | # add 'hob' flag because there has not only hob to share this code |
| 473 | if hob: |
| 474 | self.message_renderer = HobWarpCellRendererText (col_number=1) |
| 475 | else: |
| 476 | self.message_renderer = gtk.CellRendererText () |
| 477 | self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3) |
| 478 | self.message_column.add_attribute(self.message_renderer, 'background', 5) |
| 479 | self.message_renderer.set_property('editable', (not self.readonly)) |
| 480 | self.append_column (self.message_column) |
| 481 | |
| 482 | def do_button_press_event(self, event): |
| 483 | gtk.TreeView.do_button_press_event(self, event) |
| 484 | |
| 485 | if event.button == 3: |
| 486 | selection = super(RunningBuildTreeView, self).get_selection() |
| 487 | (model, it) = selection.get_selected() |
| 488 | if it is not None: |
| 489 | can_paste = model.get(it, model.COL_LOG)[0] |
| 490 | if can_paste == 'pastebin': |
| 491 | # build a simple menu with a pastebin option |
| 492 | menu = gtk.Menu() |
| 493 | menuitem = gtk.MenuItem("Copy") |
| 494 | menu.append(menuitem) |
| 495 | menuitem.connect("activate", self.clipboard_handler, (model, it)) |
| 496 | menuitem.show() |
| 497 | menuitem = gtk.MenuItem("Send log to pastebin") |
| 498 | menu.append(menuitem) |
| 499 | menuitem.connect("activate", self.pastebin_handler, (model, it)) |
| 500 | menuitem.show() |
| 501 | menu.show() |
| 502 | menu.popup(None, None, None, event.button, event.time) |
| 503 | |
| 504 | def _add_to_clipboard(self, clipping): |
| 505 | """ |
| 506 | Add the contents of clipping to the system clipboard. |
| 507 | """ |
| 508 | clipboard = gtk.clipboard_get() |
| 509 | clipboard.set_text(clipping) |
| 510 | clipboard.store() |
| 511 | |
| 512 | def pastebin_handler(self, widget, data): |
| 513 | """ |
| 514 | Send the log data to pastebin, then add the new paste url to the |
| 515 | clipboard. |
| 516 | """ |
| 517 | (model, it) = data |
| 518 | paste_url = do_pastebin(model.get(it, model.COL_MESSAGE)[0]) |
| 519 | |
| 520 | # @todo Provide visual feedback to the user that it is done and that |
| 521 | # it worked. |
| 522 | print paste_url |
| 523 | |
| 524 | self._add_to_clipboard(paste_url) |
| 525 | |
| 526 | def clipboard_handler(self, widget, data): |
| 527 | """ |
| 528 | """ |
| 529 | (model, it) = data |
| 530 | message = model.get(it, model.COL_MESSAGE)[0] |
| 531 | |
| 532 | self._add_to_clipboard(message) |
| 533 | |
| 534 | class BuildFailureTreeView(gtk.TreeView): |
| 535 | |
| 536 | def __init__ (self): |
| 537 | gtk.TreeView.__init__(self) |
| 538 | self.set_rules_hint(False) |
| 539 | self.set_headers_visible(False) |
| 540 | self.get_selection().set_mode(gtk.SELECTION_SINGLE) |
| 541 | |
| 542 | # The icon that indicates whether we're building or failed. |
| 543 | renderer = HobCellRendererPixbuf () |
| 544 | col = gtk.TreeViewColumn ("Status", renderer) |
| 545 | col.add_attribute (renderer, "icon-name", RunningBuildModel.COL_ICON) |
| 546 | self.append_column (col) |
| 547 | |
| 548 | # The message of the build. |
| 549 | self.message_renderer = HobWarpCellRendererText (col_number=1) |
| 550 | self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=RunningBuildModel.COL_MESSAGE, background=RunningBuildModel.COL_COLOR) |
| 551 | self.append_column (self.message_column) |