From 86b2b07446055932730990ba2c9535df47b86d65 Mon Sep 17 00:00:00 2001 From: i026e Date: Fri, 9 Dec 2016 08:08:32 +0300 Subject: Major update --- mime_operations.py | 138 ------- src/app_mode/mime_editor_app_mode.py | 399 ++++++++++++++++++++ src/app_mode/mime_view_app_mode.py | 269 +++++++++++++ src/app_mode/ui_app_mode.glade | 705 +++++++++++++++++++++++++++++++++++ src/cat_mode/mime_editor_cat_mode.py | 326 ++++++++++++++++ src/cat_mode/mime_view_cat_mode.py | 129 +++++++ src/cat_mode/ui_cat_mode.glade | 471 +++++++++++++++++++++++ src/common/data_filter.py | 43 +++ src/common/gtk_common.py | 227 +++++++++++ src/common/mime_categories.py | 54 +++ src/common/mime_operations.py | 198 ++++++++++ src/common/mime_view.py | 240 ++++++++++++ src/mime_editor.py | 68 ++++ src/ui_main_window.glade | 180 +++++++++ ui.glade | 626 ------------------------------- xdg-gui.py | 588 ----------------------------- 16 files changed, 3309 insertions(+), 1352 deletions(-) delete mode 100644 mime_operations.py create mode 100644 src/app_mode/mime_editor_app_mode.py create mode 100644 src/app_mode/mime_view_app_mode.py create mode 100644 src/app_mode/ui_app_mode.glade create mode 100644 src/cat_mode/mime_editor_cat_mode.py create mode 100644 src/cat_mode/mime_view_cat_mode.py create mode 100644 src/cat_mode/ui_cat_mode.glade create mode 100644 src/common/data_filter.py create mode 100644 src/common/gtk_common.py create mode 100644 src/common/mime_categories.py create mode 100644 src/common/mime_operations.py create mode 100644 src/common/mime_view.py create mode 100644 src/mime_editor.py create mode 100644 src/ui_main_window.glade delete mode 100644 ui.glade delete mode 100644 xdg-gui.py diff --git a/mime_operations.py b/mime_operations.py deleted file mode 100644 index 7d68d25..0000000 --- a/mime_operations.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Wed Nov 23 18:26:27 2016 - -@author: pavel -""" - -import gi -gi.require_version('Gtk', '3.0') - -from gi.repository import Gio, Gtk -from gi.repository import GdkPixbuf - -import os.path - -from functools import lru_cache - -def get_default_app(mime_type): - return Gio.app_info_get_default_for_type(mime_type, False) - -def get_app_from_cline(cline, name, terminal): - """ cl - command line - name - app name - terminal - open in terminal """ - flag = Gio.AppInfoCreateFlags.NEEDS_TERMINAL if terminal \ - else Gio.AppInfoCreateFlags.NONE - return Gio.AppInfo.create_from_commandline(cline, name, flag) - -@lru_cache(maxsize=256) -def get_app_bio(app, icon_size): - name = "" - icon = None - cl = "" - - if app is not None: - name = app.get_name() - icon = get_icon_by_app(app, icon_size) - cl = app.get_commandline() - - return name, icon, cl - -def get_known_mtypes(): - return Gio.content_types_get_registered() - -def get_apps_for_mtype(mime_type): - return Gio.app_info_get_all_for_type(mime_type) - -def set_app_default(app, m_types): - for m_type in m_types: - try: - app.set_as_default_for_type(m_type) - except Exception as e: - print(e) - -def reset_association(m_type): - try: - Gio.AppInfo.reset_type_associations(m_type) - except Exception as e: - print(e) - -def get_apps_for_mtypes(mime_types_list): - apps = {} - for mtype in mime_types_list: - for app in get_apps_for_mtype(mtype): - #using id_ to eliminate duplicates - id_ = app.get_commandline()#app.get_id() - apps[id_] = app - return list(apps.values()) - -@lru_cache(maxsize=256) -def get_icon_by_name(icon_name, size): - icon = None - if icon_name is not None: - try: - icon = Gtk.IconTheme.get_default().load_icon(icon_name, size, 0); - except: - try: - icon = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_name, - size, size, True) - except Exception as e: - print(icon_name, e) - - return icon - -@lru_cache(maxsize=256) -def get_icon_by_app(app, size): - #ubuntu-tweak - - icon = None - try: - gicon = app.get_icon() - - if gicon and isinstance(gicon, Gio.ThemedIcon): - names = gicon.get_names() - names_list = [] - - for name in names: - names_list.append(os.path.splitext(name)[0]) - - - theme = Gtk.IconTheme.get_default() - iconinfo = Gtk.IconTheme.choose_icon(theme, names_list, size, - Gtk.IconLookupFlags.USE_BUILTIN) - if iconinfo: - icon = iconinfo.load_icon() - - elif gicon and isinstance(gicon, Gio.FileIcon): - icon_path = app.get_icon().get_file().get_path() - - if icon_path: - icon = get_icon_by_name(icon_path, size) - - except Exception as e: - print(app, e) - - return icon - -@lru_cache(maxsize=256) -def get_mime_icon(mtype, size): - icon = None - - try: - gicon = Gio.content_type_get_icon(mtype) - - theme = Gtk.IconTheme.get_default() - iconinfo = Gtk.IconTheme.choose_icon(theme, gicon.get_names(), size, - Gtk.IconLookupFlags.USE_BUILTIN) - if iconinfo: - icon = iconinfo.load_icon() - - if icon and icon.get_width() != size: - icon = icon.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) - - except Exception as e: - print(e) - - return icon \ No newline at end of file diff --git a/src/app_mode/mime_editor_app_mode.py b/src/app_mode/mime_editor_app_mode.py new file mode 100644 index 0000000..9cb6fae --- /dev/null +++ b/src/app_mode/mime_editor_app_mode.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 25 07:54:51 2016 + +@author: pavel +""" +import os +DIR = os.path.dirname(os.path.realpath(__file__)) + +import sys +sys.path.append(os.path.join(DIR, '../common')) + + +from gi.repository import Gtk, Gdk, Gio +from gi.repository import GdkPixbuf + +from locale import gettext as _ + +import mime_operations +import mime_view_app_mode + +import gtk_common +from gtk_common import browse_for_file +from gtk_common import ICON_SIZE + + + +print(os.path.dirname(os.path.realpath(__file__))) +GLADE_FILE = os.path.join(DIR,"./ui_app_mode.glade") + + +""" +class AddMimesView(mime_types_view.MimesViewWithFlags): + def __init__(self, builder): + super(AddMimesView, self).__init__(builder, "all_mime_types_treeview") + + self._init_context_menu() + + def _init_context_menu(self): + pass + + def get_initial_data(self, *args): + return mime_operations.get_known_mtypes() + + def get_model_data_row(self, mtype, app): + flag = mime_operations.is_app_default(app, mtype) + descr, icon = mime_operations.get_mime_bio(mtype, ICON_SIZE) + + # mtype, mtype_description, mtype_icon, + # mtype_selected. mtype_selected_by_default, + # mtype_possible_to change_selection + # mtype_deleted, background_color + + return [mtype, descr, icon, + flag, flag, not flag, + False, self.CELL_COLOR_NORMAL] + + +""" + +class AppCard: + def __init__(self, builder): + self.builder = builder + + self.app_icon_view = self.builder.get_object("app_icon_view") + self.app_name_label = self.builder.get_object("app_name_label") + self.app_command_label = self.builder.get_object("app_command_label") + + def update(self, app): + if app is not None: + name, icon, cl = mime_operations.get_app_bio(app, ICON_SIZE) + + self.app_icon_view.set_from_pixbuf(icon) + self.app_name_label.set_text(name) + self.app_command_label.set_text(cl) + + +class AppsView: + APP = 0 + APP_NAME = 1 + APP_IMG = 2 + APP_VISIBLE = 3 + APP_SUPPORT_FILES = 4 + def __init__(self, builder, on_app_changed): + self.builder = builder + self.on_app_changed = on_app_changed + + self.apps_view = self.builder.get_object("treeview_applications") + + self._init_model() + self._add_columns() + self._set_data() + + #register callback + tree_selection = self.apps_view.get_selection() + tree_selection.connect("changed", self.on_selection_changes) + + + + def _add_columns(self): + column = gtk_common.ImageTextColumn(_("Application"), self.APP_IMG, self.APP_NAME) + column.set_sort_column_id(self.APP_NAME) + self.apps_view.append_column(column) + + def _init_model(self): + """liststore -> filter -> sort -> view""" + + # app, name, icon, visible, support files, + self.list_store = Gtk.ListStore(Gio.AppInfo, str ,GdkPixbuf.Pixbuf, bool, bool) + + #filter + self.cascade_filter = self.list_store.filter_new() + self.cascade_filter.set_visible_func(self._cascade_filter_func) + self.show_invisible = False + self.show_without_file_support = False + + #sorting + self.sorted = Gtk.TreeModelSort(self.cascade_filter) + self.apps_view.set_model(self.sorted) + + + + def _set_data(self): + for app in mime_operations.get_all_apps(): + name, icon, cl = mime_operations.get_app_bio(app, ICON_SIZE) + + visible = app.should_show() + f_support = app.supports_files() or app.supports_uris() + + self.list_store.append([app, name, icon, visible, f_support]) + + def _cascade_filter_func(self, *args, **kwargs): + return self._visible_only_filter_func(*args, **kwargs) and \ + self._with_file_support_only_filter_func(*args, **kwargs) + + def _visible_only_filter_func(self, model, iter, data): + return self.show_invisible or model[iter][self.APP_VISIBLE] + + def _with_file_support_only_filter_func(self, model, iter, data): + return self.show_without_file_support or model[iter][self.APP_SUPPORT_FILES] + + def filter_visible(self, show_invisible = False): + self.show_invisible = show_invisible + self.cascade_filter.refilter() + + def filter_without_file_support(self, show_without_file_support = False): + self.show_without_file_support = show_without_file_support + self.cascade_filter.refilter() + + def on_selection_changes(self, tree_selection): + (model, pathlist) = tree_selection.get_selected_rows() + app = None + + for path in pathlist : + tree_iter = model.get_iter(path) + value = model.get_value(tree_iter, self.APP) + app = value + + self.on_app_changed(app) + + def add_custom_app(self, app): + if app is not None: + name, icon, cl = mime_operations.get_app_bio(app, ICON_SIZE) + iter_ = self.list_store.append([app, name, icon, True, True]) + + + path = self.list_store.get_path(iter_) + self.apps_view.set_cursor(path) + + +class AddMTypeDialog: + def __init__(self, builder, parent_window, on_dialog_ok): + self.builder = builder + self.on_dialog_ok = on_dialog_ok + + self.dialog = self.builder.get_object("add_mime_type_dialog") + self.dialog.set_transient_for(parent_window) + self.dialog.connect("delete-event", self.hide) + + #self.view = AddMimesView(self.builder) + + def _init_buttons(self): + self.ok_button = self.builder.get_object("add_mtype_ok_button") + self.cancel_button = self.builder.get_object("add_mtype_cancel_button") + + self.ok_button.connect("clicked", self.on_ok_button_clicked) + self.cancel_button.connect("clicked", self.hide) + + def hide(self, *args): + + self.dialog.hide() + #self.list_store.clear() + + + + return True #!!! + + def show(self, app): + #self.set_data() + #self.indicate_default() + + #self.mtypes_dialog_label.set_text(" \n".join(self.mtypes)) + + self.view.set_data(app) + + self.dialog.run() + def on_ok_button_clicked(self, *args): + pass + self.hide() + +class AddAppDialog: + def __init__(self, builder, parent_window, on_dialog_ok): + self.builder = builder + self.on_dialog_ok = on_dialog_ok + + self.dialog = self.builder.get_object("add_app_dialog") + self.dialog.set_transient_for(parent_window) + self.dialog.connect("delete-event", self.hide) + + self._init_buttons() + + self.name_field = self.builder.get_object("add_app_name_field") + self.command_field = self.builder.get_object("add_app_command_field") + + def _init_buttons(self): + self.ok_button = self.builder.get_object("add_app_ok_button") + self.cancel_button = self.builder.get_object("add_app_cancel_button") + self.browse_button = self.builder.get_object("app_browse_button") + + self.ok_button.connect("clicked", self.on_ok_button_clicked) + self.cancel_button.connect("clicked", self.hide) + self.browse_button.connect("clicked", self.on_browse_button_clicked) + + def hide(self, *args): + self.dialog.hide() + + return True #!!! + + def show(self): + #self.set_data() + #self.indicate_default() + + #self.mtypes_dialog_label.set_text(" \n".join(self.mtypes)) + + self.selection = None + + self.dialog.run() + + def on_ok_button_clicked(self, *args): + name = self.name_field.get_text() + command_line = self.command_field.get_text() + app = mime_operations.get_app_from_cline(command_line, name) + + self.on_dialog_ok(app) + self.hide() + + def on_browse_button_clicked(self, *args): + browse_for_file(_('Choose an application'), + '/usr/bin', + self.dialog, + self.command_field.set_text) + +class CategoriesBox(gtk_common.CategoriesWidget): + def __init__(self, builder, on_category_changed): + super(CategoriesBox, self).__init__(builder, "categories_combobox", + on_category_changed) + + def _init_widget(self): + renderer_pixbuf = Gtk.CellRendererPixbuf() + renderer_text = Gtk.CellRendererText() + + self.widget.pack_start(renderer_pixbuf, expand = False) + self.widget.add_attribute(renderer_pixbuf, "pixbuf", self.CAT_IMG) + + self.widget.pack_start(renderer_text, expand = True) + self.widget.add_attribute(renderer_text, "text", self.CAT_NAME) + + self.widget.connect("changed", self.on_combobox_changed) + + def on_combobox_changed(self, widget): + iter_ = widget.get_active_iter() + value = self.list_store.get_value(iter_, self.CAT_ID) + self.on_category_changed(value) + + def disable(self): + self.widget.set_sensitive(False) + def enable(self) : + self.widget.set_sensitive(True) + + + +class MainWidget: + def __init__(self, builder, window): + self.builder = builder + self.window = window + + self.builder.add_from_file(GLADE_FILE) + self.builder.connect_signals(self) + + self.root_widget = self.builder.get_object("root_widget") + + self.apps_view = AppsView(self.builder, self.on_app_selected) + self.app_card = AppCard(self.builder) + self.mimes_view = mime_view_app_mode.MimeViewApps(self.builder) + self.categories_box = CategoriesBox(self.builder, self.on_category_changed) + + self.add_mtype_dialog = None + self.add_app_dialog = None + + self._init_buttons() + + self.current_app = None + + def _init_buttons(self): + visible_apps_only = self.builder.get_object("visible_apps_only") + visible_apps_only.connect("toggled", self.on_show_visible_toggled) + + apps_with_file_support_only = self.builder.get_object("apps_with_file_support_only") + apps_with_file_support_only.connect("toggled", self.on_apps_with_file_support_toggled) + + show_only_registered_radiobutton = self.builder.get_object("show_only_registered_radiobutton") + show_only_registered_radiobutton.connect("toggled", + self.on_show_only_registered_radiobutton_clicked) + + + show_all_radiobutton = self.builder.get_object("show_all_radiobutton") + show_all_radiobutton.connect("toggled", + self.on_show_all_radiobutton_clicked) + + revert_button = self.builder.get_object("revert_button") + revert_button.connect("clicked", self.on_revert_button_clicked) + + apply_button = self.builder.get_object("apply_button") + apply_button.connect("clicked", self.on_apply_button_clicked) + + add_mtype_button = self.builder.get_object("add_mtype_button") + add_mtype_button.connect("clicked", self.on_add_mtype_button_clicked) + + add_app_button = self.builder.get_object("add_app_button") + add_app_button.connect("clicked", self.on_add_app_button_clicked) + + def get_widget(self): + return self.root_widget + + def on_app_selected(self, app): + self.current_app = app + + self.app_card.update(self.current_app) + self.mimes_view.set_data(self.current_app) + + def on_show_visible_toggled(self, flag_widget): + show_visible_only = flag_widget.get_active() + self.apps_view.filter_visible(not show_visible_only) + + def on_apps_with_file_support_toggled(self, flag_widget): + show_with_file_support_only = flag_widget.get_active() + self.apps_view.filter_without_file_support(not show_with_file_support_only) + + def on_revert_button_clicked(self, *args): + self.mimes_view.set_data(self.current_app) + + def on_apply_button_clicked(self, *args): + to_delete, to_set, to_reset = self.mimes_view.get_changed_mtypes() + + print("deleting", to_delete) + mime_operations.remove_associations(self.current_app, to_delete) + print("setting default", to_set) + mime_operations.set_app_default(self.current_app, to_set) + print("resetting default", to_reset) + mime_operations.reset_associations(to_reset) + + self.mimes_view.set_data(self.current_app) + + def on_add_mtype_button_clicked(self, *args): + if self.add_mtype_dialog is None: + self.add_mtype_dialog = AddMTypeDialog(self.builder, + self.window, print) + self.add_mtype_dialog.show(self.current_app) + + def on_add_app_button_clicked(self, *args): + if self.add_app_dialog is None: + self.add_app_dialog = AddAppDialog(self.builder, + self.window, + self.apps_view.add_custom_app) + self.add_app_dialog.show() + + def on_show_only_registered_radiobutton_clicked(self, *args): + self.mimes_view.filter_associated(show_all = False) + self.categories_box.disable() + + def on_show_all_radiobutton_clicked(self, *args): + self.mimes_view.filter_associated(show_all = True) + self.categories_box.enable() + + def on_category_changed(self, category): + self.mimes_view.filter_category(category) + + diff --git a/src/app_mode/mime_view_app_mode.py b/src/app_mode/mime_view_app_mode.py new file mode 100644 index 0000000..fd6e9eb --- /dev/null +++ b/src/app_mode/mime_view_app_mode.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 1 08:15:56 2016 + +@author: pavel +""" +import sys +sys.path.append('../common') + +from gi.repository import Gtk, Gdk, Gio +from gi.repository import GdkPixbuf + + +import gtk_common +import mime_operations, mime_categories +import mime_view +import data_filter + + +from locale import gettext as _ + + + +class MimeViewApps(mime_view.MimeView): + UI_VIEW_ID = "treeview_mime_associations" + UI_CONTEXT_MENU_ID = "mime_view_context_menu" + + #model columns + MIME_TYPE = 0 + MIME_CATEGORY = 1 + MIME_DESC = 2 + MIME_ICON = 3 + MIME_ASSOCIATED_WITH_APP = 4 + MIME_SELECTED = 5 + MIME_DEFAULT_SELECTED = 6 + MIME_CHANGEABLE = 7 # forbidden to take mark of + MIME_DELETED = 8 + MIME_COLOR = 9 + + CELL_COLOR_NORMAL = "white" + CELL_COLOR_DELETED = "red" + + LIST_STORE_COLUMN_TYPES = [str, int, str, GdkPixbuf.Pixbuf, + bool, bool, bool, + bool, bool, str] + + # mtype, mtype_description, mtype_icon, + # mtype_selected. mtype_selected_by_default, + # mtype_possible_to change_selection + # mtype_deleted, background_color + + + def __init__(self, *args, **kwargs): + super(MimeViewApps, self).__init__(*args, **kwargs) + + self.list_store_modified = self.set_consistence + self.delete_menuitem = self.builder.get_object("delete_menuitem") + + def _init_mappings(self): + super(MimeViewApps, self)._init_mappings() + self.context_menu_items.update({"mark_menuitem": lambda *param : self.mark_selected(mark = True), + "unmark_menuitem": lambda *param : self.mark_selected(mark = False), + "reset_menuitem": lambda *param : (self.reset_mark_selected(), + self.undelete_selected()), + "delete_menuitem": lambda *param : self.delete_selected() + }) + + invert_selection = lambda *param : self.mark_selected(invert = True) + self.keyboard_keys_actions.update({"Return" : invert_selection, + "Enter" : invert_selection, + "space" : invert_selection, + "BackSpace" : lambda *param : self.reset_mark_selected(), + "Delete" : lambda *param : self.delete_selected() + }) + + def _init_filters(self): + super(MimeViewApps, self)._init_filters() + + associated_filter = data_filter.GeneralFilter(True, self.MIME_ASSOCIATED_WITH_APP, + lambda is_assosiated : is_assosiated) + + self._add_filter_to_cascade("associated_filter", associated_filter) + + + def _init_columns(self, *args, **kwargs): + self._add_flag_column() + self._add_filetype_column() + self._add_name_column() + + #colors + self.flag_column.set_attribute("cell-background", self.MIME_COLOR) + self.desc_column.set_attribute("cell-background", self.MIME_COLOR) + self.name_column.set_attribute("cell-background", self.MIME_COLOR) + + def _add_flag_column(self): + self.flag_column = gtk_common.FlagColumn("" ,self.MIME_SELECTED, + self.on_flag_toggled, self.on_mark_all_clicked) + self.flag_column.set_attribute("activatable", self.MIME_CHANGEABLE) + self.mtypes_view.append_column(self.flag_column) + + def _add_name_column(self): + self.name_column = gtk_common.TextColumn(_("Mime Type"), self.MIME_TYPE) + self.name_column.set_sort_column_id(self.MIME_TYPE) + self.mtypes_view.append_column(self.name_column) + + def set_data(self, app): + self.list_store.clear() + mtypes = self._get_initial_data(app) + self._add_mtypes(mtypes, app, initialization = True) + + def _add_mtypes(self, mtypes, app, initialization = False): + super(MimeViewApps, self)._add_mtypes(mtypes, app) + self.set_consistence(initial_mode = initialization) + + #def _get_initial_data(self, app): + # return mime_operations.get_mtypes_for_app(app) + + def _get_model_data_row(self, mtype, app): + mtype, category, descr, icon = super(MimeViewApps, self)._get_model_data_row(mtype) + associated = mime_operations.is_app_associated(app, mtype) + + selected = default = associated and mime_operations.is_app_default(app, mtype) + changeable = not selected + + deleted = False + cell_color = self.CELL_COLOR_NORMAL + # mtype, mtype_description, mtype_icon, + # mtype_selected. mtype_selected_by_default, + # mtype_possible_to change_selection + # mtype_deleted, background_color + + return [mtype, category, descr, icon, + associated, + selected, default, changeable, + deleted, cell_color] + + + def delete_selected(self): + self.set_value_to_selected(self.MIME_DELETED, value = True) + self.set_value_to_selected(self.MIME_COLOR, value = self.CELL_COLOR_DELETED) + + + def undelete_selected(self): + self.set_value_to_selected(self.MIME_DELETED, value = False) + self.set_value_to_selected(self.MIME_COLOR, value = self.CELL_COLOR_NORMAL) + + def changeable(self, model, iter_): + return model.get_value(iter_, self.MIME_CHANGEABLE) + + def get_swapped_mark(self, model, iter_): + return not model.get_value(iter_, self.MIME_SELECTED) + + def get_default_mark(self, model, iter_): + return model.get_value(iter_, self.MIME_DEFAULT_SELECTED) + + def reset_mark_all(self): + self.set_value_to_all(self.MIME_SELECTED, get_value_func = self.get_default_mark) + self.set_consistence() + + def reset_mark_selected(self): + self.set_value_to_selected(self.MIME_SELECTED, + get_value_func = self.get_default_mark) + self.set_consistence() + + def mark_all(self, mark = True): + self.set_value_to_all(self.MIME_SELECTED, value = mark, + conditions_func = self.changeable) + self.set_consistence() + + def mark_selected(self, invert = False, mark = True): + """for inversion mark parameter will be ignored""" + if invert: + self.set_value_to_selected(self.MIME_SELECTED, + get_value_func = self.get_swapped_mark, + conditions_func = self.changeable) + else: + self.set_value_to_selected(self.MIME_SELECTED, + value = mark, + conditions_func = self.changeable) + + self.set_consistence() + + def swap_mark_by_path(self, path): + iter_ = self.list_store.get_iter(self.get_store_path(path)) + self.set_value_if_cond(self.list_store, iter_, + self.MIME_SELECTED, + get_value_func = self.get_swapped_mark, + conditions_func = self.changeable) + self.set_consistence() + + def get_changed_mtypes(self): + to_delete = [] + to_set = [] + to_reset = [] + + tree_iter = self.list_store.get_iter_first() + while tree_iter: + mtype = self.list_store.get_value(tree_iter, self.MIME_TYPE) + + delete = self.list_store.get_value(tree_iter, self.MIME_DELETED) + if delete: + to_delete.append(mtype) + else: + default_selected = self.list_store.get_value(tree_iter, self.MIME_DEFAULT_SELECTED) + + selected = self.list_store.get_value(tree_iter, self.MIME_SELECTED) + + if (selected) and (not default_selected): + to_set.append(mtype) + elif (not selected) and (default_selected): + to_reset.append(mtype) + + tree_iter = self.list_store.iter_next(tree_iter) + return to_delete, to_set, to_reset + + + + + + def on_flag_toggled(self, renderer, str_path, *data): + self.swap_mark_by_path(Gtk.TreePath.new_from_string(str_path)) + + + def on_mark_all_clicked(self, reset, mark): + """ If reset : default value would be restored + otherwise : mark would be assigned to each row + """ + if reset: + self.reset_mark_all() + else: + self.mark_all(mark) + + def on_double_click(self, widget, event): + self.mark_selected(invert = True) #invert mark + + def set_consistence(self, initial_mode = False): + tree_iter = self.list_store.get_iter_first() + + if not tree_iter: + self.flag_column.set_flag_inactive() + return + + first_mark = self.list_store.get_value(tree_iter, self.MIME_SELECTED) + consistent = True + + while tree_iter: + if first_mark != self.list_store.get_value(tree_iter, self.MIME_SELECTED): + consistent = False + break + tree_iter = self.list_store.iter_next(tree_iter) + + + if consistent: + self.flag_column.set_flag_active(first_mark) + + if initial_mode: + self.flag_column.set_flag_degenerate(True) + else: + self.flag_column.set_flag_inconsistent() + + if initial_mode: + self.flag_column.set_flag_degenerate(False) + + def filter_associated(self, show_all = False): + self.set_filter_params("associated_filter", + enabled = not show_all) + self.delete_menuitem.set_sensitive(not show_all) + diff --git a/src/app_mode/ui_app_mode.glade b/src/app_mode/ui_app_mode.glade new file mode 100644 index 0000000..494eb18 --- /dev/null +++ b/src/app_mode/ui_app_mode.glade @@ -0,0 +1,705 @@ + + + + + + False + dialog + + + False + vertical + 2 + + + False + end + + + gtk-cancel + True + True + True + True + + + True + True + 0 + + + + + gtk-ok + True + True + True + True + + + True + True + 1 + + + + + False + False + 1 + + + + + True + False + + + True + False + start + Add new app + + + 0 + 0 + 4 + + + + + True + True + + + 1 + 1 + 3 + + + + + True + False + Name + + + 0 + 1 + + + + + True + True + + + 1 + 2 + 2 + + + + + True + False + Command + + + 0 + 2 + + + + + gtk-open + True + True + True + True + + + 3 + 2 + + + + + True + True + 0 + + + + + + + False + dialog + + + False + vertical + 2 + + + False + end + + + gtk-cancel + True + True + True + True + + + True + True + 0 + + + + + gtk-ok + True + True + True + True + 0.51999998092651367 + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + vertical + + + True + False + Select from list + + + False + True + 0 + + + + + True + True + in + + + True + True + + + + + + + + True + True + 1 + + + + + True + True + + + True + True + + + + + True + False + Add new + 0 + + + + + False + True + 2 + + + + + True + True + 1 + + + + + + + True + False + gtk-clear + + + True + False + gtk-edit + + + True + False + gtk-revert-to-saved + + + True + False + gtk-remove + + + True + False + + + Mark + True + False + image2 + False + + + + + Unmark + True + False + image1 + False + + + + + True + False + + + + + Delete + True + False + image4 + False + + + + + True + False + + + + + Reset + True + False + image3 + False + + + + + True + False + + + True + False + vertical + + + + + + True + True + True + + + True + False + vertical + + + True + True + in + + + True + True + 0 + + + + + + + + True + True + 0 + + + + + + + + True + True + + + True + False + vertical + + + With file support only + True + True + False + 0 + True + True + + + False + True + 0 + + + + + Hide invisible + True + True + False + 0 + True + True + + + False + True + 1 + + + + + + + + + + True + False + Options + + + + + False + True + 2 + + + + + True + False + start + + + gtk-new + True + True + True + True + + + True + True + 0 + + + + + False + True + 6 + + + + + True + True + + + + + True + False + vertical + + + True + False + + + True + False + gtk-missing-image + + + False + True + 0 + + + + + True + False + start + Application name + + + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 1 + end + + + gtk-add + True + True + True + True + + + True + True + 0 + + + + + gtk-undo + True + True + True + True + + + True + True + 1 + + + + + gtk-apply + True + True + True + True + + + True + True + 2 + + + + + False + True + end + 1 + + + + + True + False + start + start + Application command + True + 2 + + + + + + + + False + True + 2 + + + + + + + + True + True + in + + + True + True + True + + + + + + + + True + True + 4 + + + + + + + + True + True + + + True + False + + + Show only registered + True + True + False + 0 + True + True + + + 0 + 0 + + + + + Show all for + True + True + False + 0 + True + True + show_only_registered_radiobutton + + + 0 + 1 + + + + + True + False + False + + + 1 + 1 + + + + + + + + + + True + False + Options + + + + + False + True + 6 + + + + + True + True + + + + + True + True + 1 + + + + + + + + + diff --git a/src/cat_mode/mime_editor_cat_mode.py b/src/cat_mode/mime_editor_cat_mode.py new file mode 100644 index 0000000..37ab448 --- /dev/null +++ b/src/cat_mode/mime_editor_cat_mode.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 18 06:50:25 2016 + +@author: pavel +""" +import os +DIR = os.path.dirname(os.path.realpath(__file__)) + +import sys +sys.path.append(os.path.join(DIR, '../common')) + +from gi.repository import Gtk, Gdk, Gio +from gi.repository import GdkPixbuf + +from locale import gettext as _ + +import mime_operations +import mime_categories +import mime_view_cat_mode + +import gtk_common +from gtk_common import browse_for_file +from gtk_common import ICON_SIZE + + +GLADE_FILE = os.path.join(DIR,"ui_cat_mode.glade") +SHOW_ONLY_ASSOCIATED = True + +class CategoriesView(gtk_common.CategoriesWidget): + CAT_ID = 0 + CAT_NAME, CAT_IMG = 1, 2 + + + def __init__(self, builder, on_category_changed): + """ on_category_changed : called every time category is changed, + should accept list of category ids""" + self.categories_view = builder.get_object("categories_view") + self.on_category_changed = on_category_changed + + # id, name, img + self.list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf) + self.categories_view.set_model(self.list_store) + + self._fill_list_store() + + column = gtk_common.ImageTextColumn(_("Categories"), self.CAT_IMG, self.CAT_NAME) + self.categories_view.append_column(column) + + #register callback + tree_selection = self.categories_view.get_selection() + tree_selection.connect("changed", self.on_selection_changes) + + def _fill_list_store(self): + categories = sorted((_(name), cat ,id_) + for cat, name, id_ in mime_categories.get_known_categories()) + + # should be last + categories.append((_(mime_categories.ANY_CATEGORY_NAME), + mime_categories.ANY_CATEGORY, + mime_categories.ANY_CATEGORY_ID)) + + + for transl_name, cat, id_ in categories: + icon_name = mime_categories.get_icon_name(cat) + icon = mime_operations.get_icon_by_name(icon_name, ICON_SIZE) + self.list_store.append([id_, transl_name, icon]) + + def on_selection_changes(self, tree_selection): + (model, pathlist) = tree_selection.get_selected_rows() + cat_ids = [] + for path in pathlist : + tree_iter = model.get_iter(path) + value = model.get_value(tree_iter, self.CAT_ID) + cat_ids.append(value) + self.on_category_changed(cat_ids) + + + +class AddAppDialog: + def __init__(self, builder, parent_window, on_add_dialog_apply): + self.builder = builder + self.on_add_dialog_apply = on_add_dialog_apply + + self.dialog = self.builder.get_object("add_app_dialog") + self.app_chooser = self.builder.get_object("appchooser_widget") + self.app_chooser.set_show_all(True) + self.app_chooser.connect("application-activated", self.on_apply) + self.app_chooser.connect("application-selected", self.on_application_selected) + + self.dialog.set_transient_for(parent_window) + self.dialog.connect("delete-event", self.hide) + + self.custom_entry = self.builder.get_object("custom_entry") + self.custom_entry.connect("changed", self.on_custom_entry_changed) + + self.selected_app = None + self.cline_text = "" + + self._init_buttons() + + def _init_buttons(self): + self.cancel_button = self.builder.get_object("add_app_dialog_cancel_button") + self.apply_button = self.builder.get_object("add_app_dialog_apply_button") + self.file_chooser_button = self.builder.get_object("file_chooser_button") + + self.cancel_button.connect("clicked", self.hide) + self.apply_button.connect("clicked", self.on_apply) + self.file_chooser_button.connect("clicked", self.on_file_chooser) + + def show(self): + self.dialog.run() + + def hide(self, *args): + self.dialog.hide() + + return False #!!! + + def on_custom_entry_changed(self, *args): + text = self.custom_entry.get_text() + if text != self.cline_text: + self.cline_text = text + + self.selected_app = mime_operations.get_app_from_cline(self.cline_text, + None) + + def on_application_selected(self, widget, app): + self.selected_app = app + + name, icon, self.cline_text = mime_operations.get_app_bio(app, ICON_SIZE) + self.custom_entry.set_text(self.cline_text) + + def on_file_chooser(self, *args): + browse_for_file(_('Choose an application'), + '/usr/bin', + self.dialog, + self.custom_entry.set_text) + + def on_apply(self, *args): + #“application-activated” + self.on_add_dialog_apply(self.selected_app) + self.hide() + + +class MimeSetDialog: + APP_OBJ = 0 + APP_NAME = 1 + APP_ICON = 2 + APP_CL = 3 + + def __init__(self, builder, parent_window, on_dialog_ok): + self.builder = builder + + self.on_dialog_ok = on_dialog_ok + + self.dialog = self.builder.get_object("mimetype_set_dialog") + self.dialog.set_transient_for(parent_window) + self.dialog.connect("delete-event", self.hide) + + self.mtypes_dialog_label = builder.get_object("mtypes_dialog_label") + + self._init_buttons() + self._init_view() + self.add_app_dialog = None + + # + self.mtypes = [] + + def _init_buttons(self): + self.cancel_button = self.builder.get_object("dialog_cancel_button") + self.apply_button = self.builder.get_object("dialog_apply_button") + self.add_button = self.builder.get_object("dialog_add_button") + + self.cancel_button.connect("clicked", self.hide) + self.apply_button.connect("clicked", self.on_row_activated) + self.add_button.connect("clicked", self.on_add_app_clicked) + + def _init_view(self): + self.alt_view = self.builder.get_object("alternatives_treeview") + self.alt_view.connect("row_activated", self.on_row_activated) + + # desktop_file, app_name, icon + self.list_store = Gtk.ListStore(Gio.AppInfo, str ,GdkPixbuf.Pixbuf, str) + + self.alt_view.set_model(self.list_store) + + column_app = gtk_common.ImageTextColumn(_("Application"), self.APP_ICON, self.APP_NAME) + column_app.set_sort_column_id(self.APP_NAME) + + self.alt_view.append_column(column_app) + + column_cl = gtk_common.TextColumn(_("Command line"), self.APP_CL) + column_cl.set_sort_column_id(self.APP_CL) + + self.alt_view.append_column(column_cl) + + def show(self, mtypes): + self.mtypes = mtypes + self.set_data() + self.indicate_default() + + self.mtypes_dialog_label.set_text(" \n".join(self.mtypes)) + + self.dialog.run() + + def hide(self, *args): + self.dialog.hide() + self.list_store.clear() + + return True #!!! + + def set_data(self): + #if self.vbox is not None: return + apps = mime_operations.get_apps_for_mtypes(self.mtypes) + + for app in apps: + name, icon, cl = mime_operations.get_app_bio(app, ICON_SIZE) + + self.list_store.append([app, name, icon, cl]) + + def indicate_default(self): + if len(self.mtypes) == 1: + default_app = mime_operations.get_default_app(self.mtypes[0]) + default_cl = default_app.get_commandline() if default_app is not None else "" + + #iteratw + tree_iter = self.list_store.get_iter_first() + while tree_iter: + current_cl = self.list_store.get_value(tree_iter, self.APP_CL) + + if current_cl == default_cl: + self.alt_view.get_selection().select_iter(tree_iter) + return + tree_iter = self.list_store.iter_next(tree_iter) + + + def on_row_activated(self, *args): + tree_selection = self.alt_view.get_selection() + (model, pathlist) = tree_selection.get_selected_rows() + + selection = None + + for path in pathlist : + tree_iter = model.get_iter(path) + value = model.get_value(tree_iter, self.APP_OBJ) + self.selection = value + + self.hide() + self.on_dialog_ok(selection, self.mtypes) + + + + def on_add_new_app(self, new_app): + if new_app is not None: + name, icon, cl = mime_operations.get_app_bio(new_app, ICON_SIZE) + tree_iter = self.list_store.append([new_app, name, icon, cl]) + self.alt_view.get_selection().select_iter(tree_iter) + + def on_add_app_clicked(self, *args): + if self.add_app_dialog is None: + self.add_app_dialog = AddAppDialog(self.builder, self.dialog,\ + self.on_add_new_app) + + self.add_app_dialog.show() + + +class MainWidget: + def __init__(self, builder, window): + self.builder = builder + self.window = window + + self.builder.add_from_file(GLADE_FILE) + self.builder.connect_signals(self) + + self.root_widget = self.builder.get_object("root_widget") + + self.cat_view = CategoriesView(self.builder, + self.on_category_changed) + + self.mime_view = mime_view_cat_mode.MimeViewCategories(self.builder, + self.on_mtypes_edit, + self.on_mtypes_reset) + + self.dialog = MimeSetDialog(self.builder, + self.window, + self.on_mtypes_edit_ok) + + self._init_buttons() + + def _init_buttons(self): + # only show mimetypes that have an associated application + hide_unknown_flag = self.builder.get_object("show_associated_only_button") + hide_unknown_flag.connect("toggled", self.on_hide_unknown_flag_toggled) + hide_unknown_flag.set_active(SHOW_ONLY_ASSOCIATED) + + + def get_widget(self): + return self.root_widget + + def on_hide_unknown_flag_toggled(self, flag_widget): + hide_unknown = flag_widget.get_active() + self.mime_view.filter_associated(show_all = not hide_unknown) + + def on_category_changed(self, category_id): + self.mime_view.filter_category(category_id[0]) + + def on_mtypes_edit_ok(self, app, mtypes): + if app is not None: + mime_operations.set_app_default(app, mtypes) + self.mime_view.update_data(mtypes) + + + def on_mtypes_edit(self, *args): + selected_mime_types = self.mime_view.get_selection() + + self.dialog.show(selected_mime_types) + + def on_mtypes_reset(self, *args): + selected_mime_types = self.mime_view.get_selection() + + mime_operations.reset_associations(selected_mime_types) + + self.mime_view.update_data(selected_mime_types) + + diff --git a/src/cat_mode/mime_view_cat_mode.py b/src/cat_mode/mime_view_cat_mode.py new file mode 100644 index 0000000..c189b66 --- /dev/null +++ b/src/cat_mode/mime_view_cat_mode.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 1 08:15:56 2016 + +@author: pavel +""" +import sys +sys.path.append('../common') + +from gi.repository import Gtk, Gdk, Gio +from gi.repository import GdkPixbuf + +import gtk_common +from gtk_common import ICON_SIZE + +import mime_operations, mime_categories +import mime_view +import data_filter + +from locale import gettext as _ + + + +class MimeViewCategories(mime_view.MimeView): + UI_VIEW_ID = "mimetypes_view" + UI_CONTEXT_MENU_ID = "mimeview_context_menu" + + #model columns + MIME_TYPE = 0 + MIME_CATEGORY = 1 + MIME_DESC = 2 + MIME_ICON = 3 + + MIME_APP = 4 + MIME_APP_IMG = 5 + + LIST_STORE_COLUMN_TYPES = [str, int, str, GdkPixbuf.Pixbuf, + str, GdkPixbuf.Pixbuf] + + def __init__(self, builder, on_items_edit, on_items_reset): + """ + on_items_edit : function to call when edit action required + on_items_reset : function to call when reset action required + """ + self.on_items_edit = on_items_edit + self.on_items_reset = on_items_reset + + super(MimeViewCategories, self).__init__(builder) + self._init_buttons() + + self.set_data() + + def _init_buttons(self): + reset_button = self.builder.get_object("reset_button") + reset_button.connect("clicked", self.on_items_reset) + + edit_button = self.builder.get_object("edit_button") + edit_button.connect("clicked", self.on_items_edit) + + def _init_mappings(self): + super(MimeViewCategories, self)._init_mappings() + self.context_menu_items.update({"menuitem_revert": lambda *param : self.on_items_reset(), + "menuitem_edit": lambda *param : self.on_items_edit(), + }) + + + self.keyboard_keys_actions.update({"Return" : self.on_items_edit, + "Enter" : self.on_items_edit, + "space" : self.on_items_edit, + "BackSpace" : self.on_items_reset, + "Delete" : self.on_items_reset + }) + + + def _init_columns(self, *args, **kwargs): + self._add_filetype_column() + self._add_app_column() + + def _add_app_column(self): + column_program = gtk_common.ImageTextColumn(_("Program"), self.MIME_APP_IMG, self.MIME_APP) + column_program.set_sort_column_id(self.MIME_APP) + self.mtypes_view.append_column(column_program) + + def _init_filters(self): + super(MimeViewCategories, self)._init_filters() + + + associated_filter = data_filter.GeneralFilter(False, self.MIME_APP, + lambda app : (app is not None) and (len(app) > 0)) + self._add_filter_to_cascade("associated_filter", associated_filter) + + + def _get_model_data_row(self, mtype): + mtype, category, descr, icon = super(MimeViewCategories, self)._get_model_data_row(mtype) + + app = mime_operations.get_default_app(mtype) + app_name, app_icon, *extra = mime_operations.get_app_bio(app, ICON_SIZE) + + return [mtype, category, descr, icon, app_name, app_icon] + + + def update_data(self, mtypes_to_change): + tree_iter = self.list_store.get_iter_first() + + while tree_iter: + row_mtype = self.list_store.get_value(tree_iter, self.MTYPE) + + if row_mtype in mtypes_to_change: + app = mime_operations.get_default_app(row_mtype) + name, icon, *extra = mime_operations.get_app_bio(app, ICON_SIZE) + + self.list_store.set_value(tree_iter, self.APP, name) + self.list_store.set_value(tree_iter, self.APP_IMG, icon) + + tree_iter = self.list_store.iter_next(tree_iter) + + def filter_associated(self, show_all = True): + self.set_filter_params("associated_filter", + enabled = not show_all) + + + def on_double_click(self, widget, event): + self.on_items_edit() + + + def get_selection(self): + """return mime types from selected rows""" + return super(MimeViewCategories, self).get_selection(self.MIME_TYPE) diff --git a/src/cat_mode/ui_cat_mode.glade b/src/cat_mode/ui_cat_mode.glade new file mode 100644 index 0000000..10d1bf9 --- /dev/null +++ b/src/cat_mode/ui_cat_mode.glade @@ -0,0 +1,471 @@ + + + + + + False + Select Application + True + True + normal + + + False + vertical + 2 + + + False + end + + + gtk-cancel + True + True + True + True + + + True + True + 0 + + + + + gtk-ok + True + True + True + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + vertical + + + True + False + Select from list ... + start + 1 + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + True + True + + + True + False + + + True + True + + + True + True + 0 + + + + + gtk-open + True + True + True + True + + + False + True + 1 + + + + + + + True + False + ... or enter custom command + + + + + False + True + 2 + + + + + True + True + 1 + + + + + + + True + False + gtk-edit + + + True + False + gtk-revert-to-saved + + + True + False + + + Edit + True + False + image-gtk-edit + False + + + + + Revert + True + False + image-gtk-revert + False + + + + + False + Select + True + 640 + 400 + dialog + True + + + False + vertical + 2 + + + False + end + + + gtk-add + True + True + True + True + + + True + True + 0 + + + + + gtk-cancel + True + True + True + True + + + True + True + 1 + + + + + gtk-apply + True + True + True + True + + + True + True + 2 + + + + + False + False + 0 + + + + + True + False + vertical + + + True + False + start + Select default application for following filetype(s): + + + False + True + 0 + + + + + True + True + in + + + True + False + + + True + False + start + start + ... + True + + + + + + + False + True + 1 + + + + + True + True + in + + + True + True + + + + + + + + True + True + 2 + + + + + True + True + 1 + + + + + + + 100 + 1 + 10 + + + True + False + + + True + False + vertical + + + + + + True + False + + + True + True + + + + + + False + True + 0 + + + + + True + False + vertical + + + False + True + 1 + + + + + True + True + mimetypes_view_adj + in + + + True + True + True + mimetypes_view_adj + True + True + horizontal + True + + + multiple + + + + + + + True + True + 2 + + + + + True + True + 2 + + + + + True + False + immediate + + + Show only associated types + True + True + False + False + 0 + True + + + False + True + 0 + + + + + + + + + + + gtk-edit + True + True + True + True + False + + + False + True + end + 3 + + + + + gtk-revert-to-saved + True + True + True + True + False + + + False + True + end + 4 + + + + + False + True + 3 + + + + + + diff --git a/src/common/data_filter.py b/src/common/data_filter.py new file mode 100644 index 0000000..e97f018 --- /dev/null +++ b/src/common/data_filter.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 25 07:18:57 2016 + +@author: pavel +""" + +import mime_categories + +class DataFilter: + def __init__(self, enabled = True): + self.enabled = enabled + def process_row(self, model, iter_, data): + return not self.enabled + def set_params(self, **kwargs): + for param_name, param_value in kwargs.items(): + print("filtering", param_name, param_value) + if param_name in self.__dict__: + self.__dict__[param_name] = param_value + +class GeneralFilter(DataFilter): + def __init__(self, enabled, column, value_condition_fn): + self.enabled = enabled + self.column = column + self.value_condition_fn = value_condition_fn + def process_row(self, model, iter_, data): + if not self.enabled: return True + value = model.get_value(iter_, self.column) + return self.value_condition_fn(value) + + + +class CategoryFilter(DataFilter): + def __init__(self, category_column, current_category_id): + self.category_id_column = category_column + self.current_category_id = current_category_id + + def process_row(self, model, iter_, data): + if self.current_category_id == mime_categories.ANY_CATEGORY_ID: + return True + + return model.get_value(iter_, self.category_id_column) == self.current_category_id diff --git a/src/common/gtk_common.py b/src/common/gtk_common.py new file mode 100644 index 0000000..87fe1bc --- /dev/null +++ b/src/common/gtk_common.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 25 07:18:57 2016 + +@author: pavel +""" + +import mime_categories +import mime_operations + +from gi.repository import Gtk +from gi.repository import GdkPixbuf +from locale import gettext as _ + +ICON_SIZE = 24 + +def browse_for_file(title, start_directory, parent_window, on_success_callback): + f_dialog = Gtk.FileChooserDialog(title, + action = Gtk.FileChooserAction.OPEN, + buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT)) + f_dialog.set_current_folder(start_directory) + f_dialog.set_transient_for(parent_window) + + if f_dialog.run() == Gtk.ResponseType.ACCEPT: + on_success_callback(f_dialog.get_filename()) + + f_dialog.destroy() + +class ImageTextColumn(Gtk.TreeViewColumn): + def __init__(self, column_name, model_index_img, model_index_txt, *args, **kwargs): + super(ImageTextColumn, self).__init__(column_name, *args, **kwargs) + + renderer_pixbuf = Gtk.CellRendererPixbuf() + renderer_text = Gtk.CellRendererText() + self.cell_renderers = (renderer_pixbuf, renderer_text, ) + + self.pack_start(renderer_pixbuf, expand = False) + self.add_attribute(renderer_pixbuf, "pixbuf", model_index_img) + + self.pack_start(renderer_text, expand = True) + self.add_attribute(renderer_text, "text", model_index_txt) + + self.set_resizable(True) + + def set_attribute(self, name, model_column): + for renderer in self.cell_renderers: + self.add_attribute(renderer, name, model_column) + + +class TextColumn(Gtk.TreeViewColumn): + def __init__(self, column_name, model_index_txt, *args, **kwargs): + super(TextColumn, self).__init__(column_name, *args, **kwargs) + renderer_text = Gtk.CellRendererText() + self.cell_renderers = (renderer_text, ) + + self.pack_start(renderer_text, expand = True) + self.add_attribute(renderer_text, "text", model_index_txt) + + self.set_resizable(True) + + + + def set_attribute(self, name, model_column): + for renderer in self.cell_renderers: + self.add_attribute(renderer, name, model_column) + +class FlagColumn(Gtk.TreeViewColumn): + def __init__(self, column_name, model_index_bool, on_toggle, on_mark_all, + *args, **kwargs): + super(FlagColumn, self).__init__(None, *args, **kwargs) + + self.header_checkbox = CyclicCheckbox(on_mark_all) + self.title = Gtk.Label(column_name) + + self.header_widget = Gtk.HBox() + self.header_widget.pack_start(self.header_checkbox, True, True, 0) + self.header_widget.pack_start(self.title, True, True, 0) + self.header_widget.show_all() + self.set_widget(self.header_widget) + + + renderer_flag = Gtk.CellRendererToggle() + self.cell_renderers = (renderer_flag, ) + + renderer_flag.connect("toggled", on_toggle) + + self.pack_start(renderer_flag, expand = False) + self.add_attribute(renderer_flag, "active", model_index_bool) + + self.set_clickable(True) + self.set_resizable(False) + self.connect("clicked", self.header_checkbox.on_click) + + + #self.set_sort_indicator(True) + + + #def on_header_clicked(self, *args): + # self.header_checkbox.on_click() + + + def set_flag_degenerate(self, degenerate): + self.header_checkbox.set_degenerate(degenerate) + def set_flag_inconsistent(self): + self.header_checkbox.set_inconsistent() + def set_flag_active(self, *args): + self.header_checkbox.set_active(*args) + def set_flag_inactive(self): + self.header_checkbox.set_inactive() + + def set_attribute(self, name, model_column): + for renderer in self.cell_renderers: + self.add_attribute(renderer, name, model_column) + + +class CyclicCheckbox(Gtk.CheckButton): + STATE_ACTIVE = 0 + STATE_INACTIVE = 1 + STATE_INCONSISTENT = 2 + + + def __init__(self, on_state_change, *args, **kwargs): + super(CyclicCheckbox, self).__init__(None, *args, **kwargs) + self.on_state_change = on_state_change + + #self.connect("clicked", self.on_click) + + self.set_degenerate(False) + self.set_inactive() + + def on_click(self, *args): + self.next_state() + self.on_state_change(self.get_inconsistent(), self.get_active()) + + return False + + def next_state(self): + if self.state == CyclicCheckbox.STATE_ACTIVE: + self.set_inactive() + elif self.state == CyclicCheckbox.STATE_INACTIVE: + if self.degenerate: # skip inconsistent + self.set_active() + else: + self.set_inconsistent() + else: + self.set_active() + + def set_degenerate(self, degenerate = False): + # if degenerate, then inconsistent state will be in cycle + self.degenerate = degenerate + + + def set_inconsistent(self): + #print("inconsistent") + self.state = CyclicCheckbox.STATE_INCONSISTENT + + super(CyclicCheckbox, self).set_inconsistent(True) + super(CyclicCheckbox, self).set_active(False) + + def set_active(self, active = True): + #print("active") + if active: + self.state = CyclicCheckbox.STATE_ACTIVE + + super(CyclicCheckbox, self).set_inconsistent(False) + super(CyclicCheckbox, self).set_active(True) + else: + self.set_inactive() + + def set_inactive(self): + #print("inactive") + self.state = CyclicCheckbox.STATE_INACTIVE + + super(CyclicCheckbox, self).set_inconsistent(False) + super(CyclicCheckbox, self).set_active(False) + + + +class IconTextLabel(Gtk.Box): + def __init__(self, icon, text, *args, **kwargs): + super( IconTextLabel, self).__init__(orientation=Gtk.Orientation.HORIZONTAL) + + label = Gtk.Label(text) + + self.pack_start(icon, True, True, 0) + self.pack_start(label, True, True, 0) + +class CategoriesWidget: + CAT_ID = 0 + CAT_NAME = 1 + CAT_IMG = 2 + + def __init__(self, builder, widget_name, on_category_changed): + self.widget = builder.get_object(widget_name) + self.on_category_changed = on_category_changed + + self._init_model() + self._init_widget() + + self._set_data() + + def _init_model(self): + self.list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf) + self.widget.set_model(self.list_store) + + def _init_widget(self): + raise NotImplemented + + def _set_data(self): + categories = sorted((_(name), cat ,id_) + for cat, name, id_ in mime_categories.get_known_categories()) + + # should be last + categories.append((_(mime_categories.ANY_CATEGORY_NAME), + mime_categories.ANY_CATEGORY, + mime_categories.ANY_CATEGORY_ID)) + + + for transl_name, cat, id_ in categories: + icon_name = mime_categories.get_icon_name(cat) + icon = mime_operations.get_icon_by_name(icon_name, ICON_SIZE) + self.list_store.append([id_, transl_name, icon]) + + + diff --git a/src/common/mime_categories.py b/src/common/mime_categories.py new file mode 100644 index 0000000..0d4481e --- /dev/null +++ b/src/common/mime_categories.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Dec 6 14:51:39 2016 + +@author: pavel +""" + +ANY_CATEGORY = "any" +ANY_CATEGORY_ID = 0 +ANY_CATEGORY_NAME = "All" +ANY_CATEGORY_ICON = "unknown" + +CATEGORY_IDS = {"application" : 1, + "audio" : 2, + "image" : 3, + "text" : 4, + "video" : 5, } + +CATEGORY_NAMES = { "application" : "Application" , + "audio" : "Audio", + "image" : "Image", + "text" : "Text", + "video" : "Video"} + +# name, icon +CATEGORY_ICONS = {"application" : "application-x-executable", + "audio" : "audio-x-generic", + "image" : "image-x-generic", + "text" : "text-x-generic", + "video" : "video-x-generic"} + + +def get_category_id(mime_type): + if mime_type is None: return ANY_CATEGORY_ID + + slash_ind = mime_type.find("/") + + if slash_ind <= 0: return ANY_CATEGORY_ID + + top_level = mime_type[:slash_ind] + return CATEGORY_IDS.get(top_level, ANY_CATEGORY_ID) + +def get_category_name(cat): + return CATEGORY_NAMES.get(cat, ANY_CATEGORY_NAME) + +def get_known_categories(): + """ list of (category, category name, id) """ + return [(cat , get_category_name(cat), id_) for cat, id_ + in CATEGORY_IDS.items()] + + +def get_icon_name(cat) : + return CATEGORY_ICONS.get(cat, ANY_CATEGORY_ICON) \ No newline at end of file diff --git a/src/common/mime_operations.py b/src/common/mime_operations.py new file mode 100644 index 0000000..64d4c61 --- /dev/null +++ b/src/common/mime_operations.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Nov 23 18:26:27 2016 + +@author: pavel +""" + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gio, Gtk +from gi.repository import GdkPixbuf + +import os.path + +from functools import lru_cache + +def get_default_app(mime_type): + return Gio.app_info_get_default_for_type(mime_type, False) + +def get_app_from_cline(cline, name, terminal = False): + """ cl - command line + name - app name + terminal - open in terminal """ + + if cline is None or len(cline) == 0: + return None + + if name is None or len(name) == 0: + name = os.path.basename(cline) + flag = Gio.AppInfoCreateFlags.NEEDS_TERMINAL if terminal \ + else Gio.AppInfoCreateFlags.NONE + return Gio.AppInfo.create_from_commandline(cline, name, flag) + +@lru_cache(maxsize=256) +def get_app_bio(app, icon_size): + name = "" + icon = None + cl = "" + + if app is not None: + name = app.get_name() + icon = get_icon_by_app(app, icon_size) + cl = app.get_commandline() + + return name, icon, cl + + +@lru_cache(maxsize=256) +def get_mime_bio(mime_type, icon_size): + description = "" + icon = None + + try: + description = Gio.content_type_get_description(mime_type) + icon = get_mime_icon(mime_type, icon_size) + except Exception as e: + print(e) + + return description, icon + + +def get_known_mtypes(): + return Gio.content_types_get_registered() + +def get_mtypes_for_app(app) : + if app is None : return [] + mtypes = set(app.get_supported_types()) + #does not take in consideration associations added with g_app_info_add_supports_type() + + + for mtype in get_known_mtypes(): + for a in get_apps_for_mtype(mtype): + if app.equal(a): + mtypes.add(mtype) + break + + return sorted(list(mtypes)) + +def get_apps_for_mtype(mime_type): + return Gio.app_info_get_all_for_type(mime_type) + +def get_apps_for_mtypes(mime_types_list): + apps = {} + for mtype in mime_types_list: + for app in get_apps_for_mtype(mtype): + #using id_ to eliminate duplicates + id_ = app.get_commandline()#app.get_id() + apps[id_] = app + return list(apps.values()) + +def is_app_associated(app, mtype): + associated_app_ids = [ap.get_id() for ap in get_apps_for_mtype(mtype)] + return app.get_id() in associated_app_ids + +def is_app_default(app, mtype): + try: + default = get_default_app(mtype) + return app.equal(default) + except Exception as e: + print(e) + return False + +def set_app_default(app, mtypes): + for mtype in mtypes: + try: + app.set_as_default_for_type(mtype) + except Exception as e: + print(e) + +def reset_associations(mtypes): + for mtype in mtypes: + try: + Gio.AppInfo.reset_type_associations(mtype) + except Exception as e: + print(e) + +def remove_associations(app, mtypes): + for mtype in mtypes: + try: + app.remove_supports_type(mtype) + except Exception as e: + print(e) + + + +def get_all_apps(): + return Gio.app_info_get_all() + +@lru_cache(maxsize=256) +def get_icon_by_name(icon_name, size): + icon = None + if icon_name is not None: + try: + icon = Gtk.IconTheme.get_default().load_icon(icon_name, size, 0); + except: + try: + icon = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_name, + size, size, True) + except Exception as e: + print(icon_name, e) + + return icon + +@lru_cache(maxsize=256) +def get_icon_by_app(app, size): + #ubuntu-tweak + + icon = None + try: + gicon = app.get_icon() + + if gicon and isinstance(gicon, Gio.ThemedIcon): + names = gicon.get_names() + names_list = [] + + for name in names: + names_list.append(os.path.splitext(name)[0]) + + + theme = Gtk.IconTheme.get_default() + iconinfo = Gtk.IconTheme.choose_icon(theme, names_list, size, + Gtk.IconLookupFlags.USE_BUILTIN) + if iconinfo: + icon = iconinfo.load_icon() + + elif gicon and isinstance(gicon, Gio.FileIcon): + icon_path = app.get_icon().get_file().get_path() + + if icon_path: + icon = get_icon_by_name(icon_path, size) + + except Exception as e: + print(app, e) + + return icon + +@lru_cache(maxsize=256) +def get_mime_icon(mtype, size): + icon = None + + try: + gicon = Gio.content_type_get_icon(mtype) + + theme = Gtk.IconTheme.get_default() + iconinfo = Gtk.IconTheme.choose_icon(theme, gicon.get_names(), size, + Gtk.IconLookupFlags.USE_BUILTIN) + if iconinfo: + icon = iconinfo.load_icon() + + if icon and icon.get_width() != size: + icon = icon.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) + + except Exception as e: + print(e) + + return icon + diff --git a/src/common/mime_view.py b/src/common/mime_view.py new file mode 100644 index 0000000..06885dd --- /dev/null +++ b/src/common/mime_view.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 1 08:15:56 2016 + +@author: pavel +""" + +from gi.repository import Gtk, Gdk, Gio +from gi.repository import GdkPixbuf + +from gtk_common import ImageTextColumn, FlagColumn, TextColumn +from gtk_common import ICON_SIZE + +import mime_operations, mime_categories +import data_filter + +from locale import gettext as _ + + + +class MimeView: + UI_VIEW_ID = None + UI_CONTEXT_MENU_ID = None + + #model columns + MIME_TYPE = 0 + MIME_CATEGORY = 1 + MIME_DESC = 2 + MIME_ICON = 3 + LIST_STORE_COLUMN_TYPES = [str, int, str, GdkPixbuf.Pixbuf] + + + def __init__(self, builder): + self.builder = builder + + self.mtypes_view = self.builder.get_object(self.UI_VIEW_ID) + + #callbacks + self.mtypes_view.connect("button-press-event", self.on_mouse_clicked) + self.mtypes_view.connect("key-press-event", self.on_key_pressed) + + #allow multiple selection + self.tree_selection = self.mtypes_view.get_selection() + self.tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + self._init_mappings() + self._init_filters() + + self._init_model() + self._init_columns() + self._init_context_menu() + + def _init_mappings(self): + #mappings + self.context_menu_items = {} # dictionary of item_id: item_action pairs + self.keyboard_keys_actions = {"Menu" : self.show_context_menu + } # dictionary of key_name: key_action pairs + + def _init_filters(self): + self.ordered_data_filters = [] #order may be matter + self.data_filters = {} # save reference by name + + #add category filter + category_filter = data_filter.CategoryFilter(self.MIME_CATEGORY, + mime_categories.ANY_CATEGORY_ID) + + self._add_filter_to_cascade("category_filter", category_filter) + + def _add_filter_to_cascade(self, filter_name, data_filter): + self.ordered_data_filters.append(data_filter) + self.data_filters[filter_name] = data_filter + + def _init_model(self): + """liststore -> filter -> sort -> view""" + self.list_store = Gtk.ListStore(*self.LIST_STORE_COLUMN_TYPES) + + self.filter_model = self.list_store.filter_new() + self.filter_model.set_visible_func(self.cascade_filter_func, data=None) + + self.sortable_model = Gtk.TreeModelSort(self.filter_model) + + self.mtypes_view.set_model(self.sortable_model) + + def _init_columns(self): + self._add_filetype_column() + + def _init_context_menu(self): + self.context_menu = self.builder.get_object(self.UI_CONTEXT_MENU_ID) + + for item_id, action in self.context_menu_items.items(): + item = self.builder.get_object(item_id) + item.connect("activate", action) + + + def set_data(self, *args, **kwargs): + self.list_store.clear() + mtypes = self._get_initial_data(*args, **kwargs) + self._add_mtypes(mtypes) + + def set_filter_params(self, filter_name, **kwargs): + if filter_name in self.data_filters: + self.data_filters[filter_name].set_params(**kwargs) + + self.filter_model.refilter() + + def _add_mtypes(self, mtypes, *args, **kwargs): + for mtype in mtypes: + data_row = self._get_model_data_row(mtype, *args, **kwargs) + self.list_store.append(data_row) + + def _get_initial_data(self, *args, **kwargs): + return mime_operations.get_known_mtypes() + + def _add_filetype_column(self): + self.desc_column = ImageTextColumn(_("File Type"), self.MIME_ICON, self.MIME_DESC) + self.desc_column.set_sort_column_id(self.MIME_DESC) + self.mtypes_view.append_column(self.desc_column) + + + def _get_model_data_row(self, mtype): + descr, icon = mime_operations.get_mime_bio(mtype, ICON_SIZE) + category = mime_categories.get_category_id(mtype) + return [mtype, category, descr, icon] + + def cascade_filter_func(self, model, iter_, data): + # return True if all conditions are satisfied + for data_filter in self.ordered_data_filters: + if not data_filter.process_row(model, iter_, data): + return False + return True + + def on_mouse_clicked(self, widget, event): + if event.button == 3: #right button + self.show_context_menu(widget, event) + return True + if event.type == Gdk.EventType._2BUTTON_PRESS: #double click + self.on_double_click(widget, event) + return True + + def on_key_pressed(self, widget, event): + #do not reset selection + keyname = Gdk.keyval_name(event.keyval) + print(keyname, "button pressed") + + if keyname in self.keyboard_keys_actions: + #execute + self.keyboard_keys_actions.get(keyname, print)(widget, event) + return True + + def on_double_click(self, widget, event): + raise NotImplemented + + + def show_context_menu(self, widget, event): + self.context_menu.popup( None, None, None, None, 0, event.time) + + + def get_store_path(self, sorted_path): + """applying filters and sorting cause problem that + selection path does not correspond store path any more""" + + filtered_path = self.sortable_model.convert_path_to_child_path(sorted_path) + store_path = self.filter_model.convert_path_to_child_path(filtered_path) + + #print(sorted_path, filtered_path, store_path) + return store_path + + def get_new_value(self, model, tree_iter, get_value_func = None, + value = None, **kwargs): + if get_value_func is not None: + value = get_value_func(model, tree_iter, **kwargs) + + return value + + def set_value_if_cond(self, model, tree_iter, column, + get_value_func = None, value = None, + conditions_func = None, **kwargs): + """column = column to change value + get_value_func = function to compute new value depending on row + should accept model and iter as parameters + value = value to set if get_value_func is not specified + extra_conditions_func = function to check if possible to set new value + """ + if conditions_func is None or \ + conditions_func(model, tree_iter, **kwargs): + value = self.get_new_value(self.list_store, tree_iter, + get_value_func, value, **kwargs) + model.set_value(tree_iter, column, value) + + + def set_value_to_all(self, column, + get_value_func = None, value = None, + conditions_func = None, **kwargs): + + tree_iter = self.list_store.get_iter_first() + while tree_iter: + self.set_value_if_cond(self.list_store, tree_iter, column, + get_value_func, value, + conditions_func, **kwargs) + + tree_iter = self.list_store.iter_next(tree_iter) + + def set_value_to_selected(self, column, + get_value_func = None, value = None, + conditions_func = None, **kwargs): + (model, pathlist) = self.tree_selection.get_selected_rows() + + for path in pathlist : + tree_iter = self.list_store.get_iter(self.get_store_path(path)) + + self.set_value_if_cond(self.list_store, tree_iter, column, + get_value_func, value, + conditions_func, **kwargs) + + def set_value_to_visible(self, column, + get_value_func = None, value = None, + conditions_func = None, **kwargs): + + tree_iter = self.list_store.get_iter_first() + while tree_iter: + if self.cascade_filter_func(self.list_store, tree_iter, None): + self.set_value_if_cond(self.list_store, tree_iter, column, + get_value_func, value, + conditions_func, **kwargs) + tree_iter = self.list_store.iter_next(tree_iter) + + def get_selection(self, column): + selection = [] + + (model, pathlist) = self.tree_selection.get_selected_rows() + + for path in pathlist : + tree_iter = self.list_store.get_iter(self.get_store_path(path)) + selection.append(self.list_store.get_value(tree_iter, column)) + return selection + + def filter_category(self, category_id = mime_categories.ANY_CATEGORY_ID): + self.set_filter_params("category_filter", + current_category_id = category_id) diff --git a/src/mime_editor.py b/src/mime_editor.py new file mode 100644 index 0000000..ffccb4c --- /dev/null +++ b/src/mime_editor.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 7 15:11:32 2016 + +@author: pavel +""" +import os +DIR = os.path.dirname(os.path.realpath(__file__)) + +import sys +sys.path.append('./common') +sys.path.append('./app_mode') +sys.path.append('./cat_mode') + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import locale +locale.setlocale(locale.LC_ALL, '') +if os.path.isdir("./locale"): + locale.bindtextdomain(APP, "./locale") + locale.textdomain(APP) +from locale import gettext as _ + +import signal +signal.signal(signal.SIGINT, signal.SIG_DFL) #handle Ctrl-C + +import mime_editor_app_mode +import mime_editor_cat_mode + +APP = os.path.join(DIR, "python-mime-editor-gui") +GLADE_FILE = "ui_main_window.glade" + +editor_modes = { "by_apps" : mime_editor_app_mode.MainWidget, + "by_categories" : mime_editor_cat_mode.MainWidget } + +class MainWindow: + def __init__(self, mode): + self.builder = Gtk.Builder() + self.builder.set_translation_domain(APP) + self.builder.add_from_file(GLADE_FILE) + + self.window = self.builder.get_object("main_window") + self.window.connect("delete-event", self.on_close) + + self.viewport = self.builder.get_object("main_box") + + + editor = editor_modes.get(mode)(self.builder, self.window) + + self.viewport.pack_start(editor.get_widget(), True, True, 0) + + + def run(self): + self.window.show() + Gtk.main() + + def on_close(self, *args): + Gtk.main_quit() + + +if __name__ == "__main__": + #main(sys.argv) + #window = MainWindow("by_categories") + window = MainWindow("by_apps") + window.run() diff --git a/src/ui_main_window.glade b/src/ui_main_window.glade new file mode 100644 index 0000000..b3b650e --- /dev/null +++ b/src/ui_main_window.glade @@ -0,0 +1,180 @@ + + + + + + 640 + 400 + False + 5 + True + + + True + False + vertical + 2 + + + True + False + + + True + False + _Файл + True + + + True + False + + + gtk-new + True + False + True + True + + + + + gtk-open + True + False + True + True + + + + + gtk-save + True + False + True + True + + + + + gtk-save-as + True + False + True + True + + + + + True + False + + + + + gtk-quit + True + False + True + True + + + + + + + + + True + False + _Правка + True + + + True + False + + + gtk-cut + True + False + True + True + + + + + gtk-copy + True + False + True + True + + + + + gtk-paste + True + False + True + True + + + + + gtk-delete + True + False + True + True + + + + + + + + + True + False + _Вид + True + + + + + True + False + _Справка + True + + + True + False + + + gtk-about + True + False + True + True + + + + + + + + + False + True + 0 + + + + + + + + + diff --git a/ui.glade b/ui.glade deleted file mode 100644 index 21ac7c7..0000000 --- a/ui.glade +++ /dev/null @@ -1,626 +0,0 @@ - - - - - - False - True - True - dialog - - - False - vertical - 2 - - - False - end - - - gtk-cancel - True - True - True - True - - - True - True - 0 - - - - - gtk-ok - True - True - True - True - - - True - True - 1 - - - - - False - False - 0 - - - - - True - False - vertical - - - True - False - Select Application - 1 - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - True - True - - - True - False - - - True - True - - - True - True - 0 - - - - - gtk-open - True - True - True - True - - - False - True - 1 - - - - - - - True - False - Custom command - - - - - False - True - 2 - - - - - True - True - 1 - - - - - - - True - False - gtk-edit - - - True - False - gtk-revert-to-saved - - - True - False - - - Edit - True - False - image-gtk-edit - False - - - - - Revert - True - False - image-gtk-revert - False - - - - - False - True - 640 - 400 - dialog - True - - - False - vertical - 2 - - - False - end - - - gtk-add - True - True - True - True - - - True - True - 0 - - - - - gtk-cancel - True - True - True - True - - - True - True - 1 - - - - - gtk-apply - True - True - True - True - - - True - True - 2 - - - - - False - False - 0 - - - - - True - False - vertical - - - True - False - start - Select application for filetype(s): - - - False - True - 0 - - - - - True - True - in - - - True - False - - - True - False - start - start - ... - True - - - - - - - False - True - 1 - - - - - True - True - in - - - True - True - - - - - - - - True - True - 2 - - - - - True - True - 1 - - - - - - - 100 - 1 - 10 - - - False - 640 - 400 - - - True - False - vertical - - - True - False - - - True - False - _Файл - True - - - True - False - - - gtk-new - True - False - True - True - - - - - gtk-open - True - False - True - True - - - - - gtk-save - True - False - True - True - - - - - gtk-save-as - True - False - True - True - - - - - True - False - - - - - gtk-quit - True - False - True - True - - - - - - - - - True - False - _Правка - True - - - True - False - - - gtk-cut - True - False - True - True - - - - - gtk-copy - True - False - True - True - - - - - gtk-paste - True - False - True - True - - - - - gtk-delete - True - False - True - True - - - - - - - - - True - False - _Вид - True - - - - - True - False - _Справка - True - - - True - False - - - gtk-about - True - False - True - True - - - - - - - - - False - True - 0 - - - - - - - - True - False - - - True - True - - - - - - False - True - 0 - - - - - True - False - vertical - - - False - True - 1 - - - - - True - True - mimetypes_view_adj - in - - - True - True - True - mimetypes_view_adj - True - True - horizontal - True - - - multiple - - - - - - - True - True - 2 - - - - - True - True - 2 - - - - - True - False - immediate - - - Show only associated types - True - True - False - False - 0 - True - - - False - True - 0 - - - - - - - - - - - gtk-edit - True - True - True - True - False - - - False - True - end - 3 - - - - - gtk-revert-to-saved - True - True - True - True - False - - - False - True - end - 4 - - - - - False - True - 3 - - - - - - diff --git a/xdg-gui.py b/xdg-gui.py deleted file mode 100644 index e0805f1..0000000 --- a/xdg-gui.py +++ /dev/null @@ -1,588 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Nov 18 06:50:25 2016 - -@author: pavel -""" -import os -import sys - -import threading - - -#import mime_types -#import xdg_operations -import mime_operations - - -APP = 'xdg-gui' - -import locale -from locale import gettext as _ -locale.setlocale(locale.LC_ALL, '') -if os.path.isdir("./locale"): - locale.bindtextdomain(APP, "./locale") - locale.textdomain(APP) - -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk, Gio -from gi.repository import GdkPixbuf - -import signal -signal.signal(signal.SIGINT, signal.SIG_DFL) #handle Ctrl-C - - -GLADE_FILE = "ui.glade" -ICON_SIZE = 16 -SHOW_ONLY_ASSOCIATED = True - -# using indecies shoud speed up filtering -ALL_CATEGORY = "All" -ALL_CATEGORY_ID = 0 - -MIMETYPES_IDS = {"application" : 1, - "audio" : 2, - "image" : 3, - "text" : 4, - "video" : 5, } - -CATEGORY_IDS = { "Application" : 1, - "Audio" : 2, - "Image" : 3, - "Text" : 4, - "Video" : 5, - } - -# name, icon -CATEGORY_ICONS = {"Application" : "application-x-executable", - "Audio" : "audio-x-generic", - "Image" : "image-x-generic", - "Text" : "text-x-generic", - "Video" : "video-x-generic", - "All" : "unknown"} - -def get_category_id(mime_type): - if mime_type is None: return ALL_CATEGORY_ID - - slash_ind = mime_type.find("/") - - if slash_ind <= 0: return ALL_CATEGORY_ID - - top_level = mime_type[:slash_ind] - return MIMETYPES_IDS.get(top_level, ALL_CATEGORY_ID) - - -class ImageTextColumn(Gtk.TreeViewColumn): - def __init__(self, column_name, model_index_img, model_index_txt, *args, **kwargs): - super(ImageTextColumn, self).__init__(column_name, *args, **kwargs) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - renderer_text = Gtk.CellRendererText() - - self.pack_start(renderer_pixbuf, expand = False) - self.add_attribute(renderer_pixbuf, "pixbuf", model_index_img) - - self.pack_start(renderer_text, expand = True) - self.add_attribute(renderer_text, "text", model_index_txt) - -class TextColumn(Gtk.TreeViewColumn): - def __init__(self, column_name, model_index_txt, *args, **kwargs): - super(TextColumn, self).__init__(column_name, *args, **kwargs) - renderer_text = Gtk.CellRendererText() - - self.pack_start(renderer_text, expand = True) - self.add_attribute(renderer_text, "text", model_index_txt) - -class IconTextLabel(Gtk.Box): - def __init__(self, icon_name, text, *args, **kwargs): - super( IconTextLabel, self).__init__(orientation=Gtk.Orientation.HORIZONTAL) - - icon = mime_operations.get_icon_by_name(icon_name, ICON_SIZE) - label = Gtk.Label(text) - - self.pack_start(icon, True, True, 0) - self.pack_start(label, True, True, 0) - - -class CategoriesView: - CAT_ID = 0 - CAT_NAME, CAT_IMG = 1, 2 - - - def __init__(self, builder, on_category_changed): - """ on_category_changed : called every time category is changed, - should accept list of category ids""" - self.categories_view = builder.get_object("categories_view") - self.on_category_changed = on_category_changed - - # id, name, img - self.list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf) - self.categories_view.set_model(self.list_store) - - self._fill_list_store() - - column = ImageTextColumn(_("Categories"), self.CAT_IMG, self.CAT_NAME) - self.categories_view.append_column(column) - - #register callback - tree_selection = self.categories_view.get_selection() - tree_selection.connect("changed", self.on_selection_changes) - - def _fill_list_store(self): - categories = sorted((_(name), name ,id_) - for name, id_ in CATEGORY_IDS.items()) - categories.append((_(ALL_CATEGORY), ALL_CATEGORY, - ALL_CATEGORY_ID)) - - - for transl, name, id_ in categories: - icon_name = CATEGORY_ICONS.get(name) - icon = mime_operations.get_icon_by_name(icon_name, ICON_SIZE) - self.list_store.append([id_, transl, icon]) - - def on_selection_changes(self, tree_selection): - (model, pathlist) = tree_selection.get_selected_rows() - cat_ids = [] - for path in pathlist : - tree_iter = model.get_iter(path) - value = model.get_value(tree_iter, self.CAT_ID) - cat_ids.append(value) - self.on_category_changed(cat_ids) - -class MimeTypesView: - ID = 0 - MTYPE, MTYPE_IMG = 1, 2 - APP,APP_IMG = 3, 4 - - def __init__(self, builder, on_items_edit, on_items_reset): - """ - on_items_edit : function to call when edit action required - on_items_reset : function to call when reset action required - """ - - self.mimetypes_view = builder.get_object("mimetypes_view") - #callbacks - self.mimetypes_view.connect("button-press-event", self.on_mouse_clicked) - self.mimetypes_view.connect("key-press-event", self.on_key_pressed) - - self.on_items_edit = on_items_edit - self.on_items_reset = on_items_reset - - self.current_category_id = ALL_CATEGORY_ID - self.hide_unassociated = False - - self._init_model() - self._add_columns() - - self._init_context_menu(builder) - self._init_buttons(builder) - - self.set_data() - - def _init_model(self): - """liststore -> filter -> sort -> view""" - - # category_id, mimetype, mimetype_img, app, app_img - self.list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf, - str, GdkPixbuf.Pixbuf, ) - - #filter - self.cascade_filter = self.list_store.filter_new() - self.cascade_filter.set_visible_func(self._cascade_filter_func) - - #sorting - self.sorted = Gtk.TreeModelSort(self.cascade_filter) - self.mimetypes_view.set_model(self.sorted) - - #allow multiple selection - self.mimetypes_view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) - - def _add_columns(self): - #add columns - column_type = ImageTextColumn(_("MimeType"), self.MTYPE_IMG, self.MTYPE) - column_type.set_sort_column_id(self.MTYPE) - self.mimetypes_view.append_column(column_type) - - column_program = ImageTextColumn(_("Program"), self.APP_IMG, self.APP) - column_program.set_sort_column_id(self.APP) - self.mimetypes_view.append_column(column_program) - - def _init_context_menu(self, builder): - self.context_menu = builder.get_object("mimeview_context_menu") - revert_item = builder.get_object("menuitem_revert") - edit_item = builder.get_object("menuitem_edit") - - revert_item.connect("activate", lambda *args : self.on_items_reset()) - edit_item.connect("activate", lambda *args : self.on_items_edit()) - - def _init_buttons(self, builder): - reset_button = builder.get_object("reset_button") - edit_button = builder.get_object("edit_button") - - reset_button.connect("clicked", lambda *args : self.on_items_reset()) - edit_button.connect("clicked", lambda *args : self.on_items_edit()) - - - def set_data(self): - for m_type in mime_operations.get_known_mtypes(): - cat_id_ = get_category_id(m_type) - m_type_img = mime_operations.get_mime_icon(m_type, ICON_SIZE) - - app = mime_operations.get_default_app(m_type) - name, icon, *extra = mime_operations.get_app_bio(app, ICON_SIZE) - - self.list_store.append([cat_id_, m_type, m_type_img ,name, icon]) - - def update_data(self, m_types_to_change): - tree_iter = self.list_store.get_iter_first() - - while tree_iter: - row_mtype = self.list_store.get_value(tree_iter, self.MTYPE) - - if row_mtype in m_types_to_change: - app = mime_operations.get_default_app(row_mtype) - name, icon, *extra = mime_operations.get_app_bio(app, ICON_SIZE) - - self.list_store.set_value(tree_iter, self.APP, name) - self.list_store.set_value(tree_iter, self.APP_IMG, icon) - - tree_iter = self.list_store.iter_next(tree_iter) - - - def _cascade_filter_func(self, *args, **kwargs): - return self._category_filter_func(*args, **kwargs) \ - and self._associated_filter_func(*args, **kwargs) - - def _category_filter_func(self, model, iter, data): - if self.current_category_id == ALL_CATEGORY_ID: return True - - return model[iter][self.ID] == self.current_category_id - - def _associated_filter_func(self, model, iter, data): - if not self.hide_unassociated: return True - - return (model[iter][self.APP] is not None) and \ - (len(model[iter][self.APP]) > 0) - - def filter_category(self, category_id = ALL_CATEGORY_ID): - self.current_category_id = category_id - self.cascade_filter.refilter() - - def filter_associated(self, hide_unassociated = False): - self.hide_unassociated = hide_unassociated - self.cascade_filter.refilter() - - - def on_mouse_clicked(self, widget, event): - if event.button == 3: #right button - self.show_context_menu(event) - return True - if event.type == Gdk.EventType._2BUTTON_PRESS: #double click - self.on_items_edit() - return True - - def on_key_pressed(self, widget, event): - #do not reset selection - keyname = Gdk.keyval_name(event.keyval) - print(keyname, "pressed") - - if keyname in {"Return", "Enter", "space"}: - self.on_items_edit() - return True - elif keyname in {"BackSpace", "Delete"}: - self.on_items_reset() - return True - - - def show_context_menu(self, event): - self.context_menu.popup( None, None, None, None, - event.button, event.time) - - def get_selection(self): - """return mime types from selected rows""" - - tree_selection = self.mimetypes_view.get_selection() - (model, pathlist) = tree_selection.get_selected_rows() - - selected_mime_types = [] - for path in pathlist : - tree_iter = model.get_iter(path) - value = model.get_value(tree_iter, self.MTYPE) - - selected_mime_types.append(value) - - return selected_mime_types - -class AddAppDialog: - def __init__(self, builder, parent_window, on_add_dialog_apply): - self.builder = builder - self.on_add_dialog_apply = on_add_dialog_apply - - self.dialog = self.builder.get_object("add_app_dialog") - self.app_chooser = self.builder.get_object("appchooser_widget") - self.app_chooser.set_show_all(True) - self.app_chooser.connect("application-activated", self.on_apply) - self.app_chooser.connect("application-selected", self.on_application_selected) - - self.dialog.set_transient_for(parent_window) - self.dialog.connect("delete-event", self.hide) - - self.custom_entry = self.builder.get_object("custom_entry") - self.custom_entry.connect("changed", self.on_custom_entry_changed) - - self.selected_app = None - self.cline_text = "" - - self._init_buttons() - - def _init_buttons(self): - self.cancel_button = self.builder.get_object("add_app_dialog_cancel_button") - self.apply_button = self.builder.get_object("add_app_dialog_apply_button") - self.file_chooser_button = self.builder.get_object("file_chooser_button") - - self.cancel_button.connect("clicked", self.hide) - self.apply_button.connect("clicked", self.on_apply) - self.file_chooser_button.connect("clicked", self.on_file_chooser) - - def show(self): - self.dialog.run() - - def hide(self, *args): - self.dialog.hide() - - return False #!!! - - def on_custom_entry_changed(self, *args): - text = self.custom_entry.get_text() - if text != self.cline_text: - self.cline_text = text - - name = os.path.basename(self.cline_text) - self.selected_app = mime_operations.get_app_from_cline(self.cline_text, - name, - False) - - def on_application_selected(self, widget, app): - self.selected_app = app - - name, icon, self.cline_text = mime_operations.get_app_bio(app, ICON_SIZE) - self.custom_entry.set_text(self.cline_text) - - def on_file_chooser(self, *args): - f_dialog = Gtk.FileChooserDialog(_('Choose an application'), - action = Gtk.FileChooserAction.OPEN, - buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT)) - f_dialog.set_current_folder('/usr/bin') - f_dialog.set_transient_for(self.dialog) - - if f_dialog.run() == Gtk.ResponseType.ACCEPT: - self.custom_entry.set_text(f_dialog.get_filename()) - - f_dialog.destroy() - - def on_apply(self, *args): - #“application-activated” - self.on_add_dialog_apply(self.selected_app) - self.hide() - - -class MimeSetDialog: - APP_OBJ = 0 - APP_NAME = 1 - APP_ICON = 2 - APP_CL = 3 - - - def __init__(self, builder, parent_window, on_dialog_closed): - self.builder = builder - - self.on_dialog_closed = on_dialog_closed - - self.dialog = self.builder.get_object("mimetype_set_dialog") - self.dialog.set_transient_for(parent_window) - self.dialog.connect("delete-event", self.hide) - - self.mtypes_dialog_label = builder.get_object("mtypes_dialog_label") - - self._init_buttons() - self._init_view() - - - # - self.selection = None - self.m_types = [] - - - - self.add_app_dialog = None - - - def _init_buttons(self): - self.cancel_button = self.builder.get_object("dialog_cancel_button") - self.apply_button = self.builder.get_object("dialog_apply_button") - self.add_button = self.builder.get_object("dialog_add_button") - - self.cancel_button.connect("clicked", self.hide) - self.apply_button.connect("clicked", self.on_row_activated) - self.add_button.connect("clicked", self.on_add_app_clicked) - - def _init_view(self): - self.alt_view = self.builder.get_object("alternatives_treeview") - self.alt_view.connect("row_activated", self.on_row_activated) - - # desktop_file, app_name, icon - self.list_store = Gtk.ListStore(Gio.AppInfo, str ,GdkPixbuf.Pixbuf, str) - - self.alt_view.set_model(self.list_store) - - column_app = ImageTextColumn(_("Categories"), self.APP_ICON, self.APP_NAME) - column_app.set_sort_column_id(self.APP_NAME) - - self.alt_view.append_column(column_app) - - column_cl = TextColumn(_("Command line"), self.APP_CL) - column_cl.set_sort_column_id(self.APP_CL) - - self.alt_view.append_column(column_cl) - - def show(self, m_types): - self.m_types = m_types - self.set_data() - self.indicate_default() - - self.mtypes_dialog_label.set_text(" \n".join(self.m_types)) - - self.selection = None - - self.dialog.run() - - def hide(self, *args): - self.dialog.hide() - self.list_store.clear() - - self.on_dialog_closed(self.selection, self.m_types) - - return True #!!! - - def set_data(self): - #if self.vbox is not None: return - apps = mime_operations.get_apps_for_mtypes(self.m_types) - - for app in apps: - name, icon, cl = mime_operations.get_app_bio(app, ICON_SIZE) - - self.list_store.append([app, name, icon, cl]) - - def indicate_default(self): - if len(self.m_types) == 1: - default_app = mime_operations.get_default_app(self.m_types[0]) - default_cl = default_app.get_commandline() if default_app is not None else "" - - #iteratw - tree_iter = self.list_store.get_iter_first() - while tree_iter: - current_cl = self.list_store.get_value(tree_iter, self.APP_CL) - - if current_cl == default_cl: - self.alt_view.get_selection().select_iter(tree_iter) - return - tree_iter = self.list_store.iter_next(tree_iter) - - - - def on_row_activated(self, *args): - tree_selection = self.alt_view.get_selection() - (model, pathlist) = tree_selection.get_selected_rows() - - for path in pathlist : - tree_iter = model.get_iter(path) - value = model.get_value(tree_iter, self.APP_OBJ) - self.selection = value - - self.hide() - - def on_add_new_app(self, new_app): - if new_app is not None: - name, icon, cl = mime_operations.get_app_bio(new_app, ICON_SIZE) - tree_iter = self.list_store.append([new_app, name, icon, cl]) - self.alt_view.get_selection().select_iter(tree_iter) - - def on_add_app_clicked(self, *args): - if self.add_app_dialog is None: - self.add_app_dialog = AddAppDialog(self.builder, self.dialog,\ - self.on_add_new_app) - - self.add_app_dialog.show() - - -class MainWindow: - def __init__(self): - self.builder = Gtk.Builder() - self.builder.set_translation_domain(APP) - self.builder.add_from_file(GLADE_FILE) - self.builder.connect_signals(self) - - self.window = self.builder.get_object("main_window") - self.window.connect("delete-event", self.on_close) - - - self.cat_view = CategoriesView(self.builder, - self.on_category_changed) - - self.mime_view = MimeTypesView(self.builder, - self.on_mtypes_edit, - self.on_mtypes_reset) - - self.dialog = MimeSetDialog(self.builder, - self.window, - self.on_mtypes_edit_complete) - - # only show mimetypes that have associated application - self.hide_unknown_flag = self.builder.get_object("show_associated_only_button") - self.hide_unknown_flag.connect("toggled", self.on_hide_unknown_flag_toggled) - self.hide_unknown_flag.set_active(SHOW_ONLY_ASSOCIATED) - - - def run(self): - self.window.show() - Gtk.main() - - def on_close(self, *args): - Gtk.main_quit() - - def on_hide_unknown_flag_toggled(self, flag_widget): - hide_unknown = flag_widget.get_active() - self.mime_view.filter_associated(hide_unknown) - - def on_category_changed(self, category_id): - self.mime_view.filter_category(category_id[0]) - - def on_mtypes_edit_complete(self, app, m_types): - if app is not None: - mime_operations.set_app_default(app, m_types) - self.mime_view.update_data(m_types) - - - def on_mtypes_edit(self): - selected_mime_types = self.mime_view.get_selection() - - self.dialog.show(selected_mime_types) - - def on_mtypes_reset(self): - selected_mime_types = self.mime_view.get_selection() - - for m_type in selected_mime_types: - mime_operations.reset_association(m_type) - - self.mime_view.update_data(selected_mime_types) - - - - -if __name__ == "__main__": - window = MainWindow() - window.run() - -- cgit