From bfba5b5237ca3e2b32affedfcff8fc76d7da9521 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Thu, 24 Sep 2020 23:17:15 +0300 Subject: [PATCH] reworked and improved dnd for lists --- app/ui/main_app_window.py | 106 ++++++++++++++++++++++++++++++++++---- app/ui/main_helper.py | 93 +++++++++++++++++---------------- app/ui/main_window.glade | 12 +++-- 3 files changed, 153 insertions(+), 58 deletions(-) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 0c347ad8..92375af1 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -109,12 +109,14 @@ class Application(Gtk.Application): "on_fav_view_query_tooltip": self.on_fav_view_query_tooltip, "on_services_view_query_tooltip": self.on_services_view_query_tooltip, "on_view_drag_begin": self.on_view_drag_begin, + "on_view_drag_end": self.on_view_drag_end, "on_view_drag_data_get": self.on_view_drag_data_get, "on_services_view_drag_drop": self.on_services_view_drag_drop, "on_services_view_drag_data_received": self.on_services_view_drag_data_received, "on_view_drag_data_received": self.on_view_drag_data_received, "on_bq_view_drag_data_received": self.on_bq_view_drag_data_received, "on_view_press": self.on_view_press, + "on_view_release": self.on_view_release, "on_view_popup_menu": self.on_view_popup_menu, "on_view_focus": self.on_view_focus, "on_model_changed": self.on_model_changed, @@ -179,6 +181,7 @@ class Application(Gtk.Application): self._blacklist = set() self._current_bq_name = None self._bq_selected = "" # Current selected bouquet + self._select_enabled = True # Multiple selection # Current satellite positions in the services list self._sat_positions = [] self._marker_types = {BqServiceType.MARKER.name, BqServiceType.SPACE.name} @@ -461,6 +464,10 @@ class Application(Gtk.Application): self._app_info_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) self._app_info_box.drag_dest_add_text_targets() + # For multiple selection. + self._services_view.get_selection().set_select_function(lambda *args: self._select_enabled) + self._fav_view.get_selection().set_select_function(lambda *args: self._select_enabled) + self._bouquets_view.get_selection().set_select_function(lambda *args: self._select_enabled) def init_colors(self, update=False): """ Initialisation of background colors for the services. @@ -722,6 +729,8 @@ class Application(Gtk.Application): self._bouquets.pop("{}:{}".format(b_row[Column.BQ_NAME], b_row[Column.BQ_TYPE]), None) self._bouquets_model.remove(itr) + self._bq_selected = "" + self._bq_name_label.set_text(self._bq_selected) self._wait_dialog.hide() yield True @@ -955,22 +964,83 @@ class Application(Gtk.Application): def get_ssid_info(self, srv): """ Returns SID representation in hex and dec formats. """ + sid = srv.ssid or "0" try: - dec = "{0:04d}".format(int(srv.ssid, 16)) + dec = "{0:04d}".format(int(sid, 16)) except ValueError as e: log("SID value conversion error: {}".format(e)) else: - return "SID: 0x{} ({})".format(srv.ssid.upper(), dec) + return "SID: 0x{} ({})".format(sid.upper(), dec) - return "SID: 0x{}".format(srv.ssid.upper()) + return "SID: 0x{}".format(sid.upper()) # ***************** Drag-and-drop *********************# def on_view_drag_begin(self, view, context): - """ Selects a row under the cursor in the view at the dragging beginning. """ - path, column = view.get_cursor() - if path: - view.get_selection().select_path(path) + """ Sets its own icon for dragging. + + We have to use "connect_after" (after="yes" in xml) to override what the default handler did. + https://lazka.github.io/pgi-docs/Gtk-3.0/classes/Widget.html#Gtk.Widget.signals.drag_begin + """ + model, paths = view.get_selection().get_selected_rows() + if len(paths) < 1: + return + + name, model = get_model_data(view) + name_column, type_column = Column.SRV_SERVICE, Column.SRV_TYPE + if name == self.FAV_MODEL_NAME: + name_column, type_column = Column.FAV_SERVICE, Column.FAV_TYPE + elif name == self.BQ_MODEL_NAME: + name_column, type_column = Column.BQ_NAME, Column.BQ_TYPE + # https://stackoverflow.com/a/52248549 + Gtk.drag_set_icon_widget(context, self.get_drag_widget(model, paths, name_column, type_column), 0, 0) + + def on_view_drag_end(self, view: Gtk.TreeView, context): + self._select_enabled = True + view.get_selection().unselect_all() + + def get_drag_widget(self, model, paths, text_column, type_column): + """ Creates and returns a widget for a dragging icon. """ + frame = Gtk.Frame() + frame.set_border_width(2) + list_box = Gtk.ListBox() + padding = 10 + for index, row in enumerate([model[p] for p in paths]): + if index == 25: + list_box.add(Gtk.Arrow(Gtk.ArrowType.DOWN)) + break + + h_box = Gtk.HBox() + h_box.set_spacing(10) + s_context = h_box.get_style_context() + s_context.add_class(Gtk.STYLE_CLASS_LIST_ROW) + label = Gtk.Label(row[text_column]) + label.set_alignment(0, 0) + label.set_padding(padding, 2) + h_box.add(label) + label = Gtk.Label(row[type_column]) + label.set_halign(Gtk.Align.END) + label.set_padding(padding, 2) + h_box.add(label) + list_box.add(h_box) + + if len(paths) > 1: + list_box.add(Gtk.Separator()) + h_box = Gtk.HBox() + h_box.set_spacing(2) + img = Gtk.Image.new_from_icon_name("document-properties", 0) + h_box.add(img) + h_box.add(Gtk.Label(len(paths))) + h_box.set_halign(Gtk.Align.START) + h_box.set_margin_left(10) + h_box.set_margin_bottom(5) + h_box.set_margin_top(2) + list_box.add(h_box) + + frame.add(list_box) + frame.show_all() + + return frame def on_view_drag_data_get(self, view, drag_context, data, info, time): selection = self.get_selection(view) @@ -993,10 +1063,13 @@ class Application(Gtk.Application): txt = data.get_text() uris = data.get_uris() if txt: - if txt.startswith("file://"): + name, model = get_model_data(view) + if txt.startswith("file://") and name == self.SERVICE_MODEL_NAME: self.on_import_data(urlparse(unquote(txt)).path.strip()) - else: + elif name == self.FAV_MODEL_NAME: self.receive_selection(view=view, drop_info=view.get_dest_row_at_pos(x, y), data=txt) + else: + self.show_error_dialog("Not allowed in this context!") elif len(uris) == 2: self.picons_buffer = self.on_assign_picon(view, urlparse(unquote(uris[0])).path, urlparse(unquote(uris[1])).path + os.sep) @@ -1029,7 +1102,8 @@ class Application(Gtk.Application): if not p_itr: break if p_itr and model.get_path(p_itr)[0] == p_path: - top_iter = model.move_before(itr, top_iter) + model.move_after(itr, top_iter) + top_iter = itr else: model.insert(parent_itr, model.get_path(top_iter)[1], model[itr][:]) to_del.append(itr) @@ -1099,10 +1173,22 @@ class Application(Gtk.Application): self.show_error_dialog(str(e)) def on_view_press(self, view, event): + """ Handles a mouse click (press) to view. """ if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY: + target = view.get_path_at_pos(event.x, event.y) + # Idea taken from here: https://kevinmehall.net/2010/pygtk_multi_select_drag_drop + mask = not (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) + if target and mask and view.get_selection().path_is_selected(target[0]): + self._select_enabled = False + name, model = get_model_data(view) self.delete_views_selection(name) + def on_view_release(self, view, event): + """ Handles a mouse click (release) to view. """ + # Enable selection. + self._select_enabled = True + def delete_views_selection(self, name): if name == self.SERVICE_MODEL_NAME: self.delete_selection(self._fav_view) diff --git a/app/ui/main_helper.py b/app/ui/main_helper.py index e3c31c9a..5960d616 100644 --- a/app/ui/main_helper.py +++ b/app/ui/main_helper.py @@ -46,54 +46,57 @@ def move_items(key, view: Gtk.TreeView): """ Move items in the tree view """ selection = view.get_selection() model, paths = selection.get_selected_rows() + if not paths: + return - if paths: - mod_length = len(model) - if mod_length == len(paths): - return - cursor_path = view.get_cursor()[0] - max_path = Gtk.TreePath.new_from_indices((mod_length,)) - min_path = Gtk.TreePath.new_from_indices((0,)) + is_tree_store = type(model) is Gtk.TreeStore + mod_length = len(model) + if not is_tree_store and mod_length == len(paths): + return + + cursor_path = view.get_cursor()[0] + max_path = Gtk.TreePath.new_from_indices((mod_length,)) + min_path = Gtk.TreePath.new_from_indices((0,)) + + if is_tree_store: is_tree_store = False + parent_paths = list(filter(lambda p: p.get_depth() == 1, paths)) + if parent_paths: + paths = parent_paths + min_path = model.get_path(model.get_iter_first()) + view.collapse_all() + if mod_length == len(paths): + return + else: + if not is_some_level(paths): + return + parent_itr = model.iter_parent(model.get_iter(paths[0])) + parent_index = model.get_path(parent_itr) + children_num = model.iter_n_children(parent_itr) + if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP): + children_num -= 1 + min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0)) + max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num)) + is_tree_store = True - if type(model) is Gtk.TreeStore: - parent_paths = list(filter(lambda p: p.get_depth() == 1, paths)) - if parent_paths: - paths = parent_paths - min_path = model.get_path(model.get_iter_first()) - view.collapse_all() - if mod_length == len(paths): - return - else: - if not is_some_level(paths): - return - parent_itr = model.iter_parent(model.get_iter(paths[0])) - parent_index = model.get_path(parent_itr) - children_num = model.iter_n_children(parent_itr) - if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP): - children_num -= 1 - min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0)) - max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num)) - is_tree_store = True - - if key is KeyboardKey.UP: - top_path = Gtk.TreePath(paths[0]) - set_cursor(top_path, paths, selection, view) - top_path.prev() - move_up(top_path, model, paths) - elif key is KeyboardKey.DOWN: - down_path = Gtk.TreePath(paths[-1]) - set_cursor(down_path, paths, selection, view) - down_path.next() - if down_path < max_path: - move_down(down_path, model, paths) - else: - max_path.prev() - move_down(max_path, model, paths) - elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP): - move_up(min_path if is_tree_store else cursor_path, model, paths) - elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP): - move_down(max_path if is_tree_store else cursor_path, model, paths) + if key is KeyboardKey.UP: + top_path = Gtk.TreePath(paths[0]) + set_cursor(top_path, paths, selection, view) + top_path.prev() + move_up(top_path, model, paths) + elif key is KeyboardKey.DOWN: + down_path = Gtk.TreePath(paths[-1]) + set_cursor(down_path, paths, selection, view) + down_path.next() + if down_path < max_path: + move_down(down_path, model, paths) + else: + max_path.prev() + move_down(max_path, model, paths) + elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP): + move_up(min_path if is_tree_store else cursor_path, model, paths) + elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP): + move_down(max_path if is_tree_store else cursor_path, model, paths) def move_up(top_path, model, paths): diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 8c479931..98a0a94b 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -1894,10 +1894,12 @@ Author: Dmitriy Yefremov True - + + + @@ -2439,9 +2441,11 @@ Author: Dmitriy Yefremov True - + + + @@ -2724,9 +2728,11 @@ Author: Dmitriy Yefremov True - + + +