Beispiel #1
0
class TransferList:
    def __init__(self, frame, type):

        self.frame = frame
        self.type = type

        load_ui_elements(self, os.path.join(frame.gui_dir, "ui",
                                            type + "s.ui"))
        getattr(frame, type + "svbox").add(self.Main)
        self.widget = widget = getattr(self, type.title() + "List")

        self.last_ui_update = self.last_save = 0
        self.list = []
        self.users = {}
        self.paths = {}

        # Status list
        self.statuses = {}
        self.statuses["Queued"] = _("Queued")
        self.statuses["Getting status"] = _("Getting status")
        self.statuses["Establishing connection"] = _("Establishing connection")
        self.statuses["Transferring"] = _("Transferring")
        self.statuses["Cannot connect"] = _("Cannot connect")
        self.statuses["User logged off"] = _("User logged off")
        self.statuses["Connection closed by peer"] = _(
            "Connection closed by peer")
        self.statuses["Aborted"] = _("Aborted")
        self.statuses["Finished"] = _("Finished")
        self.statuses["Filtered"] = _("Filtered")
        self.statuses["File not shared"] = _("File not shared")
        self.statuses["File not shared."] = _(
            "File not shared"
        )  # The official client sends a variant containing a dot
        self.statuses["Download directory error"] = _(
            "Download directory error")
        self.statuses["Local file error"] = _("Local file error")
        self.statuses["Remote file error"] = _("Remote file error")

        # String templates
        self.extension_list_template = _("All %(ext)s")
        self.files_template = _("%(number)2s files ")

        self.transfersmodel = Gtk.TreeStore(
            str,  # (0)  user
            str,  # (1)  path
            str,  # (2)  file name
            str,  # (3)  status
            str,  # (4)  hqueue position
            GObject.TYPE_UINT64,  # (5)  percent
            str,  # (6)  hsize
            str,  # (7)  hspeed
            str,  # (8)  htime elapsed
            str,  # (9)  time left
            str,  # (10) path
            str,  # (11) status (non-translated)
            GObject.TYPE_UINT64,  # (12) size
            GObject.TYPE_UINT64,  # (13) current bytes
            GObject.TYPE_UINT64,  # (14) speed
            GObject.TYPE_UINT64,  # (15) time elapsed
            GObject.TYPE_UINT64,  # (16) file count
            GObject.TYPE_UINT64,  # (17) queue position
            GObject.TYPE_PYOBJECT  # (18) transfer object
        )

        self.column_numbers = list(range(self.transfersmodel.get_n_columns()))
        self.cols = cols = initialise_columns(
            type,
            widget,
            ["user", _("User"), 200, "text", None],
            ["path", _("Path"), 400, "text", None],
            ["filename", _("Filename"), 400, "text", None],
            ["status", _("Status"), 140, "text", None],
            ["queue_position",
             _("Queue Position"), 50, "number", None],
            ["percent", _("Percent"), 70, "progress", None],
            ["size", _("Size"), 170, "number", None],
            ["speed", _("Speed"), 90, "number", None],
            ["time_elapsed",
             _("Time Elapsed"), 140, "number", None],
            ["time_left", _("Time Left"), 140, "number", None],
        )

        cols["user"].set_sort_column_id(0)
        cols["path"].set_sort_column_id(1)
        cols["filename"].set_sort_column_id(2)
        cols["status"].set_sort_column_id(11)
        cols["queue_position"].set_sort_column_id(17)
        cols["percent"].set_sort_column_id(5)
        cols["size"].set_sort_column_id(12)
        cols["speed"].set_sort_column_id(14)
        cols["time_elapsed"].set_sort_column_id(8)
        cols["time_left"].set_sort_column_id(9)

        widget.set_model(self.transfersmodel)

        self.group_dropdown = getattr(frame,
                                      "ToggleTree%ss" % self.type.title())
        self.expand_button = getattr(frame, "Expand%ss" % self.type.title())

        self.group_dropdown.connect("changed", self.on_toggle_tree)
        self.group_dropdown.set_active(
            config.sections["transfers"]["group%ss" % self.type])

        self.expand_button.connect("toggled", self.on_expand_tree)
        self.expand_button.set_active(
            config.sections["transfers"]["%ssexpanded" % self.type])

        self.popup_menu_users = PopupMenu(frame)
        self.popup_menu_clear = PopupMenu(frame)

        self.popup_menu = PopupMenu(frame)
        self.popup_menu.setup(
            ("#" + "selected_files", None), ("", None),
            ("#" + _("Send to _Player"), self.on_play_files),
            ("#" + _("_Open Folder"), self.on_open_directory),
            ("#" + _("File P_roperties"), self.on_file_properties), ("", None),
            ("#" + _("Copy _File Path"), self.on_copy_file_path),
            ("#" + _("Copy _URL"), self.on_copy_url),
            ("#" + _("Copy Folder URL"), self.on_copy_dir_url), ("", None),
            ("#" + _("_Search"), self.on_file_search),
            (">" + _("User(s)"), self.popup_menu_users), ("", None),
            ("#" + _("_Retry"), self.on_retry_transfer),
            ("#" + _("Abor_t"), self.on_abort_transfer),
            ("#" + _("_Clear"), self.on_clear_transfer), ("", None),
            (">" + _("Clear Groups"), self.popup_menu_clear))

        self.update_visuals()

    def init_interface(self, list):

        self.list = list
        self.widget.set_sensitive(True)
        self.update()

    def rebuild_transfers(self):

        if self.frame.np.transfers is None:
            return

        self.clear()
        self.update()

    def save_columns(self):
        save_columns(self.type, self.widget.get_columns())

    def update_visuals(self):

        for widget in list(self.__dict__.values()):
            update_widget_visuals(widget, list_font_target="transfersfont")

    def conn_close(self):

        self.widget.set_sensitive(False)
        self.list = []
        self.clear()

    def select_transfers(self):

        self.selected_transfers = set()
        self.selected_users = set()

        model, paths = self.widget.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            self.select_transfer(model, iterator, select_user=True)

            # If we're in grouping mode, select any transfers under the selected
            # user or folder
            self.select_child_transfers(model, model.iter_children(iterator))

    def select_child_transfers(self, model, iterator):

        while iterator is not None:
            self.select_transfer(model, iterator)
            self.select_child_transfers(model, model.iter_children(iterator))
            iterator = model.iter_next(iterator)

    def select_transfer(self, model, iterator, select_user=False):

        user = model.get_value(iterator, 0)
        transfer = model.get_value(iterator, 18)

        if isinstance(transfer, Transfer):
            self.selected_transfers.add(transfer)

        if select_user:
            self.selected_users.add(user)

    def new_transfer_notification(self):
        self.frame.request_tab_icon(self.tab_label)

    def on_ban(self, *args):

        self.select_transfers()

        for user in self.selected_users:
            self.frame.np.network_filter.ban_user(user)

    def on_file_search(self, *args):

        transfer = next(iter(self.selected_transfers), None)

        if not transfer:
            return

        self.frame.SearchEntry.set_text(transfer.filename.rsplit("\\", 1)[1])
        self.frame.change_main_page("search")

    def translate_status(self, status):

        try:
            newstatus = self.statuses[status]
        except KeyError:
            newstatus = status

        return newstatus

    def update(self, transfer=None, forceupdate=False):

        if not self.widget.get_sensitive():
            """ List is not initialized """
            return

        curtime = time()

        if (curtime - self.last_save) > 15:
            """ Save downloads list to file every 15 seconds """

            if self.frame.np.transfers is not None:
                self.frame.np.transfers.save_downloads()

            self.last_save = curtime

        finished = (transfer is not None and transfer.status == "Finished")

        if forceupdate or finished or \
                (curtime - self.last_ui_update) > 1:
            self.frame.update_bandwidth()

        if not forceupdate and self.frame.current_tab_label != self.tab_label:
            """ No need to do unnecessary work if transfers are not visible """
            return

        if transfer is not None:
            self.update_specific(transfer)

        elif self.list is not None:
            for transfer in reversed(self.list):
                self.update_specific(transfer)

        if forceupdate or finished or \
                (curtime - self.last_ui_update) > 1:
            """ Unless a transfer finishes, use a cooldown to avoid updating
            too often """

            self.update_parent_rows()

    def update_parent_rows(self, only_remove=False):

        # Remove empty parent rows
        for path, pathiter in list(self.paths.items()):
            if not self.transfersmodel.iter_has_child(pathiter):
                self.transfersmodel.remove(pathiter)
                del self.paths[path]

            elif not only_remove:
                self.update_parent_row(pathiter)

        for username, useriter in list(self.users.items()):
            if isinstance(useriter, Gtk.TreeIter):
                if not self.transfersmodel.iter_has_child(useriter):
                    self.transfersmodel.remove(useriter)
                    del self.users[username]

                elif not only_remove:
                    self.update_parent_row(useriter)
            else:
                # No grouping
                if not self.users[username]:
                    del self.users[username]

        self.frame.update_bandwidth()
        self.last_ui_update = time()

    def update_parent_row(self, initer):

        speed = 0.0
        percent = totalsize = position = 0
        hspeed = helapsed = left = ""
        elapsed = 0
        filecount = 0
        salientstatus = ""
        extensions = {}

        iterator = self.transfersmodel.iter_children(initer)

        while iterator is not None:

            status = self.transfersmodel.get_value(iterator, 11)

            if salientstatus in (
                    '', "Finished",
                    "Filtered"):  # we prefer anything over ''/finished
                salientstatus = status

            filename = self.transfersmodel.get_value(iterator, 2)
            parts = filename.rsplit('.', 1)

            if len(parts) == 2:
                ext = parts[1]
                try:
                    extensions[ext.lower()] += 1
                except KeyError:
                    extensions[ext.lower()] = 1

            filecount += self.transfersmodel.get_value(iterator, 16)

            if status == "Filtered":
                # We don't want to count filtered files when calculating the progress
                iterator = self.transfersmodel.iter_next(iterator)
                continue

            elapsed += self.transfersmodel.get_value(iterator, 15)
            totalsize += self.transfersmodel.get_value(iterator, 12)
            position += self.transfersmodel.get_value(iterator, 13)

            if status == "Transferring":
                speed += float(self.transfersmodel.get_value(iterator, 14))
                left = self.transfersmodel.get_value(iterator, 9)

            if status in ("Transferring", "Banned", "Getting address",
                          "Establishing connection"):
                salientstatus = status

            iterator = self.transfersmodel.iter_next(iterator)

        if totalsize > 0:
            percent = min(((100 * position) / totalsize), 100)
        else:
            percent = 100

        if speed > 0:
            hspeed = human_speed(speed)
            left = self.frame.np.transfers.get_time(
                (totalsize - position) / speed)

        if elapsed > 0:
            helapsed = self.frame.np.transfers.get_time(elapsed)

        if not extensions:
            extensions = ""
        elif len(extensions) == 1:
            extensions = " (" + self.extension_list_template % {
                'ext': next(iter(extensions))
            } + ")"
        else:
            extensions = " (" + ", ".join(
                (str(count) + " " + ext
                 for (ext, count) in extensions.items())) + ")"

        self.transfersmodel.set_value(
            initer, 2,
            self.files_template % {'number': filecount} + extensions)
        self.transfersmodel.set_value(initer, 3,
                                      self.translate_status(salientstatus))
        self.transfersmodel.set_value(
            initer, 5, GObject.Value(GObject.TYPE_UINT64, percent))
        self.transfersmodel.set_value(
            initer, 6,
            "%s / %s" % (human_size(position), human_size(totalsize)))
        self.transfersmodel.set_value(initer, 7, hspeed)
        self.transfersmodel.set_value(initer, 8, helapsed)
        self.transfersmodel.set_value(initer, 9, left)
        self.transfersmodel.set_value(initer, 11, salientstatus)
        self.transfersmodel.set_value(
            initer, 12, GObject.Value(GObject.TYPE_UINT64, totalsize))
        self.transfersmodel.set_value(
            initer, 13, GObject.Value(GObject.TYPE_UINT64, position))
        self.transfersmodel.set_value(
            initer, 14, GObject.Value(GObject.TYPE_UINT64, speed))
        self.transfersmodel.set_value(
            initer, 15, GObject.Value(GObject.TYPE_UINT64, elapsed))
        self.transfersmodel.set_value(
            initer, 16, GObject.Value(GObject.TYPE_UINT64, filecount))

    def update_specific(self, transfer=None):

        currentbytes = transfer.currentbytes
        place = transfer.place or 0
        hplace = ""

        if place > 0:
            hplace = str(place)

        hspeed = helapsed = ""

        if currentbytes is None:
            currentbytes = 0

        status = transfer.status or ""
        hstatus = self.translate_status(status)

        try:
            size = int(transfer.size)
            if size < 0 or size > maxsize:
                size = 0
        except TypeError:
            size = 0

        hsize = "%s / %s" % (human_size(currentbytes), human_size(size))

        if transfer.modifier:
            hsize += " (%s)" % transfer.modifier

        speed = transfer.speed or 0
        elapsed = transfer.timeelapsed or 0
        left = transfer.timeleft or ""

        if speed > 0:
            speed = float(speed)
            hspeed = human_speed(speed)

        if elapsed > 0:
            helapsed = self.frame.np.transfers.get_time(elapsed)

        try:
            icurrentbytes = int(currentbytes)
            percent = min(((100 * icurrentbytes) / int(size)), 100)

        except Exception:
            icurrentbytes = 0
            percent = 100

        # Modify old transfer
        if transfer.iter is not None:
            initer = transfer.iter

            self.transfersmodel.set_value(initer, 3, hstatus)
            self.transfersmodel.set_value(initer, 4, hplace)
            self.transfersmodel.set_value(
                initer, 5, GObject.Value(GObject.TYPE_UINT64, percent))
            self.transfersmodel.set_value(initer, 6, hsize)
            self.transfersmodel.set_value(initer, 7, hspeed)
            self.transfersmodel.set_value(initer, 8, helapsed)
            self.transfersmodel.set_value(initer, 9, left)
            self.transfersmodel.set_value(initer, 11, status)
            self.transfersmodel.set_value(
                initer, 12, GObject.Value(GObject.TYPE_UINT64, size))
            self.transfersmodel.set_value(
                initer, 13, GObject.Value(GObject.TYPE_UINT64, currentbytes))
            self.transfersmodel.set_value(
                initer, 14, GObject.Value(GObject.TYPE_UINT64, speed))
            self.transfersmodel.set_value(
                initer, 15, GObject.Value(GObject.TYPE_UINT64, elapsed))
            self.transfersmodel.set_value(
                initer, 17, GObject.Value(GObject.TYPE_UINT64, place))

        else:
            fn = transfer.filename
            user = transfer.user
            shortfn = fn.split("\\")[-1]
            filecount = 1

            if self.tree_users != "ungrouped":
                # Group by folder or user

                empty_int = 0
                empty_str = ""

                if user not in self.users:
                    # Create Parent if it doesn't exist
                    # ProgressRender not visible (last column sets 4th column)
                    self.users[user] = self.transfersmodel.insert_with_values(
                        None, -1, self.column_numbers, [
                            user, empty_str, empty_str, empty_str, empty_str,
                            empty_int, empty_str, empty_str, empty_str,
                            empty_str, empty_str, empty_str, empty_int,
                            empty_int, empty_int, empty_int, filecount,
                            empty_int, lambda: None
                        ])

                parent = self.users[user]

                if self.tree_users == "folder_grouping":
                    # Group by folder
                    """ Paths can be empty if files are downloaded individually, make sure we
                    don't add files to the wrong user in the TreeView """
                    path = transfer.path
                    user_path = user + path
                    reverse_path = '/'.join(reversed(path.split('/')))

                    if user_path not in self.paths:
                        self.paths[
                            user_path] = self.transfersmodel.insert_with_values(
                                self.users[user], -1, self.column_numbers, [
                                    user, reverse_path, empty_str, empty_str,
                                    empty_str, empty_int, empty_str, empty_str,
                                    empty_str, empty_str, empty_str, empty_str,
                                    empty_int, empty_int, empty_int, empty_int,
                                    filecount, empty_int, lambda: None
                                ])

                    parent = self.paths[user_path]
            else:
                # No grouping
                # We use this list to get the total number of users
                self.users.setdefault(user, set()).add(transfer)
                parent = None

            # Add a new transfer
            if self.tree_users == "folder_grouping":
                # Group by folder, path not visible
                path = ""
            else:
                path = '/'.join(reversed(transfer.path.split('/')))

            iterator = self.transfersmodel.insert_with_values(
                parent, -1, self.column_numbers,
                (user, path, shortfn, hstatus, hplace,
                 GObject.Value(GObject.TYPE_UINT64,
                               percent), hsize, hspeed, helapsed, left, fn,
                 status, GObject.Value(GObject.TYPE_UINT64, size),
                 GObject.Value(GObject.TYPE_UINT64, icurrentbytes),
                 GObject.Value(GObject.TYPE_UINT64, speed),
                 GObject.Value(GObject.TYPE_UINT64, elapsed),
                 GObject.Value(GObject.TYPE_UINT64, filecount),
                 GObject.Value(GObject.TYPE_UINT64, place), transfer))
            transfer.iter = iterator

            # Expand path
            if parent is not None:
                transfer_path = self.transfersmodel.get_path(iterator)

                if self.tree_users == "folder_grouping":
                    # Group by folder, we need the user path to expand it
                    user_path = self.transfersmodel.get_path(self.users[user])
                else:
                    user_path = None

                self.expand(transfer_path, user_path)

    def retry_transfers(self):
        for transfer in self.selected_transfers:
            getattr(self.frame.np.transfers, "retry_" + self.type)(transfer)

    def abort_transfers(self, clear=False):

        for transfer in self.selected_transfers:
            if transfer.status != "Finished":
                self.frame.np.transfers.abort_transfer(transfer,
                                                       send_fail_message=True)

                if not clear:
                    transfer.status = "Aborted"
                    self.update(transfer)

            if clear:
                self.remove_specific(transfer)

    def remove_specific(self, transfer, cleartreeviewonly=False):

        user = transfer.user

        if user in self.users and not isinstance(self.users[user],
                                                 Gtk.TreeIter):
            # No grouping
            self.users[user].discard(transfer)

        if transfer in self.frame.np.transfers.transfer_request_times:
            del self.frame.np.transfers.transfer_request_times[transfer]

        if not cleartreeviewonly:
            self.list.remove(transfer)

        if transfer.iter is not None:
            self.transfersmodel.remove(transfer.iter)

        self.update_parent_rows(only_remove=True)

    def clear_transfers(self, status):

        for transfer in self.list.copy():
            if transfer.status in status:
                self.frame.np.transfers.abort_transfer(transfer,
                                                       send_fail_message=True)
                self.remove_specific(transfer)

    def clear(self):

        self.users.clear()
        self.paths.clear()
        self.selected_transfers = set()
        self.selected_users = set()
        self.transfersmodel.clear()

        if self.list is not None:
            for transfer in self.list:
                transfer.iter = None

    def double_click(self, event):

        self.select_transfers()
        dc = config.sections["transfers"]["%s_doubleclick" % self.type]

        if dc == 1:  # Send to player
            self.on_play_files()
        elif dc == 2:  # File manager
            self.on_open_directory()
        elif dc == 3:  # Search
            self.on_file_search()
        elif dc == 4:  # Abort
            self.abort_transfers()
        elif dc == 5:  # Clear
            self.abort_transfers(clear=True)
        elif dc == 6:  # Retry
            self.retry_transfers()

    def populate_popup_menu_users(self):

        self.popup_menu_users.clear()

        if not self.selected_users:
            return

        for user in self.selected_users:
            popup = PopupMenu(self.frame)
            popup.setup_user_menu(user)
            popup.setup(("", None), ("#" + _("Select User's Transfers"),
                                     self.on_select_user_transfers, user))

            popup.toggle_user_items()
            self.popup_menu_users.setup((">" + user, popup))

    def expand(self, transfer_path, user_path):

        if self.expand_button.get_active():
            self.widget.expand_to_path(transfer_path)

        elif user_path and self.tree_users == "folder_grouping":
            # Group by folder, show user folders in collapsed mode

            self.widget.expand_to_path(user_path)

    def on_expand_tree(self, widget):

        expand_button_icon = getattr(self.frame,
                                     "Expand%ssImage" % self.type.title())
        expanded = widget.get_active()

        if expanded:
            self.widget.expand_all()
            expand_button_icon.set_from_icon_name("go-up-symbolic",
                                                  Gtk.IconSize.BUTTON)
        else:
            collapse_treeview(self.widget, self.tree_users)
            expand_button_icon.set_from_icon_name("go-down-symbolic",
                                                  Gtk.IconSize.BUTTON)

        config.sections["transfers"]["%ssexpanded" % self.type] = expanded
        config.write_configuration()

    def on_toggle_tree(self, widget):

        active = widget.get_active()

        config.sections["transfers"]["group%ss" % self.type] = active
        self.widget.set_show_expanders(active)
        self.expand_button.set_visible(active)

        self.tree_users = widget.get_active_id()
        self.rebuild_transfers()

    def on_tooltip(self, widget, x, y, keyboard_mode, tooltip):
        return show_file_path_tooltip(widget, x, y, tooltip, 10)

    def on_popup_menu(self, *args):

        self.select_transfers()
        num_selected_transfers = len(self.selected_transfers)

        actions = self.popup_menu.get_actions()
        users = len(self.selected_users) > 0
        files = num_selected_transfers > 0

        actions[_("User(s)")].set_enabled(users)  # Users Menu
        self.populate_popup_menu_users()

        if files:
            act = True
        else:
            # Disable options
            # Send to player, File manager, file properties, Copy File Path, Copy URL, Copy Folder URL, Search filename
            act = False

        for i in (_("Send to _Player"), _("_Open Folder"),
                  _("File P_roperties"), _("Copy _File Path"), _("Copy _URL"),
                  _("Copy Folder URL"), _("_Search")):
            actions[i].set_enabled(act)

        if not users or not files:
            # Disable options
            # Retry, Abort, Clear
            act = False
        else:
            act = True

        for i in (_("_Retry"), _("Abor_t"), _("_Clear")):
            actions[i].set_enabled(act)

        self.popup_menu.set_num_selected_files(num_selected_transfers)

        self.popup_menu.popup()
        return True

    def on_list_clicked(self, widget, event):

        if triggers_context_menu(event):
            set_treeview_selected_row(widget, event)
            return self.on_popup_menu()

        if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
            self.double_click(event)
            return True

        return False

    def on_select_user_transfers(self, *args):

        if not self.selected_users:
            return

        selected_user = args[-1]

        sel = self.widget.get_selection()
        fmodel = self.widget.get_model()
        sel.unselect_all()

        iterator = fmodel.get_iter_first()

        select_user_row_iter(fmodel, sel, 0, selected_user, iterator)

        self.select_transfers()

    def on_key_press_event(self, widget, event):

        keycode = event.hardware_keycode
        self.select_transfers()

        if keycode in keyval_to_hardware_keycode(Gdk.KEY_t):
            self.abort_transfers()

        elif keycode in keyval_to_hardware_keycode(Gdk.KEY_r):
            self.retry_transfers()

        elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and \
                keycode in keyval_to_hardware_keycode(Gdk.KEY_c):
            self.on_copy_file_path()

        elif keycode in keyval_to_hardware_keycode(Gdk.KEY_Delete):
            self.abort_transfers(clear=True)

        else:
            # No key match, continue event
            return False

        widget.stop_emission_by_name("key_press_event")
        return True

    def on_file_properties(self, *args):

        if not self.frame.np.transfers:
            return

        data = []
        model, paths = self.widget.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            transfer = model.get_value(iterator, 18)

            if not isinstance(transfer, Transfer):
                continue

            user = model.get_value(iterator, 0)
            filename = model.get_value(iterator, 2)
            fullname = model.get_value(iterator, 10)
            size = speed = length = queue = immediate = num = country = bitratestr = ""

            size = str(human_size(transfer.size))

            if transfer.speed:
                speed = str(human_speed(transfer.speed))

            bitratestr = str(transfer.bitrate)
            length = str(transfer.length)

            directory = fullname.rsplit("\\", 1)[0]

            data.append({
                "user": user,
                "fn": fullname,
                "position": num,
                "filename": filename,
                "directory": directory,
                "size": size,
                "speed": speed,
                "queue": queue,
                "immediate": immediate,
                "bitrate": bitratestr,
                "length": length,
                "country": country
            })

        if paths:
            FileProperties(self.frame, data).show()

    def on_copy_file_path(self, *args):

        transfer = next(iter(self.selected_transfers), None)

        if transfer:
            self.frame.clipboard.set_text(transfer.filename, -1)

    def on_copy_url(self, *args):

        transfer = next(iter(self.selected_transfers), None)

        if transfer:
            copy_file_url(transfer.user, transfer.filename,
                          self.frame.clipboard)

    def on_copy_dir_url(self, *args):

        transfer = next(iter(self.selected_transfers), None)

        if transfer:
            copy_file_url(transfer.user,
                          transfer.filename.rsplit('\\', 1)[0] + '\\',
                          self.frame.clipboard)

    def on_retry_transfer(self, *args):
        self.select_transfers()
        self.retry_transfers()

    def on_abort_transfer(self, *args):
        self.select_transfers()
        self.abort_transfers()

    def on_clear_transfer(self, *args):
        self.select_transfers()
        self.abort_transfers(clear=True)

    def on_clear_response(self, dialog, response_id, data):

        dialog.destroy()

        if response_id == Gtk.ResponseType.OK:
            self.clear_transfers(["Queued"])

    def on_clear_queued(self, *args):
        self.clear_transfers(["Queued"])

    def on_clear_finished(self, *args):
        self.clear_transfers(["Finished"])
Beispiel #2
0
class Search(UserInterface):
    def __init__(self, searches, text, token, mode, mode_label, showtab):

        super().__init__("ui/search.ui")

        self.searches = searches
        self.frame = searches.frame
        self.filter_help = UserInterface("ui/popovers/searchfilters.ui")

        self.text = text
        self.searchterm_words_include = []
        self.searchterm_words_ignore = []

        for word in text.lower().split():
            if word.startswith('*'):
                if len(word) > 1:
                    self.searchterm_words_include.append(word[1:])

            elif word.startswith('-'):
                if len(word) > 1:
                    self.searchterm_words_ignore.append(word[1:])

            else:
                self.searchterm_words_include.append(word)

        self.token = token
        self.mode = mode
        self.mode_label = mode_label
        self.showtab = showtab
        self.usersiters = {}
        self.directoryiters = {}
        self.users = set()
        self.all_data = []
        self.selected_results = []
        self.selected_users = []
        self.selected_files_count = 0
        self.grouping_mode = None
        self.filters = None
        self.clearing_filters = False
        self.active_filter_count = 0
        self.num_results_found = 0
        self.num_results_visible = 0
        self.max_limit = config.sections["searches"]["max_displayed_results"]
        self.max_limited = False

        self.operators = {
            '<': operator.lt,
            '<=': operator.le,
            '==': operator.eq,
            '!=': operator.ne,
            '>=': operator.ge,
            '>': operator.gt
        }

        # Columns
        self.treeview_name = "file_search"
        self.resultsmodel = Gtk.TreeStore(
            int,  # (0)  num
            str,  # (1)  user
            str,  # (2)  flag
            str,  # (3)  h_speed
            str,  # (4)  h_queue
            str,  # (5)  directory
            str,  # (6)  filename
            str,  # (7)  h_size
            str,  # (8)  h_bitrate
            str,  # (9)  h_length
            GObject.TYPE_UINT,  # (10) bitrate
            str,  # (11) fullpath
            str,  # (12) country
            GObject.TYPE_UINT64,  # (13) size
            GObject.TYPE_UINT,  # (14) speed
            GObject.TYPE_UINT64,  # (15) queue
            GObject.TYPE_UINT,  # (16) length
            str  # (17) color
        )

        self.column_offsets = {}
        self.column_numbers = list(range(self.resultsmodel.get_n_columns()))
        color_col = 17
        self.cols = cols = initialise_columns(
            self.frame, "file_search", self.ResultsList,
            ["id", _("ID"), 50, "number", color_col],
            ["user", _("User"), 200, "text", color_col],
            ["country", _("Country"), 25, "icon", None],
            ["speed", _("Speed"), 100, "number", color_col],
            ["in_queue", _("In Queue"), 90, "number", color_col],
            ["folder", _("Folder"), 400, "text", color_col],
            ["filename", _("Filename"), 400, "text", color_col],
            ["size", _("Size"), 100, "number", color_col],
            ["bitrate", _("Bitrate"), 100, "number", color_col],
            ["length", _("Length"), 100, "number", color_col])

        cols["id"].set_sort_column_id(0)
        cols["user"].set_sort_column_id(1)
        cols["country"].set_sort_column_id(12)
        cols["speed"].set_sort_column_id(14)
        cols["in_queue"].set_sort_column_id(15)
        cols["folder"].set_sort_column_id(5)
        cols["filename"].set_sort_column_id(6)
        cols["size"].set_sort_column_id(13)
        cols["bitrate"].set_sort_column_id(10)
        cols["length"].set_sort_column_id(16)

        cols["country"].get_widget().hide()

        self.ResultsList.set_model(self.resultsmodel)

        for column in self.ResultsList.get_columns():
            self.column_offsets[column.get_title()] = 0
            column.connect("notify::x-offset", self.on_column_position_changed)

        self.update_visuals()

        # Popup menus
        self.popup_menu_users = PopupMenu(self.frame)

        self.popup_menu_copy = PopupMenu(self.frame)
        self.popup_menu_copy.add_items(
            ("#" + _("Copy _File Path"), self.on_copy_file_path),
            ("#" + _("Copy _URL"), self.on_copy_url),
            ("#" + _("Copy Folder U_RL"), self.on_copy_dir_url))

        self.popup_menu = PopupMenu(self.frame, self.ResultsList,
                                    self.on_popup_menu)
        self.popup_menu.add_items(
            ("#" + "selected_files", None), ("", None),
            ("#" + _("_Download File(s)"), self.on_download_files),
            ("#" + _("Download File(s) _To…"), self.on_download_files_to),
            ("#" + _("Download _Folder(s)"), self.on_download_folders),
            ("#" + _("Download F_older(s) To…"), self.on_download_folders_to),
            ("", None), ("#" + _("_Browse Folder(s)"), self.on_browse_folder),
            ("#" + _("F_ile Properties"), self.on_file_properties), ("", None),
            (">" + _("Copy"), self.popup_menu_copy),
            (">" + _("User(s)"), self.popup_menu_users))

        self.tab_menu = PopupMenu(self.frame)
        self.tab_menu.add_items(
            ("#" + _("Copy Search Term"), self.on_copy_search_term),
            ("", None), ("#" + _("Clear All Results"), self.on_clear),
            ("#" + _("Close All Tabs…"), self.on_close_all_tabs),
            ("#" + _("_Close Tab"), self.on_close))

        # Key bindings
        for widget in (self.Main, self.ResultsList):
            Accelerator("<Primary>f", widget,
                        self.on_show_filter_bar_accelerator)

        Accelerator("Escape", self.FiltersContainer,
                    self.on_close_filter_bar_accelerator)
        Accelerator("<Alt>Return", self.ResultsList,
                    self.on_file_properties_accelerator)

        # Grouping
        menu = create_grouping_menu(
            self.frame.MainWindow,
            config.sections["searches"]["group_searches"], self.on_group)
        self.ResultGrouping.set_menu_model(menu)

        self.ExpandButton.set_active(
            config.sections["searches"]["expand_searches"])

        # Filters
        self.filter_comboboxes = {
            "filterin": self.FilterIn,
            "filterout": self.FilterOut,
            "filtersize": self.FilterSize,
            "filterbr": self.FilterBitrate,
            "filtercc": self.FilterCountry,
            "filtertype": self.FilterType
        }

        self.ShowFilters.set_active(
            config.sections["searches"]["filters_visible"])
        self.populate_filters()

        # Wishlist
        self.update_wish_button()

    def set_label(self, label):
        self.tab_menu.set_parent(label)

    @staticmethod
    def on_tooltip(widget, pos_x, pos_y, _keyboard_mode, tooltip):

        country_tooltip = show_country_tooltip(widget,
                                               pos_x,
                                               pos_y,
                                               tooltip,
                                               12,
                                               strip_prefix="")
        file_path_tooltip = show_file_path_tooltip(widget, pos_x, pos_y,
                                                   tooltip, 11)

        if country_tooltip:
            return country_tooltip

        if file_path_tooltip:
            return file_path_tooltip

        return False

    def focus_combobox(self, button):

        # We have the button of a combobox, find the entry
        parent = button.get_parent()

        if parent is None:
            return

        if isinstance(parent, Gtk.ComboBox):
            entry = parent.get_child()
            entry.grab_focus()
            GLib.idle_add(entry.emit, "activate")
            return

        self.focus_combobox(parent)

    def update_filter_comboboxes(self):

        for filter_id, widget in self.filter_comboboxes.items():
            presets = ""
            widget.remove_all()

            if filter_id == "filterbr":
                presets = ("0", "128", "160", "192", "256", "320")

            elif filter_id == "filtersize":
                presets = (">10MiB", "<10MiB", "<5MiB", "<1MiB", ">0")

            elif filter_id == "filtertype":
                presets = ("flac|wav|ape|aiff|wv|cue",
                           "mp3|m4a|aac|ogg|opus|wma", "!mp3")

            for value in presets:
                widget.append_text(value)

            for value in config.sections["searches"][filter_id]:
                if value not in presets:
                    widget.append_text(value)

    def populate_filters(self):

        if not config.sections["searches"]["enablefilters"]:
            return

        sfilter = config.sections["searches"]["defilter"]
        num_filters = len(sfilter)

        if num_filters > 0:
            self.FilterIn.get_child().set_text(str(sfilter[0]))

        if num_filters > 1:
            self.FilterOut.get_child().set_text(str(sfilter[1]))

        if num_filters > 2:
            self.FilterSize.get_child().set_text(str(sfilter[2]))

        if num_filters > 3:
            self.FilterBitrate.get_child().set_text(str(sfilter[3]))

        if num_filters > 4:
            self.FilterFreeSlot.set_active(bool(sfilter[4]))

        if num_filters > 5:
            self.FilterCountry.get_child().set_text(str(sfilter[5]))

        if num_filters > 6:
            self.FilterType.get_child().set_text(str(sfilter[6]))

        self.on_refilter()

    def add_result_list(self,
                        result_list,
                        user,
                        country,
                        inqueue,
                        ulspeed,
                        h_speed,
                        h_queue,
                        color,
                        private=False):
        """ Adds a list of search results to the treeview. Lists can either contain publicly or
        privately shared files. """

        update_ui = False

        for result in result_list:
            if self.num_results_found >= self.max_limit:
                self.max_limited = True
                break

            fullpath = result[1]
            fullpath_lower = fullpath.lower()

            if any(word in fullpath_lower
                   for word in self.searchterm_words_ignore):
                # Filter out results with filtered words (e.g. nicotine -music)
                log.add_debug((
                    "Filtered out excluded search result %(filepath)s from user %(user)s for "
                    "search term \"%(query)s\""), {
                        "filepath": fullpath,
                        "user": user,
                        "query": self.text
                    })
                continue

            if not any(word in fullpath_lower
                       for word in self.searchterm_words_include):
                # Certain users may send us wrong results, filter out such ones
                log.add_search(
                    _("Filtered out incorrect search result %(filepath)s from user %(user)s for "
                      "search query \"%(query)s\""), {
                          "filepath": fullpath,
                          "user": user,
                          "query": self.text
                      })
                continue

            self.num_results_found += 1
            fullpath_split = fullpath.split('\\')

            if config.sections["ui"]["reverse_file_paths"]:
                # Reverse file path, file name is the first item. next() retrieves the name and removes
                # it from the iterator.
                fullpath_split = reversed(fullpath_split)
                name = next(fullpath_split)

            else:
                # Regular file path, file name is the last item. Retrieve it and remove it from the list.
                name = fullpath_split.pop()

            # Join the resulting items into a folder path
            directory = '\\'.join(fullpath_split)

            size = result[2]
            h_size = human_size(size)
            h_bitrate, bitrate, h_length, length = get_result_bitrate_length(
                size, result[4])

            if private:
                name = _("[PRIVATE]  %s") % name

            is_result_visible = self.append([
                self.num_results_found, user,
                get_flag_icon_name(country), h_speed, h_queue, directory, name,
                h_size, h_bitrate, h_length,
                GObject.Value(GObject.TYPE_UINT, bitrate), fullpath, country,
                GObject.Value(GObject.TYPE_UINT64, size),
                GObject.Value(GObject.TYPE_UINT, ulspeed),
                GObject.Value(GObject.TYPE_UINT64, inqueue),
                GObject.Value(GObject.TYPE_UINT, length),
                GObject.Value(GObject.TYPE_STRING, color)
            ])

            if is_result_visible:
                update_ui = True

        return update_ui

    def add_user_results(self, msg, user, country):

        if user in self.users:
            return

        self.users.add(user)

        if msg.freeulslots:
            inqueue = 0
            h_queue = ""
        else:
            inqueue = msg.inqueue or 1  # Ensure value is always >= 1
            h_queue = humanize(inqueue)

        h_speed = ""
        ulspeed = msg.ulspeed or 0

        if ulspeed > 0:
            h_speed = human_speed(ulspeed)

        color_id = "search" if msg.freeulslots else "searchq"
        color = config.sections["ui"][color_id] or None

        update_ui = self.add_result_list(msg.list, user, country, inqueue,
                                         ulspeed, h_speed, h_queue, color)

        if msg.privatelist:
            update_ui_private = self.add_result_list(msg.privatelist,
                                                     user,
                                                     country,
                                                     inqueue,
                                                     ulspeed,
                                                     h_speed,
                                                     h_queue,
                                                     color,
                                                     private=True)

            if not update_ui and update_ui_private:
                update_ui = True

        if update_ui:
            # If this search wasn't initiated by us (e.g. wishlist), and the results aren't spoofed, show tab
            if not self.showtab:
                self.searches.show_tab(self, self.text)
                self.showtab = True

            self.searches.request_tab_hilite(self.Main)

        # Update number of results, even if they are all filtered
        self.update_result_counter()

    def append(self, row):

        self.all_data.append(row)

        if not self.check_filter(row):
            return False

        self.add_row_to_model(row)
        return True

    def add_row_to_model(self, row):
        (_counter, user, flag, h_speed, h_queue, directory, _filename, _h_size,
         _h_bitrate, _h_length, _bitrate, fullpath, country, _size, speed,
         queue, _length, color) = row

        expand_user = False
        expand_folder = False

        if self.grouping_mode != "ungrouped":
            # Group by folder or user

            empty_int = 0
            empty_str = ""

            if user not in self.usersiters:
                self.usersiters[user] = self.resultsmodel.insert_with_values(
                    None, -1, self.column_numbers, [
                        empty_int, user, flag, h_speed, h_queue, empty_str,
                        empty_str, empty_str, empty_str, empty_str, empty_int,
                        empty_str, country, empty_int, speed, queue, empty_int,
                        color
                    ])

                if self.grouping_mode == "folder_grouping":
                    expand_user = True
                else:
                    expand_user = self.ExpandButton.get_active()

            parent = self.usersiters[user]

            if self.grouping_mode == "folder_grouping":
                # Group by folder

                user_directory = user + directory

                if user_directory not in self.directoryiters:
                    self.directoryiters[
                        user_directory] = self.resultsmodel.insert_with_values(
                            self.usersiters[user], -1, self.column_numbers, [
                                empty_int, user, flag, h_speed, h_queue,
                                directory, empty_str, empty_str, empty_str,
                                empty_str, empty_int,
                                fullpath.rsplit('\\', 1)[0] + '\\', country,
                                empty_int, speed, queue, empty_int, color
                            ])
                    expand_folder = self.ExpandButton.get_active()

                row = row[:]
                row[5] = ""  # Directory not visible for file row if "group by folder" is enabled

                parent = self.directoryiters[user_directory]
        else:
            parent = None

        try:
            """ Note that we use insert_with_values instead of append, as this reduces
            overhead by bypassing useless row conversion to GObject.Value in PyGObject. """

            iterator = self.resultsmodel.insert_with_values(
                parent, -1, self.column_numbers, row)

            if expand_user:
                self.ResultsList.expand_row(
                    self.resultsmodel.get_path(self.usersiters[user]), False)

            if expand_folder:
                self.ResultsList.expand_row(
                    self.resultsmodel.get_path(
                        self.directoryiters[user_directory]), False)

            self.num_results_visible += 1

        except Exception as error:
            types = []

            for i in row:
                types.append(type(i))

            log.add("Search row error: %(exception)s %(row)s", {
                'exception': error,
                'row': row
            })
            iterator = None

        return iterator

    def check_digit(self, sfilter, value, factorize=True):

        used_operator = ">="
        if sfilter.startswith((">", "<", "=")):
            used_operator, sfilter = sfilter[:1] + "=", sfilter[1:]

        if not sfilter:
            return True

        factor = 1
        if factorize:
            base = 1024  # Default to binary for "k", "m", "g" suffixes

            if sfilter[-1:].lower() == 'b':
                base = 1000  # Byte suffix detected, prepare to use decimal if necessary
                sfilter = sfilter[:-1]

            if sfilter[-1:].lower() == 'i':
                base = 1024  # Binary requested, stop using decimal
                sfilter = sfilter[:-1]

            if sfilter.lower()[-1:] == "g":
                factor = pow(base, 3)
                sfilter = sfilter[:-1]

            elif sfilter.lower()[-1:] == "m":
                factor = pow(base, 2)
                sfilter = sfilter[:-1]

            elif sfilter.lower()[-1:] == "k":
                factor = base
                sfilter = sfilter[:-1]

        if not sfilter:
            return True

        try:
            sfilter = int(sfilter) * factor
        except ValueError:
            return True

        operation = self.operators.get(used_operator)
        return operation(value, sfilter)

    @staticmethod
    def check_country(sfilter, value):

        if not isinstance(value, str):
            return False

        value = value.upper()
        allowed = False

        for country_code in sfilter.split("|"):
            if country_code == value:
                allowed = True

            elif country_code.startswith("!") and country_code[1:] != value:
                allowed = True

            elif country_code.startswith("!") and country_code[1:] == value:
                return False

        return allowed

    @staticmethod
    def check_file_type(sfilter, value):

        if not isinstance(value, str):
            return False

        value = value.lower()
        allowed = False

        for ext in sfilter.split("|"):
            exclude_ext = None

            if ext.startswith("!"):
                exclude_ext = ext[1:]

                if not exclude_ext.startswith("."):
                    exclude_ext = "." + exclude_ext

            elif not ext.startswith("."):
                ext = "." + ext

            if not ext.startswith("!") and value.endswith(ext):
                allowed = True

            elif ext.startswith("!") and not value.endswith(exclude_ext):
                allowed = True

            elif ext.startswith("!") and value.endswith(exclude_ext):
                return False

        return allowed

    def check_filter(self, row):

        if self.active_filter_count == 0:
            return True

        filters = self.filters

        # "Included text"-filter, check full file path (located at index 11 in row)
        if filters["filterin"] and not filters["filterin"].search(
                row[11].lower()):
            return False

        # "Excluded text"-filter, check full file path (located at index 11 in row)
        if filters["filterout"] and filters["filterout"].search(
                row[11].lower()):
            return False

        if filters["filtersize"] and not self.check_digit(
                filters["filtersize"], row[13].get_uint64()):
            return False

        if filters["filterbr"] and not self.check_digit(
                filters["filterbr"], row[10].get_uint(), False):
            return False

        if filters["filterslot"] and row[15].get_uint64() > 0:
            return False

        if filters["filtercc"] and not self.check_country(
                filters["filtercc"], row[12]):
            return False

        if filters["filtertype"] and not self.check_file_type(
                filters["filtertype"], row[11]):
            return False

        return True

    def update_filter_counter(self, count):

        if count > 0:
            self.FilterLabel.set_label(_("_Result Filters [%d]") % count)
        else:
            self.FilterLabel.set_label(_("_Result Filters"))

        self.FilterLabel.set_tooltip_text("%d active filter(s)" % count)

    def update_results_model(self):

        # Temporarily disable sorting for increased performance
        sort_column, sort_type = self.resultsmodel.get_sort_column_id()
        self.resultsmodel.set_default_sort_func(lambda *_args: 0)
        self.resultsmodel.set_sort_column_id(-1, Gtk.SortType.ASCENDING)

        self.usersiters.clear()
        self.directoryiters.clear()
        self.resultsmodel.clear()
        self.num_results_visible = 0

        for row in self.all_data:
            if self.check_filter(row):
                self.add_row_to_model(row)

        # Update number of results
        self.update_result_counter()
        self.update_filter_counter(self.active_filter_count)

        if sort_column is not None and sort_type is not None:
            self.resultsmodel.set_sort_column_id(sort_column, sort_type)

        if self.grouping_mode != "ungrouped":
            # Group by folder or user

            if self.ExpandButton.get_active():
                self.ResultsList.expand_all()
            else:
                collapse_treeview(self.ResultsList, self.grouping_mode)

    def update_wish_button(self):

        if self.mode not in ("global", "wishlist"):
            self.AddWish.hide()
            return

        if not self.frame.np.search.is_wish(self.text):
            self.AddWishIcon.set_property("icon-name", "list-add-symbolic")
            self.AddWishLabel.set_label(_("Add Wi_sh"))
            return

        self.AddWishIcon.set_property("icon-name", "list-remove-symbolic")
        self.AddWishLabel.set_label(_("Remove Wi_sh"))

    def on_add_wish(self, *_args):

        if self.frame.np.search.is_wish(self.text):
            self.frame.np.search.remove_wish(self.text)
        else:
            self.frame.np.search.add_wish(self.text)

    def add_popup_menu_user(self, popup, user):

        popup.setup_user_menu(user)
        popup.add_items(("", None), ("#" + _("Select User's Results"),
                                     self.on_select_user_results, user))
        popup.update_model()
        popup.toggle_user_items()

    def populate_popup_menu_users(self):

        self.popup_menu_users.clear()

        if not self.selected_users:
            return

        # Multiple users, create submenus for each user
        if len(self.selected_users) > 1:
            for user in self.selected_users:
                popup = PopupMenu(self.frame)
                self.add_popup_menu_user(popup, user)
                self.popup_menu_users.add_items((">" + user, popup))
                self.popup_menu_users.update_model()
            return

        # Single user, add items directly to "User(s)" submenu
        self.add_popup_menu_user(self.popup_menu_users, self.selected_users[0])

    def on_close_filter_bar_accelerator(self, *_args):
        """ Escape: hide filter bar """

        self.ShowFilters.set_active(False)
        return True

    def on_show_filter_bar_accelerator(self, *_args):
        """ Ctrl+F: show filter bar """

        self.ShowFilters.set_active(True)
        self.FilterIn.grab_focus()
        return True

    def on_file_properties_accelerator(self, *_args):
        """ Alt+Return: show file properties dialog """

        self.on_file_properties()
        return True

    def on_select_user_results(self, *args):

        if not self.selected_users:
            return

        selected_user = args[-1]

        sel = self.ResultsList.get_selection()
        fmodel = self.ResultsList.get_model()
        sel.unselect_all()

        iterator = fmodel.get_iter_first()

        select_user_row_iter(fmodel, sel, 1, selected_user, iterator)

        self.select_results()

    def select_result(self, model, iterator):

        user = model.get_value(iterator, 1)

        if user not in self.selected_users:
            self.selected_users.append(user)

        filename = model.get_value(iterator, 6)

        if not filename:
            return

        path = self.resultsmodel.get_path(iterator)

        if path not in self.selected_results:
            self.selected_files_count += 1
            self.selected_results.append(path)

    def select_child_results(self, model, iterator):

        while iterator is not None:
            self.select_result(model, iterator)
            self.select_child_results(model, model.iter_children(iterator))

            iterator = model.iter_next(iterator)

    def select_results(self):

        self.selected_results.clear()
        self.selected_users.clear()
        self.selected_files_count = 0

        model, paths = self.ResultsList.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            self.select_result(model, iterator)
            self.select_child_results(model, model.iter_children(iterator))

    def update_result_counter(self):

        if self.max_limited or self.num_results_found > self.num_results_visible:
            # Append plus symbol "+" if Results are Filtered and/or reached 'Maximum per search'
            str_plus = "+"

            # Display total results on the tooltip, but only if we know the exact number of results
            if self.max_limited:
                total = "> " + str(self.max_limit) + "+"
            else:
                total = self.num_results_found

            self.CounterButton.set_tooltip_text(_("Total: %s") % total)

        else:  # Hide the tooltip if there are no hidden results
            str_plus = ""
            self.CounterButton.set_has_tooltip(False)

        self.Counter.set_text(str(self.num_results_visible) + str_plus)

    def update_visuals(self):

        for widget in list(self.__dict__.values()):
            update_widget_visuals(widget, list_font_target="searchfont")

    def on_column_position_changed(self, column, _param):
        """ Save column position and width to config """

        col_title = column.get_title()
        offset = column.get_x_offset()

        if self.column_offsets[col_title] == offset:
            return

        self.column_offsets[col_title] = offset
        save_columns(self.treeview_name, self.ResultsList.get_columns())

    def on_row_activated(self, treeview, path, _column):

        self.select_results()

        iterator = self.resultsmodel.get_iter(path)
        folder = self.resultsmodel.get_value(iterator, 5)
        filename = self.resultsmodel.get_value(iterator, 6)

        if not folder and not filename:
            # Don't activate user rows
            return

        if not filename:
            self.on_download_folders()
        else:
            self.on_download_files()

        treeview.get_selection().unselect_all()

    def on_popup_menu(self, menu, _widget):

        self.select_results()
        self.populate_popup_menu_users()
        menu.set_num_selected_files(self.selected_files_count)

    def on_browse_folder(self, *_args):

        requested_users = set()
        requested_folders = set()

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            user = self.resultsmodel.get_value(iterator, 1)
            folder = self.resultsmodel.get_value(iterator, 11).rsplit(
                '\\', 1)[0] + '\\'

            if user not in requested_users and folder not in requested_folders:
                self.frame.np.userbrowse.browse_user(user, path=folder)

                requested_users.add(user)
                requested_folders.add(folder)

    def on_file_properties(self, *_args):

        data = []
        selected_size = 0
        selected_length = 0

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            virtual_path = self.resultsmodel.get_value(iterator, 11)
            directory, filename = virtual_path.rsplit('\\', 1)
            file_size = self.resultsmodel.get_value(iterator, 13)
            selected_size += file_size
            selected_length += self.resultsmodel.get_value(iterator, 16)
            country_code = self.resultsmodel.get_value(iterator, 12)
            country = "%s (%s)" % (self.frame.np.geoip.country_code_to_name(
                country_code), country_code)

            data.append({
                "user":
                self.resultsmodel.get_value(iterator, 1),
                "fn":
                virtual_path,
                "filename":
                filename,
                "directory":
                directory,
                "size":
                file_size,
                "speed":
                self.resultsmodel.get_value(iterator, 14),
                "queue_position":
                self.resultsmodel.get_value(iterator, 15),
                "bitrate":
                self.resultsmodel.get_value(iterator, 8),
                "length":
                self.resultsmodel.get_value(iterator, 9),
                "country":
                country
            })

        if data:
            FileProperties(self.frame, data, selected_size,
                           selected_length).show()

    def on_download_files(self, *_args, prefix=""):

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            user = self.resultsmodel.get_value(iterator, 1)
            filepath = self.resultsmodel.get_value(iterator, 11)
            size = self.resultsmodel.get_value(iterator, 13)
            bitrate = self.resultsmodel.get_value(iterator, 8)
            length = self.resultsmodel.get_value(iterator, 9)

            self.frame.np.transfers.get_file(user,
                                             filepath,
                                             prefix,
                                             size=size,
                                             bitrate=bitrate,
                                             length=length)

    def on_download_files_to_selected(self, selected, _data):
        self.on_download_files(prefix=selected)

    def on_download_files_to(self, *_args):

        choose_dir(parent=self.frame.MainWindow,
                   title=_("Select Destination Folder for File(s)"),
                   callback=self.on_download_files_to_selected,
                   initialdir=config.sections["transfers"]["downloaddir"],
                   multichoice=False)

    def on_download_folders(self, *_args, download_location=""):

        if download_location:
            """ Custom download location specified, remember it when peer sends a folder
            contents reply """

            requested_folders = self.frame.np.transfers.requested_folders
        else:
            requested_folders = defaultdict(dict)

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            user = self.resultsmodel.get_value(iterator, 1)
            folder = self.resultsmodel.get_value(iterator, 11).rsplit('\\',
                                                                      1)[0]

            if folder in requested_folders[user]:
                """ Ensure we don't send folder content requests for a folder more than once,
                e.g. when several selected resuls belong to the same folder. """
                continue

            requested_folders[user][folder] = download_location

            visible_files = []
            for row in self.all_data:

                # Find the wanted directory
                if folder != row[11].rsplit('\\', 1)[0]:
                    continue

                destination = self.frame.np.transfers.get_folder_destination(
                    user, folder)
                (_counter, user, _flag, _h_speed, _h_queue, _directory,
                 _filename, _h_size, h_bitrate, h_length, _bitrate, fullpath,
                 _country, size, _speed, _queue, _length, _color) = row
                visible_files.append((user, fullpath, destination,
                                      size.get_uint64(), h_bitrate, h_length))

            self.frame.np.search.request_folder_download(
                user, folder, visible_files)

    def on_download_folders_to_selected(self, selected, _data):
        self.on_download_folders(download_location=selected)

    def on_download_folders_to(self, *_args):

        choose_dir(parent=self.frame.MainWindow,
                   title=_("Select Destination Folder"),
                   callback=self.on_download_folders_to_selected,
                   initialdir=config.sections["transfers"]["downloaddir"],
                   multichoice=False)

    def on_copy_file_path(self, *_args):

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            filepath = self.resultsmodel.get_value(iterator, 11)
            copy_text(filepath)
            return

    def on_copy_url(self, *_args):

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            user = self.resultsmodel.get_value(iterator, 1)
            filepath = self.resultsmodel.get_value(iterator, 11)
            url = self.frame.np.userbrowse.get_soulseek_url(user, filepath)
            copy_text(url)
            return

    def on_copy_dir_url(self, *_args):

        for path in self.selected_results:
            iterator = self.resultsmodel.get_iter(path)

            user = self.resultsmodel.get_value(iterator, 1)
            filepath = self.resultsmodel.get_value(iterator, 11)
            url = self.frame.np.userbrowse.get_soulseek_url(
                user,
                filepath.rsplit('\\', 1)[0] + '\\')
            copy_text(url)
            return

    def on_counter_button(self, *_args):

        if self.num_results_found > self.num_results_visible:
            self.on_clear_filters()
        else:
            self.frame.on_settings(page='Searches')

    def on_group(self, action, state):

        mode = state.get_string()
        active = mode != "ungrouped"

        config.sections["searches"]["group_searches"] = mode
        self.cols["id"].set_visible(not active)
        self.ResultsList.set_show_expanders(active)
        self.ExpandButton.set_visible(active)

        self.grouping_mode = mode
        self.update_results_model()

        action.set_state(state)

    def on_toggle_expand_all(self, *_args):

        active = self.ExpandButton.get_active()

        if active:
            self.ResultsList.expand_all()
            self.expand.set_property("icon-name", "go-up-symbolic")
        else:
            collapse_treeview(self.ResultsList, self.grouping_mode)
            self.expand.set_property("icon-name", "go-down-symbolic")

        config.sections["searches"]["expand_searches"] = active

    def on_toggle_filters(self, widget):

        visible = widget.get_active()
        self.FiltersContainer.set_reveal_child(visible)
        config.sections["searches"]["filters_visible"] = visible

        if visible:
            self.FilterIn.grab_focus()
            return

        self.ResultsList.grab_focus()

    def on_copy_search_term(self, *_args):
        copy_text(self.text)

    @staticmethod
    def push_history(filter_id, value):

        if not value:
            return

        history = config.sections["searches"].get(filter_id)

        if history is None:
            return

        if value in history:
            history.remove(value)

        elif len(history) >= 5:
            del history[-1]

        history.insert(0, value)
        config.write_configuration()

    def on_refilter(self, *_args):

        if self.clearing_filters:
            return

        filter_in = self.FilterIn.get_active_text().strip().lower()
        filter_out = self.FilterOut.get_active_text().strip().lower()

        if filter_in:
            try:
                filter_in = re.compile(filter_in)
            except sre_constants.error:
                filter_in = None

        if filter_out:
            try:
                filter_out = re.compile(filter_out)
            except sre_constants.error:
                filter_out = None

        filters = {
            "filterin": filter_in,
            "filterout": filter_out,
            "filtersize": self.FilterSize.get_active_text().strip(),
            "filterbr": self.FilterBitrate.get_active_text().strip(),
            "filterslot": self.FilterFreeSlot.get_active(),
            "filtercc": self.FilterCountry.get_active_text().strip().upper(),
            "filtertype": self.FilterType.get_active_text().strip().lower()
        }

        if self.filters == filters:
            # Filters have not changed, no need to refilter
            return

        self.active_filter_count = 0

        # Set red background if invalid regex pattern is detected
        if filter_in is None:
            set_widget_fg_bg_css(self.FilterIn.get_child(),
                                 bg_color="#e04f5e",
                                 fg_color="white")
        else:
            update_widget_visuals(self.FilterIn.get_child())

        if filter_out is None:
            set_widget_fg_bg_css(self.FilterOut.get_child(),
                                 bg_color="#e04f5e",
                                 fg_color="white")
        else:
            update_widget_visuals(self.FilterOut.get_child())

        # Add filters to history
        for filter_id, value in filters.items():
            try:
                value = value.pattern
            except AttributeError:
                pass

            if not value:
                continue

            self.push_history(filter_id, value)
            self.active_filter_count += 1

        # Apply the new filters
        self.filters = filters
        self.update_filter_comboboxes()
        self.update_results_model()

    def on_filter_entry_changed(self, widget):
        if not widget.get_text():
            self.on_refilter()

    def on_clear_filters(self, *_args):

        self.clearing_filters = True

        for widget in self.filter_comboboxes.values():
            widget.get_child().set_text("")

        self.FilterFreeSlot.set_active(False)

        if self.ShowFilters.get_active():
            self.FilterIn.get_child().grab_focus()
        else:
            self.ResultsList.grab_focus()

        self.clearing_filters = False
        self.on_refilter()

    def on_clear(self, *_args):

        self.all_data = []
        self.usersiters.clear()
        self.directoryiters.clear()
        self.resultsmodel.clear()
        self.num_results_found = 0
        self.num_results_visible = 0
        self.max_limited = False
        self.max_limit = config.sections["searches"]["max_displayed_results"]

        # Allow parsing search result messages again
        self.frame.np.search.add_allowed_token(self.token)

        # Update number of results widget
        self.update_result_counter()

    def on_close(self, *_args):

        del self.searches.pages[self.token]
        self.frame.np.search.remove_search(self.token)
        self.searches.remove_page(self.Main)

    def on_close_all_tabs(self, *_args):
        self.searches.remove_all_pages()
Beispiel #3
0
class Search:
    def __init__(self, searches, text, id, mode, remember, showtab):

        self.searches = searches
        self.frame = searches.frame

        # Build the window
        load_ui_elements(self,
                         os.path.join(self.frame.gui_dir, "ui", "search.ui"))

        self.text = text
        self.searchterm_words_include = [
            p for p in text.lower().split() if not p.startswith('-')
        ]
        self.searchterm_words_ignore = [
            p[1:] for p in text.lower().split()
            if p.startswith('-') and len(p) > 1
        ]

        self.id = id
        self.mode = mode
        self.remember = remember
        self.showtab = showtab
        self.usersiters = {}
        self.directoryiters = {}
        self.users = set()
        self.all_data = []
        self.filters = None
        self.clearing_filters = False
        self.resultslimit = 2000
        self.numvisibleresults = 0
        self.active_filter_count = 0

        self.operators = {
            '<': operator.lt,
            '<=': operator.le,
            '==': operator.eq,
            '!=': operator.ne,
            '>=': operator.ge,
            '>': operator.gt
        }

        if mode not in ("global", "wishlist"):
            self.RememberCheckButton.hide()

        self.RememberCheckButton.set_active(remember)
        """ Columns """

        self.resultsmodel = Gtk.TreeStore(
            GObject.TYPE_UINT64,  # (0)  num
            str,  # (1)  user
            GObject.TYPE_OBJECT,  # (2)  flag
            str,  # (3)  immediatedl
            str,  # (4)  h_speed
            str,  # (5)  h_queue
            str,  # (6)  directory
            str,  # (7)  filename
            str,  # (8)  h_size
            str,  # (9)  h_bitrate
            str,  # (10) h_length
            GObject.TYPE_UINT64,  # (11) bitrate
            str,  # (12) fullpath
            str,  # (13) country
            GObject.TYPE_UINT64,  # (14) size
            GObject.TYPE_UINT64,  # (15) speed
            GObject.TYPE_UINT64,  # (16) queue
            GObject.TYPE_UINT64,  # (17) length
            str  # (18) color
        )

        self.column_numbers = list(range(self.resultsmodel.get_n_columns()))
        color_col = 18
        self.cols = cols = initialise_columns(
            "file_search", self.ResultsList,
            ["id", _("ID"), 50, "text", color_col],
            ["user", _("User"), 200, "text", color_col],
            ["country", _("Country"), 25, "pixbuf", None], [
                "immediate_download",
                _("Immediate Download"), 50, "center", color_col
            ], ["speed", _("Speed"), 90, "number", color_col],
            ["in_queue", _("In Queue"), 90, "center", color_col],
            ["folder", _("Folder"), 400, "text", color_col],
            ["filename", _("Filename"), 400, "text", color_col],
            ["size", _("Size"), 100, "number", color_col],
            ["bitrate", _("Bitrate"), 100, "number", color_col],
            ["length", _("Length"), 100, "number", color_col])

        cols["id"].set_sort_column_id(0)
        cols["user"].set_sort_column_id(1)
        cols["country"].set_sort_column_id(13)
        cols["immediate_download"].set_sort_column_id(3)
        cols["speed"].set_sort_column_id(15)
        cols["in_queue"].set_sort_column_id(16)
        cols["folder"].set_sort_column_id(6)
        cols["filename"].set_sort_column_id(7)
        cols["size"].set_sort_column_id(14)
        cols["bitrate"].set_sort_column_id(11)
        cols["length"].set_sort_column_id(17)

        cols["country"].get_widget().hide()

        self.ResultsList.set_model(self.resultsmodel)

        self.update_visuals()
        """ Filters """

        self.ShowFilters.set_active(
            config.sections["searches"]["filters_visible"])
        self.populate_filters()
        """ Popup """

        self.popup_menu_users = PopupMenu(self.frame)

        self.popup_menu = PopupMenu(self.frame)
        self.popup_menu.setup(
            ("#" + "selected_files", None), ("", None),
            ("#" + _("_Download File(s)"), self.on_download_files),
            ("#" + _("Download File(s) _To..."), self.on_download_files_to),
            ("#" + _("Download _Folder(s)"), self.on_download_folders),
            ("#" + _("Download F_older(s) To..."),
             self.on_download_folders_to),
            ("#" + _("_Browse Folder"), self.on_browse_folder),
            ("#" + _("File _Properties"), self.on_file_properties), ("", None),
            ("#" + _("Copy _File Path"), self.on_copy_file_path),
            ("#" + _("Copy _URL"), self.on_copy_url),
            ("#" + _("Copy Folder U_RL"), self.on_copy_dir_url), ("", None),
            (">" + _("User(s)"), self.popup_menu_users))

        self.tab_menu = PopupMenu(self.frame)
        self.tab_menu.setup(
            ("#" + _("Copy Search Term"), self.on_copy_search_term),
            ("", None), ("#" + _("Clear All Results"), self.on_clear),
            ("#" + _("Close All Tabs"), self.on_close_all_tabs),
            ("#" + _("_Close Tab"), self.on_close))
        """ Grouping """

        self.ResultGrouping.set_active(
            config.sections["searches"]["group_searches"])
        self.ExpandButton.set_active(
            config.sections["searches"]["expand_searches"])

    def on_tooltip(self, widget, x, y, keyboard_mode, tooltip):

        country_tooltip = show_country_tooltip(widget,
                                               x,
                                               y,
                                               tooltip,
                                               13,
                                               strip_prefix="")
        file_path_tooltip = show_file_path_tooltip(widget, x, y, tooltip, 12)

        if country_tooltip:
            return country_tooltip

        elif file_path_tooltip:
            return file_path_tooltip

    def populate_filters(self, set_default_filters=True):

        for combobox in (self.FilterIn, self.FilterOut, self.FilterType,
                         self.FilterSize, self.FilterBitrate,
                         self.FilterCountry):
            combobox.remove_all()

        if set_default_filters and config.sections["searches"]["enablefilters"]:

            sfilter = config.sections["searches"]["defilter"]

            self.FilterInEntry.set_text(str(sfilter[0]))
            self.FilterOutEntry.set_text(str(sfilter[1]))
            self.FilterSizeEntry.set_text(str(sfilter[2]))
            self.FilterBitrateEntry.set_text(str(sfilter[3]))
            self.FilterFreeSlot.set_active(sfilter[4])

            if len(sfilter) > 5:
                self.FilterCountryEntry.set_text(str(sfilter[5]))

            if len(sfilter) > 6:
                self.FilterTypeEntry.set_text(str(sfilter[6]))

            self.on_refilter(None)

        for i in ['0', '128', '160', '192', '256', '320']:
            self.FilterBitrate.append_text(i)

        for i in [">10MiB", "<10MiB", "<5MiB", "<1MiB", ">0"]:
            self.FilterSize.append_text(i)

        for i in [
                'flac|wav|ape|aiff|wv|cue', 'mp3|m4a|aac|ogg|opus|wma', '!mp3'
        ]:
            self.FilterType.append_text(i)

        for i in config.sections["searches"]["filterin"]:
            self.add_combo(self.FilterIn, i, True)

        for i in config.sections["searches"]["filterout"]:
            self.add_combo(self.FilterOut, i, True)

        for i in config.sections["searches"]["filtersize"]:
            self.add_combo(self.FilterSize, i, True)

        for i in config.sections["searches"]["filterbr"]:
            self.add_combo(self.FilterBitrate, i, True)

        for i in config.sections["searches"]["filtercc"]:
            self.add_combo(self.FilterCountry, i, True)

        for i in config.sections["searches"]["filtertype"]:
            self.add_combo(self.FilterType, i, True)

    def focus_combobox(self, button):

        # We have the button of a combobox, find the entry
        parent = button.get_parent()

        if parent is None:
            return

        if isinstance(parent, Gtk.ComboBox):
            entry = parent.get_child()
            entry.grab_focus()
            GLib.idle_add(entry.emit, "activate")
            return

        self.focus_combobox(parent)

    def add_combo(self, combobox, text, list=False):

        text = str(text).strip()
        if not text:
            return False

        model = combobox.get_model()
        iterator = model.get_iter_first()
        match = False

        while iterator is not None:

            value = model.get_value(iterator, 0)

            if value.strip() == text:
                match = True

            iterator = model.iter_next(iterator)

        if not match:
            if list:
                combobox.append_text(text)
            else:
                combobox.prepend_text(text)

    def add_user_results(self, msg, user, country):

        if user in self.users:
            return

        self.users.add(user)

        counter = len(self.all_data) + 1

        inqueue = msg.inqueue
        ulspeed = msg.ulspeed
        h_speed = human_speed(ulspeed)

        if msg.freeulslots:
            imdl = "Y"
            inqueue = 0
        else:
            imdl = "N"

        color_id = (imdl == "Y" and "search" or "searchq")
        color = config.sections["ui"][color_id] or None

        h_queue = humanize(inqueue)

        update_ui = False
        maxstoredresults = config.sections["searches"]["max_stored_results"]

        for result in msg.list:

            if counter > maxstoredresults:
                break

            fullpath = result[1]
            fullpath_lower = fullpath.lower()

            if any(word in fullpath_lower
                   for word in self.searchterm_words_ignore):
                """ Filter out results with filtered words (e.g. nicotine -music) """
                log.add_search(
                    _("Filtered out excluded search result " + fullpath +
                      " from user " + user))
                continue

            if not any(word in fullpath_lower
                       for word in self.searchterm_words_include):
                """ Some users may send us wrong results, filter out such ones """
                log.add_search(
                    _("Filtered out inexact or incorrect search result " +
                      fullpath + " from user " + user))
                continue

            fullpath_split = reversed(fullpath.split('\\'))
            name = next(fullpath_split)
            directory = '\\'.join(fullpath_split)

            size = result[2]
            h_size = human_size(size)
            h_bitrate, bitrate, h_length, length = get_result_bitrate_length(
                size, result[4])

            is_result_visible = self.append([
                GObject.Value(GObject.TYPE_UINT64, counter), user,
                GObject.Value(GObject.TYPE_OBJECT,
                              self.frame.get_flag_image(country)), imdl,
                h_speed, h_queue, directory, name, h_size, h_bitrate, h_length,
                GObject.Value(GObject.TYPE_UINT64, bitrate), fullpath, country,
                GObject.Value(GObject.TYPE_UINT64, size),
                GObject.Value(GObject.TYPE_UINT64, ulspeed),
                GObject.Value(GObject.TYPE_UINT64, inqueue),
                GObject.Value(GObject.TYPE_UINT64, length),
                GObject.Value(GObject.TYPE_STRING, color)
            ])

            if is_result_visible:
                update_ui = True

            counter += 1

        if update_ui:
            # If this search wasn't initiated by us (e.g. wishlist), and the results aren't spoofed, show tab
            if not self.showtab:
                self.searches.show_tab(self, self.id, self.text, self.mode)
                self.showtab = True

            # Update number of results
            self.update_result_counter()

            # Update tab notification
            self.frame.searches.request_changed(self.Main)
            self.frame.request_tab_icon(self.frame.SearchTabLabel)

    def append(self, row):

        self.all_data.append(row)

        if self.numvisibleresults >= config.sections["searches"][
                "max_displayed_results"]:
            return False

        if not self.check_filter(row):
            return False

        iterator = self.add_row_to_model(row)

        if self.ResultGrouping.get_active_id() != "ungrouped":
            # Group by folder or user

            if self.ExpandButton.get_active():
                path = None

                if iterator is not None:
                    path = self.resultsmodel.get_path(iterator)

                if path is not None:
                    self.ResultsList.expand_to_path(path)
            else:
                collapse_treeview(self.ResultsList,
                                  self.ResultGrouping.get_active_id())

        return True

    def add_row_to_model(self, row):
        counter, user, flag, immediatedl, h_speed, h_queue, directory, filename, h_size, h_bitrate, h_length, bitrate, fullpath, country, size, speed, queue, length, color = row

        if self.ResultGrouping.get_active_id() != "ungrouped":
            # Group by folder or user

            empty_int = 0
            empty_str = ""

            if user not in self.usersiters:
                self.usersiters[user] = self.resultsmodel.insert_with_values(
                    None, -1, self.column_numbers, [
                        empty_int, user, flag, immediatedl, h_speed, h_queue,
                        empty_str, empty_str, empty_str, empty_str, empty_str,
                        empty_int, empty_str, country, empty_int, speed, queue,
                        empty_int, color
                    ])

            parent = self.usersiters[user]

            if self.ResultGrouping.get_active_id() == "folder_grouping":
                # Group by folder

                if directory not in self.directoryiters:
                    self.directoryiters[
                        directory] = self.resultsmodel.insert_with_values(
                            self.usersiters[user], -1, self.column_numbers, [
                                empty_int, user, flag, immediatedl, h_speed,
                                h_queue, directory, empty_str, empty_str,
                                empty_str, empty_str, empty_int,
                                fullpath.rsplit('\\', 1)[0] + '\\', country,
                                empty_int, speed, queue, empty_int, color
                            ])

                row = row[:]
                row[6] = ""  # Directory not visible for file row if "group by folder" is enabled

                parent = self.directoryiters[directory]
        else:
            parent = None

        try:
            """ Note that we use insert_with_values instead of append, as this reduces
            overhead by bypassing useless row conversion to GObject.Value in PyGObject. """

            iterator = self.resultsmodel.insert_with_values(
                parent, -1, self.column_numbers, row)

            self.numvisibleresults += 1

        except Exception as e:
            types = []
            for i in row:
                types.append(type(i))
            log.add_warning(_("Search row error: %(exception)s %(row)s"), {
                'exception': e,
                'row': row
            })
            iterator = None

        return iterator

    def check_digit(self, sfilter, value, factorize=True):

        op = ">="
        if sfilter[:1] in (">", "<", "="):
            op, sfilter = sfilter[:1] + "=", sfilter[1:]

        if not sfilter:
            return True

        factor = 1
        if factorize:
            base = 1024  # Default to binary for "k", "m", "g" suffixes
            if sfilter[-1:].lower() == 'b':
                base = 1000  # Byte suffix detected, prepare to use decimal if necessary
                sfilter = sfilter[:-1]
            if sfilter[-1:].lower() == 'i':
                base = 1024  # Binary requested, stop using decimal
                sfilter = sfilter[:-1]
            if sfilter.lower()[-1:] == "g":
                factor = pow(base, 3)
                sfilter = sfilter[:-1]
            elif sfilter.lower()[-1:] == "m":
                factor = pow(base, 2)
                sfilter = sfilter[:-1]
            elif sfilter.lower()[-1:] == "k":
                factor = base
                sfilter = sfilter[:-1]

        if not sfilter:
            return True

        try:
            sfilter = int(sfilter) * factor
        except ValueError:
            return True

        operation = self.operators.get(op)
        return operation(value, sfilter)

    def check_country(self, sfilter, value):

        if not isinstance(value, str):
            return False

        value = value.upper()
        allowed = False

        for cc in sfilter.split("|"):
            if cc == value:
                allowed = True

            elif cc.startswith("!") and cc[1:] != value:
                allowed = True

            elif cc.startswith("!") and cc[1:] == value:
                return False

        return allowed

    def check_file_type(self, sfilter, value):

        if not isinstance(value, str):
            return False

        value = value.lower()
        allowed = False

        for ext in sfilter.split("|"):
            exclude_ext = None

            if ext.startswith("!"):
                exclude_ext = ext[1:]

                if not exclude_ext.startswith("."):
                    exclude_ext = "." + exclude_ext

            elif not ext.startswith("."):
                ext = "." + ext

            if not ext.startswith("!") and value.endswith(ext):
                allowed = True

            elif ext.startswith("!") and not value.endswith(exclude_ext):
                allowed = True

            elif ext.startswith("!") and value.endswith(exclude_ext):
                return False

        return allowed

    def check_filter(self, row):

        filters = self.filters
        if self.active_filter_count == 0:
            return True

        # "Included text"-filter, check full file path (located at index 12 in row)
        if filters["include"] and not filters["include"].search(
                row[12].lower()):
            return False

        # "Excluded text"-filter, check full file path (located at index 12 in row)
        if filters["exclude"] and filters["exclude"].search(row[12].lower()):
            return False

        if filters["size"] and not self.check_digit(filters["size"],
                                                    row[14].get_uint64()):
            return False

        if filters["bitrate"] and not self.check_digit(
                filters["bitrate"], row[11].get_uint64(), False):
            return False

        if filters["freeslot"] and row[3] != "Y":
            return False

        if filters["country"] and not self.check_country(
                filters["country"], row[13]):
            return False

        if filters["type"] and not self.check_file_type(
                filters["type"], row[12]):
            return False

        return True

    def set_filters(self, enable, f_in, f_out, size, bitrate, freeslot,
                    country, f_type):

        self.filters = {
            "include": None,
            "exclude": None,
            "size": None,
            "bitrate": None,
            "freeslot": freeslot,
            "country": None,
            "type": None
        }

        self.active_filter_count = 0

        if f_in:
            try:
                f_in = re.compile(f_in.lower())
                self.filters["include"] = f_in
            except sre_constants.error:
                set_widget_fg_bg_css(self.FilterInEntry, "red", "white")
            else:
                set_widget_fg_bg_css(self.FilterInEntry)

            self.active_filter_count += 1

        if f_out:
            try:
                f_out = re.compile(f_out.lower())
                self.filters["exclude"] = f_out
            except sre_constants.error:
                set_widget_fg_bg_css(self.FilterOutEntry, "red", "white")
            else:
                set_widget_fg_bg_css(self.FilterOutEntry)

            self.active_filter_count += 1

        if size:
            self.filters["size"] = size
            self.active_filter_count += 1

        if bitrate:
            self.filters["bitrate"] = bitrate
            self.active_filter_count += 1

        if country:
            self.filters["country"] = country.upper()
            self.active_filter_count += 1

        if f_type:
            self.filters["type"] = f_type.lower()
            self.active_filter_count += 1

        if freeslot:
            self.active_filter_count += 1

        self.usersiters.clear()
        self.directoryiters.clear()
        self.resultsmodel.clear()
        self.numvisibleresults = 0

        for row in self.all_data:
            if self.numvisibleresults >= config.sections["searches"][
                    "max_displayed_results"]:
                break

            if self.check_filter(row):
                self.add_row_to_model(row)

        # Update number of visible results
        self.update_result_counter()
        self.update_filter_counter(self.active_filter_count)

    def populate_popup_menu_users(self):

        self.popup_menu_users.clear()

        if not self.selected_users:
            return

        for user in self.selected_users:
            popup = PopupMenu(self.frame)
            popup.setup_user_menu(user)
            popup.setup(("", None), ("#" + _("Select User's Transfers"),
                                     self.on_select_user_results, user))

            popup.toggle_user_items()
            self.popup_menu_users.setup((">" + user, popup))

    def on_select_user_results(self, *args):

        if not self.selected_users:
            return

        selected_user = args[-1]

        sel = self.ResultsList.get_selection()
        fmodel = self.ResultsList.get_model()
        sel.unselect_all()

        iterator = fmodel.get_iter_first()

        select_user_row_iter(fmodel, sel, 1, selected_user, iterator)

        self.select_results()

    def select_results(self):

        self.selected_results = []
        self.selected_users = []
        self.selected_files_count = 0

        model, paths = self.ResultsList.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            user = model.get_value(iterator, 1)

            if user is None:
                continue

            if user not in self.selected_users:
                self.selected_users.append(user)

            filepath = model.get_value(iterator, 12)

            if not filepath:
                # Result is not a file or directory, don't add it
                continue

            bitrate = model.get_value(iterator, 9)
            length = model.get_value(iterator, 10)
            size = model.get_value(iterator, 14)

            self.selected_results.append(
                (user, filepath, size, bitrate, length))

            filename = model.get_value(iterator, 7)

            if filename:
                self.selected_files_count += 1

    def update_result_counter(self):
        self.Counter.set_text(str(self.numvisibleresults))

    def update_visuals(self):

        for widget in list(self.__dict__.values()):
            update_widget_visuals(widget, list_font_target="searchfont")

    def save_columns(self):
        save_columns("file_search", self.ResultsList.get_columns())

    def on_list_clicked(self, widget, event):

        if triggers_context_menu(event):
            set_treeview_selected_row(widget, event)
            return self.on_popup_menu()

        pathinfo = widget.get_path_at_pos(event.x, event.y)

        if pathinfo is None:
            widget.get_selection().unselect_all()

        elif event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
            self.select_results()
            self.on_download_files()
            self.ResultsList.get_selection().unselect_all()
            return True

        return False

    def on_key_press_event(self, widget, event):

        self.select_results()

        if event.get_state() & Gdk.ModifierType.CONTROL_MASK and \
                event.hardware_keycode in keyval_to_hardware_keycode(Gdk.KEY_c):
            self.on_copy_file_path()
        else:
            # No key match, continue event
            return False

        widget.stop_emission_by_name("key_press_event")
        return True

    def on_popup_menu(self, *args):

        self.select_results()

        actions = self.popup_menu.get_actions()
        users = len(self.selected_users) > 0
        files = len(self.selected_results) > 0

        for i in (_("_Download File(s)"), _("Download File(s) _To..."),
                  _("File _Properties"), _("Copy _URL")):
            actions[i].set_enabled(False)

        for i in (_("Download _Folder(s)"), _("Download F_older(s) To..."),
                  _("_Browse Folder"), _("Copy _File Path"),
                  _("Copy Folder U_RL")):
            actions[i].set_enabled(files)

        actions[_("User(s)")].set_enabled(users)
        self.populate_popup_menu_users()

        for result in self.selected_results:
            if not result[1].endswith('\\'):
                # At least one selected result is a file, activate file-related items

                for i in (_("_Download File(s)"), _("Download File(s) _To..."),
                          _("File _Properties"), _("Copy _URL")):
                    actions[i].set_enabled(True)

                break

        self.popup_menu.set_num_selected_files(self.selected_files_count)

        self.popup_menu.popup()
        return True

    def on_browse_folder(self, *args):

        requested_folders = set()

        for file in self.selected_results:
            user = file[0]
            folder = file[1].rsplit('\\', 1)[0]

            if folder not in requested_folders:
                self.frame.browse_user(user, folder)
                requested_folders.add(folder)

    def on_file_properties(self, *args):

        if not self.frame.np.transfers:
            return

        data = []
        model, paths = self.ResultsList.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            filename = model.get_value(iterator, 7)

            # We only want to see the metadata of files, not directories
            if not filename:
                continue

            num = model.get_value(iterator, 0)
            user = model.get_value(iterator, 1)
            immediate = model.get_value(iterator, 3)
            speed = model.get_value(iterator, 4)
            queue = model.get_value(iterator, 5)
            size = model.get_value(iterator, 8)
            bitratestr = model.get_value(iterator, 9)
            length = model.get_value(iterator, 10)
            fn = model.get_value(iterator, 12)
            directory = fn.rsplit('\\', 1)[0]
            cc = model.get_value(iterator, 13)
            country = "%s / %s" % (cc, code2name(cc))

            data.append({
                "user": user,
                "fn": fn,
                "position": num,
                "filename": filename,
                "directory": directory,
                "size": size,
                "speed": speed,
                "queue": queue,
                "immediate": immediate,
                "bitrate": bitratestr,
                "length": length,
                "country": country
            })

        if paths:
            FileProperties(self.frame, data).show()

    def on_download_files(self, *args, prefix=""):

        if not self.frame.np.transfers:
            return

        for file in self.selected_results:
            # Make sure the selected result is not a directory
            if not file[1].endswith('\\'):
                self.frame.np.transfers.get_file(file[0],
                                                 file[1],
                                                 prefix,
                                                 size=file[2],
                                                 bitrate=file[3],
                                                 length=file[4],
                                                 checkduplicate=True)

    def on_download_files_to_selected(self, selected, data):
        self.on_download_files(prefix=selected)

    def on_download_files_to(self, *args):

        choose_dir(parent=self.frame.MainWindow,
                   callback=self.on_download_files_to_selected,
                   initialdir=config.sections["transfers"]["downloaddir"],
                   multichoice=False)

    def on_download_folders(self, *args, download_location=""):

        if not self.frame.np.transfers:
            return

        if download_location:
            """ Custom download location specified, remember it when peer sends a folder
            contents reply """

            requested_folders = self.frame.np.transfers.requested_folders
        else:
            requested_folders = defaultdict(dict)

        for i in self.selected_results:
            user = i[0]
            folder = i[1].rsplit('\\', 1)[0]

            if folder in requested_folders[user]:
                """ Ensure we don't send folder content requests for a folder more than once,
                e.g. when several selected resuls belong to the same folder. """
                continue

            requested_folders[user][folder] = download_location

            # First queue the visible search results
            files = []
            for row in self.all_data:

                # Find the wanted directory
                if folder != row[12].rsplit('\\', 1)[0]:
                    continue

                destination = self.frame.np.transfers.get_folder_destination(
                    user, folder)
                counter, user, flag, immediatedl, h_speed, h_queue, directory, filename, h_size, h_bitrate, h_length, bitrate, fullpath, country, size, speed, queue, length, color = row
                files.append((user, fullpath, destination, size.get_uint64(),
                              bitrate.get_uint64(), length.get_uint64()))

            if config.sections["transfers"]["reverseorder"]:
                files.sort(key=lambda x: x[1], reverse=True)

            for file in files:
                user, fullpath, destination, size, bitrate, length = file

                self.frame.np.transfers.get_file(user,
                                                 fullpath,
                                                 destination,
                                                 size=size,
                                                 bitrate=bitrate,
                                                 length=length,
                                                 checkduplicate=True)

            # Ask for the rest of the files in the folder
            self.frame.np.send_message_to_peer(
                user, slskmessages.FolderContentsRequest(None, folder))

    def on_download_folders_to_selected(self, selected, data):
        self.on_download_folders(download_location=selected)

    def on_download_folders_to(self, *args):

        choose_dir(parent=self.frame.MainWindow,
                   callback=self.on_download_folders_to_selected,
                   initialdir=config.sections["transfers"]["downloaddir"],
                   multichoice=False)

    def on_copy_file_path(self, *args):

        if self.selected_results:
            user, path = self.selected_results[0][:2]
            self.frame.clipboard.set_text(path, -1)

    def on_copy_url(self, *args):

        if self.selected_results:
            user, path = self.selected_results[0][:2]
            copy_file_url(user, path, self.frame.clipboard)

    def on_copy_dir_url(self, *args):

        if self.selected_results:
            user, path = self.selected_results[0][:2]
            copy_file_url(user,
                          path.rsplit('\\', 1)[0] + '\\', self.frame.clipboard)

    def on_group(self, widget):

        self.on_refilter(widget)

        active = widget.get_active()

        self.ResultsList.set_show_expanders(active)
        config.sections["searches"]["group_searches"] = active
        self.cols["id"].set_visible(not active)
        self.ExpandButton.set_visible(active)

    def on_toggle_expand_all(self, widget):

        active = self.ExpandButton.get_active()

        if active:
            self.ResultsList.expand_all()
            self.expand.set_from_icon_name("go-up-symbolic",
                                           Gtk.IconSize.BUTTON)
        else:
            collapse_treeview(self.ResultsList,
                              self.ResultGrouping.get_active_id())
            self.expand.set_from_icon_name("go-down-symbolic",
                                           Gtk.IconSize.BUTTON)

        config.sections["searches"]["expand_searches"] = active

    def on_toggle_filters(self, widget):

        visible = widget.get_active()
        self.FiltersContainer.set_visible(visible)
        config.sections["searches"]["filters_visible"] = visible

    def on_copy_search_term(self, *args):
        self.frame.clipboard.set_text(self.text, -1)

    def on_toggle_remember(self, widget):

        self.remember = widget.get_active()
        search = self.searches.searches[self.id]

        if not self.remember:
            self.searches.wish_list.remove_wish(search["term"])
        else:
            self.searches.wish_list.add_wish(search["term"])

    def push_history(self, widget, title):

        text = widget.get_active_text()
        if not text.strip():
            return None

        text = text.strip()
        history = config.sections["searches"][title]

        if text in history:
            history.remove(text)
        elif len(history) >= 5:
            del history[-1]

        history.insert(0, text)
        config.write_configuration()

        self.add_combo(widget, text)
        widget.get_child().set_text(text)

        return text

    def on_refilter(self, *args):

        if self.clearing_filters:
            return

        f_in = self.push_history(self.FilterIn, "filterin")
        f_out = self.push_history(self.FilterOut, "filterout")
        f_size = self.push_history(self.FilterSize, "filtersize")
        f_br = self.push_history(self.FilterBitrate, "filterbr")
        f_free = self.FilterFreeSlot.get_active()
        f_country = self.push_history(self.FilterCountry, "filtercc")
        f_type = self.push_history(self.FilterType, "filtertype")

        self.ResultsList.set_model(None)
        self.set_filters(1, f_in, f_out, f_size, f_br, f_free, f_country,
                         f_type)
        self.ResultsList.set_model(self.resultsmodel)

        if self.ResultGrouping.get_active_id() != "ungrouped":
            # Group by folder or user

            if self.ExpandButton.get_active():
                self.ResultsList.expand_all()
            else:
                collapse_treeview(self.ResultsList,
                                  self.ResultGrouping.get_active_id())

    def on_clear_filters(self, *args):

        self.clearing_filters = True

        self.FilterInEntry.set_text("")
        self.FilterOutEntry.set_text("")
        self.FilterSizeEntry.set_text("")
        self.FilterBitrateEntry.set_text("")
        self.FilterCountryEntry.set_text("")
        self.FilterTypeEntry.set_text("")
        self.FilterFreeSlot.set_active(False)

        self.clearing_filters = False
        self.FilterInEntry.grab_focus()
        self.on_refilter()

    def on_about_filters(self, *args):

        if not hasattr(self, "AboutSearchFiltersPopover"):
            load_ui_elements(
                self,
                os.path.join(self.frame.gui_dir, "ui", "popovers",
                             "searchfilters.ui"))
            self.AboutSearchFiltersPopover.set_relative_to(self.ShowChatHelp)

        try:
            self.AboutSearchFiltersPopover.popup()

        except AttributeError:
            # GTK <3.22 support
            self.AboutSearchFiltersPopover.set_transitions_enabled(True)
            self.AboutSearchFiltersPopover.show()

    def update_filter_counter(self, count):

        if count > 0:
            self.FilterLabel.set_text(_("Result Filters") + " *")
        else:
            self.FilterLabel.set_text(_("Result Filters"))

        self.FilterLabel.set_tooltip_text("%d active filter(s)" % count)

    def on_clear(self, *args):

        self.all_data = []
        self.usersiters.clear()
        self.directoryiters.clear()
        self.resultsmodel.clear()
        self.numvisibleresults = 0

        # Update number of visible results
        self.update_result_counter()

    def on_close(self, *args):
        self.searches.remove_tab(self)

    def on_close_all_tabs(self, *args):
        self.searches.remove_all_pages()
Beispiel #4
0
class IconNotebook:
    """ This class implements a pseudo Gtk.Notebook
    On top of what a Gtk.Notebook provides:
    - You can have icons on the notebook tab.
    - You can choose the label orientation (angle).
    """
    def __init__(self,
                 images,
                 angle=0,
                 tabclosers=False,
                 show_hilite_image=True,
                 reorderable=True,
                 show_status_image=False,
                 notebookraw=None):

        # We store the real Gtk.Notebook object
        self.notebook = notebookraw
        self.notebook.set_show_border(False)

        self.tabclosers = tabclosers
        self.reorderable = reorderable

        self.images = images
        self._show_hilite_image = show_hilite_image
        self._show_status_image = show_status_image

        self.notebook.connect("key-press-event", self.on_key_press_event)
        self.notebook.connect("switch-page", self.on_switch_page)

        self.unread_button = Gtk.Button.new_from_icon_name(
            "emblem-important-symbolic", Gtk.IconSize.BUTTON)
        self.unread_button.set_relief(Gtk.ReliefStyle.NONE)
        self.unread_button.set_tooltip_text(_("Unread Tabs"))
        self.unread_button.set_halign(Gtk.Align.CENTER)
        self.unread_button.set_valign(Gtk.Align.CENTER)
        self.unread_button.connect("clicked",
                                   self.on_unread_notifications_menu)

        context = self.unread_button.get_style_context()
        context.add_class("circular")

        self.notebook.set_action_widget(self.unread_button, Gtk.PackType.END)
        self.popup_menu_unread = PopupMenu(window=self.notebook.get_toplevel())
        self.unread_pages = []

        self.angle = angle

    def get_labels(self, page):
        tab_label = self.notebook.get_tab_label(page)
        menu_label = self.notebook.get_menu_label(page)

        return tab_label, menu_label

    def set_reorderable(self, reorderable):

        self.reorderable = reorderable

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            self.notebook.set_tab_reorderable(page, self.reorderable)

    def set_tab_closers(self, closers):

        self.tabclosers = closers

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            tab_label, menu_label = self.get_labels(page)

            tab_label.set_onclose(self.tabclosers)

    def show_hilite_images(self, show_image=True):

        self._show_hilite_image = show_image

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            tab_label, menu_label = self.get_labels(page)

            tab_label.show_hilite_image(self._show_hilite_image)

    def show_status_images(self, show_image=True):

        self._show_status_image = show_image

    def set_tab_angle(self, angle):

        if angle == self.angle:
            return

        self.angle = angle

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            tab_label, menu_label = self.get_labels(page)

            tab_label.set_angle(angle)

    def set_tab_pos(self, pos):
        self.notebook.set_tab_pos(pos)

    def append_page(self,
                    page,
                    label,
                    onclose=None,
                    angle=0,
                    fulltext=None,
                    status=None):

        self.set_tab_angle(angle)
        closebutton = self.tabclosers

        label_tab = ImageLabel(label,
                               onclose,
                               closebutton=closebutton,
                               angle=angle,
                               show_hilite_image=self._show_hilite_image,
                               status_image=self.images["offline"],
                               show_status_image=self._show_status_image)

        if fulltext is None:
            fulltext = label

        # menu for all tabs
        label_tab_menu = ImageLabel(label)
        label_tab.connect('button_press_event', self.on_tab_click, page)
        label_tab.connect('popup_menu', self.on_tab_popup, page)
        label_tab.connect('touch_event', self.on_tab_click, page)
        label_tab.show()

        Gtk.Notebook.append_page_menu(self.notebook, page, label_tab,
                                      label_tab_menu)

        if status:
            self.set_user_status(page, label, status)
        else:
            label_tab.set_tooltip_text(fulltext)

        self.notebook.set_tab_reorderable(page, self.reorderable)
        self.notebook.set_show_tabs(True)

    def remove_page(self, page):

        Gtk.Notebook.remove_page(self.notebook, self.page_num(page))

        if self.notebook.get_n_pages() == 0:
            self.notebook.set_show_tabs(False)

    def remove_all_pages_response(self, dialog, response_id, data):

        dialog.destroy()

        if response_id == Gtk.ResponseType.OK:
            for page in self.notebook.get_children():
                tab_label, menu_label = self.get_labels(page)
                tab_label.onclose(dialog)

    def remove_all_pages(self):

        option_dialog(parent=self.notebook.get_toplevel(),
                      title=_('Close All Tabs?'),
                      message=_('Are you sure you wish to close all tabs?'),
                      callback=self.remove_all_pages_response)

    def get_page_owner(self, page, items):

        n = self.page_num(page)
        page = self.get_nth_page(n)

        return next(owner for owner, tab in items.items() if tab.Main is page)

    def on_tab_popup(self, widget, page):
        # Dummy implementation
        pass

    def on_tab_click(self, widget, event, page):

        if triggers_context_menu(event):
            return self.on_tab_popup(widget, page)

        elif event.button == 2:
            # Middle click
            tab_label, menu_label = self.get_labels(page)
            tab_label.onclose(widget)
            return True

        return False

    def set_status_image(self, page, status):

        tab_label, menu_label = self.get_labels(page)

        if status == 1:
            image_name = "away"
        elif status == 2:
            image_name = "online"
        else:
            image_name = "offline"

        image = self.images[image_name]

        tab_label.set_status_image(image)
        menu_label.set_status_image(image)

    def set_user_status(self, page, user, status):

        if status == 1:
            status_text = _("Away")
        elif status == 2:
            status_text = _("Online")
        else:
            status_text = _("Offline")

        if not config.sections["ui"]["tab_status_icons"]:
            self.set_text(page, "%s (%s)" % (user[:15], status_text))
        else:
            self.set_text(page, user)

        self.set_status_image(page, status)

        # Set a tab tooltip containing the user's status and name
        tab_label, menu_label = self.get_labels(page)
        tab_label.set_tooltip_text("%s (%s)" % (user, status_text))

    def set_hilite_image(self, page, status):

        tab_label, menu_label = self.get_labels(page)
        image = None

        if status > 0:
            image = self.images[("hilite3", "hilite")[status - 1]]

        if status == 1 and tab_label.get_hilite_image(
        ) == self.images["hilite"]:
            # Chat mentions have priority over normal notifications
            return

        tab_label.set_hilite_image(image)
        menu_label.set_hilite_image(image)

        # Determine if button for unread notifications should be shown
        if image:
            if page not in self.unread_pages:
                self.unread_pages.append(page)
                self.unread_button.show()
            return

        if page in self.unread_pages:
            self.unread_pages.remove(page)

        if not self.unread_pages:
            self.unread_button.hide()

    def set_text(self, page, label):

        tab_label, menu_label = self.get_labels(page)

        tab_label.set_text(label)
        menu_label.set_text(label)

    def set_text_colors(self, status):

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            self.set_text_color(page, status)

    def set_text_color(self, page, status):

        tab_label, menu_label = self.get_labels(page)
        tab_label.set_text_color(status)

    def request_hilite(self, page):

        current = self.get_nth_page(self.get_current_page())
        if current == page:
            return

        self.set_hilite_image(page, status=2)
        self.set_text_color(page, status=2)

    def request_changed(self, page):

        current = self.get_nth_page(self.get_current_page())
        if current == page:
            return

        self.set_hilite_image(page, status=1)
        self.set_text_color(page, status=1)

    def get_current_page(self):
        return self.notebook.get_current_page()

    def set_current_page(self, page_num):
        return self.notebook.set_current_page(page_num)

    def set_unread_page(self, action, state, page_num):
        self.notebook.set_current_page(page_num)

    def get_nth_page(self, page_num):
        return self.notebook.get_nth_page(page_num)

    def page_num(self, page):
        return self.notebook.page_num(page)

    def popup_enable(self):
        self.notebook.popup_enable()

    def popup_disable(self):
        self.notebook.popup_disable()

    def show(self):
        self.notebook.show()

    def on_key_press_event(self, widget, event):

        keycode = event.hardware_keycode

        if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
            if keycode in keyval_to_hardware_keycode(Gdk.KEY_w) or \
               keycode in keyval_to_hardware_keycode(Gdk.KEY_F4):
                # Ctrl+W and Ctrl+F4: close current tab

                page = self.get_nth_page(self.get_current_page())
                tab_label, menu_label = self.get_labels(page)
                tab_label.onclose(widget)
                return True

        return False

    def on_switch_page(self, notebook, new_page, page_num):

        # Hide widgets on previous page for a performance boost
        current_page = self.get_nth_page(self.get_current_page())

        for child in current_page.get_children():
            child.hide()

        for child in new_page.get_children():
            child.show()

        # Dismiss tab notification
        self.set_hilite_image(new_page, status=0)
        self.set_text_color(new_page, status=0)

    def on_unread_notifications_menu(self, widget):

        self.popup_menu_unread.clear()

        for page in self.unread_pages:
            tab_label, menu_label = self.get_labels(page)
            self.popup_menu_unread.setup(
                ("#" + tab_label.get_text(), self.set_unread_page,
                 self.page_num(page)))

        self.popup_menu_unread.popup()
Beispiel #5
0
class IconNotebook:
    """ This class implements a pseudo Gtk.Notebook
    On top of what a Gtk.Notebook provides:
    - Icons on the notebook tab
    - Dropdown menu for unread tabs
    - A few shortcuts
    """
    def __init__(self,
                 images,
                 tabclosers=False,
                 show_hilite_image=True,
                 show_status_image=False,
                 notebookraw=None):

        # We store the real Gtk.Notebook object
        self.notebook = notebookraw
        self.notebook.set_show_border(False)

        self.tabclosers = tabclosers

        self.images = images
        self._show_hilite_image = show_hilite_image
        self._show_status_image = show_status_image

        self.key_controller = connect_key_press_event(self.notebook,
                                                      self.on_key_press_event)
        self.notebook.connect("switch-page", self.on_switch_page)

        self.unread_button = Gtk.MenuButton.new()

        if Gtk.get_major_version() == 4:
            self.window = self.notebook.get_root()

            self.unread_button.set_icon_name("emblem-important-symbolic")
            self.unread_button.set_has_frame(False)
        else:
            self.window = self.notebook.get_toplevel()
            self.popup_enable()

            self.unread_button.set_image(
                Gtk.Image.new_from_icon_name("emblem-important-symbolic",
                                             Gtk.IconSize.BUTTON))
            self.unread_button.set_relief(Gtk.ReliefStyle.NONE)

        self.unread_button.set_tooltip_text(_("Unread Tabs"))
        self.unread_button.set_halign(Gtk.Align.CENTER)
        self.unread_button.set_valign(Gtk.Align.CENTER)

        context = self.unread_button.get_style_context()
        context.add_class("circular")

        self.notebook.set_action_widget(self.unread_button, Gtk.PackType.END)

        self.popup_menu_unread = PopupMenu(widget=self.unread_button,
                                           connect_events=False)
        self.unread_button.set_menu_model(self.popup_menu_unread)
        self.unread_pages = []

        self.notebook.hide()

    def get_labels(self, page):
        tab_label = self.notebook.get_tab_label(page)
        menu_label = self.notebook.get_menu_label(page)

        return tab_label, menu_label

    def get_tab_label_inner(self, page):

        if Gtk.get_major_version() == 4:
            return self.notebook.get_tab_label(page).get_first_child()
        else:
            return self.notebook.get_tab_label(page).get_children()[0]

    def set_tab_closers(self, closers):

        self.tabclosers = closers

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            tab_label, menu_label = self.get_labels(page)

            tab_label.set_onclose(self.tabclosers)

    def show_hilite_images(self, show_image=True):

        self._show_hilite_image = show_image

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            tab_label, menu_label = self.get_labels(page)

            tab_label.show_hilite_image(self._show_hilite_image)

    def show_status_images(self, show_image=True):

        self._show_status_image = show_image

    def set_tab_pos(self, pos):
        self.notebook.set_tab_pos(pos)

    def update_unread_pages_menu(self):

        self.popup_menu_unread.clear()

        for page in self.unread_pages:
            tab_label, menu_label = self.get_labels(page)
            self.popup_menu_unread.setup(
                ("#" + tab_label.get_text(), self.set_unread_page,
                 self.page_num(page)))

    def append_unread_page(self, page):

        if page in self.unread_pages:
            return

        self.unread_pages.append(page)
        self.update_unread_pages_menu()
        self.unread_button.show()

    def remove_unread_page(self, page):

        if page in self.unread_pages:
            self.unread_pages.remove(page)
            self.update_unread_pages_menu()

        if not self.unread_pages:
            self.unread_button.hide()

    def append_page(self,
                    page,
                    label,
                    onclose=None,
                    fulltext=None,
                    status=None):

        closebutton = self.tabclosers

        label_tab = ImageLabel(label,
                               onclose,
                               closebutton=closebutton,
                               show_hilite_image=self._show_hilite_image,
                               status_image=self.images["offline"],
                               show_status_image=self._show_status_image)
        label_tab.show()

        if fulltext is None:
            fulltext = label

        # menu for all tabs
        label_tab_menu = ImageLabel(label)

        if Gtk.get_major_version() == 4:
            label_tab.gesture_click = Gtk.GestureClick()
            label_tab.add_controller(label_tab.gesture_click)
        else:
            label_tab.gesture_click = Gtk.GestureMultiPress.new(label_tab)

        label_tab.gesture_click.set_button(Gdk.BUTTON_MIDDLE)
        label_tab.gesture_click.connect("pressed", label_tab.onclose, page)

        Gtk.Notebook.append_page_menu(self.notebook, page, label_tab,
                                      label_tab_menu)

        if status:
            self.set_user_status(page, label, status)
        else:
            label_tab.set_tooltip_text(fulltext)

        self.notebook.set_tab_reorderable(page, True)
        self.notebook.show()

    def remove_page(self, page):

        Gtk.Notebook.remove_page(self.notebook, self.page_num(page))

        self.remove_unread_page(page)

        if self.notebook.get_n_pages() == 0:
            self.notebook.hide()

    def remove_all_pages_response(self, dialog, response_id, data):

        dialog.destroy()

        if response_id == Gtk.ResponseType.OK:
            for i in reversed(range(self.notebook.get_n_pages())):
                page = self.notebook.get_nth_page(i)
                tab_label, menu_label = self.get_labels(page)
                tab_label.onclose(dialog)

    def remove_all_pages(self):

        option_dialog(parent=self.window,
                      title=_('Close All Tabs?'),
                      message=_('Are you sure you wish to close all tabs?'),
                      callback=self.remove_all_pages_response)

    def get_page_owner(self, page, items):

        n = self.page_num(page)
        page = self.get_nth_page(n)

        return next(owner for owner, tab in items.items() if tab.Main is page)

    def on_tab_popup(self, widget, page):
        # Dummy implementation
        pass

    def set_status_image(self, page, status):

        tab_label, menu_label = self.get_labels(page)

        if status == 1:
            image_name = "away"
        elif status == 2:
            image_name = "online"
        else:
            image_name = "offline"

        image = self.images[image_name]

        tab_label.set_status_image(image)
        menu_label.set_status_image(image)

    def set_user_status(self, page, user, status):

        if status == 1:
            status_text = _("Away")
        elif status == 2:
            status_text = _("Online")
        else:
            status_text = _("Offline")

        if not config.sections["ui"]["tab_status_icons"]:
            self.set_text(page, "%s (%s)" % (user[:15], status_text))
        else:
            self.set_text(page, user)

        self.set_status_image(page, status)

        # Set a tab tooltip containing the user's status and name
        tab_label, menu_label = self.get_labels(page)
        tab_label.set_tooltip_text("%s (%s)" % (user, status_text))

    def set_hilite_image(self, page, status):

        tab_label, menu_label = self.get_labels(page)
        image = None

        if status > 0:
            image = self.images[("hilite3", "hilite")[status - 1]]

        if status == 1 and tab_label.get_hilite_image(
        ) == self.images["hilite"]:
            # Chat mentions have priority over normal notifications
            return

        tab_label.set_hilite_image(image)
        menu_label.set_hilite_image(image)

        # Determine if button for unread notifications should be shown
        if image:
            self.append_unread_page(page)
            return

        self.remove_unread_page(page)

    def set_text(self, page, label):

        tab_label, menu_label = self.get_labels(page)

        tab_label.set_text(label)
        menu_label.set_text(label)

    def set_text_colors(self, status):

        for i in range(self.notebook.get_n_pages()):
            page = self.notebook.get_nth_page(i)
            self.set_text_color(page, status)

    def set_text_color(self, page, status):

        tab_label, menu_label = self.get_labels(page)
        tab_label.set_text_color(status)

    def request_hilite(self, page):

        current = self.get_nth_page(self.get_current_page())
        if current == page:
            return

        self.set_hilite_image(page, status=2)
        self.set_text_color(page, status=2)

    def request_changed(self, page):

        current = self.get_nth_page(self.get_current_page())
        if current == page:
            return

        self.set_hilite_image(page, status=1)
        self.set_text_color(page, status=1)

    def get_current_page(self):
        return self.notebook.get_current_page()

    def set_current_page(self, page_num):
        return self.notebook.set_current_page(page_num)

    def set_unread_page(self, action, state, page_num):
        self.notebook.set_current_page(page_num)

    def get_nth_page(self, page_num):
        return self.notebook.get_nth_page(page_num)

    def page_num(self, page):
        return self.notebook.page_num(page)

    def popup_enable(self):
        self.notebook.popup_enable()

    def popup_disable(self):
        self.notebook.popup_disable()

    def show(self):
        self.notebook.show()

    def on_key_press_event(self, *args):

        keyval, keycode, state = get_key_press_event_args(*args)
        keycodes_w, mods = parse_accelerator("<Primary>w")
        keycodes_f4, mods = parse_accelerator("<Primary>F4")

        if state & mods and (keycode in keycodes_w or keycode in keycodes_f4):
            # Ctrl+W and Ctrl+F4: close current tab

            page = self.get_nth_page(self.get_current_page())
            tab_label, menu_label = self.get_labels(page)
            tab_label.onclose(None)
            return True

        return False

    def on_switch_page(self, notebook, new_page, page_num):

        # Hide widgets on previous page for a performance boost
        current_page = self.get_nth_page(self.get_current_page())

        for child in current_page.get_children():
            child.hide()

        for child in new_page.get_children():
            child.show()

        # Dismiss tab notification
        self.set_hilite_image(new_page, status=0)
        self.set_text_color(new_page, status=0)
class TransferList(UserInterface):
    def __init__(self, frame, transfer_type):

        super().__init__("ui/" + transfer_type + "s.ui")
        getattr(frame, transfer_type + "s_content").add(self.Main)

        self.frame = frame
        self.type = transfer_type
        self.page_id = transfer_type + "s"

        self.user_counter = getattr(frame, "%sUsers" % transfer_type.title())
        self.file_counter = getattr(frame, "%sFiles" % transfer_type.title())
        grouping_button = getattr(frame,
                                  "ToggleTree%ss" % transfer_type.title())

        if Gtk.get_major_version() == 4:
            self.ClearTransfers.set_has_frame(False)

        Accelerator("t", self.Transfers, self.on_abort_transfers_accelerator)
        Accelerator("r", self.Transfers, self.on_retry_transfers_accelerator)
        Accelerator("Delete", self.Transfers,
                    self.on_clear_transfers_accelerator)
        Accelerator("<Alt>Return", self.Transfers,
                    self.on_file_properties_accelerator)

        self.last_ui_update = 0
        self.transfer_list = []
        self.users = {}
        self.paths = {}
        self.selected_users = []
        self.selected_transfers = []
        self.tree_users = None

        # Status list
        self.statuses = {
            "Queued": _("Queued"),
            "Queued (prioritized)": _("Queued (prioritized)"),
            "Queued (privileged)": _("Queued (privileged)"),
            "Getting status": _("Getting status"),
            "Transferring": _("Transferring"),
            "Cannot connect": _("Cannot connect"),
            "Pending shutdown.": _("Pending shutdown"),
            "User logged off": _("User logged off"),
            "Disallowed extension":
            _("Disallowed extension"
              ),  # Sent by Soulseek NS for filtered extensions
            "Aborted": _("Aborted"),
            "Cancelled": _("Cancelled"),
            "Paused": _("Paused"),
            "Finished": _("Finished"),
            "Filtered": _("Filtered"),
            "Banned": _("Banned"),
            "Blocked country": _("Blocked country"),
            "Too many files": _("Too many files"),
            "Too many megabytes": _("Too many megabytes"),
            "File not shared": _("File not shared"),
            "File not shared.":
            _("File not shared"),  # Newer variant containing a dot
            "Download folder error": _("Download folder error"),
            "Local file error": _("Local file error"),
            "Remote file error": _("Remote file error")
        }
        self.deprioritized_statuses = ("", "Paused", "Aborted", "Finished",
                                       "Filtered")

        self.transfersmodel = Gtk.TreeStore(
            str,  # (0)  user
            str,  # (1)  path
            str,  # (2)  file name
            str,  # (3)  translated status
            str,  # (4)  hqueue position
            int,  # (5)  percent
            str,  # (6)  hsize
            str,  # (7)  hspeed
            str,  # (8)  htime elapsed
            str,  # (9)  htime left
            GObject.TYPE_UINT64,  # (10) size
            GObject.TYPE_UINT64,  # (11) current bytes
            GObject.TYPE_UINT64,  # (12) speed
            GObject.TYPE_UINT,  # (13) queue position
            int,  # (14) time elapsed
            int,  # (15) time left
            GObject.TYPE_PYOBJECT  # (16) transfer object
        )

        self.column_numbers = list(range(self.transfersmodel.get_n_columns()))
        self.cols = cols = initialise_columns(
            frame,
            transfer_type,
            self.Transfers,
            ["user", _("User"), 200, "text", None],
            ["path", self.path_label, 400, "text", None],
            ["filename", _("Filename"), 400, "text", None],
            ["status", _("Status"), 140, "text", None],
            ["queue_position",
             _("Queue"), 75, "number", None],
            ["percent", _("Percent"), 70, "progress", None],
            ["size", _("Size"), 170, "number", None],
            ["speed", _("Speed"), 90, "number", None],
            ["time_elapsed",
             _("Time Elapsed"), 140, "number", None],
            ["time_left", _("Time Left"), 140, "number", None],
        )

        cols["user"].set_sort_column_id(0)
        cols["path"].set_sort_column_id(1)
        cols["filename"].set_sort_column_id(2)
        cols["status"].set_sort_column_id(3)
        cols["queue_position"].set_sort_column_id(13)
        cols["percent"].set_sort_column_id(5)
        cols["size"].set_sort_column_id(10)
        cols["speed"].set_sort_column_id(12)
        cols["time_elapsed"].set_sort_column_id(14)
        cols["time_left"].set_sort_column_id(15)

        self.Transfers.set_model(self.transfersmodel)

        self.status_page = getattr(frame, "%ss_status_page" % transfer_type)
        self.expand_button = getattr(frame,
                                     "Expand%ss" % transfer_type.title())

        state = GLib.Variant(
            "s",
            verify_grouping_mode(config.sections["transfers"]["group%ss" %
                                                              transfer_type]))
        action = Gio.SimpleAction(name="%sgrouping" % transfer_type,
                                  parameter_type=GLib.VariantType("s"),
                                  state=state)
        action.connect("change-state", self.on_toggle_tree)
        frame.MainWindow.add_action(action)
        action.change_state(state)

        menu = create_grouping_menu(
            frame.MainWindow,
            config.sections["transfers"]["group%ss" % transfer_type],
            self.on_toggle_tree)
        grouping_button.set_menu_model(menu)

        self.expand_button.connect("toggled", self.on_expand_tree)
        self.expand_button.set_active(
            config.sections["transfers"]["%ssexpanded" % transfer_type])

        self.popup_menu_users = PopupMenu(frame)
        self.popup_menu_clear = PopupMenu(frame)
        self.ClearTransfers.set_menu_model(self.popup_menu_clear.model)

        self.popup_menu_copy = PopupMenu(frame)
        self.popup_menu_copy.add_items(
            ("#" + _("Copy _File Path"), self.on_copy_file_path),
            ("#" + _("Copy _URL"), self.on_copy_url),
            ("#" + _("Copy Folder U_RL"), self.on_copy_dir_url))

        self.popup_menu = PopupMenu(frame, self.Transfers, self.on_popup_menu)
        self.popup_menu.add_items(
            ("#" + "selected_files", None), ("", None),
            ("#" + _("Send to _Player"), self.on_play_files),
            ("#" + _("_Open in File Manager"), self.on_open_file_manager),
            ("#" + _("F_ile Properties"), self.on_file_properties), ("", None),
            ("#" + _("_Search"), self.on_file_search),
            ("#" + _("_Browse Folder(s)"), self.on_browse_folder), ("", None),
            ("#" + self.retry_label, self.on_retry_transfer),
            ("#" + self.abort_label, self.on_abort_transfer),
            ("#" + _("_Clear"), self.on_clear_transfer), ("", None),
            (">" + _("Clear Groups"), self.popup_menu_clear),
            (">" + _("Copy"), self.popup_menu_copy),
            (">" + _("User(s)"), self.popup_menu_users))

        self.update_visuals()

    def init_transfers(self, transfer_list):
        self.transfer_list = transfer_list
        self.update(forceupdate=True)

    def server_login(self):
        pass

    def server_disconnect(self):
        pass

    def rebuild_transfers(self):
        self.clear()
        self.update()

    def save_columns(self):
        save_columns(self.type, self.Transfers.get_columns())

    def update_visuals(self):

        for widget in list(self.__dict__.values()):
            update_widget_visuals(widget, list_font_target="transfersfont")

    def select_transfers(self):

        self.selected_transfers.clear()
        self.selected_users.clear()

        model, paths = self.Transfers.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            self.select_transfer(model, iterator, select_user=True)

            # If we're in grouping mode, select any transfers under the selected
            # user or folder
            self.select_child_transfers(model, model.iter_children(iterator))

    def select_child_transfers(self, model, iterator):

        while iterator is not None:
            self.select_transfer(model, iterator)
            self.select_child_transfers(model, model.iter_children(iterator))
            iterator = model.iter_next(iterator)

    def select_transfer(self, model, iterator, select_user=False):

        transfer = model.get_value(iterator, 16)

        if transfer.filename is not None and transfer not in self.selected_transfers:
            self.selected_transfers.append(transfer)

        if select_user and transfer.user not in self.selected_users:
            self.selected_users.append(transfer.user)

    def new_transfer_notification(self, finished=False):
        if self.frame.current_page_id != self.page_id:
            self.frame.request_tab_hilite(self.page_id, mentioned=finished)

    def on_ban(self, *_args):

        self.select_transfers()

        for user in self.selected_users:
            self.frame.np.network_filter.ban_user(user)

    def on_file_search(self, *_args):

        transfer = next(iter(self.selected_transfers), None)

        if not transfer:
            return

        self.frame.SearchEntry.set_text(transfer.filename.rsplit("\\", 1)[1])
        self.frame.change_main_page("search")

    def translate_status(self, status):

        translated_status = self.statuses.get(status)

        if translated_status:
            return translated_status

        return status

    def update_num_users_files(self):
        self.user_counter.set_text(str(len(self.users)))
        self.file_counter.set_text(str(len(self.transfer_list)))

    def update(self, transfer=None, forceupdate=False, update_parent=True):

        if not forceupdate and self.frame.current_page_id != self.page_id:
            # No need to do unnecessary work if transfers are not visible
            return

        if transfer is not None:
            self.update_specific(transfer)

        elif self.transfer_list:
            for transfer_i in reversed(self.transfer_list):
                self.update_specific(transfer_i)

        if update_parent:
            self.update_parent_rows(transfer)

    def update_parent_rows(self, transfer=None):

        if self.tree_users != "ungrouped":
            if transfer is not None:
                username = transfer.user
                path = transfer.path if self.type == "download" else transfer.filename.rsplit(
                    '\\', 1)[0]
                user_path = username + path

                user_path_iter = self.paths.get(user_path)
                user_iter = self.users.get(username)

                if user_path_iter:
                    self.update_parent_row(user_path_iter,
                                           user_path,
                                           folder=True)

                if user_iter:
                    self.update_parent_row(user_iter, username)

            else:
                for user_path, user_path_iter in list(self.paths.items()):
                    self.update_parent_row(user_path_iter,
                                           user_path,
                                           folder=True)

                for username, user_iter in list(self.users.items()):
                    self.update_parent_row(user_iter, username)

        # Show tab description if necessary
        self.status_page.set_visible(not self.transfer_list)
        self.Main.set_visible(self.transfer_list)

    @staticmethod
    def get_hqueue_position(queue_position):
        return str(queue_position) if queue_position > 0 else ""

    @staticmethod
    def get_hsize(current_byte_offset, size):
        return "%s / %s" % (human_size(current_byte_offset), human_size(size))

    @staticmethod
    def get_hspeed(speed):
        return human_speed(speed) if speed > 0 else ""

    @staticmethod
    def get_helapsed(elapsed):
        return human_length(elapsed) if elapsed >= 1 else ""

    @staticmethod
    def get_hleft(left):
        return human_length(left) if left >= 1 else ""

    @staticmethod
    def get_percent(current_byte_offset, size):
        return min(((100 * int(current_byte_offset)) /
                    int(size)), 100) if size > 0 else 100

    @staticmethod
    def get_size(size):

        try:
            size = int(size)

            if size < 0 or size > maxsize:
                size = 0

        except TypeError:
            size = 0

        return size

    def update_parent_row(self, initer, key, folder=False):

        speed = 0.0
        totalsize = current_bytes = 0
        elapsed = 0
        left = 0
        salientstatus = ""

        iterator = self.transfersmodel.iter_children(initer)

        if iterator is None:
            # Remove parent row if no children are present anymore
            dictionary = self.paths if folder else self.users
            self.transfersmodel.remove(initer)
            del dictionary[key]
            return

        while iterator is not None:
            transfer = self.transfersmodel.get_value(iterator, 16)
            status = transfer.status

            if status == "Transferring" or salientstatus in self.deprioritized_statuses:
                salientstatus = status

            if status == "Filtered":
                # We don't want to count filtered files when calculating the progress
                iterator = self.transfersmodel.iter_next(iterator)
                continue

            elapsed += transfer.time_elapsed or 0
            left += transfer.time_left or 0
            totalsize += self.get_size(transfer.size)
            current_bytes += transfer.current_byte_offset or 0
            speed += transfer.speed or 0

            iterator = self.transfersmodel.iter_next(iterator)

        transfer = self.transfersmodel.get_value(initer, 16)

        if transfer.status != salientstatus:
            self.transfersmodel.set_value(initer, 3,
                                          self.translate_status(salientstatus))
            transfer.status = salientstatus

        if transfer.speed != speed:
            self.transfersmodel.set_value(initer, 7, self.get_hspeed(speed))
            self.transfersmodel.set_value(
                initer, 12, GObject.Value(GObject.TYPE_UINT64, speed))
            transfer.speed = speed

        if transfer.time_elapsed != elapsed:
            self.transfersmodel.set_value(initer, 8,
                                          self.get_helapsed(elapsed))
            self.transfersmodel.set_value(initer, 9, self.get_hleft(left))
            self.transfersmodel.set_value(initer, 14, elapsed)
            self.transfersmodel.set_value(initer, 15, left)
            transfer.time_elapsed = elapsed
            transfer.time_left = left

        if transfer.current_byte_offset != current_bytes:
            self.transfersmodel.set_value(
                initer, 5, self.get_percent(current_bytes, totalsize))
            self.transfersmodel.set_value(
                initer, 6,
                "%s / %s" % (human_size(current_bytes), human_size(totalsize)))
            self.transfersmodel.set_value(
                initer, 11, GObject.Value(GObject.TYPE_UINT64, current_bytes))
            transfer.current_byte_offset = current_bytes

        if transfer.size != totalsize:
            self.transfersmodel.set_value(
                initer, 6,
                "%s / %s" % (human_size(current_bytes), human_size(totalsize)))
            self.transfersmodel.set_value(
                initer, 10, GObject.Value(GObject.TYPE_UINT64, totalsize))
            transfer.size = totalsize

    def update_specific(self, transfer=None):

        current_byte_offset = transfer.current_byte_offset or 0
        queue_position = transfer.queue_position or 0
        modifier = transfer.modifier
        status = transfer.status or ""

        if modifier and status == "Queued":
            # Priority status
            status = status + " (%s)" % modifier

        size = self.get_size(transfer.size)
        speed = transfer.speed or 0
        hspeed = self.get_hspeed(speed)
        elapsed = transfer.time_elapsed or 0
        helapsed = self.get_helapsed(elapsed)
        left = transfer.time_left or 0
        initer = transfer.iterator

        # Modify old transfer
        if initer is not None:
            translated_status = self.translate_status(status)

            if self.transfersmodel.get_value(initer, 3) != translated_status:
                self.transfersmodel.set_value(initer, 3, translated_status)

            if self.transfersmodel.get_value(initer, 7) != hspeed:
                self.transfersmodel.set_value(initer, 7, hspeed)
                self.transfersmodel.set_value(
                    initer, 12, GObject.Value(GObject.TYPE_UINT64, speed))

            if self.transfersmodel.get_value(initer, 8) != helapsed:
                self.transfersmodel.set_value(initer, 8, helapsed)
                self.transfersmodel.set_value(initer, 9, self.get_hleft(left))
                self.transfersmodel.set_value(initer, 14, elapsed)
                self.transfersmodel.set_value(initer, 15, left)

            if self.transfersmodel.get_value(initer,
                                             11) != current_byte_offset:
                percent = self.get_percent(current_byte_offset, size)

                self.transfersmodel.set_value(initer, 5, percent)
                self.transfersmodel.set_value(
                    initer, 6, self.get_hsize(current_byte_offset, size))
                self.transfersmodel.set_value(
                    initer, 11,
                    GObject.Value(GObject.TYPE_UINT64, current_byte_offset))

            elif self.transfersmodel.get_value(initer, 10) != size:
                self.transfersmodel.set_value(
                    initer, 6, self.get_hsize(current_byte_offset, size))
                self.transfersmodel.set_value(
                    initer, 10, GObject.Value(GObject.TYPE_UINT64, size))

            if self.transfersmodel.get_value(initer, 13) != queue_position:
                self.transfersmodel.set_value(
                    initer, 4, self.get_hqueue_position(queue_position))
                self.transfersmodel.set_value(
                    initer, 13, GObject.Value(GObject.TYPE_UINT,
                                              queue_position))

            return

        expand_user = False
        expand_folder = False

        filename = transfer.filename
        user = transfer.user
        shortfn = filename.split("\\")[-1]

        if self.tree_users != "ungrouped":
            # Group by folder or user

            empty_int = 0
            empty_str = ""

            if user not in self.users:
                # Create Parent if it doesn't exist
                # ProgressRender not visible (last column sets 4th column)
                self.users[user] = self.transfersmodel.insert_with_values(
                    None, -1, self.column_numbers, [
                        user, empty_str, empty_str, empty_str, empty_str,
                        empty_int, empty_str, empty_str, empty_str, empty_str,
                        empty_int, empty_int, empty_int, empty_int, empty_int,
                        empty_int,
                        Transfer(user=user)
                    ])

                if self.tree_users == "folder_grouping":
                    expand_user = True
                else:
                    expand_user = self.expand_button.get_active()

            parent = self.users[user]

            if self.tree_users == "folder_grouping":
                # Group by folder
                """ Paths can be empty if files are downloaded individually, make sure we
                don't add files to the wrong user in the TreeView """
                full_path = path = transfer.path if self.type == "download" else transfer.filename.rsplit(
                    '\\', 1)[0]
                user_path = user + path

                if config.sections["ui"]["reverse_file_paths"]:
                    path = self.path_separator.join(
                        reversed(path.split(self.path_separator)))

                if user_path not in self.paths:
                    self.paths[
                        user_path] = self.transfersmodel.insert_with_values(
                            self.users[user], -1, self.column_numbers, [
                                user, path, empty_str, empty_str, empty_str,
                                empty_int, empty_str, empty_str, empty_str,
                                empty_str, empty_int, empty_int, empty_int,
                                empty_int, empty_int, empty_int,
                                Transfer(user=user, path=full_path)
                            ])
                    expand_folder = self.expand_button.get_active()

                parent = self.paths[user_path]
        else:
            # No grouping
            # We use this list to get the total number of users
            self.users.setdefault(user, set()).add(transfer)
            parent = None

        # Add a new transfer
        if self.tree_users == "folder_grouping":
            # Group by folder, path not visible
            path = ""
        else:
            path = transfer.path if self.type == "download" else transfer.filename.rsplit(
                '\\', 1)[0]

            if config.sections["ui"]["reverse_file_paths"]:
                path = self.path_separator.join(
                    reversed(path.split(self.path_separator)))

        iterator = self.transfersmodel.insert_with_values(
            parent, -1, self.column_numbers,
            (user, path, shortfn, self.translate_status(status),
             self.get_hqueue_position(queue_position),
             self.get_percent(current_byte_offset, size),
             self.get_hsize(current_byte_offset, size), hspeed, helapsed,
             self.get_hleft(left), GObject.Value(GObject.TYPE_UINT64, size),
             GObject.Value(GObject.TYPE_UINT64, current_byte_offset),
             GObject.Value(GObject.TYPE_UINT64, speed),
             GObject.Value(GObject.TYPE_UINT,
                           queue_position), elapsed, left, transfer))
        transfer.iterator = iterator
        self.update_num_users_files()

        if expand_user:
            self.Transfers.expand_row(
                self.transfersmodel.get_path(self.users[user]), False)

        if expand_folder:
            self.Transfers.expand_row(
                self.transfersmodel.get_path(self.paths[user_path]), False)

    def retry_transfers(self):
        for transfer in self.selected_transfers:
            getattr(self.frame.np.transfers, "retry_" + self.type)(transfer)

    def abort_transfers(self, clear=False):

        for transfer in self.selected_transfers:
            if transfer.status != "Finished":
                self.frame.np.transfers.abort_transfer(transfer,
                                                       send_fail_message=True)

                if not clear:
                    transfer.status = self.aborted_status
                    self.update(transfer)

            if clear:
                self.remove_specific(transfer, update_parent=False)

        self.update_parent_rows()
        self.update_num_users_files()

    def remove_specific(self,
                        transfer,
                        cleartreeviewonly=False,
                        update_parent=True):

        user = transfer.user

        if self.tree_users == "ungrouped" and user in self.users:
            # No grouping
            self.users[user].discard(transfer)

            if not self.users[user]:
                del self.users[user]

        if transfer in self.frame.np.transfers.transfer_request_times:
            del self.frame.np.transfers.transfer_request_times[transfer]

        if not cleartreeviewonly:
            self.transfer_list.remove(transfer)

        if transfer.iterator is not None:
            self.transfersmodel.remove(transfer.iterator)

        if update_parent:
            self.update_parent_rows(transfer)
            self.update_num_users_files()

    def clear_transfers(self, status):

        for transfer in self.transfer_list.copy():
            if transfer.status in status:
                self.frame.np.transfers.abort_transfer(transfer,
                                                       send_fail_message=True)
                self.remove_specific(transfer)

    def clear(self):

        self.users.clear()
        self.paths.clear()
        self.selected_transfers.clear()
        self.selected_users.clear()
        self.transfersmodel.clear()

        for transfer in self.transfer_list:
            transfer.iterator = None

    def add_popup_menu_user(self, popup, user):

        popup.setup_user_menu(user)
        popup.add_items(("", None), ("#" + _("Select User's Transfers"),
                                     self.on_select_user_transfers, user))
        popup.update_model()
        popup.toggle_user_items()

    def populate_popup_menu_users(self):

        self.popup_menu_users.clear()

        if not self.selected_users:
            return

        # Multiple users, create submenus for each user
        if len(self.selected_users) > 1:
            for user in self.selected_users:
                popup = PopupMenu(self.frame)
                self.add_popup_menu_user(popup, user)
                self.popup_menu_users.add_items((">" + user, popup))
                self.popup_menu_users.update_model()
            return

        # Single user, add items directly to "User(s)" submenu
        user = next(iter(self.selected_users), None)
        self.add_popup_menu_user(self.popup_menu_users, user)

    def on_expand_tree(self, widget):

        expand_button_icon = getattr(self.frame,
                                     "Expand%ssImage" % self.type.title())
        expanded = widget.get_active()

        if expanded:
            icon_name = "go-up-symbolic"
            self.Transfers.expand_all()

        else:
            icon_name = "go-down-symbolic"
            collapse_treeview(self.Transfers, self.tree_users)

        expand_button_icon.set_property("icon-name", icon_name)

        config.sections["transfers"]["%ssexpanded" % self.type] = expanded
        config.write_configuration()

    def on_toggle_tree(self, action, state):

        mode = state.get_string()
        active = mode != "ungrouped"

        config.sections["transfers"]["group%ss" % self.type] = mode
        self.Transfers.set_show_expanders(active)
        self.expand_button.set_visible(active)

        self.tree_users = mode

        if self.transfer_list:
            self.rebuild_transfers()

        action.set_state(state)

    @staticmethod
    def on_tooltip(widget, pos_x, pos_y, _keyboard_mode, tooltip):
        return show_file_path_tooltip(widget,
                                      pos_x,
                                      pos_y,
                                      tooltip,
                                      16,
                                      transfer=True)

    def on_popup_menu(self, menu, _widget):

        self.select_transfers()
        num_selected_transfers = len(self.selected_transfers)
        menu.set_num_selected_files(num_selected_transfers)

        self.populate_popup_menu_users()

    def on_row_activated(self, _treeview, _path, _column):

        self.select_transfers()
        action = config.sections["transfers"]["%s_doubleclick" % self.type]

        if action == 1:  # Send to Player
            self.on_play_files()

        elif action == 2:  # Open in File Manager
            self.on_open_file_manager()

        elif action == 3:  # Search
            self.on_file_search()

        elif action == 4:  # Pause / Abort
            self.abort_transfers()

        elif action == 5:  # Clear
            self.abort_transfers(clear=True)

        elif action == 6:  # Resume / Retry
            self.retry_transfers()

        elif action == 7:  # Browse Folder
            self.on_browse_folder()

    def on_select_user_transfers(self, *args):

        if not self.selected_users:
            return

        selected_user = args[-1]

        sel = self.Transfers.get_selection()
        fmodel = self.Transfers.get_model()
        sel.unselect_all()

        iterator = fmodel.get_iter_first()

        select_user_row_iter(fmodel, sel, 0, selected_user, iterator)

        self.select_transfers()

    def on_abort_transfers_accelerator(self, *_args):
        """ T: abort transfer """

        self.select_transfers()
        self.abort_transfers()
        return True

    def on_retry_transfers_accelerator(self, *_args):
        """ R: retry transfers """

        self.select_transfers()
        self.retry_transfers()
        return True

    def on_clear_transfers_accelerator(self, *_args):
        """ Delete: clear transfers """

        self.select_transfers()
        self.abort_transfers(clear=True)
        return True

    def on_file_properties_accelerator(self, *_args):
        """ Alt+Return: show file properties dialog """

        self.select_transfers()
        self.on_file_properties()
        return True

    def on_file_properties(self, *_args):

        data = []
        selected_size = 0

        for transfer in self.selected_transfers:
            fullname = transfer.filename
            filename = fullname.split("\\")[-1]
            directory = fullname.rsplit("\\", 1)[0]
            file_size = transfer.size
            selected_size += file_size

            data.append({
                "user": transfer.user,
                "fn": fullname,
                "filename": filename,
                "directory": directory,
                "path": transfer.path,
                "queue_position": transfer.queue_position,
                "speed": transfer.speed,
                "size": file_size,
                "bitrate": transfer.bitrate,
                "length": transfer.length
            })

        if data:
            FileProperties(self.frame,
                           data,
                           total_size=selected_size,
                           download_button=False).show()

    def on_copy_file_path(self, *_args):

        transfer = next(iter(self.selected_transfers), None)

        if transfer:
            copy_text(transfer.filename)

    def on_retry_transfer(self, *_args):
        self.select_transfers()
        self.retry_transfers()

    def on_abort_transfer(self, *_args):
        self.select_transfers()
        self.abort_transfers()

    def on_clear_transfer(self, *_args):
        self.select_transfers()
        self.abort_transfers(clear=True)

    def on_clear_response(self, dialog, response_id, data):

        dialog.destroy()

        if response_id == 2:
            if data == "queued":
                self.clear_transfers(["Queued"])

            elif data == "all":
                self.clear()

    def on_clear_queued(self, *_args):
        self.clear_transfers(["Queued"])

    def on_clear_finished(self, *_args):
        self.clear_transfers(["Finished"])