Ejemplo n.º 1
0
    def __init__(self, frame):

        IconNotebook.__init__(self, frame, frame.search_notebook, "search")
        self.notebook.connect("switch-page", self.on_switch_search_page)

        self.modes = {
            "global": _("_Global"),
            "buddies": _("_Buddies"),
            "rooms": _("_Rooms"),
            "user": _("_User")
        }

        mode_menu = PopupMenu(frame)
        mode_menu.add_items(
            ("O" + self.modes["global"], "win.searchmode", "global"),
            ("O" + self.modes["buddies"], "win.searchmode", "buddies"),
            ("O" + self.modes["rooms"], "win.searchmode", "rooms"),
            ("O" + self.modes["user"], "win.searchmode", "user"))
        mode_menu.update_model()
        frame.SearchMode.set_menu_model(mode_menu.model)
        frame.SearchModeLabel.set_label(self.modes["global"])

        if Gtk.get_major_version() == 4:
            frame.SearchMode.get_first_child().get_style_context().add_class(
                "arrow-button")

        CompletionEntry(frame.RoomSearchEntry,
                        frame.RoomSearchCombo.get_model())
        CompletionEntry(frame.SearchEntry, frame.SearchCombo.get_model())

        self.wish_list = WishList(frame, self)
        self.populate_search_history()
        self.update_visuals()
Ejemplo n.º 2
0
    def __init__(self, chats, user):

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

        self.user = user
        self.chats = chats
        self.frame = chats.frame

        self.opened = False
        self.offline_message = False
        self.status = 0

        if user in self.frame.np.user_statuses:
            self.status = self.frame.np.user_statuses[user] or 0

        # Text Search
        TextSearchBar(self.ChatScroll,
                      self.SearchBar,
                      self.SearchEntry,
                      controller_widget=self.Main,
                      focus_widget=self.ChatLine)

        self.chat_textview = TextView(self.ChatScroll, font="chatfont")

        # Chat Entry
        ChatEntry(self.frame, self.ChatLine, chats.completion, user,
                  slskmessages.MessageUser,
                  self.frame.np.privatechats.send_message,
                  self.frame.np.privatechats.CMDS)

        self.Log.set_active(config.sections["logging"]["privatechat"])

        self.toggle_chat_buttons()

        self.popup_menu_user_chat = PopupMenu(self.frame,
                                              self.ChatScroll,
                                              connect_events=False)
        self.popup_menu_user_tab = PopupMenu(self.frame, None,
                                             self.on_popup_menu_user)

        for menu in (self.popup_menu_user_chat, self.popup_menu_user_tab):
            menu.setup_user_menu(user, page="privatechat")
            menu.add_items(
                ("", None),
                ("#" + _("Close All Tabs…"), self.on_close_all_tabs),
                ("#" + _("_Close Tab"), self.on_close))

        popup = PopupMenu(self.frame, self.ChatScroll, self.on_popup_menu_chat)
        popup.add_items(
            ("#" + _("Find…"), self.on_find_chat_log),
            ("", None),
            ("#" + _("Copy"), self.chat_textview.on_copy_text),
            ("#" + _("Copy Link"), self.chat_textview.on_copy_link),
            ("#" + _("Copy All"), self.chat_textview.on_copy_all_text),
            ("", None),
            ("#" + _("View Chat Log"), self.on_view_chat_log),
            ("#" + _("Delete Chat Log…"), self.on_delete_chat_log),
            ("", None),
            ("#" + _("Clear Message View"),
             self.chat_textview.on_clear_all_text),
            ("", None),
            (">" + _("User"), self.popup_menu_user_tab),
        )

        self.create_tags()
        self.update_visuals()

        self.read_private_log()
Ejemplo n.º 3
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()
Ejemplo n.º 4
0
class RoomList(UserInterface):
    def __init__(self, frame):

        super().__init__("ui/popovers/roomlist.ui")

        self.frame = frame
        self.room_iters = {}
        self.initializing_feed = False

        self.room_model = Gtk.ListStore(str, int, Pango.Weight,
                                        Pango.Underline)

        self.room_filter = self.room_model.filter_new()
        self.room_filter.set_visible_func(self.room_match_function)
        self.room_model_filtered = Gtk.TreeModelSort(model=self.room_filter)
        self.list_view.set_model(self.room_model_filtered)

        self.column_numbers = list(range(self.room_model.get_n_columns()))
        attribute_columns = (2, 3)
        self.cols = initialise_columns(
            frame, None, self.list_view,
            ["room", _("Room"), 260, "text", attribute_columns],
            ["users", _("Users"), 100, "number", attribute_columns])
        self.cols["room"].set_sort_column_id(0)
        self.cols["users"].set_sort_column_id(1)

        self.popup_room = None
        self.popup_menu = PopupMenu(self.frame, self.list_view,
                                    self.on_popup_menu)
        self.popup_menu.add_items(("#" + _("Join Room"), self.on_popup_join),
                                  ("#" + _("Leave Room"), self.on_popup_leave),
                                  ("", None),
                                  ("#" + _("Disown Private Room"),
                                   self.on_popup_private_room_disown),
                                  ("#" + _("Cancel Room Membership"),
                                   self.on_popup_private_room_dismember))

        self.private_room_check.set_active(
            config.sections["server"]["private_chatrooms"])
        self.private_room_check.connect("toggled",
                                        self.on_toggle_accept_private_room)

        Accelerator("<Primary>f", self.popover, self.on_search_accelerator)
        CompletionEntry(frame.ChatroomsEntry, self.room_model, column=0)

        if Gtk.get_major_version() == 4:
            frame.RoomList.get_first_child().get_style_context().add_class(
                "arrow-button")

        frame.RoomList.set_popover(self.popover)

    @staticmethod
    def get_selected_room(treeview):

        model, iterator = treeview.get_selection().get_selected()

        if iterator is None:
            return None

        return model.get_value(iterator, 0)

    @staticmethod
    def private_rooms_sort(model, iter1, iter2, _column):

        try:
            private1 = model.get_value(iter1, 2) * 10000
            private1 += model.get_value(iter1, 1)
        except Exception:
            private1 = 0

        try:
            private2 = model.get_value(iter2, 2) * 10000
            private2 += model.get_value(iter2, 1)
        except Exception:
            private2 = 0

        return (private1 > private2) - (private1 < private2)

    def room_match_function(self, model, iterator, _data=None):

        query = self.search_entry.get_text().lower()

        if not query:
            return True

        value = model.get_value(iterator, 0)

        if query in value.lower():
            return True

        return False

    def set_room_list(self, rooms, owned_rooms, other_private_rooms):

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

        self.clear()

        for room, users in owned_rooms:
            self.update_room(room, users, private=True, owned=True)

        for room, users in other_private_rooms:
            self.update_room(room, users, private=True)

        for room, users in rooms:
            self.update_room(room, users)

        self.room_model.set_default_sort_func(self.private_rooms_sort)

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

    def toggle_feed_check(self, active):

        self.initializing_feed = True
        self.feed_check.set_active(active)
        self.initializing_feed = False

    def update_room(self, room, user_count, private=False, owned=False):

        iterator = self.room_iters.get(room)

        if iterator is not None:
            self.room_model.set_value(iterator, 1, user_count)
            return

        text_weight = Pango.Weight.BOLD if private else Pango.Weight.NORMAL
        text_underline = Pango.Underline.SINGLE if owned else Pango.Underline.NONE

        self.room_iters[room] = self.room_model.insert_with_valuesv(
            -1, self.column_numbers,
            [room, user_count, text_weight, text_underline])

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

        room = self.get_selected_room(treeview)

        if room is not None and room not in self.frame.np.chatrooms.joined_rooms:
            self.popup_room = room
            self.on_popup_join()

    def on_popup_menu(self, menu, widget):

        if self.room_model is None:
            return True

        room = self.get_selected_room(widget)
        self.popup_room = room

        menu.actions[_("Join Room")].set_enabled(
            room not in self.frame.np.chatrooms.joined_rooms)
        menu.actions[_("Leave Room")].set_enabled(
            room in self.frame.np.chatrooms.joined_rooms)

        menu.actions[_("Disown Private Room")].set_enabled(
            self.frame.np.chatrooms.is_private_room_owned(room))
        menu.actions[_("Cancel Room Membership")].set_enabled(
            self.frame.np.chatrooms.is_private_room_member(room))
        return False

    def on_popup_join(self, *_args):
        self.frame.np.chatrooms.request_join_room(self.popup_room)
        self.popover.hide()

    def on_show_chat_feed(self, *_args):

        if self.initializing_feed:
            return

        if self.feed_check.get_active():
            self.frame.np.chatrooms.request_join_public_room()
            self.popover.hide()
            return

        self.frame.np.chatrooms.request_leave_public_room()

    def on_popup_private_room_disown(self, *_args):
        self.frame.np.chatrooms.request_private_room_disown(self.popup_room)

    def on_popup_private_room_dismember(self, *_args):
        self.frame.np.chatrooms.request_private_room_dismember(self.popup_room)

    def on_popup_leave(self, *_args):
        self.frame.np.chatrooms.request_leave_room(self.popup_room)

    def on_search_room(self, *_args):
        self.room_filter.refilter()

    def on_refresh(self, *_args):
        self.frame.np.chatrooms.request_room_list()

    def on_toggle_accept_private_room(self, *_args):
        self.frame.np.chatrooms.request_private_room_toggle(
            self.private_room_check.get_active())

    def on_search_accelerator(self, *_args):
        """ Ctrl+F: Search rooms """

        self.search_entry.grab_focus()
        return True

    def update_visuals(self):
        for widget in list(self.__dict__.values()):
            update_widget_visuals(widget)

    def clear(self):
        self.room_model.clear()
        self.room_iters.clear()
Ejemplo n.º 5
0
    def __init__(self, userinfos, user):

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

        self.userinfos = userinfos
        self.frame = userinfos.frame

        self.info_bar = InfoBar(self.InfoBar, Gtk.MessageType.INFO)
        self.descr_textview = TextView(self.descr)
        self.UserLabel.set_text(user)

        if Gtk.get_major_version() == 4:
            self.picture = Gtk.Picture(can_shrink=False,
                                       halign=Gtk.Align.CENTER,
                                       valign=Gtk.Align.CENTER)
            self.picture_view.set_child(self.picture)

            self.scroll_controller = Gtk.EventControllerScroll(
                flags=Gtk.EventControllerScrollFlags.VERTICAL)
            self.scroll_controller.connect("scroll", self.on_scroll)
            self.picture_view.add_controller(self.scroll_controller)

        else:
            self.picture = Gtk.Image(visible=True)
            self.picture_view.add(self.picture)
            self.picture_view.connect("scroll-event", self.on_scroll_event)

        self.user = user
        self.picture_data = None
        self.zoom_factor = 5
        self.actual_zoom = 0

        # Set up likes list
        self.likes_store = Gtk.ListStore(str)

        self.like_column_numbers = list(range(
            self.likes_store.get_n_columns()))
        cols = initialise_columns(
            self.frame, None, self.Likes,
            ["likes", _("Likes"), 0, "text", None])
        cols["likes"].set_sort_column_id(0)

        self.likes_store.set_sort_column_id(0, Gtk.SortType.ASCENDING)
        self.Likes.set_model(self.likes_store)

        # Set up dislikes list
        self.hates_store = Gtk.ListStore(str)

        self.hate_column_numbers = list(range(
            self.hates_store.get_n_columns()))
        cols = initialise_columns(
            self.frame, None, self.Hates,
            ["dislikes", _("Dislikes"), 0, "text", None])
        cols["dislikes"].set_sort_column_id(0)

        self.hates_store.set_sort_column_id(0, Gtk.SortType.ASCENDING)
        self.Hates.set_model(self.hates_store)

        # Popup menus
        self.user_popup = popup = PopupMenu(self.frame, None,
                                            self.on_tab_popup)
        popup.setup_user_menu(user, page="userinfo")
        popup.add_items(("", None),
                        ("#" + _("Close All Tabs…"), self.on_close_all_tabs),
                        ("#" + _("_Close Tab"), self.on_close))

        def get_interest_items(popup):
            return (("$" + _("I _Like This"), self.on_like_recommendation,
                     popup), ("$" + _("I _Dislike This"),
                              self.on_dislike_recommendation, popup),
                    ("", None), ("#" + _("_Search for Item"),
                                 self.on_interest_recommend_search, popup))

        popup = PopupMenu(self.frame, self.Likes, self.on_popup_interest_menu)
        popup.add_items(*get_interest_items(popup))

        popup = PopupMenu(self.frame, self.Hates, self.on_popup_interest_menu)
        popup.add_items(*get_interest_items(popup))

        popup = PopupMenu(self.frame, self.picture_view)
        popup.add_items(("#" + _("Zoom 1:1"), self.make_zoom_normal),
                        ("#" + _("Zoom In"), self.make_zoom_in),
                        ("#" + _("Zoom Out"), self.make_zoom_out), ("", None),
                        ("#" + _("Save Picture"), self.on_save_picture))

        self.update_visuals()
Ejemplo n.º 6
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"])
Ejemplo n.º 7
0
class ChatRoom(UserInterface):
    def __init__(self, chatrooms, room, users):

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

        self.chatrooms = chatrooms
        self.frame = chatrooms.frame
        self.room = room

        if Gtk.get_major_version() == 4:
            self.ChatPaned.set_resize_start_child(True)
            self.ChatPaned.set_shrink_start_child(False)
            self.ChatPaned.set_resize_end_child(False)
            self.ChatPanedSecond.set_shrink_end_child(False)
        else:
            self.ChatPaned.child_set_property(self.ChatPanedSecond, "resize",
                                              True)
            self.ChatPaned.child_set_property(self.ChatPanedSecond, "shrink",
                                              False)
            self.ChatPaned.child_set_property(self.UserView, "resize", False)
            self.ChatPanedSecond.child_set_property(self.ChatView, "shrink",
                                                    False)

        self.tickers = Tickers()
        self.room_wall = RoomWall(self.frame, self)
        self.leaving = False
        self.opened = False

        self.users = {}

        # Log Text Search
        TextSearchBar(self.RoomLog, self.LogSearchBar, self.LogSearchEntry)

        self.log_textview = TextView(self.RoomLog, font="chatfont")

        # Chat Text Search
        TextSearchBar(self.ChatScroll,
                      self.ChatSearchBar,
                      self.ChatSearchEntry,
                      controller_widget=self.ChatView,
                      focus_widget=self.ChatEntry)

        self.chat_textview = TextView(self.ChatScroll, font="chatfont")

        # Chat Entry
        ChatEntry(self.frame,
                  self.ChatEntry,
                  chatrooms.completion,
                  room,
                  slskmessages.SayChatroom,
                  self.frame.np.chatrooms.send_message,
                  self.frame.np.chatrooms.CMDS,
                  is_chatroom=True)

        self.Log.set_active(config.sections["logging"]["chatrooms"])
        if not self.Log.get_active():
            self.Log.set_active(
                self.room in config.sections["logging"]["rooms"])

        self.AutoJoin.set_active(room in config.sections["server"]["autojoin"])

        self.toggle_chat_buttons()

        if room not in config.sections["columns"]["chat_room"]:
            config.sections["columns"]["chat_room"][room] = {}

        self.usersmodel = Gtk.ListStore(
            Gio.Icon,  # (0)  status_icon
            str,  # (1)  flag
            str,  # (2)  username
            str,  # (3)  h_speed
            str,  # (4)  h_files
            int,  # (5)  status
            GObject.TYPE_UINT,  # (6)  avgspeed
            GObject.TYPE_UINT,  # (7)  files
            str,  # (8)  country
            Pango.Weight,  # (9)  username_weight
            Pango.Underline  # (10) username_underline
        )
        self.UserList.set_model(self.usersmodel)

        self.column_numbers = list(range(self.usersmodel.get_n_columns()))
        attribute_columns = (9, 10)
        self.cols = cols = initialise_columns(
            self.frame, ("chat_room", room), self.UserList,
            ["status", _("Status"), 25, "icon", None],
            ["country", _("Country"), 25, "icon", None],
            ["user", _("User"), 155, "text", attribute_columns],
            ["speed", _("Speed"), 100, "number", None],
            ["files", _("Files"), -1, "number", None])

        cols["status"].set_sort_column_id(5)
        cols["country"].set_sort_column_id(8)
        cols["user"].set_sort_column_id(2)
        cols["speed"].set_sort_column_id(6)
        cols["files"].set_sort_column_id(7)

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

        for userdata in users:
            self.add_user_row(userdata)

        self.usersmodel.set_sort_column_id(2, Gtk.SortType.ASCENDING)

        self.popup_menu_private_rooms_chat = PopupMenu(self.frame)
        self.popup_menu_private_rooms_list = PopupMenu(self.frame)

        self.popup_menu_user_chat = PopupMenu(self.frame,
                                              self.ChatScroll,
                                              connect_events=False)
        self.popup_menu_user_list = PopupMenu(self.frame, self.UserList,
                                              self.on_popup_menu_user)

        for menu, menu_private_rooms in ((self.popup_menu_user_chat,
                                          self.popup_menu_private_rooms_chat),
                                         (self.popup_menu_user_list,
                                          self.popup_menu_private_rooms_list)):
            menu.setup_user_menu()
            menu.add_items(
                ("", None),
                ("#" + _("Sear_ch User's Files"), menu.on_search_user),
                (">" + _("Private Rooms"), menu_private_rooms))

        PopupMenu(self.frame, self.RoomLog, self.on_popup_menu_log).add_items(
            ("#" + _("Find…"), self.on_find_activity_log), ("", None),
            ("#" + _("Copy"), self.log_textview.on_copy_text),
            ("#" + _("Copy All"), self.log_textview.on_copy_all_text),
            ("", None), ("#" + _("Clear Activity View"),
                         self.log_textview.on_clear_all_text), ("", None),
            ("#" + _("_Leave Room"), self.on_leave_room))

        PopupMenu(
            self.frame, self.ChatScroll, self.on_popup_menu_chat).add_items(
                ("#" + _("Find…"), self.on_find_room_log), ("", None),
                ("#" + _("Copy"), self.chat_textview.on_copy_text),
                ("#" + _("Copy Link"), self.chat_textview.on_copy_link),
                ("#" + _("Copy All"), self.chat_textview.on_copy_all_text),
                ("", None), ("#" + _("View Room Log"), self.on_view_room_log),
                ("#" + _("Delete Room Log…"), self.on_delete_room_log),
                ("", None), ("#" + _("Clear Message View"),
                             self.chat_textview.on_clear_all_text),
                ("#" + _("_Leave Room"), self.on_leave_room))

        self.tab_menu = PopupMenu(self.frame)
        self.tab_menu.add_items(("#" + _("_Leave Room"), self.on_leave_room))

        self.setup_public_feed()
        self.ChatEntry.grab_focus()

        self.count_users()
        self.create_tags()
        self.update_visuals()
        self.read_room_logs()

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

    def setup_public_feed(self):

        if self.room != "Public ":
            return

        for widget in (self.ActivityView, self.UserView, self.ChatEntry,
                       self.ShowRoomWall, self.ShowChatHelp):
            widget.hide()

        for widget in (self.AutoJoin, self.Log):
            self.RoomOptions.remove(widget)
            self.ChatEntryBox.add(widget)

        self.Speech.set_active(
            False)  # Public feed is jibberish and too fast for TTS
        self.ChatEntry.set_sensitive(False)
        self.ChatEntryBox.set_halign(Gtk.Align.END)

    def add_user_row(self, userdata):

        username = userdata.username
        status = userdata.status
        country = userdata.country or ""  # country can be None, ensure string is used
        status_icon = get_status_icon(status) or get_status_icon(0)
        flag_icon = get_flag_icon_name(country)

        # Request user's IP address, so we can get the country and ignore messages by IP
        self.frame.np.queue.append(slskmessages.GetPeerAddress(username))

        h_speed = ""
        avgspeed = userdata.avgspeed

        if avgspeed > 0:
            h_speed = human_speed(avgspeed)

        files = userdata.files
        h_files = humanize(files)

        weight = Pango.Weight.NORMAL
        underline = Pango.Underline.NONE

        if self.room in self.frame.np.chatrooms.private_rooms:
            if username == self.frame.np.chatrooms.private_rooms[
                    self.room]["owner"]:
                weight = Pango.Weight.BOLD
                underline = Pango.Underline.SINGLE

            elif username in self.frame.np.chatrooms.private_rooms[
                    self.room]["operators"]:
                weight = Pango.Weight.BOLD
                underline = Pango.Underline.NONE

        iterator = self.usersmodel.insert_with_valuesv(
            -1, self.column_numbers, [
                status_icon, flag_icon, username, h_speed, h_files, status,
                GObject.Value(GObject.TYPE_UINT, avgspeed),
                GObject.Value(GObject.TYPE_UINT, files), country, weight,
                underline
            ])

        self.users[username] = iterator

    def read_room_logs(self):

        numlines = config.sections["logging"]["readroomlines"]

        if not numlines:
            return

        filename = clean_file(self.room) + ".log"
        path = os.path.join(config.sections["logging"]["roomlogsdir"],
                            filename)

        try:
            self.append_log_lines(path, numlines)
        except OSError:
            pass

    def append_log_lines(self, path, numlines):

        with open(path, "rb") as lines:
            # Only show as many log lines as specified in config
            lines = deque(lines, numlines)
            login = config.sections["server"]["login"]

            for line in lines:
                try:
                    line = line.decode("utf-8")

                except UnicodeDecodeError:
                    line = line.decode("latin-1")

                user = None
                tag = None
                usertag = None

                if "[" in line and "] " in line:
                    start = line.find("[")
                    end = line.find("] ")

                    if end > start:
                        user = line[start + 1:end].strip()
                        usertag = self.get_user_tag(user)

                        if user == login:
                            tag = self.tag_local

                        elif login.lower() in line[end:].lower():
                            tag = self.tag_hilite

                        else:
                            tag = self.tag_remote

                elif "* " in line:
                    tag = self.tag_action

                if user != login:
                    self.chat_textview.append_line(
                        self.frame.np.privatechats.censor_chat(line),
                        tag,
                        username=user,
                        usertag=usertag,
                        timestamp_format="",
                        scroll=False)
                else:
                    self.chat_textview.append_line(line,
                                                   tag,
                                                   username=user,
                                                   usertag=usertag,
                                                   timestamp_format="",
                                                   scroll=False)

            if lines:
                self.chat_textview.append_line(_("--- old messages above ---"),
                                               self.tag_hilite,
                                               scroll=False)

    def populate_user_menu(self, user, menu, menu_private_rooms):

        menu.set_user(user)
        menu.toggle_user_items()
        menu.populate_private_rooms(menu_private_rooms)

        private_rooms_enabled = (menu_private_rooms.items
                                 and menu.user != self.frame.np.login_username)
        menu.actions[_("Private Rooms")].set_enabled(private_rooms_enabled)

    def on_find_activity_log(self, *_args):
        self.LogSearchBar.set_search_mode(True)

    def on_find_room_log(self, *_args):
        self.ChatSearchBar.set_search_mode(True)

    @staticmethod
    def get_selected_username(treeview):

        model, iterator = treeview.get_selection().get_selected()

        if iterator is None:
            return None

        return model.get_value(iterator, 2)

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

        user = self.get_selected_username(treeview)

        if user is not None:
            self.frame.np.privatechats.show_user(user)
            self.frame.change_main_page("private")

    def on_popup_menu_user(self, menu, treeview):
        user = self.get_selected_username(treeview)
        self.populate_user_menu(user, menu, self.popup_menu_private_rooms_list)

    def on_popup_menu_log(self, menu, _textview):
        menu.actions[_("Copy")].set_enabled(
            self.log_textview.get_has_selection())

    def on_popup_menu_chat(self, menu, _textview):
        menu.actions[_("Copy")].set_enabled(
            self.chat_textview.get_has_selection())
        menu.actions[_("Copy Link")].set_enabled(
            bool(self.chat_textview.get_url_for_selected_pos()))

    def toggle_chat_buttons(self):
        self.Speech.set_visible(config.sections["ui"]["speechenabled"])

    def ticker_set(self, msg):

        self.tickers.clear_tickers()

        for user, message in msg.msgs:
            if self.frame.np.network_filter.is_user_ignored(user) or \
                    self.frame.np.network_filter.is_user_ip_ignored(user):
                # User ignored, ignore Ticker messages
                continue

            self.tickers.add_ticker(user, message)

    def ticker_add(self, msg):

        user = msg.user

        if self.frame.np.network_filter.is_user_ignored(
                user) or self.frame.np.network_filter.is_user_ip_ignored(user):
            # User ignored, ignore Ticker messages
            return

        self.tickers.add_ticker(msg.user, msg.msg)

    def ticker_remove(self, msg):
        self.tickers.remove_ticker(msg.user)

    def show_notification(self, login, user, text, tag, public=False):

        if user == login:
            return

        mentioned = (tag == self.tag_hilite)

        if mentioned and config.sections["notifications"][
                "notification_popup_chatroom_mention"]:
            self.frame.notifications.new_text_notification(
                text,
                title=_("%(user)s mentioned you in the %(room)s room") % {
                    "user": user,
                    "room": self.room
                },
                priority=Gio.NotificationPriority.HIGH)

        self.chatrooms.request_tab_hilite(self.Main, mentioned)

        if (self.chatrooms.get_current_page() == self.chatrooms.page_num(
                self.Main)
                and self.frame.current_page_id == self.chatrooms.page_id
                and self.frame.MainWindow.is_active()):
            # Don't show notifications if the chat is open and the window is in use
            return

        if mentioned:
            # We were mentioned, update tray icon and show urgency hint
            self.frame.notifications.add("rooms", user, self.room)
            return

        if not public and config.sections["notifications"][
                "notification_popup_chatroom"]:
            # Don't show notifications for "Public " room, they're too noisy
            self.frame.notifications.new_text_notification(
                text,
                title=_("Message by %(user)s in the %(room)s room") % {
                    "user": user,
                    "room": self.room
                },
                priority=Gio.NotificationPriority.HIGH)

    @staticmethod
    def find_whole_word(word, text, after=0):
        """ Return the position of the first mention of word that is not a subword """

        if word not in text:
            return -1

        word_boundaries = [' '] + PUNCTUATION
        whole = False
        start = 0

        while not whole and start > -1:
            start = text.find(word, after)
            after = start + len(word)

            whole = (
                (text[after] if after < len(text) else " ") in word_boundaries
                and (text[start - 1] if start > 0 else " ") in word_boundaries)

        return start if whole else -1

    def say_chat_room(self, msg, public=False):

        user = msg.user

        if self.frame.np.network_filter.is_user_ignored(user):
            return

        if self.frame.np.network_filter.is_user_ip_ignored(user):
            return

        login_username = self.frame.np.login_username
        text = msg.msg

        if user == login_username:
            tag = self.tag_local
        elif self.find_whole_word(login_username.lower(), text.lower()) > -1:
            tag = self.tag_hilite
        else:
            tag = self.tag_remote

        if text.startswith("/me "):
            tag = self.tag_action
            line = "* %s %s" % (user, text[4:])
            speech = line[2:]
        else:
            line = "[%s] %s" % (user, text)
            speech = text

        if public:
            line = "%s | %s" % (msg.room, line)

        line = "\n-- ".join(line.split("\n"))
        usertag = self.get_user_tag(user)
        timestamp_format = config.sections["logging"]["rooms_timestamp"]

        if user != login_username:
            self.chat_textview.append_line(
                self.frame.np.privatechats.censor_chat(line),
                tag,
                username=user,
                usertag=usertag,
                timestamp_format=timestamp_format)

            if self.Speech.get_active():
                self.frame.np.notifications.new_tts(
                    config.sections["ui"]["speechrooms"], {
                        "room": msg.room,
                        "user": user,
                        "message": speech
                    })

        else:
            self.chat_textview.append_line(line,
                                           tag,
                                           username=user,
                                           usertag=usertag,
                                           timestamp_format=timestamp_format)

        self.show_notification(login_username, user, speech, tag, public)

        if self.Log.get_active():
            timestamp_format = config.sections["logging"]["log_timestamp"]

            log.write_log(config.sections["logging"]["roomlogsdir"],
                          self.room,
                          line,
                          timestamp_format=timestamp_format)

    def echo_message(self, text, message_type):

        tag = self.tag_action
        timestamp_format = config.sections["logging"]["rooms_timestamp"]

        if hasattr(self, "tag_" + str(message_type)):
            tag = getattr(self, "tag_" + str(message_type))

        self.chat_textview.append_line(text,
                                       tag,
                                       timestamp_format=timestamp_format)

    def user_joined_room(self, userdata):

        username = userdata.username

        if username in self.users:
            return

        # Add to completion list, and completion drop-down
        self.chatrooms.completion.add_completion(username)

        if not self.frame.np.network_filter.is_user_ignored(username) and \
                not self.frame.np.network_filter.is_user_ip_ignored(username):
            self.log_textview.append_line(
                _("%s joined the room") % username, self.tag_log)

        self.add_user_row(userdata)

        self.update_user_tag(username)
        self.count_users()

    def user_left_room(self, username):

        if username not in self.users:
            return

        # Remove from completion list, and completion drop-down
        if username not in (i[0]
                            for i in config.sections["server"]["userlist"]):
            self.chatrooms.completion.remove_completion(username)

        if not self.frame.np.network_filter.is_user_ignored(username) and \
                not self.frame.np.network_filter.is_user_ip_ignored(username):
            self.log_textview.append_line(
                _("%s left the room") % username, self.tag_log)

        self.usersmodel.remove(self.users[username])
        del self.users[username]

        self.update_user_tag(username)
        self.count_users()

    def count_users(self):

        user_count = len(self.users)
        self.LabelPeople.set_text(str(user_count))
        self.chatrooms.roomlist.update_room(self.room, user_count)

    def get_user_stats(self, user, avgspeed, files):

        iterator = self.users.get(user)

        if iterator is None:
            return

        h_speed = ""

        if avgspeed > 0:
            h_speed = human_speed(avgspeed)

        self.usersmodel.set_value(iterator, 3, h_speed)
        self.usersmodel.set_value(iterator, 4, humanize(files))
        self.usersmodel.set_value(iterator, 6,
                                  GObject.Value(GObject.TYPE_UINT, avgspeed))
        self.usersmodel.set_value(iterator, 7,
                                  GObject.Value(GObject.TYPE_UINT, files))

    def get_user_status(self, msg):

        user = msg.user
        iterator = self.users.get(user)

        if iterator is None:
            return

        status = msg.status

        if status == self.usersmodel.get_value(iterator, 5):
            return

        status_icon = get_status_icon(status)

        if status_icon is None:
            return

        if status == 1:
            action = _("%s has gone away")
        elif status == 2:
            action = _("%s has returned")
        else:
            # If we reach this point, the server did something wrong. The user should have
            # left the room before an offline status is sent.
            return

        if not self.frame.np.network_filter.is_user_ignored(user) and \
                not self.frame.np.network_filter.is_user_ip_ignored(user):
            self.log_textview.append_line(action % user, self.tag_log)

        self.usersmodel.set_value(iterator, 0, status_icon)
        self.usersmodel.set_value(iterator, 5, status)

        self.update_user_tag(user)

    def set_user_country(self, user, country):

        iterator = self.users.get(user)

        if iterator is None:
            return

        if self.usersmodel.get_value(iterator, 8) == country:
            # Country didn't change, no need to update
            return

        flag_icon = get_flag_icon_name(country)

        if not flag_icon:
            return

        self.usersmodel.set_value(iterator, 1, flag_icon)
        self.usersmodel.set_value(iterator, 8, country)

    def update_visuals(self):

        for widget in list(self.__dict__.values()):
            update_widget_visuals(widget)

        self.room_wall.update_visuals()

    def user_name_event(self, pos_x, pos_y, user):

        menu = self.popup_menu_user_chat
        menu.update_model()
        self.populate_user_menu(user, menu, self.popup_menu_private_rooms_chat)
        menu.popup(pos_x, pos_y, button=1)

    def create_tags(self):

        self.tag_log = self.log_textview.create_tag("chatremote")

        self.tag_remote = self.chat_textview.create_tag("chatremote")
        self.tag_local = self.chat_textview.create_tag("chatlocal")
        self.tag_action = self.chat_textview.create_tag("chatme")
        self.tag_hilite = self.chat_textview.create_tag("chathilite")

        self.tag_users = {}

    def get_user_tag(self, username):

        if username not in self.tag_users:
            self.tag_users[username] = self.chat_textview.create_tag(
                callback=self.user_name_event, username=username)
            self.update_user_tag(username)

        return self.tag_users[username]

    def update_user_tag(self, username):

        if username not in self.tag_users:
            return

        if username not in self.users:
            color = "useroffline"
        else:
            status = self.usersmodel.get_value(self.users[username], 5)
            color = get_user_status_color(status)

        self.chat_textview.update_tag(self.tag_users[username], color)

    def update_tags(self):

        for tag in (self.tag_remote, self.tag_local, self.tag_action,
                    self.tag_hilite, self.tag_log):
            self.chat_textview.update_tag(tag)

        for tag in self.tag_users.values():
            self.chat_textview.update_tag(tag)

    def save_columns(self):
        save_columns("chat_room",
                     self.UserList.get_columns(),
                     subpage=self.room)

    def server_disconnect(self):

        self.usersmodel.clear()
        self.users.clear()
        self.count_users()

        if (self.room not in config.sections["server"]["autojoin"]
                and self.room in config.sections["columns"]["chat_room"]):
            del config.sections["columns"]["chat_room"][self.room]

        self.chat_textview.append_line(_("--- disconnected ---"),
                                       self.tag_hilite)

        for username in self.tag_users:
            self.update_user_tag(username)

    def rejoined(self, users):

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

        for userdata in users:
            username = userdata.username

            if username in self.users:
                self.usersmodel.remove(self.users[username])

            self.add_user_row(userdata)

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

        # Spit this line into chat log
        self.chat_textview.append_line(_("--- reconnected ---"),
                                       self.tag_hilite)

        # Update user count
        self.count_users()

        # Build completion list
        self.set_completion_list(list(self.frame.np.chatrooms.completion_list))

        # Update all username tags in chat log
        for username in self.tag_users:
            self.update_user_tag(username)

    def on_autojoin(self, widget):

        autojoin = config.sections["server"]["autojoin"]
        active = widget.get_active()

        if not active and self.room in autojoin:
            autojoin.remove(self.room)

        elif active and self.room not in autojoin:
            autojoin.append(self.room)

        config.write_configuration()

    def on_leave_room(self, *_args):

        if self.leaving:
            return

        self.leaving = True

        if self.room in config.sections["columns"]["chat_room"]:
            del config.sections["columns"]["chat_room"][self.room]

        if self.room == "Public ":
            self.chatrooms.roomlist.feed_check.set_active(False)
            return

        self.frame.np.chatrooms.request_leave_room(self.room)

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

        status_tooltip = show_user_status_tooltip(widget, pos_x, pos_y,
                                                  tooltip, 5)
        country_tooltip = show_country_tooltip(widget,
                                               pos_x,
                                               pos_y,
                                               tooltip,
                                               8,
                                               strip_prefix="")

        if status_tooltip:
            return status_tooltip

        if country_tooltip:
            return country_tooltip

        return None

    def on_log_toggled(self, widget):

        if not widget.get_active():
            if self.room in config.sections["logging"]["rooms"]:
                config.sections["logging"]["rooms"].remove(self.room)
            return

        if self.room not in config.sections["logging"]["rooms"]:
            config.sections["logging"]["rooms"].append(self.room)

    def on_view_room_log(self, *_args):
        open_log(config.sections["logging"]["roomlogsdir"], self.room)

    def on_delete_room_log_response(self, dialog, response_id, _data):

        dialog.destroy()

        if response_id == 2:
            delete_log(config.sections["logging"]["roomlogsdir"], self.room)
            self.log_textview.clear()
            self.chat_textview.clear()

    def on_delete_room_log(self, *_args):

        option_dialog(
            parent=self.frame.MainWindow,
            title=_('Delete Logged Messages?'),
            message=
            _('Do you really want to permanently delete all logged messages for this room?'
              ),
            callback=self.on_delete_room_log_response)

    def on_ignore_users_settings(self, *_args):
        self.frame.on_settings(page='IgnoredUsers')

    def set_completion_list(self, completion_list):

        # We want to include users for this room only
        if config.sections["words"]["roomusers"]:
            completion_list += self.users.keys()

        # No duplicates
        completion_list = list(set(completion_list))
        completion_list.sort(key=lambda v: v.lower())

        self.chatrooms.completion.set_completion_list(completion_list)
Ejemplo n.º 8
0
class UserBrowse(UserInterface):
    def __init__(self, userbrowses, user):

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

        self.userbrowses = userbrowses
        self.frame = userbrowses.frame
        self.user = user
        self.local_shares_type = None
        self.queued_path = None

        self.shares = {}
        self.dir_iters = {}
        self.dir_user_data = {}
        self.file_iters = {}

        self.selected_folder = None
        self.selected_folder_size = 0
        self.selected_files = {}
        self.num_selected_files = 0

        self.search_list = []
        self.query = None
        self.search_position = 0

        self.info_bar = InfoBar(self.InfoBar, Gtk.MessageType.INFO)

        # Setup FolderTreeView
        self.dir_store = Gtk.TreeStore(str)
        self.dir_column_numbers = list(range(self.dir_store.get_n_columns()))
        cols = initialise_columns(
            self.frame, None, self.FolderTreeView,
            ["folder", _("Folder"), -1, "text", None])
        cols["folder"].set_sort_column_id(0)

        self.FolderTreeView.get_selection().connect("changed",
                                                    self.on_select_dir)
        self.FolderTreeView.set_model(self.dir_store)

        # Popup Menu (FolderTreeView)
        self.user_popup = popup = PopupMenu(self.frame, None,
                                            self.on_tab_popup)
        popup.setup_user_menu(user, page="userbrowse")
        popup.add_items(("", None),
                        ("#" + _("_Save Shares List to Disk"), self.on_save),
                        ("#" + _("Close All Tabs…"), self.on_close_all_tabs),
                        ("#" + _("_Close Tab"), self.on_close))

        self.folder_popup_menu = PopupMenu(self.frame, self.FolderTreeView,
                                           self.on_folder_popup_menu)

        if user == config.sections["server"]["login"]:
            self.folder_popup_menu.add_items(
                ("#" + _("Upload Folder…"), self.on_upload_directory_to),
                ("#" + _("Upload Folder & Subfolder(s)…"),
                 self.on_upload_directory_recursive_to), ("", None),
                ("#" + _("Open in File _Manager"), self.on_file_manager),
                ("#" + _("F_ile Properties"), self.on_file_properties, True),
                ("", None),
                ("#" + _("Copy _Folder Path"), self.on_copy_folder_path),
                ("#" + _("Copy _URL"), self.on_copy_dir_url), ("", None),
                (">" + _("User"), self.user_popup))
        else:
            self.folder_popup_menu.add_items(
                ("#" + _("_Download Folder"), self.on_download_directory),
                ("#" + _("Download Folder _To…"),
                 self.on_download_directory_to),
                ("#" + _("Download Folder & Subfolder(s)"),
                 self.on_download_directory_recursive),
                ("#" + _("Download Folder & Subfolder(s) To…"),
                 self.on_download_directory_recursive_to), ("", None),
                ("#" + _("F_ile Properties"), self.on_file_properties, True),
                ("", None),
                ("#" + _("Copy _Folder Path"), self.on_copy_folder_path),
                ("#" + _("Copy _URL"), self.on_copy_dir_url), ("", None),
                (">" + _("User"), self.user_popup))

        # Setup FileTreeView
        self.treeview_name = "user_browse"
        self.file_store = Gtk.ListStore(
            str,  # (0) file name
            str,  # (1) hsize
            str,  # (2) hbitrate
            str,  # (3) hlength
            GObject.TYPE_UINT64,  # (4) size
            GObject.TYPE_UINT64,  # (5) bitrate
            GObject.TYPE_UINT64  # (6) length
        )

        self.file_column_offsets = {}
        self.file_column_numbers = list(range(self.file_store.get_n_columns()))
        cols = initialise_columns(
            self.frame, "user_browse", self.FileTreeView,
            ["filename", _("Filename"), 600, "text", None],
            ["size", _("Size"), 100, "number", None],
            ["bitrate", _("Bitrate"), 100, "number", None],
            ["length", _("Length"), 100, "number", None])
        cols["filename"].set_sort_column_id(0)
        cols["size"].set_sort_column_id(4)
        cols["bitrate"].set_sort_column_id(5)
        cols["length"].set_sort_column_id(6)

        self.FileTreeView.get_selection().connect("changed",
                                                  self.on_select_file)
        self.FileTreeView.set_model(self.file_store)

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

        # Popup Menu (FileTreeView)
        self.file_popup_menu = PopupMenu(self.frame, self.FileTreeView,
                                         self.on_file_popup_menu)

        if user == config.sections["server"]["login"]:
            self.file_popup_menu.add_items(
                ("#" + "selected_files", None), ("", None),
                ("#" + _("Up_load File(s)…"), self.on_upload_files),
                ("#" + _("Upload Folder…"), self.on_upload_directory_to),
                ("", None), ("#" + _("Send to _Player"), self.on_play_files),
                ("#" + _("Open in File _Manager"), self.on_file_manager),
                ("#" + _("F_ile Properties"), self.on_file_properties),
                ("", None),
                ("#" + _("Copy _File Path"), self.on_copy_file_path),
                ("#" + _("Copy _URL"), self.on_copy_url), ("", None),
                (">" + _("User"), self.user_popup))
        else:
            self.file_popup_menu.add_items(
                ("#" + "selected_files", None), ("", None),
                ("#" + _("_Download File(s)"), self.on_download_files),
                ("#" + _("Download File(s) _To…"), self.on_download_files_to),
                ("", None),
                ("#" + _("_Download Folder"), self.on_download_directory),
                ("#" + _("Download Folder _To…"),
                 self.on_download_directory_to), ("", None),
                ("#" + _("F_ile Properties"), self.on_file_properties),
                ("", None),
                ("#" + _("Copy _File Path"), self.on_copy_file_path),
                ("#" + _("Copy _URL"), self.on_copy_url), ("", None),
                (">" + _("User"), self.user_popup))

        # Key Bindings (FolderTreeView)
        Accelerator("Left", self.FolderTreeView,
                    self.on_folder_collapse_accelerator)
        Accelerator("minus", self.FolderTreeView,
                    self.on_folder_collapse_accelerator)  # "-"
        Accelerator("backslash", self.FolderTreeView,
                    self.on_folder_collapse_sub_accelerator)  # "\"
        Accelerator(
            "equal", self.FolderTreeView,
            self.on_folder_expand_sub_accelerator)  # "=" (for US/UK laptop)
        Accelerator("Right", self.FolderTreeView,
                    self.on_folder_expand_accelerator)

        Accelerator("<Shift>Return", self.FolderTreeView,
                    self.on_folder_focus_filetree_accelerator)  # brwse into
        Accelerator("<Primary>Return", self.FolderTreeView,
                    self.on_folder_transfer_to_accelerator)  # w/to prompt
        Accelerator("<Shift><Primary>Return", self.FolderTreeView,
                    self.on_folder_transfer_accelerator)  # no prmt
        Accelerator("<Primary><Alt>Return", self.FolderTreeView,
                    self.on_folder_open_manager_accelerator)
        Accelerator("<Alt>Return", self.FolderTreeView,
                    self.on_file_properties_accelerator, True)

        # Key Bindings (FileTreeView)
        for accelerator in ("<Shift>Tab", "BackSpace",
                            "backslash"):  # Avoid header, navigate up, "\"
            Accelerator(accelerator, self.FileTreeView,
                        self.on_focus_folder_accelerator)

        Accelerator("Left", self.FileTreeView,
                    self.on_focus_folder_left_accelerator)

        Accelerator("<Shift>Return", self.FileTreeView,
                    self.on_file_transfer_multi_accelerator)  # multi activate
        Accelerator("<Primary>Return", self.FileTreeView,
                    self.on_file_transfer_to_accelerator)  # with to prompt
        Accelerator("<Shift><Primary>Return", self.FileTreeView,
                    self.on_file_transfer_accelerator)  # no prompt
        Accelerator("<Primary><Alt>Return", self.FileTreeView,
                    self.on_file_open_manager_accelerator)
        Accelerator("<Alt>Return", self.FileTreeView,
                    self.on_file_properties_accelerator)

        # Key Bindings (General)
        for widget in (self.Main, self.FolderTreeView, self.FileTreeView):
            Accelerator("<Primary>f", widget,
                        self.on_search_accelerator)  # Find focus

        for widget in (self.Main, self.SearchEntry):
            Accelerator("<Primary>g", widget,
                        self.on_search_next_accelerator)  # Next search match
            Accelerator("<Shift><Primary>g", widget,
                        self.on_search_previous_accelerator)

        Accelerator("Escape", self.SearchEntry,
                    self.on_search_escape_accelerator)
        Accelerator("F3", self.Main, self.on_search_next_accelerator)
        Accelerator("<Shift>F3", self.Main,
                    self.on_search_previous_accelerator)

        Accelerator(
            "<Primary>backslash", self.Main,
            self.on_expand_accelerator)  # expand / collapse all (button)
        Accelerator("F5", self.Main, self.on_refresh_accelerator)
        Accelerator("<Primary>r", self.Main,
                    self.on_refresh_accelerator)  # Refresh
        Accelerator("<Primary>s", self.Main,
                    self.on_save_accelerator)  # Save Shares List

        self.ExpandButton.set_active(True)
        self.update_visuals()

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

    def update_visuals(self):

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

    """ Folder/File Views """

    def clear_model(self):

        self.query = None
        self.search_list.clear()

        self.selected_folder = None
        self.selected_files.clear()

        self.shares.clear()

        self.dir_iters.clear()
        self.dir_user_data.clear()
        self.dir_store.clear()

        self.file_iters.clear()
        self.file_store.clear()

    def make_new_model(self, shares, private_shares=None):

        self.clear_model()
        private_size = num_private_folders = 0

        # Generate the directory tree and select first directory
        size, num_folders = self.create_folder_tree(shares)

        if private_shares:
            shares = shares + private_shares
            private_size, num_private_folders = self.create_folder_tree(
                private_shares, private=True)

        # Sort files
        for _folder, files in shares:
            files.sort()

        self.shares = dict(shares)

        self.AmountShared.set_text(human_size(size + private_size))
        self.NumDirectories.set_text(str(num_folders + num_private_folders))

        if self.ExpandButton.get_active():
            self.FolderTreeView.expand_all()
        else:
            self.FolderTreeView.collapse_all()

        iterator = self.dir_store.get_iter_first()

        if iterator:
            path = self.dir_store.get_path(iterator)
            self.FolderTreeView.set_cursor(path)

        self.set_finished()

    def create_folder_tree(self, shares, private=False):

        total_size = 0

        if not shares:
            num_folders = 0
            return total_size, num_folders

        # Sort folders
        shares.sort()

        for folder, files in shares:
            current_path = None
            root_processed = False

            for subfolder in folder.split('\\'):
                parent = self.dir_iters.get(current_path)

                if not root_processed:
                    current_path = subfolder
                    root_processed = True
                else:
                    current_path = '\\'.join([current_path, subfolder])

                if current_path in self.dir_iters:
                    # Folder was already added to tree
                    continue

                if not subfolder:
                    # Most likely a root folder
                    subfolder = '\\'

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

                self.dir_iters[
                    current_path] = iterator = self.dir_store.insert_with_values(
                        parent, -1, self.dir_column_numbers, [subfolder])
                self.dir_user_data[iterator.user_data] = current_path

            for filedata in files:
                total_size += filedata[2]

        return total_size, len(shares)

    def browse_queued_path(self):

        if self.queued_path is None:
            return

        folder, filename = self.queued_path.rsplit("\\", 1)
        iterator = self.dir_iters.get(folder)

        if not iterator:
            return

        self.queued_path = None

        # Scroll to the requested folder
        path = self.dir_store.get_path(iterator)
        self.FolderTreeView.expand_to_path(path)
        self.FolderTreeView.set_cursor(path)
        self.FolderTreeView.scroll_to_cell(path, None, True, 0.5, 0.5)

        iterator = self.file_iters.get(filename)

        if not iterator:
            return

        # Scroll to the requested file
        path = self.file_store.get_path(iterator)
        self.FileTreeView.set_cursor(path)
        self.FileTreeView.scroll_to_cell(path, None, True, 0.5, 0.5)

    def shared_file_list(self, msg):

        self.make_new_model(msg.list, msg.privatelist)
        self.info_bar.set_visible(False)

        if msg.list or msg.privatelist:
            self.browse_queued_path()

        else:
            self.info_bar.show_message(
                _("User's list of shared files is empty. Either the user is not sharing anything, "
                  "or they are sharing files privately."))

        self.set_finished()

    def show_connection_error(self):

        self.info_bar.show_message(
            _("Unable to request shared files from user. Either the user is offline, you both have "
              "a closed listening port, or there's a temporary connectivity issue."
              ))

        self.set_finished()

    def set_in_progress(self, indeterminate_progress):

        if not indeterminate_progress:
            self.progressbar1.set_fraction(0.0)
        else:
            self.progressbar1.set_fraction(0.5)

        self.RefreshButton.set_sensitive(False)

    def message_progress(self, msg):

        if msg.total == 0 or msg.position == 0:
            fraction = 0.0
        elif msg.position >= msg.total:
            fraction = 1.0
        else:
            fraction = float(msg.position) / msg.total

        self.progressbar1.set_fraction(fraction)

    def set_finished(self):

        self.userbrowses.request_tab_hilite(self.Main)
        self.progressbar1.set_fraction(1.0)
        self.RefreshButton.set_sensitive(True)

    def set_directory(self, iter_user_data):

        directory = self.dir_user_data.get(iter_user_data)

        if directory is None or self.selected_folder == directory:
            return

        self.selected_folder = directory
        self.file_store.clear()
        self.file_iters.clear()

        files = self.shares.get(directory)

        if not files:
            return

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

        selected_folder_size = 0

        for file in files:
            # Filename, HSize, Bitrate, HLength, Size, Length
            filename = file[1]
            size = file[2]
            selected_folder_size += size
            h_bitrate, bitrate, h_length, length = get_result_bitrate_length(
                size, file[4])

            file_row = [
                filename,
                human_size(size), h_bitrate, h_length,
                GObject.Value(GObject.TYPE_UINT64, size),
                GObject.Value(GObject.TYPE_UINT64, bitrate),
                GObject.Value(GObject.TYPE_UINT64, length)
            ]

            self.file_iters[filename] = self.file_store.insert_with_valuesv(
                -1, self.file_column_numbers, file_row)

        self.selected_folder_size = selected_folder_size

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

    def select_files(self):

        self.selected_files.clear()
        model, paths = self.FileTreeView.get_selection().get_selected_rows()

        for path in paths:
            iterator = model.get_iter(path)
            rawfilename = model.get_value(iterator, 0)
            filesize = model.get_value(iterator, 4)

            self.selected_files[rawfilename] = filesize

    def grab_view_focus(self):

        if self.num_selected_files >= 1:
            self.FileTreeView.grab_focus()
            return

        self.FolderTreeView.grab_focus()

    """ Search """

    def rebuild_search_matches(self):

        self.search_list.clear()

        for directory, files in self.shares.items():

            if self.query in directory.lower(
            ) and directory not in self.search_list:
                self.search_list.append(directory)
                continue

            for file_data in files:
                if self.query in file_data[1].lower(
                ) and directory not in self.search_list:
                    self.search_list.append(directory)

        self.search_list.sort()

    def select_search_match_folder(self):

        directory = self.search_list[self.search_position]
        path = self.dir_store.get_path(self.dir_iters[directory])

        self.FolderTreeView.expand_to_path(path)
        self.FolderTreeView.set_cursor(path)

    def select_search_match_files(self):

        result_files = []
        found_first_match = False

        for filepath in self.file_iters:
            if self.query in filepath.lower():
                result_files.append(filepath)

        result_files.sort()

        selection = self.FileTreeView.get_selection()
        selection.unselect_all()

        for filepath in result_files:
            # Select each matching file in folder
            path = self.file_store.get_path(self.file_iters[filepath])
            selection.select_path(path)

            if found_first_match:
                continue

            # Position cursor at first match
            self.FileTreeView.scroll_to_cell(path, None, True, 0.5, 0.5)
            found_first_match = True

    def find_search_matches(self, reverse=False):

        query = self.SearchEntry.get_text().lower()

        if not query:
            return False

        if self.query != query:
            # New search query, rebuild result list
            self.search_position = 0
            self.query = query

            self.rebuild_search_matches()
        else:
            # Increment/decrement search position
            self.search_position += -1 if reverse else 1

        if not self.search_list:
            return False

        if self.search_position < 0:
            self.search_position = len(self.search_list) - 1

        elif self.search_position >= len(self.search_list):
            self.search_position = 0

        # Set active folder
        self.select_search_match_folder()

        # Get matching files in the current folder
        self.select_search_match_files()
        return True

    """ Callbacks (FolderTreeView) """

    def on_select_dir(self, selection):

        _model, iterator = selection.get_selected()

        if iterator is None:
            return

        self.set_directory(iterator.user_data)

    def on_folder_popup_menu(self, menu, _treeview):
        self.user_popup.toggle_user_items()
        menu.actions[_("F_ile Properties")].set_enabled(
            bool(self.shares.get(self.selected_folder)))

    def on_download_directory(self, *_args):

        if self.selected_folder is not None:
            self.frame.np.userbrowse.download_folder(self.user,
                                                     self.selected_folder,
                                                     self.shares)

    def on_download_directory_recursive(self, *_args):
        self.frame.np.userbrowse.download_folder(self.user,
                                                 self.selected_folder,
                                                 self.shares,
                                                 prefix="",
                                                 recurse=True)

    def on_download_directory_to_selected(self, selected, recurse):

        try:
            self.frame.np.userbrowse.download_folder(self.user,
                                                     self.selected_folder,
                                                     self.shares,
                                                     prefix=os.path.join(
                                                         selected, ""),
                                                     recurse=recurse)
        except OSError:  # failed to open
            log.add('Failed to open %r for reading', selected)  # notify user

    def on_download_directory_to(self, *_args, recurse=False):

        if recurse:
            str_title = _(
                "Select Destination for Downloading Multiple Folders")
        else:
            str_title = _("Select Destination Folder")

        choose_dir(parent=self.frame.MainWindow,
                   title=str_title,
                   callback=self.on_download_directory_to_selected,
                   callback_data=recurse,
                   initialdir=config.sections["transfers"]["downloaddir"],
                   multichoice=False)

    def on_download_directory_recursive_to(self, *_args):
        self.on_download_directory_to(recurse=True)

    def on_upload_directory_to_response(self, dialog, response_id, recurse):

        user = dialog.get_response_value()
        folder = self.selected_folder
        dialog.destroy()

        if response_id != Gtk.ResponseType.OK:
            return

        if not user or folder is None:
            return

        self.frame.np.userbrowse.send_upload_attempt_notification(user)
        self.frame.np.userbrowse.upload_folder(user,
                                               folder,
                                               self.shares,
                                               recurse=recurse)

    def on_upload_directory_to(self, *_args, recurse=False):

        folder = self.selected_folder

        if folder is None:
            return

        users = []
        for row in config.sections["server"]["userlist"]:
            if row and isinstance(row, list):
                user = str(row[0])
                users.append(user)

        users.sort()

        if recurse:
            str_title = _("Upload Folder (with Subfolders) To User")
        else:
            str_title = _("Upload Folder To User")

        entry_dialog(
            parent=self.frame.MainWindow,
            title=str_title,
            message=_('Enter the name of the user you want to upload to:'),
            callback=self.on_upload_directory_to_response,
            callback_data=recurse,
            droplist=users)

    def on_upload_directory_recursive_to(self, *_args):
        self.on_upload_directory_to(recurse=True)

    def on_copy_folder_path(self, *_args):

        if self.selected_folder is None:
            return

        copy_text(self.selected_folder)

    def on_copy_dir_url(self, *_args):

        if self.selected_folder is None:
            return

        path = self.selected_folder + '\\'
        url = self.frame.np.userbrowse.get_soulseek_url(self.user, path)
        copy_text(url)

    """ Key Bindings (FolderTreeView) """

    def on_folder_row_activated(self, _treeview, path, _column):

        if path is None:
            return

        # Keyboard accessibility support for <Return> key behaviour
        if self.FolderTreeView.row_expanded(path):
            expandable = self.FolderTreeView.collapse_row(path)
        else:
            expandable = self.FolderTreeView.expand_row(path, False)

        if not expandable and len(self.file_store) > 0:
            # This is the deepest level, so move focus over to Files if there are any
            self.FileTreeView.grab_focus()

        # Note: Other Folder actions are handled by Accelerator functions [Shift/Ctrl/Alt+Return]
        # TODO: Mouse double-click actions will need *_args for keycode state & mods [Ctrl/Alt+DblClick]

    def on_folder_collapse_accelerator(self, *_args):
        """ Left: collapse row
            Shift+Left (Gtk) | "-" | "/" (Gtk) | """

        path, _focus_column = self.FolderTreeView.get_cursor()

        if path is None:
            return False

        self.FolderTreeView.collapse_row(path)
        return True

    def on_folder_expand_accelerator(self, *_args):
        """ Right: expand row
            Shift+Right (Gtk) | "+" (Gtk) |    """

        path, _focus_column = self.FolderTreeView.get_cursor()

        if path is None:
            return False

        expandable = self.FolderTreeView.expand_row(path, False)

        if not expandable and len(self.file_store) > 0:
            self.FileTreeView.grab_focus()

        return True

    def on_folder_collapse_sub_accelerator(self, *_args):
        """ \backslash: collapse or expand to show subs """

        path, _focus_column = self.FolderTreeView.get_cursor()

        if path is None:
            return False

        self.FolderTreeView.collapse_row(path)  # show 2nd level
        self.FolderTreeView.expand_row(path, False)
        return True

    def on_folder_expand_sub_accelerator(self, *_args):
        """ =equal: expand only (dont move focus)   """

        path, _focus_column = self.FolderTreeView.get_cursor()

        if path is None:
            return False

        self.FolderTreeView.expand_row(path, False)
        return True

    def on_folder_focus_filetree_accelerator(self, *_args):
        """ Shift+Enter: focus selection over FileTree  """

        if len(self.file_store) >= 1:
            self.FileTreeView.grab_focus()
            return True

        self.on_folder_expand_sub_accelerator()
        return True

    def on_folder_transfer_to_accelerator(self, *_args):
        """ Ctrl+Enter: Upload Folder To...
                        Download Folder Into...         """

        if self.user == config.sections["server"]["login"]:
            if len(self.file_store) >= 1:
                self.on_upload_directory_to()
            else:
                self.on_upload_directory_recursive_to()

        elif len(self.file_store) >= 1:
            self.on_download_directory_to()

        return True

    def on_folder_transfer_accelerator(self, *_args):
        """ Shift+Ctrl+Enter: Upload Folder Recursive To...
            (without prompt)  Download Folder           """

        if self.user == config.sections["server"]["login"]:
            self.on_folder_expand_sub_accelerator()
            self.on_upload_directory_recursive_to()
            return True

        if len(self.file_store) <= 0:
            # don't risk accidental recursive download
            self.on_folder_expand_sub_accelerator()
            return True

        self.on_download_directory()  # without prompt
        return True

    def on_folder_open_manager_accelerator(self, *_args):
        """ Ctrl+Alt+Enter: Open folder in File Manager... """

        if self.user != config.sections["server"]["login"]:
            return False

        self.on_file_manager()
        return True

    """ Callbacks (FileTreeView) """

    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.file_column_offsets[col_title] == offset:
            return

        self.file_column_offsets[col_title] = offset
        save_columns(self.treeview_name, self.FileTreeView.get_columns())

    def on_select_file(self, selection):
        self.num_selected_files = selection.count_selected_rows()

    def on_file_popup_menu(self, menu, _widget):

        self.select_files()
        self.num_selected_files = len(self.selected_files)
        menu.set_num_selected_files(self.num_selected_files)

        self.user_popup.toggle_user_items()

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

        folder = self.selected_folder
        files = self.shares.get(folder)

        if not files:
            return

        for file_data in files:
            # Find the wanted file
            if file_data[1] not in self.selected_files:
                continue

            self.frame.np.userbrowse.download_file(self.user,
                                                   folder,
                                                   file_data,
                                                   prefix=prefix)

    def on_download_files_to_selected(self, selected, _data):

        try:
            self.on_download_files(prefix=selected)
        except OSError:  # failed to open
            log.add('failed to open %r for reading', selected)  # notify user

    def on_download_files_to(self, *_args):

        try:
            _path_start, folder = self.selected_folder.rsplit("\\", 1)
        except ValueError:
            folder = self.selected_folder

        download_folder = config.sections["transfers"]["downloaddir"]
        path = os.path.join(download_folder, folder)

        if not os.path.exists(path) or not os.path.isdir(path):
            path = download_folder

        choose_dir(parent=self.frame.MainWindow,
                   title=_("Select Destination Folder for File(s)"),
                   callback=self.on_download_files_to_selected,
                   initialdir=path,
                   multichoice=False)

    def on_upload_files_response(self, dialog, response_id, _data):

        user = dialog.get_response_value()
        folder = self.selected_folder
        dialog.destroy()

        if response_id != Gtk.ResponseType.OK:
            return

        if not user or folder is None:
            return

        self.frame.np.userbrowse.send_upload_attempt_notification(user)

        for basename, size in self.selected_files.items():
            self.frame.np.userbrowse.upload_file(user, folder,
                                                 (None, basename, size))

    def on_upload_files(self, *_args):

        users = []

        for row in config.sections["server"]["userlist"]:
            if row and isinstance(row, list):
                user = str(row[0])
                users.append(user)

        users.sort()
        entry_dialog(
            parent=self.frame.MainWindow,
            title=_('Upload File(s) To User'),
            message=_('Enter the name of the user you want to upload to:'),
            callback=self.on_upload_files_response,
            droplist=users)

    def on_play_files(self, *_args):

        path = self.frame.np.shares.virtual2real(self.selected_folder)

        for basename in self.selected_files:
            playfile = os.sep.join([path, basename])

            if os.path.exists(playfile):
                command = config.sections["players"]["default"]
                open_file_path(playfile, command)

    def on_file_manager(self, *_args):

        if self.selected_folder is None:
            return

        path = self.frame.np.shares.virtual2real(self.selected_folder)
        command = config.sections["ui"]["filemanager"]

        open_file_path(path, command)

    def on_file_properties(self, _action, _state, all_files=False):

        data = []
        folder = self.selected_folder
        selected_size = 0
        selected_length = 0

        if all_files:
            files = self.shares.get(folder)

            if not files:
                return

            for file_data in files:
                filename = file_data[1]
                file_size = file_data[2]
                virtual_path = "\\".join([folder, filename])
                h_bitrate, _bitrate, h_length, length = get_result_bitrate_length(
                    file_size, file_data[4])
                selected_size += file_size
                selected_length += length

                data.append({
                    "user": self.user,
                    "fn": virtual_path,
                    "filename": filename,
                    "directory": folder,
                    "size": file_size,
                    "bitrate": h_bitrate,
                    "length": h_length
                })

        else:
            model, paths = self.FileTreeView.get_selection().get_selected_rows(
            )

            for path in paths:
                iterator = model.get_iter(path)
                filename = model.get_value(iterator, 0)
                file_size = model.get_value(iterator, 4)
                virtual_path = "\\".join([folder, filename])
                selected_size += file_size
                selected_length += model.get_value(iterator, 6)

                data.append({
                    "user": self.user,
                    "fn": virtual_path,
                    "filename": filename,
                    "directory": folder,
                    "size": file_size,
                    "bitrate": model.get_value(iterator, 2),
                    "length": model.get_value(iterator, 3)
                })

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

    def on_copy_file_path(self, *_args):

        if self.selected_folder is None or not self.selected_files:
            return

        text = "\\".join(
            [self.selected_folder,
             next(iter(self.selected_files))])
        copy_text(text)

    def on_copy_url(self, *_args):

        if not self.selected_files:
            return

        path = "\\".join(
            [self.selected_folder,
             next(iter(self.selected_files))])
        url = self.frame.np.userbrowse.get_soulseek_url(self.user, path)
        copy_text(url)

    """ Key Bindings (FileTreeView) """

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

        self.select_files()

        if self.user == config.sections["server"]["login"]:
            self.on_play_files()
        else:
            self.on_download_files()

    def on_focus_folder_left_accelerator(self, *_args):
        """ Left: focus back parent folder (left arrow) """

        _path, column = self.FileTreeView.get_cursor()

        if self.FileTreeView.get_column(0) != column:
            return False  # allow horizontal scrolling

        self.FolderTreeView.grab_focus()
        return True

    def on_focus_folder_accelerator(self, *_args):
        """ Shift+Tab: focus selection back parent folder
            BackSpace | \backslash |                  """

        self.FolderTreeView.grab_focus()
        return True

    def on_file_transfer_to_accelerator(self, *_args):
        """ Ctrl+Enter: Upload File(s) To...
                        Download File(s) Into...  """

        if len(self.file_store) <= 0:  # avoid navigation trap
            self.FolderTreeView.grab_focus()
            return True

        if self.num_selected_files <= 0:  # do folder instead
            self.on_folder_transfer_to_accelerator()
            return True

        self.select_files()

        if self.user == config.sections["server"]["login"]:
            self.on_upload_files()
            return True

        self.on_download_files_to()  # (with prompt, Single or Multi-selection)
        return True

    def on_file_transfer_accelerator(self, *_args):
        """ Shift+Ctrl+Enter: Upload File(s) To...
            (without prompt)  Download File(s) """

        if len(self.file_store) <= 0:
            self.FolderTreeView.grab_focus()  # avoid nav trap
            return True

        self.select_files()

        if self.user == config.sections["server"]["login"]:
            if self.num_selected_files >= 1:
                self.on_upload_files()

            elif self.num_selected_files <= 0:
                self.on_upload_directory_to()

        else:  # [user is not self]
            if self.num_selected_files >= 1:
                self.on_download_files(
                )  # (no prompt, Single or Multi-selection)

            elif self.num_selected_files <= 0:
                self.on_download_directory(
                )  # (without prompt, No-selection=All)

        return True

    def on_file_transfer_multi_accelerator(self, *_args):
        """ Shift+Enter: Send to Player (multiple files)
                         Download Files (multiple)   """

        if len(self.file_store) <= 0:
            self.FolderTreeView.grab_focus()  # avoid nav trap
            return True

        self.select_files()  # support multi-select with Up/Dn keys

        if self.user == config.sections["server"]["login"]:
            self.on_play_files()
        else:
            self.on_download_files()

        return True

    def on_file_open_manager_accelerator(self, *_args):
        """ Ctrl+Alt+Enter: Open in File Manager """

        if self.user == config.sections["server"]["login"]:
            self.on_file_manager()

        else:  # [user is not self]
            self.on_file_properties_accelerator()  # same as Alt+Enter

        return True

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

        if len(self.file_store) <= 0:
            self.FolderTreeView.grab_focus()  # avoid nav trap

        self.on_file_properties(*_args)
        return True

    """ Callbacks (General) """

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

        file_path_tooltip = show_file_path_tooltip(widget, pos_x, pos_y,
                                                   tooltip, 0)

        if file_path_tooltip:
            return file_path_tooltip

        return False

    def on_expand(self, *_args):

        if self.ExpandButton.get_active():
            self.FolderTreeView.expand_all()
            self.expand.set_property("icon-name", "go-up-symbolic")
        else:
            self.FolderTreeView.collapse_all()
            self.expand.set_property("icon-name", "go-down-symbolic")

    def on_tab_popup(self, *_args):
        self.user_popup.toggle_user_items()

    def on_search(self, *_args):
        self.find_search_matches()

    def on_save(self, *_args):
        self.frame.np.userbrowse.save_shares_list_to_disk(
            self.user, list(self.shares.items()))

    def on_refresh(self, *_args):

        self.clear_model()
        self.FolderTreeView.grab_focus()
        self.info_bar.set_visible(False)

        self.set_in_progress(self.indeterminate_progress)
        self.frame.np.userbrowse.browse_user(
            self.user,
            local_shares_type=self.local_shares_type,
            new_request=True)

    def on_close(self, *_args):

        self.clear_model()

        del self.userbrowses.pages[self.user]
        self.frame.np.userbrowse.remove_user(self.user)
        self.userbrowses.remove_page(self.Main)

    def on_close_all_tabs(self, *_args):
        self.userbrowses.remove_all_pages()

    """ Key Bindings (General) """

    def on_expand_accelerator(self, *_args):
        """ Ctrl+\backslash: Expand / Collapse All """

        self.ExpandButton.set_active(not self.ExpandButton.get_active())
        return True

    def on_save_accelerator(self, *_args):
        """ Ctrl+S: Save Shares List """

        self.on_save()
        return True

    def on_refresh_accelerator(self, *_args):
        """ Ctrl+R or F5: Refresh """

        self.on_refresh()
        return True

    def on_search_accelerator(self, *_args):
        """ Ctrl+F: Find """

        self.SearchEntry.grab_focus()
        return True

    def on_search_next_accelerator(self, *_args):
        """ Ctrl+G or F3: Find Next """

        if not self.find_search_matches():
            self.SearchEntry.grab_focus()

        return True

    def on_search_previous_accelerator(self, *_args):
        """ Shift+Ctrl+G or Shift+F3: Find Previous """

        if not self.find_search_matches(reverse=True):
            self.SearchEntry.grab_focus()

        return True

    def on_search_escape_accelerator(self, *_args):
        """ Escape: navigate out of SearchEntry """

        if self.num_selected_files >= 1:
            self.FileTreeView.grab_focus()
        else:
            self.FolderTreeView.grab_focus()

        return True