def popup_theme_legend(self, widget=None): """Popup a theme legend window.""" if self.theme_legend_window is None: self.theme_legend_window = ThemeLegendWindow( self.window, self.theme) self.theme_legend_window.connect("destroy", self.destroy_theme_legend) else: self.theme_legend_window.present()
def popup_theme_legend(self, widget=None): """Popup a theme legend window.""" if self.theme_legend_window is None: self.theme_legend_window = ThemeLegendWindow( self.window, self.theme) self.theme_legend_window.connect( "destroy", self.destroy_theme_legend) else: self.theme_legend_window.present()
def launch_theme_legend(theme): """Launch a theme legend window.""" ThemeLegendWindow(None, theme)
def get_gpanel_scan_menu(suite_keys, theme_name, set_theme_func, has_stopped_suites, clear_stopped_suites_func, scanned_hosts, change_hosts_func, update_now_func, start_func, program_name, extra_items=None, owner=None, is_stopped=False): """Return a right click menu for the gpanel GUI. TODO this used to be for gscan too; simplify now it's only for gpanel? suite_keys should be a list of (host, owner, suite) tuples (if any). theme_name should be the name of the current theme. set_theme_func should be a function accepting a new theme name. has_stopped_suites should be a boolean denoting currently stopped suites. clear_stopped_suites should be a function with no arguments that removes stopped suites from the current view. scanned_hosts should be a list of currently scanned suite hosts. change_hosts_func should be a function accepting a new list of suite hosts to scan. update_now_func should be a function with no arguments that forces an update now or soon. start_func should be a function with no arguments that re-activates idle GUIs. program_name should be a string describing the parent program. extra_items (keyword) should be a list of extra menu items to add to the right click menu. owner (keyword) should be the owner of the suites, if not the current user. is_stopped (keyword) denotes whether the GUI is in an inactive state. """ menu = gtk.Menu() if is_stopped: switch_on_item = gtk.ImageMenuItem("Activate") img = gtk.image_new_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) switch_on_item.set_image(img) switch_on_item.show() switch_on_item.connect("button-press-event", lambda b, e: start_func()) menu.append(switch_on_item) # Construct gcylc launcher items for each relevant suite. for host, owner, suite in suite_keys: gcylc_item = gtk.ImageMenuItem("Launch gcylc: %s - %s@%s" % (suite.replace('_', '__'), owner, host)) img_gcylc = gtk.image_new_from_stock("gcylc", gtk.ICON_SIZE_MENU) gcylc_item.set_image(img_gcylc) gcylc_item._connect_args = (host, owner, suite) gcylc_item.connect("button-press-event", lambda b, e: launch_gcylc(b._connect_args)) gcylc_item.show() menu.append(gcylc_item) if suite_keys: sep_item = gtk.SeparatorMenuItem() sep_item.show() menu.append(sep_item) if extra_items is not None: for item in extra_items: menu.append(item) sep_item = gtk.SeparatorMenuItem() sep_item.show() menu.append(sep_item) # Construct a cylc stop item to stop a suite if len(suite_keys) > 1: stoptask_item = gtk.ImageMenuItem('Stop all') else: stoptask_item = gtk.ImageMenuItem('Stop') img_stop = gtk.image_new_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_MENU) stoptask_item.set_image(img_stop) stoptask_item._connect_args = suite_keys, 'stop' stoptask_item.connect( "button-press-event", lambda b, e: call_cylc_command(b._connect_args[0], b._connect_args[1])) stoptask_item.show() menu.append(stoptask_item) # Construct a cylc hold item to hold (pause) a suite if len(suite_keys) > 1: holdtask_item = gtk.ImageMenuItem('Hold all') else: holdtask_item = gtk.ImageMenuItem('Hold') img_hold = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU) holdtask_item.set_image(img_hold) holdtask_item._connect_args = suite_keys, 'hold' holdtask_item.connect( "button-press-event", lambda b, e: call_cylc_command(b._connect_args[0], b._connect_args[1])) menu.append(holdtask_item) holdtask_item.show() # Construct a cylc release item to release a paused/stopped suite if len(suite_keys) > 1: unstoptask_item = gtk.ImageMenuItem('Release all') else: unstoptask_item = gtk.ImageMenuItem('Release') img_release = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU) unstoptask_item.set_image(img_release) unstoptask_item._connect_args = suite_keys, 'release' unstoptask_item.connect( "button-press-event", lambda b, e: call_cylc_command(b._connect_args[0], b._connect_args[1])) unstoptask_item.show() menu.append(unstoptask_item) # Add another separator sep_item = gtk.SeparatorMenuItem() sep_item.show() menu.append(sep_item) # Construct theme chooser items (same as cylc.gui.app_main). theme_item = gtk.ImageMenuItem('Theme') img = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) theme_item.set_image(img) theme_item.set_sensitive(not is_stopped) thememenu = gtk.Menu() theme_item.set_submenu(thememenu) theme_item.show() theme_items = {} theme = "default" theme_items[theme] = gtk.RadioMenuItem(label=theme) thememenu.append(theme_items[theme]) theme_items[theme].theme_name = theme gcfg = GcylcConfig.get_inst() for theme in gcfg.get(['themes']): if theme == "default": continue theme_items[theme] = gtk.RadioMenuItem(group=theme_items['default'], label=theme) thememenu.append(theme_items[theme]) theme_items[theme].theme_name = theme # set_active then connect, to avoid causing an unnecessary toggle now. theme_items[theme_name].set_active(True) for theme in gcfg.get(['themes']): theme_items[theme].show() theme_items[theme].connect( 'toggled', lambda i: (i.get_active() and set_theme_func(i.theme_name))) menu.append(theme_item) theme_legend_item = gtk.MenuItem("Show task state key") theme_legend_item.show() theme_legend_item.set_sensitive(not is_stopped) theme_legend_item.connect( "button-press-event", lambda b, e: ThemeLegendWindow(None, gcfg.get(['themes', theme_name]))) menu.append(theme_legend_item) sep_item = gtk.SeparatorMenuItem() sep_item.show() menu.append(sep_item) # Construct a trigger update item. update_now_item = gtk.ImageMenuItem("Update Now") img = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU) update_now_item.set_image(img) update_now_item.show() update_now_item.set_sensitive(not is_stopped) update_now_item.connect("button-press-event", lambda b, e: update_now_func()) menu.append(update_now_item) # Construct a clean stopped suites item. clear_item = gtk.ImageMenuItem("Clear Stopped Suites") img = gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU) clear_item.set_image(img) clear_item.show() clear_item.set_sensitive(has_stopped_suites) clear_item.connect("button-press-event", lambda b, e: clear_stopped_suites_func()) menu.append(clear_item) # Construct a configure scanned hosts item. hosts_item = gtk.ImageMenuItem("Configure Hosts") img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) hosts_item.set_image(img) hosts_item.show() hosts_item.connect( "button-press-event", lambda b, e: launch_hosts_dialog(scanned_hosts, change_hosts_func)) menu.append(hosts_item) sep_item = gtk.SeparatorMenuItem() sep_item.show() menu.append(sep_item) # Construct an about dialog item. info_item = gtk.ImageMenuItem("About") img = gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU) info_item.set_image(img) info_item.show() info_item.connect( "button-press-event", lambda b, e: launch_about_dialog(program_name, scanned_hosts)) menu.append(info_item) return menu
class ScanApp(object): """Summarize running suite statuses for a given set of hosts.""" WARNINGS_COLUMN = 10 STATUS_COLUMN = 9 CYCLE_COLUMN = 8 UPDATE_TIME_COLUMN = 7 TITLE_COLUMN = 6 STOPPED_COLUMN = 5 VERSION_COLUMN = 4 SUITE_COLUMN = 3 OWNER_COLUMN = 2 HOST_COLUMN = 1 GROUP_COLUMN = 0 ICON_SIZE = 17 def __init__(self, hosts=None, patterns_name=None, patterns_owner=None, comms_timeout=None, interval=None): gobject.threads_init() set_exception_hook_dialog("cylc gscan") setup_icons() self.window = gtk.Window() title = "cylc gscan" for opt, items in [("-n", patterns_name), ("-o", patterns_owner)]: if items: for pattern in items: if pattern is not None: title += " %s %s" % (opt, pattern) self.window.set_title(title) self.window.set_icon(get_icon()) self.vbox = gtk.VBox() self.vbox.show() self.warnings = {} self.theme_name = GcylcConfig.get_inst().get(['use theme']) self.theme = GcylcConfig.get_inst().get(['themes', self.theme_name]) suite_treemodel = gtk.TreeStore( str, # group str, # host str, # owner str, # suite str, # version bool, # is_stopped str, # title int, # update_time str, # states str, # states_text str) # warning_text self._prev_tooltip_location_id = None self.treeview = gtk.TreeView(suite_treemodel) # Visibility of columns gsfg = GScanConfig.get_inst() vis_cols = gsfg.get(["columns"]) hide_main_menu_bar = gsfg.get(["hide main menubar"]) # Doesn't make any sense without suite name column if gsfg.COL_SUITE not in vis_cols: vis_cols.append(gsfg.COL_SUITE.lower()) # Construct the group, host, owner, suite, title, update time column. for col_title, col_id, col_cell_text_setter in [ (gsfg.COL_GROUP, self.GROUP_COLUMN, self._set_cell_text_group), (gsfg.COL_HOST, self.HOST_COLUMN, self._set_cell_text_host), (gsfg.COL_OWNER, self.OWNER_COLUMN, self._set_cell_text_owner), (gsfg.COL_SUITE, self.SUITE_COLUMN, self._set_cell_text_name), (gsfg.COL_VERSION, self.VERSION_COLUMN, self._set_cell_text_version), (gsfg.COL_TITLE, self.TITLE_COLUMN, self._set_cell_text_title), (gsfg.COL_UPDATED, self.UPDATE_TIME_COLUMN, self._set_cell_text_time), ]: column = gtk.TreeViewColumn(col_title) cell_text = gtk.CellRendererText() column.pack_start(cell_text, expand=False) column.set_cell_data_func(cell_text, col_cell_text_setter) column.set_sort_column_id(col_id) column.set_visible(col_title.lower() in vis_cols) column.set_resizable(True) self.treeview.append_column(column) # Construct the status column. status_column = gtk.TreeViewColumn(gsfg.COL_STATUS) status_column.set_sort_column_id(self.STATUS_COLUMN) status_column.set_visible(gsfg.COL_STATUS.lower() in vis_cols) status_column.set_resizable(True) cell_text_cycle = gtk.CellRendererText() status_column.pack_start(cell_text_cycle, expand=False) status_column.set_cell_data_func(cell_text_cycle, self._set_cell_text_cycle, self.CYCLE_COLUMN) self.treeview.append_column(status_column) # Warning icon. warn_icon = gtk.CellRendererPixbuf() image = gtk.Image() pixbuf = image.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_LARGE_TOOLBAR) self.warn_icon_colour = pixbuf.scale_simple( # colour warn icon pixbuf self.ICON_SIZE, self.ICON_SIZE, gtk.gdk.INTERP_HYPER) self.warn_icon_grey = pixbuf.scale_simple(self.ICON_SIZE, self.ICON_SIZE, gtk.gdk.INTERP_HYPER) self.warn_icon_colour.saturate_and_pixelate( self.warn_icon_grey, 0, False) # b&w warn icon pixbuf status_column.pack_start(warn_icon, expand=False) status_column.set_cell_data_func(warn_icon, self._set_error_icon_state) self.warn_icon_blank = gtk.gdk.Pixbuf( # Transparent pixbuff. gtk.gdk.COLORSPACE_RGB, True, 8, self.ICON_SIZE, self.ICON_SIZE).fill(0x00000000) # Task status icons. for i in range(len(TASK_STATUSES_ORDERED)): cell_pixbuf_state = gtk.CellRendererPixbuf() status_column.pack_start(cell_pixbuf_state, expand=False) status_column.set_cell_data_func(cell_pixbuf_state, self._set_cell_pixbuf_state, i) self.treeview.show() if hasattr(self.treeview, "set_has_tooltip"): self.treeview.set_has_tooltip(True) try: self.treeview.connect('query-tooltip', self._on_query_tooltip) except TypeError: # Lower PyGTK version. pass self.treeview.connect("button-press-event", self._on_button_press_event) cre_owner, cre_name = re_compile_filters(patterns_owner, patterns_name) self.updater = ScanAppUpdater(self.window, hosts, suite_treemodel, self.treeview, comms_timeout, interval, self.GROUP_COLUMN, cre_owner, cre_name) self.updater.start() self.dot_size = GcylcConfig.get_inst().get(['dot icon size']) self.dots = None self._set_dots() self.menu_bar = None self.create_menubar() accelgroup = gtk.AccelGroup() self.window.add_accel_group(accelgroup) key, modifier = gtk.accelerator_parse('<Alt>m') accelgroup.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self._toggle_hide_menu_bar) self.tool_bar = None self.create_tool_bar() self.menu_hbox = gtk.HBox() self.menu_hbox.pack_start(self.menu_bar, expand=True, fill=True) self.menu_hbox.pack_start(self.tool_bar, expand=True, fill=True) self.menu_hbox.show_all() if hide_main_menu_bar: self.menu_hbox.hide_all() scrolled_window = gtk.ScrolledWindow() scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scrolled_window.add(self.treeview) scrolled_window.show() self.vbox.pack_start(self.menu_hbox, expand=False) self.vbox.pack_start(scrolled_window, expand=True, fill=True) self.window.add(self.vbox) self.window.connect("destroy", self._on_destroy_event) wsize = gsfg.get(['window size']) self.window.set_default_size(*wsize) self.treeview.grab_focus() self.window.show() self.theme_legend_window = None self.warning_icon_shown = [] def popup_theme_legend(self, widget=None): """Popup a theme legend window.""" if self.theme_legend_window is None: self.theme_legend_window = ThemeLegendWindow( self.window, self.theme) self.theme_legend_window.connect("destroy", self.destroy_theme_legend) else: self.theme_legend_window.present() def update_theme_legend(self): """Update the theme legend window, if it exists.""" if self.theme_legend_window is not None: self.theme_legend_window.update(self.theme) def destroy_theme_legend(self, widget): """Handle a destroy of the theme legend window.""" self.theme_legend_window = None def create_menubar(self): """Create the main menu.""" self.menu_bar = gtk.MenuBar() file_menu = gtk.Menu() file_menu_root = gtk.MenuItem('_File') file_menu_root.set_submenu(file_menu) exit_item = gtk.ImageMenuItem('E_xit') img = gtk.image_new_from_stock(gtk.STOCK_QUIT, gtk.ICON_SIZE_MENU) exit_item.set_image(img) exit_item.show() exit_item.connect("activate", self._on_destroy_event) file_menu.append(exit_item) view_menu = gtk.Menu() view_menu_root = gtk.MenuItem('_View') view_menu_root.set_submenu(view_menu) col_item = gtk.ImageMenuItem("_Columns...") img = gtk.image_new_from_stock(gtk.STOCK_INDEX, gtk.ICON_SIZE_MENU) col_item.set_image(img) col_item.show() col_menu = gtk.Menu() for column_index, column in enumerate(self.treeview.get_columns()): name = column.get_title() is_visible = column.get_visible() column_item = gtk.CheckMenuItem(name.replace("_", "__")) column_item._connect_args = column_index column_item.set_active(is_visible) column_item.connect("toggled", self._on_toggle_column_visible) column_item.show() col_menu.append(column_item) col_item.set_submenu(col_menu) col_item.show_all() view_menu.append(col_item) view_menu.append(gtk.SeparatorMenuItem()) # Construct theme chooser items (same as cylc.gui.app_main). theme_item = gtk.ImageMenuItem('Theme...') img = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) theme_item.set_image(img) thememenu = gtk.Menu() theme_item.set_submenu(thememenu) theme_item.show() theme_items = {} theme = "default" theme_items[theme] = gtk.RadioMenuItem(label=theme) thememenu.append(theme_items[theme]) theme_items[theme].theme_name = theme for theme in GcylcConfig.get_inst().get(['themes']): if theme == "default": continue theme_items[theme] = gtk.RadioMenuItem( group=theme_items['default'], label=theme) thememenu.append(theme_items[theme]) theme_items[theme].theme_name = theme # set_active then connect, to avoid causing an unnecessary toggle now. theme_items[self.theme_name].set_active(True) for theme in GcylcConfig.get_inst().get(['themes']): theme_items[theme].show() theme_items[theme].connect( 'toggled', lambda i: (i.get_active() and self._set_theme(i.theme_name))) view_menu.append(theme_item) theme_legend_item = gtk.ImageMenuItem("Show task state key") img = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) theme_legend_item.set_image(img) theme_legend_item.show() theme_legend_item.connect("activate", self.popup_theme_legend) view_menu.append(theme_legend_item) view_menu.append(gtk.SeparatorMenuItem()) # Construct a configure scanned hosts item. hosts_item = gtk.ImageMenuItem("Configure Hosts") img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) hosts_item.set_image(img) hosts_item.show() hosts_item.connect( "activate", lambda w: launch_hosts_dialog(self.updater.hosts, self. updater.set_hosts)) view_menu.append(hosts_item) sep_item = gtk.SeparatorMenuItem() sep_item.show() help_menu = gtk.Menu() help_menu_root = gtk.MenuItem('_Help') help_menu_root.set_submenu(help_menu) self.menu_bar.append(file_menu_root) self.menu_bar.append(view_menu_root) self.menu_bar.append(help_menu_root) # Construct an about dialog item. info_item = gtk.ImageMenuItem("About") img = gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU) info_item.set_image(img) info_item.show() info_item.connect( "activate", lambda w: launch_about_dialog("cylc gscan", self.updater.hosts)) help_menu.append(info_item) self.menu_bar.show_all() def _set_dots(self): self.dots = DotMaker(self.theme, size=self.dot_size) def create_tool_bar(self): """Create the tool bar for the GUI.""" self.tool_bar = gtk.Toolbar() update_now_button = gtk.ToolButton( icon_widget=gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR)) update_now_button.set_label("Update Listing") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(update_now_button, "Update Suite Listing Now") update_now_button.connect("clicked", self.updater.set_update_listing) clear_stopped_button = gtk.ToolButton( icon_widget=gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_SMALL_TOOLBAR)) clear_stopped_button.set_label("Clear") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(clear_stopped_button, "Clear stopped suites") clear_stopped_button.connect("clicked", self.updater.clear_stopped_suites) expand_button = gtk.ToolButton(icon_widget=gtk.image_new_from_stock( gtk.STOCK_ADD, gtk.ICON_SIZE_SMALL_TOOLBAR)) expand_button.set_label("Expand all") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(expand_button, "Expand all rows") expand_button.connect("clicked", lambda e: self.treeview.expand_all()) collapse_button = gtk.ToolButton(icon_widget=gtk.image_new_from_stock( gtk.STOCK_REMOVE, gtk.ICON_SIZE_SMALL_TOOLBAR)) collapse_button.set_label("Collapse all") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(collapse_button, "Collapse all rows") collapse_button.connect("clicked", lambda e: self.treeview.collapse_all()) self.tool_bar.insert(update_now_button, 0) self.tool_bar.insert(clear_stopped_button, 0) self.tool_bar.insert(collapse_button, 0) self.tool_bar.insert(expand_button, 0) separator = gtk.SeparatorToolItem() separator.set_expand(True) self.tool_bar.insert(separator, 0) self.tool_bar.show_all() def _on_button_press_event(self, treeview, event): """Tree view button press callback.""" x = int(event.x) y = int(event.y) pth = treeview.get_path_at_pos(x, y) treemodel = treeview.get_model() # Dismiss warnings by clicking on a warning icon. if event.button == 1: if not pth: return False path, column, cell_x = pth[:3] if column.get_title() == GScanConfig.get_inst().COL_STATUS: dot_offset, dot_width = tuple( column.cell_get_position(column.get_cell_renderers()[1])) if not dot_width: return False try: cell_index = (cell_x - dot_offset) // dot_width except ZeroDivisionError: return False if cell_index == 0: iter_ = treemodel.get_iter(path) host, owner, suite = treemodel.get(iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) self.updater.clear_warnings(host, owner, suite) treemodel.set(iter_, self.WARNINGS_COLUMN, '') return True # Display menu on right click. if event.type != gtk.gdk._2BUTTON_PRESS and event.button != 3: return False suite_keys = [] if pth is not None: # Add a gcylc launcher item. path = pth[0] iter_ = treemodel.get_iter(path) host, owner, suite = treemodel.get(iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) if suite is not None: suite_keys.append((host, owner, suite)) elif suite is None: # On an expanded cycle point row, so get from parent. try: host, owner, suite = treemodel.get( treemodel.iter_parent(iter_), self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) suite_keys.append((host, owner, suite)) except TypeError: # Now iterate over the children instead. # We need to iterate over the children as there can be more # than one suite in a group of suites. # Get a TreeIter pointing to the first child of parent iter suite_iter = treemodel.iter_children(iter_) # Iterate over the children until you get to end while suite_iter is not None: host, owner, suite = treemodel.get( suite_iter, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) suite_keys.append((host, owner, suite)) # Advance to the next pointer in the treemodel suite_iter = treemodel.iter_next(suite_iter) if event.type == gtk.gdk._2BUTTON_PRESS: if suite_keys: launch_gcylc(suite_keys[0]) return False menu = get_scan_menu(suite_keys, self._toggle_hide_menu_bar) menu.popup(None, None, None, event.button, event.time) return False def _on_destroy_event(self, _): """Callback on destroy of main window.""" try: self.updater.quit = True gtk.main_quit() except RuntimeError: pass return False def _toggle_hide_menu_bar(self, *_): if self.menu_hbox.get_property("visible"): self.menu_hbox.hide_all() else: self.menu_hbox.show_all() def _on_query_tooltip(self, _, x, y, kbd_ctx, tooltip): """Handle a tooltip creation request.""" tip_context = self.treeview.get_tooltip_context(x, y, kbd_ctx) if tip_context is None: self._prev_tooltip_location_id = None return False x, y = self.treeview.convert_widget_to_bin_window_coords(x, y) path, column, cell_x = (self.treeview.get_path_at_pos(x, y))[:3] model = self.treeview.get_model() iter_ = model.get_iter(path) parent_iter = model.iter_parent(iter_) if parent_iter is None or parent_iter and model.iter_has_child(iter_): host, owner, suite = model.get(iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) child_row_number = None else: host, owner, suite = model.get(parent_iter, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) child_row_number = path[-1] suite_update_time = model.get_value(iter_, self.UPDATE_TIME_COLUMN) location_id = (host, owner, suite, suite_update_time, column.get_title(), child_row_number) if location_id != self._prev_tooltip_location_id: self._prev_tooltip_location_id = location_id tooltip.set_text(None) return False gsfg = GScanConfig.get_inst() if column.get_title() in [ gsfg.COL_HOST, gsfg.COL_OWNER, gsfg.COL_SUITE ]: tooltip.set_text("%s - %s:%s" % (suite, owner, host)) return True if column.get_title() == gsfg.COL_UPDATED: suite_update_point = timepoint_from_epoch(suite_update_time) if (self.updater.prev_norm_update is not None and suite_update_time != int(self.updater.prev_norm_update)): text = "Last changed at %s\n" % suite_update_point text += "Last scanned at %s" % timepoint_from_epoch( int(self.updater.prev_norm_update)) else: # An older suite (or before any updates are made?) text = "Last scanned at %s" % suite_update_point tooltip.set_text(text) return True if column.get_title() != gsfg.COL_STATUS: tooltip.set_text(None) return False # Generate text for the number of tasks in each state state_texts = [] state_text = model.get_value(iter_, self.STATUS_COLUMN) if state_text is None: tooltip.set_text(None) return False info = re.findall(r'\D+\d+', state_text) for status_number in info: status, number = status_number.rsplit(" ", 1) state_texts.append(number + " " + status.strip()) tooltip_prefix = ("<span foreground=\"#777777\">Tasks: " + ", ".join(state_texts) + "</span>") # If hovering over a status indicator set tooltip to show most recent # tasks. dot_offset, dot_width = tuple( column.cell_get_position(column.get_cell_renderers()[2])) try: cell_index = ((cell_x - dot_offset) // dot_width) + 1 except ZeroDivisionError: return False if cell_index >= 0: # NOTE: TreeViewColumn.get_cell_renderers() does not always return # cell renderers for the correct row. if cell_index == 0: # Hovering over the error symbol. point_string = model.get(iter_, self.CYCLE_COLUMN)[0] if point_string: return False if not self.warnings.get((host, owner, suite)): return False tooltip.set_markup( tooltip_prefix + '\n<b>New failures</b> (<i>last 5</i>) <i><span ' + 'foreground="#2222BB">click to dismiss</span></i>\n' + self.warnings[(host, owner, suite)]) return True else: # Hovering over a status indicator. info = re.findall(r'\D+\d+', model.get(iter_, self.STATUS_COLUMN)[0]) if cell_index > len(info): return False state = info[cell_index - 1].strip().split(' ')[0] point_string = model.get(iter_, self.CYCLE_COLUMN)[0] tooltip_text = tooltip_prefix if suite: tasks = self.updater.get_last_n_tasks( host, owner, suite, state, point_string) tooltip_text += ( '\n<b>Recent {state} tasks</b>\n{tasks}').format( state=state, tasks='\n'.join(tasks)) tooltip.set_markup(tooltip_text) return True # Set the tooltip to a generic status for this suite. tooltip.set_markup(tooltip_prefix) return True def _on_toggle_column_visible(self, menu_item): """Toggle column visibility callback.""" column_index = menu_item._connect_args column = self.treeview.get_columns()[column_index] is_visible = column.get_visible() column.set_visible(not is_visible) self.updater.update() return False def _set_cell_pixbuf_state(self, _, cell, model, iter_, index): """State info pixbuf.""" state_info = model.get_value(iter_, self.STATUS_COLUMN) if state_info is not None: is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) info = re.findall(r'\D+\d+', state_info) if index < len(info): state = info[index].strip().rsplit( " ", self.SUITE_COLUMN)[0].strip() icon = self.dots.get_icon(state, is_stopped=is_stopped) cell.set_property("visible", True) else: icon = None cell.set_property("visible", False) else: icon = None cell.set_property("visible", False) cell.set_property("pixbuf", icon) def _set_error_icon_state(self, _, cell, model, iter_): """Update the state of the warning icon.""" host, owner, suite, warnings, point_string = model.get( iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN, self.WARNINGS_COLUMN, self.CYCLE_COLUMN) key = (host, owner, suite) if point_string: # Error icon only for first row. cell.set_property('pixbuf', self.warn_icon_blank) elif warnings: cell.set_property('pixbuf', self.warn_icon_colour) self.warning_icon_shown.append(key) self.warnings[key] = warnings else: cell.set_property('pixbuf', self.warn_icon_grey) self.warnings[key] = None if key not in self.warning_icon_shown: cell.set_property('pixbuf', self.warn_icon_blank) def _set_cell_text_group(self, _, cell, model, iter_): """Set cell text for "group" column.""" group = model.get_value(iter_, self.GROUP_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", group) def _set_cell_text_host(self, _, cell, model, iter_): """Set cell text for "host" column.""" host = model.get_value(iter_, self.HOST_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", host) def _set_cell_text_owner(self, _, cell, model, iter_): """Set cell text for "owner" column.""" value = model.get_value(iter_, self.OWNER_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", value) def _set_cell_text_name(self, _, cell, model, iter_): """Set cell text for (suite name) "name" column.""" name = model.get_value(iter_, self.SUITE_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", name) def _set_cell_text_version(self, _, cell, model, iter_): """Set cell text for (suite version) "version" column.""" value = model.get_value(iter_, self.VERSION_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", value) def _set_cell_text_title(self, _, cell, model, iter_): """Set cell text for "title" column.""" title = model.get_value(iter_, self.TITLE_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", title) def _set_cell_text_time(self, _, cell, model, iter_): """Set cell text for "update-time" column.""" suite_update_time = model.get_value(iter_, self.UPDATE_TIME_COLUMN) time_point = timepoint_from_epoch(suite_update_time) time_point.set_time_zone_to_local() current_point = timepoint_from_epoch(time()) if str(time_point).split("T")[0] == str(current_point).split("T")[0]: time_string = str(time_point).split("T")[1] else: time_string = str(time_point) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", time_string) def _set_cell_text_cycle(self, _, cell, model, iter_, active_cycle): """Set cell text for "cycle" column.""" cycle = model.get_value(iter_, active_cycle) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", cycle) def _set_theme(self, new_theme_name): """Set GUI theme.""" self.theme_name = new_theme_name self.theme = GcylcConfig.get_inst().get(['themes', self.theme_name]) self._set_dots() self.updater.update() self.update_theme_legend() @staticmethod def _set_tooltip(widget, text): """Set tooltip for a widget.""" tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(widget, text)
class ScanApp(object): """Summarize running suite statuses for a given set of hosts.""" WARNINGS_COLUMN = 9 STATUS_COLUMN = 8 CYCLE_COLUMN = 7 UPDATE_TIME_COLUMN = 6 TITLE_COLUMN = 5 STOPPED_COLUMN = 4 SUITE_COLUMN = 3 OWNER_COLUMN = 2 HOST_COLUMN = 1 GROUP_COLUMN = 0 ICON_SIZE = 17 def __init__( self, hosts=None, patterns_name=None, patterns_owner=None, comms_timeout=None, poll_interval=None): gobject.threads_init() set_exception_hook_dialog("cylc gscan") setup_icons() if not hosts: hosts = GLOBAL_CFG.get(["suite host scanning", "hosts"]) self.hosts = hosts self.window = gtk.Window() title = "cylc gscan" for opt, items, skip in [ ("-n", patterns_name, None), ("-o", patterns_owner, USER)]: if items: for pattern in items: if pattern != skip: title += " %s %s" % (opt, pattern) self.window.set_title(title) self.window.set_icon(get_icon()) self.vbox = gtk.VBox() self.vbox.show() self.warnings = {} self.theme_name = gcfg.get(['use theme']) self.theme = gcfg.get(['themes', self.theme_name]) suite_treemodel = gtk.TreeStore( str, # group str, # host str, # owner str, # suite bool, # is_stopped str, # title int, # update_time str, # states str, # states_text str) # warning_text self._prev_tooltip_location_id = None self.treeview = gtk.TreeView(suite_treemodel) # Visibility of columns vis_cols = gsfg.get(["columns"]) # Doesn't make any sense without suite name column if gsfg.COL_SUITE not in vis_cols: vis_cols.append(gsfg.COL_SUITE.lower()) # In multiple host environment, add host column by default if hosts: vis_cols.append(gsfg.COL_HOST.lower()) # In multiple owner environment, add owner column by default if patterns_owner != [USER]: vis_cols.append(gsfg.COL_OWNER.lower()) # Construct the group, host, owner, suite, title, update time column. for col_title, col_id, col_cell_text_setter in [ (gsfg.COL_GROUP, self.GROUP_COLUMN, self._set_cell_text_group), (gsfg.COL_HOST, self.HOST_COLUMN, self._set_cell_text_host), (gsfg.COL_OWNER, self.OWNER_COLUMN, self._set_cell_text_owner), (gsfg.COL_SUITE, self.SUITE_COLUMN, self._set_cell_text_name), (gsfg.COL_TITLE, self.TITLE_COLUMN, self._set_cell_text_title), (gsfg.COL_UPDATED, self.UPDATE_TIME_COLUMN, self._set_cell_text_time), ]: column = gtk.TreeViewColumn(col_title) cell_text = gtk.CellRendererText() column.pack_start(cell_text, expand=False) column.set_cell_data_func(cell_text, col_cell_text_setter) column.set_sort_column_id(col_id) column.set_visible(col_title.lower() in vis_cols) column.set_resizable(True) self.treeview.append_column(column) # Construct the status column. status_column = gtk.TreeViewColumn(gsfg.COL_STATUS) status_column.set_sort_column_id(self.STATUS_COLUMN) status_column.set_visible(gsfg.COL_STATUS.lower() in vis_cols) status_column.set_resizable(True) cell_text_cycle = gtk.CellRendererText() status_column.pack_start(cell_text_cycle, expand=False) status_column.set_cell_data_func( cell_text_cycle, self._set_cell_text_cycle, self.CYCLE_COLUMN) self.treeview.append_column(status_column) # Warning icon. warn_icon = gtk.CellRendererPixbuf() image = gtk.Image() pixbuf = image.render_icon( gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_LARGE_TOOLBAR) self.warn_icon_colour = pixbuf.scale_simple( # colour warn icon pixbuf self.ICON_SIZE, self.ICON_SIZE, gtk.gdk.INTERP_HYPER) self.warn_icon_grey = pixbuf.scale_simple( self.ICON_SIZE, self.ICON_SIZE, gtk.gdk.INTERP_HYPER) self.warn_icon_colour.saturate_and_pixelate( self.warn_icon_grey, 0, False) # b&w warn icon pixbuf status_column.pack_start(warn_icon, expand=False) status_column.set_cell_data_func(warn_icon, self._set_error_icon_state) self.warn_icon_blank = gtk.gdk.Pixbuf( # Transparent pixbuff. gtk.gdk.COLORSPACE_RGB, True, 8, self.ICON_SIZE, self.ICON_SIZE ).fill(0x00000000) # Task status icons. for i in range(len(TASK_STATUSES_ORDERED)): cell_pixbuf_state = gtk.CellRendererPixbuf() status_column.pack_start(cell_pixbuf_state, expand=False) status_column.set_cell_data_func( cell_pixbuf_state, self._set_cell_pixbuf_state, i) self.treeview.show() if hasattr(self.treeview, "set_has_tooltip"): self.treeview.set_has_tooltip(True) try: self.treeview.connect('query-tooltip', self._on_query_tooltip) except TypeError: # Lower PyGTK version. pass self.treeview.connect("button-press-event", self._on_button_press_event) patterns = {"name": None, "owner": None} for label, items in [ ("owner", patterns_owner), ("name", patterns_name)]: if items: patterns[label] = r"\A(?:" + r")|(?:".join(items) + r")\Z" try: patterns[label] = re.compile(patterns[label]) except re.error: raise ValueError("Invalid %s pattern: %s" % (label, items)) self.updater = ScanAppUpdater( self.window, self.hosts, suite_treemodel, self.treeview, comms_timeout=comms_timeout, poll_interval=poll_interval, group_column_id=self.GROUP_COLUMN, name_pattern=patterns["name"], owner_pattern=patterns["owner"]) self.updater.start() self.dot_size = gcfg.get(['dot icon size']) self._set_dots() self.create_menubar() accelgroup = gtk.AccelGroup() self.window.add_accel_group(accelgroup) key, modifier = gtk.accelerator_parse('<Alt>m') accelgroup.connect_group( key, modifier, gtk.ACCEL_VISIBLE, self._toggle_hide_menu_bar) self.create_tool_bar() self.menu_hbox = gtk.HBox() self.menu_hbox.pack_start(self.menu_bar, expand=True, fill=True) self.menu_hbox.pack_start(self.tool_bar, expand=True, fill=True) self.menu_hbox.show_all() self.menu_hbox.hide_all() scrolled_window = gtk.ScrolledWindow() scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scrolled_window.add(self.treeview) scrolled_window.show() self.vbox.pack_start(self.menu_hbox, expand=False) self.vbox.pack_start(scrolled_window, expand=True, fill=True) self.window.add(self.vbox) self.window.connect("destroy", self._on_destroy_event) wsize = gsfg.get(['window size']) self.window.set_default_size(*wsize) self.treeview.grab_focus() self.window.show() self.theme_legend_window = None self.warning_icon_shown = [] def popup_theme_legend(self, widget=None): """Popup a theme legend window.""" if self.theme_legend_window is None: self.theme_legend_window = ThemeLegendWindow( self.window, self.theme) self.theme_legend_window.connect( "destroy", self.destroy_theme_legend) else: self.theme_legend_window.present() def update_theme_legend(self): """Update the theme legend window, if it exists.""" if self.theme_legend_window is not None: self.theme_legend_window.update(self.theme) def destroy_theme_legend(self, widget): """Handle a destroy of the theme legend window.""" self.theme_legend_window = None def create_menubar(self): """Create the main menu.""" self.menu_bar = gtk.MenuBar() file_menu = gtk.Menu() file_menu_root = gtk.MenuItem('_File') file_menu_root.set_submenu(file_menu) exit_item = gtk.ImageMenuItem('E_xit') img = gtk.image_new_from_stock(gtk.STOCK_QUIT, gtk.ICON_SIZE_MENU) exit_item.set_image(img) exit_item.show() exit_item.connect("activate", self._on_destroy_event) file_menu.append(exit_item) view_menu = gtk.Menu() view_menu_root = gtk.MenuItem('_View') view_menu_root.set_submenu(view_menu) col_item = gtk.ImageMenuItem("_Columns...") img = gtk.image_new_from_stock(gtk.STOCK_INDEX, gtk.ICON_SIZE_MENU) col_item.set_image(img) col_item.show() col_menu = gtk.Menu() for column_index, column in enumerate(self.treeview.get_columns()): name = column.get_title() is_visible = column.get_visible() column_item = gtk.CheckMenuItem(name.replace("_", "__")) column_item._connect_args = column_index column_item.set_active(is_visible) column_item.connect("toggled", self._on_toggle_column_visible) column_item.show() col_menu.append(column_item) col_item.set_submenu(col_menu) col_item.show_all() view_menu.append(col_item) view_menu.append(gtk.SeparatorMenuItem()) # Construct theme chooser items (same as cylc.gui.app_main). theme_item = gtk.ImageMenuItem('Theme...') img = gtk.image_new_from_stock( gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) theme_item.set_image(img) thememenu = gtk.Menu() theme_item.set_submenu(thememenu) theme_item.show() theme_items = {} theme = "default" theme_items[theme] = gtk.RadioMenuItem(label=theme) thememenu.append(theme_items[theme]) theme_items[theme].theme_name = theme for theme in gcfg.get(['themes']): if theme == "default": continue theme_items[theme] = gtk.RadioMenuItem( group=theme_items['default'], label=theme) thememenu.append(theme_items[theme]) theme_items[theme].theme_name = theme # set_active then connect, to avoid causing an unnecessary toggle now. theme_items[self.theme_name].set_active(True) for theme in gcfg.get(['themes']): theme_items[theme].show() theme_items[theme].connect( 'toggled', lambda i: (i.get_active() and self._set_theme(i.theme_name))) view_menu.append(theme_item) theme_legend_item = gtk.ImageMenuItem("Show task state key") img = gtk.image_new_from_stock( gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) theme_legend_item.set_image(img) theme_legend_item.show() theme_legend_item.connect("activate", self.popup_theme_legend) view_menu.append(theme_legend_item) view_menu.append(gtk.SeparatorMenuItem()) # Construct a configure scanned hosts item. hosts_item = gtk.ImageMenuItem("Configure Hosts") img = gtk.image_new_from_stock( gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) hosts_item.set_image(img) hosts_item.show() hosts_item.connect( "button-press-event", lambda b, e: launch_hosts_dialog( self.hosts, self.updater.set_hosts)) view_menu.append(hosts_item) sep_item = gtk.SeparatorMenuItem() sep_item.show() help_menu = gtk.Menu() help_menu_root = gtk.MenuItem('_Help') help_menu_root.set_submenu(help_menu) self.menu_bar.append(file_menu_root) self.menu_bar.append(view_menu_root) self.menu_bar.append(help_menu_root) # Construct an about dialog item. info_item = gtk.ImageMenuItem("About") img = gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU) info_item.set_image(img) info_item.show() info_item.connect( "button-press-event", lambda b, e: launch_about_dialog("cylc gscan", self.hosts) ) help_menu.append(info_item) self.menu_bar.show_all() def _set_dots(self): self.dots = DotMaker(self.theme, size=self.dot_size) def create_tool_bar(self): """Create the tool bar for the GUI.""" self.tool_bar = gtk.Toolbar() update_now_button = gtk.ToolButton( icon_widget=gtk.image_new_from_stock( gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR)) update_now_button.set_label("Update") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(update_now_button, "Update now") update_now_button.connect("clicked", self.updater.update_now) clear_stopped_button = gtk.ToolButton( icon_widget=gtk.image_new_from_stock( gtk.STOCK_CLEAR, gtk.ICON_SIZE_SMALL_TOOLBAR)) clear_stopped_button.set_label("Clear") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(clear_stopped_button, "Clear stopped suites") clear_stopped_button.connect("clicked", self.updater.clear_stopped_suites) expand_button = gtk.ToolButton( icon_widget=gtk.image_new_from_stock( gtk.STOCK_ADD, gtk.ICON_SIZE_SMALL_TOOLBAR)) expand_button.set_label("Expand all") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(expand_button, "Expand all rows") expand_button.connect( "clicked", lambda e: self.treeview.expand_all()) collapse_button = gtk.ToolButton( icon_widget=gtk.image_new_from_stock( gtk.STOCK_REMOVE, gtk.ICON_SIZE_SMALL_TOOLBAR)) collapse_button.set_label("Expand all") tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(collapse_button, "Collapse all rows") collapse_button.connect( "clicked", lambda e: self.treeview.collapse_all()) self.tool_bar.insert(update_now_button, 0) self.tool_bar.insert(clear_stopped_button, 0) self.tool_bar.insert(collapse_button, 0) self.tool_bar.insert(expand_button, 0) separator = gtk.SeparatorToolItem() separator.set_expand(True) self.tool_bar.insert(separator, 0) self.tool_bar.show_all() def _on_button_press_event(self, treeview, event): """Tree view button press callback.""" x = int(event.x) y = int(event.y) pth = treeview.get_path_at_pos(x, y) treemodel = treeview.get_model() # Dismiss warnings by clicking on a warning icon. if event.button == 1: if not pth: return False path, column, cell_x, _ = pth if column.get_title() == gsfg.COL_STATUS: dot_offset, dot_width = tuple(column.cell_get_position( column.get_cell_renderers()[1])) if not dot_width: return False try: cell_index = (cell_x - dot_offset) // dot_width except ZeroDivisionError: return False if cell_index == 0: iter_ = treemodel.get_iter(path) host, owner, suite = treemodel.get( iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) self.updater.clear_warnings(host, owner, suite) treemodel.set(iter_, self.WARNINGS_COLUMN, '') return True # Display menu on right click. if event.type != gtk.gdk._2BUTTON_PRESS and event.button != 3: return False suite_keys = [] if pth is not None: # Add a gcylc launcher item. path = pth[0] iter_ = treemodel.get_iter(path) host, owner, suite = treemodel.get( iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) if suite is not None: suite_keys.append((host, owner, suite)) elif suite is None: # On an expanded cycle point row, so get from parent. try: host, owner, suite = treemodel.get( treemodel.iter_parent(iter_), self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) suite_keys.append((host, owner, suite)) except: # Now iterate over the children instead. # We need to iterate over the children as there can be more # than one suite in a group of suites. # Get a TreeIter pointing to the first child of parent iter suite_iter = treemodel.iter_children(iter_) # Iterate over the children until you get to end while suite_iter is not None: host, owner, suite = treemodel.get(suite_iter, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) suite_keys.append((host, owner, suite)) # Advance to the next pointer in the treemodel suite_iter = treemodel.iter_next(suite_iter) if event.type == gtk.gdk._2BUTTON_PRESS: if suite_keys: launch_gcylc(suite_keys[0]) return False menu = get_scan_menu(suite_keys, self._toggle_hide_menu_bar) menu.popup(None, None, None, event.button, event.time) return False def _on_destroy_event(self, _): """Callback on destroy of main window.""" try: self.updater.quit = True gtk.main_quit() except RuntimeError: pass return False def _toggle_hide_menu_bar(self, *_): if self.menu_hbox.get_property("visible"): self.menu_hbox.hide_all() else: self.menu_hbox.show_all() def _on_query_tooltip(self, _, x, y, kbd_ctx, tooltip): """Handle a tooltip creation request.""" tip_context = self.treeview.get_tooltip_context(x, y, kbd_ctx) if tip_context is None: self._prev_tooltip_location_id = None return False x, y = self.treeview.convert_widget_to_bin_window_coords(x, y) path, column, cell_x, _ = ( self.treeview.get_path_at_pos(x, y)) model = self.treeview.get_model() iter_ = model.get_iter(path) parent_iter = model.iter_parent(iter_) if parent_iter is None or parent_iter and model.iter_has_child(iter_): host, owner, suite = model.get( iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) child_row_number = None else: host, owner, suite = model.get( parent_iter, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN) child_row_number = path[-1] suite_update_time = model.get_value(iter_, self.UPDATE_TIME_COLUMN) location_id = ( host, owner, suite, suite_update_time, column.get_title(), child_row_number) if location_id != self._prev_tooltip_location_id: self._prev_tooltip_location_id = location_id tooltip.set_text(None) return False if column.get_title() in [ gsfg.COL_HOST, gsfg.COL_OWNER, gsfg.COL_SUITE]: tooltip.set_text("%s - %s:%s" % (suite, owner, host)) return True if column.get_title() == gsfg.COL_UPDATED: suite_update_point = timepoint_from_epoch(suite_update_time) if (self.updater.last_update_time is not None and suite_update_time != int(self.updater.last_update_time)): retrieval_point = timepoint_from_epoch( int(self.updater.last_update_time)) text = "Last changed at %s\n" % suite_update_point text += "Last scanned at %s" % retrieval_point else: # An older suite (or before any updates are made?) text = "Last scanned at %s" % suite_update_point tooltip.set_text(text) return True if column.get_title() != gsfg.COL_STATUS: tooltip.set_text(None) return False # Generate text for the number of tasks in each state state_texts = [] state_text = model.get_value(iter_, self.STATUS_COLUMN) if state_text is None: tooltip.set_text(None) return False info = re.findall(r'\D+\d+', state_text) for status_number in info: status, number = status_number.rsplit(" ", 1) state_texts.append(number + " " + status.strip()) tooltip_prefix = ( "<span foreground=\"#777777\">Tasks: " + ", ".join(state_texts) + "</span>" ) # If hovering over a status indicator set tooltip to show most recent # tasks. dot_offset, dot_width = tuple(column.cell_get_position( column.get_cell_renderers()[2])) try: cell_index = ((cell_x - dot_offset) // dot_width) + 1 except ZeroDivisionError: return False if cell_index >= 0: # NOTE: TreeViewColumn.get_cell_renderers() does not always return # cell renderers for the correct row. if cell_index == 0: # Hovering over the error symbol. point_string = model.get(iter_, self.CYCLE_COLUMN)[0] if point_string: return False if not self.warnings.get((host, owner, suite)): return False tooltip.set_markup( tooltip_prefix + '\n<b>New failures</b> (<i>last 5</i>) <i><span ' + 'foreground="#2222BB">click to dismiss</span></i>\n' + self.warnings[(host, owner, suite)]) return True else: # Hovering over a status indicator. info = re.findall(r'\D+\d+', model.get(iter_, self.STATUS_COLUMN)[0]) if cell_index > len(info): return False state = info[cell_index - 1].strip().split(' ')[0] point_string = model.get(iter_, self.CYCLE_COLUMN)[0] tooltip_text = tooltip_prefix if suite: tasks = self.updater.get_last_n_tasks( host, owner, suite, state, point_string) tooltip_text += ( '\n<b>Recent {state} tasks</b>\n{tasks}').format( state=state, tasks='\n'.join(tasks)) tooltip.set_markup(tooltip_text) return True # Set the tooltip to a generic status for this suite. tooltip.set_markup(tooltip_prefix) return True def _on_toggle_column_visible(self, menu_item): """Toggle column visibility callback.""" column_index = menu_item._connect_args column = self.treeview.get_columns()[column_index] is_visible = column.get_visible() column.set_visible(not is_visible) self.updater.update() return False def _set_cell_pixbuf_state(self, _, cell, model, iter_, index): """State info pixbuf.""" state_info = model.get_value(iter_, self.STATUS_COLUMN) if state_info is not None: is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) info = re.findall(r'\D+\d+', state_info) if index < len(info): state = info[index].strip().rsplit( " ", self.SUITE_COLUMN)[0].strip() icon = self.dots.get_icon(state, is_stopped=is_stopped) cell.set_property("visible", True) else: icon = None cell.set_property("visible", False) else: icon = None cell.set_property("visible", False) cell.set_property("pixbuf", icon) def _set_error_icon_state(self, _, cell, model, iter_): """Update the state of the warning icon.""" host, owner, suite, warnings, point_string = model.get( iter_, self.HOST_COLUMN, self.OWNER_COLUMN, self.SUITE_COLUMN, self.WARNINGS_COLUMN, self.CYCLE_COLUMN) key = (host, owner, suite) if point_string: # Error icon only for first row. cell.set_property('pixbuf', self.warn_icon_blank) elif warnings: cell.set_property('pixbuf', self.warn_icon_colour) self.warning_icon_shown.append(key) self.warnings[key] = warnings else: cell.set_property('pixbuf', self.warn_icon_grey) self.warnings[key] = None if key not in self.warning_icon_shown: cell.set_property('pixbuf', self.warn_icon_blank) def _set_cell_text_group(self, _, cell, model, iter_): """Set cell text for "group" column.""" group = model.get_value(iter_, self.GROUP_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", group) def _set_cell_text_host(self, _, cell, model, iter_): """Set cell text for "host" column.""" host = model.get_value(iter_, self.HOST_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", host) def _set_cell_text_owner(self, _, cell, model, iter_): """Set cell text for "owner" column.""" value = model.get_value(iter_, self.OWNER_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", value) def _set_cell_text_name(self, _, cell, model, iter_): """Set cell text for (suite name) "name" column.""" name = model.get_value(iter_, self.SUITE_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", name) def _set_cell_text_title(self, _, cell, model, iter_): """Set cell text for "title" column.""" title = model.get_value(iter_, self.TITLE_COLUMN) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", title) def _set_cell_text_time(self, _, cell, model, iter_): """Set cell text for "update-time" column.""" suite_update_time = model.get_value(iter_, self.UPDATE_TIME_COLUMN) time_point = timepoint_from_epoch(suite_update_time) time_point.set_time_zone_to_local() current_point = timepoint_from_epoch(time()) if str(time_point).split("T")[0] == str(current_point).split("T")[0]: time_string = str(time_point).split("T")[1] else: time_string = str(time_point) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", time_string) def _set_cell_text_cycle(self, _, cell, model, iter_, active_cycle): """Set cell text for "cycle" column.""" cycle = model.get_value(iter_, active_cycle) is_stopped = model.get_value(iter_, self.STOPPED_COLUMN) cell.set_property("sensitive", not is_stopped) cell.set_property("text", cycle) def _set_theme(self, new_theme_name): """Set GUI theme.""" self.theme_name = new_theme_name self.theme = gcfg.get(['themes', self.theme_name]) self._set_dots() self.updater.update() self.update_theme_legend() @staticmethod def _set_tooltip(widget, text): """Set tooltip for a widget.""" tooltip = gtk.Tooltips() tooltip.enable() tooltip.set_tip(widget, text)