From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/ui/gtk3/path_combo_chooser.py | 1730 ++++++++++++++++++++++++++++++++++ 1 file changed, 1730 insertions(+) create mode 100755 deluge/ui/gtk3/path_combo_chooser.py (limited to 'deluge/ui/gtk3/path_combo_chooser.py') diff --git a/deluge/ui/gtk3/path_combo_chooser.py b/deluge/ui/gtk3/path_combo_chooser.py new file mode 100755 index 0000000..aeb4c7a --- /dev/null +++ b/deluge/ui/gtk3/path_combo_chooser.py @@ -0,0 +1,1730 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Bro +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import os +import warnings + +from gi.repository import Gdk, GObject, Gtk +from gi.repository.GObject import SignalFlags + +from deluge.common import resource_filename +from deluge.path_chooser_common import get_completion_paths + +# Filter the pygobject signal warning: +# g_value_get_int: assertion 'G_VALUE_HOLDS_INT (value)' failed. +# See: https://gitlab.gnome.org/GNOME/pygobject/issues/12 +warnings.filterwarnings('ignore', '.*g_value_get_int.*G_VALUE_HOLDS_INT.*', Warning) + + +def is_ascii_value(keyval, ascii_key): + try: + # Set show/hide hidden files + if chr(keyval) == ascii_key: + return True + except ValueError: + # Not in ascii range + pass + return False + + +def key_is_up(keyval): + return keyval == Gdk.KEY_Up or keyval == Gdk.KEY_KP_Up + + +def key_is_down(keyval): + return keyval == Gdk.KEY_Down or keyval == Gdk.KEY_KP_Down + + +def key_is_up_or_down(keyval): + return key_is_up(keyval) or key_is_down(keyval) + + +def key_is_pgup_or_pgdown(keyval): + return keyval == Gdk.KEY_Page_Down or keyval == Gdk.KEY_Page_Up + + +def key_is_enter(keyval): + return keyval == Gdk.KEY_Return or keyval == Gdk.KEY_KP_Enter + + +def path_without_trailing_path_sep(path): + while path.endswith('/') or path.endswith('\\'): + if path == '/': + return path + path = path[0:-1] + return path + + +class ValueList: + paths_without_trailing_path_sep = False + + def get_values_count(self): + return len(self.tree_store) + + def get_values(self): + """ + Returns the values in the list. + """ + values = [] + for row in self.tree_store: + values.append(row[0]) + return values + + def add_values( + self, paths, append=True, scroll_to_row=False, clear=False, emit_signal=False + ): + """ + Add paths to the liststore + + :param paths: the paths to add + :type paths: list + :param append: if the values should be appended or inserted + :type append: boolean + :param scroll_to_row: if the treeview should scroll to the new row + :type scroll_to_row: boolean + + """ + if clear: + self.tree_store.clear() + + for path in paths: + if self.paths_without_trailing_path_sep: + path = path_without_trailing_path_sep(path) + if append: + tree_iter = self.tree_store.append([path]) + else: + tree_iter = self.tree_store.insert(0, [path]) + + if scroll_to_row: + self.treeview.grab_focus() + tree_path = self.tree_store.get_path(tree_iter) + # Scroll to path + self.handle_list_scroll(path=tree_path) + + if emit_signal: + self.emit('list-value-added', paths) + self.emit('list-values-changed', self.get_values()) + + def set_values(self, paths, scroll_to_row=False, preserve_selection=True): + """ + Add paths to the liststore + + :param paths: the paths to add + :type paths: list + :param scroll_to_row: if the treeview should scroll to the new row + :type scroll_to_row: boolean + + """ + if not (isinstance(paths, list) or isinstance(paths, tuple)): + return + sel = None + if preserve_selection: + sel = self.get_selection_path() + self.add_values(paths, scroll_to_row=scroll_to_row, clear=True) + if sel: + self.treeview.get_selection().select_path(sel) + + def get_selection_path(self): + """Returns the (first) selected path from a treeview""" + tree_selection = self.treeview.get_selection() + model, tree_paths = tree_selection.get_selected_rows() + if len(tree_paths) > 0: + return tree_paths[0] + return None + + def get_selected_value(self): + path = self.get_selection_path() + if path: + return self.tree_store[path][0] + return None + + def remove_selected_path(self): + path = self.get_selection_path() + if path: + path_value = self.tree_store[path][0] + del self.tree_store[path] + index = path[0] + # The last row was deleted + if index == len(self.tree_store): + index -= 1 + if index >= 0: + path = (index,) + self.treeview.set_cursor(path) + self.set_path_selected(path) + self.emit('list-value-removed', path_value) + self.emit('list-values-changed', self.get_values()) + + def set_selected_value(self, value, select_first=False): + """ + Select the row of the list with value + + :param value: the value to be selected + :type value: str + :param select_first: if the first item should be selected if the value if not found. + :type select_first: boolean + + """ + for i, row in enumerate(self.tree_store): + if row[0] == value: + self.treeview.set_cursor(i) + return + # The value was not found + if select_first: + self.treeview.set_cursor((0,)) + else: + self.treeview.get_selection().unselect_all() + + def set_path_selected(self, path): + self.treeview.get_selection().select_path(path) + + def on_value_list_treeview_key_press_event(self, widget, event): + """ + Mimics Combobox behavior + + Escape or Alt+Up: Close + Enter or Return : Select + """ + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + alt_up = (state == Gdk.ModifierType.MOD1_MASK) and key_is_up(keyval) + if keyval == Gdk.KEY_Escape or alt_up: + self.popdown() + return True + # Set entry value to the selected row + elif key_is_enter(keyval): + path = self.get_selection_path() + if path: + self.set_entry_value(path, popdown=True) + return True + return False + + def on_treeview_mouse_button_press_event(self, treeview, event, double_click=False): + """ + When left clicking twice, the row value is set for the text entry + and the popup is closed. + + """ + # This is left click + if event.button != 3: + # Double clicked a row, set this as the entry value + # and close the popup + if (double_click and event.type == Gdk.EventType._2BUTTON_PRESS) or ( + not double_click and event.type == Gdk.EventType.BUTTON_PRESS + ): + path = self.get_selection_path() + if path: + self.set_entry_value(path, popdown=True) + return True + return False + + def handle_list_scroll( + self, _next=None, path=None, set_entry=False, swap=False, scroll_window=False + ): + """ + Handles changes to the row selection. + + :param _next: the direction to change selection. True means down and False means up. + None means no change. + :type _next: boolean/None + :param path: the current path. If None, the currently selected path is used. + :type path: tuple + :param set_entry: if the new value should be set in the text entry. + :type set_entry: boolean + :param swap: if the old and new value should be swapped + :type swap: boolean + + """ + if scroll_window: + adjustment = self.completion_scrolled_window.get_vadjustment() + + visible_rows_height = self.get_values_count() + if visible_rows_height > self.max_visible_rows: + visible_rows_height = self.max_visible_rows + + visible_rows_height *= self.row_height + value = adjustment.get_value() + + # Max adjustment value + max_value = adjustment.get_upper() - visible_rows_height + # Set adjustment increment to 3 times the row height + adjustment.set_step_increment(self.row_height * 3) + + if _next: + # If number of values is less than max rows, no scroll + if self.get_values_count() < self.max_visible_rows: + return + value += adjustment.get_step_increment() + if value > max_value: + value = max_value + else: + value -= adjustment.get_step_increment() + if value < 0: + value = 0 + adjustment.set_value(value) + return + + if path is None: + path = self.get_selection_path() + if not path: + # These options require a selected path + if set_entry or swap: + return + # This is a regular scroll, not setting value in entry or swapping rows, + # so we find a path value anyways + path = (0,) + cursor = self.treeview.get_cursor() + if cursor is not None and cursor[0] is not None: + path = cursor[0] + else: + # Since cursor is none, we won't advance the index + _next = None + + # If _next is None, we won't change the selection + if _next is not None: + # We move the selection either one up or down. + # If we reach end of list, we wrap + index = path[0] if path else 0 + index = index + 1 if _next else index - 1 + if index >= len(self.tree_store): + index = 0 + elif index < 0: + index = len(self.tree_store) - 1 + + # We have the index for the new path + new_path = index + if swap: + p1 = self.tree_store[path][0] + p2 = self.tree_store[new_path][0] + self.tree_store.swap( + self.tree_store.get_iter(path), self.tree_store.get_iter(new_path) + ) + self.emit('list-values-reordered', [p1, p2]) + self.emit('list-values-changed', self.get_values()) + path = new_path + + self.treeview.set_cursor(path) + self.treeview.get_selection().select_path(path) + if set_entry: + self.set_entry_value(path) + + +class StoredValuesList(ValueList): + def __init__(self): + 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' + ] = self.on_treeview_mouse_button_press_event + + self.signal_handlers[ + 'on_stored_values_treeview_key_press_event' + ] = self.on_stored_values_treeview_key_press_event + self.signal_handlers[ + 'on_stored_values_treeview_key_release_event' + ] = self.on_stored_values_treeview_key_release_event + + self.signal_handlers[ + 'on_cellrenderertext_edited' + ] = self.on_cellrenderertext_edited + + def on_cellrenderertext_edited(self, cellrenderertext, path, new_text): + """ + Callback on the 'edited' signal. + + Sets the new text in the path and disables editing on the renderer. + """ + new_text = path_without_trailing_path_sep(new_text) + self.tree_store[path][0] = new_text + self.rendererText.set_property('editable', False) + + def on_edit_path(self, path, column): + """ + Starts editing on the provided path + + :param path: the paths to edit + :type path: tuple + :param column: the column to edit + :type column: Gtk.TreeViewColumn + + """ + self.rendererText.set_property('editable', True) + self.treeview.grab_focus() + self.treeview.set_cursor(path, column=column, start_editing=True) + + def on_treeview_mouse_button_press_event(self, treeview, event): + """ + Shows popup on selected row when right clicking + When left clicking twice, the row value is set for the text entry + and the popup is closed. + + """ + # This is left click + if event.button != 3: + super().on_treeview_mouse_button_press_event( + treeview, event, double_click=True + ) + return False + + # This is right click, create popup menu for this row + x = int(event.x) + y = int(event.y) + time = event.time + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, cellx, celly = pthinfo + treeview.grab_focus() + treeview.set_cursor(path, col, 0) + + self.path_list_popup = Gtk.Menu() + menuitem_edit = Gtk.MenuItem.new_with_label(_('Edit path')) + self.path_list_popup.append(menuitem_edit) + menuitem_remove = Gtk.MenuItem.new_with_label(_('Remove path')) + self.path_list_popup.append(menuitem_remove) + + def on_edit_clicked(widget, path): + self.on_edit_path(path, self.tree_column) + + def on_remove_clicked(widget, path): + self.remove_selected_path() + + menuitem_edit.connect('activate', on_edit_clicked, path) + menuitem_remove.connect('activate', on_remove_clicked, path) + self.path_list_popup.popup(None, None, None, path, event.button, time) + self.path_list_popup.show_all() + + def remove_selected_path(self): + ValueList.remove_selected_path(self) + # Resize popup + PathChooserPopup.popup(self) + + def on_stored_values_treeview_key_press_event(self, widget, event): + super().on_value_list_treeview_key_press_event(widget, event) + # Prevent the default event handler to move the cursor in the list + if key_is_up_or_down(event.keyval): + return True + + def on_stored_values_treeview_key_release_event(self, widget, event): + """ + Mimics Combobox behavior + + Escape or Alt+Up: Close + Enter or Return : Select + + """ + keyval = event.keyval + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + + # Edit selected row + if keyval in [Gdk.KEY_Left, Gdk.KEY_Right, Gdk.KEY_space]: + path = self.get_selection_path() + if path: + self.on_edit_path(path, self.tree_column) + elif key_is_up_or_down(keyval): + # Swap the row value + if event.get_state() & Gdk.ModifierType.CONTROL_MASK: + self.handle_list_scroll(_next=key_is_down(keyval), swap=True) + else: + self.handle_list_scroll(_next=key_is_down(keyval)) + elif key_is_pgup_or_pgdown(event.keyval): + # The cursor has been changed by the default key-press-event handler + # so set the path of the cursor selected + self.set_path_selected(self.treeview.get_cursor()[0]) + elif ctrl: + # Handle key bindings for manipulating the list + # Remove the selected entry + if is_ascii_value(keyval, 'r'): + self.remove_selected_path() + return True + # Add current value to saved list + elif is_ascii_value(keyval, 's'): + super( + PathChooserComboBox, self + ).add_current_value_to_saved_list() # pylint: disable=bad-super-call + return True + # Edit selected value + elif is_ascii_value(keyval, 'e'): + self.edit_selected_path() + return True + + +class CompletionList(ValueList): + def __init__(self): + self.tree_store = self.builder.get_object('completion_tree_store') + self.tree_column = self.builder.get_object('completion_treeview_column') + self.rendererText = self.builder.get_object('completion_cellrenderertext') + self.completion_scrolled_window = self.builder.get_object( + 'completion_scrolled_window' + ) + self.signal_handlers[ + 'on_completion_treeview_key_press_event' + ] = self.on_completion_treeview_key_press_event + self.signal_handlers[ + 'on_completion_treeview_motion_notify_event' + ] = self.on_completion_treeview_motion_notify_event + + # Add super class signal handler + self.signal_handlers[ + 'on_completion_treeview_mouse_button_press_event' + ] = super().on_treeview_mouse_button_press_event + + def reduce_values(self, prefix): + """ + Reduce the values in the liststore to those starting with the prefix. + + :param prefix: the prefix to be matched + :type paths: string + + """ + values = self.get_values() + matching_values = [] + for v in values: + if v.startswith(prefix): + matching_values.append(v) + self.add_values(matching_values, clear=True) + + def on_completion_treeview_key_press_event(self, widget, event): + ret = super().on_value_list_treeview_key_press_event(widget, event) + if ret: + return ret + keyval = event.keyval + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + if key_is_up_or_down(keyval): + self.handle_list_scroll(_next=key_is_down(keyval)) + return True + elif ctrl: + # Set show/hide hidden files + if is_ascii_value(keyval, 'h'): + self.path_entry.set_show_hidden_files( + not self.path_entry.get_show_hidden_files(), do_completion=True + ) + return True + + def on_completion_treeview_motion_notify_event(self, widget, event): + if event.is_hint: + x, y, state = event.window.get_pointer() + else: + x = event.x + y = event.y + + path = self.treeview.get_path_at_pos(int(x), int(y)) + if path: + self.handle_list_scroll(path=path[0], _next=None) + + +class PathChooserPopup: + """This creates the popop window for the ComboEntry.""" + + def __init__(self, min_visible_rows, max_visible_rows, popup_alignment_widget): + self.min_visible_rows = min_visible_rows + # Maximum number of rows to display without scrolling + self.set_max_popup_rows(max_visible_rows) + self.popup_window.realize() + self.alignment_widget = popup_alignment_widget + # If set, the height of this widget is the minimum height + self.popup_buttonbox = None + + def popup(self): + """Make the popup visible.""" + # Entry is not yet visible + if not self.path_entry.get_realized(): + return + self.set_window_position_and_size() + + def popdown(self): + if not self.is_popped_up(): + return + if not self.path_entry.get_realized(): + return + self.popup_window.grab_remove() + self.popup_window.hide() + + def is_popped_up(self): + """Check if window is popped up. + + Returns: + bool: True if popped up, False otherwise. + + """ + return self.popup_window.get_mapped() + + def set_window_position_and_size(self): + if len(self.tree_store) < self.min_visible_rows: + return False + x, y, width, height = self.get_position() + self.popup_window.set_size_request(width, height) + self.popup_window.resize(width, height) + self.popup_window.move(x, y) + self.popup_window.show_all() + + def get_position(self): + """ + Returns the size of the popup window and the coordinates on the screen. + + """ + + # Necessary for the first call, to make treeview.size_request give sensible values + # self.popup_window.realize() + self.treeview.realize() + + # We start with the coordinates of the parent window + z, x, y = self.path_entry.get_window().get_origin() + + # Add the position of the alignment_widget relative to the parent window. + x += self.alignment_widget.get_allocation().x + y += self.alignment_widget.get_allocation().y + + height_extra = 8 + buttonbox_width = 0 + height = self.popup_window.get_preferred_height()[1] + width = self.popup_window.get_preferred_width()[1] + + if self.popup_buttonbox: + buttonbox_height = max( + self.popup_buttonbox.get_preferred_height()[1], + self.popup_buttonbox.get_allocation().height, + ) + buttonbox_width = max( + self.popup_buttonbox.get_preferred_width()[1], + self.popup_buttonbox.get_allocation().width, + ) + treeview_width = self.treeview.get_preferred_width()[1] + # After removing an element from the tree store, self.treeview.get_preferred_width()[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.get_allocation().width: + width = self.alignment_widget.get_allocation().width + + # 10 is extra spacing + content_width = self.treeview.get_preferred_width()[1] + buttonbox_width + 10 + + # 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.get_preferred_height()[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 + if max_height + height_extra < height: + height = max_height + height += height_extra + # Increase width because of vertical scrollbar + content_width += 15 + + 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 + + screen = self.path_entry.get_screen() + monitor_num = screen.get_monitor_at_window(self.path_entry.get_window()) + monitor = screen.get_monitor_geometry(monitor_num) + + if x < monitor.x: + x = monitor.x + elif x + width > monitor.x + monitor.width: + x = monitor.x + monitor.width - width + + # Set the position + if ( + y + self.path_entry.get_allocation().height + height + <= monitor.y + monitor.height + ): + y += self.path_entry.get_allocation().height + # Not enough space downwards on the screen + elif y - height >= monitor.y: + y -= height + elif ( + monitor.y + monitor.height - (y + self.path_entry.get_allocation().height) + > y - monitor.y + ): + y += self.path_entry.get_allocation().height + height = monitor.y + monitor.height - y + else: + height = y - monitor.y + y = monitor.y + + return x, y, width, height + + def popup_grab_window(self): + activate_time = 0 + if ( + Gdk.pointer_grab( + self.popup_window.get_window(), + True, + ( + Gdk.EventMask.BUTTON_PRESS_MASK + | Gdk.EventMask.BUTTON_RELEASE_MASK + | Gdk.EventMask.POINTER_MOTION_MASK + ), + None, + None, + activate_time, + ) + == 0 + ): + if ( + Gdk.keyboard_grab(self.popup_window.get_window(), True, activate_time) + == 0 + ): + return True + else: + self.popup_window.get_window().get_display().pointer_ungrab( + activate_time + ) + return False + return False + + def set_entry_value(self, path, popdown=False): + """ + + 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, trigger_event=True + ) + if popdown: + self.popdown() + + def set_max_popup_rows(self, rows): + try: + int(rows) + except Exception: + self.max_visible_rows = 20 + return + self.max_visible_rows = rows + + def get_max_popup_rows(self): + return self.max_visible_rows + + ################# + # Callbacks + ################# + + def on_popup_window_button_press_event(self, window, event): + # If we're clicking outside of the window close the popup + allocation = self.popup_window.get_allocation() + + if (event.x < allocation.x or event.x > allocation.width) or ( + event.y < allocation.y or event.y > allocation.height + ): + self.popdown() + + +class StoredValuesPopup(StoredValuesList, PathChooserPopup): + """ + + The stored values popup + + """ + + def __init__(self, builder, path_entry, max_visible_rows, popup_alignment_widget): + self.builder = builder + self.treeview = self.builder.get_object('stored_values_treeview') + self.popup_window = self.builder.get_object('stored_values_popup_window') + self.button_default = self.builder.get_object('button_default') + self.path_entry = path_entry + self.text_entry = path_entry.text_entry + + self.signal_handlers = {} + 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 + self.signal_handlers[ + 'on_stored_values_treeview_scroll_event' + ] = self.on_scroll_event + self.signal_handlers[ + 'on_button_toggle_dropdown_scroll_event' + ] = self.on_scroll_event + self.signal_handlers['on_entry_text_scroll_event'] = self.on_scroll_event + self.signal_handlers[ + 'on_stored_values_popup_window_focus_out_event' + ] = self.on_stored_values_popup_window_focus_out_event + # For when clicking outside the popup + self.signal_handlers[ + 'on_stored_values_popup_window_button_press_event' + ] = self.on_popup_window_button_press_event + + # Buttons for manipulating the list + self.signal_handlers['on_button_add_clicked'] = self.on_button_add_clicked + self.signal_handlers['on_button_edit_clicked'] = self.on_button_edit_clicked + self.signal_handlers['on_button_remove_clicked'] = self.on_button_remove_clicked + self.signal_handlers['on_button_up_clicked'] = self.on_button_up_clicked + self.signal_handlers['on_button_down_clicked'] = self.on_button_down_clicked + self.signal_handlers[ + 'on_button_default_clicked' + ] = self.on_button_default_clicked + self.signal_handlers[ + 'on_button_properties_clicked' + ] = self.path_entry._on_button_properties_clicked + + def popup(self): + """ + Makes the popup visible. + + """ + # Calling super popup + PathChooserPopup.popup(self) + self.popup_window.grab_focus() + + if not self.treeview.has_focus(): + self.treeview.grab_focus() + if not self.popup_grab_window(): + self.popup_window.hide() + return + + self.popup_window.grab_add() + # Set value selected if it exists + self.set_selected_value( + path_without_trailing_path_sep(self.path_entry.get_text()) + ) + + ################# + # Callbacks + ################# + + def on_stored_values_popup_window_focus_out_event(self, entry, event): + """ + Popup sometimes loses the focus to the text entry, e.g. when right click + shows a popup menu on a row. This regains the focus. + """ + self.popup_grab_window() + return True + + def on_scroll_event(self, widget, event): + """ + Handles scroll events from text entry, toggle button and treeview + + """ + + swap = event.get_state() & Gdk.ModifierType.CONTROL_MASK + scroll_window = event.get_state() & Gdk.ModifierType.SHIFT_MASK + self.handle_list_scroll( + _next=event.direction == Gdk.ScrollDirection.DOWN, + set_entry=widget != self.treeview, + swap=swap, + scroll_window=scroll_window, + ) + return True + + def on_buttonbox_key_press_event(self, widget, event): + """ + Handles when Escape or ALT+arrow up is pressed when focus + is on any of the buttons in the popup + """ + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + if keyval == Gdk.KEY_Escape or ( + key_is_up(keyval) and state == Gdk.ModifierType.MOD1_MASK + ): + self.popdown() + return True + return False + + # -------------------------------------------------- + # Funcs and callbacks on the buttons to manipulate the list + # -------------------------------------------------- + def add_current_value_to_saved_list(self): + text = self.path_entry.get_text() + text = path_without_trailing_path_sep(text) + values = self.get_values() + if text in values: + # Make the matching value selected + self.set_selected_value(text) + self.handle_list_scroll() + return True + self.add_values([text], scroll_to_row=True, append=False, emit_signal=True) + + def edit_selected_path(self): + path = self.get_selection_path() + if path: + self.on_edit_path(path, self.tree_column) + + def on_button_add_clicked(self, widget): + self.add_current_value_to_saved_list() + self.popup() + + def on_button_edit_clicked(self, widget): + self.edit_selected_path() + + def on_button_remove_clicked(self, widget): + self.remove_selected_path() + return True + + def on_button_up_clicked(self, widget): + self.handle_list_scroll(_next=False, swap=True) + + def on_button_down_clicked(self, widget): + self.handle_list_scroll(_next=True, swap=True) + + def on_button_default_clicked(self, widget): + if self.default_text: + self.set_text(self.default_text, trigger_event=True) + + +class PathCompletionPopup(CompletionList, PathChooserPopup): + """ + + The auto completion popup + + """ + + def __init__(self, builder, path_entry, max_visible_rows): + self.builder = builder + self.treeview = self.builder.get_object('completion_treeview') + self.popup_window = self.builder.get_object('completion_popup_window') + self.path_entry = path_entry + self.text_entry = path_entry.text_entry + self.show_hidden_files = False + + self.signal_handlers = {} + PathChooserPopup.__init__(self, 1, max_visible_rows, self.text_entry) + CompletionList.__init__(self) + + # Add signal handlers + self.signal_handlers[ + 'on_completion_treeview_scroll_event' + ] = self.on_scroll_event + self.signal_handlers[ + 'on_completion_popup_window_focus_out_event' + ] = self.on_completion_popup_window_focus_out_event + + # For when clicking outside the popup + self.signal_handlers[ + 'on_completion_popup_window_button_press_event' + ] = self.on_popup_window_button_press_event + + def popup(self): + """ + Makes the popup visible. + + """ + PathChooserPopup.popup(self) + self.popup_window.grab_focus() + + if not self.treeview.has_focus(): + self.treeview.grab_focus() + + if not self.popup_grab_window(): + self.popup_window.hide() + return + + 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 + ################# + + def on_completion_popup_window_focus_out_event(self, entry, event): + """ + Popup sometimes loses the focus to the text entry, e.g. when right click + shows a popup menu on a row. This regains the focus. + """ + self.popup_grab_window() + return True + + def on_scroll_event(self, widget, event): + """ + Handles scroll events from the treeview + + """ + x, y = event.window.get_pointer() + self.handle_list_scroll( + _next=event.direction == Gdk.ScrollDirection.DOWN, + set_entry=widget != self.treeview, + scroll_window=True, + ) + path = self.treeview.get_path_at_pos(int(x), int(y)) + if path: + self.handle_list_scroll(path=path[0], _next=None) + return True + + +class PathAutoCompleter: + def __init__(self, builder, path_entry, max_visible_rows): + self.completion_popup = PathCompletionPopup( + builder, path_entry, max_visible_rows + ) + self.path_entry = path_entry + self.dirs_cache = {} + self.use_popup = False + self.auto_complete_enabled = True + self.signal_handlers = self.completion_popup.signal_handlers + + self.signal_handlers[ + 'on_completion_popup_window_key_press_event' + ] = self.on_completion_popup_window_key_press_event + self.signal_handlers[ + 'on_entry_text_delete_text' + ] = 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(Gdk.KEY_Tab, 0) + + def on_entry_text_insert_text(self, entry, new_text, new_text_length, position): + if self.path_entry.get_realized(): + cur_text = self.path_entry.get_text() + pos = entry.get_position() + 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): + """ + Do completion when characters are removed + + """ + if self.completion_popup.is_popped_up(): + cur_text = self.path_entry.get_text() + 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) + if ret: + return ret + keyval = event.keyval + state = event.get_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() + if values_count == 1: + self.do_completion() + else: + self.completion_popup.handle_list_scroll(_next=True) + return True + # Buggy stuff (in pygobject?) causing type mismatch between EventKey and GdkEvent. Convert manually... + n = Gdk.Event() + n.type = event.type + n.window = event.window + n.send_event = event.send_event + n.time = event.time + n.state = event.state + n.keyval = event.keyval + n.length = event.length + n.string = event.string + n.hardware_keycode = event.hardware_keycode + n.group = event.group + n.is_modifier = event.is_modifier + self.path_entry.text_entry.emit('key-press-event', n) + + def is_auto_completion_accelerator(self, keyval, state): + return Gtk.accelerator_name(keyval, state) == self.accelerator_string + + 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)) + 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, args): + args = get_completion_paths(args) + self._end_completion(args) + + 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() and args['forward_completion']: + self.completion_popup.popdown() + + +class PathChooserComboBox(Gtk.Box, StoredValuesPopup, GObject.GObject): + __gsignals__ = { + signal: (SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)) + for signal in [ + 'text-changed', + 'accelerator-set', + 'max-rows-changed', + 'list-value-added', + 'list-value-removed', + 'list-values-changed', + 'list-values-reordered', + 'show-path-entry-toggled', + 'show-filechooser-toggled', + 'show-hidden-files-toggled', + 'show-folder-name-on-button', + 'auto-complete-enabled-toggled', + ] + } + + def __init__( + self, + max_visible_rows=20, + auto_complete=True, + use_completer_popup=True, + parent=None, + ): + Gtk.Box.__init__(self) + GObject.GObject.__init__(self) + self.list_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=0) + self._stored_values_popping_down = False + self.filechooser_visible = True + self.filechooser_enabled = True + self.path_entry_visible = True + self.properties_enabled = True + self.show_folder_name_on_button = False + self.setting_accelerator_key = False + self.builder = Gtk.Builder() + self.parent = parent + self.popup_buttonbox = self.builder.get_object('buttonbox') + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'path_combo_chooser.ui') + ) + ) + self.button_toggle = self.builder.get_object('button_toggle_dropdown') + self.text_entry = self.builder.get_object('entry_text') + self.open_filechooser_dialog_button = self.builder.get_object( + 'button_open_dialog' + ) + self.filechooser_button = self.open_filechooser_dialog_button + self.filechooserdialog = self.builder.get_object('filechooserdialog') + self.filechooserdialog.set_transient_for(self.parent) + self.filechooser_widget = self.builder.get_object('filechooser_widget') + self.folder_name_label = self.builder.get_object('folder_name_label') + self.default_text = None + self.button_properties = self.builder.get_object('button_properties') + + self.combobox_window = self.builder.get_object('combobox_window') + self.combo_hbox = self.builder.get_object('entry_combobox_hbox') + # Change the parent of the hbox from the glade Window to this hbox. + self.combobox_window.remove(self.combo_hbox) + self.combobox_window = self.get_window() + self.add(self.combo_hbox) + StoredValuesPopup.__init__( + self, self.builder, self, max_visible_rows, self.combo_hbox + ) + + self.tooltips = Gtk.Tooltip() + self.auto_completer = PathAutoCompleter(self.builder, self, max_visible_rows) + self.auto_completer.set_use_popup(use_completer_popup) + self.auto_completer.auto_complete_enabled = auto_complete + self._setup_config_dialog() + + signal_handlers = { + 'on_button_toggle_dropdown_toggled': self._on_button_toggle_dropdown_toggled, + 'on_entry_text_key_press_event': self._on_entry_text_key_press_event, + 'on_stored_values_popup_window_hide': self._on_stored_values_popup_window_hide, + 'on_button_toggle_dropdown_button_press_event': self._on_button_toggle_dropdown_button_press_event, + '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) + signal_handlers.update(self.config_dialog_signal_handlers) + self.builder.connect_signals(signal_handlers) + + def get_text(self): + """ + Get the current text in the Entry + """ + return self.text_entry.get_text() + + 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() + # We must block the "delete-text" signal to avoid the signal handler being called + self.text_entry.handler_block_by_func( + self.auto_completer.on_entry_text_delete_text + ) + self.text_entry.set_text(text) + self.text_entry.handler_unblock_by_func( + self.auto_completer.on_entry_text_delete_text + ) + + self.text_entry.select_region(0, 0) + self.text_entry.set_position(len(text) if cursor_end else 0) + self.set_selected_value(text, select_first=True) + self.combo_hbox.set_tooltip_text(text) + if default_text: + self.default_text = text + self.button_default.set_tooltip_text( + '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: + folder_name = path_without_trailing_path_sep(text) + if folder_name != '/' 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): + """ + Set the path chooser widgets sensitive + + :param sensitive: if the widget should be sensitive + :type sensitive: bool + + """ + self.text_entry.set_sensitive(sensitive) + self.filechooser_button.set_sensitive(sensitive) + self.button_toggle.set_sensitive(sensitive) + + def get_accelerator_string(self): + return self.auto_completer.accelerator_string + + def set_accelerator_string(self, accelerator): + """ + Set the accelerator string to trigger auto-completion + """ + if accelerator is None: + return + try: + # Verify that the accelerator can be parsed + keyval, mask = Gtk.accelerator_parse(self.auto_completer.accelerator_string) + self.auto_completer.accelerator_string = accelerator + except TypeError as ex: + raise TypeError('TypeError when setting accelerator string: %s' % ex) + + def get_auto_complete_enabled(self): + return self.auto_completer.auto_complete_enabled + + def set_auto_complete_enabled(self, enable): + if not isinstance(enable, bool): + return + self.auto_completer.auto_complete_enabled = enable + + def get_show_folder_name_on_button(self): + return self.show_folder_name_on_button + + def set_show_folder_name_on_button(self, show): + if not isinstance(show, bool): + return + self.show_folder_name_on_button = show + self._set_path_entry_filechooser_widths() + + def get_filechooser_button_enabled(self): + return self.filechooser_enabled + + def set_filechooser_button_enabled(self, enable): + """ + Enable/disable the filechooser button. + + By setting filechooser disabled, in will not be possible + to change the settings in the properties. + """ + if not isinstance(enable, bool): + return + self.filechooser_enabled = enable + if not enable: + self.set_filechooser_button_visible(False, update=False) + + def get_filechooser_button_visible(self): + return self.filechooser_visible + + def set_filechooser_button_visible(self, visible, update=True): + """ + Set file chooser button entry visible + """ + if not isinstance(visible, bool): + return + if update: + self.filechooser_visible = visible + if visible and not self.filechooser_enabled: + return + if visible: + self.filechooser_button.show() + else: + self.filechooser_button.hide() + # Update width properties + self._set_path_entry_filechooser_widths() + + def get_path_entry_visible(self): + return self.path_entry_visible + + def set_path_entry_visible(self, visible): + """ + Set the path entry visible + """ + if not isinstance(visible, bool): + return + self.path_entry_visible = visible + if visible: + self.text_entry.show() + else: + self.text_entry.hide() + self._set_path_entry_filechooser_widths() + + def get_show_hidden_files(self): + return self.auto_completer.completion_popup.show_hidden_files + + def set_show_hidden_files(self, show, do_completion=False, emit_event=False): + """ + Enable/disable showing hidden files on path completion + """ + if not isinstance(show, bool): + return + self.auto_completer.completion_popup.show_hidden_files = show + if do_completion: + self.auto_completer.do_completion() + if emit_event: + self.emit('show-hidden-files-toggled', show) + + def set_enable_properties(self, enable): + """ + Enable/disable the config properties + """ + if not isinstance(enable, bool): + return + self.properties_enabled = enable + if self.properties_enabled: + self.popup_buttonbox.add(self.button_properties) + else: + self.popup_buttonbox.remove(self.button_properties) + + def set_auto_completer_func(self, func): + """ + Set the function to be called when the auto completion + accelerator is triggered. + """ + self.auto_completer._start_completion = func + + def complete(self, args): + """ + Perform the auto completion with the provided 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): + # FIXME: This causes text to be deselected on right-click. + # Update text on the button label + self.set_text(self.get_text()) + + def _set_path_entry_filechooser_widths(self): + if self.path_entry_visible: + self.combo_hbox.set_child_packing( + self.filechooser_button, 0, 0, 0, Gtk.PackType.START + ) + width, height = self.folder_name_label.get_size_request() + width = 120 + if not self.show_folder_name_on_button: + width = 0 + self.folder_name_label.set_size_request(width, height) + self.combo_hbox.set_child_packing( + self.filechooser_button, 0, 0, 0, Gtk.PackType.START + ) + else: + self.combo_hbox.set_child_packing( + self.filechooser_button, 1, 1, 0, Gtk.PackType.START + ) + self.folder_name_label.set_size_request(-1, -1) + # Update text on the button label + self.set_text(self.get_text()) + + def _on_entry_combobox_hbox_realize(self, widget): + """Must do this when the widget is realized""" + self.set_filechooser_button_visible(self.filechooser_visible) + self.set_path_entry_visible(self.path_entry_visible) + + def _on_button_open_dialog_clicked(self, widget): + self.filechooser_widget.set_current_folder(self.get_text()) + response_id = self.filechooserdialog.run() + + if response_id == 0: + text = self.filechooser_widget.get_filename() + self.set_text(text, trigger_event=True) + self.filechooserdialog.hide() + + def _on_entry_text_key_press_event(self, widget, event): + """ + Listen to key events on the entry widget. + + Arrow up/down will change the value of the entry according to the + current selection in the list. + Enter will show the popup. + + Return True whenever we want no other event listeners to be called. + + """ + # on_entry_text_key_press_event Errors follow here when pressing ALT key while popup is visible") + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + + # Select new row with arrow up/down is pressed + if key_is_up_or_down(keyval): + self.handle_list_scroll(_next=key_is_down(keyval), set_entry=True) + return True + elif self.auto_completer.is_auto_completion_accelerator(keyval, state): + if self.auto_completer.auto_complete_enabled: + self.auto_completer.do_completion() + return True + # Show popup when Enter is pressed + elif key_is_enter(keyval): + # This sets the toggle active which results in + # on_button_toggle_dropdown_toggled being called which initiates the popup + self.button_toggle.set_active(True) + return True + elif ctrl: + # Swap the show hidden files value on CTRL-h + if is_ascii_value(keyval, 'h'): + # Set show/hide hidden files + self.set_show_hidden_files( + not self.get_show_hidden_files(), emit_event=True + ) + return True + elif is_ascii_value(keyval, 's'): + super().add_current_value_to_saved_list() + return True + elif is_ascii_value(keyval, 'd'): + # Set the default value in the text entry + self.set_text(self.default_text, trigger_event=True) + return True + return False + + def _on_button_toggle_dropdown_toggled(self, button): + """ + Shows the popup when clicking the toggle button. + """ + if self._stored_values_popping_down: + return + self.popup() + + def _on_stored_values_popup_window_hide(self, popup): + """Make sure the button toggle is removed when popup is closed""" + self._stored_values_popping_down = True + self.button_toggle.set_active(False) + self._stored_values_popping_down = False + + ############## + # Config dialog + ############## + + def _on_button_toggle_dropdown_button_press_event(self, widget, event): + """Show config when right clicking dropdown toggle button""" + if not self.properties_enabled: + return False + # This is right click + if event.button == 3: + self._on_button_properties_clicked(widget) + return True + + def _on_button_properties_clicked(self, widget): + self.popdown() + self.enable_completion.set_active(self.get_auto_complete_enabled()) + # Set the value of the label to the current accelerator + keyval, mask = Gtk.accelerator_parse(self.auto_completer.accelerator_string) + self.accelerator_label.set_text(Gtk.accelerator_get_label(keyval, mask)) + self.visible_rows.set_value(self.get_max_popup_rows()) + self.show_filechooser_checkbutton.set_active( + self.get_filechooser_button_visible() + ) + self.show_path_entry_checkbutton.set_active(self.path_entry_visible) + self.show_hidden_files_checkbutton.set_active(self.get_show_hidden_files()) + self.show_folder_name_on_button_checkbutton.set_active( + self.get_show_folder_name_on_button() + ) + self._set_properties_widgets_sensitive(True) + self.config_dialog.show_all() + + def _set_properties_widgets_sensitive(self, val): + self.enable_completion.set_sensitive(val) + self.config_short_cuts_frame.set_sensitive(val) + self.config_general_frame.set_sensitive(val) + self.show_hidden_files_checkbutton.set_sensitive(val) + + def _setup_config_dialog(self): + self.config_dialog = self.builder.get_object('completion_config_dialog') + self.enable_completion = self.builder.get_object( + 'enable_auto_completion_checkbutton' + ) + self.show_filechooser_checkbutton = self.builder.get_object( + 'show_filechooser_checkbutton' + ) + self.show_path_entry_checkbutton = self.builder.get_object( + 'show_path_entry_checkbutton' + ) + set_key_button = self.builder.get_object('set_completion_accelerator_button') + default_set_accelerator_tooltip = set_key_button.get_tooltip_text() + self.config_short_cuts_frame = self.builder.get_object( + 'config_short_cuts_frame' + ) + self.config_general_frame = self.builder.get_object('config_general_frame') + self.accelerator_label = self.builder.get_object('completion_accelerator_label') + self.visible_rows = self.builder.get_object('visible_rows_spinbutton') + self.visible_rows_label = self.builder.get_object('visible_rows_label') + self.show_hidden_files_checkbutton = self.builder.get_object( + 'show_hidden_files_checkbutton' + ) + self.show_folder_name_on_button_checkbutton = self.builder.get_object( + 'show_folder_name_on_button_checkbutton' + ) + self.config_dialog.set_transient_for(self.parent) + + def on_close(widget, event=None): + if not self.setting_accelerator_key: + self.config_dialog.hide() + else: + stop_setting_accelerator() + return True + + def on_enable_completion_toggled(widget): + self.set_auto_complete_enabled(self.enable_completion.get_active()) + self.emit( + 'auto-complete-enabled-toggled', self.enable_completion.get_active() + ) + + def on_show_filechooser_toggled(widget): + self.set_filechooser_button_visible( + self.show_filechooser_checkbutton.get_active() + ) + self.emit( + 'show-filechooser-toggled', + self.show_filechooser_checkbutton.get_active(), + ) + self.show_folder_name_on_button_checkbutton.set_sensitive( + self.show_path_entry_checkbutton.get_active() + and self.show_filechooser_checkbutton.get_active() + ) + if not self.filechooser_visible and not self.path_entry_visible: + self.show_path_entry_checkbutton.set_active(True) + on_show_path_entry_toggled(None) + + def on_show_path_entry_toggled(widget): + self.set_path_entry_visible(self.show_path_entry_checkbutton.get_active()) + self.emit( + 'show-path-entry-toggled', self.show_path_entry_checkbutton.get_active() + ) + self.show_folder_name_on_button_checkbutton.set_sensitive( + self.show_path_entry_checkbutton.get_active() + and self.show_filechooser_checkbutton.get_active() + ) + if not self.filechooser_visible and not self.path_entry_visible: + self.show_filechooser_checkbutton.set_active(True) + on_show_filechooser_toggled(None) + + def on_show_folder_name_on_button(widget): + self.set_show_folder_name_on_button( + self.show_folder_name_on_button_checkbutton.get_active() + ) + self._set_path_entry_filechooser_widths() + self.emit( + 'show-folder-name-on-button', + self.show_folder_name_on_button_checkbutton.get_active(), + ) + + def on_show_hidden_files_toggled(widget): + self.set_show_hidden_files( + self.show_hidden_files_checkbutton.get_active(), emit_event=True + ) + + def on_max_rows_changed(widget): + self.set_max_popup_rows(self.visible_rows.get_value_as_int()) + self.emit('max-rows-changed', self.visible_rows.get_value_as_int()) + + def set_accelerator(widget): + self.setting_accelerator_key = True + set_key_button.set_tooltip_text( + 'Press the accelerator keys for triggering auto-completion' + ) + self._set_properties_widgets_sensitive(False) + return True + + def stop_setting_accelerator(): + self.setting_accelerator_key = False + self._set_properties_widgets_sensitive(True) + set_key_button.set_active(False) + # Restore default tooltip + set_key_button.set_tooltip_text(default_set_accelerator_tooltip) + + def on_completion_config_dialog_key_release_event(widget, event): + # We are listening for a new key + if set_key_button.get_active(): + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + accelerator_mask = state.numerator + # If e.g. only CTRL key is pressed. + if not Gtk.accelerator_valid(event.keyval, accelerator_mask): + accelerator_mask = 0 + self.auto_completer.accelerator_string = Gtk.accelerator_name( + event.keyval, accelerator_mask + ) + self.accelerator_label.set_text( + Gtk.accelerator_get_label(event.keyval, accelerator_mask) + ) + self.emit('accelerator-set', self.auto_completer.accelerator_string) + stop_setting_accelerator() + return True + else: + keyval = event.keyval + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + if ctrl: + # Set show/hide hidden files + if is_ascii_value(keyval, 'h'): + self.show_hidden_files_checkbutton.set_active( + not self.get_show_hidden_files() + ) + return True + + def on_set_completion_accelerator_button_clicked(widget): + if not set_key_button.get_active(): + stop_setting_accelerator() + return True + + self.config_dialog_signal_handlers = { + 'on_enable_auto_completion_checkbutton_toggled': on_enable_completion_toggled, + 'on_show_filechooser_checkbutton_toggled': on_show_filechooser_toggled, + 'on_show_path_entry_checkbutton_toggled': on_show_path_entry_toggled, + 'on_show_folder_name_on_button_checkbutton_toggled': on_show_folder_name_on_button, + 'on_config_dialog_button_close_clicked': on_close, + 'on_visible_rows_spinbutton_value_changed': on_max_rows_changed, + 'on_completion_config_dialog_delete_event': on_close, + 'on_set_completion_accelerator_button_pressed': set_accelerator, + 'on_completion_config_dialog_key_release_event': on_completion_config_dialog_key_release_event, + 'on_set_completion_accelerator_button_clicked': on_set_completion_accelerator_button_clicked, + 'on_show_hidden_files_checkbutton_toggled': on_show_hidden_files_toggled, + } + + +GObject.type_register(PathChooserComboBox) + + +if __name__ == '__main__': + import signal + + # necessary to exit with CTRL-C (https://bugzilla.gnome.org/show_bug.cgi?id=622084) + signal.signal(signal.SIGINT, signal.SIG_DFL) + import sys + + w = Gtk.Window() + w.set_position(Gtk.WindowPosition.CENTER) + w.set_size_request(600, -1) + w.set_title('ComboEntry example') + w.connect('delete-event', Gtk.main_quit) + + box1 = Gtk.Box.new(Gtk.Orientation.VERTICAL, spacing=0) + + def get_resource2(filename): + return f'{os.path.abspath(os.path.dirname(sys.argv[0]))}/glade/{filename}' + + # Override get_resource which fetches from deluge install + # get_resource = get_resource2 + + entry1 = PathChooserComboBox(max_visible_rows=15) + entry2 = PathChooserComboBox() + + box1.add(entry1) + box1.add(entry2) + + test_paths = [ + '/home/bro/Downloads', + '/media/Movies-HD', + '/media/torrent/in', + '/media/Live-show/Misc', + '/media/Live-show/Consert', + '/media/Series/1/', + '/media/Series/2', + '/media/Series/17', + '/media/Series/18', + '/media/Series/19', + ] + + entry1.add_values(test_paths) + entry1.set_text('/home/bro/', default_text=True) + entry2.set_text( + '/home/bro/programmer/deluge/deluge-yarss-plugin/build/lib/yarss2/include/bs4/tests/', + cursor_end=False, + ) + + entry2.set_filechooser_button_visible(False) + # entry2.set_enable_properties(False) + entry2.set_filechooser_button_enabled(False) + + def list_value_added_event(widget, values): + print('Current list values:', widget.get_values()) + + entry1.connect('list-value-added', list_value_added_event) + entry2.connect('list-value-added', list_value_added_event) + w.add(box1) + w.show_all() + Gtk.main() -- cgit v1.2.3