示例#1
0
class DirCombo(HBox):
    def __init__(self, dir=None):
        HBox.__init__(self, False, 2)

        buttonImage = Image()
        buttonImage.set_from_stock(STOCK_REFRESH, ICON_SIZE_MENU)

        self.combo = ComboBox()
        self.refreshButton = Button()
        self.refreshButton.set_image(buttonImage)
        self.refreshButton.connect('clicked', self.refreshButton_clicked, None)
        self.model = ListStore(str)
        self.combo.set_model(self.model)
        self.dir = dir

        self.pack_start(self.combo, False, False, 0)
        self.pack_start(self.refreshButton, False, False, 0)

        if self.dir != None and exists(self.dir):
            self.refresh()

    def refresh(self):
        self.combo.clear()
        self.model.clear()
        list = listdir(self.dir)
        cell = CellRendererText()
        self.combo.pack_start(cell, True)
        self.combo.add_attribute(cell, 'text', 0)

        for i in list:
            if i[0] != '.' and isdir(join(self.dir, i)) == True:
                self.model.append([i])

    def refreshButton_clicked(self, widget, data):
        self.refresh()
示例#2
0
class MemberFeeCollectionEditor(object):
    def __init__(self, trans, transid, plugin, gui_parent,
                 change_register_function):
        self.fee_trans = trans
        self.transid = transid
        self.memberfee_plugin = plugin
        self.change_register_function = change_register_function

        load_glade_file_get_widgets_and_connect_signals(
            get_memberfee_glade_file(),
            'window1', self, self)

        self.vbox1.reparent(gui_parent)
        self.window1.hide()

        self.fee_spread_liststore = ListStore(str, str)
        self.fee_spread_list.set_model(self.fee_spread_liststore)
        column_one = TreeViewColumn('Date')
        column_two = TreeViewColumn('Amount')
        self.fee_spread_list.append_column(column_one)
        self.fee_spread_list.append_column(column_two)
        for i, column in enumerate((column_one, column_two)):
            cell = CellRendererText()
            column.pack_start(cell, False)
            column.add_attribute(cell, 'text', i)
        self.populate_fee_spread_liststore()

        
    def populate_fee_spread_liststore(self):
        self.fee_spread_liststore.clear()
        for month in gen_n_months_starting_from(first_of(date.today()), 4):
            self.fee_spread_liststore.append((month, '10'))

    def detach(self):
        self.vbox1.reparent(self.window1)

    def day_changed(self, *args):
        print('day_changed')

    def amount_collected_changed(self, *args):
        print('amount_collected_changed')

    def member_changed(self, *args):
        print('member_changed')
示例#3
0
class QueuedTorrents(component.Component):
    def __init__(self):
        component.Component.__init__(self, 'QueuedTorrents', depend=['StatusBar', 'AddTorrentDialog'])
        self.queue = []
        self.status_item = None

        self.config = ConfigManager('gtkui.conf')
        self.builder = Builder()
        self.builder.add_from_file(deluge.common.resource_filename(
            'deluge.ui.gtkui', os.path.join('glade', 'queuedtorrents.ui')))
        self.builder.get_object('chk_autoadd').set_active(self.config['autoadd_queued'])
        self.dialog = self.builder.get_object('queued_torrents_dialog')
        self.dialog.set_icon(get_logo(32))

        self.builder.connect_signals(self)

        self.treeview = self.builder.get_object('treeview')
        self.treeview.append_column(TreeViewColumn(_('Torrent'), CellRendererText(), text=0))

        self.liststore = ListStore(str, str)
        self.treeview.set_model(self.liststore)
        self.treeview.set_tooltip_column(1)

    def run(self):
        self.dialog.set_transient_for(component.get('MainWindow').window)
        self.dialog.show()

    def start(self):
        if len(self.queue) == 0:
            return

        # Make sure status bar info is showing
        self.update_status_bar()

        # We only want the add button sensitive if we're connected to a host
        self.builder.get_object('button_add').set_sensitive(True)

        if self.config['autoadd_queued'] or self.config['standalone']:
            self.on_button_add_clicked(None)
        else:
            self.run()

    def stop(self):
        # We only want the add button sensitive if we're connected to a host
        self.builder.get_object('button_add').set_sensitive(False)
        self.update_status_bar()

    def add_to_queue(self, torrents):
        """Adds the list of torrents to the queue"""
        # Add to the queue while removing duplicates
        self.queue = list(set(self.queue + torrents))

        # Update the liststore
        self.liststore.clear()
        for torrent in self.queue:
            if deluge.common.is_magnet(torrent):
                magnet = deluge.common.get_magnet_info(torrent)
                self.liststore.append([magnet['name'], torrent])
            else:
                self.liststore.append([os.path.split(torrent)[1], torrent])

        # Update the status bar
        self.update_status_bar()

    def update_status_bar(self):
        """Attempts to update status bar"""
        # If there are no queued torrents.. remove statusbar widgets and return
        if len(self.queue) == 0:
            if self.status_item is not None:
                component.get('StatusBar').remove_item(self.status_item)
                self.status_item = None
            return False

        try:
            component.get('StatusBar')
        except Exception:
            # The statusbar hasn't been loaded yet, so we'll add a timer to
            # update it later.
            timeout_add(100, self.update_status_bar)
            return False

        # Set the label text for statusbar
        if len(self.queue) > 1:
            label = str(len(self.queue)) + _(' Torrents Queued')
        else:
            label = str(len(self.queue)) + _(' Torrent Queued')

        # Add the statusbar items if needed, or just modify the label if they
        # have already been added.
        if self.status_item is None:
            self.status_item = component.get('StatusBar').add_item(
                stock=STOCK_SORT_DESCENDING,
                text=label,
                callback=self.on_statusbar_click)
        else:
            self.status_item.set_text(label)

        # We return False so the timer stops
        return False

    def on_statusbar_click(self, widget, event):
        log.debug('on_statusbar_click')
        self.run()

    def on_button_remove_clicked(self, widget):
        selected = self.treeview.get_selection().get_selected()[1]
        if selected is not None:
            path = self.liststore.get_value(selected, 1)
            self.liststore.remove(selected)
            self.queue.remove(path)
            self.update_status_bar()

    def on_button_clear_clicked(self, widget):
        self.liststore.clear()
        del self.queue[:]
        self.update_status_bar()

    def on_button_close_clicked(self, widget):
        self.dialog.hide()

    def on_button_add_clicked(self, widget):
        # Add all the torrents in the liststore
        def add_torrent(model, path, _iter, data):
            torrent_path = model.get_value(_iter, 1).decode('utf-8')
            process_args([torrent_path])

        self.liststore.foreach(add_torrent, None)
        del self.queue[:]
        self.dialog.hide()
        self.update_status_bar()

    def on_chk_autoadd_toggled(self, widget):
        self.config['autoadd_queued'] = widget.get_active()
示例#4
0
文件: input.py 项目: axelson/pomni
class InputWidget(Component):
    """Input mode widget for Rainbow theme."""
    
    def __init__(self, component_manager):

        Component.__init__(self, component_manager)

        self.w_tree = self.main_widget().w_tree
        self.get_widget = get_widget = self.w_tree.get_widget
        self.conf = self.config()
        self.connections = []
        self.connect_signals([\
            ("input_mode_toolbar_button_back_w", "clicked", \
                self.input_to_main_menu_cb),
            ("front_to_back_mode_selector_w", "released", \
                self.change_card_type_cb),
            ("both_way_mode_selector_w", "released", self.change_card_type_cb),
            ("three_side_mode_selector_w", "released", \
                self.change_card_type_cb),
            ("cloze_mode_selector_w", "released", self.change_card_type_cb),
            ("picture_content_button", "clicked", self.add_picture_cb),
            ("image_selection_dialog_button_select", "clicked", \
                self.select_item_cb),
            ("image_selection_dialog_button_close", "clicked",
                self.close_media_selection_dialog_cb),
            ("input_mode_prev_category_w", "clicked", self.change_category_cb),
            ("input_mode_next_category_w", "clicked", self.change_category_cb),
            ("input_mode_add_new_category_w", "clicked", \
                self.create_new_category_cb),
            ("sound_content_button", "clicked", self.add_sound_cb),
            ("category_name_container", "clicked", \
                self.show_add_category_block_cb),
            ("input_mode_close_add_category_block_w", "clicked",
                self.hide_add_category_block_cb),
            ("input_mode_snd_button", "released", \
                self.preview_sound_in_input_cb)])

        self.fact = None
        self.sounddir = None
        self.imagedir = None
        self.card_type = None
        self.categories_list = []
        self.added_new_cards = False
        #liststore = [text, type, filename, dirname, pixbuf]
        self.liststore = ListStore(str, str, str, str, gtk.gdk.Pixbuf)
        iconview_widget = get_widget("iconview_widget")
        iconview_widget.set_model(self.liststore)
        iconview_widget.set_pixbuf_column(4)
        iconview_widget.set_text_column(0)

        get_widget = self.get_widget
        # Widgets as attributes
        self.areas = {# Text areas
            "cloze": get_widget("cloze_text_w"),
            "answer":  get_widget("answer_text_w"),
            "foreign": get_widget("foreign_text_w"),
            "question": get_widget("question_text_w"),
            "translation": get_widget("translation_text_w"),
            "pronunciation": get_widget("pronun_text_w")
        }
        # Mandatory color setup fot GtkTextView
        for area in self.areas.values():
            area.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#FFFFFF"))
            area.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))

        # Change default font
        font = pango.FontDescription("Nokia Sans %s" % \
            (self.conf['font_size'] - FONT_DISTINCTION))
        for area in self.areas.values():
            area.modify_font(font)

        self.widgets = {# Other widgets
            "CurrentCategory": get_widget("category_name_w"),
            "SoundButton": get_widget("sound_content_button"),
            "PictureButton": get_widget("picture_content_button"),
            "SoundIndicator": get_widget("input_mode_snd_button"),
            "CardTypeSwithcer": get_widget("card_type_switcher_w"),
            "MediaDialog": get_widget("media_selection_dialog"),
            "SoundContainer": get_widget("input_mode_snd_container"),
            "QuestionContainer": get_widget("input_mode_question_container"),
            "NewCategory": get_widget("input_mode_new_category_entry"),
            "ChangeCategoryBlock": get_widget(\
                "input_mode_change_category_block"),
            "AddCategoryBlock": get_widget("input_mode_add_category_block")
        }
        # Mandatory color setup fot GtkEntry
        self.widgets["NewCategory"].modify_base(gtk.STATE_NORMAL, \
            gtk.gdk.color_parse("#FFFFFF"))
        self.widgets["NewCategory"].modify_text(gtk.STATE_NORMAL, \
            gtk.gdk.color_parse("#000000"))

        # card_id: {"page": page_id, "selector": selector_widget, 
        # "widgets": [(field_name:text_area_widget)...]}
        self.selectors = {
            FrontToBack.id: {
            "page": 0, 
            "selector": get_widget("front_to_back_mode_selector_w"),
            "widgets": [('q', self.areas["question"]), 
                        ('a', self.areas["answer"])]
            },
            BothWays.id: {
            "page": 0,
            "selector": get_widget("both_way_mode_selector_w"),
            "widgets": [('q', self.areas["question"]), 
                        ('a', self.areas["answer"])]
            },
            ThreeSided.id: {
            "page": 1,
            "selector": get_widget("three_side_mode_selector_w"),
            "widgets": [('f', self.areas["foreign"]),
                        ('t', self.areas["translation"]),
                        ('p', self.areas["pronunciation"])]
            },
            Cloze.id: {
            "page": 2,
            "selector": get_widget("cloze_mode_selector_w"),
            "widgets": [('text', self.areas["cloze"])]
            }
        }
        # add card_type to selectors subdict
        for card_type in self.card_types():
            self.selectors[card_type.id]["card_type"] = card_type

        # create {selector_widget:card_type.id} dict
        self.widget_card_id = dict((self.selectors[id]["selector"], id) \
            for id in self.selectors.keys())

        self.set_card_type(get_widget("front_to_back_mode_selector_w"))
        self.compose_widgets()

        # Turn off hildon autocapitalization
        try:
            for widget in self.areas.values():
                widget.set_property("hildon-input-mode", 'full')
        # stock gtk doesn't have hildon properties
        except (TypeError, AttributeError): 
            pass # so, skip silently

    def connect_signals(self, control):
        """Connect signals to widgets and save connection info."""

        for wname, signal, callback in control:
            widget = self.get_widget(wname)
            cid = widget.connect(signal, callback)
            self.connections.append((widget, cid))

    def disconnect_signals(self):
        """Disconnect previously connected signals."""

        for widget, cid in self.connections:
            widget.disconnect(cid)
        self.connections = []

    def show_snd_container(self):
        """Show or hide snd container. """
                    
        start, end = self.areas["question"].get_buffer().get_bounds()
        text = self.areas["question"].get_buffer().get_text(start, end)
        if "sound src=" in text:
            self.widgets["QuestionContainer"].hide()
            self.widgets["SoundContainer"].show()
        else:
            self.widgets["QuestionContainer"].show()
            self.widgets["SoundContainer"].hide()

    def compose_widgets (self):
        """Switch to neccessary input page. It depends on card_type."""

        self.widgets["CardTypeSwithcer"].set_current_page( \
            self.selectors[self.card_type.id]["page"])
        self.selectors[self.card_type.id]["selector"].set_active(True)
        state = self.card_type.id in (FrontToBack.id)
        self.widgets["PictureButton"].set_sensitive(state)
        self.widgets["SoundButton"].set_sensitive(state)

    def set_card_type(self, widget):
        """Set current card type."""

        card_type_id = self.widget_card_id[widget]
        self.card_type = self.selectors[card_type_id]["card_type"]

    def update_categories(self):
        """Update categories list content."""

        if not self.categories_list:
            categories = dict([(i, name) for (i, name) in \
                enumerate(self.database().tag_names())])
            if categories.values():
                for category in sorted(categories.values()):
                    self.categories_list.append(category)
                self.widgets["CurrentCategory"].set_text( \
                    sorted(categories.values())[0])
            else:
                self.categories_list.append("default category")
                self.widgets["CurrentCategory"].set_text("default category")

    def check_complete_input(self):
        """Check for non empty fields."""

        pattern_list = ["Type %s here..." % item for item in ["ANSWER", \
            "QUESTION", "FOREIGN", "PRONUNCIATION", "TRANSLATION", "TEXT"]]
        pattern_list.append("")
        for selector in self.selectors[self.card_type.id]["widgets"]:
            buf = selector[1].get_buffer()
            start, end = buf.get_bounds()
            if buf.get_text(start, end) in pattern_list:
                return False
        return True

    def change_category_cb(self, widget):
        """Change current category."""

        if widget.name == "prev_category_w":
            direction = -1
        direction = 1
        category_index = self.categories_list.index( \
            self.widgets["CurrentCategory"].get_text())
        try:
            new_category = self.categories_list[category_index + direction]
        except IndexError:
            if direction:
                new_category = self.categories_list[0]
            else:
                new_category = self.categories_list[len(self.categories_list)-1]
        self.widgets["CurrentCategory"].set_text(new_category)

    def create_new_category_cb(self, widget):
        """Create new category."""

        new_category = self.widgets["NewCategory"].get_text()
        if new_category:
            self.categories_list.append(new_category)
            self.widgets["NewCategory"].set_text("")
            self.widgets["CurrentCategory"].set_text(new_category)
            self.hide_add_category_block_cb(None)

    def add_picture_cb(self, widget):
        """Show image selection dialog."""

        self.main_widget().soundplayer.stop()
        self.liststore.clear()
        self.imagedir = self.conf['imagedir']
        if not os.path.exists(self.imagedir):
            self.imagedir = "./images" # on Desktop
            if not os.path.exists(self.imagedir):
                self.main_widget().information_box(\
                    _("'Images' directory does not exist!"))
                return
        if os.listdir(self.imagedir):
            self.widgets["MediaDialog"].show()
            for fname in os.listdir(self.imagedir):
                if os.path.isfile(os.path.join(self.imagedir, fname)):
                    pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(\
                        os.path.join(self.imagedir, fname), 100, 100)
                    self.liststore.append(["", "img", fname, \
                        self.imagedir, pixbuf])
        else:
            self.main_widget().information_box(\
                _("There are no files in 'Images' directory!"))

    def select_item_cb(self, widget):
        """ 
        Set html-text with media path and type when user
        select media filefrom media selection dialog. 
        """
        self.widgets["MediaDialog"].hide()
        item_index = self.w_tree.get_widget("iconview_widget"). \
            get_selected_items()[0]
        item_type = self.liststore.get_value( \
            self.liststore.get_iter(item_index), 1)
        item_fname = self.liststore.get_value( \
            self.liststore.get_iter(item_index), 2)
        item_dirname = self.liststore.get_value( \
            self.liststore.get_iter(item_index), 3)
        question_text = """<%s src="%s">""" % \
            (item_type, os.path.abspath(os.path.join(item_dirname, item_fname)))
        self.areas["question"].get_buffer().set_text(question_text)
        self.show_snd_container()

    def add_sound_cb(self, widget):
        """Show sound selection dialog."""

        self.main_widget().soundplayer.stop()
        self.liststore.clear()
        self.sounddir = self.conf['sounddir']
        if not os.path.exists(self.sounddir):
            self.sounddir = "./sounds" # on Desktop
            if not os.path.exists(self.sounddir):
                self.main_widget().information_box(\
                    _("'Sounds' directory does not exist!"))
                return     
        if os.listdir(self.sounddir):
            self.widgets["MediaDialog"].show()
            for fname in os.listdir(self.sounddir):
                if os.path.isfile(os.path.join(self.sounddir, fname)):
                    sound_logo_file = os.path.join( \
                        self.conf["theme_path"], "soundlogo.png")
                    pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(\
                        sound_logo_file, 100, 100)
                    self.liststore.append([fname, "sound", fname, \
                        self.sounddir, pixbuf])
        else:
            self.main_widget().information_box(\
                _("There are no files in 'Sounds' directory!"))

    def get_widgets_data(self, check_for_required=True):
        """ Get data from widgets. """

        fact = {}
        for fact_key, widget in self.selectors[self.card_type.id]["widgets"]:
            start, end = widget.get_buffer().get_bounds()
            fact[fact_key] = widget.get_buffer().get_text(start, end)
        if check_for_required:
            for required in self.card_type.required_fields:
                if not fact[required]:
                    raise ValueError
        return fact

    def set_widgets_data(self, fact):
        """Set widgets data from fact."""

        for fact_key, widget in self.selectors[self.card_type.id]["widgets"]:
            widget.get_buffer().set_text(fact[fact_key])

    def clear_widgets(self):
        """Clear data in widgets."""

        for caption in self.areas.keys():
            self.areas[caption].get_buffer().set_text( \
                "Type %s here..." % caption.upper())

    def change_card_type_cb(self, widget):
        """Change cardtype when user choose it from cardtype column."""
                
        self.main_widget().soundplayer.stop()
        self.clear_widgets()
        self.show_snd_container()
        self.set_card_type(widget)
        self.compose_widgets()

    def preview_sound_in_input_cb(self, widget):
        """Preview sound in input mode."""

        if widget.get_active():
            start, end = self.areas["question"].get_buffer().get_bounds()
            text = self.areas["question"].get_buffer().get_text(start, end)
            self.main_widget().soundplayer.play(text, self)
        else:
            self.main_widget().soundplayer.stop()

    def update_indicator(self):
        """Set non active state for widget."""

        self.widgets["SoundIndicator"].set_active(False)

    def close_media_selection_dialog_cb(self, widget):
        """Close image selection dialog."""

        self.widgets["MediaDialog"].hide()

    def show_add_category_block_cb(self, widget):
        """Show add category block."""

        self.widgets["ChangeCategoryBlock"].hide()
        self.widgets["AddCategoryBlock"].show()
        self.widgets["NewCategory"].grab_focus()

    def hide_add_category_block_cb(self, widget):
        """Hide add category block."""

        self.widgets["ChangeCategoryBlock"].show()
        self.widgets["AddCategoryBlock"].hide()

    def input_to_main_menu_cb(self, widget):
        """Return to main menu."""

        #if self.added_new_cards:
            #self.review_controller().reset()
            #self.added_new_cards = False
        self.disconnect_signals()
        self.main_widget().soundplayer.stop()
        self.main_widget().menu_()
示例#5
0
class PeersTab(Tab):
    def __init__(self):
        super(PeersTab, self).__init__('Peers', 'peers_tab', 'peers_tab_label')

        self.peer_menu = self.main_builder.get_object('menu_peer_tab')
        component.get('MainWindow').connect_signals(self)

        self.listview = self.main_builder.get_object('peers_listview')
        self.listview.props.has_tooltip = True
        self.listview.connect('button-press-event',
                              self._on_button_press_event)
        self.listview.connect('query-tooltip', self._on_query_tooltip)

        # flag, ip, client, downspd, upspd, country code, int_ip, seed/peer icon, progress
        self.liststore = ListStore(Pixbuf, str, str, int, int, str, float,
                                   Pixbuf, float)
        self.cached_flag_pixbufs = {}

        self.seed_pixbuf = icon_seeding
        self.peer_pixbuf = icon_downloading

        # key is ip address, item is row iter
        self.peers = {}

        # Country column
        column = TreeViewColumn()
        render = CellRendererPixbuf()
        column.pack_start(render, False)
        column.add_attribute(render, 'pixbuf', 0)
        column.set_sort_column_id(5)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(20)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Address column
        column = TreeViewColumn(_('Address'))
        render = CellRendererPixbuf()
        column.pack_start(render, False)
        column.add_attribute(render, 'pixbuf', 7)
        render = CellRendererText()
        column.pack_start(render, False)
        column.add_attribute(render, 'text', 1)
        column.set_sort_column_id(6)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(100)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Client column
        column = TreeViewColumn(_('Client'))
        render = CellRendererText()
        column.pack_start(render, False)
        column.add_attribute(render, 'text', 2)
        column.set_sort_column_id(2)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(100)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Progress column
        column = TreeViewColumn(_('Progress'))
        render = CellRendererProgress()
        column.pack_start(render, True)
        column.set_cell_data_func(render, cell_data_peer_progress, 8)
        column.set_sort_column_id(8)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(100)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Down Speed column
        column = TreeViewColumn(_('Down Speed'))
        render = CellRendererText()
        column.pack_start(render, False)
        column.set_cell_data_func(render, cell_data_speed_down, 3)
        column.set_sort_column_id(3)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(50)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Up Speed column
        column = TreeViewColumn(_('Up Speed'))
        render = CellRendererText()
        column.pack_start(render, False)
        column.set_cell_data_func(render, cell_data_speed_up, 4)
        column.set_sort_column_id(4)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(50)
        # Bugfix: Last column needs max_width set to stop scrollbar appearing
        column.set_max_width(150)
        column.set_reorderable(True)
        self.listview.append_column(column)

        self.listview.set_model(self.liststore)

        self.load_state()

        self.torrent_id = None

    def save_state(self):
        # Get the current sort order of the view
        column_id, sort_order = self.liststore.get_sort_column_id()

        # Setup state dict
        state = {
            'columns': {},
            'sort_id': column_id,
            'sort_order': int(sort_order) if sort_order else None
        }

        for index, column in enumerate(self.listview.get_columns()):
            state['columns'][column.get_title()] = {
                'position': index,
                'width': column.get_width()
            }
        save_pickled_state_file('peers_tab.state', state)

    def load_state(self):
        state = load_pickled_state_file('peers_tab.state')

        if state is None:
            return

        if len(state['columns']) != len(self.listview.get_columns()):
            log.warning('peers_tab.state is not compatible! rejecting..')
            return

        if state['sort_id'] and state['sort_order'] is not None:
            self.liststore.set_sort_column_id(state['sort_id'],
                                              state['sort_order'])

        for (index, column) in enumerate(self.listview.get_columns()):
            cname = column.get_title()
            if cname in state['columns']:
                cstate = state['columns'][cname]
                column.set_sizing(TREE_VIEW_COLUMN_FIXED)
                column.set_fixed_width(
                    cstate['width'] if cstate['width'] > 0 else 10)
                if state['sort_id'] == index and state[
                        'sort_order'] is not None:
                    column.set_sort_indicator(True)
                    column.set_sort_order(state['sort_order'])
                if cstate['position'] != index:
                    # Column is in wrong position
                    if cstate['position'] == 0:
                        self.listview.move_column_after(column, None)
                    elif self.listview.get_columns()[cstate['position'] -
                                                     1].get_title() != cname:
                        self.listview.move_column_after(
                            column,
                            self.listview.get_columns()[cstate['position'] -
                                                        1])

    def update(self):
        # Get the first selected torrent
        torrent_id = component.get('TorrentView').get_selected_torrents()

        # Only use the first torrent in the list or return if None selected
        if len(torrent_id) != 0:
            torrent_id = torrent_id[0]
        else:
            # No torrent is selected in the torrentview
            self.liststore.clear()
            return

        if torrent_id != self.torrent_id:
            # We only want to do this if the torrent_id has changed
            self.liststore.clear()
            self.peers = {}
            self.torrent_id = torrent_id

        component.get('SessionProxy').get_torrent_status(
            torrent_id, ['peers']).addCallback(self._on_get_torrent_status)

    def get_flag_pixbuf(self, country):
        if not country.strip():
            return None

        if country not in self.cached_flag_pixbufs:
            # We haven't created a pixbuf for this country yet
            try:
                self.cached_flag_pixbufs[country] = pixbuf_new_from_file(
                    deluge.common.resource_filename(
                        'deluge',
                        os.path.join('ui', 'data', 'pixmaps', 'flags',
                                     country.lower() + '.png')))
            except Exception as ex:
                log.debug('Unable to load flag: %s', ex)
                return None

        return self.cached_flag_pixbufs[country]

    def _on_get_torrent_status(self, status):
        new_ips = set()
        for peer in status['peers']:
            new_ips.add(peer['ip'])
            if peer['ip'] in self.peers:
                # We already have this peer in our list, so lets just update it
                row = self.peers[peer['ip']]
                if not self.liststore.iter_is_valid(row):
                    # This iter is invalid, delete it and continue to next iteration
                    del self.peers[peer['ip']]
                    continue
                values = self.liststore.get(row, 3, 4, 5, 7, 8)
                if peer['down_speed'] != values[0]:
                    self.liststore.set_value(row, 3, peer['down_speed'])
                if peer['up_speed'] != values[1]:
                    self.liststore.set_value(row, 4, peer['up_speed'])
                if peer['country'] != values[2]:
                    self.liststore.set_value(row, 5, peer['country'])
                    self.liststore.set_value(
                        row, 0, self.get_flag_pixbuf(peer['country']))
                if peer['seed']:
                    icon = self.seed_pixbuf
                else:
                    icon = self.peer_pixbuf

                if icon != values[3]:
                    self.liststore.set_value(row, 7, icon)

                if peer['progress'] != values[4]:
                    self.liststore.set_value(row, 8, peer['progress'])
            else:
                # Peer is not in list so we need to add it

                # Create an int IP address for sorting purposes
                if peer['ip'].count(':') == 1:
                    # This is an IPv4 address
                    ip_int = sum([
                        int(byte) << shift for byte, shift in zip(
                            peer['ip'].split(':')[0].split('.'), (24, 16, 8,
                                                                  0))
                    ])
                    peer_ip = peer['ip']
                else:
                    # This is an IPv6 address
                    import socket
                    import binascii
                    # Split out the :port
                    ip = ':'.join(peer['ip'].split(':')[:-1])
                    ip_int = int(
                        binascii.hexlify(socket.inet_pton(socket.AF_INET6,
                                                          ip)), 16)
                    peer_ip = '[%s]:%s' % (ip, peer['ip'].split(':')[-1])

                if peer['seed']:
                    icon = self.seed_pixbuf
                else:
                    icon = self.peer_pixbuf

                row = self.liststore.append([
                    self.get_flag_pixbuf(peer['country']), peer_ip,
                    peer['client'], peer['down_speed'], peer['up_speed'],
                    peer['country'],
                    float(ip_int), icon, peer['progress']
                ])

                self.peers[peer['ip']] = row

        # Now we need to remove any ips that were not in status["peers"] list
        for ip in set(self.peers).difference(new_ips):
            self.liststore.remove(self.peers[ip])
            del self.peers[ip]

    def clear(self):
        self.liststore.clear()

    def _on_button_press_event(self, widget, event):
        """This is a callback for showing the right-click context menu."""
        log.debug('on_button_press_event')
        # We only care about right-clicks
        if self.torrent_id and event.button == 3:
            self.peer_menu.popup(None, None, None, event.button, event.time)
            return True

    def _on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip):
        if not widget.get_tooltip_context(x, y, keyboard_tip):
            return False
        else:
            model, path, _iter = widget.get_tooltip_context(x, y, keyboard_tip)

            country_code = model.get(_iter, 5)[0]
            if country_code != '  ' and country_code in COUNTRIES:
                tooltip.set_text(COUNTRIES[country_code])
                # widget here is self.listview
                widget.set_tooltip_cell(tooltip, path, widget.get_column(0),
                                        None)
                return True
            else:
                return False

    def on_menuitem_add_peer_activate(self, menuitem):
        """This is a callback for manually adding a peer"""
        log.debug('on_menuitem_add_peer')
        builder = Builder()
        builder.add_from_file(
            deluge.common.resource_filename(
                'deluge.ui.gtkui',
                os.path.join('glade', 'connect_peer_dialog.ui')))
        peer_dialog = builder.get_object('connect_peer_dialog')
        txt_ip = builder.get_object('txt_ip')
        response = peer_dialog.run()
        if response:
            value = txt_ip.get_text()
            if value and ':' in value:
                if ']' in value:
                    # ipv6
                    ip = value.split(']')[0][1:]
                    port = value.split(']')[1][1:]
                else:
                    # ipv4
                    ip = value.split(':')[0]
                    port = value.split(':')[1]
                if deluge.common.is_ip(ip):
                    log.debug('adding peer %s to %s', value, self.torrent_id)
                    client.core.connect_peer(self.torrent_id, ip, port)
        peer_dialog.destroy()
        return True
class MainWindow(object):
    """Represents the main window for the default BoKeep shell.

    This class has a lot of imperitive/procedural code with plenty of side
    effects, and the cost of them has started to show in subtle bugs.
    It would be a lost worse if the state machine BoKeepGuiState wasn't used.
    Could be a lot better if an even higher level state machine or something
    a lot more functional and a lot less imperitive were used.

    Oh well, in the meantime, here's a depth call graph with a little pseudo-
    code of the most intertangled functions from this module and major calls to
    other modules that alter the state of the interface.
    
    This is done starting from most significant entry points into this
    class's code.
    Please keep it up to date and also try to reflect the interdependencies
    in the in per function docstrings.
    
    This is pretty much the only way one can get a big picture overview of
    how much of the code here is interrelated

    __init__ (called by shell_startup prior to gtk.main() )
      build_gui
        set_sensitivities_and_status
          set_backend_error_indicator
          set_transid_label

    startup_event_handler (first thing called by gtk.main() )
      after_background_load
        if no book selected and at least one available
           guistate.do_action(BOOK_CHANGE, first book)
        else if a book selected matches one that exists
           guistate.do_action(RESET)
        else if no book selected that matches, or none even available
             guistate.do_action(BOOK_CHANGE, None)

        books_combobox_model.append
        books_combobox.set_active
        if a current book in the combo can not be determined
          set_book_from_combo
              guistate.do_action(BOOK_CHANGE, current_book_selected)
        refresh_trans_types_and_set_sensitivities_and_status
          refresh_trans_types
            trans_type_model.clear
            trans_type_model.append
            if a transaction type was found matching current
              set_transcombo_index
              reset_trans_view
                self.hide_transaction
                self.current_editor set with new editor instance  
            else no transaction type found
              hide_transaction
          END refresh_trans_types as called by..
               refresh_trans_types_and_set_sensitivities_and_status
          set_sensitivities_and_status
            set_backend_error_indicator
            set_transid_label
        END refresh_trans_types_and_set_sensitivities_and_status
             as called by after_background_load
      END after_background_load as called by startup_event_handler
    END startup_event_handler
    
    on_books_combobox_changed (called by gtk gui thread when book combo changed)
      set_book_from_combo
        guistate.do_action(BOOK_CHANGE, newly selected book)
      refresh_trans_types_and_set_sensitivities_and_status
        refresh_trans_types
          trans_type_model.clear
          trans_type_model.append
          if a transaction type was found matching current
            set_transcombo_index
            reset_trans_view
              self.hide_transaction
              self.current_editor set with new editor instance  
          else no transaction type found
            hide_transaction
        END refresh_trans_types as called by..
             refresh_trans_types_and_set_sensitivities_and_status
        set_sensitivities_and_status
          set_backend_error_indicator
          set_transid_label
      END refresh_trans_types_and_set_sensitivities_and_status as 
           called by on_books_combobox_changed
    END on_books_combobox_changed

    new_button_clicked (called by gtk gui thread when new button clicked)
      guistate.do_action(NEW)
      set_trans_type_combo_to_current_and_reset_view
        set_transcombo_index
        reset_trans_view
          self.hide_transaction
          self.current_editor set with new editor instance
      set_sensitivities_and_status
        set_backend_error_indicator
        set_transid_label

    delete_button_clicked (called by gtk gui thread when delete button
                             clicked)
      guistate.do_action(DELETE)
         if there is no transaction left
           set_transcombo_index
           hide_transaction
         else there is a transaction left
           set_trans_type_combo_to_current_and_reset_view
            set_transcombo_index
            reset_trans_view
              self.hide_transaction
              self.current_editor set with new editor instance
           set_sensitivities_and_status
             set_backend_error_indicator
             set_transid_label
    
    trans_type_changed (called by gtk gui thread when trans type
                        combo changed)
      guistate.do_action(TYPE_CHANGE, active transaction type selection)
      self.reset_trans_view
        self.hide_transaction
        self.current_editor set with new editor instance
      set_sensitivities_and_status
        set_backend_error_indicator
        set_transid_label

    on_forward_button_clicked() and on_back_button_clicked()
      (called by gtk gui thread when back and forward buttons are clicked)
       guistate.do_action(FORWARD)
      set_trans_type_combo_to_current_and_reset_view()
        set_transcombo_index( based on transaction being moved too)
        reset_trans_view()
          self.hide_transaction()
          self.current_editor set with new editor instance
      set_sensitivities_and_status()
        all buttons, combos, and plugin menu are set_sensitive
          based on guistate, thanks to gui_built having just become True
          set_backend_error_indicator()
        gui_built is definitely true so this updates backend error
          label and related widgets
        set_transid_label()
          gui_built is definitely True, this is set based on current
           transaction

    on_configuration1_activate called by gtk gui thread when config item in
                               menu is activated
      closedown_for_config()
        gui_built = False
        guistate.do_action(CLOSE)
        books_combobox.set_active(COMBO_SELECTION_NONE)
        on_books_combobox_changed is called due to above, but the
          gui_built = False line prevents it from doing anything       
        books_combobox_model.clear() gui_built = False also stops the event
                                     handler for combo change here
        set_transcombo_index(COMBO_SELECTION_NONE)
        self.programmatic_transcombo_index = True
        self.trans_type_model.clear() event handler runs, but goes nowhere
                                      due to self.programmatic_transcombo_index
                                      flag
        self.programmatic_transcombo_index = False
      bookset.close()
      bookset = run config dialog, get new bookset from it
      after_background_load()
        self.guistate loaded from database (or established if new)
        if no book selected and at least one available
           guistate.do_action(BOOK_CHANGE, first book)
        else if a book selected matches one that exists
           guistate.do_action(RESET)
        else if no book selected that matches, or none even available
           guistate.do_action(BOOK_CHANGE, None)
        books_combobox_model filled in with possible books
        books_combobox set to the current book, or one selected if needed
        if a current book in the combo can not be determined
          books_combobox.set_active(first book)
          set_book_from_combo()
              guistate.do_action(BOOK_CHANGE, current_book_selected)
        else
           books_combobox.set_active( selected book as per self.guistate)
        self.gui_built = True
        refresh_trans_types_and_set_sensitivities_and_status()
          refresh_trans_types()
            exit if no book selected
            trans_type_model is cleared and reconstucted from all active
             plugins, attempt is made to identify transaction type
             matching current transaction (if any)
            trans_type_combo is setup with new model
            if a transaction type was found matching current
              set_transcombo_index()
              reset_trans_view()
                self.hide_transaction()
                self.current_editor set with new editor instance  
            else no transaction type found
              hide_transaction()
          END refresh_trans_types as called by..
               refresh_trans_types_and_set_sensitivities_and_status
          set_sensitivities_and_status()
            all buttons, combos, and plugin menu are set_sensitive
             based on guistate, thanks to gui_built having just become True
            set_backend_error_indicator()
              with gui_built now being true, this updates backend error
              label and related widgets
            set_transid_label()
              with gui_built now being True, this is set based on current
               transaction
          END set_sensitivities_and_status() as called by...
               refresh_trans_types_and_set_sensitivities_and_status
        END refresh_trans_types_and_set_sensitivities_and_status
             as called by after_background_load
      END after_background_load as called by on_configuration1_activate
    END on_configuration1_activate

    on_configure_plugin1_activate
      Run the configuration dialog for the active plugin
      reset_trans_view()
        self.hide_transaction()
        self.current_editor set with new editor instance
    END on_configure_plugin1_activate
    """
    
    # Functions for window initialization 

    def on_quit_activate(self, args):
        self.application_shutdown()

    def __init__(self, config_path, config, bookset, startup_callback):
        self.gui_built = False
        self.current_editor = None
        self.set_config_path_and_config(config_path, config)
        self.set_bookset(bookset)
        
        self.build_gui()
        self.programmatic_transcombo_index = False
        self.__startup_callback = startup_callback

        # program in an event that will only run once on startup
        # the startup_event_handler function will use
        # self.startup_event_handler to disconnect itself
        self.startup_event_handler = self.mainwindow.connect(
            "window-state-event", self.startup_event_handler )
        # should we do an emit to ensure it happens, or be satisfyed
        # that it always happens in tests?

    def set_config_path_and_config(self, config_path, config):
        self.__config_path = config_path
        self.__config = config

    def set_bookset(self, bookset):
        """Sets the set of BoKeep books."""
        
        self.bookset = bookset

    def flush_book_backend(self, book):
        """Save the BoKeep book."""
        
        book.get_backend_plugin().flush_backend()
        transaction.get().commit()

    def close_book_backend(self, book):
        """Close the backend used for saving the BoKeep book."""
        
        book.get_backend_plugin().close()
        transaction.get().commit()

    def application_shutdown(self):
        if hasattr(self, 'guistate'):
            self.guistate.do_action(CLOSE)
        if hasattr(self, 'bookset') and self.bookset != None:
            for bookname, book in self.bookset.iterbooks():
                book.get_backend_plugin().flush_backend()
                transaction.get().commit()
            self.bookset.close()
            # or, should I be only doing
            # self.bookset.close_primary_connection() instead..?
        main_quit()

    def startup_event_handler(self, *args):
        """An event handler programmed to run as soon as gtk's main loop
        takes over

        After disabling itself from running again, this
        calls the __start_callback attribute established in __init__ and
        if that function returns true calls after_background_load for further
        processing. If it fails, application_shutdown() is called

        Part of the motivation here is to defer stuff we didn't want to do
        in __init__ such as calling gtk.main()
        """
        # this should only be programmed to run once
        assert( hasattr(self, 'startup_event_handler') )
        self.mainwindow.disconnect(self.startup_event_handler)
        # remove the attribute startup_event_handler to intentionally
        # cause the event handler to fail
        delattr(self, 'startup_event_handler')
        assert(not self.gui_built)

        # the motivation for making a call back to the high level shell
        # code is that we're able to get the gui thread rolling
        # and have any dialogs that need to be launched by the shell
        # startup code be called at this point, parented to the main
        # window we've managed to build up and get ready in __init__
        if self.__startup_callback(self.__config_path, self.__config,
                                   self.set_config_path_and_config,
                                   self.set_bookset, self.mainwindow ):
            self.after_background_load()
            assert(self.gui_built)
        else:
            self.application_shutdown()

    def build_gui(self):
        """Called by __init__ to take care of the gtk work or putting the
        gui together.

        This consists of loading mainwindow from the glade file,
        setting the bokeep logo, and getting the cell renderers and
        empty models set up for the book and transaction type combo boxes.
        Ends with a a call to set_sensitivities_and_status to nicely
        grey things out until we have something for the shell to actually
        display.

        This is meant to be done without self.book or self.guistate being
        available yet. It shouldn't be called from anywhere other than
        __init__, and only once.
        """
        
        glade_file = get_main_window_glade_file()
        load_glade_file_get_widgets_and_connect_signals(
            glade_file, "mainwindow", self, self )
        self.mainwindow.set_icon_from_file(get_bo_keep_logo())

        self.books_combobox_model = ListStore(str, str, object)
        self.books_combobox.set_model(self.books_combobox_model)
        cell = CellRendererText()
        self.books_combobox.pack_start(cell, True)
        self.books_combobox.add_attribute(cell, 'text', 0)
        self.trans_type_model = ListStore(str, int, object)
        self.set_sensitivities_and_status()
        
    def after_background_load(self):
        """Called to instruct the shell to load from persistent storage the
        last visited book and transaction, sets up the list of books,
        determins the current transaction (if any), sets up the
        transaction type combo for the current bookm and then displays the
        current transaction.

        This is only to be called after the right database is loaded from
        cold, and so far that kind of cold load happens in two places only:
         * at the end of startup_event_handler, which is run once by
           gtk's main thread on shell startup
         * at the end of on_configuration1_activate, where the database
           connection is entirely closed down and possibly even a tottally
           new bookset is loaded
        
        This procedure sets self.guistate right at the start and may call
        self.guistate.do_action with BOOK_CHANGE, and RESET as it determines
        what the current book is. (including possibly None)
        
        Then it builds the list of available books in self.books_combobox_model
        and sets that to whatever the current book is. 

        At that point, the gui can be said to be fully built,
        as all the elements that won't change even when viewing multiple books
        are in place, so self.gui_built = True occures
        The only things left to change are the transaction type combo box
        which can differ per book, and of course the intereface for
        whichever transaction is loaded.

        As a last step, the work described above is done by
        refresh_trans_types_and_set_sensitivities_and_status
        (Be sure to read its docstring)
        """
        self.guistate = (
            self.bookset.get_dbhandle().get_sub_database_do_cls_init(
                GUI_STATE_SUB_DB, BoKeepGuiState) )
        
        # we should do some checking that self.guistate is still reasonable
        # it is possible at this point that the current book or
        # current transaction could be gone due to someone playing with
        # the config or database in some way
        
        bookname_and_book_list = list(self.bookset.iterbooks())
        # if there actually is a book when we think there isn't
        if self.guistate.get_book() == None and len(bookname_and_book_list) > 0:
            self.guistate.do_action(BOOK_CHANGE, bookname_and_book_list[0][1] )
        # if the current book doesn't actually exist anymore
        # this includes the case self.guistate.get_book() == None
        else:
            for book_name, book in bookname_and_book_list:
                # there is a matching book, but it is possible that there
                # is a transaction (when we think none), or the supposedly 
                # current one is gone
                if book == self.guistate.get_book():
                    self.guistate.do_action(RESET)
                    break # else clause below is skipped
            else: # else the current book is no longer in the official list
                self.guistate.do_action(BOOK_CHANGE, None)

        # save the (possibly updated) guistate
        transaction.get().commit()
        
        cur_book_index = None
        for i, (book_name, book) in enumerate(bookname_and_book_list):
            self.books_combobox_model.append((book_name, book_name, book))
            if self.guistate.get_book() != None and \
                    self.guistate.get_book() == book:
                cur_book_index = i
        # this would be the f****d up consequence of a book being deleted
        # ... until we can deal with it, say it can't happen
        assert( not(
                self.guistate.get_book() != None and cur_book_index == None) )
        # should do something similar for the current transaction
        if len(self.books_combobox_model) > 0:
            if cur_book_index == None:
                self.books_combobox.set_active(0)
                self.set_book_from_combo()
            else:
                self.books_combobox.set_active(cur_book_index)
        
        self.gui_built = True

        self.refresh_trans_types_and_set_sensitivities_and_status()

    def closedown_for_config(self):
        """Called soley by on_configuration1_activate to shut down everything
        in the gui prior to running the configuration dialog

        Starts right away by changing self.gui_built = False
        does self.guistate.do_action(CLOSE)
        and clears out the books combo list and transaction type combo list
        """
        self.gui_built = False
        self.guistate.do_action(CLOSE)
        transaction.get().commit() # redundant
        self.books_combobox.set_active(COMBO_SELECTION_NONE)
        self.books_combobox_model.clear()
        self.set_transcombo_index(COMBO_SELECTION_NONE)

        # this clear actually triggers the event handler even after we
        # set to none, I guess losing your model counts as a change!
        self.programmatic_transcombo_index = True
        self.trans_type_model.clear()
        self.programmatic_transcombo_index = False      
        
    # Functions for window initialisation and use thereafter

    def set_book_from_combo(self):
        """Updates self.guistate based on the book selected by the books
        combobox.

        Used by after_background_load and on_books_combobox_changed
        """
        self.guistate.do_action(
            BOOK_CHANGE, 
            self.books_combobox_model[
                self.books_combobox.get_active()][2]
            )

    def refresh_trans_types_and_set_sensitivities_and_status(self):
        """Combines a call to refresh_trans_types and
        set_sensitivities_and_status. in that order
        
        See the respective docstrings

        used by
         * after_background_load as a last step after figuring out the current
           book, constructing the book list, and setting the current book
         * on_books_combobox_changed as a last step after a new book is
           selected
        """
        self.refresh_trans_types()
        self.set_sensitivities_and_status()

    def refresh_trans_types(self):
        """Updates the transaction type combobox to reflect the currently
        selected book and transaction -- then calls self.reset_trans_view()
        to display the current transaction or hide_transaction() to at
        least clear off the old one.

        Does nothing if there isn't a currently selected book
        Alters the state of self.trans_type_model
        Calls reset_trans_view if a transaction type is selected.
        One of the things it does is call hide_transaction()
        Directly call hide_transaction() if there is no transaction type
        selected, in either case we want to be sure there isn't an old gui
        hanging around.
        """
        
        book = self.guistate.get_book()
        if book == None:
            return

        self.programmatic_transcombo_index = True
        self.trans_type_model.clear()
        self.programmatic_transcombo_index = False

        cur_trans = None
        if self.guistate.get_transaction_id() != None:
            cur_trans = book.get_transaction(self.guistate.get_transaction_id())

        frontend_plugins = book.get_frontend_plugins()
        current_trans_type_index = COMBO_SELECTION_NONE
        for i, (code, trans_cls, module) in \
                enumerate(book.get_iter_of_code_class_module_tripplets()):
            self.trans_type_model.append( (
                module.get_transaction_type_pulldown_string_from_code(code),
                code, module ) )
            if cur_trans != None and isinstance(cur_trans, trans_cls):
                current_trans_type_index = i

        self.trans_type_combo.set_model(self.trans_type_model)
        assert( self.trans_type_combo.get_active() == COMBO_SELECTION_NONE )

        if current_trans_type_index != COMBO_SELECTION_NONE:
            self.set_transcombo_index(current_trans_type_index)
            self.reset_trans_view()
        else:
            self.hide_transaction()

    def set_transcombo_index(self, indx):
        """Changed the currently selected transaction type combobox to the
        specified type by index in that combobox

        The event handler, trans_type_changed is prevented from running
        """
        self.programmatic_transcombo_index = True
        self.trans_type_combo.set_active(indx)
        self.programmatic_transcombo_index = False      

    def reset_trans_view(self):
        """Clears away an old transaction editor (if present) with
        hide_transaction and starts up a new one for the current transaction

        It's assumed here that self.guistate does specify a book, transaction,
        and current transaction type to provide an editor. So don't
        call this when that isn't true.

        Called, in this assumed context by, refresh_trans_types,
        set_trans_type_combo_to_current_and_reset_view, trans_type_changed,
        on_configure_plugin1_activate
        """
        book = self.guistate.get_book()
        currindex = self.trans_type_combo.get_active_iter()
        currcode = self.trans_type_combo.get_model().get_value(currindex,1)
        currmodule = self.trans_type_combo.get_model().get_value(currindex,2)
        editor_generator = currmodule.\
            get_transaction_edit_interface_hook_from_code(currcode)
        self.hide_transaction()
        trans_id = self.guistate.get_transaction_id()

        self.current_editor = editor_generator(
                book.get_transaction(trans_id), trans_id, currmodule,
                self.transaction_viewport, self.guistate.record_trans_dirty_in_backend,
                book)

    def hide_transaction(self):
        """If a transaction is currently displayed (self.current_editor),
        detach it.
        """
        
        if self.current_editor != None: 
            self.current_editor.detach()

    def set_sensitivities_and_status(self):
        """Update the enabled/disabled attributes of the GUI and update the
        status.

        Specifically, this uses checks for action permissability from
        self.guistate to set the sensitivity of the toolbar buttons,
        the books and transaction type comboxes, and the plugin menu.
        These checks are done conditional on self.gui_built being set
        otherwise they're always set to disabled.

        After those we call set_backend_error_indicator and
        self.set_transid_label, see thier docstrings
        """
        
        for (sensitive_widget, action_code) in \
                ( (self.back_button, BACKWARD),
                  (self.forward_button, FORWARD),
                  (self.new_button, NEW),
                  (self.delete_button, DELETE),
                  (self.trans_type_combo, TYPE_CHANGE),
                  (self.books_combobox, BOOK_CHANGE),
                  (self.plugin1, DELETE),
                  (self.plugin1_menu, DELETE), ):
                if self.gui_built:
                    sensitive_widget.set_sensitive(
                        self.guistate.action_allowed(action_code) )
                else:
                    sensitive_widget.set_sensitive(False)

        self.set_backend_error_indicator()
        self.set_transid_label()

    def set_transid_label(self):
        """Update the field indicating the current transaction index out of the
        total number of transactions.

        Only does this if self.gui_built is set ans there's a book selected,
        otherwise just sets the label blank

        Only called from set_transid_label
        """
        
        if self.gui_built and not(self.guistate.get_book() == None):
            last_trans_id = self.guistate.get_book().get_latest_transaction_id()
            if last_trans_id != None:
                self.transid_label.set_text(
                "%s / %s" %
                (self.guistate.get_transaction_id(), last_trans_id ))
                return # avoid set_text("") below
        self.transid_label.set_text("")

    def set_backend_error_indicator(self):
        """Updates the label and related widgets for displaying backend
        plugin errors with info from the active book's backend plugin

        Doesn't do anything if self.gui_built is not set, also does
        nothing if there isn't a book and transaction selected.

        Called by set_sensitivities_and_status, on_backend_flush_request,
        and on_backend_close_request
        """
        
        # don't bother if the gui isn't built yet
        if not self.gui_built: return

        # set the backend error indicator led
        book = self.guistate.get_book()
        trans_id = self.guistate.get_transaction_id()
        if book != None and trans_id != None:
            backend = book.get_backend_plugin()
            if backend.transaction_is_clean(trans_id):
                self.backend_error_light.hide()
                self.backend_error_msg_label.hide()
                self.error_details_button.hide()
            else:
                self.backend_error_light.show()
                self.backend_error_msg_label.set_text(
                    backend.reason_transaction_is_dirty(trans_id) )
                self.backend_error_msg_label.show()
                self.error_details_button.show()

    def on_error_details_button_clicked(self, *args):
        """Event handler for the user requesting to see the full backend
        plugin error display
        """
        md = MessageDialog(parent = self.mainwindow,
                           type = MESSAGE_ERROR,
                           buttons = BUTTONS_OK,
                           message_format =
                               self.backend_error_msg_label.get_text())
        md.run()
        md.destroy()

    # Functions for use to event handlers, not used during initialization

    def set_trans_type_combo_to_current_and_reset_view(self):
        """Call after the current transaction has changed from one to
        another within the same book to update the transaction type combo
        box and to update the editor interface.

        Another key note is that this doesn't change what's listed in the
        list of transaction types like refresh_trans_types does, just
        ensures the right one is used and applies the editor interface for it.

        This is called only by the only places it makes sense right now:
        new_button_clicked, delete_button_clicked (only if a transaction
        remains), on_forward_button_clicked, on_back_button_clicked .
        Notice how all of the above involve situations where prior to them
        a book was already being viewed and the transaction type list was
        already in place, and the only thing that happened was a switch
        to different transaction and a need to just display it.

        This calls reset_trans_view as a last step to take care of
        removing the old transaction from the display and displaying the
        new one
        """
        book = self.guistate.get_book()
        trans_id = self.guistate.get_transaction_id()
        assert( trans_id != None )
        assert( isinstance(trans_id, int) )
        (i, (a, b, c)) = \
            book.get_index_and_code_class_module_tripplet_for_transaction(                trans_id )
        # What the hell, why is i None sometimes,
        # (trans id not found in any module) --
        # this is a bug that has come up
        #assert( i != None)
        # should really have kept that assertion in.. but due to bugs I
        # have cheated..
        if i == None:
            i = COMBO_SELECTION_NONE
            # if this is actually still happening, perhaps we
            # want a hide_transaction() here so we at least clear
            # off the old gui (if still there...)
        self.set_transcombo_index(i)
        if i != COMBO_SELECTION_NONE:
            self.reset_trans_view()
     
    # Event handlers

    def on_books_combobox_changed(self, combobox):
        """Change the current BoKeep book."""
        
        #don't mess with stuff until we've finished constructing the gui
        if not self.gui_built:
            return

        self.set_book_from_combo()
        self.refresh_trans_types_and_set_sensitivities_and_status()
        
    def new_button_clicked(self, *args):
        """Create a new BoKeep transaction."""
        self.guistate.do_action(NEW)
        self.set_trans_type_combo_to_current_and_reset_view()
        self.set_sensitivities_and_status()

    # to maintain compatibility with listing of event handler in
    # the glade file.
    # we don't change the glade file because those are impossible to
    # maintain and merge in when developing in a branch, so once
    # this code is merged to default the glade file can be updated
    # and this comment and backwards compatibility assignment can be
    # removed
    on_new_button_clicked = new_button_clicked

    def delete_button_clicked(self, *args):
        """Delete a BoKeep transaction."""
        if gtk_yes_no_dialog(
            """Are you sure you want to delete
transaction number %s?""" % self.guistate.get_transaction_id() ):
            self.guistate.do_action(DELETE)
            book = self.guistate.get_book()
            if self.guistate.get_transaction_id() == None:
                self.set_transcombo_index(COMBO_SELECTION_NONE)
                self.hide_transaction()
            else:
                self.set_trans_type_combo_to_current_and_reset_view()
            self.set_sensitivities_and_status()

    # see comment on on_new_button_clicked
    on_delete_button_clicked = delete_button_clicked

    def trans_type_changed(self, *args):
        """Event handler for when the transaction type on a new transaction
        changes
        """
        #only refresh the trans view if it was the user changing the 
        #transaction type
        if not self.programmatic_transcombo_index:
            assert( self.gui_built ) # and never during gui building..

            # odd, when this was called without the second argument, the
            # program rightly crashed if a NEW transaction was created,
            # followed by TYPE_CHANGE here. But, on re-launch, the memory of
            # the old type transaction seemed to remain in the state
            # stuff but was no longer available in the book. The
            # zodb transaction should of prevented this, what gives?
            self.guistate.do_action(TYPE_CHANGE,
                                    self.trans_type_combo.get_active())
            self.reset_trans_view()
            self.set_sensitivities_and_status()

    def on_forward_button_clicked(self, *args):
        """Go forward to next transaction."""
        
        self.guistate.do_action(FORWARD)
        self.set_trans_type_combo_to_current_and_reset_view()
        self.set_sensitivities_and_status()
    
    def on_back_button_clicked(self, *args):
        """Go back to previous transaction."""
        
        self.guistate.do_action(BACKWARD)
        self.set_trans_type_combo_to_current_and_reset_view()
        self.set_sensitivities_and_status()

    def on_remove(self, window, event):
        self.application_shutdown()
    
    def on_configuration1_activate(self, *args):
        """Configure BoKeep."""
        
        assert( self.gui_built )
        self.closedown_for_config()
        self.bookset.close()
        assert( not self.gui_built )

        # major flaw right now is that we don't want this to
        # re-open the same DB at the end, and we need to do something
        # the return value and seting bookset
        self.bookset = \
            establish_bokeep_db(self.mainwindow, self.__config_path, self.__config, None)

        # if there's uncommited stuff, we need to ditch it because
        # the above function killed off and re-opened the db connecion
        # we had. But, if there aren't any changes anywhere, this shuldn't
        # be a problem, right? .. but on the other hand a new transaction
        # probably begins as new one dies
        transaction.get().abort()

        if self.bookset == None:
            sys.exit()

        self.after_background_load()
        assert( self.gui_built )
        

    def on_configure_backend1_activate(self, *args):
        """Configure the backend plugin."""
        
        book = self.guistate.get_book()
        if book != None:
            backend = book.get_backend_plugin()
            backend.close()
            backend.configure_backend(self.mainwindow)
            transaction.get().commit()

    def on_configure_plugin1_activate(self, *args):
        """Configure the current front end plugin."""
        
        # the gui should never allow this event handler to be triggered
        # if there is no transaction and thus no associated plugin
        # to configure
        assert( self.guistate.get_book() != None )
        assert( self.guistate.get_transaction_id() != None )

        currindex = self.trans_type_combo.get_active_iter()
        if currindex == COMBO_SELECTION_NONE:
            return
        currmodule = self.trans_type_combo.get_model().get_value(
            currindex,2)

        currmodule.run_configuration_interface(
            self.mainwindow, self.guistate.get_book().get_backend_plugin(
                ).backend_account_dialog,
            self.guistate.get_book())
        self.reset_trans_view()
        transaction.get().commit()

    def on_about_activate(self, *args):
        """Displays the Help > About dialog."""
        
        bo_keep_logo_path = get_bo_keep_logo()
        ab = AboutDialog()
        ab.set_transient_for(self.mainwindow)
        ab.set_modal(True)
        ab.set_name("Bo-Keep")
        ab.set_version("1.2.1")
        ab.set_copyright("ParIT Worker Co-operative, Ltd. 2006-2012")
        ab.set_comments(
            """Bo-Keep helps you keep your books so you don't get lost.

Developed with grant funding from:
 - Assiniboine Credit Union <http://assiniboine.mb.ca>
 - Legal Aid Manitoba <http://www.legalaid.mb.ca>
""")
        ab.set_license(
"""Bo-Keep is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
""")
        ab.set_authors(("Mark Jenkins <*****@*****.**>",
                        "Jamie Campbell <*****@*****.**>",
                        "Samuel Pauls <*****@*****.**>",
                        "Andrew Orr <*****@*****.**>",
                        "Sara Arenson <*****@*****.**>",))
        ab.set_artists(("David Henry <*****@*****.**>",))
        ab.set_program_name("Bo-Keep")
        ab.set_logo( pixbuf_new_from_file_at_size(
                bo_keep_logo_path, 300, 266) )
        ab.run()
        ab.destroy()

    def on_backend_flush_request(self, *args):
        """Saves the BoKeep transactions."""
        
        if self.guistate.get_book() == None:
            return
        self.flush_book_backend(self.guistate.get_book())
        self.set_backend_error_indicator()

    def on_backend_close_request(self, *args):
        """Releases the backend that's used to save BoKeep transactions."""
        
        book = self.guistate.get_book()
        if book == None:
            return
        self.flush_book_backend(book)
        self.close_book_backend(book)
        self.set_backend_error_indicator()
示例#7
0
class PeersTab(Tab):
    def __init__(self):
        super(PeersTab, self).__init__('Peers', 'peers_tab', 'peers_tab_label')

        self.peer_menu = self.main_builder.get_object('menu_peer_tab')
        component.get('MainWindow').connect_signals(self)

        self.listview = self.main_builder.get_object('peers_listview')
        self.listview.props.has_tooltip = True
        self.listview.connect('button-press-event', self._on_button_press_event)
        self.listview.connect('query-tooltip', self._on_query_tooltip)

        # flag, ip, client, downspd, upspd, country code, int_ip, seed/peer icon, progress
        self.liststore = ListStore(Pixbuf, str, str, int, int, str, float, Pixbuf, float)
        self.cached_flag_pixbufs = {}

        self.seed_pixbuf = icon_seeding
        self.peer_pixbuf = icon_downloading

        # key is ip address, item is row iter
        self.peers = {}

        # Country column
        column = TreeViewColumn()
        render = CellRendererPixbuf()
        column.pack_start(render, False)
        column.add_attribute(render, 'pixbuf', 0)
        column.set_sort_column_id(5)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(20)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Address column
        column = TreeViewColumn(_('Address'))
        render = CellRendererPixbuf()
        column.pack_start(render, False)
        column.add_attribute(render, 'pixbuf', 7)
        render = CellRendererText()
        column.pack_start(render, False)
        column.add_attribute(render, 'text', 1)
        column.set_sort_column_id(6)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(100)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Client column
        column = TreeViewColumn(_('Client'))
        render = CellRendererText()
        column.pack_start(render, False)
        column.add_attribute(render, 'text', 2)
        column.set_sort_column_id(2)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(100)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Progress column
        column = TreeViewColumn(_('Progress'))
        render = CellRendererProgress()
        column.pack_start(render, True)
        column.set_cell_data_func(render, cell_data_peer_progress, 8)
        column.set_sort_column_id(8)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(100)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Down Speed column
        column = TreeViewColumn(_('Down Speed'))
        render = CellRendererText()
        column.pack_start(render, False)
        column.set_cell_data_func(render, cell_data_speed_down, 3)
        column.set_sort_column_id(3)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(50)
        column.set_reorderable(True)
        self.listview.append_column(column)

        # Up Speed column
        column = TreeViewColumn(_('Up Speed'))
        render = CellRendererText()
        column.pack_start(render, False)
        column.set_cell_data_func(render, cell_data_speed_up, 4)
        column.set_sort_column_id(4)
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(50)
        # Bugfix: Last column needs max_width set to stop scrollbar appearing
        column.set_max_width(150)
        column.set_reorderable(True)
        self.listview.append_column(column)

        self.listview.set_model(self.liststore)

        self.load_state()

        self.torrent_id = None

    def save_state(self):
        # Get the current sort order of the view
        column_id, sort_order = self.liststore.get_sort_column_id()

        # Setup state dict
        state = {
            'columns': {},
            'sort_id': column_id,
            'sort_order': int(sort_order) if sort_order else None
        }

        for index, column in enumerate(self.listview.get_columns()):
            state['columns'][column.get_title()] = {
                'position': index,
                'width': column.get_width()
            }
        save_pickled_state_file('peers_tab.state', state)

    def load_state(self):
        state = load_pickled_state_file('peers_tab.state')

        if state is None:
            return

        if len(state['columns']) != len(self.listview.get_columns()):
            log.warning('peers_tab.state is not compatible! rejecting..')
            return

        if state['sort_id'] and state['sort_order'] is not None:
            self.liststore.set_sort_column_id(state['sort_id'], state['sort_order'])

        for (index, column) in enumerate(self.listview.get_columns()):
            cname = column.get_title()
            if cname in state['columns']:
                cstate = state['columns'][cname]
                column.set_sizing(TREE_VIEW_COLUMN_FIXED)
                column.set_fixed_width(cstate['width'] if cstate['width'] > 0 else 10)
                if state['sort_id'] == index and state['sort_order'] is not None:
                    column.set_sort_indicator(True)
                    column.set_sort_order(state['sort_order'])
                if cstate['position'] != index:
                    # Column is in wrong position
                    if cstate['position'] == 0:
                        self.listview.move_column_after(column, None)
                    elif self.listview.get_columns()[cstate['position'] - 1].get_title() != cname:
                        self.listview.move_column_after(column, self.listview.get_columns()[cstate['position'] - 1])

    def update(self):
        # Get the first selected torrent
        torrent_id = component.get('TorrentView').get_selected_torrents()

        # Only use the first torrent in the list or return if None selected
        if len(torrent_id) != 0:
            torrent_id = torrent_id[0]
        else:
            # No torrent is selected in the torrentview
            self.liststore.clear()
            return

        if torrent_id != self.torrent_id:
            # We only want to do this if the torrent_id has changed
            self.liststore.clear()
            self.peers = {}
            self.torrent_id = torrent_id

        component.get('SessionProxy').get_torrent_status(torrent_id, ['peers']).addCallback(self._on_get_torrent_status)

    def get_flag_pixbuf(self, country):
        if not country.strip():
            return None

        if country not in self.cached_flag_pixbufs:
            # We haven't created a pixbuf for this country yet
            try:
                self.cached_flag_pixbufs[country] = pixbuf_new_from_file(
                    deluge.common.resource_filename(
                        'deluge',
                        os.path.join('ui', 'data', 'pixmaps', 'flags', country.lower() + '.png')))
            except Exception as ex:
                log.debug('Unable to load flag: %s', ex)
                return None

        return self.cached_flag_pixbufs[country]

    def _on_get_torrent_status(self, status):
        new_ips = set()
        for peer in status['peers']:
            new_ips.add(peer['ip'])
            if peer['ip'] in self.peers:
                # We already have this peer in our list, so lets just update it
                row = self.peers[peer['ip']]
                if not self.liststore.iter_is_valid(row):
                    # This iter is invalid, delete it and continue to next iteration
                    del self.peers[peer['ip']]
                    continue
                values = self.liststore.get(row, 3, 4, 5, 7, 8)
                if peer['down_speed'] != values[0]:
                    self.liststore.set_value(row, 3, peer['down_speed'])
                if peer['up_speed'] != values[1]:
                    self.liststore.set_value(row, 4, peer['up_speed'])
                if peer['country'] != values[2]:
                    self.liststore.set_value(row, 5, peer['country'])
                    self.liststore.set_value(row, 0, self.get_flag_pixbuf(peer['country']))
                if peer['seed']:
                    icon = self.seed_pixbuf
                else:
                    icon = self.peer_pixbuf

                if icon != values[3]:
                    self.liststore.set_value(row, 7, icon)

                if peer['progress'] != values[4]:
                    self.liststore.set_value(row, 8, peer['progress'])
            else:
                # Peer is not in list so we need to add it

                # Create an int IP address for sorting purposes
                if peer['ip'].count(':') == 1:
                    # This is an IPv4 address
                    ip_int = sum([int(byte) << shift
                                  for byte, shift in zip(peer['ip'].split(':')[0].split('.'), (24, 16, 8, 0))])
                    peer_ip = peer['ip']
                else:
                    # This is an IPv6 address
                    import socket
                    import binascii
                    # Split out the :port
                    ip = ':'.join(peer['ip'].split(':')[:-1])
                    ip_int = int(binascii.hexlify(socket.inet_pton(socket.AF_INET6, ip)), 16)
                    peer_ip = '[%s]:%s' % (ip, peer['ip'].split(':')[-1])

                if peer['seed']:
                    icon = self.seed_pixbuf
                else:
                    icon = self.peer_pixbuf

                row = self.liststore.append([
                    self.get_flag_pixbuf(peer['country']),
                    peer_ip,
                    peer['client'],
                    peer['down_speed'],
                    peer['up_speed'],
                    peer['country'],
                    float(ip_int),
                    icon,
                    peer['progress']])

                self.peers[peer['ip']] = row

        # Now we need to remove any ips that were not in status["peers"] list
        for ip in set(self.peers).difference(new_ips):
            self.liststore.remove(self.peers[ip])
            del self.peers[ip]

    def clear(self):
        self.liststore.clear()

    def _on_button_press_event(self, widget, event):
        """This is a callback for showing the right-click context menu."""
        log.debug('on_button_press_event')
        # We only care about right-clicks
        if self.torrent_id and event.button == 3:
            self.peer_menu.popup(None, None, None, event.button, event.time)
            return True

    def _on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip):
        if not widget.get_tooltip_context(x, y, keyboard_tip):
            return False
        else:
            model, path, _iter = widget.get_tooltip_context(x, y, keyboard_tip)

            country_code = model.get(_iter, 5)[0]
            if country_code != '  ' and country_code in COUNTRIES:
                tooltip.set_text(COUNTRIES[country_code])
                # widget here is self.listview
                widget.set_tooltip_cell(tooltip, path, widget.get_column(0),
                                        None)
                return True
            else:
                return False

    def on_menuitem_add_peer_activate(self, menuitem):
        """This is a callback for manually adding a peer"""
        log.debug('on_menuitem_add_peer')
        builder = Builder()
        builder.add_from_file(deluge.common.resource_filename(
            'deluge.ui.gtkui', os.path.join('glade', 'connect_peer_dialog.ui')
        ))
        peer_dialog = builder.get_object('connect_peer_dialog')
        txt_ip = builder.get_object('txt_ip')
        response = peer_dialog.run()
        if response:
            value = txt_ip.get_text()
            if value and ':' in value:
                if ']' in value:
                    # ipv6
                    ip = value.split(']')[0][1:]
                    port = value.split(']')[1][1:]
                else:
                    # ipv4
                    ip = value.split(':')[0]
                    port = value.split(':')[1]
                if deluge.common.is_ip(ip):
                    log.debug('adding peer %s to %s', value, self.torrent_id)
                    client.core.connect_peer(self.torrent_id, ip, port)
        peer_dialog.destroy()
        return True