Beispiel #1
0
 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()
Beispiel #2
0
 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()
Beispiel #3
0
def launch_theme_legend(theme):
    """Launch a theme legend window."""
    ThemeLegendWindow(None, theme)
Beispiel #4
0
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
Beispiel #5
0
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)
Beispiel #6
0
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)