Compare commits

1 Commits

Author SHA1 Message Date
09c5af3821 Gtk4 convert work. 2021-12-31 18:38:22 -06:00
65 changed files with 419 additions and 894 deletions

View File

@@ -14,6 +14,7 @@ sudo apt-get install python3.8 wget python3-setproctitle python3-gi ffmpegthumbn
# TODO
<ul>
<li>Add simpleish plugin system to run bash/python scripts.</li>
<li>Add DnD context awareness for over folder drop.</li>
</ul>
# Images

Binary file not shown.

View File

@@ -1,2 +0,0 @@
### Note
Copy the example and rename it to your desired name. The Main class and passed in arguments are required. You don't necessarily need to use the passed in socket_id or event_system.

View File

@@ -1,37 +0,0 @@
# Python imports
import sys, traceback, threading, inspect, os, time
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Main:
def __init__(self, socket_id, event_system):
self._socket_id = socket_id
self._event_system = event_system
self._gtk_plug = Gtk.Plug.new(self._socket_id)
self.start_loop()
@threaded
def start_loop(self):
i = 0
cycles = 5
alive = True
while alive:
if i == cycles:
alive = False
self._event_system.push_gui_event(["some_type", "display_message", ("warning", str(i), None)])
i += 1
time.sleep(1)

0
src/debs/clear_pycache_dirs.sh Normal file → Executable file
View File

0
src/debs/solarfm-0-0-1-x64/bin/solarfm Normal file → Executable file
View File

View File

@@ -4,12 +4,12 @@ import builtins
# Lib imports
# Application imports
from signal_classes import IPCServerMixin
from signal_classes.DBusControllerMixin import DBusControllerMixin
class Builtins(IPCServerMixin):
class Builtins(DBusControllerMixin):
"""Docstring for __builtins__ extender"""
def __init__(self):
@@ -18,11 +18,6 @@ class Builtins(IPCServerMixin):
self._gui_events = []
self._fm_events = []
self.is_ipc_alive = False
self.ipc_authkey = b'solarfm-ipc'
self.ipc_address = '127.0.0.1'
self.ipc_port = 4848
self.ipc_timeout = 15.0
# Makeshift fake "events" type system FIFO
def _pop_gui_event(self):
@@ -66,8 +61,6 @@ class Builtins(IPCServerMixin):
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
builtins.app_name = "SolarFM"
builtins.event_system = Builtins()
builtins.event_sleep_time = 0.2
builtins.event_sleep_time = 0.5
builtins.debug = False
builtins.trace_debug = False

View File

@@ -13,27 +13,24 @@ from __builtins__ import Builtins
class Main(Builtins):
def __init__(self, args, unknownargs):
if not debug:
event_system.create_ipc_server()
event_system.create_ipc_server()
time.sleep(0.5)
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
time.sleep(0.2)
if not trace_debug:
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
settings = Settings()
settings.create_window()
settings.createWindow()
controller = Controller(args, unknownargs, settings)
if not controller:

View File

@@ -23,7 +23,7 @@ if __name__ == "__main__":
# import web_pdb
# web_pdb.set_trace()
setproctitle('SolarFM')
setproctitle('solarfm')
faulthandler.enable() # For better debug info
parser = argparse.ArgumentParser()
# Add long and short arguments

View File

@@ -24,9 +24,7 @@ class WindowController:
self.active_window_id = ""
self.active_tab_id = ""
self.windows = []
if not trace_debug:
self.fm_event_observer()
self.fm_event_observer()
@threaded
def fm_event_observer(self):

View File

@@ -11,7 +11,7 @@ class Path:
return os.path.expanduser("~") + self.subpath
def get_path(self):
return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}"
return "/" + "/".join(self.path)
def get_path_list(self):
return self.path
@@ -21,7 +21,7 @@ class Path:
self.load_directory()
def pop_from_path(self):
try:
if len(self.path) > 1:
self.path.pop()
if not self.go_past_home:
@@ -29,8 +29,6 @@ class Path:
self.set_to_home()
self.load_directory()
except Exception as e:
pass
def set_path(self, path):
if path == self.get_path():

View File

@@ -199,7 +199,7 @@ class View(Settings, FileHandler, Launcher, Icon, Path):
def get_current_sub_path(self):
path = self.get_path()
home = f"{self.get_home()}/"
home = self.get_home() + "/"
return path.replace(home, "")
def get_end_of_path(self):

View File

@@ -17,7 +17,7 @@ def threaded(fn):
class Icon(DesktopIconMixin, VideoIconMixin):
def create_icon(self, dir, file):
full_path = f"{dir}/{file}"
full_path = dir + "/" + file
return self.get_icon_image(dir, file, full_path)
def get_icon_image(self, dir, file, full_path):
@@ -36,32 +36,29 @@ class Icon(DesktopIconMixin, VideoIconMixin):
return None
def create_thumbnail(self, dir, file):
full_path = f"{dir}/{file}"
full_path = dir + "/" + file
try:
file_hash = hashlib.sha256(str.encode(full_path)).hexdigest()
hash_img_pth = f"{self.ABS_THUMBS_PTH}/{file_hash}.jpg"
hash_img_pth = self.ABS_THUMBS_PTH + "/" + file_hash + ".jpg"
if isfile(hash_img_pth) == False:
self.generate_video_thumbnail(full_path, hash_img_pth)
thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH)
if thumbnl == None: # If no icon whatsoever, return internal default
thumbnl = GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png")
thumbnl = GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICONS + "/video.png")
return thumbnl
except Exception as e:
print("Thumbnail generation issue:")
print( repr(e) )
return GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png")
return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICONS + "/video.png")
def create_scaled_image(self, path, wxh):
try:
if path.lower().endswith(".gif"):
return GdkPixbuf.PixbufAnimation.new_from_file(path) \
.get_static_image() \
.scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR)
else:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], True)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
scaled_pixbuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default
return scaled_pixbuf
except Exception as e:
print("Image Scaling Issue:")
print( repr(e) )

View File

@@ -59,7 +59,7 @@ class Settings:
subpath = settings["base_of_home"]
HIDE_HIDDEN_FILES = True if settings["hide_hidden_files"] == "true" else False
FFMPG_THUMBNLR = FFMPG_THUMBNLR if settings["thumbnailer_path"] == "" else settings["thumbnailer_path"]
go_past_home = True if settings["go_past_home"] == "" else settings["go_past_home"]
go_past_home = True if settings["go_past_home"] == "true" else False
lock_folder = True if settings["lock_folder"] == "true" else False
locked_folders = settings["locked_folders"].split("::::")
mplayer_options = settings["mplayer_options"].split()

View File

@@ -1,5 +1,5 @@
# Python imports
import sys, traceback, threading, inspect, os, time
import sys, traceback, threading, signal, inspect, os, time
# Lib imports
import gi
@@ -7,9 +7,8 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
# Application imports
from .mixins.ui import *
from .mixins import ShowHideMixin, KeyboardSignalsMixin
from . import Controller_Data
from .mixins import *
from . import ShowHideMixin, KeyboardSignalsMixin, Controller_Data
def threaded(fn):
@@ -24,23 +23,25 @@ class Controller(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin, \
KeyboardSignalsMixin, Controller_Data):
def __init__(self, args, unknownargs, _settings):
# sys.excepthook = self.custom_except_hook
self.setup_controller_data(_settings)
self.settings = _settings
self.setup_controller_data()
self.window.show()
self.generate_windows(self.state)
self.plugins.launch_plugins()
if not trace_debug:
self.gui_event_observer()
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)
self.gui_event_observer()
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
def tear_down(self, widget=None, eve=None):

View File

@@ -1,5 +1,4 @@
# Python imports
import signal
# Lib imports
from gi.repository import GLib
@@ -7,7 +6,6 @@ from gi.repository import GLib
# Application imports
from shellfm import WindowController
from trasher.xdgtrash import XDGTrash
from . import Plugins
@@ -16,18 +14,16 @@ class Controller_Data:
def has_method(self, o, name):
return callable(getattr(o, name, None))
def setup_controller_data(self, _settings):
self.trashman = XDGTrash()
def setup_controller_data(self):
self.window_controller = WindowController()
self.plugins = Plugins(_settings)
self.state = self.window_controller.load_state()
self.trashman = XDGTrash()
self.trashman.regenerate()
self.settings = _settings
self.builder = self.settings.get_builder()
self.logger = self.settings.get_logger()
self.state = self.window_controller.load_state()
self.builder = self.settings.builder
self.logger = self.settings.logger
self.window = self.settings.get_main_window()
self.window = self.settings.getMainWindow()
self.window1 = self.builder.get_object("window_1")
self.window2 = self.builder.get_object("window_2")
self.window3 = self.builder.get_object("window_3")
@@ -89,10 +85,10 @@ class Controller_Data:
self.is_pane3_hidden = False
self.is_pane4_hidden = False
self.override_drop_dest = None
self.is_searching = False
self.search_iconview = None
self.search_view = None
self.is_searching = False
self.search_iconview = None
self.search_view = None
self.skip_edit = False
self.cancel_edit = False
@@ -103,7 +99,3 @@ class Controller_Data:
self.success = "#88cc27"
self.warning = "#ffa800"
self.error = "#ff0000"
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)

View File

@@ -15,11 +15,11 @@ def threaded(fn):
class IPCServerMixin:
class DBusControllerMixin:
@threaded
def create_ipc_server(self):
listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
listener = Listener(('127.0.0.1', 4848), authkey=b'solarfm-ipc')
self.is_ipc_alive = True
while True:
conn = listener.accept()
@@ -49,7 +49,7 @@ class IPCServerMixin:
# NOTE: Not perfect but insures we don't lockup the connection for too long.
end_time = time.time()
if (end - start) > self.ipc_timeout:
if (end - start) > 15.0:
conn.close()
listener.close()
@@ -57,7 +57,7 @@ class IPCServerMixin:
def send_ipc_message(self, message="Empty Data..."):
try:
conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
conn = Client(('127.0.0.1', 4848), authkey=b'solarfm-ipc')
conn.send(message)
conn.send('close connection')
except Exception as e:

View File

@@ -1,41 +0,0 @@
# Python imports
import importlib
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio
# Application imports
class Plugins:
"""docstring for Plugins"""
def __init__(self, settings):
self._settings = settings
self._plugins_path = self._settings.get_plugins_path()
self._plugins_dir_watcher = None
self._socket = Gtk.Socket().new()
def launch_plugins(self):
self._set_plugins_watcher()
self.load_plugins()
def _set_plugins_watcher(self):
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.load_plugins(file)
def load_plugins(self, file=None):
print(f"(Re)loading plugins...")
print(locals())
# importlib.reload(stl_utils)

View File

@@ -2,7 +2,8 @@
Gtk Bound Signal Module
"""
from .mixins import *
from .IPCServerMixin import IPCServerMixin
from .Plugins import Plugins
from .DBusControllerMixin import DBusControllerMixin
from .KeyboardSignalsMixin import KeyboardSignalsMixin
from .ShowHideMixin import ShowHideMixin
from .Controller_Data import Controller_Data
from .Controller import Controller

View File

@@ -107,7 +107,7 @@ class WidgetFileActionMixin:
def open_with_files(self, appchooser_widget):
wid, tid, view, iconview, store = self.get_current_state()
app_info = appchooser_widget.get_app_info()
uris = self.format_to_uris(store, wid, tid, self.selected_files)
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
view.app_chooser_exec(app_info, uris)

View File

@@ -1,5 +1,5 @@
# Python imports
import os, threading, subprocess, time
import os, threading, subprocess
# Lib imports
import gi
@@ -20,13 +20,15 @@ def threaded(fn):
class WidgetMixin:
def load_store(self, view, store, save_state=False):
store.clear()
dir = view.get_current_directory()
files = view.get_files()
icon = GdkPixbuf.Pixbuf.new_from_file(view.DEFAULT_ICON)
for i, file in enumerate(files):
store.append([None, file[0]])
store.append([icon, file[0]])
self.create_icon(i, view, store, dir, file[0])
# NOTE: Not likely called often from here but it could be useful
@@ -48,14 +50,10 @@ class WidgetMixin:
try:
itr = store.get_iter(i)
except Exception as e:
try:
time.sleep(0.2)
itr = store.get_iter(i)
except Exception as e:
print(":Invalid Itr detected: (Potential race condition...)")
print(f"Index Requested: {i}")
print(f"Store Size: {len(store)}")
return
print(":Invalid Itr detected: (Potential race condition...)")
print(f"Index Requested: {i}")
print(f"Store Size: {len(store)}")
return
if not icon:
icon = self.get_system_thumbnail(fpath, view.SYS_ICON_WH[0])
@@ -115,7 +113,7 @@ class WidgetMixin:
def create_grid_iconview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.IconView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
grid.set_model(store)
grid.set_pixbuf_column(0)
@@ -133,14 +131,11 @@ class WidgetMixin:
grid.connect("button_release_event", self.grid_icon_single_click)
grid.connect("item-activated", self.grid_icon_double_click)
# grid.connect("toggle-cursor-item", self.grid_cursor_toggled)
# grid.connect("notify", self.grid_cursor_toggled)
grid.connect("selection-changed", self.grid_set_selected_items)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
targets = [ uri_target ]
@@ -159,8 +154,8 @@ class WidgetMixin:
def create_grid_treeview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.TreeView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
# store = Gtk.TreeStore(GdkPixbuf.Pixbuf or None, str)
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
# store = Gtk.TreeStore(GdkPixbuf.Pixbuf, str)
column = Gtk.TreeViewColumn("Icons")
icon = Gtk.CellRendererPixbuf()
name = Gtk.CellRendererText()

View File

@@ -82,7 +82,7 @@ class WindowMixin(TabMixin):
_wid, _tid, _view, iconview, store = self.get_current_state()
selected_files = iconview.get_selected_items()
current_directory = view.get_current_directory()
path_file = Gio.File.new_for_path(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 = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
formatted_mount_size = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::size")) )
@@ -101,16 +101,11 @@ class WindowMixin(TabMixin):
uris = self.format_to_uris(store, _wid, _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 Exception as e:
if debug:
print(repr(e))
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
formatted_size = self.sizeof_fmt(combined_size)
if view.hide_hidden:
@@ -157,9 +152,6 @@ class WindowMixin(TabMixin):
def grid_set_selected_items(self, iconview):
self.selected_files = iconview.get_selected_items()
def grid_cursor_toggled(self, iconview):
print("wat...")
def grid_icon_single_click(self, iconview, eve):
try:
self.path_menu.popdown()
@@ -221,19 +213,8 @@ class WindowMixin(TabMixin):
data.set_text(uris_text, -1)
def grid_on_drag_motion(self, iconview, drag_context, x, y, data):
current = '|'.join(self.window_controller.get_active_data())
target = iconview.get_name()
wid, tid = target.split("|")
store = iconview.get_model()
treePath = iconview.get_drag_dest_item().path
if treePath:
uri = self.format_to_uris(store, wid, tid, treePath)[0].replace("file://", "")
self.override_drop_dest = uri if isdir(uri) else None
if target not in current:
self.window_controller.set_active_data(wid, tid)
wid, tid = iconview.get_name().split("|")
self.window_controller.set_active_data(wid, tid)
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
@@ -243,14 +224,12 @@ class WindowMixin(TabMixin):
view = self.get_fm_window(wid).get_view_by_id(tid)
uris = data.get_uris()
dest = f"{view.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:
dest = f"{view.get_current_directory()}"
if len(uris) > 0:
self.move_files(uris, dest)
else:
uris = data.get_text().split("\n")
self.move_files(uris, dest)
def create_new_view_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, path)

View File

@@ -1,2 +1,5 @@
from .KeyboardSignalsMixin import KeyboardSignalsMixin
from .ShowHideMixin import ShowHideMixin
from .PaneMixin import PaneMixin
from .WidgetMixin import WidgetMixin
from .TabMixin import TabMixin
from .WindowMixin import WindowMixin
from .WidgetFileActionMixin import WidgetFileActionMixin

View File

View File

View File

View File

@@ -5,8 +5,8 @@ import os, logging
class Logger:
def __init__(self, config_path):
self._CONFIG_PATH = config_path
def __init__(self):
pass
def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True):
"""
@@ -42,8 +42,8 @@ class Logger:
log.addHandler(ch)
if createFile:
folder = self._CONFIG_PATH
file = f"{folder}/application.log"
folder = "logs"
file = folder + "/application.log"
if not os.path.exists(folder):
os.mkdir(folder)

View File

@@ -17,45 +17,39 @@ from . import Logger
class Settings:
def __init__(self):
self.logger = Logger().get_logger()
self.builder = gtk.Builder()
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__))
self.USER_HOME = path.expanduser('~')
self.CONFIG_PATH = f"{self.USER_HOME}/.config/{app_name.lower()}"
self.PLUGINS_PATH = f"{self.CONFIG_PATH}/plugins"
self.USR_SOLARFM = f"/usr/share/{app_name.lower()}"
self.CONFIG_PATH = f"{self.USER_HOME}/.config/solarfm"
self.USR_SOLARFM = "/usr/share/solarfm"
self.cssFile = f"{self.CONFIG_PATH}/stylesheet.css"
self.windows_glade = f"{self.CONFIG_PATH}/Main_Window.glade"
self.DEFAULT_ICONS = f"{self.CONFIG_PATH}/icons"
self.window_icon = f"{self.DEFAULT_ICONS}/{app_name.lower()}.png"
self.window_icon = f"{self.DEFAULT_ICONS}/solarfm.png"
self.main_window = None
if not os.path.exists(self.CONFIG_PATH):
os.mkdir(self.CONFIG_PATH)
if not os.path.exists(self.PLUGINS_PATH):
os.mkdir(self.PLUGINS_PATH)
if not os.path.exists(self.windows_glade):
self.windows_glade = f"{self.USR_SOLARFM}/Main_Window.glade"
if not os.path.exists(self.cssFile):
self.cssFile = f"{self.USR_SOLARFM}/stylesheet.css"
if not os.path.exists(self.window_icon):
self.window_icon = f"{self.USR_SOLARFM}/icons/{app_name.lower()}.png"
self.window_icon = f"{self.USR_SOLARFM}/icons/solarfm.png"
if not os.path.exists(self.DEFAULT_ICONS):
self.DEFAULT_ICONS = f"{self.USR_SOLARFM}/icons"
self.logger = Logger(self.CONFIG_PATH).get_logger()
self.builder.add_from_file(self.windows_glade)
def create_window(self):
def createWindow(self):
# Get window and connect signals
self.main_window = self.builder.get_object("Main_Window")
self._set_window_data()
self.setWindowData()
def _set_window_data(self):
def setWindowData(self):
self.main_window.set_icon_from_file(self.window_icon)
screen = self.main_window.get_screen()
visual = screen.get_rgba_visual()
@@ -63,7 +57,7 @@ class Settings:
if visual != None and screen.is_composited():
self.main_window.set_visual(visual)
self.main_window.set_app_paintable(True)
self.main_window.connect("draw", self._area_draw)
self.main_window.connect("draw", self.area_draw)
# bind css file
cssProvider = gtk.CssProvider()
@@ -72,13 +66,16 @@ class Settings:
styleContext = gtk.StyleContext()
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
def _area_draw(self, widget, cr):
def area_draw(self, widget, cr):
cr.set_source_rgba(0, 0, 0, 0.54)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def get_monitor_data(self):
def getMainWindow(self): return self.main_window
def getMonitorData(self):
screen = self.builder.get_object("Main_Window").get_screen()
monitors = []
for m in range(screen.get_n_monitors()):
@@ -88,8 +85,3 @@ class Settings:
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
return monitors
def get_builder(self): return self.builder
def get_logger(self): return self.logger
def get_main_window(self): return self.main_window
def get_plugins_path(self): return self.PLUGINS_PATH

View File

@@ -4,28 +4,20 @@ import builtins
# Lib imports
# Application imports
from controller import IPCServerMixin
from signal_classes.DBusControllerMixin import DBusControllerMixin
class Builtins(IPCServerMixin):
class Builtins(DBusControllerMixin):
"""Docstring for __builtins__ extender"""
def __init__(self):
# NOTE: The format used is list of [type, target, data] Where:
# type is useful context for control flow,
# target is the method to call,
# data is the method parameters to give
# NOTE: The format used is list of [type, target, data]
# Where data may be any kind of data
self._gui_events = []
self._fm_events = []
self.is_ipc_alive = False
self.ipc_authkey = b'solarfm-ipc'
self.ipc_address = '127.0.0.1'
self.ipc_port = 4848
self.ipc_timeout = 15.0
# Makeshift fake "events" type system FIFO
def _pop_gui_event(self):
@@ -69,8 +61,6 @@ class Builtins(IPCServerMixin):
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
builtins.app_name = "SolarFM"
builtins.event_system = Builtins()
builtins.event_sleep_time = 0.2
builtins.event_sleep_time = 0.5
builtins.debug = False
builtins.trace_debug = False

View File

@@ -5,7 +5,7 @@ import os, inspect, time
# Application imports
from utils import Settings
from controller import Controller
from signal_classes import Controller
from __builtins__ import Builtins
@@ -13,27 +13,24 @@ from __builtins__ import Builtins
class Main(Builtins):
def __init__(self, args, unknownargs):
if not debug:
event_system.create_ipc_server()
event_system.create_ipc_server()
time.sleep(0.5)
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
time.sleep(0.2)
if not trace_debug:
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
settings = Settings()
settings.create_window()
settings.createWindow()
controller = Controller(args, unknownargs, settings)
if not controller:

View File

@@ -11,7 +11,7 @@ tracemalloc.start()
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk
# Application imports
@@ -23,7 +23,7 @@ if __name__ == "__main__":
# import web_pdb
# web_pdb.set_trace()
setproctitle('SolarFM')
setproctitle('solarfm')
faulthandler.enable() # For better debug info
parser = argparse.ArgumentParser()
# Add long and short arguments

View File

@@ -1,7 +0,0 @@
"""
Gtk Bound Signal Module
"""
from .mixins import *
from .IPCServerMixin import IPCServerMixin
from .Controller_Data import Controller_Data
from .Controller import Controller

View File

@@ -1,11 +0,0 @@
# Python imports
# Gtk imports
# Application imports
from . import ShowHideMixin
from .ui import *
class UIMixin(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin):
pass

View File

@@ -1,2 +0,0 @@
from .ShowHideMixin import ShowHideMixin
from .UIMixin import UIMixin

View File

@@ -1,5 +0,0 @@
from .PaneMixin import PaneMixin
from .WidgetMixin import WidgetMixin
from .TabMixin import TabMixin
from .WindowMixin import WindowMixin
from .WidgetFileActionMixin import WidgetFileActionMixin

View File

@@ -1,27 +0,0 @@
# Python imports
# Lib imports
# Application imports
class IPCSignalsMixin:
def print_to_console(self, message=None):
print(self)
print(message)
def handle_file_from_ipc(self, path):
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
if notebook.is_visible():
self.create_tab(wid, path)
return
if not self.is_pane4_hidden:
self.create_tab(4, path)
elif not self.is_pane3_hidden:
self.create_tab(3, path)
elif not self.is_pane2_hidden:
self.create_tab(2, path)
elif not self.is_pane1_hidden:
self.create_tab(1, path)

View File

@@ -1,2 +0,0 @@
from .KeyboardSignalsMixin import KeyboardSignalsMixin
from .IPCSignalsMixin import IPCSignalsMixin

View File

@@ -1,63 +0,0 @@
# Python imports
import os, importlib
from os.path import join, isdir
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio
# Application imports
class Plugins:
"""docstring for Plugins"""
def __init__(self, settings):
self._settings = settings
self._plugins_path = self._settings.get_plugins_path()
self.gtk_socket = Gtk.Socket().new()
self._plugins_dir_watcher = None
self.gtk_socket_id = None
self._plugin_collection = []
self._settings.get_main_window().add(self.gtk_socket)
self.gtk_socket.show()
self.gtk_socket_id = self.gtk_socket.get_id()
def launch_plugins(self):
self._set_plugins_watcher()
self.load_plugins()
def _set_plugins_watcher(self):
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)
def load_plugins(self, file=None):
print(f"Loading plugins...")
for file in os.listdir(self._plugins_path):
path = join(self._plugins_path, file)
if isdir(path):
spec = importlib.util.spec_from_file_location(file, join(path, "__main__.py"))
module = importlib.util.module_from_spec(spec)
self._plugin_collection.append([file, module])
spec.loader.exec_module(module)
module.Main(self.gtk_socket_id, event_system)
def reload_plugins(self, file=None):
print(f"Reloading plugins...")
# if self._plugin_collection:
# to_unload = []
# for dir in self._plugin_collection:
# if not os.path.isdir(os.path.join(self._plugins_path, dir)):
# to_unload.append(dir)

View File

@@ -1,4 +0,0 @@
"""
Gtk Bound Plugins Module
"""
from .Plugins import Plugins

View File

@@ -24,9 +24,7 @@ class WindowController:
self.active_window_id = ""
self.active_tab_id = ""
self.windows = []
if not trace_debug:
self.fm_event_observer()
self.fm_event_observer()
@threaded
def fm_event_observer(self):

View File

@@ -11,7 +11,7 @@ class Path:
return os.path.expanduser("~") + self.subpath
def get_path(self):
return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}"
return "/" + "/".join(self.path)
def get_path_list(self):
return self.path
@@ -21,7 +21,7 @@ class Path:
self.load_directory()
def pop_from_path(self):
try:
if len(self.path) > 1:
self.path.pop()
if not self.go_past_home:
@@ -29,8 +29,6 @@ class Path:
self.set_to_home()
self.load_directory()
except Exception as e:
pass
def set_path(self, path):
if path == self.get_path():

View File

@@ -199,7 +199,7 @@ class View(Settings, FileHandler, Launcher, Icon, Path):
def get_current_sub_path(self):
path = self.get_path()
home = f"{self.get_home()}/"
home = self.get_home() + "/"
return path.replace(home, "")
def get_end_of_path(self):

View File

@@ -17,7 +17,7 @@ def threaded(fn):
class Icon(DesktopIconMixin, VideoIconMixin):
def create_icon(self, dir, file):
full_path = f"{dir}/{file}"
full_path = dir + "/" + file
return self.get_icon_image(dir, file, full_path)
def get_icon_image(self, dir, file, full_path):
@@ -36,32 +36,29 @@ class Icon(DesktopIconMixin, VideoIconMixin):
return None
def create_thumbnail(self, dir, file):
full_path = f"{dir}/{file}"
full_path = dir + "/" + file
try:
file_hash = hashlib.sha256(str.encode(full_path)).hexdigest()
hash_img_pth = f"{self.ABS_THUMBS_PTH}/{file_hash}.jpg"
hash_img_pth = self.ABS_THUMBS_PTH + "/" + file_hash + ".jpg"
if isfile(hash_img_pth) == False:
self.generate_video_thumbnail(full_path, hash_img_pth)
thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH)
if thumbnl == None: # If no icon whatsoever, return internal default
thumbnl = GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png")
thumbnl = GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICONS + "/video.png")
return thumbnl
except Exception as e:
print("Thumbnail generation issue:")
print( repr(e) )
return GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png")
return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICONS + "/video.png")
def create_scaled_image(self, path, wxh):
try:
if path.lower().endswith(".gif"):
return GdkPixbuf.PixbufAnimation.new_from_file(path) \
.get_static_image() \
.scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR)
else:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], True)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
scaled_pixbuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default
return scaled_pixbuf
except Exception as e:
print("Image Scaling Issue:")
print( repr(e) )

View File

@@ -4,7 +4,7 @@ from os.path import isfile
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk
# Application imports
@@ -21,7 +21,7 @@ class DesktopIconMixin:
if "steam" in icon:
name = xdgObj.getName()
file_hash = hashlib.sha256(str.encode(name)).hexdigest()
hash_img_pth = self.STEAM_ICONS_PTH + "/" + file_hash + ".jpg"
hash_img_pth = f"{self.STEAM_ICONS_PTH}/{file_hash}.jpg"
if isfile(hash_img_pth) == True:
# Use video sizes since headers are bigger
@@ -30,7 +30,7 @@ class DesktopIconMixin:
exec_str = xdgObj.getExec()
parts = exec_str.split("steam://rungameid/")
id = parts[len(parts) - 1]
imageLink = self.STEAM_BASE_URL + id + "/header.jpg"
imageLink = f"{self.STEAM_BASE_URL}{id}/header.jpg"
proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink])
proc.wait()
@@ -57,9 +57,9 @@ class DesktopIconMixin:
for (dirpath, dirnames, filenames) in os.walk(path):
for file in filenames:
appNM = "application-x-" + icon
appNM = f"application-x-{icon}"
if icon in file or appNM in file:
alt_icon_path = dirpath + "/" + file
alt_icon_path = "{dirpath}/{file}"
break
return alt_icon_path

View File

@@ -59,7 +59,7 @@ class Settings:
subpath = settings["base_of_home"]
HIDE_HIDDEN_FILES = True if settings["hide_hidden_files"] == "true" else False
FFMPG_THUMBNLR = FFMPG_THUMBNLR if settings["thumbnailer_path"] == "" else settings["thumbnailer_path"]
go_past_home = True if settings["go_past_home"] == "" else settings["go_past_home"]
go_past_home = True if settings["go_past_home"] == "true" else False
lock_folder = True if settings["lock_folder"] == "true" else False
locked_folders = settings["locked_folders"].split("::::")
mplayer_options = settings["mplayer_options"].split()

View File

@@ -1,15 +1,14 @@
# Python imports
import sys, traceback, threading, inspect, os, time
import sys, traceback, threading, signal, inspect, os, time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib
# Application imports
from .mixins import UIMixin
from .signals import IPCSignalsMixin, KeyboardSignalsMixin
from . import Controller_Data
from .mixins import *
from . import ShowHideMixin, KeyboardSignalsMixin, Controller_Data
def threaded(fn):
@@ -20,26 +19,29 @@ def threaded(fn):
class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, Controller_Data):
class Controller(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin, \
KeyboardSignalsMixin, Controller_Data):
def __init__(self, args, unknownargs, _settings):
sys.excepthook = self.custom_except_hook
self.setup_controller_data(_settings)
# sys.excepthook = self.custom_except_hook
self.settings = _settings
self.setup_controller_data()
self.window.show()
self.generate_windows(self.state)
self.plugins.launch_plugins()
if not trace_debug:
self.gui_event_observer()
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)
self.gui_event_observer()
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
def tear_down(self, widget=None, eve=None):
@@ -57,8 +59,8 @@ class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, Controller_Data
if event:
try:
type, target, data = event
method = getattr(self.__class__, target)
GLib.idle_add(method, *(self, *data,))
method = getattr(self.__class__, type)
GLib.idle_add(method, (self, data,))
except Exception as e:
print(repr(e))
@@ -156,7 +158,9 @@ class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, Controller_Data
if action == "empty_trash":
self.empty_trash()
if action == "create":
self.create_files()
self.hide_new_file_menu()
self.ctrlDown = False

View File

@@ -1,13 +1,11 @@
# Python imports
import signal
# Lib imports
from gi.repository import GLib
# Application imports
from trasher.xdgtrash import XDGTrash
from shellfm import WindowController
from plugins import Plugins
from trasher.xdgtrash import XDGTrash
@@ -16,18 +14,16 @@ class Controller_Data:
def has_method(self, o, name):
return callable(getattr(o, name, None))
def setup_controller_data(self, _settings):
self.trashman = XDGTrash()
def setup_controller_data(self):
self.window_controller = WindowController()
self.plugins = Plugins(_settings)
self.state = self.window_controller.load_state()
self.trashman = XDGTrash()
self.trashman.regenerate()
self.settings = _settings
self.builder = self.settings.get_builder()
self.logger = self.settings.get_logger()
self.state = self.window_controller.load_state()
self.builder = self.settings.builder
self.logger = self.settings.logger
self.window = self.settings.get_main_window()
self.window = self.settings.getMainWindow()
self.window1 = self.builder.get_object("window_1")
self.window2 = self.builder.get_object("window_2")
self.window3 = self.builder.get_object("window_3")
@@ -89,10 +85,10 @@ class Controller_Data:
self.is_pane3_hidden = False
self.is_pane4_hidden = False
self.override_drop_dest = None
self.is_searching = False
self.search_iconview = None
self.search_view = None
self.is_searching = False
self.search_iconview = None
self.search_view = None
self.skip_edit = False
self.cancel_edit = False
@@ -103,7 +99,3 @@ class Controller_Data:
self.success = "#88cc27"
self.warning = "#ffa800"
self.error = "#ff0000"
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)

View File

@@ -15,11 +15,11 @@ def threaded(fn):
class IPCServerMixin:
class DBusControllerMixin:
@threaded
def create_ipc_server(self):
listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
listener = Listener(('127.0.0.1', 4848), authkey=b'solarfm-ipc')
self.is_ipc_alive = True
while True:
conn = listener.accept()
@@ -34,7 +34,7 @@ class IPCServerMixin:
if "FILE|" in msg:
file = msg.split("FILE|")[1].strip()
if file:
event_system.push_gui_event([None, "handle_file_from_ipc", file])
event_system.push_gui_event(["create_tab_from_ipc", None, file])
conn.close()
break
@@ -49,7 +49,7 @@ class IPCServerMixin:
# NOTE: Not perfect but insures we don't lockup the connection for too long.
end_time = time.time()
if (end - start) > self.ipc_timeout:
if (end - start) > 15.0:
conn.close()
listener.close()
@@ -57,7 +57,7 @@ class IPCServerMixin:
def send_ipc_message(self, message="Empty Data..."):
try:
conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
conn = Client(('127.0.0.1', 4848), authkey=b'solarfm-ipc')
conn.send(message)
conn.send('close connection')
except Exception as e:

View File

@@ -3,8 +3,8 @@ import re
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '4.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk, Gdk
# Application imports

View File

@@ -2,8 +2,8 @@
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '4.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk, Gdk, Gio
# Application imports
@@ -16,6 +16,7 @@ class ShowHideMixin:
def stop_file_searching(self, widget=None, eve=None):
self.is_searching = False
def show_exists_page(self, widget=None, eve=None):
response = self.file_exists_dialog.run()
self.file_exists_dialog.hide()
@@ -80,14 +81,12 @@ class ShowHideMixin:
appchooser_widget = self.builder.get_object("appchooser_widget")
response = appchooser_menu.run()
if response == Gtk.ResponseType.CANCEL:
self.hide_appchooser_menu()
if response == Gtk.ResponseType.OK:
self.open_with_files(appchooser_widget)
self.hide_appchooser_menu()
if response == Gtk.ResponseType.CANCEL:
self.hide_appchooser_menu()
def hide_appchooser_menu(self, widget=None, eve=None):
self.builder.get_object("appchooser_menu").hide()
@@ -104,18 +103,12 @@ class ShowHideMixin:
def show_new_file_menu(self, widget=None, eve=None):
self.builder.get_object("context_menu_fname").set_text("")
new_file_menu = self.builder.get_object("new_file_menu")
response = new_file_menu.run()
if response == Gtk.ResponseType.APPLY:
self.create_files()
if response == Gtk.ResponseType.CANCEL:
self.hide_new_file_menu()
self.builder.get_object("new_file_menu").run()
def hide_new_file_menu(self, widget=None, eve=None):
self.builder.get_object("new_file_menu").hide()
def show_edit_file_menu(self, widget=None, eve=None):
if widget:
widget.grab_focus()

View File

@@ -0,0 +1,9 @@
"""
Gtk Bound Signal Module
"""
from .mixins import *
from .DBusControllerMixin import DBusControllerMixin
from .KeyboardSignalsMixin import KeyboardSignalsMixin
from .ShowHideMixin import ShowHideMixin
from .Controller_Data import Controller_Data
from .Controller import Controller

View File

@@ -3,8 +3,8 @@ import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '4.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk, Gdk
# Application imports
@@ -16,6 +16,24 @@ from . import WidgetMixin
class TabMixin(WidgetMixin):
"""docstring for TabMixin"""
def create_tab_from_ipc(data):
self, path = data
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
if notebook.is_visible():
self.create_tab(wid, path)
return
if not self.is_pane4_hidden:
self.create_tab(4, path)
elif not self.is_pane3_hidden:
self.create_tab(3, path)
elif not self.is_pane2_hidden:
self.create_tab(2, path)
elif not self.is_pane1_hidden:
self.create_tab(1, path)
def create_tab(self, wid, path=None):
notebook = self.builder.get_object(f"window_{wid}")
path_entry = self.builder.get_object(f"path_entry")

View File

@@ -3,8 +3,8 @@ import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, GLib, Gio
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GObject, Gio
# Application imports
@@ -107,7 +107,7 @@ class WidgetFileActionMixin:
def open_with_files(self, appchooser_widget):
wid, tid, view, iconview, store = self.get_current_state()
app_info = appchooser_widget.get_app_info()
uris = self.format_to_uris(store, wid, tid, self.selected_files)
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
view.app_chooser_exec(app_info, uris)
@@ -239,12 +239,12 @@ class WidgetFileActionMixin:
else: # Create Folder
self.handle_files([path], "create_dir")
self.hide_new_file_menu()
fname_field.set_text("")
def move_files(self, files, target):
self.handle_files(files, "move", target)
# NOTE: Gtk recommends using fail flow than pre check which is more
# NOTE: Gtk recommends using fail flow than pre check existence 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):
@@ -273,7 +273,8 @@ class WidgetFileActionMixin:
if _file.query_exists():
if not overwrite_all and not rename_auto_all:
self.setup_exists_data(file, _file)
self.exists_file_label.set_label(_file.get_basename())
self.exists_file_field.set_text(_file.get_basename())
response = self.show_exists_page()
if response == "overwrite_all":
@@ -343,45 +344,6 @@ class WidgetFileActionMixin:
self.exists_file_rename_bttn.set_sensitive(False)
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)
exists_file_diff_from = self.builder.get_object("exists_file_diff_from")
exists_file_diff_to = self.builder.get_object("exists_file_diff_to")
exists_file_from = self.builder.get_object("exists_file_from")
exists_file_to = self.builder.get_object("exists_file_to")
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()
exists_file_from.set_label(from_file.get_parent().get_path())
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')} {self.sizeof_fmt(from_size)} ( {from_size} bytes ) ( {age_text} & {size_text} )"
to_label_text = f"{to_date.format('%F %R')} {self.sizeof_fmt(to_size)} ( {to_size} bytes )"
exists_file_diff_from.set_text(from_label_text)
exists_file_diff_to.set_text(to_label_text)
def rename_proc(self, gio_file):
full_path = gio_file.get_path()

View File

@@ -1,11 +1,11 @@
# Python imports
import os, threading, subprocess, time
import os, threading, subprocess
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
gi.require_version('Gdk', '3.0')
gi.require_version("Gtk", "4.0")
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk, Gdk, GLib, Gio, GdkPixbuf
# Application imports
@@ -20,13 +20,15 @@ def threaded(fn):
class WidgetMixin:
def load_store(self, view, store, save_state=False):
store.clear()
dir = view.get_current_directory()
files = view.get_files()
icon = GdkPixbuf.Pixbuf.new_from_file(view.DEFAULT_ICON)
for i, file in enumerate(files):
store.append([None, file[0]])
store.append([icon, file[0]])
self.create_icon(i, view, store, dir, file[0])
# NOTE: Not likely called often from here but it could be useful
@@ -48,14 +50,10 @@ class WidgetMixin:
try:
itr = store.get_iter(i)
except Exception as e:
try:
time.sleep(0.2)
itr = store.get_iter(i)
except Exception as e:
print(":Invalid Itr detected: (Potential race condition...)")
print(f"Index Requested: {i}")
print(f"Store Size: {len(store)}")
return
print(":Invalid Itr detected: (Potential race condition...)")
print(f"Index Requested: {i}")
print(f"Store Size: {len(store)}")
return
if not icon:
icon = self.get_system_thumbnail(fpath, view.SYS_ICON_WH[0])
@@ -115,7 +113,7 @@ class WidgetMixin:
def create_grid_iconview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.IconView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
grid.set_model(store)
grid.set_pixbuf_column(0)
@@ -133,14 +131,11 @@ class WidgetMixin:
grid.connect("button_release_event", self.grid_icon_single_click)
grid.connect("item-activated", self.grid_icon_double_click)
# grid.connect("toggle-cursor-item", self.grid_cursor_toggled)
# grid.connect("notify", self.grid_cursor_toggled)
grid.connect("selection-changed", self.grid_set_selected_items)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
targets = [ uri_target ]
@@ -159,8 +154,8 @@ class WidgetMixin:
def create_grid_treeview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.TreeView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
# store = Gtk.TreeStore(GdkPixbuf.Pixbuf or None, str)
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
# store = Gtk.TreeStore(GdkPixbuf.Pixbuf, str)
column = Gtk.TreeViewColumn("Icons")
icon = Gtk.CellRendererPixbuf()
name = Gtk.CellRendererText()

View File

@@ -5,7 +5,7 @@ from os.path import isdir, isfile
# Lib imports
import gi
gi.require_version('Gdk', '3.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gdk, Gio
# Application imports
@@ -82,7 +82,7 @@ class WindowMixin(TabMixin):
_wid, _tid, _view, iconview, store = self.get_current_state()
selected_files = iconview.get_selected_items()
current_directory = view.get_current_directory()
path_file = Gio.File.new_for_path(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 = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
formatted_mount_size = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::size")) )
@@ -101,16 +101,11 @@ class WindowMixin(TabMixin):
uris = self.format_to_uris(store, _wid, _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 Exception as e:
if debug:
print(repr(e))
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
formatted_size = self.sizeof_fmt(combined_size)
if view.hide_hidden:
@@ -157,9 +152,6 @@ class WindowMixin(TabMixin):
def grid_set_selected_items(self, iconview):
self.selected_files = iconview.get_selected_items()
def grid_cursor_toggled(self, iconview):
print("wat...")
def grid_icon_single_click(self, iconview, eve):
try:
self.path_menu.popdown()
@@ -221,19 +213,8 @@ class WindowMixin(TabMixin):
data.set_text(uris_text, -1)
def grid_on_drag_motion(self, iconview, drag_context, x, y, data):
current = '|'.join(self.window_controller.get_active_data())
target = iconview.get_name()
wid, tid = target.split("|")
store = iconview.get_model()
treePath = iconview.get_drag_dest_item().path
if treePath:
uri = self.format_to_uris(store, wid, tid, treePath)[0].replace("file://", "")
self.override_drop_dest = uri if isdir(uri) else None
if target not in current:
self.window_controller.set_active_data(wid, tid)
wid, tid = iconview.get_name().split("|")
self.window_controller.set_active_data(wid, tid)
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
@@ -243,14 +224,12 @@ class WindowMixin(TabMixin):
view = self.get_fm_window(wid).get_view_by_id(tid)
uris = data.get_uris()
dest = f"{view.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:
dest = f"{view.get_current_directory()}"
if len(uris) > 0:
self.move_files(uris, dest)
else:
uris = data.get_text().split("\n")
self.move_files(uris, dest)
def create_new_view_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, path)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -5,8 +5,8 @@ import os, logging
class Logger:
def __init__(self, config_path):
self._CONFIG_PATH = config_path
def __init__(self):
pass
def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True):
"""
@@ -42,8 +42,8 @@ class Logger:
log.addHandler(ch)
if createFile:
folder = self._CONFIG_PATH
file = f"{folder}/application.log"
folder = "logs"
file = folder + "/application.log"
if not os.path.exists(folder):
os.mkdir(folder)

View File

@@ -4,8 +4,8 @@ from os import path
# Gtk imports
import gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '4.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk as gtk
from gi.repository import Gdk as gdk
@@ -17,68 +17,65 @@ from . import Logger
class Settings:
def __init__(self):
self.logger = Logger().get_logger()
self.builder = gtk.Builder()
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__))
self.USER_HOME = path.expanduser('~')
self.CONFIG_PATH = f"{self.USER_HOME}/.config/{app_name.lower()}"
self.PLUGINS_PATH = f"{self.CONFIG_PATH}/plugins"
self.USR_SOLARFM = f"/usr/share/{app_name.lower()}"
self.CONFIG_PATH = f"{self.USER_HOME}/.config/solarfm"
self.USR_SOLARFM = "/usr/share/solarfm"
self.CSS_FILE = f"{self.CONFIG_PATH}/stylesheet.css"
self.WINDOWS_GLADE = f"{self.CONFIG_PATH}/Main_Window.glade"
self.cssFile = f"{self.CONFIG_PATH}/stylesheet.css"
self.windows_glade = f"{self.CONFIG_PATH}/Main_Window.glade"
self.DEFAULT_ICONS = f"{self.CONFIG_PATH}/icons"
self.WINDOW_ICON = f"{self.DEFAULT_ICONS}/{app_name.lower()}.png"
self.window_icon = f"{self.DEFAULT_ICONS}/solarfm.png"
self.main_window = None
if not os.path.exists(self.CONFIG_PATH):
os.mkdir(self.CONFIG_PATH)
if not os.path.exists(self.PLUGINS_PATH):
os.mkdir(self.PLUGINS_PATH)
if not os.path.exists(self.WINDOWS_GLADE):
self.WINDOWS_GLADE = f"{self.USR_SOLARFM}/Main_Window.glade"
if not os.path.exists(self.CSS_FILE):
self.CSS_FILE = f"{self.USR_SOLARFM}/stylesheet.css"
if not os.path.exists(self.WINDOW_ICON):
self.WINDOW_ICON = f"{self.USR_SOLARFM}/icons/{app_name.lower()}.png"
if not os.path.exists(self.windows_glade):
self.windows_glade = f"{self.USR_SOLARFM}/Main_Window.glade"
if not os.path.exists(self.cssFile):
self.cssFile = f"{self.USR_SOLARFM}/stylesheet.css"
if not os.path.exists(self.window_icon):
self.window_icon = f"{self.USR_SOLARFM}/icons/solarfm.png"
if not os.path.exists(self.DEFAULT_ICONS):
self.DEFAULT_ICONS = f"{self.USR_SOLARFM}/icons"
self.logger = Logger(self.CONFIG_PATH).get_logger()
self.builder.add_from_file(self.WINDOWS_GLADE)
self.builder.add_from_file(self.windows_glade)
def create_window(self):
def createWindow(self):
# Get window and connect signals
self.main_window = self.builder.get_object("Main_Window")
self._set_window_data()
self.setWindowData()
def _set_window_data(self):
self.main_window.set_icon_from_file(self.WINDOW_ICON)
def setWindowData(self):
self.main_window.set_icon_from_file(self.window_icon)
screen = self.main_window.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.main_window.set_visual(visual)
self.main_window.set_app_paintable(True)
self.main_window.connect("draw", self._area_draw)
self.main_window.connect("draw", self.area_draw)
# bind css file
cssProvider = gtk.CssProvider()
cssProvider.load_from_path(self.CSS_FILE)
cssProvider.load_from_path(self.cssFile)
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, cr):
def area_draw(self, widget, cr):
cr.set_source_rgba(0, 0, 0, 0.54)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def get_monitor_data(self):
def getMainWindow(self): return self.main_window
def getMonitorData(self):
screen = self.builder.get_object("Main_Window").get_screen()
monitors = []
for m in range(screen.get_n_monitors()):
@@ -88,8 +85,3 @@ class Settings:
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
return monitors
def get_builder(self): return self.builder
def get_logger(self): return self.logger
def get_main_window(self): return self.main_window
def get_plugins_path(self): return self.PLUGINS_PATH

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="gtk+" version="3.24"/>
<object class="GtkAboutDialog" id="about_page">
<property name="can-focus">False</property>
<property name="border-width">5</property>
@@ -644,36 +644,10 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="button10">
<property name="label" translatable="yes">Create</property>
<property name="name">create</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Create File/Folder...</property>
<property name="image">createImage</property>
<property name="always-show-image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
<placeholder/>
</child>
<child>
<object class="GtkButton" id="button9">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="use-stock">True</property>
<property name="always-show-image">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
<placeholder/>
</child>
</object>
<packing>
@@ -749,7 +723,7 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
@@ -766,26 +740,40 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Create</property>
<property name="name">create</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Create File/Folder...</property>
<property name="image">createImage</property>
<property name="always-show-image">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-10">button10</action-widget>
<action-widget response="-6">button9</action-widget>
</action-widgets>
</object>
<object class="GtkImage" id="exec_in_term_img">
<property name="visible">True</property>
@@ -903,103 +891,6 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">15</property>
<property name="margin-bottom">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Moving From:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="exists_file_from">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="exists_file_diff_from">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">20</property>
<property name="margin-bottom">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Moving To:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="exists_file_to">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="exists_file_diff_to">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">20</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
@@ -1027,9 +918,12 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkEntry" id="exists_file_field">
<property name="visible">True</property>
@@ -1039,7 +933,7 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
<property name="position">3</property>
</packing>
</child>
<child>
@@ -1102,7 +996,7 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
<property name="position">4</property>
</packing>
</child>
</object>
@@ -1130,6 +1024,60 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="stock">gtk-justify-center</property>
</object>
<object class="GtkTextBuffer" id="message_buffer"/>
<object class="GtkPopover" id="message_widget">
<property name="width-request">320</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="position">bottom</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton">
<property name="label">gtk-save-as</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="use-stock">True</property>
<signal name="button-release-event" handler="save_debug_alerts" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="height-request">600</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="shadow-type">in</property>
<property name="overlay-scrolling">False</property>
<child>
<object class="GtkTextView" id="message_view">
<property name="name">message_view</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="editable">False</property>
<property name="cursor-visible">False</property>
<property name="buffer">message_buffer</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkImage" id="open_with_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
@@ -1343,7 +1291,7 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkMenuBar" id="menubar1">
<object class="GtkMenuBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
@@ -1359,25 +1307,19 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<child>
<object class="GtkImageMenuItem">
<property name="label">gtk-new</property>
<property name="name">create</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">New File/Folder...</property>
<property name="use-underline">True</property>
<property name="use-stock">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem">
<property name="label">gtk-open</property>
<property name="name">open</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Open...</property>
<property name="use-underline">True</property>
<property name="use-stock">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
</child>
<child>
@@ -1955,61 +1897,6 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
</object>
</child>
</object>
<object class="GtkPopover" id="message_widget">
<property name="width-request">320</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="relative-to">top_main_menubar</property>
<property name="position">bottom</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton">
<property name="label">gtk-save-as</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="use-stock">True</property>
<signal name="button-release-event" handler="save_debug_alerts" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="height-request">600</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="shadow-type">in</property>
<property name="overlay-scrolling">False</property>
<child>
<object class="GtkTextView" id="message_view">
<property name="name">message_view</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="editable">False</property>
<property name="cursor-visible">False</property>
<property name="buffer">message_buffer</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkPopover" id="path_menu">
<property name="width-request">240</property>
<property name="height-request">420</property>
@@ -2181,6 +2068,25 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child type="center">
<object class="GtkButton">
<property name="label">gtk-delete</property>
<property name="name">delete</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Delete...</property>
<property name="margin-top">20</property>
<property name="use-stock">True</property>
<property name="always-show-image">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label">gtk-open</property>
@@ -2320,6 +2226,25 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Go To Trash</property>
<property name="name">go_to_trash</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Go To Trash...</property>
<property name="image">trash_img2</property>
<property name="always-show-image">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Archive</property>
@@ -2332,65 +2257,12 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="always-show-image">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label">gtk-delete</property>
<property name="name">delete</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Delete...</property>
<property name="margin-top">20</property>
<property name="use-stock">True</property>
<property name="always-show-image">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
<child>
<object class="GtkButton" id="restore_from_trash">
<property name="label" translatable="yes">Restore From Trash</property>
<property name="name">restore_from_trash</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Restore From Trash...</property>
<property name="margin-top">20</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">10</property>
</packing>
</child>
<child>
<object class="GtkButton" id="empty_trash">
<property name="label" translatable="yes">Empty Trash</property>
<property name="name">empty_trash</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Empty Trash...</property>
<property name="margin-bottom">20</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Trash</property>
@@ -2406,25 +2278,8 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Go To Trash</property>
<property name="name">go_to_trash</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Go To Trash...</property>
<property name="image">trash_img2</property>
<property name="always-show-image">True</property>
<signal name="button-release-event" handler="do_action_from_menu_controls" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">13</property>
<property name="pack-type">end</property>
<property name="position">11</property>
</packing>
</child>
</object>