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()
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()
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()
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()
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()
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"])
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)
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