From 02bc00bfa51462cdaa5b6f2ef379eaf5f71f189c Mon Sep 17 00:00:00 2001 From: bendikro Date: Sat, 18 May 2013 21:12:02 +0200 Subject: [PATCH] Small fixes to path chooser * Handle completion when removing characteres while the popup is visible * Set Apply button sensitive when changing move completed path in options tab --- deluge/core/core.py | 4 +- deluge/path_chooser_common.py | 18 ++- deluge/ui/gtkui/glade/path_combo_chooser.ui | 1 + deluge/ui/gtkui/options_tab.py | 4 + deluge/ui/gtkui/path_chooser.py | 8 +- deluge/ui/gtkui/path_combo_chooser.py | 155 +++++++++++--------- 6 files changed, 109 insertions(+), 81 deletions(-) diff --git a/deluge/core/core.py b/deluge/core/core.py index 710993ad3..ba0953f19 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -860,11 +860,11 @@ class Core(component.Component): return lt.version @export - def get_completion_paths(self, value, hidden_files=False): + def get_completion_paths(self, args): """ Returns the available path completions for the input value. """ - return path_chooser_common.get_completion_paths(value, hidden_files) + return path_chooser_common.get_completion_paths(args) @export(AUTH_LEVEL_ADMIN) def get_known_accounts(self): diff --git a/deluge/path_chooser_common.py b/deluge/path_chooser_common.py index f44d1fbd0..d92731f0b 100644 --- a/deluge/path_chooser_common.py +++ b/deluge/path_chooser_common.py @@ -55,19 +55,23 @@ def is_hidden(filepath): return has_hidden_attribute(filepath) return name.startswith('.') -def get_completion_paths(path_value, hidden_files=False): +def get_completion_paths(args): """ Takes a path value and returns the available completions. If the path_value is a valid path, return all sub-directories. If the path_value is not a valid path, remove the basename from the path and return all sub-directories of path that start with basename. - :param path_value: path to complete - :type path_value: string - :returns: a sorted list of available completions for the input value + :param args: options + :type args: dict + :returns: the args argument containing the available completions for the completion_text :rtype: list """ + args["paths"] = [] + path_value = args["completion_text"] + hidden_files = args["show_hidden_files"] + def get_subdirs(dirname): try: return os.walk(dirname).next()[1] @@ -81,7 +85,7 @@ def get_completion_paths(path_value, hidden_files=False): dirs = get_subdirs(dirname) # No completions available if not dirs: - return [] + return args # path_value ends with path separator so # we only want all the subdirectories @@ -100,4 +104,6 @@ def get_completion_paths(path_value, hidden_files=False): if not p.endswith(os.path.sep): p += os.path.sep matching_dirs.append(p) - return sorted(matching_dirs) + + args["paths"] = sorted(matching_dirs) + return args diff --git a/deluge/ui/gtkui/glade/path_combo_chooser.ui b/deluge/ui/gtkui/glade/path_combo_chooser.ui index af98c4a6a..eeb29f365 100644 --- a/deluge/ui/gtkui/glade/path_combo_chooser.ui +++ b/deluge/ui/gtkui/glade/path_combo_chooser.ui @@ -955,6 +955,7 @@ False True True + diff --git a/deluge/ui/gtkui/options_tab.py b/deluge/ui/gtkui/options_tab.py index 3586a68ec..39bf15831 100644 --- a/deluge/ui/gtkui/options_tab.py +++ b/deluge/ui/gtkui/options_tab.py @@ -70,6 +70,7 @@ class OptionsTab(Tab): self.move_completed_path_chooser.set_sensitive(self.chk_move_completed.get_active()) self.move_completed_hbox.add(self.move_completed_path_chooser) self.move_completed_hbox.show_all() + self.move_completed_path_chooser.connect("text-changed", self._on_path_chooser_text_changed_event) self.prev_torrent_id = None self.prev_status = None @@ -300,3 +301,6 @@ class OptionsTab(Tab): def _on_entry_move_completed_changed(self, widget): if not self.button_apply.is_sensitive(): self.button_apply.set_sensitive(True) + + def _on_path_chooser_text_changed_event(self, widget, path): + self.button_apply.set_sensitive(True) diff --git a/deluge/ui/gtkui/path_chooser.py b/deluge/ui/gtkui/path_chooser.py index 533839c66..5874a5e82 100644 --- a/deluge/ui/gtkui/path_chooser.py +++ b/deluge/ui/gtkui/path_chooser.py @@ -184,8 +184,8 @@ class PathChooser(PathChooserComboBox): if self.paths_config_key and self.paths_config_key in config: self.set_values(config[self.paths_config_key]) - def on_completion(self, value, hidden_files): - def on_paths_cb(paths): - self.complete(value, paths) - d = client.core.get_completion_paths(value, hidden_files=hidden_files) + def on_completion(self, args): + def on_paths_cb(args): + self.complete(args) + d = client.core.get_completion_paths(args) d.addCallback(on_paths_cb) diff --git a/deluge/ui/gtkui/path_combo_chooser.py b/deluge/ui/gtkui/path_combo_chooser.py index fad769a58..91ba93141 100755 --- a/deluge/ui/gtkui/path_combo_chooser.py +++ b/deluge/ui/gtkui/path_combo_chooser.py @@ -76,6 +76,8 @@ def path_without_trailing_path_sep(path): class ValueList(object): + paths_without_trailing_path_sep = False + def get_values_count(self): return len(self.tree_store) @@ -105,7 +107,8 @@ class ValueList(object): self.tree_store.clear() for path in paths: - path = path_without_trailing_path_sep(path) + if self.paths_without_trailing_path_sep: + path = path_without_trailing_path_sep(path) if append: tree_iter = self.tree_store.append([path]) else: @@ -327,6 +330,7 @@ class StoredValuesList(ValueList): self.tree_store = self.builder.get_object("stored_values_tree_store") self.tree_column = self.builder.get_object("stored_values_treeview_column") self.rendererText = self.builder.get_object("stored_values_cellrenderertext") + self.paths_without_trailing_path_sep = False # Add signal handlers self.signal_handlers["on_stored_values_treeview_mouse_button_press_event"] = \ @@ -526,6 +530,7 @@ class PathChooserPopup(object): self.set_max_popup_rows(max_visible_rows) self.popup_window.realize() self.alignment_widget = popup_alignment_widget + self.popup_buttonbox = None # If set, the height of this widget is the minimum height def popup(self): """ @@ -535,8 +540,8 @@ class PathChooserPopup(object): # Entry is not yet visible if not (self.path_entry.flags() & gtk.REALIZED): return - if not self.is_popped_up(): - self.set_window_position_and_size() + #if not self.is_popped_up(): + self.set_window_position_and_size() def popdown(self): if not self.is_popped_up(): @@ -566,7 +571,6 @@ class PathChooserPopup(object): Returns the size of the popup window and the coordinates on the screen. """ - self.popup_buttonbox = self.builder.get_object("buttonbox") # Necessary for the first call, to make treeview.size_request give sensible values #self.popup_window.realize() @@ -580,46 +584,36 @@ class PathChooserPopup(object): y += self.alignment_widget.allocation.y height_extra = 8 - + buttonbox_width = 0 height = self.popup_window.size_request()[1] width = self.popup_window.size_request()[0] - treeview_height = self.treeview.size_request()[1] - treeview_width = self.treeview.size_request()[0] - - if treeview_height > height: - height = treeview_height + height_extra - - butonbox_height = max(self.popup_buttonbox.size_request()[1], self.popup_buttonbox.allocation.height) - butonbox_width = max(self.popup_buttonbox.size_request()[0], self.popup_buttonbox.allocation.width) - - if treeview_height > butonbox_height and treeview_height < height : - height = treeview_height + height_extra - - # After removing an element from the tree store, self.treeview.size_request()[0] - # returns -1 for some reason, so the requested width cannot be used until the treeview - # has been displayed once. - if treeview_width != -1: - width = treeview_width + butonbox_width - # The list is empty, so ignore initial popup width request - # Will be set to the minimum width next - elif len(self.tree_store) == 0: - width = 0 - - # Minimum width is the width of the path entry + width of buttonbox -# if width < self.alignment_widget.allocation.width + butonbox_width: -# width = self.alignment_widget.allocation.width + butonbox_width + if self.popup_buttonbox: + buttonbox_height = max(self.popup_buttonbox.size_request()[1], self.popup_buttonbox.allocation.height) + buttonbox_width = max(self.popup_buttonbox.size_request()[0], self.popup_buttonbox.allocation.width) + treeview_width = self.treeview.size_request()[0] + # After removing an element from the tree store, self.treeview.size_request()[0] + # returns -1 for some reason, so the requested width cannot be used until the treeview + # has been displayed once. + if treeview_width != -1: + width = treeview_width + buttonbox_width + # The list is empty, so ignore initial popup width request + # Will be set to the minimum width next + elif len(self.tree_store) == 0: + width = 0 if width < self.alignment_widget.allocation.width: width = self.alignment_widget.allocation.width # 10 is extra spacing - content_width = self.treeview.size_request()[0] + butonbox_width + 10 + content_width = self.treeview.size_request()[0] + buttonbox_width + 10 - # If self.max_visible_rows is -1, not restriction is set + # Adjust height according to number of list items if len(self.tree_store) > 0 and self.max_visible_rows > 0: # The height for one row in the list self.row_height = self.treeview.size_request()[1] / len(self.tree_store) + # Set height to number of rows + height = len(self.tree_store) * self.row_height + height_extra # Adjust the height according to the max number of rows max_height = self.row_height * self.max_visible_rows # Restrict height to max_visible_rows @@ -629,9 +623,10 @@ class PathChooserPopup(object): # Increase width because of vertical scrollbar content_width += 15 - # Minimum height is the height of the button box - if height < butonbox_height + height_extra: - height = butonbox_height + height_extra + if self.popup_buttonbox: + # Minimum height is the height of the button box + if height < buttonbox_height + height_extra: + height = buttonbox_height + height_extra if content_width > width: width = content_width @@ -680,7 +675,7 @@ class PathChooserPopup(object): Sets the text of the entry to the value in path """ - self.path_entry.set_text(self.tree_store[path][0], set_file_chooser_folder=True) + self.path_entry.set_text(self.tree_store[path][0], set_file_chooser_folder=True, trigger_event=True) if popdown: self.popdown() @@ -731,7 +726,6 @@ class StoredValuesPopup(StoredValuesList, PathChooserPopup): self.builder = builder self.treeview = self.builder.get_object("stored_values_treeview") self.popup_window = self.builder.get_object("stored_values_popup_window") - self.popup_buttonbox = self.builder.get_object("buttonbox") self.button_default = self.builder.get_object("button_default") self.path_entry = path_entry self.text_entry = path_entry.text_entry @@ -740,6 +734,8 @@ class StoredValuesPopup(StoredValuesList, PathChooserPopup): PathChooserPopup.__init__(self, 0, max_visible_rows, popup_alignment_widget) StoredValuesList.__init__(self) + self.popup_buttonbox = self.builder.get_object("buttonbox") + # Add signal handlers self.signal_handlers["on_buttonbox_key_press_event"] = \ self.on_buttonbox_key_press_event @@ -798,8 +794,9 @@ class StoredValuesPopup(StoredValuesList, PathChooserPopup): """ swap = event.state & gtk.gdk.CONTROL_MASK + scroll_window = event.state & gtk.gdk.SHIFT_MASK self.handle_list_scroll(next=event.direction == gdk.SCROLL_DOWN, - set_entry=widget != self.treeview, swap=swap) + set_entry=widget != self.treeview, swap=swap, scroll_window=scroll_window) return True def on_buttonbox_key_press_event(self, widget, event): @@ -854,7 +851,7 @@ class StoredValuesPopup(StoredValuesList, PathChooserPopup): def on_button_default_clicked(self, widget): if self.default_text: - self.set_text(self.default_text) + self.set_text(self.default_text, trigger_event=True) class PathCompletionPopup(CompletionList, PathChooserPopup): """ @@ -901,6 +898,7 @@ class PathCompletionPopup(CompletionList, PathChooserPopup): self.popup_window.grab_add() self.text_entry.grab_focus() self.text_entry.set_position(len(self.path_entry.text_entry.get_text())) + self.set_selected_value(path_without_trailing_path_sep(self.path_entry.get_text()), select_first=True) ################################################### # Callbacks @@ -943,7 +941,6 @@ class PathAutoCompleter(object): self.on_entry_text_delete_text self.signal_handlers["on_entry_text_insert_text"] = \ self.on_entry_text_insert_text - self.accelerator_string = gtk.accelerator_name(keysyms.Tab, 0) def on_entry_text_insert_text(self, entry, new_text, new_text_length, position): @@ -953,22 +950,27 @@ class PathAutoCompleter(object): new_complete_text = cur_text[:pos] + new_text + cur_text[pos:] # Remove all values from the list that do not start with new_complete_text self.completion_popup.reduce_values(new_complete_text) + self.completion_popup.set_selected_value(new_complete_text, select_first=True) if self.completion_popup.is_popped_up(): self.completion_popup.set_window_position_and_size() def on_entry_text_delete_text(self, entry, start, end): """ - Remove the popup when characters are removed + Do completion when characters are removed """ if self.completion_popup.is_popped_up(): - self.completion_popup.popdown() + cur_text = self.path_entry.get_text() + pos = entry.get_position() + new_complete_text = cur_text[:start] + cur_text[end:] + self.do_completion(value=new_complete_text, forward_completion=False) def set_use_popup(self, use): self.use_popup = use def on_completion_popup_window_key_press_event(self, entry, event): """ + Handles key pressed events on the auto-completion popup window """ # If on_completion_treeview_key_press_event handles the event, do nothing ret = self.completion_popup.on_completion_treeview_key_press_event(entry, event) @@ -976,16 +978,12 @@ class PathAutoCompleter(object): return ret keyval = event.keyval state = event.state & gtk.accelerator_get_default_mod_mask() - if self.is_auto_completion_accelerator(keyval, state)\ and self.auto_complete_enabled: values_count = self.completion_popup.get_values_count() - self.do_completion() if values_count == 1: - self.completion_popup.popdown() + self.do_completion() else: - #shift = event.state & gtk.gdk.SHIFT_MASK - #self.completion_popup.handle_list_scroll(next=False if shift else True) self.completion_popup.handle_list_scroll(next=True) return True self.path_entry.text_entry.emit("key-press-event", event) @@ -993,25 +991,35 @@ class PathAutoCompleter(object): def is_auto_completion_accelerator(self, keyval, state): return gtk.accelerator_name(keyval, state.numerator) == self.accelerator_string - def do_completion(self): - value = self.path_entry.get_text() + def do_completion(self, value=None, forward_completion=True): + if not value: + value = self.path_entry.get_text() self.path_entry.text_entry.set_position(len(value)) - paths = self._start_completion(value, hidden_files=self.completion_popup.show_hidden_files) + opts = {} + opts["show_hidden_files"] = self.completion_popup.show_hidden_files + opts["completion_text"] = value + opts["forward_completion"] = forward_completion + self._start_completion(opts) - def _start_completion(self, value, hidden_files): - completion_paths = get_completion_paths(value, hidden_files) - self._end_completion(value, completion_paths) + def _start_completion(self, args): + args = get_completion_paths(args) + self._end_completion(args) - def _end_completion(self, value, paths): - common_prefix = os.path.commonprefix(paths) - if len(common_prefix) > len(value): - self.path_entry.set_text(common_prefix, set_file_chooser_folder=True) + def _end_completion(self, args): + value = args["completion_text"] + paths = args["paths"] + + if args["forward_completion"]: + common_prefix = os.path.commonprefix(paths) + if len(common_prefix) > len(value): + self.path_entry.set_text(common_prefix, set_file_chooser_folder=True, trigger_event=True) self.path_entry.text_entry.set_position(len(self.path_entry.get_text())) self.completion_popup.set_values(paths, preserve_selection=True) + if self.use_popup and len(paths) > 1: self.completion_popup.popup() - elif self.completion_popup.is_popped_up(): + elif self.completion_popup.is_popped_up() and args["forward_completion"]: self.completion_popup.popdown() class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): @@ -1028,6 +1036,7 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): "show-hidden-files-toggled": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), "accelerator-set": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), "max-rows-changed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "text-changed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), } def __init__(self, max_visible_rows=20, auto_complete=True, use_completer_popup=True): @@ -1070,6 +1079,7 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): "on_entry_combobox_hbox_realize": self._on_entry_combobox_hbox_realize, "on_button_open_dialog_clicked": self._on_button_open_dialog_clicked, "on_entry_text_focus_out_event": self._on_entry_text_focus_out_event, + "on_entry_text_changed": self.on_entry_text_changed, } signal_handlers.update(self.signal_handlers) signal_handlers.update(self.auto_completer.signal_handlers) @@ -1082,11 +1092,12 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): """ return self.text_entry.get_text() - def set_text(self, text, set_file_chooser_folder=True, cursor_end=True, default_text=False): + def set_text(self, text, set_file_chooser_folder=True, cursor_end=True, default_text=False, trigger_event=False): """ Set the text for the entry. """ + old_text = self.text_entry.get_text() self.text_entry.set_text(text) self.text_entry.select_region(0, 0) self.text_entry.set_position(len(text) if cursor_end else 0) @@ -1097,13 +1108,15 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): self.tooltips.set_tip(self.button_default, "Restore the default value in the text entry:\n%s" % self.default_text) self.button_default.set_sensitive(True) # Set text for the filechooser dialog button + folder_name = "" if self.show_folder_name_on_button or not self.path_entry_visible: - text = path_without_trailing_path_sep(text) - if not text is "/" and os.path.basename(text): - text = os.path.basename(text) - else: - text = "" - self.folder_name_label.set_text(text) + folder_name = path_without_trailing_path_sep(text) + if not folder_name is "/" and os.path.basename(folder_name): + folder_name = os.path.basename(folder_name) + self.folder_name_label.set_text(folder_name) + # Only trigger event if text has changed + if old_text != text and trigger_event: + self.on_entry_text_changed(self.text_entry) def set_sensitive(self, sensitive): """ @@ -1236,17 +1249,21 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): """ self.auto_completer._start_completion = func - def complete(self, value, paths): + def complete(self, args): """ Perform the auto completion with the provided paths """ - self.auto_completer._end_completion(value, paths) + self.auto_completer._end_completion(args) ###################################### ## Callbacks and internal functions ###################################### + def on_entry_text_changed(self, entry): + self.emit("text-changed", self.get_text()) + def _on_entry_text_focus_out_event(self, widget, event): + # Update text on the button label self.set_text(self.get_text()) def _set_path_entry_filechooser_widths(self): @@ -1276,7 +1293,7 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): if response_id == 0: text = self.filechooserdialog.get_filename() - self.set_text(text) + self.set_text(text, trigger_event=True) dialog.hide() def _on_entry_text_key_press_event(self, widget, event): @@ -1320,7 +1337,7 @@ class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): return True elif is_ascii_value(keyval, 'd'): # Set the default value in the text entry - self.set_text(self.default_text) + self.set_text(self.default_text, trigger_event=True) return True return False