def _create_menu(self):
        def on_show_menu(widget):

            parent_id = labelplus.common.label.get_parent_id(self._label_id)
            if parent_id in self._store:
                items[0].show()
                items[1].show()
            else:
                items[0].hide()
                items[1].hide()

        def on_activate(widget, label_id):

            self._request_options(label_id)

        def on_activate_parent(widget):

            parent_id = labelplus.common.label.get_parent_id(self._label_id)
            self._request_options(parent_id)

        self._menu = LabelSelectionMenu(self._store.model, on_activate)
        if __debug__: RT.register(self._menu, __name__)

        items = labelplus.gtkui.common.gtklib.menu_add_items(
            self._menu, 0, (
                ((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),
                ((gtk.SeparatorMenuItem, ), ),
            ))

        if __debug__:
            for item in items:
                RT.register(item, __name__)

        self._menu.connect("show", on_show_menu)
        self._menu.show_all()
    def _create_menu(self):
        def on_activate(widget, label_id):

            id = self._get_selected_torrent()
            if id:
                self._set_torrent_label(id, label_id)
                self._display_torrent_label(id)

        items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NONE), )

        self._menu = LabelSelectionMenu(self._store.model,
                                        on_activate,
                                        root_items=items)

        if __debug__: RT.register(self._menu, __name__)
    def _create_set_label_menu(self):
        def on_activate(widget, label_id):

            torrent_ids = self._view.get_selected_torrents()
            if torrent_ids and label_id in self._store:
                log.info("Setting label %r on %r",
                         self._store[label_id]["fullname"], torrent_ids)
                client.labelplus.set_torrent_labels(torrent_ids, label_id)

        def on_activate_parent(widget):

            ids = self.get_selected_torrent_labels()
            parent_id = labelplus.common.label.get_common_parent(ids)
            on_activate(widget, parent_id)

        def on_show_menu(widget):

            items[0].hide()

            ids = self.get_selected_torrent_labels()
            parent_id = labelplus.common.label.get_common_parent(ids)
            if self._store.is_user_label(parent_id):
                items[0].show()

        root_items = (((Gtk.MenuItem, {
            'label': _(STR_NONE)
        }), on_activate, ID_NONE), )

        menu = LabelSelectionMenu(self._store.model,
                                  on_activate,
                                  root_items=root_items)
        menu.connect("show", on_show_menu)

        items = labelplus.gtkui.common.gtklib.menu_add_items(
            menu, 1, (((Gtk.MenuItem, {
                'label': _(STR_PARENT)
            }), on_activate_parent), ))

        root = Gtk.MenuItem(label=_(TITLE_SET_LABEL))
        root.set_submenu(menu)

        if __debug__: RT.register(menu, __name__)
        if __debug__: RT.register(root, __name__)

        return root
  def _create_set_label_menu(self):

    def on_activate(widget, label_id):

      torrent_ids = self._view.get_selected_torrents()
      if torrent_ids and label_id in self._store:
        log.info("Setting label %r on %r", self._store[label_id]["fullname"],
          torrent_ids)
        client.labelplus.set_torrent_labels(torrent_ids, label_id)


    def on_activate_parent(widget):

      ids = self.get_selected_torrent_labels()
      parent_id = labelplus.common.label.get_common_parent(ids)
      on_activate(widget, parent_id)


    def on_show_menu(widget):

      items[0].hide()

      ids = self.get_selected_torrent_labels()
      parent_id = labelplus.common.label.get_common_parent(ids)
      if self._store.is_user_label(parent_id):
        items[0].show()


    root_items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NONE),)

    menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=root_items)
    menu.connect("show", on_show_menu)

    items = labelplus.gtkui.common.gtklib.menu_add_items(menu, 1,
      (((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),))

    root = gtk.MenuItem(_(TITLE_SET_LABEL))
    root.set_submenu(menu)

    if __debug__: RT.register(menu, __name__)
    if __debug__: RT.register(root, __name__)

    return root
  def _create_menu(self):

    def on_show_menu(menu):

      parent_id = labelplus.common.label.get_parent_id(self._parent_id)
      if parent_id in self._store:
        items[0].show()
      else:
        items[0].hide()


    def on_activate(widget, parent_id):

      self._select_parent_label(parent_id)


    def on_activate_parent(widget):

      parent_id = labelplus.common.label.get_parent_id(self._parent_id)
      self._select_parent_label(parent_id)


    root_items = (((Gtk.MenuItem, {'label': _(STR_NONE)}), on_activate, ID_NULL),)

    self._menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=root_items)
    if __debug__: RT.register(self._menu, __name__)

    items = labelplus.gtkui.common.gtklib.menu_add_items(self._menu, 1,
            (((Gtk.MenuItem, {'label': _(STR_PARENT)}), on_activate_parent),))
    if __debug__: RT.register(items[0], __name__)

    self._menu.connect("show", on_show_menu)
    self._menu.show_all()

    if self._type == self.TYPE_RENAME:
      item = self._menu.get_label_item(self._label_id)
      if item:
        item.set_sensitive(False)
  def _create_menu(self):

    def on_activate(widget, label_id):

      id = self._get_selected_torrent()
      if id:
        self._set_torrent_label(id, label_id)
        self._display_torrent_label(id)


    items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NONE),)

    self._menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=items)

    if __debug__: RT.register(self._menu, __name__)
  def _create_menu(self):

    def on_show_menu(widget):

      parent_id = labelplus.common.label.get_parent_id(self._label_id)
      if parent_id in self._store:
        items[0].show()
        items[1].show()
      else:
        items[0].hide()
        items[1].hide()


    def on_activate(widget, label_id):

      self._request_options(label_id)


    def on_activate_parent(widget):

      parent_id = labelplus.common.label.get_parent_id(self._label_id)
      self._request_options(parent_id)


    self._menu = LabelSelectionMenu(self._store.model, on_activate)
    if __debug__: RT.register(self._menu, __name__)

    items = labelplus.gtkui.common.gtklib.menu_add_items(self._menu, 0,
      (
        ((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),
        ((gtk.SeparatorMenuItem,),),
      )
    )

    if __debug__:
      for item in items:
        RT.register(item, __name__)

    self._menu.connect("show", on_show_menu)
    self._menu.show_all()
  def _create_menu(self):

    def on_show_menu(menu):

      parent_id = labelplus.common.label.get_parent_id(self._parent_id)
      if parent_id in self._store:
        items[0].show()
      else:
        items[0].hide()


    def on_activate(widget, parent_id):

      self._select_parent_label(parent_id)


    def on_activate_parent(widget):

      parent_id = labelplus.common.label.get_parent_id(self._parent_id)
      self._select_parent_label(parent_id)


    root_items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NULL),)

    self._menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=root_items)
    if __debug__: RT.register(self._menu, __name__)

    items = labelplus.gtkui.common.gtklib.menu_add_items(self._menu, 1,
      (((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),))
    if __debug__: RT.register(items[0], __name__)

    self._menu.connect("show", on_show_menu)
    self._menu.show_all()

    if self._type == self.TYPE_RENAME:
      item = self._menu.get_label_item(self._label_id)
      if item:
        item.set_sensitive(False)
class LabelOptionsDialog(WidgetEncapsulator):

    # Section: Constants

    GLADE_FILE = labelplus.common.get_resource("wnd_label_options.glade")
    ROOT_WIDGET = "wnd_label_options"

    REQUEST_TIMEOUT = 10.0
    CLEAR_TEST_DELAY = 2.0

    OP_MAP = {
        gtk.CheckButton: ("set_active", "get_active"),
        gtk.Label: ("set_text", "get_text"),
        gtk.RadioButton: ("set_active", "get_active"),
        gtk.SpinButton: ("set_value", "get_value"),
        AutolabelBox: ("set_all_row_values", "get_all_row_values"),
        RadioButtonGroup: ("set_active_value", "get_active_value"),
    }

    SETTER = 0
    GETTER = 1

    # Section: Initialization

    def __init__(self, plugin, label_id, page=0):

        self._plugin = plugin

        if label_id in RESERVED_IDS or label_id not in plugin.store:
            raise LabelPlusError(ERR_INVALID_LABEL)

        self._store = None
        self._menu = None

        self._label_defaults = {}
        self._path_options = {}
        self._label_options = {}

        for path_type in PATH_TYPES:
            self._path_options[path_type] = {}

        super(LabelOptionsDialog, self).__init__(self.GLADE_FILE,
                                                 self.ROOT_WIDGET, "_")

        try:
            self._store = plugin.store.copy()
            self._set_label(ID_NULL)

            # Keep window alive with cyclic reference
            self._root_widget.set_data("owner", self)

            self._setup_widgets()
            self._index_widgets()

            self._load_state()
            self._nb_tabs.set_current_page(page)

            self._create_menu()

            self._request_options(label_id)

            self._plugin.register_update_func(self.update_store)
            self._plugin.register_cleanup_func(self.destroy)
        except:
            self.destroy()
            raise

    # Section: Deinitialization

    def destroy(self):

        self._plugin.deregister_update_func(self.update_store)
        self._plugin.deregister_cleanup_func(self.destroy)

        self._destroy_menu()
        self._destroy_store()

        if self.valid:
            self._root_widget.set_data("owner", None)
            super(LabelOptionsDialog, self).destroy()

    def _destroy_store(self):

        if self._store:
            self._store.destroy()
            self._store = None

    # Section: Public

    def show(self):

        self._wnd_label_options.show()

    def update_store(self, store):

        if self._label_id not in store:
            self.destroy()
            return

        self._destroy_store()
        self._store = store.copy()

        self._destroy_menu()
        self._create_menu()

        self._request_options(self._label_id)

    # Section: General

    def _set_label(self, label_id):

        if label_id in self._store:
            self._label_id = label_id
            self._label_name = self._store[label_id]["name"]
            self._label_fullname = self._store[label_id]["fullname"]
        else:
            self._label_id = ID_NULL
            self._label_name = _(STR_NONE)
            self._label_fullname = _(STR_NONE)

    def _report_error(self, context, error):

        log.error("%s: %s", context, error)
        self._set_error(error.tr())

    def _get_path_type(self, widget):

        path_type = widget.get_name()[widget.get_name().index("_") +
                                      1:widget.get_name().rindex("_")]

        if path_type in PATH_TYPES:
            return path_type

        return None

    # Section: Options

    def _request_options(self, label_id):
        def on_timeout():

            if self.valid:
                self._wnd_label_options.set_sensitive(True)
                self._report_error(STR_LOAD_OPTIONS,
                                   LabelPlusError(ERR_TIMED_OUT))

        def process_result(result):

            if self.valid:
                self._wnd_label_options.set_sensitive(True)

                for success, data in result:
                    if not success:
                        error = labelplus.common.extract_error(data)
                        if error:
                            self._report_error(STR_LOAD_OPTIONS, error)
                        else:
                            self.destroy()
                        return

                self._label_defaults = result[0][1]
                self._path_options = result[1][1]
                self._label_options = result[2][1]

                self._load_options(self._label_options)
                self._enable_options()

        self._clear_options()
        self._disable_options()
        self._set_label(label_id)
        self._refresh()

        if not label_id in self._store:
            self._report_error(STR_LOAD_OPTIONS,
                               LabelPlusError(ERR_INVALID_LABEL))
            return

        self._set_error(None)

        log.info("Loading options for %r", self._label_fullname)

        deferreds = []
        deferreds.append(client.labelplus.get_label_defaults())
        deferreds.append(client.labelplus.get_path_options(label_id))
        deferreds.append(client.labelplus.get_label_options(label_id))

        deferred = twisted.internet.defer.DeferredList(deferreds,
                                                       consumeErrors=True)

        self._wnd_label_options.set_sensitive(False)

        labelplus.common.deferred_timeout(deferred, self.REQUEST_TIMEOUT,
                                          on_timeout, process_result, None)

    def _save_options(self):
        def on_timeout(options):

            if self.valid:
                self._wnd_label_options.set_sensitive(True)
                self._report_error(STR_SAVE_OPTIONS,
                                   LabelPlusError(ERR_TIMED_OUT))

        def process_result(result, options):

            if self.valid:
                self._wnd_label_options.set_sensitive(True)

                if isinstance(result, Failure):
                    error = labelplus.common.extract_error(result)
                    if error:
                        self._report_error(STR_SAVE_OPTIONS, error)
                    else:
                        self.destroy()
                        return result
                else:
                    self._label_options = options

        if not self._label_id in self._store:
            self._report_error(STR_SAVE_OPTIONS,
                               LabelPlusError(ERR_INVALID_LABEL))
            return

        self._set_error(None)

        log.info("Saving options for %r", self._label_fullname)

        options = self._get_options()
        same = labelplus.common.dict_equals(options, self._label_options)

        if self._chk_autolabel_retroactive.get_active():
            apply_to_all = not self._chk_autolabel_unlabeled_only.get_active()
        else:
            apply_to_all = None

        if not same or apply_to_all is not None:
            deferred = client.labelplus.set_label_options(
                self._label_id, options, apply_to_all)

            self._wnd_label_options.set_sensitive(False)

            labelplus.common.deferred_timeout(deferred, self.REQUEST_TIMEOUT,
                                              on_timeout, process_result,
                                              process_result, options)
        else:
            log.info("No options were changed")

    # Section: Dialog: Setup

    def _setup_widgets(self):

        self._wnd_label_options.set_transient_for(
            deluge.component.get("MainWindow").window)

        self._wnd_label_options.set_title(_(TITLE_LABEL_OPTIONS))
        icon = self._wnd_label_options.render_icon(gtk.STOCK_PREFERENCES,
                                                   gtk.ICON_SIZE_SMALL_TOOLBAR)
        self._wnd_label_options.set_icon(icon)

        self._lbl_header.set_markup("<b>%s:</b>" % _(STR_LABEL))

        self._img_error.set_from_stock(gtk.STOCK_DIALOG_ERROR,
                                       gtk.ICON_SIZE_SMALL_TOOLBAR)

        self._btn_close.grab_focus()

        self._setup_radio_button_groups()
        self._setup_autolabel_box()
        self._setup_test_combo_box()
        self._setup_criteria_area()

        for path_type in PATH_TYPES:
            self.__dict__["_rgrp_%s_mode" % path_type].connect(
                "changed", self._do_select_mode)

        self.connect_signals({
            "do_close": self._do_close,
            "do_submit": self._do_submit,
            "do_open_select_menu": self._do_open_select_menu,
            "do_revert_to_defaults": self._do_revert_to_defaults,
            "do_toggle_fullname": self._do_toggle_fullname,
            "do_toggle_dependents": self._do_toggle_dependents,
            "on_txt_changed": self._on_txt_changed,
            "do_open_file_dialog": self._do_open_file_dialog,
            "do_test_criteria": self._do_test_criteria,
        })

    def _setup_radio_button_groups(self):

        for path_type in PATH_TYPES:
            rgrp = RadioButtonGroup((
                (getattr(self, "_rb_%s_to_parent" % path_type), MOVE_PARENT),
                (getattr(self,
                         "_rb_%s_to_subfolder" % path_type), MOVE_SUBFOLDER),
                (getattr(self, "_rb_%s_to_folder" % path_type), MOVE_FOLDER),
            ))

            rgrp.set_name("rgrp_%s_mode" % path_type)
            self.__dict__["_rgrp_%s_mode" % path_type] = rgrp

            if __debug__: RT.register(rgrp, __name__)

    def _setup_autolabel_box(self):

        crbox = AutolabelBox(row_spacing=6, column_spacing=3)
        if __debug__: RT.register(crbox, __name__)

        crbox.set_name("crbox_autolabel_rules")
        self._crbox_autolabel_rules = crbox
        self._blk_criteria_box.add(crbox)
        self._widgets.append(crbox)

        crbox.show_all()

    def _setup_test_combo_box(self):

        prop_store = labelplus.gtkui.common.gtklib.liststore_create(
            str, [_(x) for x in PROPS])
        if __debug__: RT.register(prop_store, __name__)

        cell = gtk.CellRendererText()
        if __debug__: RT.register(cell, __name__)

        self._cmb_test_criteria.pack_start(cell)
        self._cmb_test_criteria.add_attribute(cell, "text", 0)
        self._cmb_test_criteria.set_model(prop_store)
        self._cmb_test_criteria.set_active(0)

    def _setup_criteria_area(self):
        def on_mapped(widget, event):

            # Sometimes a widget is mapped but does not immediately have allocation
            if self._vp_criteria_area.allocation.x < 0:
                twisted.internet.reactor.callLater(0.1, widget.emit,
                                                   "map-event", event)
                return

            if widget.handler_is_connected(handle):
                widget.disconnect(handle)

            self._vp_criteria_area.set_data("was_mapped", True)

            if self._plugin.config["common"]["label_options_pane_pos"] > -1:
                self._vp_criteria_area.set_position(
                    self._vp_criteria_area.allocation.height -
                    self._plugin.config["common"]["label_options_pane_pos"])

                clamp_position(self._vp_criteria_area)

        def clamp_position(widget, *args):

            handle_size = widget.allocation.height - \
              widget.get_property("max-position")
            max_dist = self._hb_test_criteria.allocation.height + handle_size * 2
            threshold = max_dist / 2

            if widget.allocation.height - widget.get_position() > threshold:
                twisted.internet.reactor.callLater(
                    0.1, widget.set_position,
                    widget.allocation.height - max_dist)
            else:
                twisted.internet.reactor.callLater(0.1, widget.set_position,
                                                   widget.allocation.height)

        handle = self._eb_criteria_area.connect("map-event", on_mapped)

        self._vp_criteria_area.connect("button-release-event", clamp_position)
        self._vp_criteria_area.connect("accept-position", clamp_position)

    def _index_widgets(self):

        self._exp_group = (
            self._exp_download_location,
            self._exp_move_completed,
        )

        self._option_groups = (
            (
                self._chk_download_settings,
                self._chk_download_location,
                self._rgrp_download_location_mode,
                self._lbl_download_location_path,
                self._chk_move_completed,
                self._rgrp_move_completed_mode,
                self._lbl_move_completed_path,
                self._chk_prioritize_first_last,
            ),
            (
                self._chk_bandwidth_settings,
                self._rb_shared_limit,
                self._spn_max_download_speed,
                self._spn_max_upload_speed,
                self._spn_max_connections,
                self._spn_max_upload_slots,
            ),
            (
                self._chk_queue_settings,
                self._chk_auto_managed,
                self._chk_stop_at_ratio,
                self._spn_stop_ratio,
                self._chk_remove_at_ratio,
            ),
            (
                self._chk_autolabel_settings,
                self._rb_autolabel_match_all,
                self._crbox_autolabel_rules,
            ),
        )

        self._dependency_widgets = {
            self._chk_download_settings: (self._blk_download_settings_group, ),
            self._chk_bandwidth_settings:
            (self._blk_bandwidth_settings_group, ),
            self._chk_queue_settings: (self._blk_queue_settings_group, ),
            self._chk_autolabel_settings:
            (self._blk_autolabel_settings_group, ),
            self._chk_download_location: (self._blk_download_location_group, ),
            self._rb_download_location_to_folder:
            (self._txt_download_location_path, ),
            self._chk_move_completed: (self._blk_move_completed_group, ),
            self._rb_move_completed_to_folder:
            (self._txt_move_completed_path, ),
            self._chk_stop_at_ratio: (self._spn_stop_ratio,
                                      self._chk_remove_at_ratio),
            self._chk_autolabel_retroactive:
            (self._chk_autolabel_unlabeled_only, ),
        }

    # Section: Dialog: State

    def _load_state(self):

        if not client.is_localhost():
            for path_type in PATH_TYPES:
                self.__dict__["_btn_%s_browse" % path_type].hide()

        if self._plugin.initialized:
            pos = self._plugin.config["common"]["label_options_pos"]
            if pos:
                self._wnd_label_options.move(*pos)

            size = self._plugin.config["common"]["label_options_size"]
            if size:
                self._wnd_label_options.resize(*size)

            self._tgb_fullname.set_active(
                self._plugin.config["common"]["label_options_fullname"])

            expanded = self._plugin.config["common"]["label_options_exp_state"]
            for exp in expanded:
                widget = getattr(self, "_" + exp, None)
                if widget:
                    widget.set_expanded(True)

    def _save_state(self):

        if self._plugin.initialized:
            self._plugin.config["common"]["label_options_pos"] = \
              list(self._wnd_label_options.get_position())

            self._plugin.config["common"]["label_options_size"] = \
              list(self._wnd_label_options.get_size())

            self._plugin.config["common"]["label_options_fullname"] = \
              self._tgb_fullname.get_active()

            expanded = []
            for exp in self._exp_group:
                if exp.get_expanded():
                    expanded.append(exp.get_name())

            self._plugin.config["common"]["label_options_exp_state"] = expanded

            if self._vp_criteria_area.get_data("was_mapped"):
                self._plugin.config["common"]["label_options_pane_pos"] = \
                  self._vp_criteria_area.allocation.height - \
                  self._vp_criteria_area.get_position()

            self._plugin.config.save()

    # Section: Dialog: Options

    def _get_widget_values(self, widgets, options_out):

        for widget in widgets:
            prefix, sep, name = widget.get_name().partition("_")
            if sep and name in options_out:
                ops = self.OP_MAP.get(type(widget))
                if ops:
                    getter = getattr(widget, ops[self.GETTER])
                    options_out[name] = getter()

    def _set_widget_values(self, widgets, options_in):

        for widget in widgets:
            prefix, sep, name = widget.get_name().partition("_")
            if sep and name in options_in:
                ops = self.OP_MAP.get(type(widget))
                if ops:
                    setter = getattr(widget, ops[self.SETTER])
                    setter(options_in[name])

    def _load_options(self, options):

        for group in self._option_groups:
            self._set_widget_values(group, options)

        for path_type in PATH_TYPES:
            mode = options["%s_mode" % path_type]
            path = options["%s_path" % path_type]

            self.__dict__["_txt_%s_path" % path_type].set_text(path)

            if mode != MOVE_FOLDER:
                path = self._path_options[path_type].get(mode, "")

            self._set_path_label(path, path_type)

    def _get_options(self):

        options = copy.deepcopy(self._label_defaults)

        for group in self._option_groups:
            self._get_widget_values(group, options)

        return options

    def _revert_options_by_page(self, options_out, page):

        widgets = self._option_groups[page]

        for widget in widgets:
            prefix, sep, name = widget.get_name().partition("_")
            if sep and name in self._label_defaults:
                options_out[name] = copy.deepcopy(self._label_defaults[name])

    def _clear_options(self):

        if not self._label_defaults:
            self._label_defaults = LABEL_DEFAULTS

        for path_type in PATH_TYPES:
            self._path_options[path_type] = {}

        self._label_options = {}

        self._load_options(self._label_defaults)

    def _enable_options(self):

        self._nb_tabs.set_sensitive(True)
        self._btn_defaults.set_sensitive(True)
        self._btn_defaults_all.set_sensitive(True)
        self._btn_apply.set_sensitive(True)

    def _disable_options(self):

        self._nb_tabs.set_sensitive(False)
        self._btn_defaults.set_sensitive(False)
        self._btn_defaults_all.set_sensitive(False)
        self._btn_apply.set_sensitive(False)

    # Section: Dialog: Modifiers

    def _refresh(self):

        self._do_toggle_fullname()

        if self._label_id == ID_NULL:
            self._lbl_selected_label.set_tooltip_text(None)
        else:
            self._lbl_selected_label.set_tooltip_text(self._label_fullname)

        for widget in self._dependency_widgets:
            self._do_toggle_dependents(widget)

        self._set_test_result(None)

    def _set_path_label(self, path, path_type):

        lbl_widget = self.__dict__["_lbl_%s_path" % path_type]
        lbl_widget.set_text(path)
        lbl_widget.set_tooltip_text(path)

    def _set_test_result(self, result):

        if result is not None:
            icon = gtk.STOCK_YES if result else gtk.STOCK_NO
            self._txt_test_criteria.set_icon_from_stock(
                gtk.ENTRY_ICON_SECONDARY, icon)
        else:
            self._txt_test_criteria.set_icon_from_stock(
                gtk.ENTRY_ICON_SECONDARY, None)

    def _set_error(self, message):

        if message:
            self._img_error.set_tooltip_text(message)
            self._img_error.show()
        else:
            self._img_error.hide()

    # Section: Dialog: Handlers: General

    def _do_close(self, *args):

        self._save_state()
        self.destroy()

    def _do_submit(self, *args):

        self._save_options()

    def _do_open_select_menu(self, *args):

        if self._menu:
            self._menu.popup(None, None, None, 1, gtk.gdk.CURRENT_TIME)

    def _do_revert_to_defaults(self, widget):

        if widget is self._btn_defaults:
            options = self._get_options()
            self._revert_options_by_page(options,
                                         self._nb_tabs.get_current_page())
        else:
            options = self._label_defaults

        self._load_options(options)
        self._refresh()

    def _do_toggle_fullname(self, *args):

        if self._tgb_fullname.get_active():
            self._lbl_selected_label.set_text(self._label_fullname)
        else:
            self._lbl_selected_label.set_text(self._label_name)

    def _do_toggle_dependents(self, widget):

        if widget in self._dependency_widgets:
            toggled = widget.get_active()

            for dependent in self._dependency_widgets[widget]:
                dependent.set_sensitive(toggled)

    # Section: Dialog: Handlers: Paths

    def _do_select_mode(self, group, button, mode):

        path_type = self._get_path_type(group)
        self._do_toggle_dependents(self.__dict__["_rb_%s_to_folder" %
                                                 path_type])

        if mode == MOVE_FOLDER:
            self._on_txt_changed(self.__dict__["_txt_%s_path" % path_type])
        else:
            path = self._path_options[path_type].get(mode, "")
            self._set_path_label(path, path_type)

    def _on_txt_changed(self, widget):

        path_type = self._get_path_type(widget)
        self._set_path_label(widget.get_text(), path_type)

    def _do_open_file_dialog(self, widget):
        def on_response(widget, response):

            if self.valid and response == gtk.RESPONSE_OK:
                txt_widget.set_text(widget.get_filename())

            widget.destroy()

        path_type = self._get_path_type(widget)
        txt_widget = self.__dict__["_txt_%s_path" % path_type]

        dialog = gtk.FileChooserDialog(_(TITLE_SELECT_FOLDER),
                                       self._wnd_label_options,
                                       gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, (
                                           gtk.STOCK_CANCEL,
                                           gtk.RESPONSE_CANCEL,
                                           gtk.STOCK_OK,
                                           gtk.RESPONSE_OK,
                                       ))
        if __debug__: RT.register(dialog, __name__)

        dialog.set_destroy_with_parent(True)
        dialog.connect("response", on_response)

        path = txt_widget.get_text()
        if not os.path.exists(path):
            path = ""

        dialog.set_filename(path)
        dialog.show_all()

        widgets = labelplus.gtkui.common.gtklib.widget_get_descendents(
            dialog, (gtk.ToggleButton, ), 1)
        if widgets:
            location_toggle = widgets[0]
            location_toggle.set_active(False)

    # Section: Dialog: Handlers: Criteria Test

    def _do_test_criteria(self, *args):
        def clear_result():

            if self.valid:
                self._set_test_result(None)

        index = self._cmb_test_criteria.get_active()
        prop_name = PROPS[index]

        props = {
            prop_name: [unicode(self._txt_test_criteria.get_text(), "utf8")]
        }

        rules = self._crbox_autolabel_rules.get_all_row_values()
        match_all = self._rb_autolabel_match_all.get_active()

        log.debug("Properties: %r, Rules: %r, Match all: %s", props, rules,
                  match_all)

        result = labelplus.common.config.autolabel.find_match(
            props, rules, match_all)

        log.debug("Test result: %s", result)
        self._set_test_result(result)

        twisted.internet.reactor.callLater(self.CLEAR_TEST_DELAY, clear_result)

    # Section: Dialog: Menu

    def _create_menu(self):
        def on_show_menu(widget):

            parent_id = labelplus.common.label.get_parent_id(self._label_id)
            if parent_id in self._store:
                items[0].show()
                items[1].show()
            else:
                items[0].hide()
                items[1].hide()

        def on_activate(widget, label_id):

            self._request_options(label_id)

        def on_activate_parent(widget):

            parent_id = labelplus.common.label.get_parent_id(self._label_id)
            self._request_options(parent_id)

        self._menu = LabelSelectionMenu(self._store.model, on_activate)
        if __debug__: RT.register(self._menu, __name__)

        items = labelplus.gtkui.common.gtklib.menu_add_items(
            self._menu, 0, (
                ((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),
                ((gtk.SeparatorMenuItem, ), ),
            ))

        if __debug__:
            for item in items:
                RT.register(item, __name__)

        self._menu.connect("show", on_show_menu)
        self._menu.show_all()

    def _destroy_menu(self):

        if self._menu:
            self._menu.destroy()
            self._menu = None
class LabelOptionsDialog(WidgetEncapsulator):

  # Section: Constants

  GLADE_FILE = labelplus.common.get_resource("wnd_label_options.glade")
  ROOT_WIDGET = "wnd_label_options"

  REQUEST_TIMEOUT = 10.0
  CLEAR_TEST_DELAY = 2.0

  OP_MAP = {
    gtk.CheckButton: ("set_active", "get_active"),
    gtk.Label: ("set_text", "get_text"),
    gtk.RadioButton: ("set_active", "get_active"),
    gtk.SpinButton: ("set_value", "get_value"),
    AutolabelBox: ("set_all_row_values", "get_all_row_values"),
    RadioButtonGroup: ("set_active_value", "get_active_value"),
  }

  SETTER = 0
  GETTER = 1


  # Section: Initialization

  def __init__(self, plugin, label_id, page=0):

    self._plugin = plugin

    if label_id in RESERVED_IDS or label_id not in plugin.store:
      raise LabelPlusError(ERR_INVALID_LABEL)

    self._store = None
    self._menu = None

    self._label_defaults = {}
    self._path_options = {}
    self._label_options = {}

    for path_type in PATH_TYPES:
      self._path_options[path_type] = {}

    super(LabelOptionsDialog, self).__init__(self.GLADE_FILE, self.ROOT_WIDGET,
      "_")

    try:
      self._store = plugin.store.copy()
      self._set_label(ID_NULL)

      # Keep window alive with cyclic reference
      self._root_widget.set_data("owner", self)

      self._setup_widgets()
      self._index_widgets()

      self._load_state()
      self._nb_tabs.set_current_page(page)

      self._create_menu()

      self._request_options(label_id)

      self._plugin.register_update_func(self.update_store)
      self._plugin.register_cleanup_func(self.destroy)
    except:
      self.destroy()
      raise


  # Section: Deinitialization

  def destroy(self):

    self._plugin.deregister_update_func(self.update_store)
    self._plugin.deregister_cleanup_func(self.destroy)

    self._destroy_menu()
    self._destroy_store()

    if self.valid:
      self._root_widget.set_data("owner", None)
      super(LabelOptionsDialog, self).destroy()


  def _destroy_store(self):

    if self._store:
      self._store.destroy()
      self._store = None


  # Section: Public

  def show(self):

    self._wnd_label_options.show()


  def update_store(self, store):

    if self._label_id not in store:
      self.destroy()
      return

    self._destroy_store()
    self._store = store.copy()

    self._destroy_menu()
    self._create_menu()

    self._request_options(self._label_id)


  # Section: General

  def _set_label(self, label_id):

    if label_id in self._store:
      self._label_id = label_id
      self._label_name = self._store[label_id]["name"]
      self._label_fullname = self._store[label_id]["fullname"]
    else:
      self._label_id = ID_NULL
      self._label_name = _(STR_NONE)
      self._label_fullname = _(STR_NONE)


  def _report_error(self, context, error):

    log.error("%s: %s", context, error)
    self._set_error(error.tr())


  def _get_path_type(self, widget):

    path_type = widget.get_name()[
      widget.get_name().index("_")+1:widget.get_name().rindex("_")]

    if path_type in PATH_TYPES:
      return path_type

    return None


  # Section: Options

  def _request_options(self, label_id):

    def on_timeout():

      if self.valid:
        self._wnd_label_options.set_sensitive(True)
        self._report_error(STR_LOAD_OPTIONS, LabelPlusError(ERR_TIMED_OUT))


    def process_result(result):

      if self.valid:
        self._wnd_label_options.set_sensitive(True)

        for success, data in result:
          if not success:
            error = labelplus.common.extract_error(data)
            if error:
              self._report_error(STR_LOAD_OPTIONS, error)
            else:
              self.destroy()
            return

        self._label_defaults = result[0][1]
        self._path_options = result[1][1]
        self._label_options = result[2][1]

        self._load_options(self._label_options)
        self._enable_options()


    self._clear_options()
    self._disable_options()
    self._set_label(label_id)
    self._refresh()

    if not label_id in self._store:
      self._report_error(STR_LOAD_OPTIONS, LabelPlusError(ERR_INVALID_LABEL))
      return

    self._set_error(None)

    log.info("Loading options for %r", self._label_fullname)

    deferreds = []
    deferreds.append(client.labelplus.get_label_defaults())
    deferreds.append(client.labelplus.get_path_options(label_id))
    deferreds.append(client.labelplus.get_label_options(label_id))

    deferred = twisted.internet.defer.DeferredList(deferreds,
      consumeErrors=True)

    self._wnd_label_options.set_sensitive(False)

    labelplus.common.deferred_timeout(deferred, self.REQUEST_TIMEOUT,
      on_timeout, process_result, None)


  def _save_options(self):

    def on_timeout(options):

      if self.valid:
        self._wnd_label_options.set_sensitive(True)
        self._report_error(STR_SAVE_OPTIONS, LabelPlusError(ERR_TIMED_OUT))


    def process_result(result, options):

      if self.valid:
        self._wnd_label_options.set_sensitive(True)

        if isinstance(result, Failure):
          error = labelplus.common.extract_error(result)
          if error:
            self._report_error(STR_SAVE_OPTIONS, error)
          else:
            self.destroy()
            return result
        else:
          self._label_options = options


    if not self._label_id in self._store:
      self._report_error(STR_SAVE_OPTIONS, LabelPlusError(ERR_INVALID_LABEL))
      return

    self._set_error(None)

    log.info("Saving options for %r", self._label_fullname)

    options = self._get_options()
    same = labelplus.common.dict_equals(options, self._label_options)

    if self._chk_autolabel_retroactive.get_active():
      apply_to_all = not self._chk_autolabel_unlabeled_only.get_active()
    else:
      apply_to_all = None

    if not same or apply_to_all is not None:
      deferred = client.labelplus.set_label_options(self._label_id, options,
        apply_to_all)

      self._wnd_label_options.set_sensitive(False)

      labelplus.common.deferred_timeout(deferred, self.REQUEST_TIMEOUT,
        on_timeout, process_result, process_result, options)
    else:
      log.info("No options were changed")


  # Section: Dialog: Setup

  def _setup_widgets(self):

    self._wnd_label_options.set_transient_for(
      deluge.component.get("MainWindow").window)

    self._wnd_label_options.set_title(_(TITLE_LABEL_OPTIONS))
    icon = self._wnd_label_options.render_icon(gtk.STOCK_PREFERENCES,
      gtk.ICON_SIZE_SMALL_TOOLBAR)
    self._wnd_label_options.set_icon(icon)

    self._lbl_header.set_markup("<b>%s:</b>" % _(STR_LABEL))

    self._img_error.set_from_stock(gtk.STOCK_DIALOG_ERROR,
      gtk.ICON_SIZE_SMALL_TOOLBAR)

    self._btn_close.grab_focus()

    self._setup_radio_button_groups()
    self._setup_autolabel_box()
    self._setup_test_combo_box()
    self._setup_criteria_area()

    for path_type in PATH_TYPES:
      self.__dict__["_rgrp_%s_mode" % path_type].connect(
        "changed", self._do_select_mode)

    self.connect_signals({
      "do_close": self._do_close,
      "do_submit": self._do_submit,
      "do_open_select_menu": self._do_open_select_menu,
      "do_revert_to_defaults": self._do_revert_to_defaults,
      "do_toggle_fullname": self._do_toggle_fullname,
      "do_toggle_dependents": self._do_toggle_dependents,
      "on_txt_changed": self._on_txt_changed,
      "do_open_file_dialog": self._do_open_file_dialog,
      "do_test_criteria": self._do_test_criteria,
    })


  def _setup_radio_button_groups(self):

    for path_type in PATH_TYPES:
      rgrp = RadioButtonGroup((
        (getattr(self, "_rb_%s_to_parent" % path_type), MOVE_PARENT),
        (getattr(self, "_rb_%s_to_subfolder" % path_type), MOVE_SUBFOLDER),
        (getattr(self, "_rb_%s_to_folder" % path_type), MOVE_FOLDER),
      ))

      rgrp.set_name("rgrp_%s_mode" % path_type)
      self.__dict__["_rgrp_%s_mode" % path_type] = rgrp

      if __debug__: RT.register(rgrp, __name__)


  def _setup_autolabel_box(self):

    crbox = AutolabelBox(row_spacing=6, column_spacing=3)
    if __debug__: RT.register(crbox, __name__)

    crbox.set_name("crbox_autolabel_rules")
    self._crbox_autolabel_rules = crbox
    self._blk_criteria_box.add(crbox)
    self._widgets.append(crbox)

    crbox.show_all()


  def _setup_test_combo_box(self):

    prop_store = labelplus.gtkui.common.gtklib.liststore_create(str,
      [_(x) for x in PROPS])
    if __debug__: RT.register(prop_store, __name__)

    cell = gtk.CellRendererText()
    if __debug__: RT.register(cell, __name__)

    self._cmb_test_criteria.pack_start(cell)
    self._cmb_test_criteria.add_attribute(cell, "text", 0)
    self._cmb_test_criteria.set_model(prop_store)
    self._cmb_test_criteria.set_active(0)


  def _setup_criteria_area(self):

    def on_mapped(widget, event):

      # Sometimes a widget is mapped but does not immediately have allocation
      if self._vp_criteria_area.allocation.x < 0:
        twisted.internet.reactor.callLater(0.1, widget.emit, "map-event",
          event)
        return

      if widget.handler_is_connected(handle):
        widget.disconnect(handle)

      self._vp_criteria_area.set_data("was_mapped", True)

      if self._plugin.config["common"]["label_options_pane_pos"] > -1:
        self._vp_criteria_area.set_position(
          self._vp_criteria_area.allocation.height -
          self._plugin.config["common"]["label_options_pane_pos"])

        clamp_position(self._vp_criteria_area)


    def clamp_position(widget, *args):

      handle_size = widget.allocation.height - \
        widget.get_property("max-position")
      max_dist = self._hb_test_criteria.allocation.height + handle_size*2
      threshold = max_dist/2

      if widget.allocation.height - widget.get_position() > threshold:
        twisted.internet.reactor.callLater(0.1, widget.set_position,
          widget.allocation.height - max_dist)
      else:
        twisted.internet.reactor.callLater(0.1, widget.set_position,
          widget.allocation.height)


    handle = self._eb_criteria_area.connect("map-event", on_mapped)

    self._vp_criteria_area.connect("button-release-event", clamp_position)
    self._vp_criteria_area.connect("accept-position", clamp_position)


  def _index_widgets(self):

    self._exp_group = (
      self._exp_download_location,
      self._exp_move_completed,
    )

    self._option_groups = (
      (
        self._chk_download_settings,
        self._chk_download_location,
        self._rgrp_download_location_mode,
        self._lbl_download_location_path,
        self._chk_move_completed,
        self._rgrp_move_completed_mode,
        self._lbl_move_completed_path,
        self._chk_prioritize_first_last,
      ),
      (
        self._chk_bandwidth_settings,
        self._rb_shared_limit,
        self._spn_max_download_speed,
        self._spn_max_upload_speed,
        self._spn_max_connections,
        self._spn_max_upload_slots,
      ),
      (
        self._chk_queue_settings,
        self._chk_auto_managed,
        self._chk_stop_at_ratio,
        self._spn_stop_ratio,
        self._chk_remove_at_ratio,
      ),
      (
        self._chk_autolabel_settings,
        self._rb_autolabel_match_all,
        self._crbox_autolabel_rules,
      ),
    )

    self._dependency_widgets = {
      self._chk_download_settings: (self._blk_download_settings_group,),
      self._chk_bandwidth_settings: (self._blk_bandwidth_settings_group,),
      self._chk_queue_settings: (self._blk_queue_settings_group,),
      self._chk_autolabel_settings: (self._blk_autolabel_settings_group,),

      self._chk_download_location: (self._blk_download_location_group,),
      self._rb_download_location_to_folder: (self._txt_download_location_path,),

      self._chk_move_completed: (self._blk_move_completed_group,),
      self._rb_move_completed_to_folder: (self._txt_move_completed_path,),

      self._chk_stop_at_ratio:
        (self._spn_stop_ratio, self._chk_remove_at_ratio),

      self._chk_autolabel_retroactive: (self._chk_autolabel_unlabeled_only,),
    }


  # Section: Dialog: State

  def _load_state(self):

    if not client.is_localhost():
      for path_type in PATH_TYPES:
        self.__dict__["_btn_%s_browse" % path_type].hide()

    if self._plugin.initialized:
      pos = self._plugin.config["common"]["label_options_pos"]
      if pos:
        self._wnd_label_options.move(*pos)

      size = self._plugin.config["common"]["label_options_size"]
      if size:
        self._wnd_label_options.resize(*size)

      self._tgb_fullname.set_active(
        self._plugin.config["common"]["label_options_fullname"])

      expanded = self._plugin.config["common"]["label_options_exp_state"]
      for exp in expanded:
        widget = getattr(self, "_" + exp, None)
        if widget:
          widget.set_expanded(True)


  def _save_state(self):

    if self._plugin.initialized:
      self._plugin.config["common"]["label_options_pos"] = \
        list(self._wnd_label_options.get_position())

      self._plugin.config["common"]["label_options_size"] = \
        list(self._wnd_label_options.get_size())

      self._plugin.config["common"]["label_options_fullname"] = \
        self._tgb_fullname.get_active()

      expanded = []
      for exp in self._exp_group:
        if exp.get_expanded():
          expanded.append(exp.get_name())

      self._plugin.config["common"]["label_options_exp_state"] = expanded

      if self._vp_criteria_area.get_data("was_mapped"):
        self._plugin.config["common"]["label_options_pane_pos"] = \
          self._vp_criteria_area.allocation.height - \
          self._vp_criteria_area.get_position()

      self._plugin.config.save()


  # Section: Dialog: Options

  def _get_widget_values(self, widgets, options_out):

    for widget in widgets:
      prefix, sep, name = widget.get_name().partition("_")
      if sep and name in options_out:
        ops = self.OP_MAP.get(type(widget))
        if ops:
          getter = getattr(widget, ops[self.GETTER])
          options_out[name] = getter()


  def _set_widget_values(self, widgets, options_in):

    for widget in widgets:
      prefix, sep, name = widget.get_name().partition("_")
      if sep and name in options_in:
        ops = self.OP_MAP.get(type(widget))
        if ops:
          setter = getattr(widget, ops[self.SETTER])
          setter(options_in[name])


  def _load_options(self, options):

    for group in self._option_groups:
      self._set_widget_values(group, options)

    for path_type in PATH_TYPES:
      mode = options["%s_mode" % path_type]
      path = options["%s_path" % path_type]

      self.__dict__["_txt_%s_path" % path_type].set_text(path)

      if mode != MOVE_FOLDER:
        path = self._path_options[path_type].get(mode, "")

      self._set_path_label(path, path_type)


  def _get_options(self):

    options = copy.deepcopy(self._label_defaults)

    for group in self._option_groups:
      self._get_widget_values(group, options)

    return options


  def _revert_options_by_page(self, options_out, page):

    widgets = self._option_groups[page]

    for widget in widgets:
      prefix, sep, name = widget.get_name().partition("_")
      if sep and name in self._label_defaults:
        options_out[name] = copy.deepcopy(self._label_defaults[name])


  def _clear_options(self):

    if not self._label_defaults:
      self._label_defaults = LABEL_DEFAULTS

    for path_type in PATH_TYPES:
      self._path_options[path_type] = {}

    self._label_options = {}

    self._load_options(self._label_defaults)


  def _enable_options(self):

    self._nb_tabs.set_sensitive(True)
    self._btn_defaults.set_sensitive(True)
    self._btn_defaults_all.set_sensitive(True)
    self._btn_apply.set_sensitive(True)


  def _disable_options(self):

    self._nb_tabs.set_sensitive(False)
    self._btn_defaults.set_sensitive(False)
    self._btn_defaults_all.set_sensitive(False)
    self._btn_apply.set_sensitive(False)


  # Section: Dialog: Modifiers

  def _refresh(self):

    self._do_toggle_fullname()

    if self._label_id == ID_NULL:
      self._lbl_selected_label.set_tooltip_text(None)
    else:
      self._lbl_selected_label.set_tooltip_text(self._label_fullname)

    for widget in self._dependency_widgets:
      self._do_toggle_dependents(widget)

    self._set_test_result(None)


  def _set_path_label(self, path, path_type):

    lbl_widget = self.__dict__["_lbl_%s_path" % path_type]
    lbl_widget.set_text(path)
    lbl_widget.set_tooltip_text(path)


  def _set_test_result(self, result):

    if result is not None:
      icon = gtk.STOCK_YES if result else gtk.STOCK_NO
      self._txt_test_criteria.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY,
        icon)
    else:
      self._txt_test_criteria.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY,
        None)


  def _set_error(self, message):

    if message:
      self._img_error.set_tooltip_text(message)
      self._img_error.show()
    else:
      self._img_error.hide()


  # Section: Dialog: Handlers: General

  def _do_close(self, *args):

    self._save_state()
    self.destroy()


  def _do_submit(self, *args):

    self._save_options()


  def _do_open_select_menu(self, *args):

    if self._menu:
      self._menu.popup(None, None, None, 1, gtk.gdk.CURRENT_TIME)


  def _do_revert_to_defaults(self, widget):

    if widget is self._btn_defaults:
      options = self._get_options()
      self._revert_options_by_page(options, self._nb_tabs.get_current_page())
    else:
      options = self._label_defaults

    self._load_options(options)
    self._refresh()


  def _do_toggle_fullname(self, *args):

    if self._tgb_fullname.get_active():
      self._lbl_selected_label.set_text(self._label_fullname)
    else:
      self._lbl_selected_label.set_text(self._label_name)


  def _do_toggle_dependents(self, widget):

    if widget in self._dependency_widgets:
      toggled = widget.get_active()

      for dependent in self._dependency_widgets[widget]:
        dependent.set_sensitive(toggled)


  # Section: Dialog: Handlers: Paths

  def _do_select_mode(self, group, button, mode):

    path_type = self._get_path_type(group)
    self._do_toggle_dependents(self.__dict__["_rb_%s_to_folder" % path_type])

    if mode == MOVE_FOLDER:
      self._on_txt_changed(self.__dict__["_txt_%s_path" % path_type])
    else:
      path = self._path_options[path_type].get(mode, "")
      self._set_path_label(path, path_type)


  def _on_txt_changed(self, widget):

    path_type = self._get_path_type(widget)
    self._set_path_label(widget.get_text(), path_type)


  def _do_open_file_dialog(self, widget):

    def on_response(widget, response):

      if self.valid and response == gtk.RESPONSE_OK:
        txt_widget.set_text(widget.get_filename())

      widget.destroy()


    path_type = self._get_path_type(widget)
    txt_widget = self.__dict__["_txt_%s_path" % path_type]

    dialog = gtk.FileChooserDialog(_(TITLE_SELECT_FOLDER),
      self._wnd_label_options, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
      (
        gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
        gtk.STOCK_OK, gtk.RESPONSE_OK,
      )
    )
    if __debug__: RT.register(dialog, __name__)

    dialog.set_destroy_with_parent(True)
    dialog.connect("response", on_response)

    path = txt_widget.get_text()
    if not os.path.exists(path):
      path = ""

    dialog.set_filename(path)
    dialog.show_all()

    widgets = labelplus.gtkui.common.gtklib.widget_get_descendents(dialog,
      (gtk.ToggleButton,), 1)
    if widgets:
      location_toggle = widgets[0]
      location_toggle.set_active(False)


  # Section: Dialog: Handlers: Criteria Test

  def _do_test_criteria(self, *args):

    def clear_result():

      if self.valid:
        self._set_test_result(None)


    index = self._cmb_test_criteria.get_active()
    prop_name = PROPS[index]

    props = {
      prop_name: [unicode(self._txt_test_criteria.get_text(), "utf8")]
    }

    rules = self._crbox_autolabel_rules.get_all_row_values()
    match_all = self._rb_autolabel_match_all.get_active()

    log.debug("Properties: %r, Rules: %r, Match all: %s", props, rules,
      match_all)

    result = labelplus.common.config.autolabel.find_match(props, rules,
      match_all)

    log.debug("Test result: %s", result)
    self._set_test_result(result)

    twisted.internet.reactor.callLater(self.CLEAR_TEST_DELAY, clear_result)


  # Section: Dialog: Menu

  def _create_menu(self):

    def on_show_menu(widget):

      parent_id = labelplus.common.label.get_parent_id(self._label_id)
      if parent_id in self._store:
        items[0].show()
        items[1].show()
      else:
        items[0].hide()
        items[1].hide()


    def on_activate(widget, label_id):

      self._request_options(label_id)


    def on_activate_parent(widget):

      parent_id = labelplus.common.label.get_parent_id(self._label_id)
      self._request_options(parent_id)


    self._menu = LabelSelectionMenu(self._store.model, on_activate)
    if __debug__: RT.register(self._menu, __name__)

    items = labelplus.gtkui.common.gtklib.menu_add_items(self._menu, 0,
      (
        ((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),
        ((gtk.SeparatorMenuItem,),),
      )
    )

    if __debug__:
      for item in items:
        RT.register(item, __name__)

    self._menu.connect("show", on_show_menu)
    self._menu.show_all()


  def _destroy_menu(self):

    if self._menu:
      self._menu.destroy()
      self._menu = None
class NameInputDialog(WidgetEncapsulator):

  # Section: Constants

  GLADE_FILE = labelplus.common.get_resource("wnd_name_input.glade")
  ROOT_WIDGET = "wnd_name_input"

  REQUEST_TIMEOUT = 10.0

  TYPE_ADD = "add"
  TYPE_RENAME = "rename"

  DIALOG_SPECS = {
    TYPE_ADD: (_(TITLE_ADD_LABEL), gtk.STOCK_ADD, STR_ADD_LABEL),
    TYPE_RENAME: (_(TITLE_RENAME_LABEL), gtk.STOCK_EDIT, STR_RENAME_LABEL),
  }

  DIALOG_NAME = 0
  DIALOG_ICON = 1
  DIALOG_CONTEXT = 2


  # Section: Initialization

  def __init__(self, plugin, dialog_type, label_id):

    self._plugin = plugin
    self._type = dialog_type

    if self._type == self.TYPE_ADD:
      self._parent_id = label_id
    elif self._type == self.TYPE_RENAME:
      if label_id in plugin.store:
        self._parent_id = labelplus.common.label.get_parent_id(label_id)
        self._label_id = label_id
        self._label_name = plugin.store[label_id]["name"]
        self._label_fullname = plugin.store[label_id]["fullname"]
      else:
        raise LabelPlusError(ERR_INVALID_LABEL)
    else:
      raise LabelPlusError(ERR_INVALID_TYPE)

    self._store = None
    self._menu = None

    super(NameInputDialog, self).__init__(self.GLADE_FILE, self.ROOT_WIDGET,
      "_")

    try:
      self._store = plugin.store.copy()
      self._set_parent_label(self._parent_id)

      # Keep window alive with cyclic reference
      self._root_widget.set_data("owner", self)

      self._setup_widgets()

      self._load_state()

      self._create_menu()

      self._refresh()

      self._plugin.register_update_func(self.update_store)
      self._plugin.register_cleanup_func(self.destroy)
    except:
      self.destroy()
      raise


  # Section: Deinitialization

  def destroy(self):

    self._plugin.deregister_update_func(self.update_store)
    self._plugin.deregister_cleanup_func(self.destroy)

    self._destroy_menu()
    self._destroy_store()

    if self.valid:
      self._root_widget.set_data("owner", None)
      super(NameInputDialog, self).destroy()


  def _destroy_store(self):

    if self._store:
      self._store.destroy()
      self._store = None


  # Section: Public

  def show(self):

    self._wnd_name_input.show()


  def update_store(self, store):

    if self._type == self.TYPE_RENAME:
      if self._label_id not in store:
        self.destroy()
        return

    self._destroy_store()
    self._store = store.copy()

    self._destroy_menu()
    self._create_menu()

    self._select_parent_label(self._parent_id)


  # Section: General

  def _set_parent_label(self, parent_id):

    if parent_id in self._store:
      self._parent_id = parent_id
      self._parent_name = self._store[parent_id]["name"]
      self._parent_fullname = self._store[parent_id]["fullname"]
    else:
      self._parent_id = ID_NULL
      self._parent_name = _(STR_NONE)
      self._parent_fullname = _(STR_NONE)


  def _validate(self):

    if self._parent_id != ID_NULL and self._parent_id not in self._store:
      raise LabelPlusError(ERR_INVALID_PARENT)

    if self._type == self.TYPE_RENAME:
      if self._label_id not in self._store:
        raise LabelPlusError(ERR_INVALID_LABEL)

      if (self._label_id == self._parent_id or
          labelplus.common.label.is_ancestor(self._label_id,
            self._parent_id)):
        raise LabelPlusError(ERR_INVALID_PARENT)

    name = unicode(self._txt_name.get_text(), "utf8")
    labelplus.common.label.validate_name(name)

    for id in self._store.get_descendent_ids(self._parent_id, max_depth=1):
      if name == self._store[id]["name"]:
        raise LabelPlusError(ERR_LABEL_EXISTS)


  def _report_error(self, error):

    log.error("%s: %s", self.DIALOG_SPECS[self._type][self.DIALOG_CONTEXT],
      error)
    self._set_error(error.tr())


  # Section: Dialog: Setup

  def _setup_widgets(self):

    self._wnd_name_input.set_transient_for(
      deluge.component.get("MainWindow").window)

    spec = self.DIALOG_SPECS[self._type]
    self._wnd_name_input.set_title(spec[self.DIALOG_NAME])
    icon = self._wnd_name_input.render_icon(spec[self.DIALOG_ICON],
      gtk.ICON_SIZE_SMALL_TOOLBAR)
    self._wnd_name_input.set_icon(icon)

    self._lbl_header.set_markup("<b>%s:</b>" % _(STR_PARENT))

    self._img_error.set_from_stock(gtk.STOCK_DIALOG_ERROR,
      gtk.ICON_SIZE_SMALL_TOOLBAR)

    if self._type == self.TYPE_RENAME:
      self._btn_revert.show()
      self._txt_name.set_text(self._label_name)
      self._txt_name.grab_focus()

    self.connect_signals({
      "do_close" : self._do_close,
      "do_submit" : self._do_submit,
      "do_open_select_menu": self._do_open_select_menu,
      "do_toggle_fullname": self._do_toggle_fullname,
      "do_check_input": self._do_check_input,
      "do_revert": self._do_revert,
    })


  # Section: Dialog: State

  def _load_state(self):

    if self._plugin.initialized:
      pos = self._plugin.config["common"]["name_input_pos"]
      if pos:
        self._wnd_name_input.move(*pos)

      size = self._plugin.config["common"]["name_input_size"]
      if size:
        self._wnd_name_input.resize(*size)

      self._tgb_fullname.set_active(
        self._plugin.config["common"]["name_input_fullname"])


  def _save_state(self):

    if self._plugin.initialized:
      self._plugin.config["common"]["name_input_pos"] = \
        list(self._wnd_name_input.get_position())

      self._plugin.config["common"]["name_input_size"] = \
        list(self._wnd_name_input.get_size())

      self._plugin.config["common"]["name_input_fullname"] = \
        self._tgb_fullname.get_active()

      self._plugin.config.save()


  # Section: Dialog: Modifiers

  def _refresh(self):

    self._do_toggle_fullname()

    if self._parent_id == ID_NULL:
      self._lbl_selected_label.set_tooltip_text(None)
    else:
      self._lbl_selected_label.set_tooltip_text(self._parent_fullname)

    self._do_check_input()


  def _set_error(self, message):

    if message:
      self._img_error.set_tooltip_text(message)
      self._img_error.show()
    else:
      self._img_error.hide()


  def _select_parent_label(self, parent_id):

      self._set_parent_label(parent_id)
      self._refresh()
      self._txt_name.grab_focus()


  # Section: Dialog: Handlers

  def _do_close(self, *args):

    self._save_state()
    self.destroy()


  def _do_submit(self, *args):

    def on_timeout():

      if self.valid:
        self._wnd_name_input.set_sensitive(True)
        self._report_error(LabelPlusError(ERR_TIMED_OUT))


    def process_result(result):

      if self.valid:
        self._wnd_name_input.set_sensitive(True)

        if isinstance(result, Failure):
          error = labelplus.common.extract_error(result)
          if error:
            self._report_error(error)
          else:
            self.destroy()
            return result
        else:
          self._do_close()


    self._do_check_input()
    if not self._btn_ok.get_property("sensitive"):
      return

    name = unicode(self._txt_name.get_text(), "utf8")

    if self._parent_id != ID_NULL:
      dest_name = "%s/%s" % (self._parent_fullname, name)
    else:
      dest_name = name

    if self._type == self.TYPE_ADD:
      log.info("Adding label: %r", dest_name)
      deferred = client.labelplus.add_label(self._parent_id, name)
    elif self._type == self.TYPE_RENAME:
      log.info("Renaming label: %r -> %r", self._label_fullname, dest_name)
      deferred = client.labelplus.move_label(self._label_id, self._parent_id,
        name)

    self._wnd_name_input.set_sensitive(False)

    labelplus.common.deferred_timeout(deferred, self.REQUEST_TIMEOUT,
      on_timeout, process_result, process_result)


  def _do_open_select_menu(self, *args):

    if self._menu:
      self._menu.popup(None, None, None, 1, gtk.gdk.CURRENT_TIME)


  def _do_toggle_fullname(self, *args):

    if self._tgb_fullname.get_active():
      self._lbl_selected_label.set_text(self._parent_fullname)
    else:
      self._lbl_selected_label.set_text(self._parent_name)


  def _do_check_input(self, *args):

    try:
      self._validate()
      self._btn_ok.set_sensitive(True)
      self._set_error(None)
    except LabelPlusError as e:
      self._btn_ok.set_sensitive(False)
      self._set_error(e.tr())


  def _do_revert(self, *args):

    if self._type != self.TYPE_RENAME:
      return

    self._txt_name.set_text(self._label_name)

    parent_id = labelplus.common.label.get_parent_id(self._label_id)
    self._select_parent_label(parent_id)


  # Section: Dialog: Menu

  def _create_menu(self):

    def on_show_menu(menu):

      parent_id = labelplus.common.label.get_parent_id(self._parent_id)
      if parent_id in self._store:
        items[0].show()
      else:
        items[0].hide()


    def on_activate(widget, parent_id):

      self._select_parent_label(parent_id)


    def on_activate_parent(widget):

      parent_id = labelplus.common.label.get_parent_id(self._parent_id)
      self._select_parent_label(parent_id)


    root_items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NULL),)

    self._menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=root_items)
    if __debug__: RT.register(self._menu, __name__)

    items = labelplus.gtkui.common.gtklib.menu_add_items(self._menu, 1,
      (((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),))
    if __debug__: RT.register(items[0], __name__)

    self._menu.connect("show", on_show_menu)
    self._menu.show_all()

    if self._type == self.TYPE_RENAME:
      item = self._menu.get_label_item(self._label_id)
      if item:
        item.set_sensitive(False)


  def _destroy_menu(self):

    if self._menu:
      self._menu.destroy()
      self._menu = None
class AddTorrentExt(WidgetEncapsulator):

  # Section: Constants

  GLADE_FILE = labelplus.common.get_resource("blk_add_torrent_ext.ui")
  ROOT_WIDGET = "blk_add_torrent_ext"

  TORRENT_ID = 0


  # Section: Initialization

  def __init__(self, plugin):

    self._plugin = plugin
    self._dialog = deluge.component.get("AddTorrentDialog")
    self._view = self._dialog.listview_torrents

    self._store = None
    self._menu = None

    self._mappings = {}
    self._handlers = []

    super(AddTorrentExt, self).__init__(self.GLADE_FILE, self.ROOT_WIDGET, "_")

    try:
      self._store = plugin.store.copy()
      if __debug__: RT.register(self._store, __name__)

      log.debug("Setting up widgets...")
      self._setup_widgets()

      log.debug("Installing widgets...")
      self._install_widgets()
      self._register_handlers()

      log.debug("Loading state...")
      self._display_torrent_label(None)
      self._update_sensitivity()

      log.debug("Creating menu...")
      self._create_menu()

      self._plugin.register_update_func(self.update_store)
    except:
      self.unload()
      raise


  def _setup_widgets(self):

    def on_click(widget):

      if self._menu:
        self._menu.popup(None, None, None, None, 1, Gdk.CURRENT_TIME)


    def on_toggle(widget):

      self._refresh_torrent_label()


    self._blk_add_torrent_ext.get_label_widget().set_markup("<b>%s</b>" %
      labelplus.common.DISPLAY_NAME)

    self._btn_select.connect("clicked", on_click)
    self._tgb_fullname.connect("toggled", on_toggle)

    self._load_state()


  def _install_widgets(self):

    widget = self._dialog.builder.get_object("button_revert")

    box = widget.get_ancestor(Gtk.Box).get_parent().get_ancestor(Gtk.Box)

    box.pack_start(self._blk_add_torrent_ext, False, True, 0)

    box.child_set_property(self._blk_add_torrent_ext, "position",
      box.child_get_property(self._blk_add_torrent_ext, "position")-1)


  def _register_handlers(self):

    self._register_handler(self._view.get_selection(), "changed",
      self._on_selection_changed)

    self._register_handler(self._dialog.builder.get_object("button_revert"),
      "clicked", self._do_revert)

    self._register_handler(self._dialog.builder.get_object("button_apply"),
      "clicked", self._do_apply_to_all)

    self._register_handler(self._dialog.builder.get_object("button_remove"),
      "clicked", self._on_remove_torrent)

    self._register_handler(self._dialog.builder.get_object("button_cancel"),
      "clicked", self._on_close)

    self._register_handler(self._dialog.builder.get_object("button_add"),
      "clicked", self._on_add_torrent)


  def _create_menu(self):

    def on_activate(widget, label_id):

      id = self._get_selected_torrent()
      if id:
        self._set_torrent_label(id, label_id)
        self._display_torrent_label(id)


    items = (((Gtk.MenuItem, {'label': _(STR_NONE)}), on_activate, ID_NONE),)

    self._menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=items)

    if __debug__: RT.register(self._menu, __name__)


  # Section: Deinitialization

  def unload(self):

    self._plugin.deregister_update_func(self.update_store)

    self._deregister_handlers()
    self._uninstall_widgets()

    self._destroy_menu()
    self._destroy_store()

    super(AddTorrentExt, self).destroy()


  def _deregister_handlers(self):

    for widget, handle in self._handlers:
      widget.disconnect(handle)


  def _uninstall_widgets(self):

    box = self._blk_add_torrent_ext.get_parent()
    if box:
      box.remove(self._blk_add_torrent_ext)


  def _destroy_menu(self):

    if self._menu:
      self._menu.destroy()
      self._menu = None


  def _destroy_store(self):

    if self._store:
      self._store.destroy()
      self._store = None


  # Section: General

  def _register_handler(self, obj, signal, func, *args, **kwargs):

    handle = obj.connect(signal, func, *args, **kwargs)
    self._handlers.append((obj, handle))


  def _get_selected_torrent(self):

    model, row = self._view.get_selection().get_selected()
    if row:
      return model[row][self.TORRENT_ID]

    return None


  # Section: Update

  def update_store(self, store):

    self._store.destroy()
    self._store = store.copy()
    if __debug__: RT.register(self._store, __name__)

    self._destroy_menu()
    self._create_menu()

    self._refresh_torrent_label()


  # Section: Widget State

  def _load_state(self):

    if self._plugin.initialized:
      self._tgb_fullname.set_active(
        self._plugin.config["common"]["add_torrent_ext_fullname"])


  def _save_state(self):

    if self._plugin.initialized:
      self._plugin.config["common"]["add_torrent_ext_fullname"] = \
        self._tgb_fullname.get_active()

      self._plugin.config.save()


  # Section: Widget Modifiers

  def _update_sensitivity(self):

    id = self._get_selected_torrent()
    self._blk_add_torrent_ext.set_sensitive(id is not None)


  def _display_torrent_label(self, id):

    if id in self._mappings:
      label_id = self._mappings[id]
    else:
      label_id = None

    if label_id in self._store:
      name = self._store[label_id]["name"]
      fullname = self._store[label_id]["fullname"]
    else:
      name = _(STR_NONE)
      fullname = _(STR_NONE)

    if self._tgb_fullname.get_active():
      self._lbl_name.set_text(fullname)
    else:
      self._lbl_name.set_text(name)

    if label_id:
      self._lbl_name.set_tooltip_text(fullname)
    else:
      self._lbl_name.set_tooltip_text(None)


  def _refresh_torrent_label(self):

    id = self._get_selected_torrent()
    self._display_torrent_label(id)


  def _set_torrent_label(self, torrent_id, label_id):

    if label_id not in RESERVED_IDS and label_id in self._store:
      log.info("Setting label for %s to %r", torrent_id,
        self._store[label_id]["fullname"])
      self._mappings[torrent_id] = label_id
    else:
      log.info("Removing label from %s", torrent_id)

      if label_id not in self._store:
        log.error("%s", LabelPlusError(ERR_INVALID_LABEL))

      if torrent_id in self._mappings:
        del self._mappings[torrent_id]


  def _clear(self):

    self._mappings.clear()
    self._display_torrent_label(None)


  # Section: Deluge Handlers

  def _on_selection_changed(self, selection):

    self._refresh_torrent_label()
    self._update_sensitivity()


  def _do_revert(self, widget):

    id = self._get_selected_torrent()
    if id in self._mappings:
      log.info("Resetting label setting on %s", id)
      del self._mappings[id]
      self._display_torrent_label(None)


  def _do_apply_to_all(self, widget):

    def set_mapping(model, path, row):

      id = model[row][self.TORRENT_ID]
      self._mappings[id] = label_id


    id = self._get_selected_torrent()
    if id in self._mappings:
      label_id = self._mappings[id]
      log.info("Applying label %r to all torrents",
        self._store[label_id]["fullname"])
      self._view.get_model().foreach(set_mapping)
    else:
      self._mappings.clear()


  def _on_remove_torrent(self, widget):

    ids = list(self._dialog.files.keys())
    for key in list(self._mappings.keys()):
      if key not in ids:
        del self._mappings[key]

    self._refresh_torrent_label()


  def _on_add_torrent(self, widget):

    log.info("Applying labels to the torrents added")

    reverse_map = {}
    for torrent_id, label_id in list(self._mappings.items()):
      if label_id in RESERVED_IDS or label_id not in self._store:
        continue

      if label_id not in reverse_map:
        reverse_map[label_id] = []

      reverse_map[label_id].append(torrent_id)

    for label_id, torrent_ids in list(reverse_map.items()):
      client.labelplus.set_torrent_labels(torrent_ids, label_id)

    self._on_close(widget)


  def _on_close(self, widget):

    self._save_state()
    self._clear()
  def _create_filter_menu(self):

    def on_activate(widget, ids):

      if not isinstance(ids, list):
        ids = [ids]

      self._set_filter_sync_sidebar(ids)


    def on_activate_parent(widget):

      ids = self.get_any_selected_labels()
      parent_id = labelplus.common.label.get_common_parent(ids)
      on_activate(widget, parent_id)


    def on_activate_selected(widget):

      ids = self.get_selected_torrent_labels()
      on_activate(widget, ids)


    def on_show_menu(widget):

      items[0].hide()
      items[1].hide()

      ids = self.get_any_selected_labels()
      parent_id = labelplus.common.label.get_common_parent(ids)
      if self._store.is_user_label(parent_id):
        items[0].show()

      ids = self.get_selected_torrent_labels()
      if self._store.user_labels(ids):
        items[1].show()


    root_items = (
      ((gtk.MenuItem, _(STR_ALL)), on_activate, ID_ALL),
      ((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NONE),
    )

    menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=root_items)
    menu.connect("show", on_show_menu)

    items = labelplus.gtkui.common.gtklib.menu_add_items(menu, 2,
      (
        ((gtk.MenuItem, _(STR_PARENT)), on_activate_parent),
        ((gtk.MenuItem, _(STR_SELECTED)), on_activate_selected),
      )
    )

    root = gtk.MenuItem(_(TITLE_SET_FILTER))
    root.set_submenu(menu)

    if __debug__: RT.register(menu, __name__)
    if __debug__: RT.register(root, __name__)

    return root
class AddTorrentExt(WidgetEncapsulator):

  # Section: Constants

  GLADE_FILE = labelplus.common.get_resource("blk_add_torrent_ext.glade")
  ROOT_WIDGET = "blk_add_torrent_ext"

  TORRENT_ID = 0


  # Section: Initialization

  def __init__(self, plugin):

    self._plugin = plugin
    self._dialog = deluge.component.get("AddTorrentDialog")
    self._view = self._dialog.listview_torrents

    self._store = None
    self._menu = None

    self._mappings = {}
    self._handlers = []

    super(AddTorrentExt, self).__init__(self.GLADE_FILE, self.ROOT_WIDGET, "_")

    try:
      self._store = plugin.store.copy()
      if __debug__: RT.register(self._store, __name__)

      log.debug("Setting up widgets...")
      self._setup_widgets()

      log.debug("Installing widgets...")
      self._install_widgets()
      self._register_handlers()

      log.debug("Loading state...")
      self._display_torrent_label(None)
      self._update_sensitivity()

      log.debug("Creating menu...")
      self._create_menu()

      self._plugin.register_update_func(self.update_store)
    except:
      self.unload()
      raise


  def _setup_widgets(self):

    def on_click(widget):

      if self._menu:
        self._menu.popup(None, None, None, 1, gtk.gdk.CURRENT_TIME)


    def on_toggle(widget):

      self._refresh_torrent_label()


    self._blk_add_torrent_ext.get_label_widget().set_markup("<b>%s</b>" %
      labelplus.common.DISPLAY_NAME)

    self._btn_select.connect("clicked", on_click)
    self._tgb_fullname.connect("toggled", on_toggle)

    self._load_state()


  def _install_widgets(self):

    widget = self._dialog.glade.get_widget("button_revert")

    box = widget.get_ancestor(gtk.VBox)
    box.pack_start(self._blk_add_torrent_ext, expand=False)

    box.child_set_property(self._blk_add_torrent_ext, "position",
      box.child_get_property(self._blk_add_torrent_ext, "position")-1)


  def _register_handlers(self):

    self._register_handler(self._view.get_selection(), "changed",
      self._on_selection_changed)

    self._register_handler(self._dialog.glade.get_widget("button_revert"),
      "clicked", self._do_revert)

    self._register_handler(self._dialog.glade.get_widget("button_apply"),
      "clicked", self._do_apply_to_all)

    self._register_handler(self._dialog.glade.get_widget("button_remove"),
      "clicked", self._on_remove_torrent)

    self._register_handler(self._dialog.glade.get_widget("button_cancel"),
      "clicked", self._on_close)

    self._register_handler(self._dialog.glade.get_widget("button_add"),
      "clicked", self._on_add_torrent)


  def _create_menu(self):

    def on_activate(widget, label_id):

      id = self._get_selected_torrent()
      if id:
        self._set_torrent_label(id, label_id)
        self._display_torrent_label(id)


    items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NONE),)

    self._menu = LabelSelectionMenu(self._store.model, on_activate,
      root_items=items)

    if __debug__: RT.register(self._menu, __name__)


  # Section: Deinitialization

  def unload(self):

    self._plugin.deregister_update_func(self.update_store)

    self._deregister_handlers()
    self._uninstall_widgets()

    self._destroy_menu()
    self._destroy_store()

    super(AddTorrentExt, self).destroy()


  def _deregister_handlers(self):

    for widget, handle in self._handlers:
      widget.disconnect(handle)


  def _uninstall_widgets(self):

    box = self._blk_add_torrent_ext.get_parent()
    if box:
      box.remove(self._blk_add_torrent_ext)


  def _destroy_menu(self):

    if self._menu:
      self._menu.destroy()
      self._menu = None


  def _destroy_store(self):

    if self._store:
      self._store.destroy()
      self._store = None


  # Section: General

  def _register_handler(self, obj, signal, func, *args, **kwargs):

    handle = obj.connect(signal, func, *args, **kwargs)
    self._handlers.append((obj, handle))


  def _get_selected_torrent(self):

    model, row = self._view.get_selection().get_selected()
    if row:
      return model[row][self.TORRENT_ID]

    return None


  # Section: Update

  def update_store(self, store):

    self._store.destroy()
    self._store = store.copy()
    if __debug__: RT.register(self._store, __name__)

    self._destroy_menu()
    self._create_menu()

    self._refresh_torrent_label()


  # Section: Widget State

  def _load_state(self):

    if self._plugin.initialized:
      self._tgb_fullname.set_active(
        self._plugin.config["common"]["add_torrent_ext_fullname"])


  def _save_state(self):

    if self._plugin.initialized:
      self._plugin.config["common"]["add_torrent_ext_fullname"] = \
        self._tgb_fullname.get_active()

      self._plugin.config.save()


  # Section: Widget Modifiers

  def _update_sensitivity(self):

    id = self._get_selected_torrent()
    self._blk_add_torrent_ext.set_sensitive(id is not None)


  def _display_torrent_label(self, id):

    if id in self._mappings:
      label_id = self._mappings[id]
    else:
      label_id = None

    if label_id in self._store:
      name = self._store[label_id]["name"]
      fullname = self._store[label_id]["fullname"]
    else:
      name = _(STR_NONE)
      fullname = _(STR_NONE)

    if self._tgb_fullname.get_active():
      self._lbl_name.set_text(fullname)
    else:
      self._lbl_name.set_text(name)

    if label_id:
      self._lbl_name.set_tooltip_text(fullname)
    else:
      self._lbl_name.set_tooltip_text(None)


  def _refresh_torrent_label(self):

    id = self._get_selected_torrent()
    self._display_torrent_label(id)


  def _set_torrent_label(self, torrent_id, label_id):

    if label_id not in RESERVED_IDS and label_id in self._store:
      log.info("Setting label for %s to %r", torrent_id,
        self._store[label_id]["fullname"])
      self._mappings[torrent_id] = label_id
    else:
      log.info("Removing label from %s", torrent_id)

      if label_id not in self._store:
        log.error("%s", LabelPlusError(ERR_INVALID_LABEL))

      if torrent_id in self._mappings:
        del self._mappings[torrent_id]


  def _clear(self):

    self._mappings.clear()
    self._display_torrent_label(None)


  # Section: Deluge Handlers

  def _on_selection_changed(self, selection):

    self._refresh_torrent_label()
    self._update_sensitivity()


  def _do_revert(self, widget):

    id = self._get_selected_torrent()
    if id in self._mappings:
      log.info("Resetting label setting on %s", id)
      del self._mappings[id]
      self._display_torrent_label(None)


  def _do_apply_to_all(self, widget):

    def set_mapping(model, path, row):

      id = model[row][self.TORRENT_ID]
      self._mappings[id] = label_id


    id = self._get_selected_torrent()
    if id in self._mappings:
      label_id = self._mappings[id]
      log.info("Applying label %r to all torrents",
        self._store[label_id]["fullname"])
      self._view.get_model().foreach(set_mapping)
    else:
      self._mappings.clear()


  def _on_remove_torrent(self, widget):

    ids = self._dialog.files.keys()
    for key in self._mappings.keys():
      if key not in ids:
        del self._mappings[key]

    self._refresh_torrent_label()


  def _on_add_torrent(self, widget):

    log.info("Applying labels to the torrents added")

    reverse_map = {}
    for torrent_id, label_id in self._mappings.iteritems():
      if label_id in RESERVED_IDS or label_id not in self._store:
        continue

      if label_id not in reverse_map:
        reverse_map[label_id] = []

      reverse_map[label_id].append(torrent_id)

    for label_id, torrent_ids in reverse_map.iteritems():
      client.labelplus.set_torrent_labels(torrent_ids, label_id)

    self._on_close(widget)


  def _on_close(self, widget):

    self._save_state()
    self._clear()
class NameInputDialog(WidgetEncapsulator):

    # Section: Constants

    GLADE_FILE = labelplus.common.get_resource("wnd_name_input.glade")
    ROOT_WIDGET = "wnd_name_input"

    REQUEST_TIMEOUT = 10.0

    TYPE_ADD = "add"
    TYPE_RENAME = "rename"

    DIALOG_SPECS = {
        TYPE_ADD: (_(TITLE_ADD_LABEL), gtk.STOCK_ADD, STR_ADD_LABEL),
        TYPE_RENAME: (_(TITLE_RENAME_LABEL), gtk.STOCK_EDIT, STR_RENAME_LABEL),
    }

    DIALOG_NAME = 0
    DIALOG_ICON = 1
    DIALOG_CONTEXT = 2

    # Section: Initialization

    def __init__(self, plugin, dialog_type, label_id):

        self._plugin = plugin
        self._type = dialog_type

        if self._type == self.TYPE_ADD:
            self._parent_id = label_id
        elif self._type == self.TYPE_RENAME:
            if label_id in plugin.store:
                self._parent_id = labelplus.common.label.get_parent_id(
                    label_id)
                self._label_id = label_id
                self._label_name = plugin.store[label_id]["name"]
                self._label_fullname = plugin.store[label_id]["fullname"]
            else:
                raise LabelPlusError(ERR_INVALID_LABEL)
        else:
            raise LabelPlusError(ERR_INVALID_TYPE)

        self._store = None
        self._menu = None

        super(NameInputDialog, self).__init__(self.GLADE_FILE,
                                              self.ROOT_WIDGET, "_")

        try:
            self._store = plugin.store.copy()
            self._set_parent_label(self._parent_id)

            # Keep window alive with cyclic reference
            self._root_widget.set_data("owner", self)

            self._setup_widgets()

            self._load_state()

            self._create_menu()

            self._refresh()

            self._plugin.register_update_func(self.update_store)
            self._plugin.register_cleanup_func(self.destroy)
        except:
            self.destroy()
            raise

    # Section: Deinitialization

    def destroy(self):

        self._plugin.deregister_update_func(self.update_store)
        self._plugin.deregister_cleanup_func(self.destroy)

        self._destroy_menu()
        self._destroy_store()

        if self.valid:
            self._root_widget.set_data("owner", None)
            super(NameInputDialog, self).destroy()

    def _destroy_store(self):

        if self._store:
            self._store.destroy()
            self._store = None

    # Section: Public

    def show(self):

        self._wnd_name_input.show()

    def update_store(self, store):

        if self._type == self.TYPE_RENAME:
            if self._label_id not in store:
                self.destroy()
                return

        self._destroy_store()
        self._store = store.copy()

        self._destroy_menu()
        self._create_menu()

        self._select_parent_label(self._parent_id)

    # Section: General

    def _set_parent_label(self, parent_id):

        if parent_id in self._store:
            self._parent_id = parent_id
            self._parent_name = self._store[parent_id]["name"]
            self._parent_fullname = self._store[parent_id]["fullname"]
        else:
            self._parent_id = ID_NULL
            self._parent_name = _(STR_NONE)
            self._parent_fullname = _(STR_NONE)

    def _validate(self):

        if self._parent_id != ID_NULL and self._parent_id not in self._store:
            raise LabelPlusError(ERR_INVALID_PARENT)

        if self._type == self.TYPE_RENAME:
            if self._label_id not in self._store:
                raise LabelPlusError(ERR_INVALID_LABEL)

            if (self._label_id == self._parent_id
                    or labelplus.common.label.is_ancestor(
                        self._label_id, self._parent_id)):
                raise LabelPlusError(ERR_INVALID_PARENT)

        name = unicode(self._txt_name.get_text(), "utf8")
        labelplus.common.label.validate_name(name)

        for id in self._store.get_descendent_ids(self._parent_id, max_depth=1):
            if name == self._store[id]["name"]:
                raise LabelPlusError(ERR_LABEL_EXISTS)

    def _report_error(self, error):

        log.error("%s: %s", self.DIALOG_SPECS[self._type][self.DIALOG_CONTEXT],
                  error)
        self._set_error(error.tr())

    # Section: Dialog: Setup

    def _setup_widgets(self):

        self._wnd_name_input.set_transient_for(
            deluge.component.get("MainWindow").window)

        spec = self.DIALOG_SPECS[self._type]
        self._wnd_name_input.set_title(spec[self.DIALOG_NAME])
        icon = self._wnd_name_input.render_icon(spec[self.DIALOG_ICON],
                                                gtk.ICON_SIZE_SMALL_TOOLBAR)
        self._wnd_name_input.set_icon(icon)

        self._lbl_header.set_markup("<b>%s:</b>" % _(STR_PARENT))

        self._img_error.set_from_stock(gtk.STOCK_DIALOG_ERROR,
                                       gtk.ICON_SIZE_SMALL_TOOLBAR)

        if self._type == self.TYPE_RENAME:
            self._btn_revert.show()
            self._txt_name.set_text(self._label_name)
            self._txt_name.grab_focus()

        self.connect_signals({
            "do_close": self._do_close,
            "do_submit": self._do_submit,
            "do_open_select_menu": self._do_open_select_menu,
            "do_toggle_fullname": self._do_toggle_fullname,
            "do_check_input": self._do_check_input,
            "do_revert": self._do_revert,
        })

    # Section: Dialog: State

    def _load_state(self):

        if self._plugin.initialized:
            pos = self._plugin.config["common"]["name_input_pos"]
            if pos:
                self._wnd_name_input.move(*pos)

            size = self._plugin.config["common"]["name_input_size"]
            if size:
                self._wnd_name_input.resize(*size)

            self._tgb_fullname.set_active(
                self._plugin.config["common"]["name_input_fullname"])

    def _save_state(self):

        if self._plugin.initialized:
            self._plugin.config["common"]["name_input_pos"] = \
              list(self._wnd_name_input.get_position())

            self._plugin.config["common"]["name_input_size"] = \
              list(self._wnd_name_input.get_size())

            self._plugin.config["common"]["name_input_fullname"] = \
              self._tgb_fullname.get_active()

            self._plugin.config.save()

    # Section: Dialog: Modifiers

    def _refresh(self):

        self._do_toggle_fullname()

        if self._parent_id == ID_NULL:
            self._lbl_selected_label.set_tooltip_text(None)
        else:
            self._lbl_selected_label.set_tooltip_text(self._parent_fullname)

        self._do_check_input()

    def _set_error(self, message):

        if message:
            self._img_error.set_tooltip_text(message)
            self._img_error.show()
        else:
            self._img_error.hide()

    def _select_parent_label(self, parent_id):

        self._set_parent_label(parent_id)
        self._refresh()
        self._txt_name.grab_focus()

    # Section: Dialog: Handlers

    def _do_close(self, *args):

        self._save_state()
        self.destroy()

    def _do_submit(self, *args):
        def on_timeout():

            if self.valid:
                self._wnd_name_input.set_sensitive(True)
                self._report_error(LabelPlusError(ERR_TIMED_OUT))

        def process_result(result):

            if self.valid:
                self._wnd_name_input.set_sensitive(True)

                if isinstance(result, Failure):
                    error = labelplus.common.extract_error(result)
                    if error:
                        self._report_error(error)
                    else:
                        self.destroy()
                        return result
                else:
                    self._do_close()

        self._do_check_input()
        if not self._btn_ok.get_property("sensitive"):
            return

        name = unicode(self._txt_name.get_text(), "utf8")

        if self._parent_id != ID_NULL:
            dest_name = "%s/%s" % (self._parent_fullname, name)
        else:
            dest_name = name

        if self._type == self.TYPE_ADD:
            log.info("Adding label: %r", dest_name)
            deferred = client.labelplus.add_label(self._parent_id, name)
        elif self._type == self.TYPE_RENAME:
            log.info("Renaming label: %r -> %r", self._label_fullname,
                     dest_name)
            deferred = client.labelplus.move_label(self._label_id,
                                                   self._parent_id, name)

        self._wnd_name_input.set_sensitive(False)

        labelplus.common.deferred_timeout(deferred, self.REQUEST_TIMEOUT,
                                          on_timeout, process_result,
                                          process_result)

    def _do_open_select_menu(self, *args):

        if self._menu:
            self._menu.popup(None, None, None, 1, gtk.gdk.CURRENT_TIME)

    def _do_toggle_fullname(self, *args):

        if self._tgb_fullname.get_active():
            self._lbl_selected_label.set_text(self._parent_fullname)
        else:
            self._lbl_selected_label.set_text(self._parent_name)

    def _do_check_input(self, *args):

        try:
            self._validate()
            self._btn_ok.set_sensitive(True)
            self._set_error(None)
        except LabelPlusError as e:
            self._btn_ok.set_sensitive(False)
            self._set_error(e.tr())

    def _do_revert(self, *args):

        if self._type != self.TYPE_RENAME:
            return

        self._txt_name.set_text(self._label_name)

        parent_id = labelplus.common.label.get_parent_id(self._label_id)
        self._select_parent_label(parent_id)

    # Section: Dialog: Menu

    def _create_menu(self):
        def on_show_menu(menu):

            parent_id = labelplus.common.label.get_parent_id(self._parent_id)
            if parent_id in self._store:
                items[0].show()
            else:
                items[0].hide()

        def on_activate(widget, parent_id):

            self._select_parent_label(parent_id)

        def on_activate_parent(widget):

            parent_id = labelplus.common.label.get_parent_id(self._parent_id)
            self._select_parent_label(parent_id)

        root_items = (((gtk.MenuItem, _(STR_NONE)), on_activate, ID_NULL), )

        self._menu = LabelSelectionMenu(self._store.model,
                                        on_activate,
                                        root_items=root_items)
        if __debug__: RT.register(self._menu, __name__)

        items = labelplus.gtkui.common.gtklib.menu_add_items(
            self._menu, 1,
            (((gtk.MenuItem, _(STR_PARENT)), on_activate_parent), ))
        if __debug__: RT.register(items[0], __name__)

        self._menu.connect("show", on_show_menu)
        self._menu.show_all()

        if self._type == self.TYPE_RENAME:
            item = self._menu.get_label_item(self._label_id)
            if item:
                item.set_sensitive(False)

    def _destroy_menu(self):

        if self._menu:
            self._menu.destroy()
            self._menu = None
    def _create_filter_menu(self):
        def on_activate(widget, ids):

            if not isinstance(ids, list):
                ids = [ids]

            self._set_filter_sync_sidebar(ids)

        def on_activate_parent(widget):

            ids = self.get_any_selected_labels()
            parent_id = labelplus.common.label.get_common_parent(ids)
            on_activate(widget, parent_id)

        def on_activate_selected(widget):

            ids = self.get_selected_torrent_labels()
            on_activate(widget, ids)

        def on_show_menu(widget):

            items[0].hide()
            items[1].hide()

            ids = self.get_any_selected_labels()
            parent_id = labelplus.common.label.get_common_parent(ids)
            if self._store.is_user_label(parent_id):
                items[0].show()

            ids = self.get_selected_torrent_labels()
            if self._store.user_labels(ids):
                items[1].show()

        root_items = (
            ((Gtk.MenuItem, {
                'label': _(STR_ALL)
            }), on_activate, ID_ALL),
            ((Gtk.MenuItem, {
                'label': _(STR_NONE)
            }), on_activate, ID_NONE),
        )

        menu = LabelSelectionMenu(self._store.model,
                                  on_activate,
                                  root_items=root_items)
        menu.connect("show", on_show_menu)

        items = labelplus.gtkui.common.gtklib.menu_add_items(
            menu, 2, (
                ((Gtk.MenuItem, {
                    'label': _(STR_PARENT)
                }), on_activate_parent),
                ((Gtk.MenuItem, {
                    'label': _(STR_SELECTED)
                }), on_activate_selected),
            ))

        root = Gtk.MenuItem(label=_(TITLE_SET_FILTER))
        root.set_submenu(menu)

        if __debug__: RT.register(menu, __name__)
        if __debug__: RT.register(root, __name__)

        return root