Restructuring src folder
This commit is contained in:
64
src/solarfm/__builtins__.py
Normal file
64
src/solarfm/__builtins__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Python imports
|
||||
import builtins
|
||||
import threading
|
||||
import sys
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from utils.event_system import EventSystem
|
||||
from utils.keybindings import Keybindings
|
||||
from utils.logger import Logger
|
||||
from utils.settings_manager.manager import SettingsManager
|
||||
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded_wrapper(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded_wrapper(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
def sizeof_fmt_def(num, suffix="B"):
|
||||
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
|
||||
if abs(num) < 1024.0:
|
||||
return f"{num:3.1f} {unit}{suffix}"
|
||||
num /= 1024.0
|
||||
return f"{num:.1f} Yi{suffix}"
|
||||
|
||||
|
||||
|
||||
# NOTE: Just reminding myself we can add to builtins two different ways...
|
||||
# __builtins__.update({"event_system": Builtins()})
|
||||
builtins.app_name = "SolarFM"
|
||||
builtins.keybindings = Keybindings()
|
||||
builtins.event_system = EventSystem()
|
||||
builtins.settings_manager = SettingsManager()
|
||||
|
||||
settings_manager.load_settings()
|
||||
|
||||
builtins.settings = settings_manager.settings
|
||||
builtins.logger = Logger(settings_manager.get_home_config_path(), \
|
||||
_ch_log_lvl=settings.debugging.ch_log_lvl, \
|
||||
_fh_log_lvl=settings.debugging.fh_log_lvl).get_logger()
|
||||
|
||||
builtins.threaded = threaded_wrapper
|
||||
builtins.daemon_threaded = daemon_threaded_wrapper
|
||||
builtins.sizeof_fmt = sizeof_fmt_def
|
||||
|
||||
|
||||
|
||||
def custom_except_hook(exc_type, exc_value, exc_traceback):
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
return
|
||||
|
||||
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
|
||||
|
||||
sys.excepthook = custom_except_hook
|
3
src/solarfm/__init__.py
Normal file
3
src/solarfm/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Base module
|
||||
"""
|
56
src/solarfm/__main__.py
Normal file
56
src/solarfm/__main__.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Python imports
|
||||
import argparse
|
||||
import faulthandler
|
||||
import locale
|
||||
import traceback
|
||||
from setproctitle import setproctitle
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from __builtins__ import *
|
||||
from app import Application
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
try:
|
||||
locale.setlocale(locale.LC_NUMERIC, 'C')
|
||||
|
||||
setproctitle(f"{app_name}")
|
||||
faulthandler.enable() # For better debug info
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
# Add long and short arguments
|
||||
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.")
|
||||
parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.")
|
||||
parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.")
|
||||
|
||||
parser.add_argument("--new-tab", "-t", default="", help="Open a file into new tab.")
|
||||
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
|
||||
|
||||
# Read arguments (If any...)
|
||||
args, unknownargs = parser.parse_known_args()
|
||||
|
||||
if args.debug == "true":
|
||||
settings_manager.set_debug(True)
|
||||
|
||||
if args.trace_debug == "true":
|
||||
settings_manager.set_trace_debug(True)
|
||||
|
||||
settings_manager.do_dirty_start_check()
|
||||
Application(args, unknownargs)
|
||||
Gtk.main()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
""" Set process title, get arguments, and create GTK main thread. """
|
||||
run()
|
59
src/solarfm/app.py
Normal file
59
src/solarfm/app.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Python imports
|
||||
import signal
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from utils.debugging import debug_signal_handler
|
||||
from utils.ipc_server import IPCServer
|
||||
from core.window import Window
|
||||
|
||||
|
||||
|
||||
class AppLaunchException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class Application(IPCServer):
|
||||
""" docstring for Application. """
|
||||
|
||||
def __init__(self, args, unknownargs):
|
||||
super(Application, self).__init__()
|
||||
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.socket_realization_check()
|
||||
|
||||
if not self.is_ipc_alive:
|
||||
for arg in unknownargs + [args.new_tab,]:
|
||||
if os.path.isdir(arg):
|
||||
message = f"FILE|{arg}"
|
||||
self.send_ipc_message(message)
|
||||
|
||||
raise AppLaunchException(f"{app_name} IPC Server Exists: Will send path(s) to it and close...")
|
||||
|
||||
self.setup_debug_hook()
|
||||
Window(args, unknownargs)
|
||||
|
||||
|
||||
def socket_realization_check(self):
|
||||
try:
|
||||
self.create_ipc_listener()
|
||||
except Exception:
|
||||
self.send_test_ipc_message()
|
||||
|
||||
try:
|
||||
self.create_ipc_listener()
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
def setup_debug_hook(self):
|
||||
try:
|
||||
# kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows
|
||||
signal.signal(
|
||||
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"),
|
||||
debug_signal_handler
|
||||
)
|
||||
except ValueError:
|
||||
# Typically: ValueError: signal only works in main thread
|
||||
...
|
3
src/solarfm/core/__init__.py
Normal file
3
src/solarfm/core/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Core Module
|
||||
"""
|
195
src/solarfm/core/controller.py
Normal file
195
src/solarfm/core/controller.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# Python imports
|
||||
import os
|
||||
import time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from .controller_data import Controller_Data
|
||||
from .fs_actions.file_system_actions import FileSystemActions
|
||||
from .mixins.signals_mixins import SignalsMixins
|
||||
|
||||
from .widgets.dialogs.about_widget import AboutWidget
|
||||
from .widgets.dialogs.appchooser_widget import AppchooserWidget
|
||||
from .widgets.dialogs.file_exists_widget import FileExistsWidget
|
||||
from .widgets.dialogs.new_file_widget import NewFileWidget
|
||||
from .widgets.dialogs.rename_widget import RenameWidget
|
||||
from .widgets.dialogs.save_load_widget import SaveLoadWidget
|
||||
|
||||
from .widgets.popups.message_popup_widget import MessagePopupWidget
|
||||
from .widgets.popups.path_menu_popup_widget import PathMenuPopupWidget
|
||||
from .widgets.popups.plugins_popup_widget import PluginsPopupWidget
|
||||
from .widgets.popups.io_popup_widget import IOPopupWidget
|
||||
|
||||
from .widgets.context_menu_widget import ContextMenuWidget
|
||||
from .widgets.bottom_status_info_widget import BottomStatusInfoWidget
|
||||
|
||||
from .ui_mixin import UIMixin
|
||||
|
||||
|
||||
|
||||
|
||||
class Controller(UIMixin, SignalsMixins, Controller_Data):
|
||||
""" Controller coordinates the mixins and is somewhat the root hub of it all. """
|
||||
|
||||
def __init__(self, args, unknownargs):
|
||||
self._setup_controller_data()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
self._generate_file_views(self.fm_controller_data)
|
||||
|
||||
if args.no_plugins == "false":
|
||||
self.plugins.launch_plugins()
|
||||
|
||||
for arg in unknownargs + [args.new_tab,]:
|
||||
if os.path.isdir(arg):
|
||||
message = f"FILE|{arg}"
|
||||
event_system.emit("post_file_to_ipc", message)
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
self.window.connect("focus-out-event", self.unset_keys_and_data)
|
||||
self.window.connect("key-press-event", self.on_global_key_press_controller)
|
||||
self.window.connect("key-release-event", self.on_global_key_release_controller)
|
||||
FileSystemActions()
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("shutting_down", self._shutting_down)
|
||||
event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc)
|
||||
event_system.subscribe("generate_file_views", self._generate_file_views)
|
||||
event_system.subscribe("clear_notebooks", self.clear_notebooks)
|
||||
|
||||
event_system.subscribe("set_window_title", self._set_window_title)
|
||||
event_system.subscribe("unset_selected_files_views", self._unset_selected_files_views)
|
||||
event_system.subscribe("get_current_state", self.get_current_state)
|
||||
event_system.subscribe("go_to_path", self.go_to_path)
|
||||
event_system.subscribe("format_to_uris", self.format_to_uris)
|
||||
event_system.subscribe("do_action_from_menu_controls", self.do_action_from_menu_controls)
|
||||
event_system.subscribe("set_clipboard_data", self.set_clipboard_data)
|
||||
|
||||
def _load_glade_file(self):
|
||||
self.builder.add_from_file( settings_manager.get_glade_file() )
|
||||
self.builder.expose_object("main_window", self.window)
|
||||
|
||||
self.core_widget = self.builder.get_object("core_widget")
|
||||
|
||||
settings_manager.set_builder(self.builder)
|
||||
settings_manager.register_signals_to_builder([self,], self.builder)
|
||||
|
||||
def get_core_widget(self):
|
||||
return self.core_widget
|
||||
|
||||
|
||||
# NOTE: Really we will move these to the UI/(New) Window 'base' controller
|
||||
# after we're done cleaning and refactoring to use fewer mixins.
|
||||
def _load_widgets(self):
|
||||
BottomStatusInfoWidget()
|
||||
|
||||
IOPopupWidget()
|
||||
MessagePopupWidget()
|
||||
PathMenuPopupWidget()
|
||||
PluginsPopupWidget()
|
||||
|
||||
AboutWidget()
|
||||
AppchooserWidget()
|
||||
ContextMenuWidget()
|
||||
NewFileWidget()
|
||||
RenameWidget()
|
||||
FileExistsWidget()
|
||||
SaveLoadWidget()
|
||||
|
||||
def _shutting_down(self):
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def reload_plugins(self, widget=None, eve=None):
|
||||
self.plugins.reload_plugins()
|
||||
|
||||
|
||||
def do_action_from_menu_controls(self, _action=None, eve=None):
|
||||
if not _action: return
|
||||
|
||||
if not isinstance(_action, str):
|
||||
action = _action.get_name()
|
||||
else:
|
||||
action = _action
|
||||
|
||||
event_system.emit("hide_context_menu")
|
||||
event_system.emit("hide_new_file_menu")
|
||||
event_system.emit("hide_rename_file_menu")
|
||||
|
||||
if action == "open":
|
||||
event_system.emit("open_files")
|
||||
if action == "open_with":
|
||||
event_system.emit("show_appchooser_menu")
|
||||
if action == "execute":
|
||||
event_system.emit("execute_files")
|
||||
if action == "execute_in_terminal":
|
||||
event_system.emit("execute_files", (True,))
|
||||
if action == "rename":
|
||||
event_system.emit("rename_files")
|
||||
if action == "cut":
|
||||
event_system.emit("cut_files")
|
||||
if action == "copy":
|
||||
event_system.emit("copy_files")
|
||||
if action == "copy_path":
|
||||
event_system.emit("copy_path")
|
||||
if action == "copy_name":
|
||||
event_system.emit("copy_name")
|
||||
if action == "copy_path_name":
|
||||
event_system.emit("copy_path_name")
|
||||
if action == "paste":
|
||||
event_system.emit("paste_files")
|
||||
if action == "create":
|
||||
event_system.emit("create_files")
|
||||
if action in ["save_session", "save_session_as", "load_session"]:
|
||||
event_system.emit("save_load_session", (action))
|
||||
|
||||
if action == "about_page":
|
||||
event_system.emit("show_about_page")
|
||||
if action == "io_popup":
|
||||
event_system.emit("show_io_popup")
|
||||
if action == "plugins_popup":
|
||||
event_system.emit("show_plugins_popup")
|
||||
if action == "messages_popup":
|
||||
event_system.emit("show_messages_popup")
|
||||
if action == "ui_debug":
|
||||
event_system.emit("load_interactive_debug")
|
||||
if action == "tear_down":
|
||||
event_system.emit("tear_down")
|
||||
|
||||
|
||||
def go_home(self, widget=None, eve=None):
|
||||
self.builder.get_object("go_home").released()
|
||||
|
||||
def refresh_tab(self, widget=None, eve=None):
|
||||
self.builder.get_object("refresh_tab").released()
|
||||
|
||||
def go_up(self, widget=None, eve=None):
|
||||
self.builder.get_object("go_up").released()
|
||||
|
||||
def grab_focus_path_entry(self, widget=None, eve=None):
|
||||
self.builder.get_object("path_entry").grab_focus()
|
||||
|
||||
def tggl_top_main_menubar(self, widget=None, eve=None):
|
||||
top_main_menubar = self.builder.get_object("top_main_menubar")
|
||||
top_main_menubar.hide() if top_main_menubar.is_visible() else top_main_menubar.show()
|
||||
|
||||
def open_terminal(self, widget=None, eve=None):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
tab.execute([f"{tab.terminal_app}"], start_dir=tab.get_current_directory())
|
||||
|
||||
def go_to_path(self, path: str):
|
||||
self.builder.get_object("path_entry").set_text(path)
|
189
src/solarfm/core/controller_data.py
Normal file
189
src/solarfm/core/controller_data.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# Python imports
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from .sfm_builder import SFMBuilder
|
||||
from .widgets.dialogs.message_widget import MessageWidget
|
||||
from .widgets.dialogs.user_pass_widget import UserPassWidget
|
||||
|
||||
from utils.types.state import State
|
||||
from plugins.plugins_controller import PluginsController
|
||||
from shellfm.windows.controller import WindowController
|
||||
|
||||
|
||||
|
||||
class Controller_Data:
|
||||
""" Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """
|
||||
__slots__ = "settings", "builder", "logger", "keybindings", "trashman", "fm_controller", "window", "window1", "window2", "window3", "window4"
|
||||
|
||||
def _setup_controller_data(self) -> None:
|
||||
self.window = settings_manager.get_main_window()
|
||||
self.builder = SFMBuilder()
|
||||
self.core_widget = None
|
||||
|
||||
self._load_glade_file()
|
||||
self.fm_controller = WindowController()
|
||||
self.plugins = PluginsController()
|
||||
self.fm_controller_data = self.fm_controller.get_state_from_file()
|
||||
|
||||
self.window1 = self.builder.get_object("window_1")
|
||||
self.window2 = self.builder.get_object("window_2")
|
||||
self.window3 = self.builder.get_object("window_3")
|
||||
self.window4 = self.builder.get_object("window_4")
|
||||
|
||||
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
|
||||
self.selected_files = []
|
||||
self.to_copy_files = []
|
||||
self.to_cut_files = []
|
||||
self.soft_update_lock = {}
|
||||
self.dnd_left_primed = 0
|
||||
|
||||
self.single_click_open = False
|
||||
self.is_pane1_hidden = False
|
||||
self.is_pane2_hidden = False
|
||||
self.is_pane3_hidden = False
|
||||
self.is_pane4_hidden = False
|
||||
|
||||
self.override_drop_dest = None
|
||||
self.ctrl_down = False
|
||||
self.shift_down = False
|
||||
self.alt_down = False
|
||||
|
||||
self._state = State()
|
||||
self.message_dialog = MessageWidget()
|
||||
self.user_pass_dialog = UserPassWidget()
|
||||
|
||||
|
||||
def get_current_state(self) -> State:
|
||||
'''
|
||||
Returns the state info most useful for any given context and action intent.
|
||||
|
||||
Parameters:
|
||||
a (obj): self
|
||||
|
||||
Returns:
|
||||
state (obj): State
|
||||
'''
|
||||
# state = State()
|
||||
state = self._state
|
||||
state.fm_controller = self.fm_controller
|
||||
state.notebooks = self.notebooks
|
||||
state.wid, state.tid = self.fm_controller.get_active_wid_and_tid()
|
||||
state.tab = self.get_fm_window(state.wid).get_tab_by_id(state.tid)
|
||||
state.icon_grid = self.builder.get_object(f"{state.wid}|{state.tid}|icon_grid", use_gtk = False)
|
||||
# state.icon_grid = event_system.emit_and_await("get_files_view_icon_grid", (state.wid, state.tid))
|
||||
state.store = state.icon_grid.get_model()
|
||||
|
||||
# NOTE: Need to watch this as I thought we had issues with just using single reference upon closing it.
|
||||
# But, I found that not doing it this way caused objects to generate upon every click... (Because we're getting state info, duh)
|
||||
# Yet interactive debug view shows them just pilling on and never clearing...
|
||||
state.message_dialog = self.message_dialog
|
||||
state.user_pass_dialog = self.user_pass_dialog
|
||||
# state.message_dialog = MessageWidget()
|
||||
# state.user_pass_dialog = UserPassWidget()
|
||||
|
||||
selected_files = state.icon_grid.get_selected_items()
|
||||
if selected_files:
|
||||
state.uris = self.format_to_uris(state.store, state.wid, state.tid, selected_files, True)
|
||||
state.uris_raw = self.format_to_uris(state.store, state.wid, state.tid, selected_files)
|
||||
|
||||
state.selected_files = event_system.emit_and_await("get_selected_files")
|
||||
|
||||
# if self.to_copy_files:
|
||||
# state.to_copy_files = self.format_to_uris(state.store, state.wid, state.tid, self.to_copy_files, True)
|
||||
#
|
||||
# if self.to_cut_files:
|
||||
# state.to_cut_files = self.format_to_uris(state.store, state.wid, state.tid, self.to_cut_files, True)
|
||||
|
||||
event_system.emit("update_state_info_plugins", state) # NOTE: Need to remove after we convert plugins to use emit_and_await
|
||||
return state
|
||||
|
||||
def format_to_uris(self, store, wid, tid, treePaths, use_just_path = False):
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
dir = tab.get_current_directory()
|
||||
uris = []
|
||||
|
||||
for path in treePaths:
|
||||
itr = store.get_iter(path)
|
||||
file = store.get(itr, 1)[0]
|
||||
fpath = ""
|
||||
|
||||
if not use_just_path:
|
||||
fpath = f"file://{dir}/{file}"
|
||||
else:
|
||||
fpath = f"{dir}/{file}"
|
||||
|
||||
uris.append(fpath)
|
||||
|
||||
return uris
|
||||
|
||||
|
||||
def get_fm_window(self, wid):
|
||||
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
|
||||
|
||||
def _unset_selected_files_views(self):
|
||||
for _notebook in self.notebooks:
|
||||
ctx = _notebook.get_style_context()
|
||||
ctx.remove_class("notebook-selected-focus")
|
||||
ctx.add_class("notebook-unselected-focus")
|
||||
|
||||
def _set_window_title(self, dir):
|
||||
self.window.set_title(f"{app_name} ~ {dir}")
|
||||
|
||||
|
||||
def clear_console(self) -> None:
|
||||
''' Clears the terminal screen. '''
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def call_method(self, _method_name: str, data: type = None) -> type:
|
||||
'''
|
||||
Calls a method from scope of class.
|
||||
|
||||
Parameters:
|
||||
a (obj): self
|
||||
b (str): method name to be called
|
||||
c (*): Data (if any) to be passed to the method.
|
||||
Note: It must be structured according to the given methods requirements.
|
||||
|
||||
Returns:
|
||||
Return data is that which the calling method gives.
|
||||
'''
|
||||
method_name = str(_method_name)
|
||||
method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}")
|
||||
return method(data) if data else method()
|
||||
|
||||
def has_method(self, obj, name) -> type:
|
||||
''' Checks if a given method exists. '''
|
||||
return callable(getattr(obj, name, None))
|
||||
|
||||
|
||||
def clear_notebooks(self) -> None:
|
||||
self.ctrl_down = False
|
||||
self.shift_down = False
|
||||
self.alt_down = False
|
||||
|
||||
for notebook in self.notebooks:
|
||||
self.clear_children(notebook)
|
||||
|
||||
def clear_children(self, widget: type) -> None:
|
||||
''' Clear children of a gtk widget. '''
|
||||
for child in widget.get_children():
|
||||
widget.remove(child)
|
||||
|
||||
def get_clipboard_data(self) -> str:
|
||||
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
|
||||
retcode = proc.wait()
|
||||
data = proc.stdout.read()
|
||||
return data.decode("utf-8").strip()
|
||||
|
||||
def set_clipboard_data(self, data: type) -> None:
|
||||
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
|
||||
proc.stdin.write(data.encode("utf-8"))
|
||||
proc.stdin.close()
|
||||
retcode = proc.wait()
|
3
src/solarfm/core/fs_actions/__init__.py
Normal file
3
src/solarfm/core/fs_actions/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
FS Actions Module
|
||||
"""
|
70
src/solarfm/core/fs_actions/crud_mixin.py
Normal file
70
src/solarfm/core/fs_actions/crud_mixin.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class CRUDMixin:
|
||||
"""docstring for CRUDMixin"""
|
||||
|
||||
def move_files(self, files, target):
|
||||
self.handle_files(files, "move", target)
|
||||
|
||||
def paste_files(self):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
target = f"{state.tab.get_current_directory()}"
|
||||
|
||||
if self._to_copy_files:
|
||||
self.handle_files(self._to_copy_files, "copy", target)
|
||||
elif self._to_cut_files:
|
||||
self.handle_files(self._to_cut_files, "move", target)
|
||||
|
||||
def create_files(self):
|
||||
fname_field = self._builder.get_object("new_fname_field")
|
||||
cancel_creation = event_system.emit_and_await("show_new_file_menu", fname_field)
|
||||
|
||||
if cancel_creation:
|
||||
event_system.emit("hide_new_file_menu")
|
||||
return
|
||||
|
||||
file_name = fname_field.get_text().strip()
|
||||
type = self._builder.get_object("new_file_toggle_type").get_state()
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
target = f"{state.tab.get_current_directory()}"
|
||||
|
||||
if file_name:
|
||||
path = f"{target}/{file_name}"
|
||||
|
||||
if type == True: # Create File
|
||||
self.handle_files([path], "create_file")
|
||||
else: # Create Folder
|
||||
self.handle_files([path], "create_dir")
|
||||
|
||||
event_system.emit("hide_new_file_menu")
|
||||
|
||||
def rename_files(self):
|
||||
rename_label = self._builder.get_object("file_to_rename_label")
|
||||
rename_input = self._builder.get_object("rename_fname")
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
|
||||
for uri in state.uris:
|
||||
entry = uri.split("/")[-1]
|
||||
rename_label.set_label(entry)
|
||||
rename_input.set_text(entry)
|
||||
|
||||
response = event_system.emit_and_await("show_rename_file_menu", rename_input)
|
||||
if response == "skip_edit":
|
||||
continue
|
||||
if response == "cancel_edit":
|
||||
break
|
||||
|
||||
rname_to = rename_input.get_text().strip()
|
||||
if rname_to:
|
||||
target = f"{state.tab.get_current_directory()}/{rname_to}"
|
||||
self.handle_files([uri], "rename", target)
|
||||
|
||||
event_system.emit("hide_rename_file_menu")
|
||||
event_system.emit_and_await("get_selected_files").clear()
|
114
src/solarfm/core/fs_actions/file_system_actions.py
Normal file
114
src/solarfm/core/fs_actions/file_system_actions.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Python imports
|
||||
import shlex
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .crud_mixin import CRUDMixin
|
||||
from .handler_mixin import HandlerMixin
|
||||
|
||||
|
||||
|
||||
|
||||
class FileSystemActions(HandlerMixin, CRUDMixin):
|
||||
"""docstring for FileSystemActions"""
|
||||
|
||||
def __init__(self):
|
||||
super(FileSystemActions, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
self._selected_files = []
|
||||
self._to_copy_files = []
|
||||
self._to_cut_files = []
|
||||
|
||||
self._builder = settings_manager.get_builder()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("set_selected_files", self.set_selected_files)
|
||||
event_system.subscribe("get_selected_files", self.get_selected_files)
|
||||
|
||||
event_system.subscribe("open_files", self.open_files)
|
||||
event_system.subscribe("open_with_files", self.open_with_files)
|
||||
event_system.subscribe("execute_files", self.execute_files)
|
||||
|
||||
event_system.subscribe("cut_files", self.cut_files)
|
||||
event_system.subscribe("copy_files", self.copy_files)
|
||||
event_system.subscribe("paste_files", self.paste_files)
|
||||
event_system.subscribe("move_files", self.move_files)
|
||||
event_system.subscribe("copy_name", self.copy_name)
|
||||
event_system.subscribe("copy_path", self.copy_path)
|
||||
event_system.subscribe("copy_path_name", self.copy_path_name)
|
||||
event_system.subscribe("create_files", self.create_files)
|
||||
event_system.subscribe("rename_files", self.rename_files)
|
||||
|
||||
def _load_widgets(self):
|
||||
...
|
||||
|
||||
|
||||
def set_selected_files(self, selected_files: []):
|
||||
self._selected_files = selected_files
|
||||
|
||||
def get_selected_files(self):
|
||||
return self._selected_files
|
||||
|
||||
|
||||
def cut_files(self):
|
||||
self._to_copy_files.clear()
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
self._to_cut_files = state.uris
|
||||
|
||||
def copy_files(self):
|
||||
self._to_cut_files.clear()
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
self._to_copy_files = state.uris
|
||||
|
||||
def copy_path(self):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
path = state.tab.get_current_directory()
|
||||
event_system.emit("set_clipboard_data", (path,))
|
||||
|
||||
def copy_name(self):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
if len(state.uris) == 1:
|
||||
file_name = state.uris[0].split("/")[-1]
|
||||
event_system.emit("set_clipboard_data", (file_name,))
|
||||
|
||||
def copy_path_name(self):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
if len(state.uris) == 1:
|
||||
file = state.uris[0].replace("file://", "")
|
||||
event_system.emit("set_clipboard_data", (file,))
|
||||
|
||||
def open_files(self):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
for file in state.uris:
|
||||
state.tab.open_file_locally(file)
|
||||
|
||||
def open_with_files(self, app_info):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
uris = state.uris_raw
|
||||
|
||||
if not state.uris_raw:
|
||||
uris = [f"file://{state.tab.get_current_directory()}"]
|
||||
|
||||
state.tab.app_chooser_exec(app_info, uris)
|
||||
|
||||
|
||||
def execute_files(self, in_terminal=False):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
current_dir = state.tab.get_current_directory()
|
||||
command = None
|
||||
for path in state.uris:
|
||||
command = f"{shlex.quote(path)}" if not in_terminal else f"{state.tab.terminal_app} -e {shlex.quote(path)}"
|
||||
state.tab.execute(shlex.split(command), start_dir=state.tab.get_current_directory())
|
165
src/solarfm/core/fs_actions/handler_mixin.py
Normal file
165
src/solarfm/core/fs_actions/handler_mixin.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
from ..widgets.io_widget import IOWidget
|
||||
|
||||
|
||||
class HandlerMixinException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class HandlerMixin:
|
||||
"""docstring for HandlerMixin"""
|
||||
|
||||
# NOTE: Gtk recommends using fail flow than pre check which is more
|
||||
# race condition proof. They're right; but, they can't even delete
|
||||
# directories properly. So... f**k them. I'll do it my way.
|
||||
def handle_files(self, paths, action, _target_path=None):
|
||||
target = None
|
||||
_file = None
|
||||
response = None
|
||||
overwrite_all = False
|
||||
rename_auto_all = False
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
if "file://" in path:
|
||||
path = path.split("file://")[1]
|
||||
|
||||
file = Gio.File.new_for_path(path)
|
||||
if _target_path:
|
||||
if file.get_parent().get_path() == _target_path:
|
||||
raise HandlerMixinException("Parent dir of target and file locations are the same! Won't copy or move!")
|
||||
|
||||
if os.path.isdir(_target_path):
|
||||
info = file.query_info("standard::display-name", 0, cancellable=None)
|
||||
_target = f"{_target_path}/{info.get_display_name()}"
|
||||
_file = Gio.File.new_for_path(_target)
|
||||
else:
|
||||
_file = Gio.File.new_for_path(_target_path)
|
||||
else:
|
||||
_file = Gio.File.new_for_path(path)
|
||||
|
||||
|
||||
if _file.query_exists():
|
||||
if not overwrite_all and not rename_auto_all:
|
||||
event_system.emit("setup_exists_data", (file, _file))
|
||||
response = event_system.emit_and_await("show_exists_page")
|
||||
|
||||
if response == "overwrite_all":
|
||||
overwrite_all = True
|
||||
if response == "rename_auto_all":
|
||||
rename_auto_all = True
|
||||
|
||||
if response == "rename":
|
||||
base_path = _file.get_parent().get_path()
|
||||
new_name = self._builder.get_object("exists_file_field").get_text().strip()
|
||||
rfPath = f"{base_path}/{new_name}"
|
||||
_file = Gio.File.new_for_path(rfPath)
|
||||
|
||||
if response == "rename_auto" or rename_auto_all:
|
||||
_file = self.rename_proc(_file)
|
||||
|
||||
if response == "overwrite" or overwrite_all:
|
||||
type = _file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
|
||||
|
||||
if type == Gio.FileType.DIRECTORY:
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
state.tab.delete_file( _file.get_path() )
|
||||
else:
|
||||
_file.delete(cancellable=None)
|
||||
|
||||
if response == "skip":
|
||||
continue
|
||||
if response == "skip_all":
|
||||
break
|
||||
|
||||
if _target_path:
|
||||
target = _file
|
||||
else:
|
||||
file = _file
|
||||
|
||||
|
||||
if action == "create_file":
|
||||
file.create(flags=Gio.FileCreateFlags.NONE, cancellable=None)
|
||||
continue
|
||||
if action == "create_dir":
|
||||
file.make_directory(cancellable=None)
|
||||
continue
|
||||
|
||||
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
|
||||
if type == Gio.FileType.DIRECTORY:
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
tab = state.tab
|
||||
fPath = file.get_path()
|
||||
tPath = target.get_path()
|
||||
state = True
|
||||
|
||||
if action == "copy":
|
||||
tab.copy_file(fPath, tPath)
|
||||
if action == "move" or action == "rename":
|
||||
tab.move_file(fPath, tPath)
|
||||
else:
|
||||
io_widget = IOWidget(action, file)
|
||||
|
||||
if action == "copy":
|
||||
file.copy_async(destination=target,
|
||||
flags=Gio.FileCopyFlags.BACKUP,
|
||||
io_priority=98,
|
||||
cancellable=io_widget.cancle_eve,
|
||||
progress_callback=io_widget.update_progress,
|
||||
callback=io_widget.finish_callback)
|
||||
|
||||
self._builder.get_object("io_list").add(io_widget)
|
||||
if action == "move" or action == "rename":
|
||||
file.move_async(destination=target,
|
||||
flags=Gio.FileCopyFlags.BACKUP,
|
||||
io_priority=98,
|
||||
cancellable=io_widget.cancle_eve,
|
||||
progress_callback=None,
|
||||
# NOTE: progress_callback here causes seg fault when set
|
||||
callback=io_widget.finish_callback)
|
||||
|
||||
self._builder.get_object("io_list").add(io_widget)
|
||||
|
||||
except GObject.GError as e:
|
||||
raise OSError(e)
|
||||
|
||||
self._builder.get_object("exists_file_rename_bttn").set_sensitive(False)
|
||||
|
||||
def rename_proc(self, gio_file):
|
||||
full_path = gio_file.get_path()
|
||||
base_path = gio_file.get_parent().get_path()
|
||||
file_name = os.path.splitext(gio_file.get_basename())[0]
|
||||
extension = os.path.splitext(full_path)[-1]
|
||||
target = Gio.File.new_for_path(full_path)
|
||||
start = "-copy"
|
||||
|
||||
if settings_manager.is_debug():
|
||||
logger.debug(f"Path: {full_path}")
|
||||
logger.debug(f"Base Path: {base_path}")
|
||||
logger.debug(f'Name: {file_name}')
|
||||
logger.debug(f"Extension: {extension}")
|
||||
|
||||
i = 2
|
||||
while target.query_exists():
|
||||
try:
|
||||
value = file_name[(file_name.find(start)+len(start)):]
|
||||
int(value)
|
||||
file_name = file_name.split(start)[0]
|
||||
except HandlerMixinException as e:
|
||||
pass
|
||||
|
||||
target = Gio.File.new_for_path(f"{base_path}/{file_name}-copy{i}{extension}")
|
||||
i += 1
|
||||
|
||||
return target
|
3
src/solarfm/core/mixins/__init__.py
Normal file
3
src/solarfm/core/mixins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Mixins module
|
||||
"""
|
3
src/solarfm/core/mixins/signals/__init__.py
Normal file
3
src/solarfm/core/mixins/signals/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Signals module
|
||||
"""
|
105
src/solarfm/core/mixins/signals/file_action_signals_mixin.py
Normal file
105
src/solarfm/core/mixins/signals/file_action_signals_mixin.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Python imports
|
||||
import time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class FileActionSignalsMixin:
|
||||
"""docstring for FileActionSignalsMixin"""
|
||||
|
||||
def set_file_watcher(self, tab):
|
||||
if tab.get_dir_watcher():
|
||||
watcher = tab.get_dir_watcher()
|
||||
watcher.cancel()
|
||||
if settings_manager.is_debug():
|
||||
logger.debug(f"Watcher Is Cancelled: {watcher.is_cancelled()}")
|
||||
|
||||
cur_dir = tab.get_current_directory()
|
||||
|
||||
dir_watcher = Gio.File.new_for_path(cur_dir) \
|
||||
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
|
||||
|
||||
wid = tab.get_wid()
|
||||
tid = tab.get_id()
|
||||
dir_watcher.connect("changed", self.dir_watch_updates, (f"{wid}|{tid}",))
|
||||
tab.set_dir_watcher(dir_watcher)
|
||||
|
||||
# NOTE: Too lazy to impliment a proper update handler and so just regen store and update tab.
|
||||
# Use a lock system to prevent too many update calls for certain instances but user can manually refresh if they have urgency
|
||||
def dir_watch_updates(self, file_monitor, file, other_file = None, eve_type = None, data = None):
|
||||
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
|
||||
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
|
||||
Gio.FileMonitorEvent.MOVED_OUT]:
|
||||
logger.debug(eve_type)
|
||||
|
||||
if eve_type in [Gio.FileMonitorEvent.MOVED_IN, Gio.FileMonitorEvent.MOVED_OUT]:
|
||||
self.update_on_soft_lock_end(data[0])
|
||||
elif data[0] in self.soft_update_lock.keys():
|
||||
self.soft_update_lock[data[0]]["last_update_time"] = time.time()
|
||||
else:
|
||||
self.soft_lock_countdown(data[0])
|
||||
|
||||
@threaded
|
||||
def soft_lock_countdown(self, tab_widget):
|
||||
self.soft_update_lock[tab_widget] = { "last_update_time": time.time()}
|
||||
|
||||
lock = True
|
||||
while lock:
|
||||
time.sleep(0.6)
|
||||
last_update_time = self.soft_update_lock[tab_widget]["last_update_time"]
|
||||
current_time = time.time()
|
||||
if (current_time - last_update_time) > 0.6:
|
||||
lock = False
|
||||
|
||||
self.soft_update_lock.pop(tab_widget, None)
|
||||
GLib.idle_add(self.update_on_soft_lock_end, *(tab_widget,))
|
||||
|
||||
|
||||
def update_on_soft_lock_end(self, tab_widget):
|
||||
wid, tid = tab_widget.split("|")
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid", use_gtk = False)
|
||||
store = icon_grid.get_model()
|
||||
_store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
|
||||
|
||||
tab.load_directory()
|
||||
icon_grid.clear_and_set_new_store()
|
||||
self.load_store(tab, icon_grid.get_store())
|
||||
|
||||
tab_widget_label.set_label(tab.get_end_of_path())
|
||||
state = self.get_current_state()
|
||||
if [wid, tid] in [state.wid, state.tid]:
|
||||
self.set_bottom_labels(tab)
|
||||
|
||||
|
||||
def do_file_search(self, widget, eve = None):
|
||||
if not self.ctrl_down and not self.shift_down and not self.alt_down:
|
||||
target = widget.get_name()
|
||||
notebook = self.builder.get_object(target)
|
||||
page = notebook.get_current_page()
|
||||
nth_page = notebook.get_nth_page(page)
|
||||
icon_grid = nth_page.get_children()[0]
|
||||
|
||||
wid, tid = icon_grid.get_name().split("|")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
query = widget.get_text().lower()
|
||||
|
||||
icon_grid.unselect_all()
|
||||
for i, file in enumerate(tab.get_files()):
|
||||
if query and query in file[0].lower():
|
||||
path = Gtk.TreePath().new_from_indices([i])
|
||||
icon_grid.select_path(path)
|
||||
|
||||
items = icon_grid.get_selected_items()
|
||||
if len(items) > 0:
|
||||
icon_grid.scroll_to_path(items[0], False, 0.5, 0.5)
|
35
src/solarfm/core/mixins/signals/ipc_signals_mixin.py
Normal file
35
src/solarfm/core/mixins/signals/ipc_signals_mixin.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class IPCSignalsMixin:
|
||||
""" IPCSignalsMixin handle messages from another starting solarfm process. """
|
||||
|
||||
def print_to_console(self, message=None):
|
||||
print(message)
|
||||
|
||||
def handle_file_from_ipc(self, path):
|
||||
window = self.builder.get_object("main_window")
|
||||
window.deiconify()
|
||||
window.show()
|
||||
window.present()
|
||||
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
if notebook.is_visible():
|
||||
self.create_tab(wid, None, path)
|
||||
return
|
||||
|
||||
if not self.is_pane4_hidden:
|
||||
self.create_tab(4, None, path)
|
||||
elif not self.is_pane3_hidden:
|
||||
self.create_tab(3, None, path)
|
||||
elif not self.is_pane2_hidden:
|
||||
self.create_tab(2, None, path)
|
||||
elif not self.is_pane1_hidden:
|
||||
self.create_tab(1, None, path)
|
87
src/solarfm/core/mixins/signals/keyboard_signals_mixin.py
Normal file
87
src/solarfm/core/mixins/signals/keyboard_signals_mixin.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Python imports
|
||||
import re
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
|
||||
|
||||
|
||||
|
||||
|
||||
class KeyboardSignalsMixin:
|
||||
""" KeyboardSignalsMixin keyboard hooks controller. """
|
||||
|
||||
# TODO: Need to set methods that use this to somehow check the keybindings state instead.
|
||||
def unset_keys_and_data(self, widget=None, eve=None):
|
||||
self.ctrl_down = False
|
||||
self.shift_down = False
|
||||
self.alt_down = False
|
||||
|
||||
def on_global_key_press_controller(self, eve, user_data):
|
||||
keyname = Gdk.keyval_name(user_data.keyval).lower()
|
||||
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
|
||||
if "control" in keyname:
|
||||
self.ctrl_down = True
|
||||
if "shift" in keyname:
|
||||
self.shift_down = True
|
||||
if "alt" in keyname:
|
||||
self.alt_down = True
|
||||
|
||||
def on_global_key_release_controller(self, widget, event):
|
||||
"""Handler for keyboard events"""
|
||||
keyname = Gdk.keyval_name(event.keyval).lower()
|
||||
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
|
||||
if "control" in keyname:
|
||||
self.ctrl_down = False
|
||||
if "shift" in keyname:
|
||||
self.shift_down = False
|
||||
if "alt" in keyname:
|
||||
self.alt_down = False
|
||||
|
||||
mapping = keybindings.lookup(event)
|
||||
if mapping:
|
||||
# See if in filemanager scope
|
||||
try:
|
||||
getattr(self, mapping)()
|
||||
return True
|
||||
except Exception:
|
||||
# Must be plugins scope, event call, OR we forgot to add method to file manager scope
|
||||
if "||" in mapping:
|
||||
sender, eve_type = mapping.split("||")
|
||||
else:
|
||||
sender = ""
|
||||
eve_type = mapping
|
||||
|
||||
self.handle_plugin_key_event(sender, eve_type)
|
||||
else:
|
||||
logger.debug(f"on_global_key_release_controller > key > {keyname}")
|
||||
|
||||
if self.ctrl_down:
|
||||
if keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]:
|
||||
self.builder.get_object(f"tggl_notebook_{keyname.strip('kp_')}").released()
|
||||
|
||||
def handle_plugin_key_event(self, sender, eve_type):
|
||||
event_system.emit(eve_type)
|
||||
|
||||
def keyboard_close_tab(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
scroll = self.builder.get_object(f"{wid}|{tid}", use_gtk = False)
|
||||
page = notebook.page_num(scroll)
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
watcher = tab.get_dir_watcher()
|
||||
watcher.cancel()
|
||||
|
||||
self.get_fm_window(wid).delete_tab_by_id(tid)
|
||||
notebook.remove_page(page)
|
||||
if not trace_debug:
|
||||
self.fm_controller.save_state()
|
||||
self.set_window_title()
|
14
src/solarfm/core/mixins/signals_mixins.py
Normal file
14
src/solarfm/core/mixins/signals_mixins.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .signals.file_action_signals_mixin import FileActionSignalsMixin
|
||||
from .signals.ipc_signals_mixin import IPCSignalsMixin
|
||||
from .signals.keyboard_signals_mixin import KeyboardSignalsMixin
|
||||
|
||||
|
||||
|
||||
|
||||
class SignalsMixins(FileActionSignalsMixin, KeyboardSignalsMixin, IPCSignalsMixin):
|
||||
...
|
3
src/solarfm/core/mixins/ui/__init__.py
Normal file
3
src/solarfm/core/mixins/ui/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
UI module
|
||||
"""
|
147
src/solarfm/core/mixins/ui/grid_mixin.py
Normal file
147
src/solarfm/core/mixins/ui/grid_mixin.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# Python imports
|
||||
import asyncio
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from ...widgets.tab_header_widget import TabHeaderWidget
|
||||
from ...widgets.icon_grid_widget import IconGridWidget
|
||||
from ...widgets.icon_tree_widget import IconTreeWidget
|
||||
|
||||
|
||||
|
||||
class GridMixin:
|
||||
"""docstring for GridMixin"""
|
||||
|
||||
def load_store(self, tab, store, save_state = False, use_generator = False):
|
||||
dir = tab.get_current_directory()
|
||||
files = tab.get_files()
|
||||
|
||||
for file in files:
|
||||
store.append([None, file[0]])
|
||||
|
||||
Gtk.main_iteration()
|
||||
# for i, file in enumerate(files):
|
||||
# self.create_icon(i, tab, store, dir, file[0])
|
||||
|
||||
if use_generator:
|
||||
# NOTE: tab > icon > _get_system_thumbnail_gtk_thread must not be used
|
||||
# as the attempted promotion back to gtk threading stalls the generator. (We're already in main gtk thread)
|
||||
for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ):
|
||||
self.load_icon(i, store, icon)
|
||||
else:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = None
|
||||
|
||||
if loop and loop.is_running():
|
||||
loop.create_task( self.create_icons(tab, store, dir, files) )
|
||||
else:
|
||||
asyncio.run( self.create_icons(tab, store, dir, files) )
|
||||
|
||||
# NOTE: Not likely called often from here but it could be useful
|
||||
if save_state and not trace_debug:
|
||||
self.fm_controller.save_state()
|
||||
|
||||
async def create_icons(self, tab, store, dir, files):
|
||||
tasks = [self.update_store(i, store, dir, tab, file[0]) for i, file in enumerate(files)]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def load_icon(self, i, store, icon):
|
||||
self.update_store(i, store, icon)
|
||||
|
||||
async def update_store(self, i, store, dir, tab, file):
|
||||
icon = tab.create_icon(dir, file)
|
||||
itr = store.get_iter(i)
|
||||
store.set_value(itr, 0, icon)
|
||||
|
||||
def create_icons_generator(self, tab, dir, files):
|
||||
for file in files:
|
||||
icon = tab.create_icon(dir, file[0])
|
||||
yield icon
|
||||
|
||||
# @daemon_threaded
|
||||
# def create_icon(self, i, tab, store, dir, file):
|
||||
# icon = tab.create_icon(dir, file)
|
||||
# GLib.idle_add(self.update_store, *(i, store, icon,))
|
||||
#
|
||||
# @daemon_threaded
|
||||
# def load_icon(self, i, store, icon):
|
||||
# GLib.idle_add(self.update_store, *(i, store, icon,))
|
||||
|
||||
# def update_store(self, i, store, icon):
|
||||
# itr = store.get_iter(i)
|
||||
# store.set_value(itr, 0, icon)
|
||||
|
||||
def create_tab_widget(self, tab):
|
||||
return TabHeaderWidget(tab, self.close_tab)
|
||||
|
||||
def create_scroll_and_store(self, tab, wid, use_tree_view = False):
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
|
||||
if not use_tree_view:
|
||||
grid = self.create_icon_grid_widget()
|
||||
else:
|
||||
# TODO: Fix global logic to make the below work too
|
||||
grid = self.create_icon_tree_widget()
|
||||
|
||||
scroll.add(grid)
|
||||
scroll.set_name(f"{wid}|{tab.get_id()}")
|
||||
grid.set_name(f"{wid}|{tab.get_id()}")
|
||||
self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid, use_gtk = False)
|
||||
self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll, use_gtk = False)
|
||||
|
||||
return scroll, grid.get_store()
|
||||
|
||||
def create_icon_grid_widget(self):
|
||||
grid = IconGridWidget()
|
||||
grid._setup_additional_signals(
|
||||
self.grid_icon_single_click,
|
||||
self.grid_icon_double_click,
|
||||
self.grid_set_selected_items,
|
||||
self.grid_on_drag_set,
|
||||
self.grid_on_drag_data_received,
|
||||
self.grid_on_drag_motion
|
||||
)
|
||||
|
||||
return grid
|
||||
|
||||
def create_icon_tree_widget(self):
|
||||
grid = IconTreeWidget()
|
||||
grid._setup_additional_signals(
|
||||
self.grid_icon_single_click,
|
||||
self.grid_icon_double_click,
|
||||
self.grid_on_drag_set,
|
||||
self.grid_on_drag_data_received,
|
||||
self.grid_on_drag_motion
|
||||
)
|
||||
|
||||
grid.columns_autosize()
|
||||
return grid
|
||||
|
||||
def get_store_and_label_from_notebook(self, notebook, _name):
|
||||
icon_grid = None
|
||||
tab_label = None
|
||||
store = None
|
||||
|
||||
for obj in notebook.get_children():
|
||||
icon_grid = obj.get_children()[0]
|
||||
name = icon_grid.get_name()
|
||||
if name == _name:
|
||||
store = icon_grid.get_model()
|
||||
tab_label = notebook.get_tab_label(obj).get_children()[0]
|
||||
|
||||
return store, tab_label
|
||||
|
||||
def get_icon_grid_from_notebook(self, notebook, _name):
|
||||
for obj in notebook.get_children():
|
||||
icon_grid = obj.get_children()[0]
|
||||
name = icon_grid.get_name()
|
||||
if name == _name:
|
||||
return icon_grid
|
58
src/solarfm/core/mixins/ui/pane_mixin.py
Normal file
58
src/solarfm/core/mixins/ui/pane_mixin.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO: Should rewrite to try and support more windows more naturally
|
||||
class PaneMixin:
|
||||
"""docstring for PaneMixin"""
|
||||
|
||||
def toggle_pane(self, child):
|
||||
if child.is_visible():
|
||||
child.hide()
|
||||
else:
|
||||
child.show()
|
||||
|
||||
def run_flag_toggle(self, pane_index):
|
||||
tggl_button = self.builder.get_object(f"tggl_notebook_{pane_index}")
|
||||
if pane_index == 1:
|
||||
self.is_pane1_hidden = not self.is_pane1_hidden
|
||||
tggl_button.set_active(not self.is_pane1_hidden)
|
||||
return self.is_pane1_hidden
|
||||
elif pane_index == 2:
|
||||
self.is_pane2_hidden = not self.is_pane2_hidden
|
||||
tggl_button.set_active(not self.is_pane2_hidden)
|
||||
return self.is_pane2_hidden
|
||||
elif pane_index == 3:
|
||||
self.is_pane3_hidden = not self.is_pane3_hidden
|
||||
tggl_button.set_active(not self.is_pane3_hidden)
|
||||
return self.is_pane3_hidden
|
||||
elif pane_index == 4:
|
||||
self.is_pane4_hidden = not self.is_pane4_hidden
|
||||
tggl_button.set_active(not self.is_pane4_hidden)
|
||||
return self.is_pane4_hidden
|
||||
|
||||
def toggle_notebook_pane(self, widget, eve=None):
|
||||
name = widget.get_name()
|
||||
pane_index = int(name[-1])
|
||||
master_pane = self.builder.get_object("pane_master")
|
||||
pane = self.builder.get_object("pane_top") if pane_index in [1, 2] else self.builder.get_object("pane_bottom")
|
||||
|
||||
state = self.run_flag_toggle(pane_index)
|
||||
if self.is_pane1_hidden and self.is_pane2_hidden and self.is_pane3_hidden and self.is_pane4_hidden:
|
||||
state = self.run_flag_toggle(pane_index)
|
||||
self._set_fm_state(state, pane_index)
|
||||
return
|
||||
|
||||
child = pane.get_child1() if pane_index in [1, 3] else pane.get_child2()
|
||||
|
||||
self.toggle_pane(child)
|
||||
self._set_fm_state(state, pane_index)
|
||||
|
||||
def _set_fm_state(self, state, pane_index):
|
||||
window = self.fm_controller.get_window_by_index(pane_index - 1)
|
||||
window.set_is_hidden(state)
|
239
src/solarfm/core/mixins/ui/tab_mixin.py
Normal file
239
src/solarfm/core/mixins/ui/tab_mixin.py
Normal file
@@ -0,0 +1,239 @@
|
||||
# Python imports
|
||||
import os
|
||||
import gc
|
||||
import time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from .grid_mixin import GridMixin
|
||||
|
||||
|
||||
|
||||
class TabMixin(GridMixin):
|
||||
"""docstring for TabMixin"""
|
||||
|
||||
def create_tab(self, wid: int = None, tid: int = None, path: str = None):
|
||||
if not wid:
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
path_entry = self.builder.get_object(f"path_entry")
|
||||
tab = self.fm_controller.add_tab_for_window_by_nickname(f"window_{wid}")
|
||||
tab.logger = logger
|
||||
|
||||
tab.set_wid(wid)
|
||||
if not path:
|
||||
if wid and tid:
|
||||
_tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
tab.set_path(_tab.get_current_directory())
|
||||
else:
|
||||
tab.set_path(path)
|
||||
|
||||
tab_widget = self.create_tab_widget(tab)
|
||||
scroll, store = self.create_scroll_and_store(tab, wid)
|
||||
index = notebook.append_page(scroll, tab_widget)
|
||||
notebook.set_tab_detachable(scroll, True)
|
||||
|
||||
self.fm_controller.set_wid_and_tid(wid, tab.get_id())
|
||||
path_entry.set_text(tab.get_current_directory())
|
||||
notebook.show_all()
|
||||
notebook.set_current_page(index)
|
||||
|
||||
ctx = notebook.get_style_context()
|
||||
ctx.add_class("notebook-unselected-focus")
|
||||
notebook.set_tab_reorderable(scroll, True)
|
||||
self.load_store(tab, store)
|
||||
self.set_window_title()
|
||||
self.set_file_watcher(tab)
|
||||
|
||||
|
||||
def close_tab(self, button, eve = None):
|
||||
notebook = button.get_parent().get_parent()
|
||||
if notebook.get_n_pages() == 1:
|
||||
return
|
||||
|
||||
tab_box = button.get_parent()
|
||||
wid = int(notebook.get_name()[-1])
|
||||
tid = self.get_id_from_tab_box(tab_box)
|
||||
scroll = self.builder.get_object(f"{wid}|{tid}", use_gtk = False)
|
||||
icon_grid = scroll.get_children()[0]
|
||||
store = icon_grid.get_store()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
watcher = tab.get_dir_watcher()
|
||||
|
||||
watcher.cancel()
|
||||
self.get_fm_window(wid).delete_tab_by_id(tid)
|
||||
|
||||
self.builder.dereference_object(f"{wid}|{tid}|icon_grid")
|
||||
self.builder.dereference_object(f"{wid}|{tid}")
|
||||
|
||||
store.clear()
|
||||
icon_grid.destroy()
|
||||
scroll.destroy()
|
||||
tab_box.destroy()
|
||||
|
||||
del store
|
||||
del icon_grid
|
||||
del scroll
|
||||
del tab_box
|
||||
del watcher
|
||||
del tab
|
||||
|
||||
gc.collect()
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
self.set_window_title()
|
||||
|
||||
# NOTE: Not actually getting called even tho set in the glade file...
|
||||
def on_tab_dnded(self, notebook, page, x, y):
|
||||
...
|
||||
|
||||
def on_tab_reorder(self, child, page_num, new_index):
|
||||
wid, tid = page_num.get_name().split("|")
|
||||
window = self.get_fm_window(wid)
|
||||
tab = None
|
||||
|
||||
for i, tab in enumerate(window.get_all_tabs()):
|
||||
if tab.get_id() == tid:
|
||||
_tab = window.get_tab_by_id(tid)
|
||||
watcher = _tab.get_dir_watcher()
|
||||
watcher.cancel()
|
||||
window.get_all_tabs().insert(new_index, window.get_all_tabs().pop(i))
|
||||
|
||||
tab = window.get_tab_by_id(tid)
|
||||
self.set_file_watcher(tab)
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def on_tab_switch_update(self, notebook, content = None, index = None):
|
||||
self.selected_files.clear()
|
||||
wid, tid = content.get_children()[0].get_name().split("|")
|
||||
self.fm_controller.set_wid_and_tid(wid, tid)
|
||||
self.set_path_text(wid, tid)
|
||||
self.set_window_title()
|
||||
|
||||
def get_id_from_tab_box(self, tab_box):
|
||||
return tab_box.get_children()[2].get_text()
|
||||
|
||||
def get_tab_label(self, notebook, icon_grid):
|
||||
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
|
||||
|
||||
def get_tab_close(self, notebook, icon_grid):
|
||||
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[1]
|
||||
|
||||
def get_tab_icon_grid_from_notebook(self, notebook):
|
||||
return notebook.get_children()[1].get_children()[0]
|
||||
|
||||
def refresh_tab(data = None):
|
||||
state = self.get_current_state()
|
||||
state.tab.load_directory()
|
||||
self.load_store(state.tab, state.store)
|
||||
|
||||
def update_tab(self, tab_label, tab, store, wid, tid):
|
||||
self.load_store(tab, store)
|
||||
self.set_path_text(wid, tid)
|
||||
|
||||
char_width = len(tab.get_end_of_path())
|
||||
tab_label.set_width_chars(char_width)
|
||||
tab_label.set_label(tab.get_end_of_path())
|
||||
self.set_window_title()
|
||||
self.set_file_watcher(tab)
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def do_action_from_bar_controls(self, widget, eve = None):
|
||||
action = widget.get_name()
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
|
||||
if action == "create_tab":
|
||||
dir = tab.get_current_directory()
|
||||
self.create_tab(wid, None, dir)
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
return
|
||||
if action == "go_up":
|
||||
tab.pop_from_path()
|
||||
if action == "go_home":
|
||||
tab.set_to_home()
|
||||
if action == "refresh_tab":
|
||||
tab.load_directory()
|
||||
if action == "path_entry":
|
||||
focused_obj = self.window.get_focus()
|
||||
dir = f"{tab.get_current_directory()}/"
|
||||
path = widget.get_text()
|
||||
|
||||
if isinstance(focused_obj, Gtk.Entry):
|
||||
self.process_path_menu(widget, tab, dir)
|
||||
|
||||
if path.endswith(".") or path == dir:
|
||||
return
|
||||
|
||||
if not tab.set_path(path):
|
||||
return
|
||||
|
||||
icon_grid = self.get_icon_grid_from_notebook(notebook, f"{wid}|{tid}")
|
||||
icon_grid.clear_and_set_new_store()
|
||||
self.update_tab(tab_label, tab, icon_grid.get_store(), wid, tid)
|
||||
|
||||
def process_path_menu(self, gtk_entry, tab, dir):
|
||||
path_menu_buttons = self.builder.get_object("path_menu_buttons")
|
||||
query = gtk_entry.get_text().replace(dir, "")
|
||||
files = tab.get_files() + tab.get_hidden()
|
||||
|
||||
self.clear_children(path_menu_buttons)
|
||||
show_path_menu = False
|
||||
for file, hash, size in files:
|
||||
if os.path.isdir(f"{dir}{file}"):
|
||||
if query.lower() in file.lower():
|
||||
button = Gtk.Button(label=file)
|
||||
button.show()
|
||||
button.connect("clicked", self.set_path_entry)
|
||||
path_menu_buttons.add(button)
|
||||
show_path_menu = True
|
||||
|
||||
if not show_path_menu:
|
||||
event_system.emit("hide_path_menu")
|
||||
else:
|
||||
event_system.emit("show_path_menu")
|
||||
buttons = path_menu_buttons.get_children()
|
||||
|
||||
if len(buttons) == 1:
|
||||
self.slowed_focus(buttons[0])
|
||||
|
||||
@daemon_threaded
|
||||
def slowed_focus(self, button):
|
||||
time.sleep(0.05)
|
||||
GLib.idle_add(self.do_focused_click, *(button,))
|
||||
|
||||
def do_focused_click(self, button):
|
||||
button.grab_focus()
|
||||
button.clicked()
|
||||
|
||||
def set_path_entry(self, button = None, eve = None):
|
||||
self.path_auto_filled = True
|
||||
state = self.get_current_state()
|
||||
path = f"{state.tab.get_current_directory()}/{button.get_label()}"
|
||||
path_entry = self.builder.get_object("path_entry")
|
||||
|
||||
path_entry.set_text(path)
|
||||
path_entry.grab_focus_without_selecting()
|
||||
path_entry.set_position(-1)
|
||||
event_system.emit("hide_path_menu")
|
||||
|
||||
|
||||
def show_hide_hidden_files(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
tab.set_hiding_hidden(not tab.is_hiding_hidden())
|
||||
tab.load_directory()
|
||||
self.builder.get_object("refresh_tab").released()
|
182
src/solarfm/core/mixins/ui/window_mixin.py
Normal file
182
src/solarfm/core/mixins/ui/window_mixin.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# Python imports
|
||||
import copy
|
||||
import traceback
|
||||
from os.path import isdir
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
from .tab_mixin import TabMixin
|
||||
|
||||
|
||||
class WindowException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class WindowMixin(TabMixin):
|
||||
"""docstring for WindowMixin"""
|
||||
|
||||
def get_fm_window(self, wid):
|
||||
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
|
||||
|
||||
def set_bottom_labels(self, tab):
|
||||
event_system.emit("set_bottom_labels", (tab,))
|
||||
|
||||
def set_window_title(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
dir = tab.get_current_directory()
|
||||
|
||||
for _notebook in self.notebooks:
|
||||
ctx = _notebook.get_style_context()
|
||||
ctx.remove_class("notebook-selected-focus")
|
||||
ctx.add_class("notebook-unselected-focus")
|
||||
|
||||
ctx = notebook.get_style_context()
|
||||
ctx.remove_class("notebook-unselected-focus")
|
||||
ctx.add_class("notebook-selected-focus")
|
||||
|
||||
self.window.set_title(f"{app_name} ~ {dir}")
|
||||
self.set_bottom_labels(tab)
|
||||
|
||||
def set_path_text(self, wid, tid):
|
||||
path_entry = self.builder.get_object("path_entry")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
path_entry.set_text(tab.get_current_directory())
|
||||
|
||||
def grid_set_selected_items(self, icons_grid):
|
||||
new_items = icons_grid.get_selected_items()
|
||||
items_size = len(new_items)
|
||||
selected_items = event_system.emit_and_await("get_selected_files")
|
||||
|
||||
if items_size == 1:
|
||||
# NOTE: If already in selection, likely dnd else not so wont readd
|
||||
if new_items[0] in selected_items:
|
||||
self.dnd_left_primed += 1
|
||||
# NOTE: If in selection but trying to just select an already selected item.
|
||||
if self.dnd_left_primed > 1:
|
||||
self.dnd_left_primed = 0
|
||||
selected_items.clear()
|
||||
|
||||
# NOTE: Likely trying dnd, just readd to selection the former set.
|
||||
# Prevents losing highlighting of grid selected.
|
||||
for path in selected_items:
|
||||
icons_grid.select_path(path)
|
||||
|
||||
if items_size > 0:
|
||||
event_system.emit("set_selected_files", (new_items,))
|
||||
else:
|
||||
self.dnd_left_primed = 0
|
||||
selected_items.clear()
|
||||
|
||||
def grid_icon_single_click(self, icons_grid, eve):
|
||||
try:
|
||||
event_system.emit("hide_path_menu")
|
||||
wid, tid = icons_grid.get_name().split("|")
|
||||
self.fm_controller.set_wid_and_tid(wid, tid)
|
||||
self.set_path_text(wid, tid)
|
||||
self.set_window_title()
|
||||
|
||||
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
|
||||
if eve.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
self.dnd_left_primed = 0
|
||||
|
||||
if self.single_click_open: # FIXME: need to find a way to pass the model index
|
||||
self.grid_icon_double_click(icons_grid)
|
||||
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
|
||||
event_system.emit("show_context_menu")
|
||||
|
||||
except WindowException as e:
|
||||
logger.info(repr(e))
|
||||
self.display_message(settings.theming.error_color, f"{repr(e)}")
|
||||
|
||||
def grid_icon_double_click(self, icons_grid, item, data=None):
|
||||
try:
|
||||
if self.ctrl_down and self.shift_down:
|
||||
self.unset_keys_and_data()
|
||||
self.execute_files(in_terminal=True)
|
||||
return
|
||||
elif self.ctrl_down:
|
||||
self.unset_keys_and_data()
|
||||
self.execute_files()
|
||||
return
|
||||
|
||||
state = self.get_current_state()
|
||||
notebook = self.builder.get_object(f"window_{state.wid}")
|
||||
tab_label = self.get_tab_label(notebook, state.icon_grid)
|
||||
|
||||
fileName = state.store[item][1]
|
||||
dir = state.tab.get_current_directory()
|
||||
file = f"{dir}/{fileName}"
|
||||
|
||||
if isdir(file):
|
||||
state.tab.set_path(file)
|
||||
state.icon_grid.clear_and_set_new_store()
|
||||
self.update_tab(tab_label, state.tab, state.icon_grid.get_store(), state.wid, state.tid)
|
||||
else:
|
||||
event_system.emit("open_files")
|
||||
except WindowException as e:
|
||||
traceback.print_exc()
|
||||
self.display_message(settings.theming.error_color, f"{repr(e)}")
|
||||
|
||||
|
||||
|
||||
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
|
||||
action = icons_grid.get_name()
|
||||
wid, tid = action.split("|")
|
||||
store = icons_grid.get_model()
|
||||
treePaths = icons_grid.get_selected_items()
|
||||
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
|
||||
# further down call chain when doing internal fm stuff.
|
||||
uris = self.format_to_uris(store, wid, tid, treePaths)
|
||||
uris_text = '\n'.join(uris)
|
||||
|
||||
data.set_uris(uris)
|
||||
data.set_text(uris_text, -1)
|
||||
|
||||
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
|
||||
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
|
||||
target = icons_grid.get_name()
|
||||
wid, tid = target.split("|")
|
||||
store = icons_grid.get_model()
|
||||
path_at_loc = None
|
||||
|
||||
try:
|
||||
data = icons_grid.get_dest_item_at_pos(x, y)
|
||||
path_at_loc = data[0]
|
||||
drop_position = data[1]
|
||||
highlighted_item_path = icons_grid.get_drag_dest_item().path
|
||||
if path_at_loc and path_at_loc == highlighted_item_path and drop_position == Gtk.IconViewDropPosition.DROP_INTO:
|
||||
uri = self.format_to_uris(store, wid, tid, highlighted_item_path)[0].replace("file://", "")
|
||||
self.override_drop_dest = uri if isdir(uri) else None
|
||||
else:
|
||||
self.override_drop_dest = None
|
||||
except Exception as e:
|
||||
self.override_drop_dest = None
|
||||
|
||||
if target not in current:
|
||||
self.fm_controller.set_wid_and_tid(wid, tid)
|
||||
|
||||
|
||||
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
|
||||
if info == 80:
|
||||
uris = data.get_uris()
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
dest = f"{state.tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
|
||||
if len(uris) == 0:
|
||||
uris = data.get_text().split("\n")
|
||||
|
||||
from_uri = '/'.join(uris[0].replace("file://", "").split("/")[:-1])
|
||||
if from_uri != dest:
|
||||
event_system.emit("move_files", (uris, dest))
|
||||
|
||||
|
||||
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
|
||||
self.create_tab(wid, None, path)
|
33
src/solarfm/core/sfm_builder.py
Normal file
33
src/solarfm/core/sfm_builder.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class SFMBuilder(Gtk.Builder):
|
||||
"""docstring for SFMBuilder."""
|
||||
|
||||
def __init__(self):
|
||||
super(SFMBuilder, self).__init__()
|
||||
|
||||
self.objects = {}
|
||||
|
||||
def get_object(self, id: str, use_gtk: bool = True) -> any:
|
||||
if not use_gtk:
|
||||
return self.objects[id]
|
||||
|
||||
return super(SFMBuilder, self).get_object(id)
|
||||
|
||||
def expose_object(self, id: str, object: any, use_gtk: bool = True) -> None:
|
||||
if not use_gtk:
|
||||
self.objects[id] = object
|
||||
else:
|
||||
super(SFMBuilder, self).expose_object(id, object)
|
||||
|
||||
def dereference_object(self, id: str) -> None:
|
||||
del self.objects[id]
|
88
src/solarfm/core/ui_mixin.py
Normal file
88
src/solarfm/core/ui_mixin.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
from .mixins.ui.pane_mixin import PaneMixin
|
||||
from .mixins.ui.window_mixin import WindowMixin
|
||||
|
||||
from .widgets.files_view.files_widget import FilesWidget
|
||||
|
||||
|
||||
class UIMixinException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class UIMixin(PaneMixin, WindowMixin):
|
||||
# class UIMixin(PaneMixin):
|
||||
def _generate_file_views(self, session_json = None):
|
||||
# self._wip_hybrid_approach_loading_process(session_json)
|
||||
self._current_loading_process(session_json)
|
||||
|
||||
|
||||
def _wip_hybrid_approach_loading_process(self, session_json = None):
|
||||
for session in session_json:
|
||||
self.fm_controller.create_window()
|
||||
FilesWidget().set_fm_controller(self.fm_controller)
|
||||
|
||||
for session in session_json:
|
||||
nickname = session["window"]["Nickname"]
|
||||
tabs = session["window"]["tabs"]
|
||||
isHidden = True if session["window"]["isHidden"] == "True" else False
|
||||
event_system.emit("load_files_view_state", (nickname, tabs))
|
||||
|
||||
@daemon_threaded
|
||||
def _focus_last_visible_notebook(self, icon_grid):
|
||||
import time
|
||||
|
||||
window = settings_manager.get_main_window()
|
||||
while not window.is_visible() and not window.get_realized():
|
||||
time.sleep(0.1)
|
||||
|
||||
icon_grid.event(Gdk.Event().new(type = Gdk.EventType.BUTTON_RELEASE))
|
||||
|
||||
def _current_loading_process(self, session_json = None):
|
||||
if session_json:
|
||||
for j, value in enumerate(session_json):
|
||||
i = j + 1
|
||||
notebook_tggl_button = self.builder.get_object(f"tggl_notebook_{i}")
|
||||
is_hidden = True if value["window"]["isHidden"] == "True" else False
|
||||
tabs = value["window"]["tabs"]
|
||||
self.fm_controller.create_window()
|
||||
notebook_tggl_button.set_active(True)
|
||||
|
||||
if tabs:
|
||||
for tab in tabs:
|
||||
self.create_new_tab_notebook(None, i, tab)
|
||||
else:
|
||||
self.create_new_tab_notebook(None, i, None)
|
||||
|
||||
if is_hidden:
|
||||
self.toggle_notebook_pane(notebook_tggl_button)
|
||||
|
||||
try:
|
||||
if not self.is_pane4_hidden:
|
||||
notebook = self.window4
|
||||
elif not self.is_pane3_hidden:
|
||||
notebook = self.window3
|
||||
elif not self.is_pane2_hidden:
|
||||
notebook = self.window2
|
||||
elif not self.is_pane1_hidden:
|
||||
notebook = self.window1
|
||||
|
||||
scroll_win = notebook.get_children()[-1]
|
||||
icon_grid = scroll_win.get_children()[0]
|
||||
self._focus_last_visible_notebook(icon_grid)
|
||||
except UIMixinException as e:
|
||||
logger.info("\n: The saved session might be missing window data! :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n")
|
||||
logger.debug(repr(e))
|
||||
else:
|
||||
for j in range(0, 4):
|
||||
i = j + 1
|
||||
self.fm_controller.create_window()
|
||||
self.create_new_tab_notebook(None, i, None)
|
3
src/solarfm/core/widgets/__init__.py
Normal file
3
src/solarfm/core/widgets/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Widgets module
|
||||
"""
|
105
src/solarfm/core/widgets/bottom_status_info_widget.py
Normal file
105
src/solarfm/core/widgets/bottom_status_info_widget.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class BottomStatusInfoException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class BottomStatusInfoWidget:
|
||||
"""docstring for BottomStatusInfoWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(BottomStatusInfoWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/bottom_status_info_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("set_bottom_labels", self.set_bottom_labels)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self.bottom_status_info = self._builder.get_object("bottom_status_info")
|
||||
self.bottom_size_label = self._builder.get_object("bottom_size_label")
|
||||
self.bottom_file_count_label = self._builder.get_object("bottom_file_count_label")
|
||||
self.bottom_path_label = self._builder.get_object("bottom_path_label")
|
||||
|
||||
builder.expose_object(f"bottom_status_info", self.bottom_status_info)
|
||||
builder.expose_object(f"bottom_size_label", self.bottom_size_label)
|
||||
builder.expose_object(f"bottom_file_count_label", self.bottom_file_count_label)
|
||||
builder.expose_object(f"bottom_path_label", self.bottom_path_label)
|
||||
|
||||
builder.get_object("core_widget").add(self.bottom_status_info)
|
||||
|
||||
|
||||
def set_bottom_labels(self, tab):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
selected_files = state.icon_grid.get_selected_items()
|
||||
current_directory = tab.get_current_directory()
|
||||
path_file = Gio.File.new_for_path(current_directory)
|
||||
mount_file = path_file.query_filesystem_info(attributes="filesystem::*", cancellable=None)
|
||||
formatted_mount_free = sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
|
||||
formatted_mount_size = sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::size")) )
|
||||
|
||||
# NOTE: Hides empty trash and other desired buttons based on context.
|
||||
if settings_manager.get_trash_files_path() == current_directory:
|
||||
event_system.emit("show_trash_buttons")
|
||||
else:
|
||||
event_system.emit("hide_trash_buttons")
|
||||
|
||||
# If something selected
|
||||
self.bottom_size_label.set_label(f"{formatted_mount_free} free / {formatted_mount_size}")
|
||||
self.bottom_path_label.set_label(tab.get_current_directory())
|
||||
if selected_files:
|
||||
uris = event_system.emit_and_await("format_to_uris", (state.store, state.wid, state.tid, selected_files, True))
|
||||
combined_size = 0
|
||||
for uri in uris:
|
||||
try:
|
||||
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::size",
|
||||
flags=Gio.FileQueryInfoFlags.NONE,
|
||||
cancellable=None)
|
||||
file_size = file_info.get_size()
|
||||
combined_size += file_size
|
||||
except BottomStatusInfoException as e:
|
||||
logger.debug(repr(e))
|
||||
|
||||
formatted_size = sizeof_fmt(combined_size)
|
||||
if tab.is_hiding_hidden():
|
||||
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_files_count()} ({formatted_size})")
|
||||
else:
|
||||
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_not_hidden_count()} ({formatted_size})")
|
||||
|
||||
return
|
||||
|
||||
# If nothing selected
|
||||
if tab.is_hiding_hidden():
|
||||
if tab.get_hidden_count() > 0:
|
||||
self.bottom_file_count_label.set_label(f"{tab.get_not_hidden_count()} visible ({tab.get_hidden_count()} hidden)")
|
||||
else:
|
||||
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
|
||||
else:
|
||||
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
|
93
src/solarfm/core/widgets/context_menu_widget.py
Normal file
93
src/solarfm/core/widgets/context_menu_widget.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class ContextMenuWidget(Gtk.Menu):
|
||||
"""docstring for ContextMenuWidget"""
|
||||
|
||||
def __init__(self):
|
||||
super(ContextMenuWidget, self).__init__()
|
||||
|
||||
self.builder = settings_manager.get_builder()
|
||||
self._builder = Gtk.Builder()
|
||||
self._context_menu_data = settings_manager.get_context_menu_data()
|
||||
self._window = settings_manager.get_main_window()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_context_menu", self.show_context_menu)
|
||||
event_system.subscribe("hide_context_menu", self.hide_context_menu)
|
||||
settings_manager.register_signals_to_builder([self,], self._builder)
|
||||
|
||||
def _load_widgets(self):
|
||||
self.build_context_menu()
|
||||
|
||||
def _emit(self, menu_item, type):
|
||||
event_system.emit("do_action_from_menu_controls", type)
|
||||
|
||||
|
||||
def make_submenu(self, name, data, keys):
|
||||
menu = Gtk.Menu()
|
||||
menu_item = Gtk.MenuItem(name)
|
||||
|
||||
for key in keys:
|
||||
if isinstance(data, dict):
|
||||
entry = self.make_menu_item(key, data[key])
|
||||
elif isinstance(data, list):
|
||||
entry = self.make_menu_item(key, data)
|
||||
else:
|
||||
continue
|
||||
|
||||
menu.append(entry)
|
||||
|
||||
menu_item.set_submenu(menu)
|
||||
return menu_item
|
||||
|
||||
def make_menu_item(self, name, data) -> Gtk.MenuItem:
|
||||
if isinstance(data, dict):
|
||||
return self.make_submenu(name, data, data.keys())
|
||||
elif isinstance(data, list):
|
||||
entry = Gtk.ImageMenuItem(name)
|
||||
icon = getattr(Gtk, f"{data[0]}")
|
||||
entry.set_image( Gtk.Image(stock=icon) )
|
||||
entry.set_always_show_image(True)
|
||||
entry.connect("activate", self._emit, (data[1]))
|
||||
return entry
|
||||
|
||||
def build_context_menu(self) -> None:
|
||||
data = self._context_menu_data
|
||||
dkeys = data.keys()
|
||||
plugins_entry = None
|
||||
|
||||
for dkey in dkeys:
|
||||
entry = self.make_menu_item(dkey, data[dkey])
|
||||
self.append(entry)
|
||||
if dkey == "Plugins":
|
||||
plugins_entry = entry
|
||||
|
||||
self.attach_to_widget(self._window, None)
|
||||
self.show_all()
|
||||
self.builder.expose_object("context_menu", self)
|
||||
if plugins_entry:
|
||||
self.builder.expose_object("context_menu_plugins", plugins_entry.get_submenu())
|
||||
|
||||
def show_context_menu(self, widget=None, eve=None):
|
||||
self.builder.get_object("context_menu").popup_at_pointer(None)
|
||||
|
||||
def hide_context_menu(self, widget=None, eve=None):
|
||||
self.builder.get_object("context_menu").popdown()
|
3
src/solarfm/core/widgets/dialogs/__init__.py
Normal file
3
src/solarfm/core/widgets/dialogs/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Dialogs module
|
||||
"""
|
49
src/solarfm/core/widgets/dialogs/about_widget.py
Normal file
49
src/solarfm/core/widgets/dialogs/about_widget.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class AboutWidget:
|
||||
"""docstring for AboutWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(AboutWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/about_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_about_page", self.show_about_page)
|
||||
event_system.subscribe("hide_about_page", self.hide_about_page)
|
||||
settings_manager.register_signals_to_builder([self,], self._builder)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self.about_page = self._builder.get_object("about_page")
|
||||
builder.expose_object(f"about_page", self.about_page)
|
||||
|
||||
|
||||
def show_about_page(self, widget=None, eve=None):
|
||||
response = self.about_page.run()
|
||||
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
|
||||
self.hide_about_page()
|
||||
|
||||
def hide_about_page(self, widget=None, eve=None):
|
||||
self.about_page.hide()
|
61
src/solarfm/core/widgets/dialogs/appchooser_widget.py
Normal file
61
src/solarfm/core/widgets/dialogs/appchooser_widget.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class AppchooserWidget:
|
||||
"""docstring for AppchooserWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(AppchooserWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/appchooser_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_appchooser_menu", self.show_appchooser_menu)
|
||||
event_system.subscribe("hide_appchooser_menu", self.hide_appchooser_menu)
|
||||
event_system.subscribe("run_appchooser_launch", self.run_appchooser_launch)
|
||||
settings_manager.register_signals_to_builder([self,], self._builder)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self._appchooser_menu = self._builder.get_object("appchooser_menu")
|
||||
self._appchooser_widget = self._builder.get_object("appchooser_widget")
|
||||
|
||||
builder.expose_object(f"appchooser_menu", self._appchooser_menu)
|
||||
builder.expose_object(f"appchooser_widget", self._appchooser_widget)
|
||||
|
||||
|
||||
def show_appchooser_menu(self, widget=None, eve=None):
|
||||
response = self._appchooser_menu.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
app_info = self._appchooser_widget.get_app_info()
|
||||
event_system.emit("open_with_files", app_info)
|
||||
self.hide_appchooser_menu()
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
self.hide_appchooser_menu()
|
||||
|
||||
|
||||
def hide_appchooser_menu(self, widget=None, eve=None):
|
||||
self._appchooser_menu.hide()
|
||||
|
||||
def run_appchooser_launch(self, widget=None, eve=None):
|
||||
self._appchooser_menu.response(Gtk.ResponseType.OK)
|
130
src/solarfm/core/widgets/dialogs/file_exists_widget.py
Normal file
130
src/solarfm/core/widgets/dialogs/file_exists_widget.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class FileExistsWidget:
|
||||
"""docstring for FileExistsWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(FileExistsWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/file_exists_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("setup_exists_data", self.setup_exists_data)
|
||||
event_system.subscribe("show_exists_page", self.show_exists_page)
|
||||
settings_manager.register_signals_to_builder([self,], self._builder)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self.file_exists_dialog = self._builder.get_object("file_exists_dialog")
|
||||
self._exists_file_label = self._builder.get_object("exists_file_label")
|
||||
self._exists_file_diff_from = self._builder.get_object("exists_file_diff_from")
|
||||
self._exists_file_diff_to = self._builder.get_object("exists_file_diff_to")
|
||||
self._exists_file_field = self._builder.get_object("exists_file_field")
|
||||
self._exists_file_from = self._builder.get_object("exists_file_from")
|
||||
self._exists_file_to = self._builder.get_object("exists_file_to")
|
||||
self._exists_file_rename_bttn = self._builder.get_object("exists_file_rename_bttn")
|
||||
|
||||
builder.expose_object(f"file_exists_dialog", self.file_exists_dialog)
|
||||
builder.expose_object(f"exists_file_diff_from", self._exists_file_diff_from)
|
||||
builder.expose_object(f"exists_file_diff_to", self._exists_file_diff_to)
|
||||
builder.expose_object(f"exists_file_field", self._exists_file_field)
|
||||
builder.expose_object(f"exists_file_rename_bttn", self._exists_file_rename_bttn)
|
||||
|
||||
|
||||
def show_exists_page(self, widget=None, eve=None):
|
||||
response = self.file_exists_dialog.run()
|
||||
self.file_exists_dialog.hide()
|
||||
|
||||
if response == Gtk.ResponseType.OK:
|
||||
return "rename"
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
return "rename_auto"
|
||||
if response == Gtk.ResponseType.CLOSE:
|
||||
return "rename_auto_all"
|
||||
if response == Gtk.ResponseType.YES:
|
||||
return "overwrite"
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return "overwrite_all"
|
||||
if response == Gtk.ResponseType.NO:
|
||||
return "skip"
|
||||
if response == Gtk.ResponseType.REJECT:
|
||||
return "skip_all"
|
||||
|
||||
def hide_exists_page_rename(self, widget=None, eve=None):
|
||||
self.file_exists_dialog.response(Gtk.ResponseType.OK)
|
||||
|
||||
def hide_exists_page_auto_rename(self, widget=None, eve=None):
|
||||
self.file_exists_dialog.response(Gtk.ResponseType.ACCEPT)
|
||||
|
||||
def hide_exists_page_auto_rename_all(self, widget=None, eve=None):
|
||||
self.file_exists_dialog.response(Gtk.ResponseType.CLOSE)
|
||||
|
||||
def setup_exists_data(self, from_file, to_file):
|
||||
from_info = from_file.query_info("standard::*,time::modified", 0, cancellable=None)
|
||||
to_info = to_file.query_info("standard::*,time::modified", 0, cancellable=None)
|
||||
from_date = from_info.get_modification_date_time()
|
||||
to_date = to_info.get_modification_date_time()
|
||||
from_size = from_info.get_size()
|
||||
to_size = to_info.get_size()
|
||||
|
||||
self._exists_file_from.set_label(from_file.get_parent().get_path())
|
||||
self._exists_file_to.set_label(to_file.get_parent().get_path())
|
||||
self._exists_file_label.set_label(to_file.get_basename())
|
||||
self._exists_file_field.set_text(to_file.get_basename())
|
||||
|
||||
# Returns: -1, 0 or 1 if dt1 is less than, equal to or greater than dt2.
|
||||
age = GLib.DateTime.compare(from_date, to_date)
|
||||
age_text = "( same time )"
|
||||
if age == -1:
|
||||
age_text = "older"
|
||||
if age == 1:
|
||||
age_text = "newer"
|
||||
|
||||
size_text = "( same size )"
|
||||
if from_size < to_size:
|
||||
size_text = "smaller"
|
||||
if from_size > to_size:
|
||||
size_text = "larger"
|
||||
|
||||
from_label_text = f"{age_text} & {size_text}"
|
||||
if age_text != "( same time )" or size_text != "( same size )":
|
||||
from_label_text = f"{from_date.format('%F %R')} {sizeof_fmt(from_size)} ( {from_size} bytes ) ( {age_text} & {size_text} )"
|
||||
to_label_text = f"{to_date.format('%F %R')} {sizeof_fmt(to_size)} ( {to_size} bytes )"
|
||||
|
||||
self._exists_file_diff_from.set_text(from_label_text)
|
||||
self._exists_file_diff_to.set_text(to_label_text)
|
||||
|
||||
|
||||
def exists_rename_field_changed(self, widget):
|
||||
nfile_name = widget.get_text().strip()
|
||||
ofile_name = self._exists_file_label.get_label()
|
||||
|
||||
if nfile_name:
|
||||
if nfile_name == ofile_name:
|
||||
self._exists_file_rename_bttn.set_sensitive(False)
|
||||
else:
|
||||
self._exists_file_rename_bttn.set_sensitive(True)
|
||||
else:
|
||||
self._exists_file_rename_bttn.set_sensitive(False)
|
41
src/solarfm/core/widgets/dialogs/message_widget.py
Normal file
41
src/solarfm/core/widgets/dialogs/message_widget.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class MessageWidget(Gtk.MessageDialog):
|
||||
"""docstring for MessageWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(MessageWidget, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.type = Gtk.MessageType.WARNING
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
...
|
||||
|
||||
def _load_widgets(self):
|
||||
message_area = self.get_message_area()
|
||||
message_area.get_children()[0].set_label("Alert!")
|
||||
message_area.show_all()
|
||||
|
||||
self.set_image( Gtk.Image.new_from_icon_name("user-alert", 16) )
|
||||
self.add_buttons(Gtk.STOCK_NO, Gtk.ResponseType.NO)
|
||||
self.add_buttons(Gtk.STOCK_YES, Gtk.ResponseType.YES)
|
67
src/solarfm/core/widgets/dialogs/new_file_widget.py
Normal file
67
src/solarfm/core/widgets/dialogs/new_file_widget.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class NewFileWidget:
|
||||
"""docstring for NewFileWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(NewFileWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/new_file_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_new_file_menu", self.show_new_file_menu)
|
||||
event_system.subscribe("hide_new_file_menu", self.hide_new_file_menu)
|
||||
settings_manager.register_signals_to_builder([self,], self._builder)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self._new_file_menu = self._builder.get_object("new_file_menu")
|
||||
self._new_fname_field = self._builder.get_object("new_fname_field")
|
||||
self._new_file_toggle_type = self._builder.get_object("new_file_toggle_type")
|
||||
|
||||
builder.expose_object(f"new_file_menu", self._new_file_menu)
|
||||
builder.expose_object(f"new_fname_field", self._new_fname_field)
|
||||
builder.expose_object(f"new_file_toggle_type", self._new_file_toggle_type)
|
||||
|
||||
|
||||
def show_new_file_menu(self, widget=None, eve=None):
|
||||
if widget:
|
||||
widget.set_text("")
|
||||
widget.grab_focus()
|
||||
|
||||
response = self._new_file_menu.run()
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def hide_new_file_menu(self, widget=None, eve=None):
|
||||
self._builder.get_object("new_file_menu").hide()
|
||||
|
||||
def hide_new_file_menu_enter_key(self, widget=None, eve=None):
|
||||
keyname = Gdk.keyval_name(eve.keyval).lower()
|
||||
if keyname in ["return", "enter"]:
|
||||
self._new_file_menu.hide()
|
81
src/solarfm/core/widgets/dialogs/rename_widget.py
Normal file
81
src/solarfm/core/widgets/dialogs/rename_widget.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class RenameWidget:
|
||||
"""docstring for RenameWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(RenameWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/rename_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_rename_file_menu", self.show_rename_file_menu)
|
||||
event_system.subscribe("hide_rename_file_menu", self.hide_rename_file_menu)
|
||||
settings_manager.register_signals_to_builder([self,], self._builder)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self._rename_file_menu = self._builder.get_object("rename_file_menu")
|
||||
self._rename_fname = self._builder.get_object("rename_fname")
|
||||
self._file_to_rename_label = self._builder.get_object("file_to_rename_label")
|
||||
|
||||
builder.expose_object(f"rename_file_menu", self._rename_file_menu)
|
||||
builder.expose_object(f"rename_fname", self._rename_fname)
|
||||
builder.expose_object(f"file_to_rename_label", self._file_to_rename_label)
|
||||
|
||||
|
||||
def show_rename_file_menu(self, widget=None, eve=None):
|
||||
if widget:
|
||||
widget.grab_focus()
|
||||
|
||||
response = self._rename_file_menu.run()
|
||||
if response == Gtk.ResponseType.CLOSE:
|
||||
return "skip_edit"
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return "cancel_edit"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def set_to_title_case(self, widget, eve=None):
|
||||
self._rename_fname.set_text( self._rename_fname.get_text().title() )
|
||||
|
||||
def set_to_upper_case(self, widget, eve=None):
|
||||
self._rename_fname.set_text( self._rename_fname.get_text().upper() )
|
||||
|
||||
def set_to_lower_case(self, widget, eve=None):
|
||||
self._rename_fname.set_text( self._rename_fname.get_text().lower() )
|
||||
|
||||
def set_to_invert_case(self, widget, eve=None):
|
||||
self._rename_fname.set_text( self._rename_fname.get_text().swapcase() )
|
||||
|
||||
def hide_rename_file_menu(self, widget=None, eve=None):
|
||||
self._rename_file_menu.hide()
|
||||
|
||||
def hide_rename_file_menu_enter_key(self, widget=None, eve=None):
|
||||
keyname = Gdk.keyval_name(eve.keyval).lower()
|
||||
if keyname in ["return", "enter"]:
|
||||
self._rename_file_menu.hide()
|
84
src/solarfm/core/widgets/dialogs/save_load_widget.py
Normal file
84
src/solarfm/core/widgets/dialogs/save_load_widget.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Python imports
|
||||
import gc
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class SaveLoadWidgetException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class SaveLoadWidget:
|
||||
"""docstring for SaveLoadWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(SaveLoadWidget, self).__init__()
|
||||
|
||||
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/save_load_ui.glade"
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(_GLADE_FILE)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("save_load_session", self.save_load_session)
|
||||
|
||||
def _load_widgets(self):
|
||||
builder = settings_manager.get_builder()
|
||||
|
||||
self.save_load_dialog = self._builder.get_object("save_load_dialog")
|
||||
builder.expose_object(f"save_load_dialog", self.save_load_dialog)
|
||||
|
||||
|
||||
def save_load_session(self, action="save_session"):
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
|
||||
if action == "save_session":
|
||||
if not settings_manager.is_trace_debug():
|
||||
state.fm_controller.save_state()
|
||||
|
||||
return
|
||||
elif action == "save_session_as":
|
||||
self.save_load_dialog.set_action(Gtk.FileChooserAction.SAVE)
|
||||
elif action == "load_session":
|
||||
self.save_load_dialog.set_action(Gtk.FileChooserAction.OPEN)
|
||||
else:
|
||||
raise SaveLoadWidgetException(f"Unknown action given: {action}")
|
||||
|
||||
self.save_load_dialog.set_current_folder(state.tab.get_current_directory())
|
||||
self.save_load_dialog.set_current_name("session.json")
|
||||
response = self.save_load_dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
if action == "save_session_as":
|
||||
path = f"{self.save_load_dialog.get_current_folder()}/{self.save_load_dialog.get_current_name()}"
|
||||
state.fm_controller.save_state(path)
|
||||
elif action == "load_session":
|
||||
path = f"{self.save_load_dialog.get_file().get_path()}"
|
||||
session_json = state.fm_controller.get_state_from_file(path)
|
||||
self.load_session(session_json)
|
||||
if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT):
|
||||
...
|
||||
|
||||
self.save_load_dialog.hide()
|
||||
|
||||
def load_session(self, session_json):
|
||||
if settings_manager.is_debug():
|
||||
logger.debug(f"Session Data: {session_json}")
|
||||
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
event_system.emit("clear_notebooks")
|
||||
state.fm_controller.unload_tabs_and_windows()
|
||||
event_system.emit("generate_file_views", (session_json,))
|
||||
gc.collect()
|
63
src/solarfm/core/widgets/dialogs/user_pass_widget.py
Normal file
63
src/solarfm/core/widgets/dialogs/user_pass_widget.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class UserPassWidget(Gtk.Dialog):
|
||||
"""docstring for UserPassWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(UserPassWidget, self).__init__()
|
||||
|
||||
self.user_input = Gtk.Entry()
|
||||
self.pass_input = Gtk.Entry()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_modal(True)
|
||||
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
|
||||
self.user_input.set_placeholder_text("User:")
|
||||
self.pass_input.set_placeholder_text("Password:")
|
||||
self.pass_input.set_visibility(False)
|
||||
|
||||
def _setup_signals(self):
|
||||
# self.connect("response", self.on_response)
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
...
|
||||
|
||||
def _load_widgets(self):
|
||||
vbox = self.get_content_area()
|
||||
label = Gtk.Label(label="User & Password")
|
||||
vbox.add(label)
|
||||
vbox.add(self.user_input)
|
||||
vbox.add(self.pass_input)
|
||||
vbox.show_all()
|
||||
|
||||
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
"Skip Input", Gtk.ResponseType.CLOSE,
|
||||
"Submit Input", Gtk.ResponseType.OK
|
||||
)
|
||||
|
||||
def do_response(self, response_id):
|
||||
self.hide()
|
||||
return response_id
|
||||
|
||||
# def on_response(self, widget, response_id):
|
||||
# self.hide()
|
||||
# return response_id
|
3
src/solarfm/core/widgets/files_view/__init__.py
Normal file
3
src/solarfm/core/widgets/files_view/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Files View module
|
||||
"""
|
71
src/solarfm/core/widgets/files_view/files_widget.py
Normal file
71
src/solarfm/core/widgets/files_view/files_widget.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from ...mixins.signals.file_action_signals_mixin import FileActionSignalsMixin
|
||||
from .window_mixin import WindowMixin
|
||||
|
||||
|
||||
|
||||
class FilesWidget(FileActionSignalsMixin, WindowMixin):
|
||||
"""docstring for FilesWidget."""
|
||||
|
||||
ccount = 0
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
obj = super(FilesWidget, cls).__new__(cls)
|
||||
cls.ccount += 1
|
||||
|
||||
return obj
|
||||
|
||||
def __init__(self):
|
||||
super(FilesWidget, self).__init__()
|
||||
|
||||
self.INDEX = self.ccount
|
||||
self.NAME = f"window_{self.INDEX}"
|
||||
self.builder = Gtk.Builder()
|
||||
self.files_view = None
|
||||
self.fm_controller = None
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
settings_manager.register_signals_to_builder([self,], self.builder)
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("load_files_view_state", self._load_files_view_state)
|
||||
event_system.subscribe("get_files_view_icon_grid", self._get_files_view_icon_grid)
|
||||
|
||||
def _load_widgets(self):
|
||||
_builder = settings_manager.get_builder()
|
||||
self.files_view = _builder.get_object(f"{self.NAME}")
|
||||
|
||||
self.files_view.set_group_name("files_widget")
|
||||
self.builder.expose_object(f"{self.NAME}", self.files_view)
|
||||
|
||||
def _load_files_view_state(self, win_name = None, tabs = None):
|
||||
if win_name == self.NAME:
|
||||
if tabs:
|
||||
for tab in tabs:
|
||||
self.create_new_tab_notebook(None, self.INDEX, tab)
|
||||
else:
|
||||
self.create_new_tab_notebook(None, self.INDEX, None)
|
||||
|
||||
def _get_files_view_icon_grid(self, win_index = None, tid = None):
|
||||
if win_index == str(self.INDEX):
|
||||
return self.builder.get_object(f"{self.INDEX}|{tid}|icon_grid", use_gtk = False)
|
||||
|
||||
|
||||
def set_fm_controller(self, _fm_controller):
|
||||
self.fm_controller = _fm_controller
|
139
src/solarfm/core/widgets/files_view/grid_mixin.py
Normal file
139
src/solarfm/core/widgets/files_view/grid_mixin.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# Python imports
|
||||
import asyncio
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from ...widgets.tab_header_widget import TabHeaderWidget
|
||||
from ...widgets.icon_grid_widget import IconGridWidget
|
||||
from ...widgets.icon_tree_widget import IconTreeWidget
|
||||
|
||||
|
||||
|
||||
class GridMixin:
|
||||
"""docstring for GridMixin"""
|
||||
|
||||
def load_store(self, tab, store, save_state = False, use_generator = False):
|
||||
dir = tab.get_current_directory()
|
||||
files = tab.get_files()
|
||||
|
||||
for file in files:
|
||||
store.append([None, file[0]])
|
||||
|
||||
Gtk.main_iteration()
|
||||
if use_generator:
|
||||
# NOTE: tab > icon > _get_system_thumbnail_gtk_thread must not be used
|
||||
# as the attempted promotion back to gtk threading stalls the generator. (We're already in main gtk thread)
|
||||
for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ):
|
||||
self.load_icon(i, store, icon)
|
||||
else:
|
||||
# for i, file in enumerate(files):
|
||||
# self.create_icon(i, tab, store, dir, file[0])
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = None
|
||||
|
||||
if loop and loop.is_running():
|
||||
loop.create_task( self.create_icons(tab, store, dir, files) )
|
||||
else:
|
||||
asyncio.run( self.create_icons(tab, store, dir, files) )
|
||||
|
||||
# NOTE: Not likely called often from here but it could be useful
|
||||
if save_state and not trace_debug:
|
||||
self.fm_controller.save_state()
|
||||
|
||||
async def create_icons(self, tab, store, dir, files):
|
||||
tasks = [self.update_store(i, store, dir, tab, file[0]) for i, file in enumerate(files)]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def load_icon(self, i, store, icon):
|
||||
self.update_store(i, store, icon)
|
||||
|
||||
async def update_store(self, i, store, dir, tab, file):
|
||||
icon = tab.create_icon(dir, file)
|
||||
itr = store.get_iter(i)
|
||||
store.set_value(itr, 0, icon)
|
||||
|
||||
def create_icons_generator(self, tab, dir, files):
|
||||
for file in files:
|
||||
icon = tab.create_icon(dir, file[0])
|
||||
yield icon
|
||||
|
||||
# @daemon_threaded
|
||||
# def create_icon(self, i, tab, store, dir, file):
|
||||
# icon = tab.create_icon(dir, file)
|
||||
# GLib.idle_add(self.update_store, *(i, store, icon,))
|
||||
#
|
||||
# @daemon_threaded
|
||||
# def load_icon(self, i, store, icon):
|
||||
# GLib.idle_add(self.update_store, *(i, store, icon,))
|
||||
#
|
||||
# def update_store(self, i, store, icon):
|
||||
# itr = store.get_iter(i)
|
||||
# store.set_value(itr, 0, icon)
|
||||
|
||||
def create_tab_widget(self, tab):
|
||||
return TabHeaderWidget(tab, self.close_tab)
|
||||
|
||||
def create_scroll_and_store(self, tab, wid, use_tree_view = False):
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
|
||||
if not use_tree_view:
|
||||
grid = self.create_icon_grid_widget()
|
||||
else:
|
||||
# TODO: Fix global logic to make the below work too
|
||||
grid = self.create_icon_tree_widget()
|
||||
|
||||
scroll.add(grid)
|
||||
scroll.set_name(f"{wid}|{tab.get_id()}")
|
||||
grid.set_name(f"{wid}|{tab.get_id()}")
|
||||
self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid, use_gtk = False)
|
||||
self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll, use_gtk = False)
|
||||
|
||||
return scroll, grid.get_store()
|
||||
|
||||
def create_icon_grid_widget(self):
|
||||
grid = IconGridWidget()
|
||||
grid._setup_additional_signals(
|
||||
self.grid_icon_single_click,
|
||||
self.grid_icon_double_click,
|
||||
self.grid_set_selected_items,
|
||||
self.grid_on_drag_set,
|
||||
self.grid_on_drag_data_received,
|
||||
self.grid_on_drag_motion
|
||||
)
|
||||
|
||||
return grid
|
||||
|
||||
def create_icon_tree_widget(self):
|
||||
grid = IconTreeWidget()
|
||||
grid._setup_additional_signals(
|
||||
self.grid_icon_single_click,
|
||||
self.grid_icon_double_click,
|
||||
self.grid_on_drag_set,
|
||||
self.grid_on_drag_data_received,
|
||||
self.grid_on_drag_motion
|
||||
)
|
||||
|
||||
grid.columns_autosize()
|
||||
return grid
|
||||
|
||||
def get_store_and_label_from_notebook(self, notebook, _name):
|
||||
icon_grid = None
|
||||
tab_label = None
|
||||
store = None
|
||||
|
||||
for obj in notebook.get_children():
|
||||
icon_grid = obj.get_children()[0]
|
||||
name = icon_grid.get_name()
|
||||
if name == _name:
|
||||
store = icon_grid.get_model()
|
||||
tab_label = notebook.get_tab_label(obj).get_children()[0]
|
||||
|
||||
return store, tab_label
|
244
src/solarfm/core/widgets/files_view/tab_mixin.py
Normal file
244
src/solarfm/core/widgets/files_view/tab_mixin.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# Python imports
|
||||
import os
|
||||
import gc
|
||||
import time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from .grid_mixin import GridMixin
|
||||
|
||||
|
||||
|
||||
class TabMixin(GridMixin):
|
||||
"""docstring for TabMixin"""
|
||||
|
||||
def create_tab(self, wid: int = None, tid: int = None, path: str = None):
|
||||
if not wid:
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
# path_entry = self.builder.get_object(f"path_entry")
|
||||
tab = self.fm_controller.add_tab_for_window_by_nickname(f"window_{wid}")
|
||||
# tab.logger = logger
|
||||
|
||||
tab.set_wid(wid)
|
||||
if not path:
|
||||
if wid and tid:
|
||||
_tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
tab.set_path(_tab.get_current_directory())
|
||||
else:
|
||||
tab.set_path(path)
|
||||
|
||||
tab_widget = self.create_tab_widget(tab)
|
||||
scroll, store = self.create_scroll_and_store(tab, wid)
|
||||
index = notebook.append_page(scroll, tab_widget)
|
||||
notebook.set_tab_detachable(scroll, True)
|
||||
|
||||
self.fm_controller.set_wid_and_tid(wid, tab.get_id())
|
||||
event_system.emit("go_to_path", (tab.get_current_directory(),)) # NOTE: Not efficent if I understand how
|
||||
# path_entry.set_text(tab.get_current_directory())
|
||||
notebook.show_all()
|
||||
notebook.set_current_page(index)
|
||||
|
||||
ctx = notebook.get_style_context()
|
||||
ctx.add_class("notebook-unselected-focus")
|
||||
notebook.set_tab_reorderable(scroll, True)
|
||||
self.load_store(tab, store)
|
||||
# self.set_window_title()
|
||||
event_system.emit("set_window_title", (tab.get_current_directory(),))
|
||||
self.set_file_watcher(tab)
|
||||
|
||||
|
||||
def close_tab(self, button, eve = None):
|
||||
notebook = button.get_parent().get_parent()
|
||||
if notebook.get_n_pages() == 1:
|
||||
return
|
||||
|
||||
tab_box = button.get_parent()
|
||||
wid = int(notebook.get_name()[-1])
|
||||
tid = self.get_id_from_tab_box(tab_box)
|
||||
scroll = self.builder.get_object(f"{wid}|{tid}", use_gtk = False)
|
||||
icon_grid = scroll.get_children()[0]
|
||||
store = icon_grid.get_model()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
watcher = tab.get_dir_watcher()
|
||||
|
||||
watcher.cancel()
|
||||
self.get_fm_window(wid).delete_tab_by_id(tid)
|
||||
|
||||
self.builder.dereference_object(f"{wid}|{tid}|icon_grid")
|
||||
self.builder.dereference_object(f"{wid}|{tid}")
|
||||
|
||||
store.clear()
|
||||
# store.run_dispose()
|
||||
icon_grid.destroy()
|
||||
# icon_grid.run_dispose()
|
||||
scroll.destroy()
|
||||
#scroll.run_dispose()
|
||||
tab_box.destroy()
|
||||
#tab_box.run_dispose()
|
||||
|
||||
del store
|
||||
del icon_grid
|
||||
del scroll
|
||||
del tab_box
|
||||
del watcher
|
||||
del tab
|
||||
|
||||
gc.collect()
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
self.set_window_title()
|
||||
|
||||
# NOTE: Not actually getting called even tho set in the glade file...
|
||||
def on_tab_dnded(self, notebook, page, x, y):
|
||||
...
|
||||
|
||||
def on_tab_reorder(self, child, page_num, new_index):
|
||||
wid, tid = page_num.get_name().split("|")
|
||||
window = self.get_fm_window(wid)
|
||||
tab = None
|
||||
|
||||
for i, tab in enumerate(window.get_all_tabs()):
|
||||
if tab.get_id() == tid:
|
||||
_tab = window.get_tab_by_id(tid)
|
||||
watcher = _tab.get_dir_watcher()
|
||||
watcher.cancel()
|
||||
window.get_all_tabs().insert(new_index, window.get_all_tabs().pop(i))
|
||||
|
||||
tab = window.get_tab_by_id(tid)
|
||||
self.set_file_watcher(tab)
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def on_tab_switch_update(self, notebook, content = None, index = None):
|
||||
self.selected_files.clear()
|
||||
wid, tid = content.get_children()[0].get_name().split("|")
|
||||
self.fm_controller.set_wid_and_tid(wid, tid)
|
||||
self.set_path_text(wid, tid)
|
||||
self.set_window_title()
|
||||
|
||||
def get_id_from_tab_box(self, tab_box):
|
||||
return tab_box.get_children()[2].get_text()
|
||||
|
||||
def get_tab_label(self, notebook, icon_grid):
|
||||
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
|
||||
|
||||
def get_tab_close(self, notebook, icon_grid):
|
||||
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[1]
|
||||
|
||||
def get_tab_icon_grid_from_notebook(self, notebook):
|
||||
return notebook.get_children()[1].get_children()[0]
|
||||
|
||||
def refresh_tab(data = None):
|
||||
state = self.get_current_state()
|
||||
state.tab.load_directory()
|
||||
self.load_store(state.tab, state.store)
|
||||
|
||||
def update_tab(self, tab_label, tab, store, wid, tid):
|
||||
self.load_store(tab, store)
|
||||
self.set_path_text(wid, tid)
|
||||
|
||||
char_width = len(tab.get_end_of_path())
|
||||
tab_label.set_width_chars(char_width)
|
||||
tab_label.set_label(tab.get_end_of_path())
|
||||
self.set_window_title()
|
||||
self.set_file_watcher(tab)
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
def do_action_from_bar_controls(self, widget, eve = None):
|
||||
action = widget.get_name()
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
|
||||
if action == "create_tab":
|
||||
dir = tab.get_current_directory()
|
||||
self.create_tab(wid, None, dir)
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.fm_controller.save_state()
|
||||
|
||||
return
|
||||
if action == "go_up":
|
||||
tab.pop_from_path()
|
||||
if action == "go_home":
|
||||
tab.set_to_home()
|
||||
if action == "refresh_tab":
|
||||
tab.load_directory()
|
||||
if action == "path_entry":
|
||||
focused_obj = self.window.get_focus()
|
||||
dir = f"{tab.get_current_directory()}/"
|
||||
path = widget.get_text()
|
||||
|
||||
if isinstance(focused_obj, Gtk.Entry):
|
||||
self.process_path_menu(widget, tab, dir)
|
||||
|
||||
if path.endswith(".") or path == dir:
|
||||
return
|
||||
|
||||
if not tab.set_path(path):
|
||||
return
|
||||
|
||||
icon_grid = self.get_icon_grid_from_notebook(notebook, f"{wid}|{tid}")
|
||||
icon_grid.clear_and_set_new_store()
|
||||
self.update_tab(tab_label, tab, store, wid, tid)
|
||||
|
||||
def process_path_menu(self, gtk_entry, tab, dir):
|
||||
path_menu_buttons = self.builder.get_object("path_menu_buttons")
|
||||
query = gtk_entry.get_text().replace(dir, "")
|
||||
files = tab.get_files() + tab.get_hidden()
|
||||
|
||||
self.clear_children(path_menu_buttons)
|
||||
show_path_menu = False
|
||||
for file, hash, size in files:
|
||||
if os.path.isdir(f"{dir}{file}"):
|
||||
if query.lower() in file.lower():
|
||||
button = Gtk.Button(label=file)
|
||||
button.show()
|
||||
button.connect("clicked", self.set_path_entry)
|
||||
path_menu_buttons.add(button)
|
||||
show_path_menu = True
|
||||
|
||||
if not show_path_menu:
|
||||
event_system.emit("hide_path_menu")
|
||||
else:
|
||||
event_system.emit("show_path_menu")
|
||||
buttons = path_menu_buttons.get_children()
|
||||
|
||||
if len(buttons) == 1:
|
||||
self.slowed_focus(buttons[0])
|
||||
|
||||
@daemon_threaded
|
||||
def slowed_focus(self, button):
|
||||
time.sleep(0.05)
|
||||
GLib.idle_add(self.do_focused_click, *(button,))
|
||||
|
||||
def do_focused_click(self, button):
|
||||
button.grab_focus()
|
||||
button.clicked()
|
||||
|
||||
def set_path_entry(self, button = None, eve = None):
|
||||
self.path_auto_filled = True
|
||||
state = self.get_current_state()
|
||||
path = f"{state.tab.get_current_directory()}/{button.get_label()}"
|
||||
path_entry = self.builder.get_object("path_entry")
|
||||
|
||||
path_entry.set_text(path)
|
||||
path_entry.grab_focus_without_selecting()
|
||||
path_entry.set_position(-1)
|
||||
event_system.emit("hide_path_menu")
|
||||
|
||||
def show_hide_hidden_files(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
tab.set_hiding_hidden(not tab.is_hiding_hidden())
|
||||
tab.load_directory()
|
||||
self.builder.get_object("refresh_tab").released()
|
178
src/solarfm/core/widgets/files_view/window_mixin.py
Normal file
178
src/solarfm/core/widgets/files_view/window_mixin.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# Python imports
|
||||
import copy
|
||||
import traceback
|
||||
from os.path import isdir
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
from .tab_mixin import TabMixin
|
||||
|
||||
|
||||
class WindowException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class WindowMixin(TabMixin):
|
||||
"""docstring for WindowMixin"""
|
||||
|
||||
def get_fm_window(self, wid):
|
||||
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
|
||||
|
||||
def set_bottom_labels(self, tab):
|
||||
event_system.emit("set_bottom_labels", (tab,))
|
||||
|
||||
def set_window_title(self):
|
||||
wid, tid = self.fm_controller.get_active_wid_and_tid()
|
||||
notebook = self.builder.get_object(f"window_{wid}")
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
dir = tab.get_current_directory()
|
||||
|
||||
event_system.emit("unset_selected_files_views")
|
||||
ctx = self.files_view.get_style_context()
|
||||
ctx.remove_class("notebook-unselected-focus")
|
||||
ctx.add_class("notebook-selected-focus")
|
||||
|
||||
event_system.emit("set_window_title", (dir,))
|
||||
self.set_bottom_labels(tab)
|
||||
|
||||
def set_path_text(self, wid, tid):
|
||||
tab = self.get_fm_window(wid).get_tab_by_id(tid)
|
||||
event_system.emit("go_to_path", (tab.get_current_directory(),))
|
||||
|
||||
def grid_set_selected_items(self, icons_grid):
|
||||
new_items = icons_grid.get_selected_items()
|
||||
items_size = len(new_items)
|
||||
selected_items = event_system.emit_and_await("get_selected_files")
|
||||
|
||||
if items_size == 1:
|
||||
# NOTE: If already in selection, likely dnd else not so wont readd
|
||||
if new_items[0] in selected_items:
|
||||
self.dnd_left_primed += 1
|
||||
# NOTE: If in selection but trying to just select an already selected item.
|
||||
if self.dnd_left_primed > 1:
|
||||
self.dnd_left_primed = 0
|
||||
selected_items.clear()
|
||||
|
||||
# NOTE: Likely trying dnd, just readd to selection the former set.
|
||||
# Prevents losing highlighting of grid selected.
|
||||
for path in selected_items:
|
||||
icons_grid.select_path(path)
|
||||
|
||||
if items_size > 0:
|
||||
event_system.emit("set_selected_files", (new_items,))
|
||||
else:
|
||||
self.dnd_left_primed = 0
|
||||
selected_items.clear()
|
||||
|
||||
def grid_icon_single_click(self, icons_grid, eve):
|
||||
try:
|
||||
event_system.emit("hide_path_menu")
|
||||
wid, tid = icons_grid.get_name().split("|")
|
||||
self.fm_controller.set_wid_and_tid(wid, tid)
|
||||
self.set_path_text(wid, tid)
|
||||
self.set_window_title()
|
||||
|
||||
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
|
||||
if eve.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
self.dnd_left_primed = 0
|
||||
|
||||
if self.single_click_open: # FIXME: need to find a way to pass the model index
|
||||
self.grid_icon_double_click(icons_grid)
|
||||
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
|
||||
event_system.emit("show_context_menu")
|
||||
|
||||
except WindowException as e:
|
||||
logger.info(repr(e))
|
||||
self.display_message(settings.theming.error_color, f"{repr(e)}")
|
||||
|
||||
def grid_icon_double_click(self, icons_grid, item, data=None):
|
||||
try:
|
||||
if self.ctrl_down and self.shift_down:
|
||||
self.unset_keys_and_data()
|
||||
self.execute_files(in_terminal=True)
|
||||
return
|
||||
elif self.ctrl_down:
|
||||
self.unset_keys_and_data()
|
||||
self.execute_files()
|
||||
return
|
||||
|
||||
|
||||
state = self.get_current_state()
|
||||
notebook = self.builder.get_object(f"window_{state.wid}")
|
||||
tab_label = self.get_tab_label(notebook, state.icon_grid)
|
||||
|
||||
fileName = state.store[item][1]
|
||||
dir = state.tab.get_current_directory()
|
||||
file = f"{dir}/{fileName}"
|
||||
|
||||
if isdir(file):
|
||||
state.tab.set_path(file)
|
||||
state.icon_grid.clear_and_set_new_store()
|
||||
self.update_tab(tab_label, state.tab, state.icon_grid.get_store(), state.wid, state.tid)
|
||||
else:
|
||||
event_system.emit("open_files")
|
||||
except WindowException as e:
|
||||
traceback.print_exc()
|
||||
self.display_message(settings.theming.error_color, f"{repr(e)}")
|
||||
|
||||
|
||||
|
||||
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
|
||||
action = icons_grid.get_name()
|
||||
wid, tid = action.split("|")
|
||||
store = icons_grid.get_model()
|
||||
treePaths = icons_grid.get_selected_items()
|
||||
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
|
||||
# further down call chain when doing internal fm stuff.
|
||||
uris = self.format_to_uris(store, wid, tid, treePaths)
|
||||
uris_text = '\n'.join(uris)
|
||||
|
||||
data.set_uris(uris)
|
||||
data.set_text(uris_text, -1)
|
||||
|
||||
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
|
||||
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
|
||||
target = icons_grid.get_name()
|
||||
wid, tid = target.split("|")
|
||||
store = icons_grid.get_model()
|
||||
path_at_loc = None
|
||||
|
||||
try:
|
||||
data = icons_grid.get_dest_item_at_pos(x, y)
|
||||
path_at_loc = data[0]
|
||||
drop_position = data[1]
|
||||
highlighted_item_path = icons_grid.get_drag_dest_item().path
|
||||
if path_at_loc and path_at_loc == highlighted_item_path and drop_position == Gtk.IconViewDropPosition.DROP_INTO:
|
||||
uri = self.format_to_uris(store, wid, tid, highlighted_item_path)[0].replace("file://", "")
|
||||
self.override_drop_dest = uri if isdir(uri) else None
|
||||
else:
|
||||
self.override_drop_dest = None
|
||||
except Exception as e:
|
||||
self.override_drop_dest = None
|
||||
|
||||
if target not in current:
|
||||
self.fm_controller.set_wid_and_tid(wid, tid)
|
||||
|
||||
|
||||
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
|
||||
if info == 80:
|
||||
uris = data.get_uris()
|
||||
state = event_system.emit_and_await("get_current_state")
|
||||
dest = f"{state.tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
|
||||
if len(uris) == 0:
|
||||
uris = data.get_text().split("\n")
|
||||
|
||||
from_uri = '/'.join(uris[0].replace("file://", "").split("/")[:-1])
|
||||
if from_uri != dest:
|
||||
event_system.emit("move_files", (uris, dest))
|
||||
|
||||
|
||||
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
|
||||
self.create_tab(wid, None, path)
|
80
src/solarfm/core/widgets/icon_grid_widget.py
Normal file
80
src/solarfm/core/widgets/icon_grid_widget.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class IconGridWidget(Gtk.IconView):
|
||||
"""docstring for IconGridWidget"""
|
||||
|
||||
def __init__(self):
|
||||
super(IconGridWidget, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._set_up_dnd()
|
||||
self._load_widgets()
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_pixbuf_column(0)
|
||||
self.set_text_column(1)
|
||||
|
||||
self.set_item_orientation(1)
|
||||
self.set_selection_mode(3)
|
||||
self.set_item_width(96)
|
||||
self.set_item_padding(8)
|
||||
self.set_margin(12)
|
||||
self.set_row_spacing(18)
|
||||
self.set_columns(-1)
|
||||
self.set_spacing(12)
|
||||
self.set_column_spacing(18)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _setup_additional_signals(self, grid_icon_single_click,
|
||||
grid_icon_double_click,
|
||||
grid_set_selected_items,
|
||||
grid_on_drag_set,
|
||||
grid_on_drag_data_received,
|
||||
grid_on_drag_motion):
|
||||
|
||||
self.connect("button_release_event", grid_icon_single_click)
|
||||
self.connect("item-activated", grid_icon_double_click)
|
||||
self.connect("selection-changed", grid_set_selected_items)
|
||||
self.connect("drag-data-get", grid_on_drag_set)
|
||||
self.connect("drag-data-received", grid_on_drag_data_received)
|
||||
self.connect("drag-motion", grid_on_drag_motion)
|
||||
|
||||
def _load_widgets(self):
|
||||
self.clear_and_set_new_store()
|
||||
|
||||
def _set_up_dnd(self):
|
||||
URI_TARGET_TYPE = 80
|
||||
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
|
||||
targets = [ uri_target ]
|
||||
action = Gdk.DragAction.COPY
|
||||
self.enable_model_drag_dest(targets, action)
|
||||
self.enable_model_drag_source(0, targets, action)
|
||||
|
||||
|
||||
def get_store(self):
|
||||
return self.get_model()
|
||||
|
||||
def clear_and_set_new_store(self):
|
||||
self.set_model(None)
|
||||
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str or None)
|
||||
self.set_model(store)
|
83
src/solarfm/core/widgets/icon_tree_widget.py
Normal file
83
src/solarfm/core/widgets/icon_tree_widget.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class IconTreeWidget(Gtk.TreeView):
|
||||
"""docstring for IconTreeWidget"""
|
||||
|
||||
def __init__(self):
|
||||
super(IconTreeWidget, self).__init__()
|
||||
|
||||
self._store = None
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._set_up_dnd()
|
||||
self._load_widgets()
|
||||
|
||||
self.show_all()
|
||||
|
||||
def get_store(self):
|
||||
return self._store
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_search_column(1)
|
||||
self.set_rubber_banding(True)
|
||||
self.set_headers_visible(False)
|
||||
self.set_enable_tree_lines(False)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _setup_additional_signals(self, grid_icon_single_click,
|
||||
grid_icon_double_click,
|
||||
grid_on_drag_set,
|
||||
grid_on_drag_data_received,
|
||||
grid_on_drag_motion):
|
||||
|
||||
self.connect("button_release_event", self.grid_icon_single_click)
|
||||
self.connect("row-activated", self.grid_icon_double_click)
|
||||
self.connect("drag-data-get", self.grid_on_drag_set)
|
||||
self.connect("drag-data-received", self.grid_on_drag_data_received)
|
||||
self.connect("drag-motion", self.grid_on_drag_motion)
|
||||
|
||||
def _load_widgets(self):
|
||||
self._store = Gtk.TreeStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str or None)
|
||||
column = Gtk.TreeViewColumn("Icons")
|
||||
icon = Gtk.CellRendererPixbuf()
|
||||
name = Gtk.CellRendererText()
|
||||
selec = self.get_selection()
|
||||
|
||||
self.set_model(store)
|
||||
selec.set_mode(3)
|
||||
|
||||
column.pack_start(icon, False)
|
||||
column.pack_start(name, True)
|
||||
column.add_attribute(icon, "pixbuf", 0)
|
||||
column.add_attribute(name, "text", 1)
|
||||
column.set_expand(False)
|
||||
column.set_sizing(2)
|
||||
column.set_min_width(120)
|
||||
column.set_max_width(74)
|
||||
|
||||
self.append_column(column)
|
||||
|
||||
|
||||
def _set_up_dnd(self):
|
||||
URI_TARGET_TYPE = 80
|
||||
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
|
||||
targets = [ uri_target ]
|
||||
action = Gdk.DragAction.COPY
|
||||
self.enable_model_drag_dest(targets, action)
|
||||
self.enable_model_drag_source(0, targets, action)
|
83
src/solarfm/core/widgets/io_widget.py
Normal file
83
src/solarfm/core/widgets/io_widget.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class IOWidget(Gtk.Box):
|
||||
"""docstring for IOWidget"""
|
||||
|
||||
def __init__(self, action, file):
|
||||
super(IOWidget, self).__init__()
|
||||
|
||||
self._action = action
|
||||
self._file = file
|
||||
self._basename = self._file.get_basename()
|
||||
|
||||
self.cancle_eve = Gio.Cancellable.new()
|
||||
self.progress = None
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_orientation(1)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _load_widgets(self):
|
||||
stats = Gtk.Box()
|
||||
label = Gtk.Label()
|
||||
cncl_button = Gtk.Button(label="Cancel")
|
||||
del_button = Gtk.Button(label="Clear")
|
||||
self.progress = Gtk.ProgressBar()
|
||||
|
||||
label.set_label(self._basename)
|
||||
self.progress.set_show_text(True)
|
||||
self.progress.set_text(f"{self._action.upper()}ING")
|
||||
stats.set_orientation(0)
|
||||
|
||||
stats.pack_end(del_button, False, False, 5)
|
||||
del_button.connect("clicked", self.delete_self, ())
|
||||
|
||||
if not self._action in ("create", "rename"):
|
||||
stats.pack_end(cncl_button, False, False, 5)
|
||||
cncl_button.connect("clicked", self.do_cancel, *(self, self.cancle_eve))
|
||||
|
||||
stats.add(self.progress)
|
||||
self.add(label)
|
||||
self.add(stats)
|
||||
|
||||
|
||||
def do_cancel(self, widget, container, eve):
|
||||
logger.info(f"Canceling: [{self._action}] of {self._basename} ...")
|
||||
eve.cancel()
|
||||
|
||||
def update_progress(self, current, total, eve=None):
|
||||
self.progress.set_fraction(current/total)
|
||||
|
||||
def finish_callback(self, file, task=None, eve=None):
|
||||
if self._action == "move" or self._action == "rename":
|
||||
status = self._file.move_finish(task)
|
||||
if self._action == "copy":
|
||||
status = self._file.copy_finish(task)
|
||||
|
||||
if status:
|
||||
self.delete_self()
|
||||
else:
|
||||
logger.info(f"{self._action} of {self._basename} failed...")
|
||||
|
||||
def delete_self(self, widget=None, eve=None):
|
||||
self.get_parent().remove(self)
|
3
src/solarfm/core/widgets/popups/__init__.py
Normal file
3
src/solarfm/core/widgets/popups/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Popups module
|
||||
"""
|
46
src/solarfm/core/widgets/popups/io_popup_widget.py
Normal file
46
src/solarfm/core/widgets/popups/io_popup_widget.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class IOPopupWidget(Gtk.Popover):
|
||||
"""docstring for IOPopupWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(IOPopupWidget, self).__init__()
|
||||
|
||||
self._builder = settings_manager.get_builder()
|
||||
self._builder.expose_object(f"io_popup", self)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
io_button = self._builder.get_object(f"io_button")
|
||||
self.set_relative_to(io_button)
|
||||
self.set_modal(True)
|
||||
self.set_position(Gtk.PositionType.BOTTOM)
|
||||
self.set_size_request(320, 280)
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_io_popup", self.show_io_popup)
|
||||
|
||||
def _load_widgets(self):
|
||||
vbox = Gtk.Box()
|
||||
|
||||
vbox.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self._builder.expose_object(f"io_list", vbox)
|
||||
self.add(vbox)
|
||||
|
||||
|
||||
def show_io_popup(self, widget=None, eve=None):
|
||||
self.popup()
|
129
src/solarfm/core/widgets/popups/message_popup_widget.py
Normal file
129
src/solarfm/core/widgets/popups/message_popup_widget.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Python imports
|
||||
import datetime
|
||||
import inspect
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class MessagePopupWidget(Gtk.Popover):
|
||||
""" MessagePopupWidget custom exception hook viewer to reroute to log Gtk text area too. """
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super(MessagePopupWidget, self).__init__()
|
||||
|
||||
self.builder = settings_manager.get_builder()
|
||||
self.builder.expose_object(f"message_popup_widget", self)
|
||||
|
||||
self._message_buffer = None
|
||||
|
||||
sys.excepthook = self.custom_except_hook
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_relative_to( self.builder.get_object(f"main_menu_bar") )
|
||||
self.set_modal(True)
|
||||
self.set_position(Gtk.PositionType.BOTTOM)
|
||||
self.set_hexpand(True)
|
||||
self.set_vexpand(True)
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_messages_popup", self.show_messages_popup)
|
||||
event_system.subscribe("hide_messages_popup", self.hide_messages_popup)
|
||||
|
||||
def _load_widgets(self):
|
||||
self._message_buffer = Gtk.TextBuffer()
|
||||
vbox = Gtk.Box()
|
||||
scroll_window = Gtk.ScrolledWindow()
|
||||
message_text_view = Gtk.TextView.new_with_buffer(self._message_buffer)
|
||||
|
||||
button = Gtk.Button(label="Save As")
|
||||
button.set_image( Gtk.Image(stock=Gtk.STOCK_SAVE_AS) )
|
||||
button.connect("released", self.save_debug_alerts)
|
||||
button.set_always_show_image(True)
|
||||
|
||||
scroll_window.set_vexpand(True)
|
||||
scroll_window.set_hexpand(True)
|
||||
vbox.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self.builder.expose_object(f"message_popup_widget", self)
|
||||
self.builder.expose_object(f"message_text_view", message_text_view)
|
||||
|
||||
scroll_window.add(message_text_view)
|
||||
vbox.add(button)
|
||||
vbox.add(scroll_window)
|
||||
vbox.show_all()
|
||||
self.add(vbox)
|
||||
|
||||
|
||||
def show_messages_popup(self):
|
||||
self.popup()
|
||||
|
||||
def hide_messages_popup(self):
|
||||
self.popup()
|
||||
|
||||
def custom_except_hook(self, exc_type, exc_value, exc_traceback):
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
return
|
||||
|
||||
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
|
||||
self._exception_to_ui(exc_type, exc_value, exc_traceback)
|
||||
|
||||
def _exception_to_ui(self, exc_type, exc_value, exc_traceback):
|
||||
trace = ''.join(traceback.format_tb(exc_traceback))
|
||||
current_time = datetime.datetime.now()
|
||||
data = f"{current_time}\nExec Type: {exc_type} <--> Value: {exc_value}\n\n{trace}\n\n"
|
||||
|
||||
self.display_message(settings.theming.error_color, data)
|
||||
|
||||
def display_message(self, type, text, seconds=None):
|
||||
# start_itr = self._message_buffer.get_start_iter()
|
||||
start_itr = self._message_buffer.get_iter_at_line(0)
|
||||
|
||||
self._message_buffer.place_cursor(start_itr)
|
||||
self._message_buffer.insert_at_cursor(text)
|
||||
|
||||
if seconds:
|
||||
self.popup()
|
||||
self.hide_message_timeout(seconds)
|
||||
|
||||
@threaded
|
||||
def hide_message_timeout(self, seconds=3):
|
||||
time.sleep(seconds)
|
||||
GLib.idle_add(event_system.emit, ("hide_messages_popup"))
|
||||
|
||||
def save_debug_alerts(self, widget=None, eve=None):
|
||||
start_itr, end_itr = self._message_buffer.get_bounds()
|
||||
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", settings_manager.get_main_window(), \
|
||||
action = Gtk.FileChooserAction.SAVE, \
|
||||
buttons = (Gtk.STOCK_CANCEL, \
|
||||
Gtk.ResponseType.CANCEL, \
|
||||
Gtk.STOCK_SAVE, \
|
||||
Gtk.ResponseType.OK))
|
||||
|
||||
text = self._message_buffer.get_text(start_itr, end_itr, False)
|
||||
resp = save_location_prompt.run()
|
||||
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
|
||||
pass
|
||||
elif resp == Gtk.ResponseType.OK:
|
||||
target = save_location_prompt.get_filename();
|
||||
with open(target, "w") as f:
|
||||
f.write(text)
|
||||
|
||||
save_location_prompt.destroy()
|
58
src/solarfm/core/widgets/popups/path_menu_popup_widget.py
Normal file
58
src/solarfm/core/widgets/popups/path_menu_popup_widget.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class PathMenuPopupWidget(Gtk.Popover):
|
||||
"""docstring for PathMenuPopupWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(PathMenuPopupWidget, self).__init__()
|
||||
|
||||
self.builder = settings_manager.get_builder()
|
||||
self.builder.expose_object(f"path_menu", self)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
path_entry = self.builder.get_object(f"path_entry")
|
||||
self.set_relative_to(path_entry)
|
||||
self.set_modal(False)
|
||||
self.set_position(Gtk.PositionType.BOTTOM)
|
||||
self.set_size_request(240, 420)
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_path_menu", self.show_path_menu)
|
||||
event_system.subscribe("hide_path_menu", self.hide_path_menu)
|
||||
|
||||
def _load_widgets(self):
|
||||
path_menu_buttons = Gtk.ButtonBox()
|
||||
scroll_window = Gtk.ScrolledWindow()
|
||||
view_port = Gtk.Viewport()
|
||||
|
||||
scroll_window.set_vexpand(True)
|
||||
scroll_window.set_hexpand(True)
|
||||
path_menu_buttons.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self.builder.expose_object(f"path_menu_buttons", path_menu_buttons)
|
||||
view_port.add(path_menu_buttons)
|
||||
scroll_window.add(view_port)
|
||||
scroll_window.show_all()
|
||||
self.add(scroll_window)
|
||||
|
||||
|
||||
def show_path_menu(self, widget=None, eve=None):
|
||||
self.popup()
|
||||
|
||||
def hide_path_menu(self, widget=None, eve=None):
|
||||
self.popdown()
|
50
src/solarfm/core/widgets/popups/plugins_popup_widget.py
Normal file
50
src/solarfm/core/widgets/popups/plugins_popup_widget.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class PluginsPopupWidget(Gtk.Popover):
|
||||
"""docstring for PluginsPopupWidget."""
|
||||
|
||||
def __init__(self):
|
||||
super(PluginsPopupWidget, self).__init__()
|
||||
|
||||
self.builder = settings_manager.get_builder()
|
||||
self.builder.expose_object(f"plugin_controls", self)
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
plugins_button = self.builder.get_object(f"plugins_button")
|
||||
self.set_relative_to(plugins_button)
|
||||
self.set_modal(True)
|
||||
self.set_position(Gtk.PositionType.BOTTOM)
|
||||
|
||||
def _setup_signals(self):
|
||||
event_system.subscribe("show_plugins_popup", self.show_plugins_popup)
|
||||
self.connect("button-release-event", self.hide_plugins_popup)
|
||||
self.connect("key-release-event", self.hide_plugins_popup)
|
||||
|
||||
def _load_widgets(self):
|
||||
vbox = Gtk.Box()
|
||||
|
||||
vbox.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.builder.expose_object(f"plugin_control_list", vbox)
|
||||
self.add(vbox)
|
||||
|
||||
|
||||
def show_plugins_popup(self, widget=None, eve=None):
|
||||
self.popup()
|
||||
|
||||
def hide_plugins_popup(self, widget=None, eve=None):
|
||||
self.hide()
|
56
src/solarfm/core/widgets/tab_header_widget.py
Normal file
56
src/solarfm/core/widgets/tab_header_widget.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class TabHeaderWidget(Gtk.Box):
|
||||
"""docstring for TabHeaderWidget"""
|
||||
|
||||
def __init__(self, tab, close_tab):
|
||||
super(TabHeaderWidget, self).__init__()
|
||||
|
||||
self._tab = tab
|
||||
self._close_tab = close_tab # NOTE: Close method in tab_mixin
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_orientation(0)
|
||||
self.set_hexpand(False)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _load_widgets(self):
|
||||
label = Gtk.Label()
|
||||
tid = Gtk.Label()
|
||||
close = Gtk.Button()
|
||||
icon = Gtk.Image(stock=Gtk.STOCK_CLOSE)
|
||||
|
||||
label.set_label(f"{self._tab.get_end_of_path()}")
|
||||
label.set_width_chars(len(self._tab.get_end_of_path()))
|
||||
label.set_xalign(0.0)
|
||||
label.set_margin_left(25)
|
||||
label.set_margin_right(25)
|
||||
label.set_hexpand(True)
|
||||
tid.set_label(f"{self._tab.get_id()}")
|
||||
|
||||
close.connect("released", self._close_tab)
|
||||
|
||||
close.add(icon)
|
||||
self.add(label)
|
||||
self.add(close)
|
||||
self.add(tid)
|
||||
|
||||
self.show_all()
|
||||
tid.hide()
|
98
src/solarfm/core/window.py
Normal file
98
src/solarfm/core/window.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Python imports
|
||||
import time
|
||||
import signal
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
import cairo
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from core.controller import Controller
|
||||
|
||||
|
||||
|
||||
class ControllerStartException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class Window(Gtk.ApplicationWindow):
|
||||
"""docstring for Window."""
|
||||
|
||||
def __init__(self, args, unknownargs):
|
||||
super(Window, self).__init__()
|
||||
|
||||
self._controller = None
|
||||
settings_manager.set_main_window(self)
|
||||
|
||||
self._set_window_data()
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
|
||||
self._load_widgets(args, unknownargs)
|
||||
|
||||
self.show()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_default_size(settings_manager.get_main_window_width(),
|
||||
settings_manager.get_main_window_height())
|
||||
self.set_title(f"{app_name}")
|
||||
self.set_icon_from_file( settings_manager.get_window_icon() )
|
||||
self.set_gravity(5) # 5 = CENTER
|
||||
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
|
||||
|
||||
def _setup_signals(self):
|
||||
self.connect("delete-event", self._tear_down)
|
||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._tear_down)
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("tear_down", self._tear_down)
|
||||
event_system.subscribe("load_interactive_debug", self._load_interactive_debug)
|
||||
|
||||
def _load_widgets(self, args, unknownargs):
|
||||
if settings_manager.is_debug():
|
||||
self.set_interactive_debugging(True)
|
||||
|
||||
self._controller = Controller(args, unknownargs)
|
||||
|
||||
if not self._controller:
|
||||
raise ControllerStartException("Controller exited and doesn't exist...")
|
||||
|
||||
self.add( self._controller.get_core_widget() )
|
||||
|
||||
def _set_window_data(self) -> None:
|
||||
screen = self.get_screen()
|
||||
visual = screen.get_rgba_visual()
|
||||
|
||||
if visual != None and screen.is_composited():
|
||||
self.set_visual(visual)
|
||||
self.set_app_paintable(True)
|
||||
self.connect("draw", self._area_draw)
|
||||
|
||||
# bind css file
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path( settings_manager.get_css_file() )
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def _area_draw(self, widget: Gtk.ApplicationWindow, cr: cairo.Context) -> None:
|
||||
cr.set_source_rgba( *settings_manager.get_paint_bg_color() )
|
||||
cr.set_operator(cairo.OPERATOR_SOURCE)
|
||||
cr.paint()
|
||||
cr.set_operator(cairo.OPERATOR_OVER)
|
||||
|
||||
def _load_interactive_debug(self):
|
||||
self.set_interactive_debugging(True)
|
||||
|
||||
|
||||
def _tear_down(self, widget = None, eve = None):
|
||||
event_system.emit("shutting_down")
|
||||
settings_manager.clear_pid()
|
||||
Gtk.main_quit()
|
3
src/solarfm/plugins/__init__.py
Normal file
3
src/solarfm/plugins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Gtk Bound Plugins Module
|
||||
"""
|
94
src/solarfm/plugins/manifest.py
Normal file
94
src/solarfm/plugins/manifest.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# Python imports
|
||||
import os
|
||||
import json
|
||||
from os.path import join
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class ManifestProcessorException(Exception):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PluginInfo:
|
||||
path: str = None
|
||||
name: str = None
|
||||
author: str = None
|
||||
version: str = None
|
||||
support: str = None
|
||||
requests:{} = None
|
||||
reference: type = None
|
||||
|
||||
|
||||
class ManifestProcessor:
|
||||
def __init__(self, path, builder):
|
||||
manifest = join(path, "manifest.json")
|
||||
if not os.path.exists(manifest):
|
||||
raise ManifestProcessorException("Invalid Plugin Structure: Plugin doesn't have 'manifest.json'. Aboarting load...")
|
||||
|
||||
self._path = path
|
||||
self._builder = builder
|
||||
with open(manifest) as f:
|
||||
data = json.load(f)
|
||||
self._manifest = data["manifest"]
|
||||
self._plugin = self.collect_info()
|
||||
|
||||
def collect_info(self) -> PluginInfo:
|
||||
plugin = PluginInfo()
|
||||
plugin.path = self._path
|
||||
plugin.name = self._manifest["name"]
|
||||
plugin.author = self._manifest["author"]
|
||||
plugin.version = self._manifest["version"]
|
||||
plugin.support = self._manifest["support"]
|
||||
plugin.requests = self._manifest["requests"]
|
||||
|
||||
return plugin
|
||||
|
||||
def get_loading_data(self):
|
||||
loading_data = {}
|
||||
requests = self._plugin.requests
|
||||
keys = requests.keys()
|
||||
|
||||
if "ui_target" in keys:
|
||||
if requests["ui_target"] in [
|
||||
"none", "other", "main_Window", "main_menu_bar",
|
||||
"main_menu_bttn_box_bar", "path_menu_bar", "plugin_control_list",
|
||||
"context_menu", "context_menu_plugins", "window_1",
|
||||
"window_2", "window_3", "window_4"
|
||||
]:
|
||||
if requests["ui_target"] == "other":
|
||||
if "ui_target_id" in keys:
|
||||
loading_data["ui_target"] = self._builder.get_object(requests["ui_target_id"])
|
||||
if loading_data["ui_target"] == None:
|
||||
raise ManifestProcessorException('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...')
|
||||
else:
|
||||
raise ManifestProcessorException('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...')
|
||||
else:
|
||||
loading_data["ui_target"] = self._builder.get_object(requests["ui_target"])
|
||||
else:
|
||||
raise ManifestProcessorException('Unknown "ui_target" given in requests.')
|
||||
|
||||
if "pass_fm_events" in keys:
|
||||
if requests["pass_fm_events"] in ["true"]:
|
||||
loading_data["pass_fm_events"] = True
|
||||
|
||||
if "pass_ui_objects" in keys:
|
||||
if len(requests["pass_ui_objects"]) > 0:
|
||||
loading_data["pass_ui_objects"] = []
|
||||
for ui_id in requests["pass_ui_objects"]:
|
||||
try:
|
||||
loading_data["pass_ui_objects"].append( self._builder.get_object(ui_id) )
|
||||
except ManifestProcessorException as e:
|
||||
logger.error(repr(e))
|
||||
|
||||
if "bind_keys" in keys:
|
||||
if isinstance(requests["bind_keys"], list):
|
||||
loading_data["bind_keys"] = requests["bind_keys"]
|
||||
|
||||
return self._plugin, loading_data
|
96
src/solarfm/plugins/plugin_base.py
Normal file
96
src/solarfm/plugins/plugin_base.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Python imports
|
||||
import os
|
||||
import time
|
||||
import inspect
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class PluginBaseException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class PluginBase:
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.name = "Example Plugin" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self._builder = None
|
||||
self._ui_objects = None
|
||||
self._fm_state = None
|
||||
self._event_system = None
|
||||
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Must define regardless if needed and can 'pass' if plugin doesn't need it.
|
||||
Is intended to be used to setup internal signals or custom Gtk Builders/UI logic.
|
||||
"""
|
||||
raise PluginBaseException("Method hasn't been overriden...")
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
"""
|
||||
Requests Key: 'ui_target': "plugin_control_list",
|
||||
Must define regardless if needed and can 'pass' if plugin doesn't use it.
|
||||
Must return a widget if "ui_target" is set.
|
||||
"""
|
||||
raise PluginBaseException("Method hasn't been overriden...")
|
||||
|
||||
def set_ui_object_collection(self, ui_objects):
|
||||
"""
|
||||
Requests Key: "pass_ui_objects": [""]
|
||||
Request reference to a UI component. Will be passed back as array to plugin.
|
||||
Must define in plugin if set and an array of valid glade UI IDs is given.
|
||||
"""
|
||||
self._ui_objects = ui_objects
|
||||
|
||||
def set_fm_event_system(self, fm_event_system):
|
||||
"""
|
||||
Requests Key: 'pass_fm_events': "true"
|
||||
Must define in plugin if "pass_fm_events" is set to "true" string.
|
||||
"""
|
||||
self._event_system = fm_event_system
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("update_state_info_plugins", self._update_fm_state_info)
|
||||
|
||||
def _update_fm_state_info(self, state):
|
||||
self._fm_state = state
|
||||
|
||||
def _connect_builder_signals(self, caller_class, builder):
|
||||
classes = [caller_class]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
logger.debug(repr(e))
|
||||
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
def reload_package(self, plugin_path, module_dict_main=locals()):
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
def reload_package_recursive(current_dir, module_dict):
|
||||
for path in current_dir.iterdir():
|
||||
if "__init__" in str(path) or path.stem not in module_dict:
|
||||
continue
|
||||
|
||||
if path.is_file() and path.suffix == ".py":
|
||||
importlib.reload(module_dict[path.stem])
|
||||
elif path.is_dir():
|
||||
reload_package_recursive(path, module_dict[path.stem].__dict__)
|
||||
|
||||
reload_package_recursive(Path(plugin_path).parent, module_dict_main["module_dict_main"])
|
||||
|
||||
|
||||
def clear_children(self, widget: type) -> None:
|
||||
""" Clear children of a gtk widget. """
|
||||
for child in widget.get_children():
|
||||
widget.remove(child)
|
130
src/solarfm/plugins/plugins_controller.py
Normal file
130
src/solarfm/plugins/plugins_controller.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Python imports
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import traceback
|
||||
from os.path import join
|
||||
from os.path import isdir
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
from .manifest import PluginInfo
|
||||
from .manifest import ManifestProcessor
|
||||
|
||||
|
||||
|
||||
|
||||
class InvalidPluginException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class PluginsController:
|
||||
"""PluginsController controller"""
|
||||
|
||||
def __init__(self):
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, path) # NOTE: I think I'm not using this correctly...
|
||||
|
||||
self._builder = settings_manager.get_builder()
|
||||
self._plugins_path = settings_manager.get_plugins_path()
|
||||
|
||||
self._plugins_dir_watcher = None
|
||||
self._plugin_collection = []
|
||||
|
||||
|
||||
def launch_plugins(self) -> None:
|
||||
self._set_plugins_watcher()
|
||||
self.load_plugins()
|
||||
|
||||
def _set_plugins_watcher(self) -> None:
|
||||
self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \
|
||||
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
|
||||
self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ())
|
||||
|
||||
def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None):
|
||||
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
|
||||
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
|
||||
Gio.FileMonitorEvent.MOVED_OUT]:
|
||||
self.reload_plugins(file)
|
||||
|
||||
@daemon_threaded
|
||||
def load_plugins(self, file: str = None) -> None:
|
||||
logger.info(f"Loading plugins...")
|
||||
parent_path = os.getcwd()
|
||||
|
||||
for path, folder in [[join(self._plugins_path, item), item] if os.path.isdir(join(self._plugins_path, item)) else None for item in os.listdir(self._plugins_path)]:
|
||||
try:
|
||||
target = join(path, "plugin.py")
|
||||
manifest = ManifestProcessor(path, self._builder)
|
||||
|
||||
if not os.path.exists(target):
|
||||
raise FileNotFoundError("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...")
|
||||
|
||||
plugin, loading_data = manifest.get_loading_data()
|
||||
module = self.load_plugin_module(path, folder, target)
|
||||
|
||||
GLib.idle_add(self.execute_plugin, *(module, plugin, loading_data))
|
||||
# self.execute_plugin(module, plugin, loading_data)
|
||||
except InvalidPluginException as e:
|
||||
logger.info(f"Malformed Plugin: Not loading -->: '{folder}' !")
|
||||
logger.debug("Trace: ", traceback.print_exc())
|
||||
|
||||
os.chdir(parent_path)
|
||||
|
||||
|
||||
def load_plugin_module(self, path, folder, target):
|
||||
os.chdir(path)
|
||||
|
||||
locations = []
|
||||
self.collect_search_locations(path, locations)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(folder, target, submodule_search_locations = locations)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[folder] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
return module
|
||||
|
||||
def collect_search_locations(self, path, locations):
|
||||
locations.append(path)
|
||||
for file in os.listdir(path):
|
||||
_path = os.path.join(path, file)
|
||||
if os.path.isdir(_path):
|
||||
self.collect_search_locations(_path, locations)
|
||||
|
||||
def execute_plugin(self, module: type, plugin: PluginInfo, loading_data: []):
|
||||
plugin.reference = module.Plugin()
|
||||
keys = loading_data.keys()
|
||||
|
||||
if "ui_target" in keys:
|
||||
loading_data["ui_target"].add( plugin.reference.generate_reference_ui_element() )
|
||||
loading_data["ui_target"].show_all()
|
||||
|
||||
if "pass_ui_objects" in keys:
|
||||
plugin.reference.set_ui_object_collection( loading_data["pass_ui_objects"] )
|
||||
|
||||
if "pass_fm_events" in keys:
|
||||
plugin.reference.set_fm_event_system(event_system)
|
||||
plugin.reference.subscribe_to_events()
|
||||
|
||||
if "bind_keys" in keys:
|
||||
keybindings.append_bindings( loading_data["bind_keys"] )
|
||||
|
||||
plugin.reference.run()
|
||||
self._plugin_collection.append(plugin)
|
||||
|
||||
def reload_plugins(self, file: str = None) -> None:
|
||||
logger.info(f"Reloading plugins...")
|
||||
parent_path = os.getcwd()
|
||||
|
||||
for plugin in self._plugin_collection:
|
||||
os.chdir(plugin.path)
|
||||
plugin.reference.reload_package(f"{plugin.path}/plugin.py")
|
||||
|
||||
os.chdir(parent_path)
|
3
src/solarfm/shellfm/__init__.py
Normal file
3
src/solarfm/shellfm/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Root of ShellFM
|
||||
"""
|
3
src/solarfm/shellfm/windows/__init__.py
Normal file
3
src/solarfm/shellfm/windows/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Window module
|
||||
"""
|
180
src/solarfm/shellfm/windows/controller.py
Normal file
180
src/solarfm/shellfm/windows/controller.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# Python imports
|
||||
import threading
|
||||
import json
|
||||
from os import path
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .window import Window
|
||||
|
||||
|
||||
|
||||
|
||||
class WindowController:
|
||||
def __init__(self):
|
||||
USER_HOME: str = path.expanduser('~')
|
||||
CONFIG_PATH: str = f"{USER_HOME}/.config/solarfm"
|
||||
self._session_file: srr = f"{CONFIG_PATH}/session.json"
|
||||
|
||||
self._event_sleep_time: int = 1
|
||||
self._active_window_id: str = ""
|
||||
self._active_tab_id: str = ""
|
||||
self._windows: list = []
|
||||
|
||||
|
||||
def set_wid_and_tid(self, wid: int, tid: int) -> None:
|
||||
self._active_window_id = str(wid)
|
||||
self._active_tab_id = str(tid)
|
||||
|
||||
def get_active_wid_and_tid(self) -> list:
|
||||
return self._active_window_id, self._active_tab_id
|
||||
|
||||
def create_window(self) -> Window:
|
||||
window = Window()
|
||||
window.set_nickname(f"window_{str(len(self._windows) + 1)}")
|
||||
self._windows.append(window)
|
||||
return window
|
||||
|
||||
|
||||
def add_tab_for_window(self, win_id: str) -> None:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window.create_tab()
|
||||
|
||||
def add_tab_for_window_by_name(self, name: str) -> None:
|
||||
for window in self._windows:
|
||||
if window.get_name() == name:
|
||||
return window.create_tab()
|
||||
|
||||
def add_tab_for_window_by_nickname(self, nickname: str) -> None:
|
||||
for window in self._windows:
|
||||
if window.get_nickname() == nickname:
|
||||
return window.create_tab()
|
||||
|
||||
def pop_window(self) -> None:
|
||||
self._windows.pop()
|
||||
|
||||
def delete_window_by_id(self, win_id: str) -> None:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
self._windows.remove(window)
|
||||
break
|
||||
|
||||
def delete_window_by_name(self, name: str) -> str:
|
||||
for window in self._windows:
|
||||
if window.get_name() == name:
|
||||
self._windows.remove(window)
|
||||
break
|
||||
|
||||
def delete_window_by_nickname(self, nickname: str) -> str:
|
||||
for window in self._windows:
|
||||
if window.get_nickname() == nickname:
|
||||
self._windows.remove(window)
|
||||
break
|
||||
|
||||
def get_window_by_id(self, win_id: str) -> Window:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window
|
||||
|
||||
raise(f"No Window by ID {win_id} found!")
|
||||
|
||||
def get_window_by_name(self, name: str) -> Window:
|
||||
for window in self._windows:
|
||||
if window.get_name() == name:
|
||||
return window
|
||||
|
||||
raise(f"No Window by Name {name} found!")
|
||||
|
||||
def get_window_by_nickname(self, nickname: str) -> Window:
|
||||
for window in self._windows:
|
||||
if window.get_nickname() == nickname:
|
||||
return window
|
||||
|
||||
raise(f"No Window by Nickname {nickname} found!")
|
||||
|
||||
def get_window_by_index(self, index: int) -> Window:
|
||||
return self._windows[index]
|
||||
|
||||
def get_all_windows(self) -> list:
|
||||
return self._windows
|
||||
|
||||
|
||||
def set_window_nickname(self, win_id: str = None, nickname: str = "") -> None:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
window.set_nickname(nickname)
|
||||
|
||||
def list_windows(self) -> None:
|
||||
print("\n[ ---- Windows ---- ]\n")
|
||||
for window in self._windows:
|
||||
print(f"\nID: {window.get_id()}")
|
||||
print(f"Name: {window.get_name()}")
|
||||
print(f"Nickname: {window.get_nickname()}")
|
||||
print(f"Is Hidden: {window.is_hidden()}")
|
||||
print(f"Tab Count: {window.get_tabs_count()}")
|
||||
print("\n-------------------------\n")
|
||||
|
||||
|
||||
|
||||
def list_files_from_tabs_of_window(self, win_id: str) -> None:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
window.list_files_from_tabs()
|
||||
break
|
||||
|
||||
def get_tabs_count(self, win_id: str) -> int:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window.get_tabs_count()
|
||||
|
||||
def get_tabs_from_window(self, win_id: str) -> list:
|
||||
for window in self._windows:
|
||||
if window.get_id() == win_id:
|
||||
return window.get_all_tabs()
|
||||
|
||||
|
||||
|
||||
|
||||
def unload_tabs_and_windows(self) -> None:
|
||||
for window in self._windows:
|
||||
window.get_all_tabs().clear()
|
||||
|
||||
self._windows.clear()
|
||||
|
||||
def save_state(self, session_file: str = None) -> None:
|
||||
if not session_file:
|
||||
session_file = self._session_file
|
||||
|
||||
if len(self._windows) > 0:
|
||||
windows = []
|
||||
for window in self._windows:
|
||||
tabs = []
|
||||
for tab in window.get_all_tabs():
|
||||
tabs.append(tab.get_current_directory())
|
||||
|
||||
windows.append(
|
||||
{
|
||||
'window':{
|
||||
"ID": window.get_id(),
|
||||
"Name": window.get_name(),
|
||||
"Nickname": window.get_nickname(),
|
||||
"isHidden": f"{window.is_hidden()}",
|
||||
'tabs': tabs
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
with open(session_file, 'w') as outfile:
|
||||
json.dump(windows, outfile, separators=(',', ':'), indent=4)
|
||||
else:
|
||||
raise Exception("Window data corrupted! Can not save session!")
|
||||
|
||||
def get_state_from_file(self, session_file: str = None) -> dict:
|
||||
if not session_file:
|
||||
session_file = self._session_file
|
||||
|
||||
if path.isfile(session_file):
|
||||
with open(session_file) as infile:
|
||||
return json.load(infile)
|
3
src/solarfm/shellfm/windows/tabs/__init__.py
Normal file
3
src/solarfm/shellfm/windows/tabs/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Tabs module
|
||||
"""
|
3
src/solarfm/shellfm/windows/tabs/icons/__init__.py
Normal file
3
src/solarfm/shellfm/windows/tabs/icons/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Icons module
|
||||
"""
|
204
src/solarfm/shellfm/windows/tabs/icons/icon.py
Normal file
204
src/solarfm/shellfm/windows/tabs/icons/icon.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# Python imports
|
||||
import os
|
||||
from os.path import isfile
|
||||
import hashlib
|
||||
import threading
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
try:
|
||||
from PIL import Image as PImage
|
||||
except ModuleNotFoundError as e:
|
||||
PImage = None
|
||||
|
||||
# Application imports
|
||||
from .mixins.videoiconmixin import VideoIconMixin
|
||||
from .mixins.meshsiconmixin import MeshsIconMixin
|
||||
from .mixins.desktopiconmixin import DesktopIconMixin
|
||||
|
||||
|
||||
|
||||
class IconException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin):
|
||||
def create_icon(self, dir, file):
|
||||
full_path = f"{dir}/{file}"
|
||||
return self.get_icon_image(dir, file, full_path)
|
||||
|
||||
def get_icon_image(self, dir, file, full_path):
|
||||
try:
|
||||
thumbnl = None
|
||||
|
||||
if file.lower().endswith(self.fmeshs): # 3D Mesh icon
|
||||
...
|
||||
elif file.lower().endswith(self.fvideos): # Video icon
|
||||
thumbnl = self.create_video_thumbnail(full_path)
|
||||
elif file.lower().endswith(self.fimages): # Image Icon
|
||||
thumbnl = self.create_scaled_image(full_path)
|
||||
elif file.lower().endswith( (".blend",) ): # Blender icon
|
||||
thumbnl = self.create_blender_thumbnail(full_path)
|
||||
elif full_path.lower().endswith( ('.desktop',) ): # .desktop file parsing
|
||||
thumbnl = self.find_thumbnail_from_desktop_file(full_path)
|
||||
|
||||
if not thumbnl:
|
||||
# TODO: Detect if not in a thread and use directly for speed get_system_thumbnail
|
||||
thumbnl = self.get_system_thumbnail(full_path, self.sys_icon_wh[0])
|
||||
# thumbnl = self._get_system_thumbnail_gtk_thread(full_path, self.sys_icon_wh[0])
|
||||
if not thumbnl:
|
||||
raise IconException("No known icons found.")
|
||||
|
||||
|
||||
return thumbnl
|
||||
except IconException:
|
||||
...
|
||||
|
||||
return self.get_generic_icon()
|
||||
|
||||
def create_blender_thumbnail(self, full_path, returnHashInstead=False):
|
||||
try:
|
||||
path_exists, img_hash, hash_img_path = self.generate_hash_and_path(full_path)
|
||||
if not path_exists:
|
||||
self.generate_blender_thumbnail(full_path, hash_img_path)
|
||||
|
||||
if returnHashInstead:
|
||||
return img_hash, hash_img_path
|
||||
|
||||
return self.create_scaled_image(hash_img_path, self.video_icon_wh)
|
||||
except IconException as e:
|
||||
print("Blender thumbnail generation issue:")
|
||||
print( repr(e) )
|
||||
|
||||
return None
|
||||
|
||||
def create_video_thumbnail(self, full_path, scrub_percent = "65%", replace=False, returnHashInstead=False):
|
||||
try:
|
||||
path_exists, img_hash, hash_img_path = self.generate_hash_and_path(full_path)
|
||||
if path_exists and replace:
|
||||
os.remove(hash_img_path)
|
||||
path_exists = False
|
||||
|
||||
if not path_exists:
|
||||
self.generate_video_thumbnail(full_path, hash_img_path, scrub_percent)
|
||||
|
||||
if returnHashInstead:
|
||||
return img_hash, hash_img_path
|
||||
|
||||
return self.create_scaled_image(hash_img_path, self.video_icon_wh)
|
||||
except IconException as e:
|
||||
print("Image/Video thumbnail generation issue:")
|
||||
print( repr(e) )
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def create_scaled_image(self, full_path, wxh = None):
|
||||
if not wxh:
|
||||
wxh = self.video_icon_wh
|
||||
|
||||
if full_path:
|
||||
try:
|
||||
if full_path.lower().endswith(".gif"):
|
||||
return GdkPixbuf.PixbufAnimation.new_from_file(full_path) \
|
||||
.get_static_image() \
|
||||
.scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR)
|
||||
elif full_path.lower().endswith(".webp") and PImage:
|
||||
return self.image2pixbuf(full_path, wxh)
|
||||
|
||||
pixbuf = None
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(full_path, wxh[0], wxh[1], True)
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
return pixbuf
|
||||
except IconException as e:
|
||||
print("Image Scaling Issue:")
|
||||
print( repr(e) )
|
||||
|
||||
return None
|
||||
|
||||
def create_from_file(self, full_path):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file(full_path)
|
||||
except IconException as e:
|
||||
print("Image from file Issue:")
|
||||
print( repr(e) )
|
||||
|
||||
return None
|
||||
|
||||
def _get_system_thumbnail_gtk_thread(self, full_path, size):
|
||||
def _call_gtk_thread(event, result):
|
||||
result.append( self.get_system_thumbnail(full_path, size) )
|
||||
event.set()
|
||||
|
||||
result = []
|
||||
event = threading.Event()
|
||||
GLib.idle_add(_call_gtk_thread, event, result)
|
||||
event.wait()
|
||||
return result[0]
|
||||
|
||||
|
||||
def get_system_thumbnail(self, full_path, size):
|
||||
try:
|
||||
gio_file = Gio.File.new_for_path(full_path)
|
||||
info = gio_file.query_info('standard::icon' , 0, None)
|
||||
icon = info.get_icon().get_names()[0]
|
||||
data = settings_manager.get_icon_theme().lookup_icon(icon , size , 0)
|
||||
|
||||
if data:
|
||||
icon_path = data.get_filename()
|
||||
return GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
||||
|
||||
raise IconException("No system icon found...")
|
||||
except IconException:
|
||||
...
|
||||
|
||||
return None
|
||||
|
||||
def get_generic_icon(self):
|
||||
return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICON)
|
||||
|
||||
def generate_hash_and_path(self, full_path):
|
||||
img_hash = self.fast_hash(full_path)
|
||||
hash_img_path = f"{self.ABS_THUMBS_PTH}/{img_hash}.jpg"
|
||||
path_exists = True if isfile(hash_img_path) else False
|
||||
|
||||
return path_exists, img_hash, hash_img_path
|
||||
|
||||
|
||||
def fast_hash(self, filename: str, hash_factory: callable = hashlib.md5, chunk_num_blocks: int = 128, i: int = 1) -> str:
|
||||
h = hash_factory()
|
||||
with open(filename,'rb') as f:
|
||||
# NOTE: Jump to middle of file
|
||||
f.seek(0, 2)
|
||||
mid = int(f.tell() / 2)
|
||||
f.seek(mid, 0)
|
||||
|
||||
while chunk := f.read(chunk_num_blocks * h.block_size):
|
||||
h.update(chunk)
|
||||
if (i == 12):
|
||||
break
|
||||
|
||||
i += 1
|
||||
|
||||
return h.hexdigest()
|
||||
|
||||
def image2pixbuf(self, full_path, wxh):
|
||||
"""Convert Pillow image to GdkPixbuf"""
|
||||
im = PImage.open(full_path)
|
||||
data = im.tobytes()
|
||||
data = GLib.Bytes.new(data)
|
||||
w, h = im.size
|
||||
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
|
||||
False, 8, w, h, w * 3)
|
||||
|
||||
return pixbuf.scale_simple(wxh[0], wxh[1], 2) # BILINEAR = 2
|
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Icons mixins module
|
||||
"""
|
@@ -0,0 +1,76 @@
|
||||
# Python imports
|
||||
import os
|
||||
from os.path import isfile
|
||||
import subprocess
|
||||
import hashlib
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
from .xdg.DesktopEntry import DesktopEntry
|
||||
|
||||
|
||||
|
||||
|
||||
class DesktopIconMixin:
|
||||
def find_thumbnail_from_desktop_file(self, full_path):
|
||||
try:
|
||||
xdgObj = DesktopEntry(full_path)
|
||||
icon = xdgObj.getIcon()
|
||||
alt_icon_path = ""
|
||||
|
||||
if "steam" in icon:
|
||||
return self.get_steam_img(xdgObj)
|
||||
elif os.path.exists(icon):
|
||||
return self.create_scaled_image(icon, self.sys_icon_wh)
|
||||
else:
|
||||
pixbuf, alt_icon_path = self.get_icon_from_traversal(icon)
|
||||
if pixbuf:
|
||||
return pixbuf
|
||||
|
||||
return self.create_scaled_image(alt_icon_path, self.sys_icon_wh)
|
||||
except Exception as e:
|
||||
print(".desktop icon generation issue:")
|
||||
print( repr(e) )
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_steam_img(self, xdgObj):
|
||||
name = xdgObj.getName()
|
||||
file_hash = hashlib.sha256(str.encode(name)).hexdigest()
|
||||
hash_img_pth = f"{self.STEAM_ICONS_PTH}/{file_hash}.jpg"
|
||||
|
||||
if not isfile(hash_img_pth):
|
||||
exec_str = xdgObj.getExec()
|
||||
parts = exec_str.split("steam://rungameid/")
|
||||
id = parts[len(parts) - 1]
|
||||
imageLink = f"{self.STEAM_CDN_URL}{id}/header.jpg"
|
||||
proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink])
|
||||
proc.wait()
|
||||
|
||||
return self.create_scaled_image(hash_img_pth, self.video_icon_wh)
|
||||
|
||||
def get_icon_from_traversal(self, icon):
|
||||
gio_icon = Gio.Icon.new_for_string(icon)
|
||||
gicon = Gtk.Image.new_from_gicon(gio_icon, 32)
|
||||
pixbuf = gicon.get_pixbuf()
|
||||
|
||||
alt_icon_path = ""
|
||||
for dir in self.ICON_DIRS:
|
||||
alt_icon_path = self.traverse_icons_folder(dir, icon)
|
||||
if alt_icon_path != "":
|
||||
break
|
||||
|
||||
return pixbuf, alt_icon_path
|
||||
|
||||
def traverse_icons_folder(self, path, icon):
|
||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||
for file in filenames:
|
||||
appNM = "application-x-" + icon
|
||||
if icon in file or appNM in file:
|
||||
return f"{dirpath}/{file}"
|
@@ -0,0 +1,17 @@
|
||||
# Python imports
|
||||
import subprocess
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class MeshsIconMixin:
|
||||
def generate_blender_thumbnail(self, full_path, hash_img_path):
|
||||
try:
|
||||
proc = subprocess.Popen([self.BLENDER_THUMBNLR, full_path, hash_img_path])
|
||||
proc.wait()
|
||||
except Exception as e:
|
||||
self.logger.debug(repr(e))
|
@@ -0,0 +1,55 @@
|
||||
# Python imports
|
||||
import subprocess
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class VideoIconMixin:
|
||||
def generate_video_thumbnail(self, full_path, hash_img_path, scrub_percent = "65%"):
|
||||
try:
|
||||
proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", scrub_percent, "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_path])
|
||||
proc.wait()
|
||||
except Exception as e:
|
||||
self.logger.debug(repr(e))
|
||||
self.ffprobe_generate_video_thumbnail(full_path, hash_img_path)
|
||||
|
||||
|
||||
def ffprobe_generate_video_thumbnail(self, full_path, hash_img_path):
|
||||
proc = None
|
||||
try:
|
||||
# Stream duration
|
||||
command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||
data = subprocess.run(command, stdout=subprocess.PIPE)
|
||||
duration = data.stdout.decode('utf-8')
|
||||
|
||||
# Format (container) duration
|
||||
if "N/A" in duration:
|
||||
command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||
data = subprocess.run(command , stdout=subprocess.PIPE)
|
||||
duration = data.stdout.decode('utf-8')
|
||||
|
||||
# Stream duration type: image2
|
||||
if "N/A" in duration:
|
||||
command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||
data = subprocess.run(command, stdout=subprocess.PIPE)
|
||||
duration = data.stdout.decode('utf-8')
|
||||
|
||||
# Format (container) duration type: image2
|
||||
if "N/A" in duration:
|
||||
command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||
data = subprocess.run(command , stdout=subprocess.PIPE)
|
||||
duration = data.stdout.decode('utf-8')
|
||||
|
||||
# Get frame roughly 35% through video
|
||||
grabTime = str( int( float( duration.split(".")[0] ) * 0.35) )
|
||||
command = ["ffmpeg", "-ss", grabTime, "-an", "-i", full_path, "-s", "320x180", "-vframes", "1", hash_img_path]
|
||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||
proc.wait()
|
||||
except Exception as e:
|
||||
print("Video thumbnail generation issue in thread:")
|
||||
print( repr(e) )
|
||||
self.logger.debug(repr(e))
|
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
This module is based on a rox module (LGPL):
|
||||
|
||||
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log
|
||||
|
||||
The freedesktop.org Base Directory specification provides a way for
|
||||
applications to locate shared data and configuration:
|
||||
|
||||
http://standards.freedesktop.org/basedir-spec/
|
||||
|
||||
(based on version 0.6)
|
||||
|
||||
This module can be used to load and save from and to these directories.
|
||||
|
||||
Typical usage:
|
||||
|
||||
from rox import basedir
|
||||
|
||||
for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'):
|
||||
print "Load settings from", dir
|
||||
|
||||
dir = basedir.save_config_path('mydomain.org', 'MyProg')
|
||||
print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2"
|
||||
|
||||
Note: see the rox.Options module for a higher-level API for managing options.
|
||||
"""
|
||||
|
||||
import os, stat
|
||||
|
||||
_home = os.path.expanduser('~')
|
||||
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
|
||||
os.path.join(_home, '.local', 'share')
|
||||
|
||||
xdg_data_dirs = [xdg_data_home] + \
|
||||
(os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':')
|
||||
|
||||
xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \
|
||||
os.path.join(_home, '.config')
|
||||
|
||||
xdg_config_dirs = [xdg_config_home] + \
|
||||
(os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':')
|
||||
|
||||
xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \
|
||||
os.path.join(_home, '.cache')
|
||||
|
||||
xdg_data_dirs = [x for x in xdg_data_dirs if x]
|
||||
xdg_config_dirs = [x for x in xdg_config_dirs if x]
|
||||
|
||||
def save_config_path(*resource):
|
||||
"""Ensure ``$XDG_CONFIG_HOME/<resource>/`` exists, and return its path.
|
||||
'resource' should normally be the name of your application. Use this
|
||||
when saving configuration settings.
|
||||
"""
|
||||
resource = os.path.join(*resource)
|
||||
assert not resource.startswith('/')
|
||||
path = os.path.join(xdg_config_home, resource)
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path, 0o700)
|
||||
return path
|
||||
|
||||
def save_data_path(*resource):
|
||||
"""Ensure ``$XDG_DATA_HOME/<resource>/`` exists, and return its path.
|
||||
'resource' should normally be the name of your application or a shared
|
||||
resource. Use this when saving or updating application data.
|
||||
"""
|
||||
resource = os.path.join(*resource)
|
||||
assert not resource.startswith('/')
|
||||
path = os.path.join(xdg_data_home, resource)
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
def save_cache_path(*resource):
|
||||
"""Ensure ``$XDG_CACHE_HOME/<resource>/`` exists, and return its path.
|
||||
'resource' should normally be the name of your application or a shared
|
||||
resource."""
|
||||
resource = os.path.join(*resource)
|
||||
assert not resource.startswith('/')
|
||||
path = os.path.join(xdg_cache_home, resource)
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
def load_config_paths(*resource):
|
||||
"""Returns an iterator which gives each directory named 'resource' in the
|
||||
configuration search path. Information provided by earlier directories should
|
||||
take precedence over later ones, and the user-specific config dir comes
|
||||
first."""
|
||||
resource = os.path.join(*resource)
|
||||
for config_dir in xdg_config_dirs:
|
||||
path = os.path.join(config_dir, resource)
|
||||
if os.path.exists(path): yield path
|
||||
|
||||
def load_first_config(*resource):
|
||||
"""Returns the first result from load_config_paths, or None if there is nothing
|
||||
to load."""
|
||||
for x in load_config_paths(*resource):
|
||||
return x
|
||||
return None
|
||||
|
||||
def load_data_paths(*resource):
|
||||
"""Returns an iterator which gives each directory named 'resource' in the
|
||||
application data search path. Information provided by earlier directories
|
||||
should take precedence over later ones."""
|
||||
resource = os.path.join(*resource)
|
||||
for data_dir in xdg_data_dirs:
|
||||
path = os.path.join(data_dir, resource)
|
||||
if os.path.exists(path): yield path
|
||||
|
||||
def get_runtime_dir(strict=True):
|
||||
"""Returns the value of $XDG_RUNTIME_DIR, a directory path.
|
||||
|
||||
This directory is intended for 'user-specific non-essential runtime files
|
||||
and other file objects (such as sockets, named pipes, ...)', and
|
||||
'communication and synchronization purposes'.
|
||||
|
||||
As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not
|
||||
set, with ``strict=True`` (the default), a KeyError is raised. With
|
||||
``strict=False``, PyXDG will create a fallback under /tmp for the current
|
||||
user. This fallback does *not* provide the same guarantees as the
|
||||
specification requires for the runtime directory.
|
||||
|
||||
The strict default is deliberately conservative, so that application
|
||||
developers can make a conscious decision to allow the fallback.
|
||||
"""
|
||||
try:
|
||||
return os.environ['XDG_RUNTIME_DIR']
|
||||
except KeyError:
|
||||
if strict:
|
||||
raise
|
||||
|
||||
import getpass
|
||||
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
|
||||
create = False
|
||||
|
||||
try:
|
||||
# This must be a real directory, not a symlink, so attackers can't
|
||||
# point it elsewhere. So we use lstat to check it.
|
||||
st = os.lstat(fallback)
|
||||
except OSError as e:
|
||||
import errno
|
||||
if e.errno == errno.ENOENT:
|
||||
create = True
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# The fallback must be a directory
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
os.unlink(fallback)
|
||||
create = True
|
||||
# Must be owned by the user and not accessible by anyone else
|
||||
elif (st.st_uid != os.getuid()) \
|
||||
or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
|
||||
os.rmdir(fallback)
|
||||
create = True
|
||||
|
||||
if create:
|
||||
os.mkdir(fallback, 0o700)
|
||||
|
||||
return fallback
|
39
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Config.py
Normal file
39
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Config.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Functions to configure Basic Settings
|
||||
"""
|
||||
|
||||
language = "C"
|
||||
windowmanager = None
|
||||
icon_theme = "hicolor"
|
||||
icon_size = 48
|
||||
cache_time = 5
|
||||
root_mode = False
|
||||
|
||||
def setWindowManager(wm):
|
||||
global windowmanager
|
||||
windowmanager = wm
|
||||
|
||||
def setIconTheme(theme):
|
||||
global icon_theme
|
||||
icon_theme = theme
|
||||
import xdg.IconTheme
|
||||
xdg.IconTheme.themes = []
|
||||
|
||||
def setIconSize(size):
|
||||
global icon_size
|
||||
icon_size = size
|
||||
|
||||
def setCacheTime(time):
|
||||
global cache_time
|
||||
cache_time = time
|
||||
|
||||
def setLocale(lang):
|
||||
import locale
|
||||
lang = locale.normalize(lang)
|
||||
locale.setlocale(locale.LC_ALL, lang)
|
||||
import xdg.Locale
|
||||
xdg.Locale.update(lang)
|
||||
|
||||
def setRootMode(boolean):
|
||||
global root_mode
|
||||
root_mode = boolean
|
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
Complete implementation of the XDG Desktop Entry Specification
|
||||
http://standards.freedesktop.org/desktop-entry-spec/
|
||||
|
||||
Not supported:
|
||||
- Encoding: Legacy Mixed
|
||||
- Does not check exec parameters
|
||||
- Does not check URL's
|
||||
- Does not completly validate deprecated/kde items
|
||||
- Does not completly check categories
|
||||
"""
|
||||
|
||||
from .IniFile import IniFile
|
||||
from . import Locale
|
||||
|
||||
from .IniFile import is_ascii
|
||||
|
||||
from .Exceptions import ParsingError
|
||||
from .util import which
|
||||
import os.path
|
||||
import re
|
||||
import warnings
|
||||
|
||||
class DesktopEntry(IniFile):
|
||||
"Class to parse and validate Desktop Entries"
|
||||
|
||||
defaultGroup = 'Desktop Entry'
|
||||
|
||||
def __init__(self, filename=None):
|
||||
"""Create a new DesktopEntry.
|
||||
|
||||
If filename exists, it will be parsed as a desktop entry file. If not,
|
||||
or if filename is None, a blank DesktopEntry is created.
|
||||
"""
|
||||
self.content = dict()
|
||||
if filename and os.path.exists(filename):
|
||||
self.parse(filename)
|
||||
elif filename:
|
||||
self.new(filename)
|
||||
|
||||
def __str__(self):
|
||||
return self.getName()
|
||||
|
||||
def parse(self, file):
|
||||
"""Parse a desktop entry file.
|
||||
|
||||
This can raise :class:`~xdg.Exceptions.ParsingError`,
|
||||
:class:`~xdg.Exceptions.DuplicateGroupError` or
|
||||
:class:`~xdg.Exceptions.DuplicateKeyError`.
|
||||
"""
|
||||
IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
|
||||
|
||||
def findTryExec(self):
|
||||
"""Looks in the PATH for the executable given in the TryExec field.
|
||||
|
||||
Returns the full path to the executable if it is found, None if not.
|
||||
Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present.
|
||||
"""
|
||||
tryexec = self.get('TryExec', strict=True)
|
||||
return which(tryexec)
|
||||
|
||||
# start standard keys
|
||||
def getType(self):
|
||||
return self.get('Type')
|
||||
def getVersion(self):
|
||||
"""deprecated, use getVersionString instead """
|
||||
return self.get('Version', type="numeric")
|
||||
def getVersionString(self):
|
||||
return self.get('Version')
|
||||
def getName(self):
|
||||
return self.get('Name', locale=True)
|
||||
def getGenericName(self):
|
||||
return self.get('GenericName', locale=True)
|
||||
def getNoDisplay(self):
|
||||
return self.get('NoDisplay', type="boolean")
|
||||
def getComment(self):
|
||||
return self.get('Comment', locale=True)
|
||||
def getIcon(self):
|
||||
return self.get('Icon', locale=True)
|
||||
def getHidden(self):
|
||||
return self.get('Hidden', type="boolean")
|
||||
def getOnlyShowIn(self):
|
||||
return self.get('OnlyShowIn', list=True)
|
||||
def getNotShowIn(self):
|
||||
return self.get('NotShowIn', list=True)
|
||||
def getTryExec(self):
|
||||
return self.get('TryExec')
|
||||
def getExec(self):
|
||||
return self.get('Exec')
|
||||
def getPath(self):
|
||||
return self.get('Path')
|
||||
def getTerminal(self):
|
||||
return self.get('Terminal', type="boolean")
|
||||
def getMimeType(self):
|
||||
"""deprecated, use getMimeTypes instead """
|
||||
return self.get('MimeType', list=True, type="regex")
|
||||
def getMimeTypes(self):
|
||||
return self.get('MimeType', list=True)
|
||||
def getCategories(self):
|
||||
return self.get('Categories', list=True)
|
||||
def getStartupNotify(self):
|
||||
return self.get('StartupNotify', type="boolean")
|
||||
def getStartupWMClass(self):
|
||||
return self.get('StartupWMClass')
|
||||
def getURL(self):
|
||||
return self.get('URL')
|
||||
# end standard keys
|
||||
|
||||
# start kde keys
|
||||
def getServiceTypes(self):
|
||||
return self.get('ServiceTypes', list=True)
|
||||
def getDocPath(self):
|
||||
return self.get('DocPath')
|
||||
def getKeywords(self):
|
||||
return self.get('Keywords', list=True, locale=True)
|
||||
def getInitialPreference(self):
|
||||
return self.get('InitialPreference')
|
||||
def getDev(self):
|
||||
return self.get('Dev')
|
||||
def getFSType(self):
|
||||
return self.get('FSType')
|
||||
def getMountPoint(self):
|
||||
return self.get('MountPoint')
|
||||
def getReadonly(self):
|
||||
return self.get('ReadOnly', type="boolean")
|
||||
def getUnmountIcon(self):
|
||||
return self.get('UnmountIcon', locale=True)
|
||||
# end kde keys
|
||||
|
||||
# start deprecated keys
|
||||
def getMiniIcon(self):
|
||||
return self.get('MiniIcon', locale=True)
|
||||
def getTerminalOptions(self):
|
||||
return self.get('TerminalOptions')
|
||||
def getDefaultApp(self):
|
||||
return self.get('DefaultApp')
|
||||
def getProtocols(self):
|
||||
return self.get('Protocols', list=True)
|
||||
def getExtensions(self):
|
||||
return self.get('Extensions', list=True)
|
||||
def getBinaryPattern(self):
|
||||
return self.get('BinaryPattern')
|
||||
def getMapNotify(self):
|
||||
return self.get('MapNotify')
|
||||
def getEncoding(self):
|
||||
return self.get('Encoding')
|
||||
def getSwallowTitle(self):
|
||||
return self.get('SwallowTitle', locale=True)
|
||||
def getSwallowExec(self):
|
||||
return self.get('SwallowExec')
|
||||
def getSortOrder(self):
|
||||
return self.get('SortOrder', list=True)
|
||||
def getFilePattern(self):
|
||||
return self.get('FilePattern', type="regex")
|
||||
def getActions(self):
|
||||
return self.get('Actions', list=True)
|
||||
# end deprecated keys
|
||||
|
||||
# desktop entry edit stuff
|
||||
def new(self, filename):
|
||||
"""Make this instance into a new, blank desktop entry.
|
||||
|
||||
If filename has a .desktop extension, Type is set to Application. If it
|
||||
has a .directory extension, Type is Directory. Other extensions will
|
||||
cause :class:`~xdg.Exceptions.ParsingError` to be raised.
|
||||
"""
|
||||
if os.path.splitext(filename)[1] == ".desktop":
|
||||
type = "Application"
|
||||
elif os.path.splitext(filename)[1] == ".directory":
|
||||
type = "Directory"
|
||||
else:
|
||||
raise ParsingError("Unknown extension", filename)
|
||||
|
||||
self.content = dict()
|
||||
self.addGroup(self.defaultGroup)
|
||||
self.set("Type", type)
|
||||
self.filename = filename
|
||||
# end desktop entry edit stuff
|
||||
|
||||
# validation stuff
|
||||
def checkExtras(self):
|
||||
# header
|
||||
if self.defaultGroup == "KDE Desktop Entry":
|
||||
self.warnings.append('[KDE Desktop Entry]-Header is deprecated')
|
||||
|
||||
# file extension
|
||||
if self.fileExtension == ".kdelnk":
|
||||
self.warnings.append("File extension .kdelnk is deprecated")
|
||||
elif self.fileExtension != ".desktop" and self.fileExtension != ".directory":
|
||||
self.warnings.append('Unknown File extension')
|
||||
|
||||
# Type
|
||||
try:
|
||||
self.type = self.content[self.defaultGroup]["Type"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Type' is missing")
|
||||
|
||||
# Name
|
||||
try:
|
||||
self.name = self.content[self.defaultGroup]["Name"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Name' is missing")
|
||||
|
||||
def checkGroup(self, group):
|
||||
# check if group header is valid
|
||||
if not (group == self.defaultGroup \
|
||||
or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \
|
||||
or (re.match("^X-", group) and is_ascii(group))):
|
||||
self.errors.append("Invalid Group name: %s" % group)
|
||||
else:
|
||||
#OnlyShowIn and NotShowIn
|
||||
if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]):
|
||||
self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both")
|
||||
|
||||
def checkKey(self, key, value, group):
|
||||
# standard keys
|
||||
if key == "Type":
|
||||
if value == "ServiceType" or value == "Service" or value == "FSDevice":
|
||||
self.warnings.append("Type=%s is a KDE extension" % key)
|
||||
elif value == "MimeType":
|
||||
self.warnings.append("Type=MimeType is deprecated")
|
||||
elif not (value == "Application" or value == "Link" or value == "Directory"):
|
||||
self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value)
|
||||
|
||||
if self.fileExtension == ".directory" and not value == "Directory":
|
||||
self.warnings.append("File extension is .directory, but Type is '%s'" % value)
|
||||
elif self.fileExtension == ".desktop" and value == "Directory":
|
||||
self.warnings.append("Files with Type=Directory should have the extension .directory")
|
||||
|
||||
if value == "Application":
|
||||
if "Exec" not in self.content[group]:
|
||||
self.warnings.append("Type=Application needs 'Exec' key")
|
||||
if value == "Link":
|
||||
if "URL" not in self.content[group]:
|
||||
self.warnings.append("Type=Link needs 'URL' key")
|
||||
|
||||
elif key == "Version":
|
||||
self.checkValue(key, value)
|
||||
|
||||
elif re.match("^Name"+xdg.Locale.regex+"$", key):
|
||||
pass # locale string
|
||||
|
||||
elif re.match("^GenericName"+xdg.Locale.regex+"$", key):
|
||||
pass # locale string
|
||||
|
||||
elif key == "NoDisplay":
|
||||
self.checkValue(key, value, type="boolean")
|
||||
|
||||
elif re.match("^Comment"+xdg.Locale.regex+"$", key):
|
||||
pass # locale string
|
||||
|
||||
elif re.match("^Icon"+xdg.Locale.regex+"$", key):
|
||||
self.checkValue(key, value)
|
||||
|
||||
elif key == "Hidden":
|
||||
self.checkValue(key, value, type="boolean")
|
||||
|
||||
elif key == "OnlyShowIn":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.checkOnlyShowIn(value)
|
||||
|
||||
elif key == "NotShowIn":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.checkOnlyShowIn(value)
|
||||
|
||||
elif key == "TryExec":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "Exec":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "Path":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "Terminal":
|
||||
self.checkValue(key, value, type="boolean")
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "Actions":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "MimeType":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "Categories":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "Application")
|
||||
self.checkCategories(value)
|
||||
|
||||
elif re.match("^Keywords"+xdg.Locale.regex+"$", key):
|
||||
self.checkValue(key, value, type="localestring", list=True)
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "StartupNotify":
|
||||
self.checkValue(key, value, type="boolean")
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "StartupWMClass":
|
||||
self.checkType(key, "Application")
|
||||
|
||||
elif key == "URL":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "URL")
|
||||
|
||||
# kde extensions
|
||||
elif key == "ServiceTypes":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif key == "DocPath":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif key == "InitialPreference":
|
||||
self.checkValue(key, value, type="numeric")
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif key == "Dev":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "FSDevice")
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif key == "FSType":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "FSDevice")
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif key == "MountPoint":
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "FSDevice")
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif key == "ReadOnly":
|
||||
self.checkValue(key, value, type="boolean")
|
||||
self.checkType(key, "FSDevice")
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key):
|
||||
self.checkValue(key, value)
|
||||
self.checkType(key, "FSDevice")
|
||||
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||
|
||||
# deprecated keys
|
||||
elif key == "Encoding":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key):
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "TerminalOptions":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "DefaultApp":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "Protocols":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "Extensions":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "BinaryPattern":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "MapNotify":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key):
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "SwallowExec":
|
||||
self.checkValue(key, value)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "FilePattern":
|
||||
self.checkValue(key, value, type="regex", list=True)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
elif key == "SortOrder":
|
||||
self.checkValue(key, value, list=True)
|
||||
self.warnings.append("Key '%s' is deprecated" % key)
|
||||
|
||||
# "X-" extensions
|
||||
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||
pass
|
||||
|
||||
else:
|
||||
self.errors.append("Invalid key: %s" % key)
|
||||
|
||||
def checkType(self, key, type):
|
||||
if not self.getType() == type:
|
||||
self.errors.append("Key '%s' only allowed in Type=%s" % (key, type))
|
||||
|
||||
def checkOnlyShowIn(self, value):
|
||||
values = self.getList(value)
|
||||
valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity",
|
||||
"XFCE", "Old"]
|
||||
for item in values:
|
||||
if item not in valid and item[0:2] != "X-":
|
||||
self.errors.append("'%s' is not a registered OnlyShowIn value" % item);
|
||||
|
||||
def checkCategories(self, value):
|
||||
values = self.getList(value)
|
||||
|
||||
main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"]
|
||||
if not any(item in main for item in values):
|
||||
self.errors.append("Missing main category")
|
||||
|
||||
additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly']
|
||||
allcategories = additional + main
|
||||
|
||||
for item in values:
|
||||
if item not in allcategories and not item.startswith("X-"):
|
||||
self.errors.append("'%s' is not a registered Category" % item);
|
||||
|
||||
def checkCategorie(self, value):
|
||||
"""Deprecated alias for checkCategories - only exists for backwards
|
||||
compatibility.
|
||||
"""
|
||||
warnings.warn("checkCategorie is deprecated, use checkCategories",
|
||||
DeprecationWarning)
|
||||
return self.checkCategories(value)
|
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Exception Classes for the xdg package
|
||||
"""
|
||||
|
||||
debug = False
|
||||
|
||||
class Error(Exception):
|
||||
"""Base class for exceptions defined here."""
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
Exception.__init__(self, msg)
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
class ValidationError(Error):
|
||||
"""Raised when a file fails to validate.
|
||||
|
||||
The filename is the .file attribute.
|
||||
"""
|
||||
def __init__(self, msg, file):
|
||||
self.msg = msg
|
||||
self.file = file
|
||||
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
|
||||
|
||||
class ParsingError(Error):
|
||||
"""Raised when a file cannot be parsed.
|
||||
|
||||
The filename is the .file attribute.
|
||||
"""
|
||||
def __init__(self, msg, file):
|
||||
self.msg = msg
|
||||
self.file = file
|
||||
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
|
||||
|
||||
class NoKeyError(Error):
|
||||
"""Raised when trying to access a nonexistant key in an INI-style file.
|
||||
|
||||
Attributes are .key, .group and .file.
|
||||
"""
|
||||
def __init__(self, key, group, file):
|
||||
Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
|
||||
self.key = key
|
||||
self.group = group
|
||||
self.file = file
|
||||
|
||||
class DuplicateKeyError(Error):
|
||||
"""Raised when the same key occurs twice in an INI-style file.
|
||||
|
||||
Attributes are .key, .group and .file.
|
||||
"""
|
||||
def __init__(self, key, group, file):
|
||||
Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
|
||||
self.key = key
|
||||
self.group = group
|
||||
self.file = file
|
||||
|
||||
class NoGroupError(Error):
|
||||
"""Raised when trying to access a nonexistant group in an INI-style file.
|
||||
|
||||
Attributes are .group and .file.
|
||||
"""
|
||||
def __init__(self, group, file):
|
||||
Error.__init__(self, "No group: %s in file %s" % (group, file))
|
||||
self.group = group
|
||||
self.file = file
|
||||
|
||||
class DuplicateGroupError(Error):
|
||||
"""Raised when the same key occurs twice in an INI-style file.
|
||||
|
||||
Attributes are .group and .file.
|
||||
"""
|
||||
def __init__(self, group, file):
|
||||
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
|
||||
self.group = group
|
||||
self.file = file
|
||||
|
||||
class NoThemeError(Error):
|
||||
"""Raised when trying to access a nonexistant icon theme.
|
||||
|
||||
The name of the theme is the .theme attribute.
|
||||
"""
|
||||
def __init__(self, theme):
|
||||
Error.__init__(self, "No such icon-theme: %s" % theme)
|
||||
self.theme = theme
|
443
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/IconTheme.py
Normal file
443
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/IconTheme.py
Normal file
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
Complete implementation of the XDG Icon Spec
|
||||
http://standards.freedesktop.org/icon-theme-spec/
|
||||
"""
|
||||
|
||||
import os, time
|
||||
import re
|
||||
|
||||
from . import IniFile, Config
|
||||
from .IniFile import is_ascii
|
||||
from .BaseDirectory import xdg_data_dirs
|
||||
from .Exceptions import NoThemeError, debug
|
||||
|
||||
|
||||
class IconTheme(IniFile):
|
||||
"Class to parse and validate IconThemes"
|
||||
def __init__(self):
|
||||
IniFile.__init__(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def parse(self, file):
|
||||
IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"])
|
||||
self.dir = os.path.dirname(file)
|
||||
(nil, self.name) = os.path.split(self.dir)
|
||||
|
||||
def getDir(self):
|
||||
return self.dir
|
||||
|
||||
# Standard Keys
|
||||
def getName(self):
|
||||
return self.get('Name', locale=True)
|
||||
def getComment(self):
|
||||
return self.get('Comment', locale=True)
|
||||
def getInherits(self):
|
||||
return self.get('Inherits', list=True)
|
||||
def getDirectories(self):
|
||||
return self.get('Directories', list=True)
|
||||
def getScaledDirectories(self):
|
||||
return self.get('ScaledDirectories', list=True)
|
||||
def getHidden(self):
|
||||
return self.get('Hidden', type="boolean")
|
||||
def getExample(self):
|
||||
return self.get('Example')
|
||||
|
||||
# Per Directory Keys
|
||||
def getSize(self, directory):
|
||||
return self.get('Size', type="integer", group=directory)
|
||||
def getContext(self, directory):
|
||||
return self.get('Context', group=directory)
|
||||
def getType(self, directory):
|
||||
value = self.get('Type', group=directory)
|
||||
if value:
|
||||
return value
|
||||
else:
|
||||
return "Threshold"
|
||||
def getMaxSize(self, directory):
|
||||
value = self.get('MaxSize', type="integer", group=directory)
|
||||
if value or value == 0:
|
||||
return value
|
||||
else:
|
||||
return self.getSize(directory)
|
||||
def getMinSize(self, directory):
|
||||
value = self.get('MinSize', type="integer", group=directory)
|
||||
if value or value == 0:
|
||||
return value
|
||||
else:
|
||||
return self.getSize(directory)
|
||||
def getThreshold(self, directory):
|
||||
value = self.get('Threshold', type="integer", group=directory)
|
||||
if value or value == 0:
|
||||
return value
|
||||
else:
|
||||
return 2
|
||||
|
||||
def getScale(self, directory):
|
||||
value = self.get('Scale', type="integer", group=directory)
|
||||
return value or 1
|
||||
|
||||
# validation stuff
|
||||
def checkExtras(self):
|
||||
# header
|
||||
if self.defaultGroup == "KDE Icon Theme":
|
||||
self.warnings.append('[KDE Icon Theme]-Header is deprecated')
|
||||
|
||||
# file extension
|
||||
if self.fileExtension == ".theme":
|
||||
pass
|
||||
elif self.fileExtension == ".desktop":
|
||||
self.warnings.append('.desktop fileExtension is deprecated')
|
||||
else:
|
||||
self.warnings.append('Unknown File extension')
|
||||
|
||||
# Check required keys
|
||||
# Name
|
||||
try:
|
||||
self.name = self.content[self.defaultGroup]["Name"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Name' is missing")
|
||||
|
||||
# Comment
|
||||
try:
|
||||
self.comment = self.content[self.defaultGroup]["Comment"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Comment' is missing")
|
||||
|
||||
# Directories
|
||||
try:
|
||||
self.directories = self.content[self.defaultGroup]["Directories"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Directories' is missing")
|
||||
|
||||
def checkGroup(self, group):
|
||||
# check if group header is valid
|
||||
if group == self.defaultGroup:
|
||||
try:
|
||||
self.name = self.content[group]["Name"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Name' in Group '%s' is missing" % group)
|
||||
try:
|
||||
self.name = self.content[group]["Comment"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Comment' in Group '%s' is missing" % group)
|
||||
elif group in self.getDirectories():
|
||||
try:
|
||||
self.type = self.content[group]["Type"]
|
||||
except KeyError:
|
||||
self.type = "Threshold"
|
||||
try:
|
||||
self.name = self.content[group]["Size"]
|
||||
except KeyError:
|
||||
self.errors.append("Key 'Size' in Group '%s' is missing" % group)
|
||||
elif not (re.match(r"^\[X-", group) and is_ascii(group)):
|
||||
self.errors.append("Invalid Group name: %s" % group)
|
||||
|
||||
def checkKey(self, key, value, group):
|
||||
# standard keys
|
||||
if group == self.defaultGroup:
|
||||
if re.match("^Name"+xdg.Locale.regex+"$", key):
|
||||
pass
|
||||
elif re.match("^Comment"+xdg.Locale.regex+"$", key):
|
||||
pass
|
||||
elif key == "Inherits":
|
||||
self.checkValue(key, value, list=True)
|
||||
elif key == "Directories":
|
||||
self.checkValue(key, value, list=True)
|
||||
elif key == "ScaledDirectories":
|
||||
self.checkValue(key, value, list=True)
|
||||
elif key == "Hidden":
|
||||
self.checkValue(key, value, type="boolean")
|
||||
elif key == "Example":
|
||||
self.checkValue(key, value)
|
||||
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||
pass
|
||||
else:
|
||||
self.errors.append("Invalid key: %s" % key)
|
||||
elif group in self.getDirectories():
|
||||
if key == "Size":
|
||||
self.checkValue(key, value, type="integer")
|
||||
elif key == "Context":
|
||||
self.checkValue(key, value)
|
||||
elif key == "Type":
|
||||
self.checkValue(key, value)
|
||||
if value not in ["Fixed", "Scalable", "Threshold"]:
|
||||
self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value)
|
||||
elif key == "MaxSize":
|
||||
self.checkValue(key, value, type="integer")
|
||||
if self.type != "Scalable":
|
||||
self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type)
|
||||
elif key == "MinSize":
|
||||
self.checkValue(key, value, type="integer")
|
||||
if self.type != "Scalable":
|
||||
self.errors.append("Key 'MinSize' give, but Type is %s" % self.type)
|
||||
elif key == "Threshold":
|
||||
self.checkValue(key, value, type="integer")
|
||||
if self.type != "Threshold":
|
||||
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
|
||||
elif key == "Scale":
|
||||
self.checkValue(key, value, type="integer")
|
||||
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||
pass
|
||||
else:
|
||||
self.errors.append("Invalid key: %s" % key)
|
||||
|
||||
|
||||
class IconData(IniFile):
|
||||
"Class to parse and validate IconData Files"
|
||||
def __init__(self):
|
||||
IniFile.__init__(self)
|
||||
|
||||
def __repr__(self):
|
||||
displayname = self.getDisplayName()
|
||||
if displayname:
|
||||
return "<IconData: %s>" % displayname
|
||||
else:
|
||||
return "<IconData>"
|
||||
|
||||
def parse(self, file):
|
||||
IniFile.parse(self, file, ["Icon Data"])
|
||||
|
||||
# Standard Keys
|
||||
def getDisplayName(self):
|
||||
"""Retrieve the display name from the icon data, if one is specified."""
|
||||
return self.get('DisplayName', locale=True)
|
||||
def getEmbeddedTextRectangle(self):
|
||||
"""Retrieve the embedded text rectangle from the icon data as a list of
|
||||
numbers (x0, y0, x1, y1), if it is specified."""
|
||||
return self.get('EmbeddedTextRectangle', type="integer", list=True)
|
||||
def getAttachPoints(self):
|
||||
"""Retrieve the anchor points for overlays & emblems from the icon data,
|
||||
as a list of co-ordinate pairs, if they are specified."""
|
||||
return self.get('AttachPoints', type="point", list=True)
|
||||
|
||||
# validation stuff
|
||||
def checkExtras(self):
|
||||
# file extension
|
||||
if self.fileExtension != ".icon":
|
||||
self.warnings.append('Unknown File extension')
|
||||
|
||||
def checkGroup(self, group):
|
||||
# check if group header is valid
|
||||
if not (group == self.defaultGroup \
|
||||
or (re.match(r"^\[X-", group) and is_ascii(group))):
|
||||
self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
|
||||
|
||||
def checkKey(self, key, value, group):
|
||||
# standard keys
|
||||
if re.match("^DisplayName"+xdg.Locale.regex+"$", key):
|
||||
pass
|
||||
elif key == "EmbeddedTextRectangle":
|
||||
self.checkValue(key, value, type="integer", list=True)
|
||||
elif key == "AttachPoints":
|
||||
self.checkValue(key, value, type="point", list=True)
|
||||
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||
pass
|
||||
else:
|
||||
self.errors.append("Invalid key: %s" % key)
|
||||
|
||||
|
||||
|
||||
icondirs = []
|
||||
for basedir in xdg_data_dirs:
|
||||
icondirs.append(os.path.join(basedir, "icons"))
|
||||
icondirs.append(os.path.join(basedir, "pixmaps"))
|
||||
icondirs.append(os.path.expanduser("~/.icons"))
|
||||
|
||||
# just cache variables, they give a 10x speed improvement
|
||||
themes = []
|
||||
theme_cache = {}
|
||||
dir_cache = {}
|
||||
icon_cache = {}
|
||||
|
||||
def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]):
|
||||
"""Get the path to a specified icon.
|
||||
|
||||
size :
|
||||
Icon size in pixels. Defaults to ``xdg.Config.icon_size``.
|
||||
theme :
|
||||
Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't
|
||||
found in the specified theme, it will be looked up in the basic 'hicolor'
|
||||
theme.
|
||||
extensions :
|
||||
List of preferred file extensions.
|
||||
|
||||
Example::
|
||||
|
||||
>>> getIconPath("inkscape", 32)
|
||||
'/usr/share/icons/hicolor/32x32/apps/inkscape.png'
|
||||
"""
|
||||
|
||||
global themes
|
||||
|
||||
if size == None:
|
||||
size = xdg.Config.icon_size
|
||||
if theme == None:
|
||||
theme = xdg.Config.icon_theme
|
||||
|
||||
# if we have an absolute path, just return it
|
||||
if os.path.isabs(iconname):
|
||||
return iconname
|
||||
|
||||
# check if it has an extension and strip it
|
||||
if os.path.splitext(iconname)[1][1:] in extensions:
|
||||
iconname = os.path.splitext(iconname)[0]
|
||||
|
||||
# parse theme files
|
||||
if (themes == []) or (themes[0].name != theme):
|
||||
themes = list(__get_themes(theme))
|
||||
|
||||
# more caching (icon looked up in the last 5 seconds?)
|
||||
tmp = (iconname, size, theme, tuple(extensions))
|
||||
try:
|
||||
timestamp, icon = icon_cache[tmp]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if (time.time() - timestamp) >= xdg.Config.cache_time:
|
||||
del icon_cache[tmp]
|
||||
else:
|
||||
return icon
|
||||
|
||||
for thme in themes:
|
||||
icon = LookupIcon(iconname, size, thme, extensions)
|
||||
if icon:
|
||||
icon_cache[tmp] = (time.time(), icon)
|
||||
return icon
|
||||
|
||||
# cache stuff again (directories looked up in the last 5 seconds?)
|
||||
for directory in icondirs:
|
||||
if (directory not in dir_cache \
|
||||
or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \
|
||||
and dir_cache[directory][2] < os.path.getmtime(directory))) \
|
||||
and os.path.isdir(directory):
|
||||
dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory))
|
||||
|
||||
for dir, values in dir_cache.items():
|
||||
for extension in extensions:
|
||||
try:
|
||||
if iconname + "." + extension in values[0]:
|
||||
icon = os.path.join(dir, iconname + "." + extension)
|
||||
icon_cache[tmp] = [time.time(), icon]
|
||||
return icon
|
||||
except UnicodeDecodeError as e:
|
||||
...
|
||||
|
||||
|
||||
# we haven't found anything? "hicolor" is our fallback
|
||||
if theme != "hicolor":
|
||||
icon = getIconPath(iconname, size, "hicolor")
|
||||
icon_cache[tmp] = [time.time(), icon]
|
||||
return icon
|
||||
|
||||
def getIconData(path):
|
||||
"""Retrieve the data from the .icon file corresponding to the given file. If
|
||||
there is no .icon file, it returns None.
|
||||
|
||||
Example::
|
||||
|
||||
getIconData("/usr/share/icons/Tango/scalable/places/folder.svg")
|
||||
"""
|
||||
if os.path.isfile(path):
|
||||
icon_file = os.path.splitext(path)[0] + ".icon"
|
||||
if os.path.isfile(icon_file):
|
||||
data = IconData()
|
||||
data.parse(icon_file)
|
||||
return data
|
||||
|
||||
def __get_themes(themename):
|
||||
"""Generator yielding IconTheme objects for a specified theme and any themes
|
||||
from which it inherits.
|
||||
"""
|
||||
for dir in icondirs:
|
||||
theme_file = os.path.join(dir, themename, "index.theme")
|
||||
if os.path.isfile(theme_file):
|
||||
break
|
||||
theme_file = os.path.join(dir, themename, "index.desktop")
|
||||
if os.path.isfile(theme_file):
|
||||
break
|
||||
else:
|
||||
if debug:
|
||||
raise NoThemeError(themename)
|
||||
return
|
||||
|
||||
theme = IconTheme()
|
||||
theme.parse(theme_file)
|
||||
yield theme
|
||||
for subtheme in theme.getInherits():
|
||||
for t in __get_themes(subtheme):
|
||||
yield t
|
||||
|
||||
def LookupIcon(iconname, size, theme, extensions):
|
||||
# look for the cache
|
||||
if theme.name not in theme_cache:
|
||||
theme_cache[theme.name] = []
|
||||
theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup
|
||||
theme_cache[theme.name].append(0) # [1] mtime
|
||||
theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]]
|
||||
|
||||
# cache stuff (directory lookuped up the in the last 5 seconds?)
|
||||
if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time:
|
||||
theme_cache[theme.name][0] = time.time()
|
||||
for subdir in theme.getDirectories():
|
||||
for directory in icondirs:
|
||||
dir = os.path.join(directory,theme.name,subdir)
|
||||
if (dir not in theme_cache[theme.name][2] \
|
||||
or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \
|
||||
and subdir != "" \
|
||||
and os.path.isdir(dir):
|
||||
theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)]
|
||||
theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name))
|
||||
|
||||
for dir, values in theme_cache[theme.name][2].items():
|
||||
if DirectoryMatchesSize(values[0], size, theme):
|
||||
for extension in extensions:
|
||||
if iconname + "." + extension in values[1]:
|
||||
return os.path.join(dir, iconname + "." + extension)
|
||||
|
||||
minimal_size = 2**31
|
||||
closest_filename = ""
|
||||
for dir, values in theme_cache[theme.name][2].items():
|
||||
distance = DirectorySizeDistance(values[0], size, theme)
|
||||
if distance < minimal_size:
|
||||
for extension in extensions:
|
||||
if iconname + "." + extension in values[1]:
|
||||
closest_filename = os.path.join(dir, iconname + "." + extension)
|
||||
minimal_size = distance
|
||||
|
||||
return closest_filename
|
||||
|
||||
def DirectoryMatchesSize(subdir, iconsize, theme):
|
||||
Type = theme.getType(subdir)
|
||||
Size = theme.getSize(subdir)
|
||||
Threshold = theme.getThreshold(subdir)
|
||||
MinSize = theme.getMinSize(subdir)
|
||||
MaxSize = theme.getMaxSize(subdir)
|
||||
if Type == "Fixed":
|
||||
return Size == iconsize
|
||||
elif Type == "Scaleable":
|
||||
return MinSize <= iconsize <= MaxSize
|
||||
elif Type == "Threshold":
|
||||
return Size - Threshold <= iconsize <= Size + Threshold
|
||||
|
||||
def DirectorySizeDistance(subdir, iconsize, theme):
|
||||
Type = theme.getType(subdir)
|
||||
Size = theme.getSize(subdir)
|
||||
Threshold = theme.getThreshold(subdir)
|
||||
MinSize = theme.getMinSize(subdir)
|
||||
MaxSize = theme.getMaxSize(subdir)
|
||||
if Type == "Fixed":
|
||||
return abs(Size - iconsize)
|
||||
elif Type == "Scalable":
|
||||
if iconsize < MinSize:
|
||||
return MinSize - iconsize
|
||||
elif iconsize > MaxSize:
|
||||
return MaxSize - iconsize
|
||||
return 0
|
||||
elif Type == "Threshold":
|
||||
if iconsize < Size - Threshold:
|
||||
return MinSize - iconsize
|
||||
elif iconsize > Size + Threshold:
|
||||
return iconsize - MaxSize
|
||||
return 0
|
419
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/IniFile.py
Normal file
419
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/IniFile.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
Base Class for DesktopEntry, IconTheme and IconData
|
||||
"""
|
||||
|
||||
import re, os, stat, io
|
||||
from .Exceptions import (ParsingError, DuplicateGroupError, NoGroupError,
|
||||
NoKeyError, DuplicateKeyError, ValidationError,
|
||||
debug)
|
||||
# import xdg.Locale
|
||||
from . import Locale
|
||||
from .util import u
|
||||
|
||||
def is_ascii(s):
|
||||
"""Return True if a string consists entirely of ASCII characters."""
|
||||
try:
|
||||
s.encode('ascii', 'strict')
|
||||
return True
|
||||
except UnicodeError:
|
||||
return False
|
||||
|
||||
class IniFile:
|
||||
defaultGroup = ''
|
||||
fileExtension = ''
|
||||
|
||||
filename = ''
|
||||
|
||||
tainted = False
|
||||
|
||||
def __init__(self, filename=None):
|
||||
self.content = dict()
|
||||
if filename:
|
||||
self.parse(filename)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.content, other.content)
|
||||
|
||||
def parse(self, filename, headers=None):
|
||||
'''Parse an INI file.
|
||||
|
||||
headers -- list of headers the parser will try to select as a default header
|
||||
'''
|
||||
# for performance reasons
|
||||
content = self.content
|
||||
|
||||
if not os.path.isfile(filename):
|
||||
raise ParsingError("File not found", filename)
|
||||
|
||||
try:
|
||||
# The content should be UTF-8, but legacy files can have other
|
||||
# encodings, including mixed encodings in one file. We don't attempt
|
||||
# to decode them, but we silence the errors.
|
||||
fd = io.open(filename, 'r', encoding='utf-8', errors='replace')
|
||||
except IOError as e:
|
||||
if debug:
|
||||
raise e
|
||||
else:
|
||||
return
|
||||
|
||||
# parse file
|
||||
for line in fd:
|
||||
line = line.strip()
|
||||
# empty line
|
||||
if not line:
|
||||
continue
|
||||
# comment
|
||||
elif line[0] == '#':
|
||||
continue
|
||||
# new group
|
||||
elif line[0] == '[':
|
||||
currentGroup = line.lstrip("[").rstrip("]")
|
||||
if debug and self.hasGroup(currentGroup):
|
||||
raise DuplicateGroupError(currentGroup, filename)
|
||||
else:
|
||||
content[currentGroup] = {}
|
||||
# key
|
||||
else:
|
||||
try:
|
||||
key, value = line.split("=", 1)
|
||||
except ValueError:
|
||||
raise ParsingError("Invalid line: " + line, filename)
|
||||
|
||||
key = key.strip() # Spaces before/after '=' should be ignored
|
||||
try:
|
||||
if debug and self.hasKey(key, currentGroup):
|
||||
raise DuplicateKeyError(key, currentGroup, filename)
|
||||
else:
|
||||
content[currentGroup][key] = value.strip()
|
||||
except (IndexError, UnboundLocalError):
|
||||
raise ParsingError("Parsing error on key, group missing", filename)
|
||||
|
||||
fd.close()
|
||||
|
||||
self.filename = filename
|
||||
self.tainted = False
|
||||
|
||||
# check header
|
||||
if headers:
|
||||
for header in headers:
|
||||
if header in content:
|
||||
self.defaultGroup = header
|
||||
break
|
||||
else:
|
||||
raise ParsingError("[%s]-Header missing" % headers[0], filename)
|
||||
|
||||
# start stuff to access the keys
|
||||
def get(self, key, group=None, locale=False, type="string", list=False, strict=False):
|
||||
# set default group
|
||||
if not group:
|
||||
group = self.defaultGroup
|
||||
|
||||
# return key (with locale)
|
||||
if (group in self.content) and (key in self.content[group]):
|
||||
if locale:
|
||||
value = self.content[group][self.__addLocale(key, group)]
|
||||
else:
|
||||
value = self.content[group][key]
|
||||
else:
|
||||
if strict or debug:
|
||||
if group not in self.content:
|
||||
raise NoGroupError(group, self.filename)
|
||||
elif key not in self.content[group]:
|
||||
raise NoKeyError(key, group, self.filename)
|
||||
else:
|
||||
value = ""
|
||||
|
||||
if list == True:
|
||||
values = self.getList(value)
|
||||
result = []
|
||||
else:
|
||||
values = [value]
|
||||
|
||||
for value in values:
|
||||
if type == "boolean":
|
||||
value = self.__getBoolean(value)
|
||||
elif type == "integer":
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
value = 0
|
||||
elif type == "numeric":
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = 0.0
|
||||
elif type == "regex":
|
||||
value = re.compile(value)
|
||||
elif type == "point":
|
||||
x, y = value.split(",")
|
||||
value = int(x), int(y)
|
||||
|
||||
if list == True:
|
||||
result.append(value)
|
||||
else:
|
||||
result = value
|
||||
|
||||
return result
|
||||
# end stuff to access the keys
|
||||
|
||||
# start subget
|
||||
def getList(self, string):
|
||||
if re.search(r"(?<!\\)\;", string):
|
||||
list = re.split(r"(?<!\\);", string)
|
||||
elif re.search(r"(?<!\\)\|", string):
|
||||
list = re.split(r"(?<!\\)\|", string)
|
||||
elif re.search(r"(?<!\\),", string):
|
||||
list = re.split(r"(?<!\\),", string)
|
||||
else:
|
||||
list = [string]
|
||||
if list[-1] == "":
|
||||
list.pop()
|
||||
return list
|
||||
|
||||
def __getBoolean(self, boolean):
|
||||
if boolean == 1 or boolean == "true" or boolean == "True":
|
||||
return True
|
||||
elif boolean == 0 or boolean == "false" or boolean == "False":
|
||||
return False
|
||||
return False
|
||||
# end subget
|
||||
|
||||
def __addLocale(self, key, group=None):
|
||||
"add locale to key according the current lc_messages"
|
||||
# set default group
|
||||
if not group:
|
||||
group = self.defaultGroup
|
||||
|
||||
for lang in Locale.langs:
|
||||
langkey = "%s[%s]" % (key, lang)
|
||||
if langkey in self.content[group]:
|
||||
return langkey
|
||||
|
||||
return key
|
||||
|
||||
# start validation stuff
|
||||
def validate(self, report="All"):
|
||||
"""Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
|
||||
if there is anything amiss.
|
||||
|
||||
report can be 'All' / 'Warnings' / 'Errors'
|
||||
"""
|
||||
|
||||
self.warnings = []
|
||||
self.errors = []
|
||||
|
||||
# get file extension
|
||||
self.fileExtension = os.path.splitext(self.filename)[1]
|
||||
|
||||
# overwrite this for own checkings
|
||||
self.checkExtras()
|
||||
|
||||
# check all keys
|
||||
for group in self.content:
|
||||
self.checkGroup(group)
|
||||
for key in self.content[group]:
|
||||
self.checkKey(key, self.content[group][key], group)
|
||||
# check if value is empty
|
||||
if self.content[group][key] == "":
|
||||
self.warnings.append("Value of Key '%s' is empty" % key)
|
||||
|
||||
# raise Warnings / Errors
|
||||
msg = ""
|
||||
|
||||
if report == "All" or report == "Warnings":
|
||||
for line in self.warnings:
|
||||
msg += "\n- " + line
|
||||
|
||||
if report == "All" or report == "Errors":
|
||||
for line in self.errors:
|
||||
msg += "\n- " + line
|
||||
|
||||
if msg:
|
||||
raise ValidationError(msg, self.filename)
|
||||
|
||||
# check if group header is valid
|
||||
def checkGroup(self, group):
|
||||
pass
|
||||
|
||||
# check if key is valid
|
||||
def checkKey(self, key, value, group):
|
||||
pass
|
||||
|
||||
# check random stuff
|
||||
def checkValue(self, key, value, type="string", list=False):
|
||||
if list == True:
|
||||
values = self.getList(value)
|
||||
else:
|
||||
values = [value]
|
||||
|
||||
for value in values:
|
||||
if type == "string":
|
||||
code = self.checkString(value)
|
||||
if type == "localestring":
|
||||
continue
|
||||
elif type == "boolean":
|
||||
code = self.checkBoolean(value)
|
||||
elif type == "numeric":
|
||||
code = self.checkNumber(value)
|
||||
elif type == "integer":
|
||||
code = self.checkInteger(value)
|
||||
elif type == "regex":
|
||||
code = self.checkRegex(value)
|
||||
elif type == "point":
|
||||
code = self.checkPoint(value)
|
||||
if code == 1:
|
||||
self.errors.append("'%s' is not a valid %s" % (value, type))
|
||||
elif code == 2:
|
||||
self.warnings.append("Value of key '%s' is deprecated" % key)
|
||||
|
||||
def checkExtras(self):
|
||||
pass
|
||||
|
||||
def checkBoolean(self, value):
|
||||
# 1 or 0 : deprecated
|
||||
if (value == "1" or value == "0"):
|
||||
return 2
|
||||
# true or false: ok
|
||||
elif not (value == "true" or value == "false"):
|
||||
return 1
|
||||
|
||||
def checkNumber(self, value):
|
||||
# float() ValueError
|
||||
try:
|
||||
float(value)
|
||||
except:
|
||||
return 1
|
||||
|
||||
def checkInteger(self, value):
|
||||
# int() ValueError
|
||||
try:
|
||||
int(value)
|
||||
except:
|
||||
return 1
|
||||
|
||||
def checkPoint(self, value):
|
||||
if not re.match("^[0-9]+,[0-9]+$", value):
|
||||
return 1
|
||||
|
||||
def checkString(self, value):
|
||||
return 0 if is_ascii(value) else 1
|
||||
|
||||
def checkRegex(self, value):
|
||||
try:
|
||||
re.compile(value)
|
||||
except:
|
||||
return 1
|
||||
|
||||
# write support
|
||||
def write(self, filename=None, trusted=False):
|
||||
if not filename and not self.filename:
|
||||
raise ParsingError("File not found", "")
|
||||
|
||||
if filename:
|
||||
self.filename = filename
|
||||
else:
|
||||
filename = self.filename
|
||||
|
||||
if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
|
||||
os.makedirs(os.path.dirname(filename))
|
||||
|
||||
with io.open(filename, 'w', encoding='utf-8') as fp:
|
||||
|
||||
# An executable bit signifies that the desktop file is
|
||||
# trusted, but then the file can be executed. Add hashbang to
|
||||
# make sure that the file is opened by something that
|
||||
# understands desktop files.
|
||||
if trusted:
|
||||
fp.write(u("#!/usr/bin/env xdg-open\n"))
|
||||
|
||||
if self.defaultGroup:
|
||||
fp.write(u("[%s]\n") % self.defaultGroup)
|
||||
for (key, value) in self.content[self.defaultGroup].items():
|
||||
fp.write(u("%s=%s\n") % (key, value))
|
||||
fp.write(u("\n"))
|
||||
for (name, group) in self.content.items():
|
||||
if name != self.defaultGroup:
|
||||
fp.write(u("[%s]\n") % name)
|
||||
for (key, value) in group.items():
|
||||
fp.write(u("%s=%s\n") % (key, value))
|
||||
fp.write(u("\n"))
|
||||
|
||||
# Add executable bits to the file to show that it's trusted.
|
||||
if trusted:
|
||||
oldmode = os.stat(filename).st_mode
|
||||
mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
os.chmod(filename, mode)
|
||||
|
||||
self.tainted = False
|
||||
|
||||
def set(self, key, value, group=None, locale=False):
|
||||
# set default group
|
||||
if not group:
|
||||
group = self.defaultGroup
|
||||
|
||||
if locale == True and len(xdg.Locale.langs) > 0:
|
||||
key = key + "[" + xdg.Locale.langs[0] + "]"
|
||||
|
||||
try:
|
||||
self.content[group][key] = value
|
||||
except KeyError:
|
||||
raise NoGroupError(group, self.filename)
|
||||
|
||||
self.tainted = (value == self.get(key, group))
|
||||
|
||||
def addGroup(self, group):
|
||||
if self.hasGroup(group):
|
||||
if debug:
|
||||
raise DuplicateGroupError(group, self.filename)
|
||||
else:
|
||||
self.content[group] = {}
|
||||
self.tainted = True
|
||||
|
||||
def removeGroup(self, group):
|
||||
existed = group in self.content
|
||||
if existed:
|
||||
del self.content[group]
|
||||
self.tainted = True
|
||||
else:
|
||||
if debug:
|
||||
raise NoGroupError(group, self.filename)
|
||||
return existed
|
||||
|
||||
def removeKey(self, key, group=None, locales=True):
|
||||
# set default group
|
||||
if not group:
|
||||
group = self.defaultGroup
|
||||
|
||||
try:
|
||||
if locales:
|
||||
for name in list(self.content[group]):
|
||||
if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key:
|
||||
del self.content[group][name]
|
||||
value = self.content[group].pop(key)
|
||||
self.tainted = True
|
||||
return value
|
||||
except KeyError as e:
|
||||
if debug:
|
||||
if e == group:
|
||||
raise NoGroupError(group, self.filename)
|
||||
else:
|
||||
raise NoKeyError(key, group, self.filename)
|
||||
else:
|
||||
return ""
|
||||
|
||||
# misc
|
||||
def groups(self):
|
||||
return self.content.keys()
|
||||
|
||||
def hasGroup(self, group):
|
||||
return group in self.content
|
||||
|
||||
def hasKey(self, key, group=None):
|
||||
# set default group
|
||||
if not group:
|
||||
group = self.defaultGroup
|
||||
|
||||
return key in self.content[group]
|
||||
|
||||
def getFileName(self):
|
||||
return self.filename
|
79
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Locale.py
Normal file
79
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Locale.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Helper Module for Locale settings
|
||||
|
||||
This module is based on a ROX module (LGPL):
|
||||
|
||||
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log
|
||||
"""
|
||||
|
||||
import os
|
||||
from locale import normalize
|
||||
|
||||
regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?"
|
||||
|
||||
def _expand_lang(locale):
|
||||
locale = normalize(locale)
|
||||
COMPONENT_CODESET = 1 << 0
|
||||
COMPONENT_MODIFIER = 1 << 1
|
||||
COMPONENT_TERRITORY = 1 << 2
|
||||
# split up the locale into its base components
|
||||
mask = 0
|
||||
pos = locale.find('@')
|
||||
if pos >= 0:
|
||||
modifier = locale[pos:]
|
||||
locale = locale[:pos]
|
||||
mask |= COMPONENT_MODIFIER
|
||||
else:
|
||||
modifier = ''
|
||||
pos = locale.find('.')
|
||||
codeset = ''
|
||||
if pos >= 0:
|
||||
locale = locale[:pos]
|
||||
pos = locale.find('_')
|
||||
if pos >= 0:
|
||||
territory = locale[pos:]
|
||||
locale = locale[:pos]
|
||||
mask |= COMPONENT_TERRITORY
|
||||
else:
|
||||
territory = ''
|
||||
language = locale
|
||||
ret = []
|
||||
for i in range(mask+1):
|
||||
if not (i & ~mask): # if all components for this combo exist ...
|
||||
val = language
|
||||
if i & COMPONENT_TERRITORY: val += territory
|
||||
if i & COMPONENT_CODESET: val += codeset
|
||||
if i & COMPONENT_MODIFIER: val += modifier
|
||||
ret.append(val)
|
||||
ret.reverse()
|
||||
return ret
|
||||
|
||||
def expand_languages(languages=None):
|
||||
# Get some reasonable defaults for arguments that were not supplied
|
||||
if languages is None:
|
||||
languages = []
|
||||
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
|
||||
val = os.environ.get(envar)
|
||||
if val:
|
||||
languages = val.split(':')
|
||||
break
|
||||
#if 'C' not in languages:
|
||||
# languages.append('C')
|
||||
|
||||
# now normalize and expand the languages
|
||||
nelangs = []
|
||||
for lang in languages:
|
||||
for nelang in _expand_lang(lang):
|
||||
if nelang not in nelangs:
|
||||
nelangs.append(nelang)
|
||||
return nelangs
|
||||
|
||||
def update(language=None):
|
||||
global langs
|
||||
if language:
|
||||
langs = expand_languages([language])
|
||||
else:
|
||||
langs = expand_languages()
|
||||
|
||||
langs = []
|
||||
update()
|
1125
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Menu.py
Normal file
1125
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Menu.py
Normal file
File diff suppressed because it is too large
Load Diff
541
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/MenuEditor.py
Normal file
541
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/MenuEditor.py
Normal file
@@ -0,0 +1,541 @@
|
||||
""" CLass to edit XDG Menus """
|
||||
import os
|
||||
try:
|
||||
import xml.etree.cElementTree as etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from .Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder
|
||||
from .BaseDirectory import xdg_config_dirs, xdg_data_dirs
|
||||
from .Exceptions import ParsingError
|
||||
from .Config import setRootMode
|
||||
|
||||
# XML-Cleanups: Move / Exclude
|
||||
# FIXME: proper reverte/delete
|
||||
# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
|
||||
# FIXME: catch Exceptions
|
||||
# FIXME: copy functions
|
||||
# FIXME: More Layout stuff
|
||||
# FIXME: unod/redo function / remove menu...
|
||||
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
|
||||
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
|
||||
|
||||
|
||||
class MenuEditor(object):
|
||||
|
||||
def __init__(self, menu=None, filename=None, root=False):
|
||||
self.menu = None
|
||||
self.filename = None
|
||||
self.tree = None
|
||||
self.parser = XMLMenuBuilder()
|
||||
self.parse(menu, filename, root)
|
||||
|
||||
# fix for creating two menus with the same name on the fly
|
||||
self.filenames = []
|
||||
|
||||
def parse(self, menu=None, filename=None, root=False):
|
||||
if root:
|
||||
setRootMode(True)
|
||||
|
||||
if isinstance(menu, Menu):
|
||||
self.menu = menu
|
||||
elif menu:
|
||||
self.menu = self.parser.parse(menu)
|
||||
else:
|
||||
self.menu = self.parser.parse()
|
||||
|
||||
if root:
|
||||
self.filename = self.menu.Filename
|
||||
elif filename:
|
||||
self.filename = filename
|
||||
else:
|
||||
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
|
||||
|
||||
try:
|
||||
self.tree = etree.parse(self.filename)
|
||||
except IOError:
|
||||
root = etree.fromtring("""
|
||||
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd">
|
||||
<Menu>
|
||||
<Name>Applications</Name>
|
||||
<MergeFile type="parent">%s</MergeFile>
|
||||
</Menu>
|
||||
""" % self.menu.Filename)
|
||||
self.tree = etree.ElementTree(root)
|
||||
except ParsingError:
|
||||
raise ParsingError('Not a valid .menu file', self.filename)
|
||||
|
||||
#FIXME: is this needed with etree ?
|
||||
self.__remove_whitespace_nodes(self.tree)
|
||||
|
||||
def save(self):
|
||||
self.__saveEntries(self.menu)
|
||||
self.__saveMenu()
|
||||
|
||||
def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
|
||||
menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
|
||||
menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
|
||||
|
||||
self.__addEntry(parent, menuentry, after, before)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return menuentry
|
||||
|
||||
def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
|
||||
menu = Menu()
|
||||
|
||||
menu.Parent = parent
|
||||
menu.Depth = parent.Depth + 1
|
||||
menu.Layout = parent.DefaultLayout
|
||||
menu.DefaultLayout = parent.DefaultLayout
|
||||
|
||||
menu = self.editMenu(menu, name, genericname, comment, icon)
|
||||
|
||||
self.__addEntry(parent, menu, after, before)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return menu
|
||||
|
||||
def createSeparator(self, parent, after=None, before=None):
|
||||
separator = Separator(parent)
|
||||
|
||||
self.__addEntry(parent, separator, after, before)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return separator
|
||||
|
||||
def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
|
||||
self.__deleteEntry(oldparent, menuentry, after, before)
|
||||
self.__addEntry(newparent, menuentry, after, before)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return menuentry
|
||||
|
||||
def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
|
||||
self.__deleteEntry(oldparent, menu, after, before)
|
||||
self.__addEntry(newparent, menu, after, before)
|
||||
|
||||
root_menu = self.__getXmlMenu(self.menu.Name)
|
||||
if oldparent.getPath(True) != newparent.getPath(True):
|
||||
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return menu
|
||||
|
||||
def moveSeparator(self, separator, parent, after=None, before=None):
|
||||
self.__deleteEntry(parent, separator, after, before)
|
||||
self.__addEntry(parent, separator, after, before)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return separator
|
||||
|
||||
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
|
||||
self.__addEntry(newparent, menuentry, after, before)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return menuentry
|
||||
|
||||
def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
|
||||
deskentry = menuentry.DesktopEntry
|
||||
|
||||
if name:
|
||||
if not deskentry.hasKey("Name"):
|
||||
deskentry.set("Name", name)
|
||||
deskentry.set("Name", name, locale=True)
|
||||
if comment:
|
||||
if not deskentry.hasKey("Comment"):
|
||||
deskentry.set("Comment", comment)
|
||||
deskentry.set("Comment", comment, locale=True)
|
||||
if genericname:
|
||||
if not deskentry.hasKey("GenericName"):
|
||||
deskentry.set("GenericName", genericname)
|
||||
deskentry.set("GenericName", genericname, locale=True)
|
||||
if command:
|
||||
deskentry.set("Exec", command)
|
||||
if icon:
|
||||
deskentry.set("Icon", icon)
|
||||
|
||||
if terminal:
|
||||
deskentry.set("Terminal", "true")
|
||||
elif not terminal:
|
||||
deskentry.set("Terminal", "false")
|
||||
|
||||
if nodisplay is True:
|
||||
deskentry.set("NoDisplay", "true")
|
||||
elif nodisplay is False:
|
||||
deskentry.set("NoDisplay", "false")
|
||||
|
||||
if hidden is True:
|
||||
deskentry.set("Hidden", "true")
|
||||
elif hidden is False:
|
||||
deskentry.set("Hidden", "false")
|
||||
|
||||
menuentry.updateAttributes()
|
||||
|
||||
if len(menuentry.Parents) > 0:
|
||||
self.menu.sort()
|
||||
|
||||
return menuentry
|
||||
|
||||
def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
|
||||
# Hack for legacy dirs
|
||||
if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
|
||||
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||
self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
|
||||
menu.Directory.setAttributes(menu.Name + ".directory")
|
||||
# Hack for New Entries
|
||||
elif not isinstance(menu.Directory, MenuEntry):
|
||||
if not name:
|
||||
name = menu.Name
|
||||
filename = self.__getFileName(name, ".directory").replace("/", "")
|
||||
if not menu.Name:
|
||||
menu.Name = filename.replace(".directory", "")
|
||||
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||
self.__addXmlTextElement(xml_menu, 'Directory', filename)
|
||||
menu.Directory = MenuEntry(filename)
|
||||
|
||||
deskentry = menu.Directory.DesktopEntry
|
||||
|
||||
if name:
|
||||
if not deskentry.hasKey("Name"):
|
||||
deskentry.set("Name", name)
|
||||
deskentry.set("Name", name, locale=True)
|
||||
if genericname:
|
||||
if not deskentry.hasKey("GenericName"):
|
||||
deskentry.set("GenericName", genericname)
|
||||
deskentry.set("GenericName", genericname, locale=True)
|
||||
if comment:
|
||||
if not deskentry.hasKey("Comment"):
|
||||
deskentry.set("Comment", comment)
|
||||
deskentry.set("Comment", comment, locale=True)
|
||||
if icon:
|
||||
deskentry.set("Icon", icon)
|
||||
|
||||
if nodisplay is True:
|
||||
deskentry.set("NoDisplay", "true")
|
||||
elif nodisplay is False:
|
||||
deskentry.set("NoDisplay", "false")
|
||||
|
||||
if hidden is True:
|
||||
deskentry.set("Hidden", "true")
|
||||
elif hidden is False:
|
||||
deskentry.set("Hidden", "false")
|
||||
|
||||
menu.Directory.updateAttributes()
|
||||
|
||||
if isinstance(menu.Parent, Menu):
|
||||
self.menu.sort()
|
||||
|
||||
return menu
|
||||
|
||||
def hideMenuEntry(self, menuentry):
|
||||
self.editMenuEntry(menuentry, nodisplay=True)
|
||||
|
||||
def unhideMenuEntry(self, menuentry):
|
||||
self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
|
||||
|
||||
def hideMenu(self, menu):
|
||||
self.editMenu(menu, nodisplay=True)
|
||||
|
||||
def unhideMenu(self, menu):
|
||||
self.editMenu(menu, nodisplay=False, hidden=False)
|
||||
xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
|
||||
deleted = xml_menu.findall('Deleted')
|
||||
not_deleted = xml_menu.findall('NotDeleted')
|
||||
for node in deleted + not_deleted:
|
||||
xml_menu.remove(node)
|
||||
|
||||
def deleteMenuEntry(self, menuentry):
|
||||
if self.getAction(menuentry) == "delete":
|
||||
self.__deleteFile(menuentry.DesktopEntry.filename)
|
||||
for parent in menuentry.Parents:
|
||||
self.__deleteEntry(parent, menuentry)
|
||||
self.menu.sort()
|
||||
return menuentry
|
||||
|
||||
def revertMenuEntry(self, menuentry):
|
||||
if self.getAction(menuentry) == "revert":
|
||||
self.__deleteFile(menuentry.DesktopEntry.filename)
|
||||
menuentry.Original.Parents = []
|
||||
for parent in menuentry.Parents:
|
||||
index = parent.Entries.index(menuentry)
|
||||
parent.Entries[index] = menuentry.Original
|
||||
index = parent.MenuEntries.index(menuentry)
|
||||
parent.MenuEntries[index] = menuentry.Original
|
||||
menuentry.Original.Parents.append(parent)
|
||||
self.menu.sort()
|
||||
return menuentry
|
||||
|
||||
def deleteMenu(self, menu):
|
||||
if self.getAction(menu) == "delete":
|
||||
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
||||
self.__deleteEntry(menu.Parent, menu)
|
||||
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||
parent = self.__get_parent_node(xml_menu)
|
||||
parent.remove(xml_menu)
|
||||
self.menu.sort()
|
||||
return menu
|
||||
|
||||
def revertMenu(self, menu):
|
||||
if self.getAction(menu) == "revert":
|
||||
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
||||
menu.Directory = menu.Directory.Original
|
||||
self.menu.sort()
|
||||
return menu
|
||||
|
||||
def deleteSeparator(self, separator):
|
||||
self.__deleteEntry(separator.Parent, separator, after=True)
|
||||
|
||||
self.menu.sort()
|
||||
|
||||
return separator
|
||||
|
||||
""" Private Stuff """
|
||||
def getAction(self, entry):
|
||||
if isinstance(entry, Menu):
|
||||
if not isinstance(entry.Directory, MenuEntry):
|
||||
return "none"
|
||||
elif entry.Directory.getType() == "Both":
|
||||
return "revert"
|
||||
elif entry.Directory.getType() == "User" and (
|
||||
len(entry.Submenus) + len(entry.MenuEntries)
|
||||
) == 0:
|
||||
return "delete"
|
||||
|
||||
elif isinstance(entry, MenuEntry):
|
||||
if entry.getType() == "Both":
|
||||
return "revert"
|
||||
elif entry.getType() == "User":
|
||||
return "delete"
|
||||
else:
|
||||
return "none"
|
||||
|
||||
return "none"
|
||||
|
||||
def __saveEntries(self, menu):
|
||||
if not menu:
|
||||
menu = self.menu
|
||||
if isinstance(menu.Directory, MenuEntry):
|
||||
menu.Directory.save()
|
||||
for entry in menu.getEntries(hidden=True):
|
||||
if isinstance(entry, MenuEntry):
|
||||
entry.save()
|
||||
elif isinstance(entry, Menu):
|
||||
self.__saveEntries(entry)
|
||||
|
||||
def __saveMenu(self):
|
||||
if not os.path.isdir(os.path.dirname(self.filename)):
|
||||
os.makedirs(os.path.dirname(self.filename))
|
||||
self.tree.write(self.filename, encoding='utf-8')
|
||||
|
||||
def __getFileName(self, name, extension):
|
||||
postfix = 0
|
||||
while 1:
|
||||
if postfix == 0:
|
||||
filename = name + extension
|
||||
else:
|
||||
filename = name + "-" + str(postfix) + extension
|
||||
if extension == ".desktop":
|
||||
dir = "applications"
|
||||
elif extension == ".directory":
|
||||
dir = "desktop-directories"
|
||||
if not filename in self.filenames and not os.path.isfile(
|
||||
os.path.join(xdg_data_dirs[0], dir, filename)
|
||||
):
|
||||
self.filenames.append(filename)
|
||||
break
|
||||
else:
|
||||
postfix += 1
|
||||
|
||||
return filename
|
||||
|
||||
def __getXmlMenu(self, path, create=True, element=None):
|
||||
# FIXME: we should also return the menu's parent,
|
||||
# to avoid looking for it later on
|
||||
# @see Element.getiterator()
|
||||
if not element:
|
||||
element = self.tree
|
||||
|
||||
if "/" in path:
|
||||
(name, path) = path.split("/", 1)
|
||||
else:
|
||||
name = path
|
||||
path = ""
|
||||
|
||||
found = None
|
||||
for node in element.findall("Menu"):
|
||||
name_node = node.find('Name')
|
||||
if name_node.text == name:
|
||||
if path:
|
||||
found = self.__getXmlMenu(path, create, node)
|
||||
else:
|
||||
found = node
|
||||
if found:
|
||||
break
|
||||
if not found and create:
|
||||
node = self.__addXmlMenuElement(element, name)
|
||||
if path:
|
||||
found = self.__getXmlMenu(path, create, node)
|
||||
else:
|
||||
found = node
|
||||
|
||||
return found
|
||||
|
||||
def __addXmlMenuElement(self, element, name):
|
||||
menu_node = etree.SubElement('Menu', element)
|
||||
name_node = etree.SubElement('Name', menu_node)
|
||||
name_node.text = name
|
||||
return menu_node
|
||||
|
||||
def __addXmlTextElement(self, element, name, text):
|
||||
node = etree.SubElement(name, element)
|
||||
node.text = text
|
||||
return node
|
||||
|
||||
def __addXmlFilename(self, element, filename, type_="Include"):
|
||||
# remove old filenames
|
||||
includes = element.findall('Include')
|
||||
excludes = element.findall('Exclude')
|
||||
rules = includes + excludes
|
||||
for rule in rules:
|
||||
#FIXME: this finds only Rules whose FIRST child is a Filename element
|
||||
if rule[0].tag == "Filename" and rule[0].text == filename:
|
||||
element.remove(rule)
|
||||
# shouldn't it remove all occurences, like the following:
|
||||
#filename_nodes = rule.findall('.//Filename'):
|
||||
#for fn in filename_nodes:
|
||||
#if fn.text == filename:
|
||||
##element.remove(rule)
|
||||
#parent = self.__get_parent_node(fn)
|
||||
#parent.remove(fn)
|
||||
|
||||
# add new filename
|
||||
node = etree.SubElement(type_, element)
|
||||
self.__addXmlTextElement(node, 'Filename', filename)
|
||||
return node
|
||||
|
||||
def __addXmlMove(self, element, old, new):
|
||||
node = etree.SubElement("Move", element)
|
||||
self.__addXmlTextElement(node, 'Old', old)
|
||||
self.__addXmlTextElement(node, 'New', new)
|
||||
return node
|
||||
|
||||
def __addXmlLayout(self, element, layout):
|
||||
# remove old layout
|
||||
for node in element.findall("Layout"):
|
||||
element.remove(node)
|
||||
|
||||
# add new layout
|
||||
node = etree.SubElement("Layout", element)
|
||||
for order in layout.order:
|
||||
if order[0] == "Separator":
|
||||
child = etree.SubElement("Separator", node)
|
||||
elif order[0] == "Filename":
|
||||
child = self.__addXmlTextElement(node, "Filename", order[1])
|
||||
elif order[0] == "Menuname":
|
||||
child = self.__addXmlTextElement(node, "Menuname", order[1])
|
||||
elif order[0] == "Merge":
|
||||
child = etree.SubElement("Merge", node)
|
||||
child.attrib["type"] = order[1]
|
||||
return node
|
||||
|
||||
def __addLayout(self, parent):
|
||||
layout = Layout()
|
||||
layout.order = []
|
||||
layout.show_empty = parent.Layout.show_empty
|
||||
layout.inline = parent.Layout.inline
|
||||
layout.inline_header = parent.Layout.inline_header
|
||||
layout.inline_alias = parent.Layout.inline_alias
|
||||
layout.inline_limit = parent.Layout.inline_limit
|
||||
|
||||
layout.order.append(["Merge", "menus"])
|
||||
for entry in parent.Entries:
|
||||
if isinstance(entry, Menu):
|
||||
layout.parseMenuname(entry.Name)
|
||||
elif isinstance(entry, MenuEntry):
|
||||
layout.parseFilename(entry.DesktopFileID)
|
||||
elif isinstance(entry, Separator):
|
||||
layout.parseSeparator()
|
||||
layout.order.append(["Merge", "files"])
|
||||
|
||||
parent.Layout = layout
|
||||
|
||||
return layout
|
||||
|
||||
def __addEntry(self, parent, entry, after=None, before=None):
|
||||
if after or before:
|
||||
if after:
|
||||
index = parent.Entries.index(after) + 1
|
||||
elif before:
|
||||
index = parent.Entries.index(before)
|
||||
parent.Entries.insert(index, entry)
|
||||
else:
|
||||
parent.Entries.append(entry)
|
||||
|
||||
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
|
||||
|
||||
if isinstance(entry, MenuEntry):
|
||||
parent.MenuEntries.append(entry)
|
||||
entry.Parents.append(parent)
|
||||
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
|
||||
elif isinstance(entry, Menu):
|
||||
parent.addSubmenu(entry)
|
||||
|
||||
if after or before:
|
||||
self.__addLayout(parent)
|
||||
self.__addXmlLayout(xml_parent, parent.Layout)
|
||||
|
||||
def __deleteEntry(self, parent, entry, after=None, before=None):
|
||||
parent.Entries.remove(entry)
|
||||
|
||||
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
|
||||
|
||||
if isinstance(entry, MenuEntry):
|
||||
entry.Parents.remove(parent)
|
||||
parent.MenuEntries.remove(entry)
|
||||
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
|
||||
elif isinstance(entry, Menu):
|
||||
parent.Submenus.remove(entry)
|
||||
|
||||
if after or before:
|
||||
self.__addLayout(parent)
|
||||
self.__addXmlLayout(xml_parent, parent.Layout)
|
||||
|
||||
def __deleteFile(self, filename):
|
||||
try:
|
||||
os.remove(filename)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
self.filenames.remove(filename)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def __remove_whitespace_nodes(self, node):
|
||||
for child in node:
|
||||
text = child.text.strip()
|
||||
if not text:
|
||||
child.text = ''
|
||||
tail = child.tail.strip()
|
||||
if not tail:
|
||||
child.tail = ''
|
||||
if len(child):
|
||||
self.__remove_whilespace_nodes(child)
|
||||
|
||||
def __get_parent_node(self, node):
|
||||
# elements in ElementTree doesn't hold a reference to their parent
|
||||
for parent, child in self.__iter_parent():
|
||||
if child is node:
|
||||
return child
|
||||
|
||||
def __iter_parent(self):
|
||||
for parent in self.tree.getiterator():
|
||||
for child in parent:
|
||||
yield parent, child
|
780
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Mime.py
Normal file
780
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/Mime.py
Normal file
@@ -0,0 +1,780 @@
|
||||
"""
|
||||
This module is based on a rox module (LGPL):
|
||||
|
||||
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
|
||||
|
||||
This module provides access to the shared MIME database.
|
||||
|
||||
types is a dictionary of all known MIME types, indexed by the type name, e.g.
|
||||
types['application/x-python']
|
||||
|
||||
Applications can install information about MIME types by storing an
|
||||
XML file as <MIME>/packages/<application>.xml and running the
|
||||
update-mime-database command, which is provided by the freedesktop.org
|
||||
shared mime database package.
|
||||
|
||||
See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
|
||||
information about the format of these files.
|
||||
|
||||
(based on version 0.13)
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import fnmatch
|
||||
|
||||
from . import BaseDirectory, Locale
|
||||
|
||||
from .dom import minidom, XML_NAMESPACE
|
||||
from collections import defaultdict
|
||||
|
||||
FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
|
||||
|
||||
types = {} # Maps MIME names to type objects
|
||||
|
||||
exts = None # Maps extensions to types
|
||||
globs = None # List of (glob, type) pairs
|
||||
literals = None # Maps liternal names to types
|
||||
magic = None
|
||||
|
||||
PY3 = (sys.version_info[0] >= 3)
|
||||
|
||||
def _get_node_data(node):
|
||||
"""Get text of XML node"""
|
||||
return ''.join([n.nodeValue for n in node.childNodes]).strip()
|
||||
|
||||
def lookup(media, subtype = None):
|
||||
"""Get the MIMEtype object for the given type.
|
||||
|
||||
This remains for backwards compatibility; calling MIMEtype now does
|
||||
the same thing.
|
||||
|
||||
The name can either be passed as one part ('text/plain'), or as two
|
||||
('text', 'plain').
|
||||
"""
|
||||
return MIMEtype(media, subtype)
|
||||
|
||||
class MIMEtype(object):
|
||||
"""Class holding data about a MIME type.
|
||||
|
||||
Calling the class will return a cached instance, so there is only one
|
||||
instance for each MIME type. The name can either be passed as one part
|
||||
('text/plain'), or as two ('text', 'plain').
|
||||
"""
|
||||
def __new__(cls, media, subtype=None):
|
||||
if subtype is None and '/' in media:
|
||||
media, subtype = media.split('/', 1)
|
||||
assert '/' not in subtype
|
||||
media = media.lower()
|
||||
subtype = subtype.lower()
|
||||
|
||||
try:
|
||||
return types[(media, subtype)]
|
||||
except KeyError:
|
||||
mtype = super(MIMEtype, cls).__new__(cls)
|
||||
mtype._init(media, subtype)
|
||||
types[(media, subtype)] = mtype
|
||||
return mtype
|
||||
|
||||
# If this is done in __init__, it is automatically called again each time
|
||||
# the MIMEtype is returned by __new__, which we don't want. So we call it
|
||||
# explicitly only when we construct a new instance.
|
||||
def _init(self, media, subtype):
|
||||
self.media = media
|
||||
self.subtype = subtype
|
||||
self._comment = None
|
||||
|
||||
def _load(self):
|
||||
"Loads comment for current language. Use get_comment() instead."
|
||||
resource = os.path.join('mime', self.media, self.subtype + '.xml')
|
||||
for path in BaseDirectory.load_data_paths(resource):
|
||||
doc = minidom.parse(path)
|
||||
if doc is None:
|
||||
continue
|
||||
for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
|
||||
lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
|
||||
goodness = 1 + (lang in xdg.Locale.langs)
|
||||
if goodness > self._comment[0]:
|
||||
self._comment = (goodness, _get_node_data(comment))
|
||||
if goodness == 2: return
|
||||
|
||||
# FIXME: add get_icon method
|
||||
def get_comment(self):
|
||||
"""Returns comment for current language, loading it if needed."""
|
||||
# Should we ever reload?
|
||||
if self._comment is None:
|
||||
self._comment = (0, str(self))
|
||||
self._load()
|
||||
return self._comment[1]
|
||||
|
||||
def canonical(self):
|
||||
"""Returns the canonical MimeType object if this is an alias."""
|
||||
update_cache()
|
||||
s = str(self)
|
||||
if s in aliases:
|
||||
return lookup(aliases[s])
|
||||
return self
|
||||
|
||||
def inherits_from(self):
|
||||
"""Returns a set of Mime types which this inherits from."""
|
||||
update_cache()
|
||||
return set(lookup(t) for t in inheritance[str(self)])
|
||||
|
||||
def __str__(self):
|
||||
return self.media + '/' + self.subtype
|
||||
|
||||
def __repr__(self):
|
||||
return 'MIMEtype(%r, %r)' % (self.media, self.subtype)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.media) ^ hash(self.subtype)
|
||||
|
||||
class UnknownMagicRuleFormat(ValueError):
|
||||
pass
|
||||
|
||||
class DiscardMagicRules(Exception):
|
||||
"Raised when __NOMAGIC__ is found, and caught to discard previous rules."
|
||||
pass
|
||||
|
||||
class MagicRule:
|
||||
also = None
|
||||
|
||||
def __init__(self, start, value, mask, word, range):
|
||||
self.start = start
|
||||
self.value = value
|
||||
self.mask = mask
|
||||
self.word = word
|
||||
self.range = range
|
||||
|
||||
rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$')
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, f):
|
||||
"""Read a rule from the binary magics file. Returns a 2-tuple of
|
||||
the nesting depth and the MagicRule."""
|
||||
line = f.readline()
|
||||
#print line
|
||||
|
||||
# [indent] '>'
|
||||
nest_depth, line = line.split(b'>', 1)
|
||||
nest_depth = int(nest_depth) if nest_depth else 0
|
||||
|
||||
# start-offset '='
|
||||
start, line = line.split(b'=', 1)
|
||||
start = int(start)
|
||||
|
||||
if line == b'__NOMAGIC__\n':
|
||||
raise DiscardMagicRules
|
||||
|
||||
# value length (2 bytes, big endian)
|
||||
if sys.version_info[0] >= 3:
|
||||
lenvalue = int.from_bytes(line[:2], byteorder='big')
|
||||
else:
|
||||
lenvalue = (ord(line[0])<<8)+ord(line[1])
|
||||
line = line[2:]
|
||||
|
||||
# value
|
||||
# This can contain newlines, so we may need to read more lines
|
||||
while len(line) <= lenvalue:
|
||||
line += f.readline()
|
||||
value, line = line[:lenvalue], line[lenvalue:]
|
||||
|
||||
# ['&' mask]
|
||||
if line.startswith(b'&'):
|
||||
# This can contain newlines, so we may need to read more lines
|
||||
while len(line) <= lenvalue:
|
||||
line += f.readline()
|
||||
mask, line = line[1:lenvalue+1], line[lenvalue+1:]
|
||||
else:
|
||||
mask = None
|
||||
|
||||
# ['~' word-size] ['+' range-length]
|
||||
ending = cls.rule_ending_re.match(line)
|
||||
if not ending:
|
||||
# Per the spec, this will be caught and ignored, to allow
|
||||
# for future extensions.
|
||||
raise UnknownMagicRuleFormat(repr(line))
|
||||
|
||||
word, range = ending.groups()
|
||||
word = int(word) if (word is not None) else 1
|
||||
range = int(range) if (range is not None) else 1
|
||||
|
||||
return nest_depth, cls(start, value, mask, word, range)
|
||||
|
||||
def maxlen(self):
|
||||
l = self.start + len(self.value) + self.range
|
||||
if self.also:
|
||||
return max(l, self.also.maxlen())
|
||||
return l
|
||||
|
||||
def match(self, buffer):
|
||||
if self.match0(buffer):
|
||||
if self.also:
|
||||
return self.also.match(buffer)
|
||||
return True
|
||||
|
||||
def match0(self, buffer):
|
||||
l=len(buffer)
|
||||
lenvalue = len(self.value)
|
||||
for o in range(self.range):
|
||||
s=self.start+o
|
||||
e=s+lenvalue
|
||||
if l<e:
|
||||
return False
|
||||
if self.mask:
|
||||
test=''
|
||||
for i in range(lenvalue):
|
||||
if PY3:
|
||||
c = buffer[s+i] & self.mask[i]
|
||||
else:
|
||||
c = ord(buffer[s+i]) & ord(self.mask[i])
|
||||
test += chr(c)
|
||||
else:
|
||||
test = buffer[s:e]
|
||||
|
||||
if test==self.value:
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return 'MagicRule(start=%r, value=%r, mask=%r, word=%r, range=%r)' %(
|
||||
self.start,
|
||||
self.value,
|
||||
self.mask,
|
||||
self.word,
|
||||
self.range)
|
||||
|
||||
|
||||
class MagicMatchAny(object):
|
||||
"""Match any of a set of magic rules.
|
||||
|
||||
This has a similar interface to MagicRule objects (i.e. its match() and
|
||||
maxlen() methods), to allow for duck typing.
|
||||
"""
|
||||
def __init__(self, rules):
|
||||
self.rules = rules
|
||||
|
||||
def match(self, buffer):
|
||||
return any(r.match(buffer) for r in self.rules)
|
||||
|
||||
def maxlen(self):
|
||||
return max(r.maxlen() for r in self.rules)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, f):
|
||||
"""Read a set of rules from the binary magic file."""
|
||||
c=f.read(1)
|
||||
f.seek(-1, 1)
|
||||
depths_rules = []
|
||||
while c and c != b'[':
|
||||
try:
|
||||
depths_rules.append(MagicRule.from_file(f))
|
||||
except UnknownMagicRuleFormat:
|
||||
# Ignored to allow for extensions to the rule format.
|
||||
pass
|
||||
c=f.read(1)
|
||||
if c:
|
||||
f.seek(-1, 1)
|
||||
|
||||
# Build the rule tree
|
||||
tree = [] # (rule, [(subrule,[subsubrule,...]), ...])
|
||||
insert_points = {0:tree}
|
||||
for depth, rule in depths_rules:
|
||||
subrules = []
|
||||
insert_points[depth].append((rule, subrules))
|
||||
insert_points[depth+1] = subrules
|
||||
|
||||
return cls.from_rule_tree(tree)
|
||||
|
||||
@classmethod
|
||||
def from_rule_tree(cls, tree):
|
||||
"""From a nested list of (rule, subrules) pairs, build a MagicMatchAny
|
||||
instance, recursing down the tree.
|
||||
|
||||
Where there's only one top-level rule, this is returned directly,
|
||||
to simplify the nested structure. Returns None if no rules were read.
|
||||
"""
|
||||
rules = []
|
||||
for rule, subrules in tree:
|
||||
if subrules:
|
||||
rule.also = cls.from_rule_tree(subrules)
|
||||
rules.append(rule)
|
||||
|
||||
if len(rules)==0:
|
||||
return None
|
||||
if len(rules)==1:
|
||||
return rules[0]
|
||||
return cls(rules)
|
||||
|
||||
class MagicDB:
|
||||
def __init__(self):
|
||||
self.bytype = defaultdict(list) # mimetype -> [(priority, rule), ...]
|
||||
|
||||
def merge_file(self, fname):
|
||||
"""Read a magic binary file, and add its rules to this MagicDB."""
|
||||
with open(fname, 'rb') as f:
|
||||
line = f.readline()
|
||||
if line != b'MIME-Magic\0\n':
|
||||
raise IOError('Not a MIME magic file')
|
||||
|
||||
while True:
|
||||
shead = f.readline().decode('ascii')
|
||||
#print(shead)
|
||||
if not shead:
|
||||
break
|
||||
if shead[0] != '[' or shead[-2:] != ']\n':
|
||||
raise ValueError('Malformed section heading', shead)
|
||||
pri, tname = shead[1:-2].split(':')
|
||||
#print shead[1:-2]
|
||||
pri = int(pri)
|
||||
mtype = lookup(tname)
|
||||
try:
|
||||
rule = MagicMatchAny.from_file(f)
|
||||
except DiscardMagicRules:
|
||||
self.bytype.pop(mtype, None)
|
||||
rule = MagicMatchAny.from_file(f)
|
||||
if rule is None:
|
||||
continue
|
||||
#print rule
|
||||
|
||||
self.bytype[mtype].append((pri, rule))
|
||||
|
||||
def finalise(self):
|
||||
"""Prepare the MagicDB for matching.
|
||||
|
||||
This should be called after all rules have been merged into it.
|
||||
"""
|
||||
maxlen = 0
|
||||
self.alltypes = [] # (priority, mimetype, rule)
|
||||
|
||||
for mtype, rules in self.bytype.items():
|
||||
for pri, rule in rules:
|
||||
self.alltypes.append((pri, mtype, rule))
|
||||
maxlen = max(maxlen, rule.maxlen())
|
||||
|
||||
self.maxlen = maxlen # Number of bytes to read from files
|
||||
self.alltypes.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
def match_data(self, data, max_pri=100, min_pri=0, possible=None):
|
||||
"""Do magic sniffing on some bytes.
|
||||
|
||||
max_pri & min_pri can be used to specify the maximum & minimum priority
|
||||
rules to look for. possible can be a list of mimetypes to check, or None
|
||||
(the default) to check all mimetypes until one matches.
|
||||
|
||||
Returns the MIMEtype found, or None if no entries match.
|
||||
"""
|
||||
if possible is not None:
|
||||
types = []
|
||||
for mt in possible:
|
||||
for pri, rule in self.bytype[mt]:
|
||||
types.append((pri, mt, rule))
|
||||
types.sort(key=lambda x: x[0])
|
||||
else:
|
||||
types = self.alltypes
|
||||
|
||||
for priority, mimetype, rule in types:
|
||||
#print priority, max_pri, min_pri
|
||||
if priority > max_pri:
|
||||
continue
|
||||
if priority < min_pri:
|
||||
break
|
||||
|
||||
if rule.match(data):
|
||||
return mimetype
|
||||
|
||||
def match(self, path, max_pri=100, min_pri=0, possible=None):
|
||||
"""Read data from the file and do magic sniffing on it.
|
||||
|
||||
max_pri & min_pri can be used to specify the maximum & minimum priority
|
||||
rules to look for. possible can be a list of mimetypes to check, or None
|
||||
(the default) to check all mimetypes until one matches.
|
||||
|
||||
Returns the MIMEtype found, or None if no entries match. Raises IOError
|
||||
if the file can't be opened.
|
||||
"""
|
||||
with open(path, 'rb') as f:
|
||||
buf = f.read(self.maxlen)
|
||||
return self.match_data(buf, max_pri, min_pri, possible)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MagicDB (%d types)>' % len(self.alltypes)
|
||||
|
||||
class GlobDB(object):
|
||||
def __init__(self):
|
||||
"""Prepare the GlobDB. It can't actually be used until .finalise() is
|
||||
called, but merge_file() can be used to add data before that.
|
||||
"""
|
||||
# Maps mimetype to {(weight, glob, flags), ...}
|
||||
self.allglobs = defaultdict(set)
|
||||
|
||||
def merge_file(self, path):
|
||||
"""Loads name matching information from a globs2 file."""#
|
||||
allglobs = self.allglobs
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
if line.startswith('#'): continue # Comment
|
||||
|
||||
fields = line[:-1].split(':')
|
||||
weight, type_name, pattern = fields[:3]
|
||||
weight = int(weight)
|
||||
mtype = lookup(type_name)
|
||||
if len(fields) > 3:
|
||||
flags = fields[3].split(',')
|
||||
else:
|
||||
flags = ()
|
||||
|
||||
if pattern == '__NOGLOBS__':
|
||||
# This signals to discard any previous globs
|
||||
allglobs.pop(mtype, None)
|
||||
continue
|
||||
|
||||
allglobs[mtype].add((weight, pattern, tuple(flags)))
|
||||
|
||||
def finalise(self):
|
||||
"""Prepare the GlobDB for matching.
|
||||
|
||||
This should be called after all files have been merged into it.
|
||||
"""
|
||||
self.exts = defaultdict(list) # Maps extensions to [(type, weight),...]
|
||||
self.cased_exts = defaultdict(list)
|
||||
self.globs = [] # List of (regex, type, weight) triplets
|
||||
self.literals = {} # Maps literal names to (type, weight)
|
||||
self.cased_literals = {}
|
||||
|
||||
for mtype, globs in self.allglobs.items():
|
||||
mtype = mtype.canonical()
|
||||
for weight, pattern, flags in globs:
|
||||
|
||||
cased = 'cs' in flags
|
||||
|
||||
if pattern.startswith('*.'):
|
||||
# *.foo -- extension pattern
|
||||
rest = pattern[2:]
|
||||
if not ('*' in rest or '[' in rest or '?' in rest):
|
||||
if cased:
|
||||
self.cased_exts[rest].append((mtype, weight))
|
||||
else:
|
||||
self.exts[rest.lower()].append((mtype, weight))
|
||||
continue
|
||||
|
||||
if ('*' in pattern or '[' in pattern or '?' in pattern):
|
||||
# Translate the glob pattern to a regex & compile it
|
||||
re_flags = 0 if cased else re.I
|
||||
pattern = re.compile(fnmatch.translate(pattern), flags=re_flags)
|
||||
self.globs.append((pattern, mtype, weight))
|
||||
else:
|
||||
# No wildcards - literal pattern
|
||||
if cased:
|
||||
self.cased_literals[pattern] = (mtype, weight)
|
||||
else:
|
||||
self.literals[pattern.lower()] = (mtype, weight)
|
||||
|
||||
# Sort globs by weight & length
|
||||
self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) )
|
||||
|
||||
def first_match(self, path):
|
||||
"""Return the first match found for a given path, or None if no match
|
||||
is found."""
|
||||
try:
|
||||
return next(self._match_path(path))[0]
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
def all_matches(self, path):
|
||||
"""Return a list of (MIMEtype, glob weight) pairs for the path."""
|
||||
return list(self._match_path(path))
|
||||
|
||||
def _match_path(self, path):
|
||||
"""Yields pairs of (mimetype, glob weight)."""
|
||||
leaf = os.path.basename(path)
|
||||
|
||||
# Literals (no wildcards)
|
||||
if leaf in self.cased_literals:
|
||||
yield self.cased_literals[leaf]
|
||||
|
||||
lleaf = leaf.lower()
|
||||
if lleaf in self.literals:
|
||||
yield self.literals[lleaf]
|
||||
|
||||
# Extensions
|
||||
ext = leaf
|
||||
while 1:
|
||||
p = ext.find('.')
|
||||
if p < 0: break
|
||||
ext = ext[p + 1:]
|
||||
if ext in self.cased_exts:
|
||||
for res in self.cased_exts[ext]:
|
||||
yield res
|
||||
ext = lleaf
|
||||
while 1:
|
||||
p = ext.find('.')
|
||||
if p < 0: break
|
||||
ext = ext[p+1:]
|
||||
if ext in self.exts:
|
||||
for res in self.exts[ext]:
|
||||
yield res
|
||||
|
||||
# Other globs
|
||||
for (regex, mime_type, weight) in self.globs:
|
||||
if regex.match(leaf):
|
||||
yield (mime_type, weight)
|
||||
|
||||
# Some well-known types
|
||||
text = lookup('text', 'plain')
|
||||
octet_stream = lookup('application', 'octet-stream')
|
||||
inode_block = lookup('inode', 'blockdevice')
|
||||
inode_char = lookup('inode', 'chardevice')
|
||||
inode_dir = lookup('inode', 'directory')
|
||||
inode_fifo = lookup('inode', 'fifo')
|
||||
inode_socket = lookup('inode', 'socket')
|
||||
inode_symlink = lookup('inode', 'symlink')
|
||||
inode_door = lookup('inode', 'door')
|
||||
app_exe = lookup('application', 'executable')
|
||||
|
||||
_cache_uptodate = False
|
||||
|
||||
def _cache_database():
|
||||
global globs, magic, aliases, inheritance, _cache_uptodate
|
||||
|
||||
_cache_uptodate = True
|
||||
|
||||
aliases = {} # Maps alias Mime types to canonical names
|
||||
inheritance = defaultdict(set) # Maps to sets of parent mime types.
|
||||
|
||||
# Load aliases
|
||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')):
|
||||
with open(path, 'r') as f:
|
||||
for line in f:
|
||||
alias, canonical = line.strip().split(None, 1)
|
||||
aliases[alias] = canonical
|
||||
|
||||
# Load filename patterns (globs)
|
||||
globs = GlobDB()
|
||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')):
|
||||
globs.merge_file(path)
|
||||
globs.finalise()
|
||||
|
||||
# Load magic sniffing data
|
||||
magic = MagicDB()
|
||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
|
||||
magic.merge_file(path)
|
||||
magic.finalise()
|
||||
|
||||
# Load subclasses
|
||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
|
||||
with open(path, 'r') as f:
|
||||
for line in f:
|
||||
sub, parent = line.strip().split(None, 1)
|
||||
inheritance[sub].add(parent)
|
||||
|
||||
def update_cache():
|
||||
if not _cache_uptodate:
|
||||
_cache_database()
|
||||
|
||||
def get_type_by_name(path):
|
||||
"""Returns type of file by its name, or None if not known"""
|
||||
update_cache()
|
||||
return globs.first_match(path)
|
||||
|
||||
def get_type_by_contents(path, max_pri=100, min_pri=0):
|
||||
"""Returns type of file by its contents, or None if not known"""
|
||||
update_cache()
|
||||
|
||||
return magic.match(path, max_pri, min_pri)
|
||||
|
||||
def get_type_by_data(data, max_pri=100, min_pri=0):
|
||||
"""Returns type of the data, which should be bytes."""
|
||||
update_cache()
|
||||
|
||||
return magic.match_data(data, max_pri, min_pri)
|
||||
|
||||
def _get_type_by_stat(st_mode):
|
||||
"""Match special filesystem objects to Mimetypes."""
|
||||
if stat.S_ISDIR(st_mode): return inode_dir
|
||||
elif stat.S_ISCHR(st_mode): return inode_char
|
||||
elif stat.S_ISBLK(st_mode): return inode_block
|
||||
elif stat.S_ISFIFO(st_mode): return inode_fifo
|
||||
elif stat.S_ISLNK(st_mode): return inode_symlink
|
||||
elif stat.S_ISSOCK(st_mode): return inode_socket
|
||||
return inode_door
|
||||
|
||||
def get_type(path, follow=True, name_pri=100):
|
||||
"""Returns type of file indicated by path.
|
||||
|
||||
This function is *deprecated* - :func:`get_type2` is more accurate.
|
||||
|
||||
:param path: pathname to check (need not exist)
|
||||
:param follow: when reading file, follow symbolic links
|
||||
:param name_pri: Priority to do name matches. 100=override magic
|
||||
|
||||
This tries to use the contents of the file, and falls back to the name. It
|
||||
can also handle special filesystem objects like directories and sockets.
|
||||
"""
|
||||
update_cache()
|
||||
|
||||
try:
|
||||
if follow:
|
||||
st = os.stat(path)
|
||||
else:
|
||||
st = os.lstat(path)
|
||||
except:
|
||||
t = get_type_by_name(path)
|
||||
return t or text
|
||||
|
||||
if stat.S_ISREG(st.st_mode):
|
||||
# Regular file
|
||||
t = get_type_by_contents(path, min_pri=name_pri)
|
||||
if not t: t = get_type_by_name(path)
|
||||
if not t: t = get_type_by_contents(path, max_pri=name_pri)
|
||||
if t is None:
|
||||
if stat.S_IMODE(st.st_mode) & 0o111:
|
||||
return app_exe
|
||||
else:
|
||||
return text
|
||||
return t
|
||||
else:
|
||||
return _get_type_by_stat(st.st_mode)
|
||||
|
||||
def get_type2(path, follow=True):
|
||||
"""Find the MIMEtype of a file using the XDG recommended checking order.
|
||||
|
||||
This first checks the filename, then uses file contents if the name doesn't
|
||||
give an unambiguous MIMEtype. It can also handle special filesystem objects
|
||||
like directories and sockets.
|
||||
|
||||
:param path: file path to examine (need not exist)
|
||||
:param follow: whether to follow symlinks
|
||||
|
||||
:rtype: :class:`MIMEtype`
|
||||
|
||||
.. versionadded:: 1.0
|
||||
"""
|
||||
update_cache()
|
||||
|
||||
try:
|
||||
st = os.stat(path) if follow else os.lstat(path)
|
||||
except OSError:
|
||||
return get_type_by_name(path) or octet_stream
|
||||
|
||||
if not stat.S_ISREG(st.st_mode):
|
||||
# Special filesystem objects
|
||||
return _get_type_by_stat(st.st_mode)
|
||||
|
||||
mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True)
|
||||
if mtypes:
|
||||
max_weight = mtypes[0][1]
|
||||
i = 1
|
||||
for mt, w in mtypes[1:]:
|
||||
if w < max_weight:
|
||||
break
|
||||
i += 1
|
||||
mtypes = mtypes[:i]
|
||||
if len(mtypes) == 1:
|
||||
return mtypes[0][0]
|
||||
|
||||
possible = [mt for mt,w in mtypes]
|
||||
else:
|
||||
possible = None # Try all magic matches
|
||||
|
||||
try:
|
||||
t = magic.match(path, possible=possible)
|
||||
except IOError:
|
||||
t = None
|
||||
|
||||
if t:
|
||||
return t
|
||||
elif mtypes:
|
||||
return mtypes[0][0]
|
||||
elif stat.S_IMODE(st.st_mode) & 0o111:
|
||||
return app_exe
|
||||
else:
|
||||
return text if is_text_file(path) else octet_stream
|
||||
|
||||
def is_text_file(path):
|
||||
"""Guess whether a file contains text or binary data.
|
||||
|
||||
Heuristic: binary if the first 32 bytes include ASCII control characters.
|
||||
This rule may change in future versions.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
"""
|
||||
try:
|
||||
f = open(path, 'rb')
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
with f:
|
||||
return _is_text(f.read(32))
|
||||
|
||||
if PY3:
|
||||
def _is_text(data):
|
||||
return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data)
|
||||
else:
|
||||
def _is_text(data):
|
||||
return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \
|
||||
for b in data)
|
||||
|
||||
_mime2ext_cache = None
|
||||
_mime2ext_cache_uptodate = False
|
||||
|
||||
def get_extensions(mimetype):
|
||||
"""Retrieve the set of filename extensions matching a given MIMEtype.
|
||||
|
||||
Extensions are returned without a leading dot, e.g. 'py'. If no extensions
|
||||
are registered for the MIMEtype, returns an empty set.
|
||||
|
||||
The extensions are stored in a cache the first time this is called.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
"""
|
||||
global _mime2ext_cache, _mime2ext_cache_uptodate
|
||||
update_cache()
|
||||
if not _mime2ext_cache_uptodate:
|
||||
_mime2ext_cache = defaultdict(set)
|
||||
for ext, mtypes in globs.exts.items():
|
||||
for mtype, prio in mtypes:
|
||||
_mime2ext_cache[mtype].add(ext)
|
||||
_mime2ext_cache_uptodate = True
|
||||
|
||||
return _mime2ext_cache[mimetype]
|
||||
|
||||
|
||||
def install_mime_info(application, package_file):
|
||||
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.``
|
||||
If package_file is None, install ``<app_dir>/<application>.xml``.
|
||||
If already installed, does nothing. May overwrite an existing
|
||||
file with the same name (if the contents are different)"""
|
||||
application += '.xml'
|
||||
|
||||
new_data = open(package_file).read()
|
||||
|
||||
# See if the file is already installed
|
||||
package_dir = os.path.join('mime', 'packages')
|
||||
resource = os.path.join(package_dir, application)
|
||||
for x in BaseDirectory.load_data_paths(resource):
|
||||
try:
|
||||
old_data = open(x).read()
|
||||
except:
|
||||
continue
|
||||
if old_data == new_data:
|
||||
return # Already installed
|
||||
|
||||
global _cache_uptodate
|
||||
_cache_uptodate = False
|
||||
|
||||
# Not already installed; add a new copy
|
||||
# Create the directory structure...
|
||||
new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application)
|
||||
|
||||
# Write the file...
|
||||
open(new_file, 'w').write(new_data)
|
||||
|
||||
# Update the database...
|
||||
command = 'update-mime-database'
|
||||
if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')):
|
||||
os.unlink(new_file)
|
||||
raise Exception("The '%s' command returned an error code!\n" \
|
||||
"Make sure you have the freedesktop.org shared MIME package:\n" \
|
||||
"http://standards.freedesktop.org/shared-mime-info/" % command)
|
181
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/RecentFiles.py
Normal file
181
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/RecentFiles.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Implementation of the XDG Recent File Storage Specification
|
||||
http://standards.freedesktop.org/recent-file-spec
|
||||
"""
|
||||
|
||||
import xml.dom.minidom, xml.sax.saxutils
|
||||
import os, time, fcntl
|
||||
from .Exceptions import ParsingError
|
||||
|
||||
class RecentFiles:
|
||||
def __init__(self):
|
||||
self.RecentFiles = []
|
||||
self.filename = ""
|
||||
|
||||
def parse(self, filename=None):
|
||||
"""Parse a list of recently used files.
|
||||
|
||||
filename defaults to ``~/.recently-used``.
|
||||
"""
|
||||
if not filename:
|
||||
filename = os.path.join(os.getenv("HOME"), ".recently-used")
|
||||
|
||||
try:
|
||||
doc = xml.dom.minidom.parse(filename)
|
||||
except IOError:
|
||||
raise ParsingError('File not found', filename)
|
||||
except xml.parsers.expat.ExpatError:
|
||||
raise ParsingError('Not a valid .menu file', filename)
|
||||
|
||||
self.filename = filename
|
||||
|
||||
for child in doc.childNodes:
|
||||
if child.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||
if child.tagName == "RecentFiles":
|
||||
for recent in child.childNodes:
|
||||
if recent.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||
if recent.tagName == "RecentItem":
|
||||
self.__parseRecentItem(recent)
|
||||
|
||||
self.sort()
|
||||
|
||||
def __parseRecentItem(self, item):
|
||||
recent = RecentFile()
|
||||
self.RecentFiles.append(recent)
|
||||
|
||||
for attribute in item.childNodes:
|
||||
if attribute.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||
if attribute.tagName == "URI":
|
||||
recent.URI = attribute.childNodes[0].nodeValue
|
||||
elif attribute.tagName == "Mime-Type":
|
||||
recent.MimeType = attribute.childNodes[0].nodeValue
|
||||
elif attribute.tagName == "Timestamp":
|
||||
recent.Timestamp = int(attribute.childNodes[0].nodeValue)
|
||||
elif attribute.tagName == "Private":
|
||||
recent.Prviate = True
|
||||
elif attribute.tagName == "Groups":
|
||||
|
||||
for group in attribute.childNodes:
|
||||
if group.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||
if group.tagName == "Group":
|
||||
recent.Groups.append(group.childNodes[0].nodeValue)
|
||||
|
||||
def write(self, filename=None):
|
||||
"""Write the list of recently used files to disk.
|
||||
|
||||
If the instance is already associated with a file, filename can be
|
||||
omitted to save it there again.
|
||||
"""
|
||||
if not filename and not self.filename:
|
||||
raise ParsingError('File not found', filename)
|
||||
elif not filename:
|
||||
filename = self.filename
|
||||
|
||||
f = open(filename, "w")
|
||||
fcntl.lockf(f, fcntl.LOCK_EX)
|
||||
f.write('<?xml version="1.0"?>\n')
|
||||
f.write("<RecentFiles>\n")
|
||||
|
||||
for r in self.RecentFiles:
|
||||
f.write(" <RecentItem>\n")
|
||||
f.write(" <URI>%s</URI>\n" % xml.sax.saxutils.escape(r.URI))
|
||||
f.write(" <Mime-Type>%s</Mime-Type>\n" % r.MimeType)
|
||||
f.write(" <Timestamp>%s</Timestamp>\n" % r.Timestamp)
|
||||
if r.Private == True:
|
||||
f.write(" <Private/>\n")
|
||||
if len(r.Groups) > 0:
|
||||
f.write(" <Groups>\n")
|
||||
for group in r.Groups:
|
||||
f.write(" <Group>%s</Group>\n" % group)
|
||||
f.write(" </Groups>\n")
|
||||
f.write(" </RecentItem>\n")
|
||||
|
||||
f.write("</RecentFiles>\n")
|
||||
fcntl.lockf(f, fcntl.LOCK_UN)
|
||||
f.close()
|
||||
|
||||
def getFiles(self, mimetypes=None, groups=None, limit=0):
|
||||
"""Get a list of recently used files.
|
||||
|
||||
The parameters can be used to filter by mime types, by group, or to
|
||||
limit the number of items returned. By default, the entire list is
|
||||
returned, except for items marked private.
|
||||
"""
|
||||
tmp = []
|
||||
i = 0
|
||||
for item in self.RecentFiles:
|
||||
if groups:
|
||||
for group in groups:
|
||||
if group in item.Groups:
|
||||
tmp.append(item)
|
||||
i += 1
|
||||
elif mimetypes:
|
||||
for mimetype in mimetypes:
|
||||
if mimetype == item.MimeType:
|
||||
tmp.append(item)
|
||||
i += 1
|
||||
else:
|
||||
if item.Private == False:
|
||||
tmp.append(item)
|
||||
i += 1
|
||||
if limit != 0 and i == limit:
|
||||
break
|
||||
|
||||
return tmp
|
||||
|
||||
def addFile(self, item, mimetype, groups=None, private=False):
|
||||
"""Add a recently used file.
|
||||
|
||||
item should be the URI of the file, typically starting with ``file:///``.
|
||||
"""
|
||||
# check if entry already there
|
||||
if item in self.RecentFiles:
|
||||
index = self.RecentFiles.index(item)
|
||||
recent = self.RecentFiles[index]
|
||||
else:
|
||||
# delete if more then 500 files
|
||||
if len(self.RecentFiles) == 500:
|
||||
self.RecentFiles.pop()
|
||||
# add entry
|
||||
recent = RecentFile()
|
||||
self.RecentFiles.append(recent)
|
||||
|
||||
recent.URI = item
|
||||
recent.MimeType = mimetype
|
||||
recent.Timestamp = int(time.time())
|
||||
recent.Private = private
|
||||
if groups:
|
||||
recent.Groups = groups
|
||||
|
||||
self.sort()
|
||||
|
||||
def deleteFile(self, item):
|
||||
"""Remove a recently used file, by URI, from the list.
|
||||
"""
|
||||
if item in self.RecentFiles:
|
||||
self.RecentFiles.remove(item)
|
||||
|
||||
def sort(self):
|
||||
self.RecentFiles.sort()
|
||||
self.RecentFiles.reverse()
|
||||
|
||||
|
||||
class RecentFile:
|
||||
def __init__(self):
|
||||
self.URI = ""
|
||||
self.MimeType = ""
|
||||
self.Timestamp = ""
|
||||
self.Private = False
|
||||
self.Groups = []
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.Timestamp, other.Timestamp)
|
||||
|
||||
def __lt__ (self, other):
|
||||
return self.Timestamp < other.Timestamp
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.URI == str(other)
|
||||
|
||||
def __str__(self):
|
||||
return self.URI
|
@@ -0,0 +1,3 @@
|
||||
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
|
||||
|
||||
__version__ = "0.26"
|
75
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/util.py
Normal file
75
src/solarfm/shellfm/windows/tabs/icons/mixins/xdg/util.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] >= 3
|
||||
|
||||
if PY3:
|
||||
def u(s):
|
||||
return s
|
||||
else:
|
||||
# Unicode-like literals
|
||||
def u(s):
|
||||
return s.decode('utf-8')
|
||||
|
||||
try:
|
||||
# which() is available from Python 3.3
|
||||
from shutil import which
|
||||
except ImportError:
|
||||
import os
|
||||
# This is a copy of which() from Python 3.3
|
||||
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
"""Given a command, mode, and a PATH string, return the path which
|
||||
conforms to the given mode on the PATH, or None if there is no such
|
||||
file.
|
||||
|
||||
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
|
||||
of os.environ.get("PATH"), or can be overridden with a custom search
|
||||
path.
|
||||
|
||||
"""
|
||||
# Check that a given file can be accessed with the correct mode.
|
||||
# Additionally check that `file` is not a directory, as on Windows
|
||||
# directories pass the os.access check.
|
||||
def _access_check(fn, mode):
|
||||
return (os.path.exists(fn) and os.access(fn, mode)
|
||||
and not os.path.isdir(fn))
|
||||
|
||||
# If we're given a path with a directory part, look it up directly rather
|
||||
# than referring to PATH directories. This includes checking relative to the
|
||||
# current directory, e.g. ./script
|
||||
if os.path.dirname(cmd):
|
||||
if _access_check(cmd, mode):
|
||||
return cmd
|
||||
return None
|
||||
|
||||
path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep)
|
||||
|
||||
if sys.platform == "win32":
|
||||
# The current directory takes precedence on Windows.
|
||||
if not os.curdir in path:
|
||||
path.insert(0, os.curdir)
|
||||
|
||||
# PATHEXT is necessary to check on Windows.
|
||||
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
|
||||
# See if the given file matches any of the expected path extensions.
|
||||
# This will allow us to short circuit when given "python.exe".
|
||||
# If it does match, only test that one, otherwise we have to try
|
||||
# others.
|
||||
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
|
||||
files = [cmd]
|
||||
else:
|
||||
files = [cmd + ext for ext in pathext]
|
||||
else:
|
||||
# On other platforms you don't have things like PATHEXT to tell you
|
||||
# what file suffixes are executable, so just pass on cmd as-is.
|
||||
files = [cmd]
|
||||
|
||||
seen = set()
|
||||
for dir in path:
|
||||
normdir = os.path.normcase(dir)
|
||||
if not normdir in seen:
|
||||
seen.add(normdir)
|
||||
for thefile in files:
|
||||
name = os.path.join(dir, thefile)
|
||||
if _access_check(name, mode):
|
||||
return name
|
||||
return None
|
64
src/solarfm/shellfm/windows/tabs/path.py
Normal file
64
src/solarfm/shellfm/windows/tabs/path.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class Path:
|
||||
def get_home(self) -> str:
|
||||
return os.path.expanduser("~") + self.subpath
|
||||
|
||||
def get_path(self) -> str:
|
||||
return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}"
|
||||
|
||||
def get_path_list(self) -> list:
|
||||
return self.path
|
||||
|
||||
def push_to_path(self, dir: str):
|
||||
self.path.append(dir)
|
||||
self.load_directory()
|
||||
|
||||
def pop_from_path(self) -> None:
|
||||
try:
|
||||
self.path.pop()
|
||||
|
||||
if not self.go_past_home:
|
||||
if self.get_home() not in self.get_path():
|
||||
self.set_to_home()
|
||||
|
||||
self.load_directory()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def set_path(self, path: str) -> bool:
|
||||
if path == self.get_path():
|
||||
return False
|
||||
|
||||
if os.path.isdir(path):
|
||||
self.path = list( filter(None, path.replace("\\", "/").split('/')) )
|
||||
self.load_directory()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_path_with_sub_path(self, sub_path: str) -> bool:
|
||||
path = os.path.join(self.get_home(), sub_path)
|
||||
if path == self.get_path():
|
||||
return False
|
||||
|
||||
if os.path.isdir(path):
|
||||
self.path = list( filter(None, path.replace("\\", "/").split('/')) )
|
||||
self.load_directory()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_to_home(self) -> None:
|
||||
home = os.path.expanduser("~") + self.subpath
|
||||
path = list( filter(None, home.replace("\\", "/").split('/')) )
|
||||
self.path = path
|
||||
self.load_directory()
|
291
src/solarfm/shellfm/windows/tabs/tab.py
Normal file
291
src/solarfm/shellfm/windows/tabs/tab.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# Python imports
|
||||
import os
|
||||
import hashlib
|
||||
import re
|
||||
from os import listdir
|
||||
from os.path import isdir
|
||||
from os.path import isfile
|
||||
from os.path import join
|
||||
from random import randint
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .utils.settings import Settings
|
||||
from .utils.launcher import Launcher
|
||||
from .utils.filehandler import FileHandler
|
||||
from .icons.icon import Icon
|
||||
from .path import Path
|
||||
|
||||
|
||||
try:
|
||||
get_file_size("/")
|
||||
except Exception as e:
|
||||
import os
|
||||
|
||||
def sizeof_fmt_def(num, suffix="B"):
|
||||
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
|
||||
if abs(num) < 1024.0:
|
||||
return f"{num:3.1f} {unit}{suffix}"
|
||||
num /= 1024.0
|
||||
return f"{num:.1f} Yi{suffix}"
|
||||
|
||||
def _get_file_size(file):
|
||||
try:
|
||||
return "4K" if isdir(file) else sizeof_fmt_def(os.path.getsize(file))
|
||||
except Exception as e:
|
||||
return "0K"
|
||||
|
||||
get_file_size = _get_file_size
|
||||
|
||||
|
||||
|
||||
class Tab(Settings, FileHandler, Launcher, Icon, Path):
|
||||
def __init__(self):
|
||||
self.logger = None
|
||||
self._id_length: int = 10
|
||||
|
||||
self._id: str = ""
|
||||
self._wid: str = None
|
||||
self.error_message: str = None
|
||||
self._dir_watcher = None
|
||||
self._hide_hidden: bool = self.HIDE_HIDDEN_FILES
|
||||
self._files: list = []
|
||||
self._dirs: list = []
|
||||
self._vids: list = []
|
||||
self._images: list = []
|
||||
self._desktop: list = []
|
||||
self._ungrouped: list = []
|
||||
self._hidden: list = []
|
||||
|
||||
self._generate_id()
|
||||
self.set_to_home()
|
||||
|
||||
def load_directory(self) -> None:
|
||||
path = self.get_path()
|
||||
self._dirs = []
|
||||
self._vids = []
|
||||
self._images = []
|
||||
self._desktop = []
|
||||
self._ungrouped = []
|
||||
self._hidden = []
|
||||
self._files = []
|
||||
|
||||
if not isdir(path):
|
||||
self._set_error_message("Path can not be accessed.")
|
||||
self.set_to_home()
|
||||
return ""
|
||||
|
||||
for f in listdir(path):
|
||||
file = join(path, f)
|
||||
if self._hide_hidden:
|
||||
if f.startswith('.'):
|
||||
self._hidden.append(f)
|
||||
continue
|
||||
|
||||
if isfile(file):
|
||||
lowerName = file.lower()
|
||||
if lowerName.endswith(self.fvideos):
|
||||
self._vids.append(f)
|
||||
elif lowerName.endswith(self.fimages):
|
||||
self._images.append(f)
|
||||
elif lowerName.endswith((".desktop",)):
|
||||
self._desktop.append(f)
|
||||
else:
|
||||
self._ungrouped.append(f)
|
||||
else:
|
||||
self._dirs.append(f)
|
||||
|
||||
self._dirs.sort(key=self._natural_keys)
|
||||
self._vids.sort(key=self._natural_keys)
|
||||
self._images.sort(key=self._natural_keys)
|
||||
self._desktop.sort(key=self._natural_keys)
|
||||
self._ungrouped.sort(key=self._natural_keys)
|
||||
|
||||
self._files = self._dirs + self._vids + self._images + self._desktop + self._ungrouped
|
||||
|
||||
def is_folder_locked(self, hash):
|
||||
if self.lock_folder:
|
||||
path_parts = self.get_path().split('/')
|
||||
file = self.get_path_part_from_hash(hash)
|
||||
|
||||
# Insure chilren folders are locked too.
|
||||
lockedFolderInPath = False
|
||||
for folder in self.locked_folders:
|
||||
if folder in path_parts:
|
||||
lockedFolderInPath = True
|
||||
break
|
||||
|
||||
return (file in self.locked_folders or lockedFolderInPath)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_not_hidden_count(self) -> int:
|
||||
return len(self._files) + \
|
||||
len(self._dirs) + \
|
||||
len(self._vids) + \
|
||||
len(self._images) + \
|
||||
len(self._desktop) + \
|
||||
len(self._ungrouped)
|
||||
|
||||
def get_hidden_count(self) -> int:
|
||||
return len(self._hidden)
|
||||
|
||||
def get_files_count(self) -> int:
|
||||
return len(self._files)
|
||||
|
||||
def get_path_part_from_hash(self, hash: str) -> str:
|
||||
files = self.get_files()
|
||||
file = None
|
||||
|
||||
for f in files:
|
||||
if hash == f[1]:
|
||||
file = f[0]
|
||||
break
|
||||
|
||||
return file
|
||||
|
||||
def get_files_formatted(self) -> dict:
|
||||
files = self._hash_set(self._files),
|
||||
dirs = self._hash_set(self._dirs),
|
||||
videos = self.get_videos(),
|
||||
images = self._hash_set(self._images),
|
||||
desktops = self._hash_set(self._desktop),
|
||||
ungrouped = self._hash_set(self._ungrouped)
|
||||
hidden = self._hash_set(self._hidden)
|
||||
|
||||
return {
|
||||
'path_head': self.get_path(),
|
||||
'list': {
|
||||
'files': files,
|
||||
'dirs': dirs,
|
||||
'videos': videos,
|
||||
'images': images,
|
||||
'desktops': desktops,
|
||||
'ungrouped': ungrouped,
|
||||
'hidden': hidden
|
||||
}
|
||||
}
|
||||
|
||||
def get_video_icons(self) -> list:
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self._vids:
|
||||
img_hash, hash_img_path = self.create_video_thumbnail(full_path=f"{dir}/{file}", returnHashInstead=True)
|
||||
data.append([img_hash, hash_img_path])
|
||||
|
||||
return data
|
||||
|
||||
def get_pixbuf_icon_str_combo(self):
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self._files:
|
||||
icon = self.create_icon(dir, file).get_pixbuf()
|
||||
data.append([icon, file])
|
||||
|
||||
return data
|
||||
|
||||
def get_gtk_icon_str_combo(self) -> list:
|
||||
data = []
|
||||
dir = self.get_current_directory()
|
||||
for file in self._files:
|
||||
icon = self.create_icon(dir, file)
|
||||
data.append([icon, file[0]])
|
||||
|
||||
return data
|
||||
|
||||
def get_current_directory(self) -> str:
|
||||
return self.get_path()
|
||||
|
||||
def get_current_sub_path(self) -> str:
|
||||
path = self.get_path()
|
||||
home = f"{self.get_home()}/"
|
||||
return path.replace(home, "")
|
||||
|
||||
def get_end_of_path(self) -> str:
|
||||
parts = self.get_current_directory().split("/")
|
||||
size = len(parts)
|
||||
return parts[size - 1]
|
||||
|
||||
|
||||
def set_hiding_hidden(self, state: bool) -> None:
|
||||
self._hide_hidden = state
|
||||
|
||||
def is_hiding_hidden(self) -> bool:
|
||||
return self._hide_hidden
|
||||
|
||||
def get_dot_dots(self) -> list:
|
||||
return self._hash_set(['.', '..'])
|
||||
|
||||
def get_files(self) -> list:
|
||||
return self._hash_set(self._files)
|
||||
|
||||
def get_dirs(self) -> list:
|
||||
return self._hash_set(self._dirs)
|
||||
|
||||
def get_videos(self) -> list:
|
||||
return self._hash_set(self._vids)
|
||||
|
||||
def get_images(self) -> list:
|
||||
return self._hash_set(self._images)
|
||||
|
||||
def get_desktops(self) -> list:
|
||||
return self._hash_set(self._desktop)
|
||||
|
||||
def get_ungrouped(self) -> list:
|
||||
return self._hash_set(self._ungrouped)
|
||||
|
||||
def get_hidden(self) -> list:
|
||||
return self._hash_set(self._hidden)
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def set_wid(self, _wid: str) -> None:
|
||||
self._wid = _wid
|
||||
|
||||
def get_wid(self) -> str:
|
||||
return self._wid
|
||||
|
||||
def set_dir_watcher(self, watcher):
|
||||
self._dir_watcher = watcher
|
||||
|
||||
def get_dir_watcher(self):
|
||||
return self._dir_watcher
|
||||
|
||||
def get_error_message(self):
|
||||
return self.error_message
|
||||
|
||||
def unset_error_message(self):
|
||||
self.error_message = None
|
||||
|
||||
def _atoi(self, text):
|
||||
return int(text) if text.isdigit() else text
|
||||
|
||||
def _natural_keys(self, text):
|
||||
return [ self._atoi(c) for c in re.split('(\d+)',text) ]
|
||||
|
||||
def _hash_text(self, text) -> str:
|
||||
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
|
||||
|
||||
def _hash_set(self, arry: list) -> list:
|
||||
path = self.get_current_directory()
|
||||
data = []
|
||||
for arr in arry:
|
||||
file = f"{path}/{arr}"
|
||||
size = get_file_size(file)
|
||||
data.append([arr, self._hash_text(arr), size])
|
||||
return data
|
||||
|
||||
|
||||
def _random_with_N_digits(self, n: int) -> int:
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
def _generate_id(self) -> str:
|
||||
self._id = str(self._random_with_N_digits(self._id_length))
|
||||
|
||||
def _set_error_message(self, text: str):
|
||||
self.error_message = text
|
3
src/solarfm/shellfm/windows/tabs/utils/__init__.py
Normal file
3
src/solarfm/shellfm/windows/tabs/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Utils module
|
||||
"""
|
87
src/solarfm/shellfm/windows/tabs/utils/filehandler.py
Normal file
87
src/solarfm/shellfm/windows/tabs/utils/filehandler.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Python imports
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class FileHandler:
|
||||
def create_file(self, nFile, type):
|
||||
try:
|
||||
if type == "dir":
|
||||
os.mkdir(nFile)
|
||||
elif type == "file":
|
||||
open(nFile, 'a').close()
|
||||
except Exception as e:
|
||||
print("An error occured creating the file/dir:")
|
||||
print(repr(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_file(self, oFile, nFile):
|
||||
try:
|
||||
print(f"Renaming: {oFile} --> {nFile}")
|
||||
os.rename(oFile, nFile)
|
||||
except Exception as e:
|
||||
print("An error occured renaming the file:")
|
||||
print(repr(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def delete_file(self, to_delete_file):
|
||||
try:
|
||||
print(f"Deleting: {to_delete_file}")
|
||||
if os.path.exists(to_delete_file):
|
||||
if os.path.isfile(to_delete_file):
|
||||
os.remove(to_delete_file)
|
||||
elif os.path.isdir(to_delete_file):
|
||||
shutil.rmtree(to_delete_file)
|
||||
else:
|
||||
print("An error occured deleting the file:")
|
||||
return False
|
||||
else:
|
||||
print("The folder/file does not exist")
|
||||
return False
|
||||
except Exception as e:
|
||||
print("An error occured deleting the file:")
|
||||
print(repr(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def move_file(self, fFile, tFile):
|
||||
try:
|
||||
print(f"Moving: {fFile} --> {tFile}")
|
||||
if os.path.exists(fFile) and not os.path.exists(tFile):
|
||||
if not tFile.endswith("/"):
|
||||
tFile += "/"
|
||||
|
||||
shutil.move(fFile, tFile)
|
||||
else:
|
||||
print("The folder/file does not exist")
|
||||
return False
|
||||
except Exception as e:
|
||||
print("An error occured moving the file:")
|
||||
print(repr(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def copy_file(self, fFile, tFile, symlinks = False, ignore = None):
|
||||
try:
|
||||
if os.path.isdir(fFile):
|
||||
shutil.copytree(fFile, tFile, symlinks, ignore)
|
||||
else:
|
||||
shutil.copy2(fFile, tFile)
|
||||
except Exception as e:
|
||||
print("An error occured copying the file:")
|
||||
print(repr(e))
|
||||
return False
|
||||
|
||||
return True
|
116
src/solarfm/shellfm/windows/tabs/utils/launcher.py
Normal file
116
src/solarfm/shellfm/windows/tabs/utils/launcher.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# Python imports
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Apoplication imports
|
||||
|
||||
|
||||
class ShellFMLauncherException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class Launcher:
|
||||
def open_file_locally(self, file):
|
||||
lowerName = file.lower()
|
||||
command = []
|
||||
|
||||
if lowerName.endswith(self.fvideos):
|
||||
command = [self.media_app, file]
|
||||
elif lowerName.endswith(self.fimages):
|
||||
command = [self.image_app, file]
|
||||
elif lowerName.endswith(self.fmusic):
|
||||
command = [self.music_app, file]
|
||||
elif lowerName.endswith(self.foffice):
|
||||
command = [self.office_app, file]
|
||||
elif lowerName.endswith(self.fcode):
|
||||
command = [self.code_app, file]
|
||||
elif lowerName.endswith(self.ftext):
|
||||
command = [self.text_app, file]
|
||||
elif lowerName.endswith(self.fpdf):
|
||||
command = [self.pdf_app, file]
|
||||
elif lowerName.endswith("placeholder-until-i-can-get-a-use-pref-fm-flag"):
|
||||
command = [self.file_manager_app, file]
|
||||
else:
|
||||
command = ["xdg-open", file]
|
||||
|
||||
self.execute(command)
|
||||
|
||||
|
||||
def execute(self, command, start_dir=os.getenv("HOME"), use_shell=False):
|
||||
try:
|
||||
self.logger.debug(command)
|
||||
subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=True, stdout=None, stderr=None, close_fds=True)
|
||||
except ShellFMLauncherException as e:
|
||||
self.logger.error(f"Couldn't execute: {command}")
|
||||
self.logger.error(e)
|
||||
|
||||
# TODO: Return std(out/in/err) handlers along with subprocess instead of sinking to null
|
||||
def execute_and_return_thread_handler(self, command, start_dir=os.getenv("HOME"), use_shell=False):
|
||||
try:
|
||||
DEVNULL = open(os.devnull, 'w')
|
||||
return subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=False, stdout=DEVNULL, stderr=DEVNULL, close_fds=False)
|
||||
except ShellFMLauncherException as e:
|
||||
self.logger.error(f"Couldn't execute and return thread: {command}")
|
||||
self.logger.error(e)
|
||||
return None
|
||||
|
||||
@threaded
|
||||
def app_chooser_exec(self, app_info, uris):
|
||||
app_info.launch_uris_async(uris)
|
||||
|
||||
def remux_video(self, hash, file):
|
||||
remux_vid_pth = "{self.REMUX_FOLDER}/{hash}.mp4"
|
||||
self.logger.debug(remux_vid_pth)
|
||||
|
||||
if not os.path.isfile(remux_vid_pth):
|
||||
self.check_remux_space()
|
||||
|
||||
command = ["ffmpeg", "-i", file, "-hide_banner", "-movflags", "+faststart"]
|
||||
if file.endswith("mkv"):
|
||||
command += ["-codec", "copy", "-strict", "-2"]
|
||||
if file.endswith("avi"):
|
||||
command += ["-c:v", "libx264", "-crf", "21", "-c:a", "aac", "-b:a", "192k", "-ac", "2"]
|
||||
if file.endswith("wmv"):
|
||||
command += ["-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-strict", "-2", "-q:a", "100"]
|
||||
if file.endswith("f4v") or file.endswith("flv"):
|
||||
command += ["-vcodec", "copy"]
|
||||
|
||||
command += [remux_vid_pth]
|
||||
try:
|
||||
proc = subprocess.Popen(command)
|
||||
proc.wait()
|
||||
except ShellFMLauncherException as e:
|
||||
self.logger.error(message)
|
||||
self.logger.error(e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_remux_space(self):
|
||||
limit = self.remux_folder_max_disk_usage
|
||||
try:
|
||||
limit = int(limit)
|
||||
except ShellFMLauncherException as e:
|
||||
self.logger.debug(e)
|
||||
return
|
||||
|
||||
usage = self.get_remux_folder_usage(self.REMUX_FOLDER)
|
||||
if usage > limit:
|
||||
files = os.listdir(self.REMUX_FOLDER)
|
||||
for file in files:
|
||||
fp = os.path.join(self.REMUX_FOLDER, file)
|
||||
os.unlink(fp)
|
||||
|
||||
|
||||
def get_remux_folder_usage(self, start_path = "."):
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(start_path):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
if not os.path.islink(fp): # Skip if it is symbolic link
|
||||
total_size += os.path.getsize(fp)
|
||||
|
||||
return total_size
|
101
src/solarfm/shellfm/windows/tabs/utils/settings.py
Normal file
101
src/solarfm/shellfm/windows/tabs/utils/settings.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Python imports
|
||||
import json
|
||||
import os
|
||||
from os import path
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Apoplication imports
|
||||
|
||||
|
||||
class ShellFMSettingsException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class Settings:
|
||||
logger = None
|
||||
|
||||
# NOTE: app_name should be defined using python 'builtins'
|
||||
app_name_exists = False
|
||||
try:
|
||||
app_name
|
||||
app_name_exists = True
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
APP_CONTEXT = f"{app_name.lower()}" if app_name_exists else "shellfm"
|
||||
USR_APP_CONTEXT = f"/usr/share/{APP_CONTEXT}"
|
||||
USER_HOME = path.expanduser('~')
|
||||
CONFIG_PATH = f"{USER_HOME}/.config/{APP_CONTEXT}"
|
||||
CONFIG_FILE = f"{CONFIG_PATH}/settings.json"
|
||||
HIDE_HIDDEN_FILES = True
|
||||
|
||||
DEFAULT_ICONS = f"{CONFIG_PATH}/icons"
|
||||
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
|
||||
FFMPG_THUMBNLR = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary
|
||||
BLENDER_THUMBNLR = f"{CONFIG_PATH}/blender-thumbnailer" # Blender thumbnail generator binary
|
||||
REMUX_FOLDER = f"{USER_HOME}/.remuxs" # Remuxed files folder
|
||||
|
||||
ICON_DIRS = ["/usr/share/icons", f"{USER_HOME}/.icons" "/usr/share/pixmaps"]
|
||||
BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails"
|
||||
ABS_THUMBS_PTH = f"{BASE_THUMBS_PTH}/normal"
|
||||
STEAM_ICONS_PTH = f"{BASE_THUMBS_PTH}/steam_icons"
|
||||
|
||||
if not os.path.exists(CONFIG_PATH) or not os.path.exists(CONFIG_FILE):
|
||||
msg = f"No config file located! Aborting loading ShellFM library...\nExpected: {CONFIG_FILE}"
|
||||
raise ShellFMSettingsException(msg)
|
||||
|
||||
if not path.isdir(REMUX_FOLDER):
|
||||
os.mkdir(REMUX_FOLDER)
|
||||
|
||||
if not path.isdir(BASE_THUMBS_PTH):
|
||||
os.mkdir(BASE_THUMBS_PTH)
|
||||
|
||||
if not path.isdir(ABS_THUMBS_PTH):
|
||||
os.mkdir(ABS_THUMBS_PTH)
|
||||
|
||||
if not path.isdir(STEAM_ICONS_PTH):
|
||||
os.mkdir(STEAM_ICONS_PTH)
|
||||
|
||||
if not os.path.exists(DEFAULT_ICONS):
|
||||
DEFAULT_ICONS = f"{USR_APP_CONTEXT}/icons"
|
||||
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
|
||||
|
||||
with open(CONFIG_FILE) as f:
|
||||
settings = json.load(f)
|
||||
config = settings["config"]
|
||||
|
||||
subpath = config["base_of_home"]
|
||||
STEAM_CDN_URL = config["steam_cdn_url"]
|
||||
FFMPG_THUMBNLR = FFMPG_THUMBNLR if config["thumbnailer_path"] == "" else config["thumbnailer_path"]
|
||||
BLENDER_THUMBNLR = BLENDER_THUMBNLR if config["blender_thumbnailer_path"] == "" else config["blender_thumbnailer_path"]
|
||||
HIDE_HIDDEN_FILES = True if config["hide_hidden_files"] in ["true", ""] else False
|
||||
go_past_home = True if config["go_past_home"] in ["true", ""] else False
|
||||
lock_folder = False if config["lock_folder"] in ["false", ""] else True
|
||||
locked_folders = config["locked_folders"].split("::::")
|
||||
mplayer_options = config["mplayer_options"].split()
|
||||
music_app = config["music_app"]
|
||||
media_app = config["media_app"]
|
||||
image_app = config["image_app"]
|
||||
office_app = config["office_app"]
|
||||
pdf_app = config["pdf_app"]
|
||||
code_app = config["code_app"]
|
||||
text_app = config["text_app"]
|
||||
terminal_app = config["terminal_app"]
|
||||
container_icon_wh = config["container_icon_wh"]
|
||||
video_icon_wh = config["video_icon_wh"]
|
||||
sys_icon_wh = config["sys_icon_wh"]
|
||||
file_manager_app = config["file_manager_app"]
|
||||
remux_folder_max_disk_usage = config["remux_folder_max_disk_usage"]
|
||||
|
||||
# Filters
|
||||
filters = settings["filters"]
|
||||
fmeshs = tuple(filters["meshs"])
|
||||
fcode = tuple(filters["code"])
|
||||
fvideos = tuple(filters["videos"])
|
||||
foffice = tuple(filters["office"])
|
||||
fimages = tuple(filters["images"])
|
||||
ftext = tuple(filters["text"])
|
||||
fmusic = tuple(filters["music"])
|
||||
fpdf = tuple(filters["pdf"])
|
93
src/solarfm/shellfm/windows/window.py
Normal file
93
src/solarfm/shellfm/windows/window.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# Python imports
|
||||
from random import randint
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .tabs.tab import Tab
|
||||
|
||||
|
||||
|
||||
|
||||
class Window:
|
||||
def __init__(self):
|
||||
self._id_length: int = 10
|
||||
self._id: str = ""
|
||||
self._name: str = ""
|
||||
self._nickname:str = ""
|
||||
self._isHidden: bool = False
|
||||
self._active_tab: int = 0
|
||||
self._tabs: list = []
|
||||
|
||||
self._generate_id()
|
||||
self._set_name()
|
||||
|
||||
|
||||
def create_tab(self) -> Tab:
|
||||
tab = Tab()
|
||||
self._tabs.append(tab)
|
||||
return tab
|
||||
|
||||
def pop_tab(self) -> None:
|
||||
self._tabs.pop()
|
||||
|
||||
def delete_tab_by_id(self, tid: str):
|
||||
for tab in self._tabs:
|
||||
if tab.get_id() == tid:
|
||||
self._tabs.remove(tab)
|
||||
break
|
||||
|
||||
|
||||
def get_tab_by_id(self, tid: str) -> Tab:
|
||||
for tab in self._tabs:
|
||||
if tab.get_id() == tid:
|
||||
return tab
|
||||
|
||||
def get_tab_by_index(self, index) -> Tab:
|
||||
return self._tabs[index]
|
||||
|
||||
def get_tabs_count(self) -> int:
|
||||
return len(self._tabs)
|
||||
|
||||
def get_all_tabs(self) -> list:
|
||||
return self._tabs
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def get_nickname(self) -> str:
|
||||
return self._nickname
|
||||
|
||||
def is_hidden(self) -> bool:
|
||||
return self._isHidden
|
||||
|
||||
def list_files_from_tabs(self) -> None:
|
||||
for tab in self._tabs:
|
||||
print(tab.get_files())
|
||||
|
||||
def set_active_tab(self, index: int):
|
||||
self._active_tab = index
|
||||
|
||||
def get_active_tab(self) -> Tab:
|
||||
return self._tabs[self._active_tab]
|
||||
|
||||
def set_nickname(self, nickname):
|
||||
self._nickname = f"{nickname}"
|
||||
|
||||
def set_is_hidden(self, state):
|
||||
self._isHidden = f"{state}"
|
||||
|
||||
def _set_name(self):
|
||||
self._name = "window_" + self.get_id()
|
||||
|
||||
|
||||
def _random_with_N_digits(self, n):
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
def _generate_id(self):
|
||||
self._id = str(self._random_with_N_digits(self._id_length))
|
3
src/solarfm/utils/__init__.py
Normal file
3
src/solarfm/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Utils module
|
||||
"""
|
52
src/solarfm/utils/debugging.py
Normal file
52
src/solarfm/utils/debugging.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
# Break into a Python console upon SIGUSR1 (Linux) or SIGBREAK (Windows:
|
||||
# CTRL+Pause/Break). To be included in all production code, just in case.
|
||||
def debug_signal_handler(signal, frame):
|
||||
del signal
|
||||
del frame
|
||||
|
||||
try:
|
||||
import rpdb2
|
||||
logger.debug("\n\nStarting embedded RPDB2 debugger. Password is 'foobar'\n\n")
|
||||
rpdb2.start_embedded_debugger("foobar", True, True)
|
||||
rpdb2.setbreak(depth=1)
|
||||
return
|
||||
except StandardError:
|
||||
...
|
||||
|
||||
try:
|
||||
from rfoo.utils import rconsole
|
||||
logger.debug("\n\nStarting embedded rconsole debugger...\n\n")
|
||||
rconsole.spawn_server()
|
||||
return
|
||||
except StandardError as ex:
|
||||
...
|
||||
|
||||
try:
|
||||
from pudb import set_trace
|
||||
logger.debug("\n\nStarting PuDB debugger...\n\n")
|
||||
set_trace(paused = True)
|
||||
return
|
||||
except StandardError as ex:
|
||||
...
|
||||
|
||||
try:
|
||||
import pdb
|
||||
logger.debug("\n\nStarting embedded PDB debugger...\n\n")
|
||||
pdb.Pdb(skip=['gi.*']).set_trace()
|
||||
return
|
||||
except StandardError as ex:
|
||||
...
|
||||
|
||||
try:
|
||||
import code
|
||||
code.interact()
|
||||
except StandardError as ex:
|
||||
logger.debug(f"{ex}, returning to normal program flow...")
|
73
src/solarfm/utils/event_system.py
Normal file
73
src/solarfm/utils/event_system.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Python imports
|
||||
from collections import defaultdict
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .singleton import Singleton
|
||||
|
||||
|
||||
|
||||
class EventSystem(Singleton):
|
||||
""" Create event system. """
|
||||
|
||||
def __init__(self):
|
||||
self.subscribers = defaultdict(list)
|
||||
self._is_paused = False
|
||||
|
||||
self._subscribe_to_events()
|
||||
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
self.subscribe("pause_event_processing", self._pause_processing_events)
|
||||
self.subscribe("resume_event_processing", self._resume_processing_events)
|
||||
|
||||
def _pause_processing_events(self):
|
||||
self._is_paused = True
|
||||
|
||||
def _resume_processing_events(self):
|
||||
self._is_paused = False
|
||||
|
||||
def subscribe(self, event_type, fn):
|
||||
self.subscribers[event_type].append(fn)
|
||||
|
||||
def unsubscribe(self, event_type, fn):
|
||||
self.subscribers[event_type].remove(fn)
|
||||
|
||||
def unsubscribe_all(self, event_type):
|
||||
self.subscribers.pop(event_type, None)
|
||||
|
||||
def emit(self, event_type, data = None):
|
||||
if self._is_paused and event_type != "resume_event_processing":
|
||||
return
|
||||
|
||||
if event_type in self.subscribers:
|
||||
for fn in self.subscribers[event_type]:
|
||||
if data:
|
||||
if hasattr(data, '__iter__') and not type(data) is str:
|
||||
fn(*data)
|
||||
else:
|
||||
fn(data)
|
||||
else:
|
||||
fn()
|
||||
|
||||
def emit_and_await(self, event_type, data = None):
|
||||
if self._is_paused and event_type != "resume_event_processing":
|
||||
return
|
||||
|
||||
""" NOTE: Should be used when signal has only one listener and vis-a-vis """
|
||||
if event_type in self.subscribers:
|
||||
response = None
|
||||
for fn in self.subscribers[event_type]:
|
||||
if data:
|
||||
if hasattr(data, '__iter__') and not type(data) is str:
|
||||
response = fn(*data)
|
||||
else:
|
||||
response = fn(data)
|
||||
else:
|
||||
response = fn()
|
||||
|
||||
if not response in (None, ''):
|
||||
break
|
||||
|
||||
return response
|
126
src/solarfm/utils/ipc_server.py
Normal file
126
src/solarfm/utils/ipc_server.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Python imports
|
||||
import os
|
||||
import time
|
||||
from multiprocessing.connection import Client
|
||||
from multiprocessing.connection import Listener
|
||||
|
||||
# Lib imports
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from .singleton import Singleton
|
||||
|
||||
|
||||
|
||||
class IPCServer(Singleton):
|
||||
""" Create a listener so that other SolarFM instances send requests back to existing instance. """
|
||||
def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"):
|
||||
self.is_ipc_alive = False
|
||||
self._ipc_port = 4848
|
||||
self._ipc_address = ipc_address
|
||||
self._conn_type = conn_type
|
||||
self._ipc_authkey = b'' + bytes(f'{app_name}-ipc', 'utf-8')
|
||||
self._ipc_timeout = 15.0
|
||||
|
||||
if conn_type == "socket":
|
||||
self._ipc_address = f'/tmp/{app_name}-ipc.sock'
|
||||
elif conn_type == "full_network":
|
||||
self._ipc_address = '0.0.0.0'
|
||||
elif conn_type == "full_network_unsecured":
|
||||
self._ipc_authkey = None
|
||||
self._ipc_address = '0.0.0.0'
|
||||
elif conn_type == "local_network_unsecured":
|
||||
self._ipc_authkey = None
|
||||
|
||||
self._subscribe_to_events()
|
||||
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("post_file_to_ipc", self.send_ipc_message)
|
||||
|
||||
def create_ipc_listener(self) -> None:
|
||||
if self._conn_type == "socket":
|
||||
if os.path.exists(self._ipc_address) and settings_manager.is_dirty_start():
|
||||
os.unlink(self._ipc_address)
|
||||
|
||||
listener = Listener(address = self._ipc_address, family = "AF_UNIX", authkey = self._ipc_authkey)
|
||||
|
||||
elif "unsecured" not in self._conn_type:
|
||||
listener = Listener((self._ipc_address, self._ipc_port), authkey = self._ipc_authkey)
|
||||
else:
|
||||
listener = Listener((self._ipc_address, self._ipc_port))
|
||||
|
||||
self.is_ipc_alive = True
|
||||
self._run_ipc_loop(listener)
|
||||
|
||||
@daemon_threaded
|
||||
def _run_ipc_loop(self, listener) -> None:
|
||||
while True:
|
||||
try:
|
||||
conn = listener.accept()
|
||||
start_time = time.perf_counter()
|
||||
GLib.idle_add(self._handle_ipc_message, *(conn, start_time,))
|
||||
except Exception as e:
|
||||
logger.debug( repr(e) )
|
||||
|
||||
listener.close()
|
||||
|
||||
def _handle_ipc_message(self, conn, start_time) -> None:
|
||||
while True:
|
||||
msg = conn.recv()
|
||||
logger.debug(msg)
|
||||
|
||||
if "FILE|" in msg:
|
||||
file = msg.split("FILE|")[1].strip()
|
||||
if file:
|
||||
event_system.emit("handle_file_from_ipc", file)
|
||||
|
||||
conn.close()
|
||||
break
|
||||
|
||||
|
||||
if msg in ['close connection', 'close server']:
|
||||
conn.close()
|
||||
break
|
||||
|
||||
# NOTE: Not perfect but insures we don't lock up the connection for too long.
|
||||
end_time = time.perf_counter()
|
||||
if (end_time - start_time) > self._ipc_timeout:
|
||||
conn.close()
|
||||
break
|
||||
|
||||
|
||||
def send_ipc_message(self, message: str = "Empty Data...") -> None:
|
||||
try:
|
||||
if self._conn_type == "socket":
|
||||
conn = Client(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey)
|
||||
elif "unsecured" not in self._conn_type:
|
||||
conn = Client((self._ipc_address, self._ipc_port), authkey=self._ipc_authkey)
|
||||
else:
|
||||
conn = Client((self._ipc_address, self._ipc_port))
|
||||
|
||||
conn.send(message)
|
||||
conn.close()
|
||||
except ConnectionRefusedError as e:
|
||||
logger.error("Connection refused...")
|
||||
except Exception as e:
|
||||
logger.error( repr(e) )
|
||||
|
||||
|
||||
def send_test_ipc_message(self, message: str = "Empty Data...") -> None:
|
||||
try:
|
||||
if self._conn_type == "socket":
|
||||
conn = Client(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey)
|
||||
elif "unsecured" not in self._conn_type:
|
||||
conn = Client((self._ipc_address, self._ipc_port), authkey=self._ipc_authkey)
|
||||
else:
|
||||
conn = Client((self._ipc_address, self._ipc_port))
|
||||
|
||||
conn.send(message)
|
||||
conn.close()
|
||||
except ConnectionRefusedError as e:
|
||||
if self._conn_type == "socket":
|
||||
logger.error("IPC Socket no longer valid.... Removing.")
|
||||
os.unlink(self._ipc_address)
|
||||
except Exception as e:
|
||||
logger.error( repr(e) )
|
138
src/solarfm/utils/keybindings.py
Normal file
138
src/solarfm/utils/keybindings.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# Python imports
|
||||
import re
|
||||
|
||||
# Gtk imports
|
||||
import gi
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
from .singleton import Singleton
|
||||
|
||||
|
||||
|
||||
def logger(log = ""):
|
||||
print(log)
|
||||
|
||||
|
||||
class KeymapError(Exception):
|
||||
""" Custom exception for errors in keybinding configurations """
|
||||
|
||||
MODIFIER = re.compile('<([^<]+)>')
|
||||
class Keybindings(Singleton):
|
||||
""" Class to handle loading and lookup of Terminator keybindings """
|
||||
|
||||
modifiers = {
|
||||
'ctrl': Gdk.ModifierType.CONTROL_MASK,
|
||||
'control': Gdk.ModifierType.CONTROL_MASK,
|
||||
'primary': Gdk.ModifierType.CONTROL_MASK,
|
||||
'shift': Gdk.ModifierType.SHIFT_MASK,
|
||||
'alt': Gdk.ModifierType.MOD1_MASK,
|
||||
'super': Gdk.ModifierType.SUPER_MASK,
|
||||
'hyper': Gdk.ModifierType.HYPER_MASK,
|
||||
'mod2': Gdk.ModifierType.MOD2_MASK
|
||||
}
|
||||
|
||||
empty = {}
|
||||
keys = None
|
||||
_masks = None
|
||||
_lookup = None
|
||||
|
||||
def __init__(self):
|
||||
self.keymap = Gdk.Keymap.get_default()
|
||||
self.configure({})
|
||||
|
||||
def print_keys(self):
|
||||
print(self.keys)
|
||||
|
||||
def append_bindings(self, combos):
|
||||
""" Accept new binding(s) and reload """
|
||||
for item in combos:
|
||||
method, keys = item.split(":")
|
||||
self.keys[method] = keys
|
||||
|
||||
self.reload()
|
||||
|
||||
def configure(self, bindings):
|
||||
""" Accept new bindings and reconfigure with them """
|
||||
self.keys = bindings
|
||||
self.reload()
|
||||
|
||||
def reload(self):
|
||||
""" Parse bindings and mangle into an appropriate form """
|
||||
self._lookup = {}
|
||||
self._masks = 0
|
||||
|
||||
for action, bindings in list(self.keys.items()):
|
||||
if isinstance(bindings, list):
|
||||
bindings = (*bindings,)
|
||||
elif not isinstance(bindings, tuple):
|
||||
bindings = (bindings,)
|
||||
|
||||
|
||||
for binding in bindings:
|
||||
if not binding or binding == "None":
|
||||
continue
|
||||
|
||||
try:
|
||||
keyval, mask = self._parsebinding(binding)
|
||||
# Does much the same, but with poorer error handling.
|
||||
# keyval, mask = Gtk.accelerator_parse(binding)
|
||||
except KeymapError as e:
|
||||
logger(f"Keybinding reload failed to parse binding '{binding}': {e}")
|
||||
else:
|
||||
if mask & Gdk.ModifierType.SHIFT_MASK:
|
||||
if keyval == Gdk.KEY_Tab:
|
||||
keyval = Gdk.KEY_ISO_Left_Tab
|
||||
mask &= ~Gdk.ModifierType.SHIFT_MASK
|
||||
else:
|
||||
keyvals = Gdk.keyval_convert_case(keyval)
|
||||
if keyvals[0] != keyvals[1]:
|
||||
keyval = keyvals[1]
|
||||
mask &= ~Gdk.ModifierType.SHIFT_MASK
|
||||
else:
|
||||
keyval = Gdk.keyval_to_lower(keyval)
|
||||
|
||||
self._lookup.setdefault(mask, {})
|
||||
self._lookup[mask][keyval] = action
|
||||
self._masks |= mask
|
||||
|
||||
def _parsebinding(self, binding):
|
||||
""" Parse an individual binding using Gtk's binding function """
|
||||
mask = 0
|
||||
modifiers = re.findall(MODIFIER, binding)
|
||||
|
||||
if modifiers:
|
||||
for modifier in modifiers:
|
||||
mask |= self._lookup_modifier(modifier)
|
||||
|
||||
key = re.sub(MODIFIER, '', binding)
|
||||
if key == '':
|
||||
raise KeymapError('No key found!')
|
||||
|
||||
keyval = Gdk.keyval_from_name(key)
|
||||
|
||||
if keyval == 0:
|
||||
raise KeymapError(f"Key '{key}' is unrecognised...")
|
||||
return (keyval, mask)
|
||||
|
||||
def _lookup_modifier(self, modifier):
|
||||
""" Map modifier names to gtk values """
|
||||
try:
|
||||
return self.modifiers[modifier.lower()]
|
||||
except KeyError:
|
||||
raise KeymapError(f"Unhandled modifier '<{modifier}>'")
|
||||
|
||||
def lookup(self, event):
|
||||
""" Translate a keyboard event into a mapped key """
|
||||
try:
|
||||
_found, keyval, _egp, _lvl, consumed = self.keymap.translate_keyboard_state(
|
||||
event.hardware_keycode,
|
||||
Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK),
|
||||
event.group)
|
||||
except TypeError:
|
||||
logger("Keybinding lookup failed to translate keyboard event: {dir(event)}")
|
||||
return None
|
||||
|
||||
mask = (event.get_state() & ~consumed) & self._masks
|
||||
return self._lookup.get(mask, self.empty).get(keyval, None)
|
59
src/solarfm/utils/logger.py
Normal file
59
src/solarfm/utils/logger.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Python imports
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Application imports
|
||||
from .singleton import Singleton
|
||||
|
||||
|
||||
|
||||
class Logger(Singleton):
|
||||
"""
|
||||
Create a new logging object and return it.
|
||||
:note:
|
||||
NOSET # Don't know the actual log level of this... (defaulting or literally none?)
|
||||
Log Levels (From least to most)
|
||||
Type Value
|
||||
CRITICAL 50
|
||||
ERROR 40
|
||||
WARNING 30
|
||||
INFO 20
|
||||
DEBUG 10
|
||||
:param loggerName: Sets the name of the logger object. (Used in log lines)
|
||||
:param createFile: Whether we create a log file or just pump to terminal
|
||||
|
||||
:return: the logging object we created
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: str, _ch_log_lvl = logging.CRITICAL, _fh_log_lvl = logging.INFO):
|
||||
self._CONFIG_PATH = config_path
|
||||
self.global_lvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels
|
||||
self.ch_log_lvl = _ch_log_lvl # Prety much the only one we ever change
|
||||
self.fh_log_lvl = _fh_log_lvl
|
||||
|
||||
def get_logger(self, loggerName: str = "NO_LOGGER_NAME_PASSED", createFile: bool = True) -> logging.Logger:
|
||||
log = logging.getLogger(loggerName)
|
||||
log.setLevel(self.global_lvl)
|
||||
|
||||
# Set our log output styles
|
||||
fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S')
|
||||
cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s')
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(level=self.ch_log_lvl)
|
||||
ch.setFormatter(cFormatter)
|
||||
log.addHandler(ch)
|
||||
|
||||
if createFile:
|
||||
folder = self._CONFIG_PATH
|
||||
file = f"{folder}/application.log"
|
||||
|
||||
if not os.path.exists(folder):
|
||||
os.mkdir(folder)
|
||||
|
||||
fh = logging.FileHandler(file)
|
||||
fh.setLevel(level=self.fh_log_lvl)
|
||||
fh.setFormatter(fFormatter)
|
||||
log.addHandler(fh)
|
||||
|
||||
return log
|
4
src/solarfm/utils/settings_manager/__init__.py
Normal file
4
src/solarfm/utils/settings_manager/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Settings module
|
||||
"""
|
||||
from .manager import SettingsManager
|
167
src/solarfm/utils/settings_manager/manager.py
Normal file
167
src/solarfm/utils/settings_manager/manager.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Python imports
|
||||
import inspect
|
||||
import json
|
||||
from os import path
|
||||
from os import mkdir
|
||||
|
||||
# Gtk imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from ..singleton import Singleton
|
||||
from .start_check_mixin import StartCheckMixin
|
||||
from .options.settings import Settings
|
||||
|
||||
|
||||
class MissingConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SettingsManager(StartCheckMixin, Singleton):
|
||||
def __init__(self):
|
||||
self._SCRIPT_PTH = path.dirname(path.realpath(__file__))
|
||||
self._USER_HOME = path.expanduser('~')
|
||||
self._USR_PATH = f"/usr/share/{app_name.lower()}"
|
||||
|
||||
self._USR_CONFIG_FILE = f"{self._USR_PATH}/settings.json"
|
||||
self._HOME_CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}"
|
||||
self._PLUGINS_PATH = f"{self._HOME_CONFIG_PATH}/plugins"
|
||||
self._DEFAULT_ICONS = f"{self._HOME_CONFIG_PATH}/icons"
|
||||
self._CONFIG_FILE = f"{self._HOME_CONFIG_PATH}/settings.json"
|
||||
self._GLADE_FILE = f"{self._HOME_CONFIG_PATH}/Main_Window.glade"
|
||||
self._CSS_FILE = f"{self._HOME_CONFIG_PATH}/stylesheet.css"
|
||||
self._KEY_BINDINGS_FILE = f"{self._HOME_CONFIG_PATH}/key-bindings.json"
|
||||
self._PID_FILE = f"{self._HOME_CONFIG_PATH}/{app_name.lower()}.pid"
|
||||
self._WINDOW_ICON = f"{self._DEFAULT_ICONS}/{app_name.lower()}.png"
|
||||
self._UI_WIDEGTS_PATH = f"{self._HOME_CONFIG_PATH}/ui_widgets"
|
||||
self._CONTEXT_MENU = f"{self._HOME_CONFIG_PATH}/contexct_menu.json"
|
||||
self._TRASH_FILES_PATH = f"{GLib.get_user_data_dir()}/Trash/files"
|
||||
self._TRASH_INFO_PATH = f"{GLib.get_user_data_dir()}/Trash/info"
|
||||
self._ICON_THEME = Gtk.IconTheme.get_default()
|
||||
|
||||
if not path.exists(self._HOME_CONFIG_PATH):
|
||||
mkdir(self._HOME_CONFIG_PATH)
|
||||
if not path.exists(self._PLUGINS_PATH):
|
||||
mkdir(self._PLUGINS_PATH)
|
||||
|
||||
if not path.exists(self._DEFAULT_ICONS):
|
||||
self._DEFAULT_ICONS = f"{self._USR_PATH}/icons"
|
||||
if not path.exists(self._DEFAULT_ICONS):
|
||||
raise MissingConfigError("Unable to find the application icons directory.")
|
||||
if not path.exists(self._GLADE_FILE):
|
||||
self._GLADE_FILE = f"{self._USR_PATH}/Main_Window.glade"
|
||||
if not path.exists(self._GLADE_FILE):
|
||||
raise MissingConfigError("Unable to find the application Glade file.")
|
||||
if not path.exists(self._KEY_BINDINGS_FILE):
|
||||
self._KEY_BINDINGS_FILE = f"{self._USR_PATH}/key-bindings.json"
|
||||
if not path.exists(self._KEY_BINDINGS_FILE):
|
||||
raise MissingConfigError("Unable to find the application Keybindings file.")
|
||||
if not path.exists(self._CSS_FILE):
|
||||
self._CSS_FILE = f"{self._USR_PATH}/stylesheet.css"
|
||||
if not path.exists(self._CSS_FILE):
|
||||
raise MissingConfigError("Unable to find the application Stylesheet file.")
|
||||
if not path.exists(self._WINDOW_ICON):
|
||||
self._WINDOW_ICON = f"{self._USR_PATH}/icons/{app_name.lower()}.png"
|
||||
if not path.exists(self._WINDOW_ICON):
|
||||
raise MissingConfigError("Unable to find the application icon.")
|
||||
if not path.exists(self._UI_WIDEGTS_PATH):
|
||||
self._UI_WIDEGTS_PATH = f"{self._USR_PATH}/ui_widgets"
|
||||
if not path.exists(self._CONTEXT_MENU):
|
||||
self._CONTEXT_MENU = f"{self._USR_PATH}/contexct_menu.json"
|
||||
|
||||
|
||||
try:
|
||||
with open(self._KEY_BINDINGS_FILE) as file:
|
||||
bindings = json.load(file)["keybindings"]
|
||||
keybindings.configure(bindings)
|
||||
except Exception as e:
|
||||
print( f"Settings: {self._KEY_BINDINGS_FILE}\n\t\t{repr(e)}" )
|
||||
|
||||
try:
|
||||
with open(self._CONTEXT_MENU) as file:
|
||||
self._context_menu_data = json.load(file)
|
||||
except Exception as e:
|
||||
print( f"Settings: {self._CONTEXT_MENU}\n\t\t{repr(e)}" )
|
||||
|
||||
|
||||
self.settings: Settings = None
|
||||
self._main_window = None
|
||||
self._main_window_w = 1670
|
||||
self._main_window_h = 830
|
||||
self._builder = None
|
||||
self.PAINT_BG_COLOR = (0, 0, 0, 0.0)
|
||||
|
||||
self._trace_debug = False
|
||||
self._debug = False
|
||||
self._dirty_start = False
|
||||
|
||||
|
||||
def register_signals_to_builder(self, classes=None, builder=None):
|
||||
handlers = {}
|
||||
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
def get_monitor_data(self) -> list:
|
||||
screen = self._main_window.get_screen()
|
||||
monitors = []
|
||||
for m in range(screen.get_n_monitors()):
|
||||
monitors.append(screen.get_monitor_geometry(m))
|
||||
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
|
||||
|
||||
return monitors
|
||||
|
||||
def set_builder(self, builder) -> any: self._builder = builder
|
||||
def set_main_window(self, window): self._main_window = window
|
||||
|
||||
def get_main_window(self) -> Gtk.ApplicationWindow: return self._main_window
|
||||
def get_main_window_width(self) -> Gtk.ApplicationWindow: return self._main_window_w
|
||||
def get_main_window_height(self) -> Gtk.ApplicationWindow: return self._main_window_h
|
||||
def get_builder(self) -> Gtk.Builder: return self._builder
|
||||
def get_paint_bg_color(self) -> list: return self.PAINT_BG_COLOR
|
||||
|
||||
def get_glade_file(self) -> str: return self._GLADE_FILE
|
||||
def get_icon_theme(self) -> str: return self._ICON_THEME
|
||||
def get_css_file(self) -> str: return self._CSS_FILE
|
||||
def get_home_config_path(self) -> str: return self._HOME_CONFIG_PATH
|
||||
def get_window_icon(self) -> str: return self._WINDOW_ICON
|
||||
|
||||
def get_context_menu_data(self) -> str: return self._context_menu_data
|
||||
def get_home_path(self) -> str: return self._USER_HOME
|
||||
def get_ui_widgets_path(self) -> str: return self._UI_WIDEGTS_PATH
|
||||
def get_trash_files_path(self) -> str: return self._TRASH_FILES_PATH
|
||||
def get_trash_info_path(self) -> str: return self._TRASH_INFO_PATH
|
||||
def get_plugins_path(self) -> str: return self._PLUGINS_PATH
|
||||
|
||||
def is_trace_debug(self) -> bool: return self._trace_debug
|
||||
def is_debug(self) -> bool: return self._debug
|
||||
|
||||
def set_trace_debug(self, trace_debug: bool):
|
||||
self._trace_debug = trace_debug
|
||||
|
||||
def set_debug(self, debug: bool):
|
||||
self._debug = debug
|
||||
|
||||
def load_settings(self):
|
||||
if not path.exists(self._CONFIG_FILE):
|
||||
self.settings = Settings()
|
||||
return
|
||||
|
||||
with open(self._CONFIG_FILE) as file:
|
||||
data = json.load(file)
|
||||
data["load_defaults"] = False
|
||||
self.settings = Settings(**data)
|
||||
|
||||
def save_settings(self):
|
||||
with open(self._CONFIG_FILE, 'w') as outfile:
|
||||
json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4)
|
8
src/solarfm/utils/settings_manager/options/__init__.py
Normal file
8
src/solarfm/utils/settings_manager/options/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Options module
|
||||
"""
|
||||
from .settings import Settings
|
||||
from .config import Config
|
||||
from .filters import Filters
|
||||
from .theming import Theming
|
||||
from .debugging import Debugging
|
36
src/solarfm/utils/settings_manager/options/config.py
Normal file
36
src/solarfm/utils/settings_manager/options/config.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
base_of_home: str = ""
|
||||
hide_hidden_files: str = "true"
|
||||
thumbnailer_path: str = "ffmpegthumbnailer"
|
||||
blender_thumbnailer_path: str = ""
|
||||
go_past_home: str = "true"
|
||||
lock_folder: str = "false"
|
||||
locked_folders: list = field(default_factory=lambda: ["venv", "flasks"])
|
||||
mplayer_options: str = "-quiet -really-quiet -xy 1600 -geometry 50%:50%"
|
||||
music_app: str = "deadbeef"
|
||||
media_app: str = "mpv"
|
||||
image_app: str = "mirage"
|
||||
office_app: str = "libreoffice"
|
||||
pdf_app: str = "evince"
|
||||
code_app: str = "atom"
|
||||
text_app: str = "mousepad"
|
||||
terminal_app: str = "terminator"
|
||||
file_manager_app: str = "solarfm"
|
||||
container_icon_wh: list = field(default_factory=lambda: [128, 128])
|
||||
video_icon_wh: list = field(default_factory=lambda: [128, 64])
|
||||
sys_icon_wh: list = field(default_factory=lambda: [56, 56])
|
||||
steam_cdn_url: str = "https://steamcdn-a.akamaihd.net/steam/apps/"
|
||||
remux_folder_max_disk_usage: str = "8589934592"
|
||||
application_dirs: list = field(default_factory=lambda: [
|
||||
"/usr/share/applications",
|
||||
f"{settings_manager.get_home_path()}/.local/share/applications"
|
||||
])
|
12
src/solarfm/utils/settings_manager/options/debugging.py
Normal file
12
src/solarfm/utils/settings_manager/options/debugging.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
@dataclass
|
||||
class Debugging:
|
||||
ch_log_lvl: int = 10
|
||||
fh_log_lvl: int = 20
|
90
src/solarfm/utils/settings_manager/options/filters.py
Normal file
90
src/solarfm/utils/settings_manager/options/filters.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
@dataclass
|
||||
class Filters:
|
||||
meshs: list = field(default_factory=lambda: [
|
||||
".blend",
|
||||
".dae",
|
||||
".fbx",
|
||||
".gltf",
|
||||
".obj",
|
||||
".stl"
|
||||
])
|
||||
code: list = field(default_factory=lambda: [
|
||||
".cpp",
|
||||
".css",
|
||||
".c",
|
||||
".go",
|
||||
".html",
|
||||
".htm",
|
||||
".java",
|
||||
".js",
|
||||
".json",
|
||||
".lua",
|
||||
".md",
|
||||
".py",
|
||||
".rs",
|
||||
".toml",
|
||||
".xml",
|
||||
".pom"
|
||||
])
|
||||
videos: list = field(default_factory=lambda:[
|
||||
".mkv",
|
||||
".mp4",
|
||||
".webm",
|
||||
".avi",
|
||||
".mov",
|
||||
".m4v",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".wmv",
|
||||
".flv"
|
||||
])
|
||||
office: list = field(default_factory=lambda: [
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".xlt",
|
||||
".xltx",
|
||||
".xlm",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".pps",
|
||||
".ppsx",
|
||||
".odt",
|
||||
".rtf"
|
||||
])
|
||||
images: list = field(default_factory=lambda: [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".ico",
|
||||
".tga",
|
||||
".webp"
|
||||
])
|
||||
text: list = field(default_factory=lambda: [
|
||||
".txt",
|
||||
".text",
|
||||
".sh",
|
||||
".cfg",
|
||||
".conf",
|
||||
".log"
|
||||
])
|
||||
music: list = field(default_factory=lambda: [
|
||||
".psf",
|
||||
".mp3",
|
||||
".ogg",
|
||||
".flac",
|
||||
".m4a"
|
||||
])
|
||||
pdf: list = field(default_factory=lambda: [
|
||||
".pdf"
|
||||
])
|
31
src/solarfm/utils/settings_manager/options/settings.py
Normal file
31
src/solarfm/utils/settings_manager/options/settings.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import asdict
|
||||
|
||||
# Gtk imports
|
||||
|
||||
# Application imports
|
||||
from .config import Config
|
||||
from .filters import Filters
|
||||
from .theming import Theming
|
||||
from .debugging import Debugging
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
load_defaults: bool = True
|
||||
config: Config = field(default_factory=lambda: Config())
|
||||
filters: Filters = field(default_factory=lambda: Filters())
|
||||
theming: Theming = field(default_factory=lambda: Theming())
|
||||
debugging: Debugging = field(default_factory=lambda: Debugging())
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.load_defaults:
|
||||
self.load_defaults = False
|
||||
self.config = Config(**self.config)
|
||||
self.filters = Filters(**self.filters)
|
||||
self.theming = Theming(**self.theming)
|
||||
self.debugging = Debugging(**self.debugging)
|
||||
|
||||
def as_dict(self):
|
||||
return asdict(self)
|
13
src/solarfm/utils/settings_manager/options/theming.py
Normal file
13
src/solarfm/utils/settings_manager/options/theming.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
@dataclass
|
||||
class Theming:
|
||||
success_color: str = "#88cc27"
|
||||
warning_color: str = "#ffa800"
|
||||
error_color: str = "#ff0000"
|
51
src/solarfm/utils/settings_manager/start_check_mixin.py
Normal file
51
src/solarfm/utils/settings_manager/start_check_mixin.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Python imports
|
||||
import os
|
||||
import json
|
||||
import inspect
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
|
||||
class StartCheckMixin:
|
||||
def is_dirty_start(self) -> bool: return self._dirty_start
|
||||
def clear_pid(self): self._clean_pid()
|
||||
|
||||
def do_dirty_start_check(self):
|
||||
if not os.path.exists(self._PID_FILE):
|
||||
self._write_new_pid()
|
||||
else:
|
||||
with open(self._PID_FILE, "r") as _pid:
|
||||
pid = _pid.readline().strip()
|
||||
if pid not in ("", None):
|
||||
self._check_alive_status(int(pid))
|
||||
else:
|
||||
self._write_new_pid()
|
||||
|
||||
""" Check For the existence of a unix pid. """
|
||||
def _check_alive_status(self, pid):
|
||||
print(f"PID Found: {pid}")
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
print(f"{app_name} is starting dirty...")
|
||||
self._dirty_start = True
|
||||
self._write_new_pid()
|
||||
return
|
||||
|
||||
print("PID is alive... Let downstream errors (sans debug args) handle app closure propigation.")
|
||||
|
||||
def _write_new_pid(self):
|
||||
pid = os.getpid()
|
||||
self._write_pid(pid)
|
||||
print(f"{app_name} PID: {pid}")
|
||||
|
||||
def _clean_pid(self):
|
||||
os.unlink(self._PID_FILE)
|
||||
|
||||
def _write_pid(self, pid):
|
||||
with open(self._PID_FILE, "w") as _pid:
|
||||
_pid.write(f"{pid}")
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user