Compare commits

...

18 Commits

Author SHA1 Message Date
itdominator 3a2e8eeb08 Attempted further memory leak prevention; fixed bugs from moving to python 12; misc. 2024-09-11 02:11:00 -05:00
itdominator 35456f2bca pyright changes, start.sh changes, misc. 2024-07-26 19:52:00 -05:00
itdominator 9d3a5b9f3b Attempting to prompt for gc; About page updates; small non crit errors fixed 2024-07-04 17:24:31 -05:00
itdominator ce00970171 moved thumbnail generation to plugin; extended plugin loading for pre and post window loading 2024-06-29 21:37:44 -05:00
itdominator 2f954f4c79 updated dir watch; removed keys call where senseable; added additional. debug hook; added threading and async code for testing 2024-06-12 00:32:14 -05:00
itdominator a362039e73 Fixed depricated exception class usage; fixed usertype interupt 2024-03-25 22:49:31 -05:00
itdominator 02c31719d1 Fixing translate plugin; attempted dispose call 2024-03-11 22:28:42 -05:00
itdominator d65ea8dec8 Scaling system icons as some do not match expected scale. 2024-03-11 20:03:48 -05:00
itdominator fec0d26ab7 Improved selection bounds on rename 2024-02-12 19:58:23 -06:00
itdominator a47bd23e78 made main method 2024-02-08 21:24:01 -06:00
itdominator 44ef6ea2bb Reworking some tab logic to omit adding a label widget 2024-01-29 22:53:51 -06:00
itdominator be7be00f78 refactoring pid logic; addedd window state preservation; slight thread rework 2024-01-08 21:11:10 -06:00
itdominator 8e5ae4824c idle_add refactor for event source clearing; Gtk main call moved 2024-01-03 20:36:17 -06:00
itdominator 37e3265be5 GLib idle add return effort 2 2023-12-31 22:35:43 -06:00
itdominator 4cafb7ff9f GLib idle add return effort 2023-12-31 22:20:04 -06:00
itdominator 9336df2afa Changing out threading for some sections; added 2 new tab option 2023-12-24 13:23:24 -06:00
itdominator d936b17429 Improved keybinding clarity; trying to fix thread and async issues 2023-11-25 15:52:43 -06:00
itdominator e6739c3087 Wrapped async in daemon thread for icon loading 2023-11-12 23:25:46 -06:00
77 changed files with 1179 additions and 576 deletions

View File

@ -8,7 +8,7 @@ Additionally, if not building a .deb then just move the contents of user_config
Copy the share/solarfm folder to your user .config/ directory too.
`pyrightconfig.json`
<p>The pyrightconfig file needs to stay on same level as the .git folders in order to have settings detected when using pyright with lsp functionality.</p>
<p>The pyrightconfig file needs to stay on same level as the .git folders in order to have settings detected when using pyright with lsp functionality. "pyrightconfig.json" can prompt IDEs such as Zed on settings to use and where imports are located- look at venvPath and venv. "venvPath" is parent path of "venv" where "venv" is just the name of the folder under the parent path that is the python created venv.
<h6>Install Setup</h6>
```

View File

@ -14,6 +14,7 @@ class Manifest:
'ui_target': "plugin_control_list",
'pass_fm_events': "true"
}
pre_launch: bool = False
```

View File

@ -122,7 +122,6 @@ class Plugin(PluginBase):
uri = state.uris[0]
path = state.tab.get_current_directory()
properties = self._set_ui_data(uri, path)
response = self._properties_dialog.run()
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
@ -168,13 +167,13 @@ class Plugin(PluginBase):
def _set_ui_data(self, uri, path):
properties = Properties()
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::*,owner::*,time::access,time::changed",
flags=Gio.FileQueryInfoFlags.NONE,
cancellable=None)
file_info = Gio.File.new_for_path(uri).query_info(attributes = "standard::*,owner::*,time::access,time::changed",
flags = Gio.FileQueryInfoFlags.NONE,
cancellable = None)
is_symlink = file_info.get_attribute_as_string("standard::is-symlink")
properties.file_uri = uri
properties.file_target = file_info.get_attribute_as_string("standard::symlink-target") if is_symlink else ""
properties.file_target = file_info.get_attribute_as_string("standard::symlink-target") if is_symlink in [True, "TRUE"] else ""
properties.file_name = file_info.get_display_name()
properties.file_location = path
properties.mime_type = file_info.get_content_type()
@ -186,7 +185,7 @@ class Plugin(PluginBase):
# NOTE: Read = 4, Write = 2, Exec = 1
command = ["stat", "-c", "%a", uri]
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
with subprocess.Popen(command, stdout = subprocess.PIPE) as proc:
properties.chmod_stat = list(proc.stdout.read().decode("UTF-8").strip())
owner = self._chmod_map[f"{properties.chmod_stat[0]}"]
group = self._chmod_map[f"{properties.chmod_stat[1]}"]

View File

@ -48,7 +48,7 @@ class GrepPreviewWidget(Gtk.Box):
return bytes(f"\n<span foreground='{color}'>{target}</span>", "utf-8").decode("utf-8")
def make_utf8_line_highlight(self, buffer, itr, i, color, target, query):
parts = re.split(r"(" + query + ")(?i)", target.replace("\n", ""))
parts = re.split(r"(?i)(" + query + ")", target.replace("\n", ""))
for part in parts:
itr = buffer.get_end_iter()

View File

@ -8,6 +8,7 @@
"ui_target": "plugin_control_list",
"pass_fm_events": "true",
"bind_keys": ["Example Plugin||send_message:<Control>f"]
}
},
"pre_launch": "false"
}
}

View File

@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@ -0,0 +1,73 @@
# Python imports
import json
import os
from os import path
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .icon import Icon
class IconController(Icon):
def __init__(self):
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
# NOTE: app_name should be defined using python 'builtins' and so too must be logger used in the various classes
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}"
self.DEFAULT_ICONS = f"{CONFIG_PATH}/icons"
self.DEFAULT_ICON = f"{self.DEFAULT_ICONS}/text.png"
self.FFMPG_THUMBNLR = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary
self.BLENDER_THUMBNLR = f"{CONFIG_PATH}/blender-thumbnailer" # Blender thumbnail generator binary
self.ICON_DIRS = ["/usr/share/icons", f"{USER_HOME}/.icons" "/usr/share/pixmaps"]
self.BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails"
self.ABS_THUMBS_PTH = f"{self.BASE_THUMBS_PTH}/normal"
self.STEAM_ICONS_PTH = f"{self.BASE_THUMBS_PTH}/steam_icons"
if not path.isdir(self.BASE_THUMBS_PTH):
os.mkdir(self.BASE_THUMBS_PTH)
if not path.isdir(self.ABS_THUMBS_PTH):
os.mkdir(self.ABS_THUMBS_PTH)
if not path.isdir(self.STEAM_ICONS_PTH):
os.mkdir(self.STEAM_ICONS_PTH)
if not os.path.exists(self.DEFAULT_ICONS):
self.DEFAULT_ICONS = f"{USR_APP_CONTEXT}/icons"
self.DEFAULT_ICON = f"{self.DEFAULT_ICONS}/text.png"
CONFIG_FILE = f"{CURRENT_PATH}/../settings.json"
with open(CONFIG_FILE) as f:
settings = json.load(f)
config = settings["config"]
self.container_icon_wh = config["container_icon_wh"]
self.video_icon_wh = config["video_icon_wh"]
self.sys_icon_wh = config["sys_icon_wh"]
# Filters
filters = settings["filters"]
self.fmeshs = tuple(filters["meshs"])
self.fcode = tuple(filters["code"])
self.fvideos = tuple(filters["videos"])
self.foffice = tuple(filters["office"])
self.fimages = tuple(filters["images"])
self.ftext = tuple(filters["text"])
self.fmusic = tuple(filters["music"])
self.fpdf = tuple(filters["pdf"])

View File

@ -138,6 +138,7 @@ class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin):
def _call_gtk_thread(event, result):
result.append( self.get_system_thumbnail(full_path, size) )
event.set()
return False
result = []
event = threading.Event()
@ -151,11 +152,11 @@ class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin):
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)
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)
return GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, width = size, height = size)
raise IconException("No system icon found...")
except IconException:

View File

@ -14,4 +14,4 @@ class MeshsIconMixin:
proc = subprocess.Popen([self.BLENDER_THUMBNLR, full_path, hash_img_path])
proc.wait()
except Exception as e:
self.logger.debug(repr(e))
logger.debug(repr(e))

View File

@ -14,7 +14,7 @@ class VideoIconMixin:
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))
logger.info(repr(e))
self.ffprobe_generate_video_thumbnail(full_path, hash_img_path)
@ -51,5 +51,4 @@ class VideoIconMixin:
proc.wait()
except Exception as e:
print("Video thumbnail generation issue in thread:")
print( repr(e) )
self.logger.debug(repr(e))
logger.info(repr(e))

View File

@ -0,0 +1,12 @@
{
"manifest": {
"name": "Thumbnailer",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {
"pass_fm_events": "true"
},
"pre_launch": "true"
}
}

View File

@ -0,0 +1,59 @@
# Python imports
import os
# Lib imports
# Application imports
from plugins.plugin_base import PluginBase
from .icons.controller import IconController
class Plugin(PluginBase):
def __init__(self):
super().__init__()
self.name = "Thumbnailer" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
# where self.name should not be needed for message comms
# self.path = os.path.dirname(os.path.realpath(__file__))
def run(self):
self.icon_controller = IconController()
self._event_system.subscribe("create-thumbnail", self.create_thumbnail)
def generate_reference_ui_element(self):
...
def create_thumbnail(self, dir, file) -> str:
return self.icon_controller.create_icon(dir, file)
def get_video_icons(self, dir) -> list:
data = []
def get_video_icons(self) -> list:
data = []
fvideos = self.icon_controller.fvideos
vids = [ file for file in os.path.list_dir(dir) if file.lower().endswith(fvideos) ]
for file in 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, dir) -> list:
data = []
for file in os.path.list_dir(dir):
icon = self.icon_controller.create_icon(dir, file).get_pixbuf()
data.append([icon, file])
return data
def get_gtk_icon_str_combo(self, dir) -> list:
data = []
for file in os.path.list_dir(dir):
icon = self.icon_controller.create_icon(dir, file)
data.append([icon, file[0]])
return data

View File

@ -0,0 +1,101 @@
{
"config":{
"thumbnailer_path":"ffmpegthumbnailer",
"blender_thumbnailer_path":"",
"container_icon_wh":[
128,
128
],
"video_icon_wh":[
128,
64
],
"sys_icon_wh":[
56,
56
],
"steam_cdn_url":"https://steamcdn-a.akamaihd.net/steam/apps/",
"remux_folder_max_disk_usage":"8589934592"
},
"filters":{
"meshs":[
".dae",
".fbx",
".gltf",
".obj",
".stl"
],
"code":[
".cpp",
".css",
".c",
".go",
".html",
".htm",
".java",
".js",
".json",
".lua",
".md",
".py",
".rs",
".toml",
".xml",
".pom"
],
"videos":[
".mkv",
".mp4",
".webm",
".avi",
".mov",
".m4v",
".mpg",
".mpeg",
".wmv",
".flv"
],
"office":[
".doc",
".docx",
".xls",
".xlsx",
".xlt",
".xltx",
".xlm",
".ppt",
".pptx",
".pps",
".ppsx",
".odt",
".rtf"
],
"images":[
".png",
".jpg",
".jpeg",
".gif",
".ico",
".tga",
".webp"
],
"text":[
".txt",
".text",
".sh",
".cfg",
".conf",
".log"
],
"music":[
".psf",
".mp3",
".ogg",
".flac",
".m4a"
],
"pdf":[
".pdf"
]
}
}

View File

@ -184,8 +184,8 @@ class Plugin(PluginBase):
response = requests.post(self.vqd_link, headers=self.vqd_headers, data=self.vqd_data, timeout=2)
if response.status_code == 200:
data = response.content
vqd_start_index = data.index(b"vqd='") + 5
vqd_end_index = data.index(b"'", vqd_start_index)
vqd_start_index = data.index(b"vqd=\"") + 5
vqd_end_index = data.index(b"\"", vqd_start_index)
self._vqd_attrib = data[vqd_start_index:vqd_end_index].decode("utf-8")
print(f"Translation VQD: {self._vqd_attrib}")

View File

@ -111,6 +111,8 @@ class Plugin(PluginBase):
for uri in state.uris:
self.trashman.trash(uri, verbocity)
self.trashman.regenerate()
def restore_trash_files(self, widget = None, eve = None, verbocity = False):
self._event_system.emit("get_current_state")
state = self._fm_state

View File

@ -43,4 +43,4 @@ class Trash(object):
def restore(self, filename, verbose):
"""Restore a file from trash."""
raise NotImplementedError(_('Backend didnt \ implement this functionality'))
raise NotImplementedError(_('Backend didnt implement this functionality'))

View File

@ -127,7 +127,7 @@ DeletionDate={}
f.write(infofile)
f.close()
self.regenerate()
# self.regenerate()
if verbose:
sys.stderr.write(_('trashed \'{}\'\n').format(filename))

View File

@ -7,5 +7,7 @@
{
"root": "./src/versions/solarfm-0.0.1/solarfm"
}
]
],
"venvPath": "/home/abaddon/Portable_Apps/py-venvs/pylsp-venv/",
"venv": "venv"
}

View File

@ -16,13 +16,17 @@ 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()
thread = threading.Thread(target = fn, args = args, kwargs = kwargs, daemon = False)
thread.start()
return thread
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()
thread = threading.Thread(target = fn, args = args, kwargs = kwargs, daemon = True)
thread.start()
return thread
return wrapper
def sizeof_fmt_def(num, suffix="B"):

View File

@ -3,14 +3,13 @@
# Python imports
import argparse
import faulthandler
import locale
import traceback
from setproctitle import setproctitle
import tracemalloc
tracemalloc.start()
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from __builtins__ import *
@ -18,24 +17,8 @@ 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()
def main(args, unknownargs):
setproctitle(f'{app_name}')
if args.debug == "true":
settings_manager.set_debug(True)
@ -45,12 +28,27 @@ def run():
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()
''' Set process title, get arguments, and create GTK main thread. '''
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", "-nt", default="false", help="Opens a 'New Tab' if a handler is set for it.")
parser.add_argument("--file", "-f", default="default", help="JUST SOME FILE ARG.")
# Read arguments (If any...)
args, unknownargs = parser.parse_known_args()
try:
faulthandler.enable() # For better debug info
main(args, unknownargs)
except Exception as e:
traceback.print_exc()
quit()

View File

@ -15,35 +15,40 @@ class AppLaunchException(Exception):
...
class Application(IPCServer):
class Application:
""" 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.load_ipc(args, unknownargs)
self.setup_debug_hook()
Window(args, unknownargs)
Window(args, unknownargs).main()
def socket_realization_check(self):
def load_ipc(self, args, unknownargs):
ipc_server = IPCServer()
self.ipc_realization_check(ipc_server)
if not ipc_server.is_ipc_alive:
for arg in unknownargs + [args.new_tab,]:
if os.path.isfile(arg):
message = f"FILE|{arg}"
ipc_server.send_ipc_message(message)
raise AppLaunchException(f"{app_name} IPC Server Exists: Have sent path(s) to it and closing...")
def ipc_realization_check(self, ipc_server):
try:
self.create_ipc_listener()
ipc_server.create_ipc_listener()
except Exception:
self.send_test_ipc_message()
ipc_server.send_test_ipc_message()
try:
self.create_ipc_listener()
ipc_server.create_ipc_listener()
except Exception as e:
...
@ -51,7 +56,7 @@ class Application(IPCServer):
try:
# kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows
signal.signal(
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"),
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR2"),
debug_signal_handler
)
except ValueError:

View File

@ -44,10 +44,13 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
self._subscribe_to_events()
self._load_widgets()
if args.no_plugins == "false":
self.plugins_controller.pre_launch_plugins()
self._generate_file_views(self.fm_controller_data)
if args.no_plugins == "false":
self.plugins.launch_plugins()
self.plugins_controller.post_launch_plugins()
for arg in unknownargs + [args.new_tab,]:
if os.path.isdir(arg):
@ -78,6 +81,7 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
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)
@ -113,8 +117,9 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
def reload_plugins(self, widget=None, eve=None):
self.plugins.reload_plugins()
self.plugins_controller.reload_plugins()
def do_action_from_menu_controls(self, _action=None, eve=None):
@ -130,44 +135,48 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
event_system.emit("hide_rename_file_menu")
if action == "open":
event_system.emit("open_files")
event_system.emit_and_await("open_files")
if action == "open_with":
event_system.emit("show_appchooser_menu")
event_system.emit_and_await("show_appchooser_menu")
if action == "open_2_new_tab":
event_system.emit_and_await("open_2_new_tab")
if action == "execute":
event_system.emit("execute_files")
event_system.emit_and_await("execute_files")
if action == "execute_in_terminal":
event_system.emit("execute_files", (True,))
event_system.emit_and_await("execute_files", (True,))
if action == "rename":
event_system.emit("rename_files")
event_system.emit_and_await("rename_files")
if action == "cut":
event_system.emit("cut_files")
event_system.emit_and_await("cut_files")
if action == "copy":
event_system.emit("copy_files")
event_system.emit_and_await("copy_files")
if action == "copy_path":
event_system.emit("copy_path")
event_system.emit_and_await("copy_path")
if action == "copy_name":
event_system.emit("copy_name")
event_system.emit_and_await("copy_name")
if action == "copy_path_name":
event_system.emit("copy_path_name")
event_system.emit_and_await("copy_path_name")
if action == "paste":
event_system.emit("paste_files")
event_system.emit_and_await("paste_files")
if action == "create":
event_system.emit("create_files")
event_system.emit_and_await("create_files")
if action in ["save_session", "save_session_as", "load_session"]:
event_system.emit("save_load_session", (action))
event_system.emit_and_await("save_load_session", (action))
if action == "about_page":
event_system.emit("show_about_page")
event_system.emit_and_await("show_about_page")
if action == "io_popup":
event_system.emit("show_io_popup")
event_system.emit_and_await("show_io_popup")
if action == "plugins_popup":
event_system.emit("show_plugins_popup")
event_system.emit_and_await("show_plugins_popup")
if action == "messages_popup":
event_system.emit("show_messages_popup")
event_system.emit_and_await("show_messages_popup")
if action == "ui_debug":
event_system.emit("load_interactive_debug")
event_system.emit_and_await("load_interactive_debug")
if action == "tear_down":
event_system.emit("tear_down")
event_system.emit_and_await("tear_down")
action = None
def go_home(self, widget=None, eve=None):
@ -185,11 +194,14 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
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()
top_main_menubar = None
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())
wid, tid, tab = None, None, None
def go_to_path(self, path: str):
self.builder.get_object("path_entry").set_text(path)

View File

@ -29,7 +29,7 @@ class Controller_Data:
self._load_glade_file()
self.fm_controller = WindowController()
self.plugins = PluginsController()
self.plugins_controller = PluginsController()
self.fm_controller_data = self.fm_controller.get_state_from_file()
self.window1 = self.builder.get_object("window_1")
@ -70,23 +70,16 @@ class Controller_Data:
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:
@ -121,6 +114,9 @@ class Controller_Data:
uris.append(fpath)
tab = None
dir = None
return uris

View File

@ -40,6 +40,7 @@ class FileSystemActions(HandlerMixin, CRUDMixin):
event_system.subscribe("open_files", self.open_files)
event_system.subscribe("open_with_files", self.open_with_files)
event_system.subscribe("open_2_new_tab", self.open_2_new_tab)
event_system.subscribe("execute_files", self.execute_files)
event_system.subscribe("cut_files", self.cut_files)
@ -104,6 +105,12 @@ class FileSystemActions(HandlerMixin, CRUDMixin):
state.tab.app_chooser_exec(app_info, uris)
def open_2_new_tab(self):
state = event_system.emit_and_await("get_current_state")
uri = state.uris[0]
message = f"FILE|{uri}"
logger.info(message)
event_system.emit("post_file_to_ipc", message)
def execute_files(self, in_terminal=False):
state = event_system.emit_and_await("get_current_state")

View File

@ -110,26 +110,30 @@ class HandlerMixin:
tab.move_file(fPath, tPath)
else:
io_widget = IOWidget(action, file)
io_list = self._builder.get_object("io_list")
io_list.add(io_widget)
io_list.show_all()
if action == "copy":
file.copy_async(destination=target,
flags=Gio.FileCopyFlags.BACKUP,
io_priority=98,
io_priority=45,
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,
io_priority=45,
cancellable=io_widget.cancle_eve,
progress_callback=None,
# NOTE: progress_callback here causes seg fault when set
progress_callback=None,
callback=io_widget.finish_callback)
self._builder.get_object("io_list").add(io_widget)
io_widget = None
io_list = None
except GObject.GError as e:
raise OSError(e)

View File

@ -30,57 +30,53 @@ class FileActionSignalsMixin:
wid = tab.get_wid()
tid = tab.get_id()
dir_watcher.connect("changed", self.dir_watch_updates, (f"{wid}|{tid}",))
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):
def dir_watch_updates(self, file_monitor, file, other_file = None, eve_type = None, tab_widget_id = 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])
self.soft_lock_countdown(tab_widget_id)
@threaded
def soft_lock_countdown(self, tab_widget):
self.soft_update_lock[tab_widget] = { "last_update_time": time.time()}
def soft_lock_countdown(self, tab_widget_id):
if tab_widget_id in self.soft_update_lock:
timeout_id = self.soft_update_lock[tab_widget_id]["timeout_id"]
GLib.source_remove(timeout_id)
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,))
timeout_id = GLib.timeout_add(0, self.update_on_soft_lock_end, 600, *(tab_widget_id,))
self.soft_update_lock[tab_widget_id] = { "timeout_id": timeout_id }
def update_on_soft_lock_end(self, tab_widget):
wid, tid = tab_widget.split("|")
def update_on_soft_lock_end(self, timout_ms, tab_widget_id):
self.soft_update_lock.pop(tab_widget_id, None)
wid, tid = tab_widget_id.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}")
_store, tab_widget_id_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())
tab_widget_id_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)
wid, tid = None, None
notebook = None
tab = None
icon_grid = None
store = None
_store, tab_widget_id_label = None, None
state = None
return False
def do_file_search(self, widget, eve = None):
if not self.ctrl_down and not self.shift_down and not self.alt_down:

View File

@ -11,8 +11,8 @@ from gi.repository import Gdk
# Application imports
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
@ -20,13 +20,25 @@ 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):
def unset_keys_and_data(self, widget = None, eve = None):
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
def unmap_special_keys(self, keyname):
if "control" in keyname:
self.ctrl_down = False
if "shift" in keyname:
self.shift_down = False
if "alt" in keyname:
self.alt_down = False
def on_global_key_press_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
modifiers = Gdk.ModifierType(user_data.get_state() & ~Gdk.ModifierType.LOCK_MASK)
self.was_midified_key = True if modifiers != 0 else False
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
self.ctrl_down = True
@ -36,52 +48,49 @@ class KeyboardSignalsMixin:
self.alt_down = True
def on_global_key_release_controller(self, widget, event):
"""Handler for keyboard events"""
""" Handler for keyboard events """
keyname = Gdk.keyval_name(event.keyval).lower()
modifiers = Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK)
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
should_return = self.was_midified_key and (self.ctrl_down or self.shift_down or self.alt_down)
self.unmap_special_keys(keyname)
if should_return:
self.was_midified_key = False
return
mapping = keybindings.lookup(event)
logger.debug(f"on_global_key_release_controller > key > {keyname}")
logger.debug(f"on_global_key_release_controller > keyval > {event.keyval}")
logger.debug(f"on_global_key_release_controller > mapping > {mapping}")
if mapping:
# See if in filemanager scope
self.handle_mapped_key_event(mapping)
else:
self.handle_as_key_event_scope(keyname)
def handle_mapped_key_event(self, mapping):
try:
getattr(self, mapping)()
return True
self.handle_as_controller_scope(mapping)
except Exception:
# Must be plugins scope, event call, OR we forgot to add method to file manager scope
self.handle_as_plugin_scope(mapping)
def handle_as_controller_scope(self, mapping):
getattr(self, mapping)()
def handle_as_plugin_scope(self, mapping):
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}")
self.handle_key_event_system(sender, eve_type)
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_as_key_event_scope(self, keyname):
if self.ctrl_down and not keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]:
self.handle_key_event_system(None, keyname)
def handle_plugin_key_event(self, sender, eve_type):
def handle_key_event_system(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()

View File

@ -7,6 +7,7 @@ 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 ...widgets.tab_header_widget import TabHeaderWidget
@ -19,6 +20,17 @@ class GridMixin:
"""docstring for GridMixin"""
def load_store(self, tab, store, save_state = False, use_generator = False):
# dir = tab.get_current_directory()
# file = Gio.File.new_for_path(dir)
# dir_list = Gtk.DirectoryList.new("standard::*", file)
# store.set(dir_list)
# file = Gio.File.new_for_path(dir)
# for file in file.enumerate_children("standard::*", Gio.FILE_ATTRIBUTE_STANDARD_NAME, None):
# store.append(file)
# return
dir = tab.get_current_directory()
files = tab.get_files()
@ -26,61 +38,77 @@ class GridMixin:
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) )
self.generate_icons(tab, store, dir, files)
# GLib.Thread("", self.generate_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)
dir = None
files = None
async def load_icon(self, i, store, icon):
self.update_store(i, store, icon)
@daemon_threaded
def generate_icons(self, tab, store, dir, files):
for i, file in enumerate(files):
# GLib.Thread(f"{i}", self.make_and_load_icon, i, store, tab, dir, file[0])
self.make_and_load_icon( i, store, tab, dir, file[0])
async def update_store(self, i, store, dir, tab, file):
icon = tab.create_icon(dir, file)
def update_store(self, i, store, icon):
itr = store.get_iter(i)
GLib.idle_add(self.insert_store, store, itr, icon.copy())
itr = None
del icon
@daemon_threaded
def make_and_load_icon(self, i, store, tab, dir, file):
icon = tab.create_icon(dir, file)
self.update_store(i, store, icon)
icon = None
def get_icon(self, tab, dir, file):
tab.create_icon(dir, file)
# @daemon_threaded
# def generate_icons(self, tab, store, dir, files):
# try:
# loop = asyncio.get_running_loop()
# except RuntimeError:
# loop = None
# if loop and loop.is_running():
# loop = asyncio.get_event_loop()
# loop.create_task( self.create_icons(tab, store, dir, files) )
# else:
# asyncio.run( self.create_icons(tab, store, dir, files) )
# async def create_icons(self, tab, store, dir, files):
# icons = [self.get_icon(tab, dir, file[0]) for file in files]
# data = await asyncio.gather(*icons)
# tasks = [self.update_store(i, store, icon) for i, icon in enumerate(data)]
# asyncio.gather(*tasks)
# async def update_store(self, i, store, icon):
# itr = store.get_iter(i)
# GLib.idle_add(self.insert_store, store, itr, icon)
# async def get_icon(self, tab, dir, file):
# return tab.create_icon(dir, file)
def insert_store(self, store, itr, icon):
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
# Note: If the function returns GLib.SOURCE_REMOVE or False it is automatically removed from the list of event sources and will not be called again.
return False
# @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 do_ui_update(self):
Gtk.main_iteration()
return False
# 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_tab_widget(self):
return TabHeaderWidget(self.close_tab)
def create_scroll_and_store(self, tab, wid, use_tree_view = False):
scroll = Gtk.ScrolledWindow()
@ -137,6 +165,7 @@ class GridMixin:
store = icon_grid.get_model()
tab_label = notebook.get_tab_label(obj).get_children()[0]
icon_grid = None
return store, tab_label
def get_icon_grid_from_notebook(self, notebook, _name):

View File

@ -34,27 +34,46 @@ class TabMixin(GridMixin):
else:
tab.set_path(path)
tab_widget = self.create_tab_widget(tab)
tab_widget = self.get_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)
notebook.set_tab_reorderable(scroll, True)
self.fm_controller.set_wid_and_tid(wid, tab.get_id())
path_entry.set_text(tab.get_current_directory())
# path_entry.set_text(tab.get_current_directory())
event_system.emit("go_to_path", (tab.get_current_directory(),)) # NOTE: Not efficent if I understand how
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)
tab_widget = None
scroll, store = None, None
index = None
notebook = None
path_entry = None
tab = None
ctx = None
def get_tab_widget(self, tab):
tab_widget = self.create_tab_widget()
tab_widget.tab = tab
tab_widget.label.set_label(f"{tab.get_end_of_path()}")
tab_widget.label.set_width_chars(len(tab.get_end_of_path()))
return tab_widget
def close_tab(self, button, eve = None):
notebook = button.get_parent().get_parent()
if notebook.get_n_pages() == 1:
notebook = None
return
tab_box = button.get_parent()
@ -72,23 +91,35 @@ class TabMixin(GridMixin):
self.builder.dereference_object(f"{wid}|{tid}|icon_grid")
self.builder.dereference_object(f"{wid}|{tid}")
iter = store.get_iter_first()
while iter:
next_iter = store.iter_next(iter)
store.unref_node(iter)
iter = next_iter
store.clear()
icon_grid.destroy()
scroll.destroy()
tab_box.destroy()
store.run_dispose()
del store
del icon_grid
del scroll
del tab_box
del watcher
del tab
icon_grid.set_model(None)
icon_grid.run_dispose()
scroll.run_dispose()
tab_box.run_dispose()
iter = None
wid, tid = None, None
store = None
icon_grid = None
scroll = None
tab_box = None
watcher = None
tab = None
notebook = None
gc.collect()
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
self.set_window_title()
gc.collect()
# NOTE: Not actually getting called even tho set in the glade file...
def on_tab_dnded(self, notebook, page, x, y):
@ -111,15 +142,23 @@ class TabMixin(GridMixin):
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
wid, tid = None, None
window = None
tab = None
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()
wid, tid = None, None
def get_id_from_tab_box(self, tab_box):
return tab_box.get_children()[2].get_text()
return tab_box.tab.get_id()
def get_tab_label(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
@ -135,6 +174,8 @@ class TabMixin(GridMixin):
state.tab.load_directory()
self.load_store(state.tab, state.store)
state = None
def update_tab(self, tab_label, tab, store, wid, tid):
self.load_store(tab, store)
self.set_path_text(wid, tid)
@ -175,16 +216,38 @@ class TabMixin(GridMixin):
if isinstance(focused_obj, Gtk.Entry):
self.process_path_menu(widget, tab, dir)
action = None
store = None
if path.endswith(".") or path == dir:
tab_label = None
notebook = None
wid, tid = None, None
path = None
tab = None
return
if not tab.set_path(path):
tab_label = None
notebook = None
wid, tid = None, None
path = None
tab = None
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)
action = None
wid, tid = None, None
notebook = None
store, tab_label = None, None
path = None
tab = None
icon_grid = None
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, "")
@ -201,11 +264,16 @@ class TabMixin(GridMixin):
path_menu_buttons.add(button)
show_path_menu = True
query = None
files = None
if not show_path_menu:
path_menu_buttons = None
event_system.emit("hide_path_menu")
else:
event_system.emit("show_path_menu")
buttons = path_menu_buttons.get_children()
path_menu_buttons = None
if len(buttons) == 1:
self.slowed_focus(buttons[0])
@ -218,6 +286,7 @@ class TabMixin(GridMixin):
def do_focused_click(self, button):
button.grab_focus()
button.clicked()
return False
def set_path_entry(self, button = None, eve = None):
self.path_auto_filled = True
@ -230,6 +299,10 @@ class TabMixin(GridMixin):
path_entry.set_position(-1)
event_system.emit("hide_path_menu")
state = None
path = None
path_entry = None
def show_hide_hidden_files(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
@ -237,3 +310,6 @@ class TabMixin(GridMixin):
tab.set_hiding_hidden(not tab.is_hiding_hidden())
tab.load_directory()
self.builder.get_object("refresh_tab").released()
wid, tid = None, None
tab = None

View File

@ -46,11 +46,19 @@ class WindowMixin(TabMixin):
self.window.set_title(f"{app_name} ~ {dir}")
self.set_bottom_labels(tab)
wid, tid = None, None
notebook = None
tab = None
dir = None
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())
path_entry = None
tab = None
def grid_set_selected_items(self, icons_grid):
new_items = icons_grid.get_selected_items()
items_size = len(new_items)
@ -122,6 +130,10 @@ class WindowMixin(TabMixin):
self.update_tab(tab_label, state.tab, state.icon_grid.get_store(), state.wid, state.tid)
else:
event_system.emit("open_files")
state = None
notebook = None
tab_label = None
except WindowException as e:
traceback.print_exc()
self.display_message(settings.theming.error_color, f"{repr(e)}")
@ -164,6 +176,12 @@ class WindowMixin(TabMixin):
if target not in current:
self.fm_controller.set_wid_and_tid(wid, tid)
current = None
target = None
wid, tid = None, None
store = None
path_at_loc = None
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
@ -177,6 +195,10 @@ class WindowMixin(TabMixin):
if from_uri != dest:
event_system.emit("move_files", (uris, dest))
Gtk.drag_finish(drag_context, True, False, time)
return
Gtk.drag_finish(drag_context, False, False, time)
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, None, path)

View File

@ -6,6 +6,7 @@ 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 .mixins.ui.pane_mixin import PaneMixin
@ -34,18 +35,20 @@ class UIMixin(PaneMixin, WindowMixin):
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))
event_system.emit_and_await("load_files_view_state", (nickname, tabs, isHidden))
@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)
time.sleep(0.2)
icon_grid.event(Gdk.Event().new(type = Gdk.EventType.BUTTON_RELEASE))
window = None
def _current_loading_process(self, session_json = None):
if session_json:
for j, value in enumerate(session_json):
@ -77,7 +80,7 @@ class UIMixin(PaneMixin, WindowMixin):
scroll_win = notebook.get_children()[-1]
icon_grid = scroll_win.get_children()[0]
self._focus_last_visible_notebook(icon_grid)
GLib.Thread("", 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))

View File

@ -16,10 +16,7 @@ class ContextMenuWidget(Gtk.Menu):
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()
@ -32,24 +29,57 @@ class ContextMenuWidget(Gtk.Menu):
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)
settings_manager.register_signals_to_builder(self, self._builder)
def _load_widgets(self):
self.builder = settings_manager.get_builder()
self._window = settings_manager.get_main_window()
self._context_menu_data = settings_manager.get_context_menu_data()
self.builder.expose_object("context_menu", 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):
def build_context_menu(self) -> None:
data = self._context_menu_data
plugins_entry = None
for key, value in data.items():
entry = self.make_menu_item(key, value)
self.append(entry)
if key == "Plugins":
plugins_entry = entry
self.attach_to_widget(self._window, None)
self.show_all()
if plugins_entry:
self.builder.expose_object("context_menu_plugins", plugins_entry.get_submenu())
def make_menu_item(self, label, data) -> Gtk.MenuItem:
if isinstance(data, dict):
return self.make_submenu(label, data)
elif isinstance(data, list):
entry = Gtk.ImageMenuItem(label)
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 make_submenu(self, name, data):
menu = Gtk.Menu()
menu_item = Gtk.MenuItem(name)
for key in keys:
for key, value in data.items():
if isinstance(data, dict):
entry = self.make_menu_item(key, data[key])
entry = self.make_menu_item(key, value)
elif isinstance(data, list):
entry = self.make_menu_item(key, data)
entry = self.make_menu_item(key, value)
else:
continue
@ -58,36 +88,9 @@ class ContextMenuWidget(Gtk.Menu):
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):
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):
def hide_context_menu(self, widget = None, eve = None):
self.builder.get_object("context_menu").popdown()

View File

@ -39,11 +39,10 @@ class AboutWidget:
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):
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):
def hide_about_page(self, widget = None, eve = None):
self.about_page.hide()

View File

@ -50,6 +50,9 @@ class RenameWidget:
def show_rename_file_menu(self, widget=None, eve=None):
if widget:
widget.grab_focus()
end_i = widget.get_text().rfind(".")
if end_i > 0:
widget.select_region(0, end_i)
response = self._rename_file_menu.run()
if response == Gtk.ResponseType.CLOSE:

View File

@ -1,11 +1,9 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ...sfm_builder import SFMBuilder
from ...mixins.signals.file_action_signals_mixin import FileActionSignalsMixin
from .window_mixin import WindowMixin
@ -27,7 +25,8 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin):
self.INDEX = self.ccount
self.NAME = f"window_{self.INDEX}"
self.builder = Gtk.Builder()
self.builder = SFMBuilder()
self.files_view = None
self.fm_controller = None
@ -41,7 +40,7 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin):
...
def _setup_signals(self):
settings_manager.register_signals_to_builder([self,], self.builder)
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)
@ -52,9 +51,10 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin):
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):
def _load_files_view_state(self, win_name = None, tabs = None, isHidden = False):
if win_name == self.NAME:
if tabs:
for tab in tabs:
@ -62,6 +62,9 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin):
else:
self.create_new_tab_notebook(None, self.INDEX, None)
if isHidden:
self.files_view.hide()
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)

View File

@ -7,6 +7,7 @@ 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 ...widgets.tab_header_widget import TabHeaderWidget
@ -19,6 +20,17 @@ class GridMixin:
"""docstring for GridMixin"""
def load_store(self, tab, store, save_state = False, use_generator = False):
# dir = tab.get_current_directory()
# file = Gio.File.new_for_path(dir)
# dir_list = Gtk.DirectoryList.new("standard::*", file)
# store.set(dir_list)
# file = Gio.File.new_for_path(dir)
# for file in file.enumerate_children("standard::*", Gio.FILE_ATTRIBUTE_STANDARD_NAME, None):
# store.append(file)
# return
dir = tab.get_current_directory()
files = tab.get_files()
@ -26,60 +38,76 @@ class GridMixin:
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) )
self.generate_icons(tab, store, dir, files)
# GLib.Thread("", self.generate_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)
dir = None
files = None
async def load_icon(self, i, store, icon):
self.update_store(i, store, icon)
@daemon_threaded
def generate_icons(self, tab, store, dir, files):
for i, file in enumerate(files):
# GLib.Thread(f"{i}", self.make_and_load_icon, i, store, tab, dir, file[0])
self.make_and_load_icon( i, store, tab, dir, file[0])
async def update_store(self, i, store, dir, tab, file):
icon = tab.create_icon(dir, file)
def update_store(self, i, store, icon):
itr = store.get_iter(i)
GLib.idle_add(self.insert_store, store, itr, icon)
itr = None
@daemon_threaded
def make_and_load_icon(self, i, store, tab, dir, file):
icon = tab.create_icon(dir, file)
self.update_store(i, store, icon)
icon = None
def get_icon(self, tab, dir, file):
tab.create_icon(dir, file)
# @daemon_threaded
# def generate_icons(self, tab, store, dir, files):
# try:
# loop = asyncio.get_running_loop()
# except RuntimeError:
# loop = None
# if loop and loop.is_running():
# loop = asyncio.get_event_loop()
# loop.create_task( self.create_icons(tab, store, dir, files) )
# else:
# asyncio.run( self.create_icons(tab, store, dir, files) )
# async def create_icons(self, tab, store, dir, files):
# icons = [self.get_icon(tab, dir, file[0]) for file in files]
# data = await asyncio.gather(*icons)
# tasks = [self.update_store(i, store, icon) for i, icon in enumerate(data)]
# asyncio.gather(*tasks)
# async def update_store(self, i, store, icon):
# itr = store.get_iter(i)
# GLib.idle_add(self.insert_store, store, itr, icon)
# async def get_icon(self, tab, dir, file):
# return tab.create_icon(dir, file)
def insert_store(self, store, itr, icon):
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
# Note: If the function returns GLib.SOURCE_REMOVE or False it is automatically removed from the list of event sources and will not be called again.
return False
# @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 do_ui_update(self):
Gtk.main_iteration()
return False
def create_tab_widget(self, tab):
return TabHeaderWidget(tab, self.close_tab)
def create_tab_widget(self):
return TabHeaderWidget(self.close_tab)
def create_scroll_and_store(self, tab, wid, use_tree_view = False):
scroll = Gtk.ScrolledWindow()
@ -137,3 +165,10 @@ class GridMixin:
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

View File

@ -34,29 +34,46 @@ class TabMixin(GridMixin):
else:
tab.set_path(path)
tab_widget = self.create_tab_widget(tab)
tab_widget = self.get_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)
notebook.set_tab_reorderable(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())
event_system.emit("go_to_path", (tab.get_current_directory(),)) # NOTE: Not efficent if I understand how
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)
tab_widget = None
scroll, store = None, None
index = None
notebook = None
# path_entry = None
tab = None
ctx = None
def get_tab_widget(self, tab):
tab_widget = self.create_tab_widget()
tab_widget.tab_id = tab.get_id()
tab_widget.label.set_label(f"{tab.get_end_of_path()}")
tab_widget.label.set_width_chars(len(tab.get_end_of_path()))
return tab_widget
def close_tab(self, button, eve = None):
notebook = button.get_parent().get_parent()
if notebook.get_n_pages() == 1:
notebook = None
return
tab_box = button.get_parent()
@ -74,28 +91,37 @@ class TabMixin(GridMixin):
self.builder.dereference_object(f"{wid}|{tid}|icon_grid")
self.builder.dereference_object(f"{wid}|{tid}")
iter = store.get_iter_first()
while iter:
next_iter = store.iter_next(iter)
store.unref_node(iter)
iter = next_iter
store.clear()
# store.run_dispose()
icon_grid.destroy()
# icon_grid.run_dispose()
scroll.destroy()
#scroll.run_dispose()
tab_box.destroy()
#tab_box.run_dispose()
store.run_dispose()
del store
del icon_grid
del scroll
del tab_box
del watcher
del tab
icon_grid.set_model(None)
icon_grid.run_dispose()
scroll.run_dispose()
tab_box.run_dispose()
iter = None
wid, tid = None, None
store = None
icon_grid = None
scroll = None
tab_box = None
watcher = None
tab = None
notebook = None
gc.collect()
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
self.set_window_title()
gc.collect()
# NOTE: Not actually getting called even tho set in the glade file...
def on_tab_dnded(self, notebook, page, x, y):
...
@ -117,15 +143,22 @@ class TabMixin(GridMixin):
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
wid, tid = None, None
window = None
tab = None
def on_tab_switch_update(self, notebook, content = None, index = None):
self.selected_files.clear()
wid, tid = content.get_children()[0].get_name().split("|")
wid, tid = content.get_children()[0].tab.get_name().split("|")
self.fm_controller.set_wid_and_tid(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
wid, tid = None, None
def get_id_from_tab_box(self, tab_box):
return tab_box.get_children()[2].get_text()
return tab_box.tab.get_id()
def get_tab_label(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
@ -141,6 +174,8 @@ class TabMixin(GridMixin):
state.tab.load_directory()
self.load_store(state.tab, state.store)
state = None
def update_tab(self, tab_label, tab, store, wid, tid):
self.load_store(tab, store)
self.set_path_text(wid, tid)
@ -181,16 +216,38 @@ class TabMixin(GridMixin):
if isinstance(focused_obj, Gtk.Entry):
self.process_path_menu(widget, tab, dir)
action = None
store = None
if path.endswith(".") or path == dir:
tab_label = None
notebook = None
wid, tid = None, None
path = None
tab = None
return
if not tab.set_path(path):
tab_label = None
notebook = None
wid, tid = None, None
path = None
tab = None
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)
action = None
wid, tid = None, None
notebook = None
store, tab_label = None, None
path = None
tab = None
icon_grid = None
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, "")
@ -207,6 +264,10 @@ class TabMixin(GridMixin):
path_menu_buttons.add(button)
show_path_menu = True
path_menu_buttons = None
query = None
files = None
if not show_path_menu:
event_system.emit("hide_path_menu")
else:
@ -225,6 +286,8 @@ class TabMixin(GridMixin):
button.grab_focus()
button.clicked()
return False
def set_path_entry(self, button = None, eve = None):
self.path_auto_filled = True
state = self.get_current_state()
@ -236,9 +299,16 @@ class TabMixin(GridMixin):
path_entry.set_position(-1)
event_system.emit("hide_path_menu")
state = None
path = None
path_entry = None
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()
wid, tid = None, None
tab = None

View File

@ -42,10 +42,17 @@ class WindowMixin(TabMixin):
event_system.emit("set_window_title", (dir,))
self.set_bottom_labels(tab)
wid, tid = None, None
notebook = None
tab = None
dir = None
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(),))
tab = None
def grid_set_selected_items(self, icons_grid):
new_items = icons_grid.get_selected_items()
items_size = len(new_items)
@ -160,6 +167,12 @@ class WindowMixin(TabMixin):
if target not in current:
self.fm_controller.set_wid_and_tid(wid, tid)
current = None
target = None
wid, tid = None, None
store = None
path_at_loc = None
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
@ -173,6 +186,10 @@ class WindowMixin(TabMixin):
if from_uri != dest:
event_system.emit("move_files", (uris, dest))
Gtk.drag_finish(drag_context, True, False, time)
return
Gtk.drag_finish(drag_context, False, False, time)
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, None, path)

View File

@ -75,6 +75,20 @@ class IconGridWidget(Gtk.IconView):
return self.get_model()
def clear_and_set_new_store(self):
store = self.get_model()
if store:
iter = store.get_iter_first()
while iter:
next_iter = store.iter_next(iter)
store.unref_node(iter)
iter = next_iter
store.clear()
store.run_dispose()
store = None
self.set_model(None)
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str or None)
# store = Gtk.ListStore(Gtk.DirectoryList)
self.set_model(store)
store = None

View File

@ -59,7 +59,7 @@ class IconTreeWidget(Gtk.TreeView):
name = Gtk.CellRendererText()
selec = self.get_selection()
self.set_model(store)
self.set_model(self._store)
selec.set_mode(3)
column.pack_start(icon, False)

View File

@ -65,19 +65,20 @@ class IOWidget(Gtk.Box):
logger.info(f"Canceling: [{self._action}] of {self._basename} ...")
eve.cancel()
def update_progress(self, current, total, eve=None):
def update_progress(self, current, total, eve = None):
self.progress.set_fraction(current/total)
def finish_callback(self, file, task=None, eve=None):
def finish_callback(self, file, task = None, eve = None):
if task.had_error():
logger.info(f"{self._action} of {self._basename} cancelled/failed...")
return
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):
def delete_self(self, widget = None, eve = None):
self.get_parent().remove(self)

View File

@ -61,7 +61,6 @@ class MessagePopupWidget(Gtk.Popover):
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)
@ -103,7 +102,7 @@ class MessagePopupWidget(Gtk.Popover):
self.popup()
self.hide_message_timeout(seconds)
@threaded
@daemon_threaded
def hide_message_timeout(self, seconds=3):
time.sleep(seconds)
GLib.idle_add(event_system.emit, ("hide_messages_popup"))

View File

@ -29,16 +29,16 @@ class PathMenuPopupWidget(Gtk.Popover):
self.set_relative_to(path_entry)
self.set_modal(False)
self.set_position(Gtk.PositionType.BOTTOM)
self.set_size_request(240, 420)
self.set_size_request(480, 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()
path_menu_buttons = Gtk.Box()
scroll_window.set_vexpand(True)
scroll_window.set_hexpand(True)
@ -47,12 +47,13 @@ class PathMenuPopupWidget(Gtk.Popover):
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)
scroll_window.show_all()
def show_path_menu(self, widget=None, eve=None):
def show_path_menu(self, widget = None, eve = None):
self.popup()
def hide_path_menu(self, widget=None, eve=None):
def hide_path_menu(self, widget = None, eve = None):
self.popdown()

View File

@ -9,14 +9,12 @@ from gi.repository import Gtk
class TabHeaderWidget(Gtk.Box):
"""docstring for TabHeaderWidget"""
def __init__(self, tab, close_tab):
def __init__(self, close_tab):
super(TabHeaderWidget, self).__init__()
self._tab = tab
self._close_tab = close_tab # NOTE: Close method in tab_mixin
self._setup_styling()
@ -32,25 +30,19 @@ class TabHeaderWidget(Gtk.Box):
...
def _load_widgets(self):
label = Gtk.Label()
tid = Gtk.Label()
self.label = 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()}")
self.label.set_xalign(0.0)
self.label.set_margin_left(25)
self.label.set_margin_right(25)
self.label.set_hexpand(True)
close.connect("released", self._close_tab)
close.add(icon)
self.add(label)
self.add(self.label)
self.add(close)
self.add(tid)
self.show_all()
tid.hide()

View File

@ -1,5 +1,4 @@
# Python imports
import time
import signal
# Lib imports
@ -10,6 +9,7 @@ gi.require_version('Gdk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import GObject
# Application imports
from core.controller import Controller
@ -24,18 +24,20 @@ class Window(Gtk.ApplicationWindow):
"""docstring for Window."""
def __init__(self, args, unknownargs):
super(Window, self).__init__()
GObject.threads_init()
self._controller = None
super(Window, self).__init__()
settings_manager.set_main_window(self)
self._set_window_data()
self._controller = None
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets(args, unknownargs)
self._set_window_data()
self._set_size_constraints()
self.show()
@ -66,6 +68,18 @@ class Window(Gtk.ApplicationWindow):
self.add( self._controller.get_core_widget() )
def _set_size_constraints(self):
_window_x = settings.config.main_window_x
_window_y = settings.config.main_window_y
_min_width = settings.config.main_window_min_width
_min_height = settings.config.main_window_min_height
_width = settings.config.main_window_width
_height = settings.config.main_window_height
self.move(_window_x, _window_y - 28)
self.set_size_request(_min_width, _min_height)
self.set_default_size(_width, _height)
def _set_window_data(self) -> None:
screen = self.get_screen()
visual = screen.get_rgba_visual()
@ -73,7 +87,7 @@ class Window(Gtk.ApplicationWindow):
if visual != None and screen.is_composited():
self.set_visual(visual)
self.set_app_paintable(True)
self.connect("draw", self._area_draw)
# self.connect("draw", self._area_draw)
# bind css file
cssProvider = Gtk.CssProvider()
@ -94,5 +108,18 @@ class Window(Gtk.ApplicationWindow):
def _tear_down(self, widget = None, eve = None):
event_system.emit("shutting_down")
size = self.get_size()
pos = self.get_position()
settings_manager.set_main_window_width(size.width)
settings_manager.set_main_window_height(size.height)
settings_manager.set_main_window_x(pos.root_x)
settings_manager.set_main_window_y(pos.root_y)
settings_manager.save_settings()
settings_manager.clear_pid()
Gtk.main_quit()
def main(self):
Gtk.main()

View File

@ -15,7 +15,7 @@ class ManifestProcessorException(Exception):
...
@dataclass(slots=True)
@dataclass(slots = True)
class PluginInfo:
path: str = None
name: str = None
@ -24,23 +24,28 @@ class PluginInfo:
support: str = None
requests:{} = None
reference: type = None
pre_launch: bool = False
class ManifestProcessor:
def __init__(self, path, builder):
manifest = join(path, "manifest.json")
if not os.path.exists(manifest):
manifest_pth = join(path, "manifest.json")
if not os.path.exists(manifest_pth):
raise ManifestProcessorException("Invalid Plugin Structure: Plugin doesn't have 'manifest.json'. Aboarting load...")
self._path = path
self._builder = builder
with open(manifest) as f:
with open(manifest_pth) as f:
data = json.load(f)
self._manifest = data["manifest"]
self._plugin = self.collect_info()
def is_pre_launch(self) -> bool:
return self._plugin.pre_launch
def collect_info(self) -> PluginInfo:
plugin = PluginInfo()
plugin.path = self._path
plugin.name = self._manifest["name"]
plugin.author = self._manifest["author"]
@ -48,14 +53,16 @@ class ManifestProcessor:
plugin.support = self._manifest["support"]
plugin.requests = self._manifest["requests"]
if "pre_launch" in self._manifest.keys():
plugin.pre_launch = True if self._manifest["pre_launch"] == "true" else False
return plugin
def get_loading_data(self):
loading_data = {}
requests = self._plugin.requests
keys = requests.keys()
if "ui_target" in keys:
if "ui_target" in requests:
if requests["ui_target"] in [
"none", "other", "main_Window", "main_menu_bar",
"main_menu_bttn_box_bar", "path_menu_bar", "plugin_control_list",
@ -63,7 +70,7 @@ class ManifestProcessor:
"window_2", "window_3", "window_4"
]:
if requests["ui_target"] == "other":
if "ui_target_id" in keys:
if "ui_target_id" in requests:
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"...')
@ -74,11 +81,11 @@ class ManifestProcessor:
else:
raise ManifestProcessorException('Unknown "ui_target" given in requests.')
if "pass_fm_events" in keys:
if "pass_fm_events" in requests:
if requests["pass_fm_events"] in ["true"]:
loading_data["pass_fm_events"] = True
if "pass_ui_objects" in keys:
if "pass_ui_objects" in requests:
if len(requests["pass_ui_objects"]) > 0:
loading_data["pass_ui_objects"] = []
for ui_id in requests["pass_ui_objects"]:
@ -87,7 +94,7 @@ class ManifestProcessor:
except ManifestProcessorException as e:
logger.error(repr(e))
if "bind_keys" in keys:
if "bind_keys" in requests:
if isinstance(requests["bind_keys"], list):
loading_data["bind_keys"] = requests["bind_keys"]

View File

@ -36,41 +36,76 @@ class PluginsController:
self._plugins_dir_watcher = None
self._plugin_collection = []
self._plugin_manifests = {}
self._load_manifests()
def launch_plugins(self) -> None:
def _load_manifests(self):
logger.info(f"Loading manifests...")
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)]:
manifest = ManifestProcessor(path, self._builder)
self._plugin_manifests[path] = {
"path": path,
"folder": folder,
"manifest": manifest
}
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):
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...")
def pre_launch_plugins(self) -> None:
logger.info(f"Loading pre-launch plugins...")
plugin_manifests: {} = {}
for key in self._plugin_manifests:
target_manifest = self._plugin_manifests[key]["manifest"]
if target_manifest.is_pre_launch():
plugin_manifests[key] = self._plugin_manifests[key]
self._load_plugins(plugin_manifests, is_pre_launch = True)
def post_launch_plugins(self) -> None:
logger.info(f"Loading post-launch plugins...")
plugin_manifests: {} = {}
for key in self._plugin_manifests:
target_manifest = self._plugin_manifests[key]["manifest"]
if not target_manifest.is_pre_launch():
plugin_manifests[key] = self._plugin_manifests[key]
self._load_plugins(plugin_manifests)
def _load_plugins(self, plugin_manifests: {} = {}, is_pre_launch: bool = False) -> None:
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)]:
for key in plugin_manifests:
target_manifest = plugin_manifests[key]
path, folder, manifest = target_manifest["path"], target_manifest["folder"], target_manifest["manifest"]
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)
if is_pre_launch:
self.execute_plugin(module, plugin, loading_data)
else:
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())
@ -100,25 +135,26 @@ class PluginsController:
def execute_plugin(self, module: type, plugin: PluginInfo, loading_data: []):
plugin.reference = module.Plugin()
keys = loading_data.keys()
if "ui_target" in keys:
if "ui_target" in loading_data:
loading_data["ui_target"].add( plugin.reference.generate_reference_ui_element() )
loading_data["ui_target"].show_all()
if "pass_ui_objects" in keys:
if "pass_ui_objects" in loading_data:
plugin.reference.set_ui_object_collection( loading_data["pass_ui_objects"] )
if "pass_fm_events" in keys:
if "pass_fm_events" in loading_data:
plugin.reference.set_fm_event_system(event_system)
plugin.reference.subscribe_to_events()
if "bind_keys" in keys:
if "bind_keys" in loading_data:
keybindings.append_bindings( loading_data["bind_keys"] )
plugin.reference.run()
self._plugin_collection.append(plugin)
return False
def reload_plugins(self, file: str = None) -> None:
logger.info(f"Reloading plugins...")
parent_path = os.getcwd()

View File

@ -14,7 +14,6 @@ from random import randint
from .utils.settings import Settings
from .utils.launcher import Launcher
from .utils.filehandler import FileHandler
from .icons.icon import Icon
from .path import Path
@ -40,9 +39,8 @@ except Exception as e:
class Tab(Settings, FileHandler, Launcher, Icon, Path):
class Tab(Settings, FileHandler, Launcher, Path):
def __init__(self):
self.logger = None
self._id_length: int = 10
self._id: str = ""
@ -168,33 +166,6 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
}
}
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()
@ -264,7 +235,7 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
return int(text) if text.isdigit() else text
def _natural_keys(self, text):
return [ self._atoi(c) for c in re.split('(\d+)',text) ]
return [ self._atoi(c) for c in re.split(r'(\d+)', text) ]
def _hash_text(self, text) -> str:
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
@ -289,3 +260,7 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
def _set_error_message(self, text: str):
self.error_message = text
def create_icon(self, dir, file):
return event_system.emit_and_await("create-thumbnail", (dir, file,))

View File

@ -41,11 +41,11 @@ class Launcher:
def execute(self, command, start_dir=os.getenv("HOME"), use_shell=False):
try:
self.logger.debug(command)
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)
logger.error(f"Couldn't execute: {command}")
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):
@ -53,8 +53,8 @@ class Launcher:
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)
logger.error(f"Couldn't execute and return thread: {command}")
logger.error(e)
return None
@threaded
@ -63,7 +63,7 @@ class Launcher:
def remux_video(self, hash, file):
remux_vid_pth = "{self.REMUX_FOLDER}/{hash}.mp4"
self.logger.debug(remux_vid_pth)
logger.debug(remux_vid_pth)
if not os.path.isfile(remux_vid_pth):
self.check_remux_space()
@ -83,8 +83,8 @@ class Launcher:
proc = subprocess.Popen(command)
proc.wait()
except ShellFMLauncherException as e:
self.logger.error(message)
self.logger.error(e)
logger.error(message)
logger.error(e)
return False
return True
@ -94,7 +94,7 @@ class Launcher:
try:
limit = int(limit)
except ShellFMLauncherException as e:
self.logger.debug(e)
logger.debug(e)
return
usage = self.get_remux_folder_usage(self.REMUX_FOLDER)

View File

@ -14,8 +14,6 @@ class ShellFMSettingsException(Exception):
class Settings:
logger = None
# NOTE: app_name should be defined using python 'builtins'
app_name_exists = False
try:
@ -31,45 +29,13 @@ class Settings:
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
@ -83,9 +49,6 @@ class Settings:
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"]

View File

@ -18,7 +18,7 @@ def debug_signal_handler(signal, frame):
rpdb2.start_embedded_debugger("foobar", True, True)
rpdb2.setbreak(depth=1)
return
except StandardError:
except Exception:
...
try:
@ -26,7 +26,7 @@ def debug_signal_handler(signal, frame):
logger.debug("\n\nStarting embedded rconsole debugger...\n\n")
rconsole.spawn_server()
return
except StandardError as ex:
except Exception as ex:
...
try:
@ -34,7 +34,15 @@ def debug_signal_handler(signal, frame):
logger.debug("\n\nStarting PuDB debugger...\n\n")
set_trace(paused = True)
return
except StandardError as ex:
except Exception as ex:
...
try:
import ipdb
logger.debug("\n\nStarting IPDB debugger...\n\n")
ipdb.set_trace()
return
except Exception as ex:
...
try:
@ -42,11 +50,11 @@ def debug_signal_handler(signal, frame):
logger.debug("\n\nStarting embedded PDB debugger...\n\n")
pdb.Pdb(skip=['gi.*']).set_trace()
return
except StandardError as ex:
except Exception as ex:
...
try:
import code
code.interact()
except StandardError as ex:
except Exception as ex:
logger.debug(f"{ex}, returning to normal program flow...")

View File

@ -51,15 +51,20 @@ class IPCServer(Singleton):
listener = Listener((self._ipc_address, self._ipc_port))
self.is_ipc_alive = True
self._run_ipc_loop(listener)
# self._run_ipc_loop(listener)
GLib.Thread("", self._run_ipc_loop, listener)
@daemon_threaded
# @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,))
conn = None
start_time = None
except Exception as e:
logger.debug( repr(e) )
@ -73,22 +78,29 @@ class IPCServer(Singleton):
if "FILE|" in msg:
file = msg.split("FILE|")[1].strip()
if file:
event_system.emit("handle_file_from_ipc", file)
event_system.emit_and_await("handle_file_from_ipc", file)
msg = None
file = None
conn.close()
break
if msg in ['close connection', 'close server']:
msg = None
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:
msg = None
end_time = None
conn.close()
break
return False
def send_ipc_message(self, message: str = "Empty Data...") -> None:
try:

View File

@ -146,6 +146,13 @@ class SettingsManager(StartCheckMixin, Singleton):
def is_trace_debug(self) -> bool: return self._trace_debug
def is_debug(self) -> bool: return self._debug
def set_main_window_x(self, x = 0): self.settings.config.main_window_x = x
def set_main_window_y(self, y = 0): self.settings.config.main_window_y = y
def set_main_window_width(self, width = 800): self.settings.config.main_window_width = width
def set_main_window_height(self, height = 600): self.settings.config.main_window_height = height
def set_main_window_min_width(self, width = 720): self.settings.config.main_window_min_width = width
def set_main_window_min_height(self, height = 480): self.settings.config.main_window_min_height = height
def set_trace_debug(self, trace_debug: bool):
self._trace_debug = trace_debug

View File

@ -30,6 +30,13 @@ class Config:
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"
make_transparent: int = 0
main_window_x: int = 721
main_window_y: int = 465
main_window_min_width: int = 720
main_window_min_height: int = 480
main_window_width: int = 800
main_window_height: int = 600
application_dirs: list = field(default_factory=lambda: [
"/usr/share/applications",
f"{settings_manager.get_home_path()}/.local/share/applications"

View File

@ -11,36 +11,48 @@ import inspect
class StartCheckMixin:
def is_dirty_start(self) -> bool: return self._dirty_start
def clear_pid(self): self._clean_pid()
def is_dirty_start(self) -> bool:
return self._dirty_start
def clear_pid(self):
if not self.is_trace_debug():
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 self.is_trace_debug():
pid = os.getpid()
self._print_pid(pid)
return
if os.path.exists(self._PID_FILE):
with open(self._PID_FILE, "r") as f:
pid = f.readline().strip()
if pid not in ("", None):
self._check_alive_status(int(pid))
else:
if self.is_pid_alive( int(pid) ):
print("PID file exists and PID is alive... Letting downstream errors (sans debug args) handle app closure propigation.")
return
self._write_new_pid()
""" Check For the existence of a unix pid. """
def _check_alive_status(self, pid):
def is_pid_alive(self, pid):
print(f"PID Found: {pid}")
try:
os.kill(pid, 0)
except OSError:
print(f"{app_name} is starting dirty...")
print(f"{app_name} PID file exists but PID is irrelevant; starting dirty...")
self._dirty_start = True
self._write_new_pid()
return
return False
print("PID is alive... Let downstream errors (sans debug args) handle app closure propigation.")
return True
def _write_new_pid(self):
pid = os.getpid()
self._write_pid(pid)
self._print_pid(pid)
def _print_pid(self, pid):
print(f"{app_name} PID: {pid}")
def _clean_pid(self):

View File

@ -20,6 +20,10 @@ function main() {
files[$size]="${target}"
done
python /opt/solarfm.zip "${files[@]}"
export G_SLICE=always-malloc
export G_DEBUG=gc-friendly
export GOBJECT_DEBUG=instance-count
export GSK_RENDERER=cairo
python /opt/solarfm.zip "$@"
}
main "$@";

View File

@ -1,17 +0,0 @@
#!/bin/bash
# . CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
call_path=`pwd`
cd "${call_path}"
echo "Working Dir: " $(pwd)
python /opt/solarfm.zip "$@"
}
main "$@";

View File

@ -2,6 +2,7 @@
"Open Actions": {
"Open": ["STOCK_OPEN", "open"],
"Open With": ["STOCK_OPEN", "open_with"],
"Open 2 Tab": ["STOCK_OPEN", "open_2_new_tab"],
"Execute": ["STOCK_EXECUTE", "execute"],
"Execute in Terminal": ["STOCK_EXECUTE", "execute_in_terminal"]
},

View File

@ -2,8 +2,6 @@
"config": {
"base_of_home": "",
"hide_hidden_files": "true",
"thumbnailer_path": "ffmpegthumbnailer",
"blender_thumbnailer_path": "",
"go_past_home": "true",
"lock_folder": "false",
"locked_folders": "venv::::flasks",
@ -16,12 +14,18 @@
"code_app": "newton",
"text_app": "mousepad",
"terminal_app": "terminator",
"container_icon_wh": [128, 128],
"video_icon_wh": [128, 64],
"sys_icon_wh": [56, 56],
"file_manager_app": "solarfm",
"steam_cdn_url": "https://steamcdn-a.akamaihd.net/steam/apps/",
"remux_folder_max_disk_usage": "8589934592"
"remux_folder_max_disk_usage": "8589934592",
"make_transparent":0,
"main_window_x":721,
"main_window_y":465,
"main_window_min_width":720,
"main_window_min_height":480,
"main_window_width":800,
"main_window_height":600,
"application_dirs":[
"/usr/share/applications"
]
},
"filters": {
"meshs": [".dae", ".fbx", ".gltf", ".obj", ".stl"],

View File

@ -8,7 +8,7 @@
<property name="can-focus">False</property>
<property name="border-width">5</property>
<property name="window-position">center-on-parent</property>
<property name="icon">../icons/solarfm.png</property>
<property name="icon">../icons/solarfm-64x64.png</property>
<property name="type-hint">dialog</property>
<property name="skip-taskbar-hint">True</property>
<property name="skip-pager-hint">True</property>
@ -19,6 +19,7 @@
<property name="copyright" translatable="yes">Copyright (C) 2021 GPL2</property>
<property name="comments" translatable="yes">by ITDominator</property>
<property name="website">https://code.itdominator.com/itdominator/SolarFM</property>
<property name="website-label" translatable="yes">ITDominator</property>
<property name="license" translatable="yes">SolarFM - Copyright (C) 2021 ITDominator GPL2
@ -367,7 +368,9 @@ Public License instead of this License.
SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspection.</property>
<property name="translator-credits" translatable="yes" comments="Please replace this line with your own names, one name per line. ">translator-credits</property>
<property name="documenters">...</property>
<property name="translator-credits" translatable="yes" comments="Please replace this line with your own names, one name per line. ">...</property>
<property name="artists">...</property>
<property name="logo">../icons/solarfm-64x64.png</property>
<property name="wrap-license">True</property>
<property name="license-type">custom</property>