From 03e5909c236a8528733780e305214c8781ef140d Mon Sep 17 00:00:00 2001 From: Dmitriy Yefremov Date: Mon, 16 Apr 2018 18:50:48 +0300 Subject: [PATCH] support of "Home" and "End" keys, new variant of move in lists --- README.md | 2 +- app/eparser/enigma/lamedb.py | 2 +- app/eparser/iptv.py | 2 +- app/eparser/neutrino/bouquets.py | 2 +- app/ui/__init__.py | 27 -------- app/ui/dialogs.py | 2 +- app/ui/download_dialog.py | 2 +- app/ui/iptv.py | 2 +- app/ui/main_app_window.py | 15 ++--- app/ui/main_helper.py | 112 +++++++++++++++++++------------ app/ui/main_window.glade | 1 + app/ui/picons_dialog.py | 2 +- app/ui/satellites_dialog.py | 6 +- app/ui/service_details_dialog.py | 2 +- app/ui/settings_dialog.py | 2 +- app/ui/uicommons.py | 52 ++++++++++++++ 16 files changed, 140 insertions(+), 93 deletions(-) create mode 100644 app/ui/uicommons.py diff --git a/README.md b/README.md index b2a51377..836ef7df 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc). Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc) ### Keyboard shortcuts: -Ctrl + X, C, V, Up, Down, PageUp, PageDown, S, T, E, L, H, Space; Insert, Delete, F2. +####Ctrl + X, C, V, Up, Down, PageUp, PageDown, Home, End, S, T, E, L, H, Space; Insert, Delete, F2. Insert - copies the selected channels from the main list to the bouquet or inserts (creates) a new bouquet. Ctrl + X - only in bouquet list. Ctrl + C - only in services list. Clipboard is "rubber". There is an accumulation before the insertion! diff --git a/app/eparser/enigma/lamedb.py b/app/eparser/enigma/lamedb.py index 7e632941..bcec6e6d 100644 --- a/app/eparser/enigma/lamedb.py +++ b/app/eparser/enigma/lamedb.py @@ -4,7 +4,7 @@ Description of format taken from here: http://www.satsupreme.com/showthread.php/194074-Lamedb-format-explained """ from app.commons import log -from app.ui import CODED_ICON, LOCKED_ICON, HIDE_ICON +from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON from .blacklist import get_blacklist from ..ecommons import Service, POLARIZATION, SYSTEM, FEC, SERVICE_TYPE, Flag diff --git a/app/eparser/iptv.py b/app/eparser/iptv.py index 71b43248..b068d945 100644 --- a/app/eparser/iptv.py +++ b/app/eparser/iptv.py @@ -2,7 +2,7 @@ from enum import Enum from app.properties import Profile -from app.ui import IPTV_ICON +from app.ui.uicommons import IPTV_ICON from .ecommons import BqServiceType, Service # url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group diff --git a/app/eparser/neutrino/bouquets.py b/app/eparser/neutrino/bouquets.py index 020ced51..a57fbd56 100644 --- a/app/eparser/neutrino/bouquets.py +++ b/app/eparser/neutrino/bouquets.py @@ -2,7 +2,7 @@ import os from xml.dom.minidom import parse, Document from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT -from app.ui import LOCKED_ICON, HIDE_ICON +from app.ui.uicommons import LOCKED_ICON, HIDE_ICON from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType _FILE = "bouquets.xml" diff --git a/app/ui/__init__.py b/app/ui/__init__.py index e2a34d62..8b137891 100644 --- a/app/ui/__init__.py +++ b/app/ui/__init__.py @@ -1,28 +1 @@ -import locale -import gi -import os -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk - -# path to *.glade files -UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/" - -# translation -TEXT_DOMAIN = "demon-editor" -if UI_RESOURCES_PATH == "app/ui/": - LANG_DIR = UI_RESOURCES_PATH + "lang" - locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang") - -theme = Gtk.IconTheme.get_default() -_IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None -CODED_ICON = theme.load_icon("emblem-readonly", 16, 0) if theme.lookup_icon( - "emblem-readonly", 16, 0) else _IMAGE_MISSING -LOCKED_ICON = theme.load_icon("system-lock-screen", 16, 0) if theme.lookup_icon( - "system-lock-screen", 16, 0) else _IMAGE_MISSING -HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING -TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING -IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.load_icon("emblem-shared", 16, 0) else None - -if __name__ == "__main__": - pass diff --git a/app/ui/dialogs.py b/app/ui/dialogs.py index be427c2a..cf35d0e3 100644 --- a/app/ui/dialogs.py +++ b/app/ui/dialogs.py @@ -3,7 +3,7 @@ import locale from enum import Enum from app.commons import run_idle -from . import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN +from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN class Action(Enum): diff --git a/app/ui/download_dialog.py b/app/ui/download_dialog.py index 49c1d942..e364d0bd 100644 --- a/app/ui/download_dialog.py +++ b/app/ui/download_dialog.py @@ -1,7 +1,7 @@ from app.commons import run_idle, run_task from app.ftp import download_data, DownloadDataType, upload_data from app.properties import Profile -from . import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN +from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN from .dialogs import show_dialog, DialogType, get_message diff --git a/app/ui/iptv.py b/app/ui/iptv.py index 6412827a..9063ea51 100644 --- a/app/ui/iptv.py +++ b/app/ui/iptv.py @@ -4,7 +4,7 @@ from urllib.parse import urlparse from app.eparser.ecommons import BqServiceType, Service from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT from app.properties import Profile -from . import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON +from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON from .dialogs import Action, show_dialog, DialogType from .main_helper import get_base_model diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 77c4892c..4b26d809 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1,9 +1,9 @@ import os +import shutil + from contextlib import suppress from functools import lru_cache -import shutil - from app.commons import run_idle, log from app.eparser import get_blacklist, write_blacklist, parse_m3u from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service @@ -13,7 +13,7 @@ from app.eparser.neutrino.bouquets import BqType from app.properties import get_config, write_config, Profile from .iptv import IptvDialog from .search import SearchProvider -from . import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, get_message from .download_dialog import show_download_dialog from .main_helper import edit_marker, insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services, \ @@ -228,10 +228,7 @@ class MainAppWindow: """ Move items in fav or bouquets tree view """ if self._services_view.is_focus(): return - elif self._fav_view.is_focus(): - move_items(key, self._fav_view) - elif self._bouquets_view and key not in (Gdk.KEY_Page_Up, Gdk.KEY_Page_Down): - move_items(key, self._bouquets_view) + move_items(key, self._fav_view if self._fav_view.is_focus() else self._bouquets_view) def on_cut(self, view): for row in tuple(self.on_delete(view)): @@ -736,9 +733,7 @@ class MainAppWindow: if key == Gdk.KEY_Delete: self.on_delete(view) - elif ctrl and key in (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up): # KEY_KP_Page_Up for laptop! - self.move_items(key) - elif ctrl and key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down): + elif ctrl and key in MOVE_KEYS: self.move_items(key) elif model_name == self._FAV_LIST_NAME and key == Gdk.KEY_Control_L or key == Gdk.KEY_Control_R: self.update_fav_num_column(model) diff --git a/app/ui/main_helper.py b/app/ui/main_helper.py index 8291a88a..a2c13de4 100644 --- a/app/ui/main_helper.py +++ b/app/ui/main_helper.py @@ -1,7 +1,6 @@ """ This is helper module for ui """ import os import shutil -from enum import Enum from gi.repository import GdkPixbuf from app.commons import run_task @@ -9,27 +8,10 @@ from app.eparser import Service from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id from app.properties import Profile -from . import Gtk, Gdk, HIDE_ICON, LOCKED_ICON +from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog -class ViewTarget(Enum): - """ Used for set target view """ - BOUQUET = 0 - FAV = 1 - SERVICES = 2 - - -class BqGenType(Enum): - """ Bouquet generation type """ - SAT = 0 - EACH_SAT = 1 - PACKAGE = 2 - EACH_PACKAGE = 3 - TYPE = 4 - EACH_TYPE = 5 - - # ***************** Markers *******************# def insert_marker(view, bouquets, selected_bouquet, channels, parent_window): @@ -76,35 +58,81 @@ def edit_marker(view, bouquets, selected_bouquet, channels, parent_window): # ***************** Movement *******************# -def move_items(key, view): - """ Move items in tree view """ +def move_items(key, view: Gtk.TreeView): + """ Move items in the tree view """ selection = view.get_selection() model, paths = selection.get_selected_rows() if paths: - # grouping the scattered rows - if len(paths) > 1: - top_iter = model.get_iter(paths[0]) - for i in range(1, len(paths)): - itr = model.get_iter(paths[i]) - model.move_after(itr, top_iter) - top_iter = itr + mod_length = len(model) + 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 = False - model, paths = selection.get_selected_rows() - # for correct down move! - if key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down): - paths = reversed(paths) + 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()) + 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 (Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down, Gdk.KEY_End): + 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 - for path in paths: - itr = model.get_iter(path) - if key == Gdk.KEY_Down: - model.move_after(itr, model.iter_next(itr)) - elif key == Gdk.KEY_Up: - model.move_before(itr, model.iter_previous(itr)) - elif key == Gdk.KEY_Page_Up or key == Gdk.KEY_KP_Page_Up: - model.move_before(itr, model.get_iter(view.get_cursor()[0])) - elif key == Gdk.KEY_Page_Down or key == Gdk.KEY_KP_Page_Down: - model.move_after(itr, model.get_iter(view.get_cursor()[0])) + if mod_length == len(paths): + return + + if key == Gdk.KEY_Up: + top_path = Gtk.TreePath(paths[0]) + top_path.prev() + move_up(top_path, model, paths) + elif key == Gdk.KEY_Down: + down_path = Gtk.TreePath(paths[-1]) + 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 (Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up, Gdk.KEY_Home): + move_up(min_path if is_tree_store else cursor_path, model, paths) + elif key in (Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down, Gdk.KEY_End): + move_down(max_path if is_tree_store else cursor_path, model, paths) + + +def move_up(top_path, model, paths): + top_iter = model.get_iter(top_path) + for path in paths: + itr = model.get_iter(path) + model.move_before(itr, top_iter) + top_path.next() + top_iter = model.get_iter(top_path) + + +def move_down(down_path, model, paths): + top_iter = model.get_iter(down_path) + for path in reversed(paths): + itr = model.get_iter(path) + model.move_after(itr, top_iter) + down_path.prev() + top_iter = model.get_iter(down_path) + + +def is_some_level(paths): + for i in range(1, len(paths)): + prev = paths[i - 1] + current = paths[i] + if len(prev) != len(current) or (len(prev) == 2 and len(current) == 2 and prev[0] != current[0]): + return + return True # ***************** Rename *******************# diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 14bd86dc..3a945e62 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -2248,6 +2248,7 @@ False 0 True + True True diff --git a/app/ui/picons_dialog.py b/app/ui/picons_dialog.py index a30afa37..eb57185b 100644 --- a/app/ui/picons_dialog.py +++ b/app/ui/picons_dialog.py @@ -9,7 +9,7 @@ from app.commons import run_idle, run_task from app.ftp import upload_data, DownloadDataType from app.picons.picons import PiconsParser, parse_providers, Provider, convert_to from app.properties import Profile -from . import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN from .dialogs import show_dialog, DialogType, get_message from .main_helper import update_entry_data diff --git a/app/ui/satellites_dialog.py b/app/ui/satellites_dialog.py index 69ef5d46..cccb70c8 100644 --- a/app/ui/satellites_dialog.py +++ b/app/ui/satellites_dialog.py @@ -3,7 +3,7 @@ from math import fabs from app.commons import run_idle from app.eparser import get_satellites, write_satellites, Satellite, Transponder -from . import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN from .dialogs import show_dialog, DialogType, WaitDialog from .main_helper import move_items, scroll_to @@ -125,9 +125,7 @@ class SatellitesDialog: self.on_transponder() elif key == Gdk.KEY_space: pass - elif ctrl and key in (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up): # KEY_KP_Page_Up for laptop! - move_items(key, self._sat_view) - elif ctrl and key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down): + elif ctrl and key in _MOVE_KEYS: move_items(key, self._sat_view) elif key == Gdk.KEY_Left or key == Gdk.KEY_Right: view.do_unselect_all(view) diff --git a/app/ui/service_details_dialog.py b/app/ui/service_details_dialog.py index abf0edce..a0ad21ce 100644 --- a/app/ui/service_details_dialog.py +++ b/app/ui/service_details_dialog.py @@ -6,7 +6,7 @@ from app.eparser import Service from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, \ get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE from app.properties import Profile -from . import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON from .dialogs import show_dialog, DialogType, Action from .main_helper import get_base_model diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 24234d1f..f6139e94 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -1,5 +1,5 @@ from app.properties import write_config, Profile, get_default_settings -from . import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN +from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN from .main_helper import update_entry_data diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py new file mode 100644 index 00000000..978b9a89 --- /dev/null +++ b/app/ui/uicommons.py @@ -0,0 +1,52 @@ +import locale +import os + +import gi +from enum import Enum + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + +# path to *.glade files +UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/" + +# translation +TEXT_DOMAIN = "demon-editor" +if UI_RESOURCES_PATH == "app/ui/": + LANG_DIR = UI_RESOURCES_PATH + "lang" + locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang") + +theme = Gtk.IconTheme.get_default() +_IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None +CODED_ICON = theme.load_icon("emblem-readonly", 16, 0) if theme.lookup_icon( + "emblem-readonly", 16, 0) else _IMAGE_MISSING +LOCKED_ICON = theme.load_icon("system-lock-screen", 16, 0) if theme.lookup_icon( + "system-lock-screen", 16, 0) else _IMAGE_MISSING +HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING +TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING +IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.load_icon("emblem-shared", 16, 0) else None + +# keys for move in lists +MOVE_KEYS = (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_Home, Gdk.KEY_End, + Gdk.KEY_KP_Page_Up, Gdk.KEY_KP_Page_Down) # KEY_KP_Page_Up(Down) for laptop! + + +class ViewTarget(Enum): + """ Used for set target view """ + BOUQUET = 0 + FAV = 1 + SERVICES = 2 + + +class BqGenType(Enum): + """ Bouquet generation type """ + SAT = 0 + EACH_SAT = 1 + PACKAGE = 2 + EACH_PACKAGE = 3 + TYPE = 4 + EACH_TYPE = 5 + + +if __name__ == "__main__": + pass