Mostly integrated keybindings setup

This commit is contained in:
itdominator 2022-03-06 21:27:47 -06:00
parent 8eccdfce7c
commit 4aeaffdd44
10 changed files with 317 additions and 155 deletions

View File

@ -77,12 +77,6 @@ class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, ExceptionHookMi
data = method(*(self, *parameters))
self.plugins.send_message_to_plugin(type, data)
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)
dir = tab.get_current_directory()
tab.execute(f"{tab.terminal_app}", dir)
def save_load_session(self, action="save_session"):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
@ -169,3 +163,30 @@ class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, ExceptionHookMi
self.show_new_file_menu()
if action in ["save_session", "save_session_as", "load_session"]:
self.save_load_session(action)
def go_home(self, widget=None, eve=None):
self.builder.get_object("go_home").released()
def refresh_tab(self, widget=None, eve=None):
self.builder.get_object("refresh_tab").released()
def go_up(self, widget=None, eve=None):
self.builder.get_object("go_up").released()
def grab_focus_path_entry(self, widget=None, eve=None):
self.builder.get_object("path_entry").grab_focus()
def tggl_top_main_menubar(self, widget=None, eve=None):
top_main_menubar = self.builder.get_object("top_main_menubar")
top_main_menubar.hide() if top_main_menubar.is_visible() else top_main_menubar.show()
def open_terminal(self, widget=None, eve=None):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
tab.execute(f"{tab.terminal_app}", dir)

View File

@ -10,22 +10,29 @@ from shellfm.windows.controller import WindowController
from plugins.plugins import Plugins
class State:
wid = None
tid = None
tab = None
icon_grid = None
store = None
class Controller_Data:
""" Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """
def setup_controller_data(self, _settings):
self.settings = _settings
self.builder = self.settings.get_builder()
self.logger = self.settings.get_logger()
self.keybindings = self.settings.get_keybindings()
self.trashman = XDGTrash()
self.fm_controller = WindowController()
self.plugins = Plugins(_settings)
self.state = self.fm_controller.load_state()
self.trashman.regenerate()
self.settings = _settings
self.builder = self.settings.get_builder()
self.logger = self.settings.get_logger()
self.window = self.settings.get_main_window()
self.window1 = self.builder.get_object("window_1")
self.window2 = self.builder.get_object("window_2")
@ -109,6 +116,7 @@ class Controller_Data:
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)
def get_current_state(self):
'''
Returns the state info most useful for any given context and action intent.
@ -117,13 +125,15 @@ class Controller_Data:
a (obj): self
Returns:
wid, tid, tab, icon_grid, store
state (obj): State
'''
state = State()
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid")
store = icon_grid.get_model()
return wid, tid, tab, icon_grid, store
state.wid, state.tid = self.fm_controller.get_active_wid_and_tid()
state.tab = self.get_fm_window(wid).get_tab_by_id(tid)
state.icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid")
state.store = state.icon_grid.get_model()
return state
def clear_console(self):

View File

@ -23,7 +23,7 @@ class ExceptionHookMixin:
data = f"Exec Type: {exec_type} <--> Value: {value}\n\n{trace}\n\n\n\n"
start_itr = self.message_buffer.get_start_iter()
self.message_buffer.place_cursor(start_itr)
self.display_message(self.error, data)
self.display_message(self.error_color, data)
def display_message(self, type, text, seconds=None):
self.message_buffer.insert_at_cursor(text)

View File

@ -96,9 +96,9 @@ class TabMixin(WidgetMixin):
return notebook.get_children()[1].get_children()[0]
def refresh_tab(data=None):
wid, tid, tab, icon_grid, store = self.get_current_state()
tab.load_directory()
self.load_store(tab, store)
state = self.get_current_state()
state.tab.load_directory()
self.load_store(state.tab, state.store)
def update_tab(self, tab_label, tab, store, wid, tid):
self.load_store(tab, store)
@ -172,28 +172,14 @@ class TabMixin(WidgetMixin):
pass
def set_path_entry(self, button=None, eve=None):
wid, tid, tab, icon_grid, store = self.get_current_state()
path = f"{tab.get_current_directory()}/{button.get_label()}"
state = self.get_current_state()
path = f"{state.tab.get_current_directory()}/{button.get_label()}"
path_entry = self.builder.get_object("path_entry")
path_entry.set_text(path)
path_entry.grab_focus_without_selecting()
path_entry.set_position(-1)
self.path_menu.popdown()
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}")
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)
self.fm_controller.save_state()
self.set_window_title()
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)

View File

@ -102,10 +102,8 @@ class WidgetFileActionMixin:
self.load_store(tab, store)
tab_widget_label.set_label(tab.get_end_of_path())
_wid, _tid, _tab, _icon_grid, _store = self.get_current_state()
if [wid, tid] in [_wid, _tid]:
state = self.get_current_state()
if [wid, tid] in [state.wid, state.tid]:
self.set_bottom_labels(tab)
@ -130,47 +128,44 @@ class WidgetFileActionMixin:
def open_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
for file in uris:
tab.open_file_locally(file)
state.tab.open_file_locally(file)
def open_with_files(self, appchooser_widget):
wid, tid, tab, icon_grid, store = self.get_current_state()
state = self.get_current_state()
app_info = appchooser_widget.get_app_info()
uris = self.format_to_uris(store, wid, tid, self.selected_files)
tab.app_chooser_exec(app_info, uris)
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files)
state.tab.app_chooser_exec(app_info, uris)
def execute_files(self, in_terminal=False):
wid, tid, tab, icon_grid, store = self.get_current_state()
paths = self.format_to_uris(store, wid, tid, self.selected_files, True)
current_dir = tab.get_current_directory()
state = self.get_current_state()
paths = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
current_dir = state.tab.get_current_directory()
command = None
for path in paths:
command = f"exec '{path}'" if not in_terminal else f"{tab.terminal_app} -e '{path}'"
tab.execute(command, start_dir=tab.get_current_directory(), use_os_system=False)
command = f"exec '{path}'" if not in_terminal else f"{state.tab.terminal_app} -e '{path}'"
state.tab.execute(command, start_dir=state.tab.get_current_directory(), use_os_system=False)
def archive_files(self, archiver_dialogue):
wid, tid, tab, icon_grid, store = self.get_current_state()
paths = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
paths = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
save_target = archiver_dialogue.get_filename();
sItr, eItr = self.arc_command_buffer.get_bounds()
pre_command = self.arc_command_buffer.get_text(sItr, eItr, False)
pre_command = pre_command.replace("%o", save_target)
pre_command = pre_command.replace("%N", ' '.join(paths))
command = f"{tab.terminal_app} -e '{pre_command}'"
command = f"{state.tab.terminal_app} -e '{pre_command}'"
tab.execute(command, start_dir=None, use_os_system=True)
state.tab.execute(command, start_dir=None, use_os_system=True)
def rename_files(self):
rename_label = self.builder.get_object("file_to_rename_label")
rename_input = self.builder.get_object("new_rename_fname")
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
for uri in uris:
entry = uri.split("/")[-1]
@ -186,7 +181,7 @@ class WidgetFileActionMixin:
break
rname_to = rename_input.get_text().strip()
target = f"{tab.get_current_directory()}/{rname_to}"
target = f"{state.tab.get_current_directory()}/{rname_to}"
self.handle_files([uri], "rename", target)
@ -196,13 +191,13 @@ class WidgetFileActionMixin:
self.selected_files.clear()
def cut_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
self.to_cut_files = uris
def copy_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
self.to_copy_files = uris
def paste_files(self):
@ -216,8 +211,8 @@ class WidgetFileActionMixin:
self.handle_files(self.to_cut_files, "move", target)
def delete_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
response = None
self.warning_alert.format_secondary_text(f"Do you really want to delete the {len(uris)} file(s)?")
@ -231,7 +226,7 @@ class WidgetFileActionMixin:
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
tab.delete_file( file.get_path() )
state.tab.delete_file( file.get_path() )
else:
file.delete(cancellable=None)
else:
@ -239,14 +234,14 @@ class WidgetFileActionMixin:
def trash_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
for uri in uris:
self.trashman.trash(uri, False)
def restore_trash_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
for uri in uris:
self.trashman.restore(filename=uri.split("/")[-1], verbose=False)

View File

@ -25,8 +25,11 @@ class WindowMixin(TabMixin):
self.fm_controller.create_window()
notebook_tggl_button.set_active(True)
if tabs:
for tab in tabs:
self.create_new_tab_notebook(None, i, tab)
else:
self.create_new_tab_notebook(None, i, None)
if is_hidden:
self.toggle_notebook_pane(notebook_tggl_button)
@ -77,8 +80,8 @@ class WindowMixin(TabMixin):
def set_bottom_labels(self, tab):
_wid, _tid, _tab, icon_grid, store = self.get_current_state()
selected_files = icon_grid.get_selected_items()
state = self.get_current_state()
selected_files = state.icon_grid.get_selected_items()
current_directory = tab.get_current_directory()
path_file = Gio.File.new_for_path(current_directory)
mount_file = path_file.query_filesystem_info(attributes="filesystem::*", cancellable=None)
@ -96,7 +99,7 @@ class WindowMixin(TabMixin):
self.bottom_size_label.set_label(f"{formatted_mount_free} free / {formatted_mount_size}")
self.bottom_path_label.set_label(tab.get_current_directory())
if selected_files:
uris = self.format_to_uris(store, _wid, _tid, selected_files, True)
uris = self.format_to_uris(state.store, state.wid, state.tid, selected_files, True)
combined_size = 0
for uri in uris:
try:
@ -189,17 +192,17 @@ class WindowMixin(TabMixin):
return
wid, tid, tab, _icons_grid, store = self.get_current_state()
notebook = self.builder.get_object(f"window_{wid}")
state = self.get_current_state()
notebook = self.builder.get_object(f"window_{state.wid}")
tab_label = self.get_tab_label(notebook, icons_grid)
fileName = store[item][1]
dir = tab.get_current_directory()
fileName = state.store[item][1]
dir = state.tab.get_current_directory()
file = f"{dir}/{fileName}"
if isdir(file):
tab.set_path(file)
self.update_tab(tab_label, tab, store, wid, tid)
state.tab.set_path(file)
self.update_tab(tab_label, state.tab, state.store, state.wid, state.tid)
else:
self.open_files()
except Exception as e:

View File

@ -16,13 +16,14 @@ valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
class KeyboardSignalsMixin:
""" KeyboardSignalsMixin keyboard hooks controller. """
# TODO: Need to set methods that use this to somehow check the keybindings state instead.
def unset_keys_and_data(self, widget=None, eve=None):
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
self.is_searching = False
def global_key_press_controller(self, eve, user_data):
def on_global_key_press_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
@ -32,13 +33,9 @@ class KeyboardSignalsMixin:
if "alt" in keyname:
self.alt_down = True
# NOTE: Yes, this should actually be mapped to some key controller setting
# file or something. Sue me.
def global_key_release_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if debug:
print(f"global_key_release_controller > key > {keyname}")
def on_global_key_release_controller(self, widget, event):
"""Handler for keyboard events"""
keyname = Gdk.keyval_name(event.keyval).lower()
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
self.ctrl_down = False
@ -47,60 +44,18 @@ class KeyboardSignalsMixin:
if "alt" in keyname:
self.alt_down = False
if self.ctrl_down and self.shift_down and keyname == "t":
self.unset_keys_and_data()
self.trash_files()
mapping = self.keybindings.lookup(event)
if mapping:
getattr(self, mapping)()
return True
else:
if debug:
print(f"on_global_key_release_controller > key > {keyname}")
if self.ctrl_down:
if keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]:
self.builder.get_object(f"tggl_notebook_{keyname.strip('kp_')}").released()
if keyname == "q":
self.tear_down()
if keyname == "slash" or keyname == "home":
self.builder.get_object("go_home").released()
if keyname == "r" or keyname == "f5":
self.builder.get_object("refresh_tab").released()
if keyname == "up" or keyname == "u":
self.builder.get_object("go_up").released()
if keyname == "l":
self.unset_keys_and_data()
self.builder.get_object("path_entry").grab_focus()
if keyname == "t":
self.builder.get_object("create_tab").released()
if keyname == "o":
self.unset_keys_and_data()
self.open_files()
if keyname == "w":
self.keyboard_close_tab()
if keyname == "h":
self.show_hide_hidden_files()
if keyname == "e":
self.unset_keys_and_data()
self.rename_files()
if keyname == "c":
self.copy_files()
self.to_cut_files.clear()
if keyname == "x":
self.to_copy_files.clear()
self.cut_files()
if keyname == "v":
self.paste_files()
if keyname == "n":
self.unset_keys_and_data()
self.show_new_file_menu()
if keyname == "delete":
self.unset_keys_and_data()
self.delete_files()
if keyname == "f2":
self.unset_keys_and_data()
self.rename_files()
if keyname == "f4":
self.unset_keys_and_data()
self.open_terminal()
if keyname in ["alt_l", "alt_r"]:
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()
if re.fullmatch(valid_keyvalue_pat, keyname):
if not self.is_searching and not self.ctrl_down \
@ -108,7 +63,37 @@ class KeyboardSignalsMixin:
focused_obj = self.window.get_focus()
if isinstance(focused_obj, Gtk.IconView):
self.is_searching = True
wid, tid, self.search_tab, self.search_icon_grid, store = self.get_current_state()
state = self.get_current_state()
self.search_tab = state.tab
self.search_icon_grid = state.icon_grid
self.unset_keys_and_data()
self.popup_search_files(wid, keyname)
return
self.popup_search_files(state.wid, keyname)
return True
def keyboard_create_tab(self, widget=None, eve=None):
self.builder.get_object("create_tab").released()
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}")
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)
self.fm_controller.save_state()
self.set_window_title()
def keyboard_copy_files(self, widget=None, eve=None):
self.to_cut_files.clear()
self.copy_files()
def keyboard_cut_files(self, widget=None, eve=None):
self.to_copy_files.clear()
self.cut_files()

View File

@ -0,0 +1,124 @@
# Python imports
import re
# Gtk imports
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
# Application imports
def err(log = ""):
"""Print an error message"""
print(log)
class KeymapError(Exception):
"""Custom exception for errors in keybinding configurations"""
MODIFIER = re.compile('<([^<]+)>')
class Keybindings:
"""Class to handle loading and lookup of Terminator keybindings"""
modifiers = {
'ctrl': Gdk.ModifierType.CONTROL_MASK,
'control': Gdk.ModifierType.CONTROL_MASK,
'primary': Gdk.ModifierType.CONTROL_MASK,
'shift': Gdk.ModifierType.SHIFT_MASK,
'alt': Gdk.ModifierType.MOD1_MASK,
'super': Gdk.ModifierType.SUPER_MASK,
'hyper': Gdk.ModifierType.HYPER_MASK,
'mod2': Gdk.ModifierType.MOD2_MASK
}
empty = {}
keys = None
_masks = None
_lookup = None
def __init__(self):
self.keymap = Gdk.Keymap.get_default()
self.configure({})
def configure(self, bindings):
"""Accept new bindings and reconfigure with them"""
self.keys = bindings
self.reload()
def reload(self):
"""Parse bindings and mangle into an appropriate form"""
self._lookup = {}
self._masks = 0
for action, bindings in list(self.keys.items()):
if not isinstance(bindings, tuple):
bindings = (bindings,)
for binding in bindings:
if not binding or binding == "None":
continue
try:
keyval, mask = self._parsebinding(binding)
# Does much the same, but with poorer error handling.
#keyval, mask = Gtk.accelerator_parse(binding)
except KeymapError as e:
err ("keybinding reload failed to parse binding '%s': %s" % (binding, e))
else:
if mask & Gdk.ModifierType.SHIFT_MASK:
if keyval == Gdk.KEY_Tab:
keyval = Gdk.KEY_ISO_Left_Tab
mask &= ~Gdk.ModifierType.SHIFT_MASK
else:
keyvals = Gdk.keyval_convert_case(keyval)
if keyvals[0] != keyvals[1]:
keyval = keyvals[1]
mask &= ~Gdk.ModifierType.SHIFT_MASK
else:
keyval = Gdk.keyval_to_lower(keyval)
self._lookup.setdefault(mask, {})
self._lookup[mask][keyval] = action
self._masks |= mask
def _parsebinding(self, binding):
"""Parse an individual binding using Gtk's binding function"""
mask = 0
modifiers = re.findall(MODIFIER, binding)
if modifiers:
for modifier in modifiers:
mask |= self._lookup_modifier(modifier)
key = re.sub(MODIFIER, '', binding)
if key == '':
raise KeymapError('No key found!')
keyval = Gdk.keyval_from_name(key)
if keyval == 0:
raise KeymapError("Key '%s' is unrecognised..." % key)
return (keyval, mask)
def _lookup_modifier(self, modifier):
"""Map modifier names to gtk values"""
try:
return self.modifiers[modifier.lower()]
except KeyError:
raise KeymapError("Unhandled modifier '<%s>'" % modifier)
def lookup(self, event):
"""Translate a keyboard event into a mapped key"""
try:
_found, keyval, _egp, _lvl, consumed = self.keymap.translate_keyboard_state(
event.hardware_keycode,
Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK),
event.group)
except TypeError:
err ("Keybinding lookup failed to translate keyboard event: %s" % dir(event))
return None
mask = (event.get_state() & ~consumed) & self._masks
return self._lookup.get(mask, self.empty).get(keyval, None)

View File

@ -13,6 +13,39 @@ from gi.repository import Gdk
# Application imports
from .logger import Logger
from .keybindings import Keybindings
DEFAULTS = {
'keybindings': {
'help' : 'F1',
'rename_files' : 'F2',
'open_terminal' : 'F4',
'refresh_tab' : 'F5',
'delete_files' : 'Delete',
# 'tggl_top_main_menubar' : '<Alt>',
'trash_files' : '<Shift><Control>t',
'tear_down' : '<Control>q',
'go_home' : '<Control>slash',
'refresh_tab' : '<Control>r',
'go_up' : '<Control>Up',
'grab_focus_path_entry' : '<Control>l',
'open_files' : '<Control>o',
'show_hide_hidden_files' : '<Control>h',
'rename_files' : '<Control>e',
'keyboard_create_tab' : '<Control>t',
'keyboard_close_tab' : '<Control>w',
'keyboard_copy_files' : '<Control>c',
'keyboard_cut_files' : '<Control>x',
'paste_files' : '<Control>v',
'show_new_file_menu' : '<Control>n',
},
}
class Settings:
@ -46,9 +79,11 @@ class Settings:
self._warning_color = "#ffa800"
self._error_color = "#ff0000"
self.keybindings = Keybindings()
self.main_window = None
self.logger = Logger(self._CONFIG_PATH).get_logger()
self.builder = Gtk.Builder()
self.keybindings.configure(DEFAULTS['keybindings'])
self.builder.add_from_file(self._WINDOWS_GLADE)
@ -94,6 +129,7 @@ class Settings:
def get_builder(self): return self.builder
def get_logger(self): return self.logger
def get_keybindings(self): return self.keybindings
def get_main_window(self): return self.main_window
def get_plugins_path(self): return self._PLUGINS_PATH

View File

@ -1182,6 +1182,7 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<child>
<object class="GtkButton" id="button11">
<property name="label">gtk-cancel</property>
<property name="use-action-appearance">True</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
@ -1196,6 +1197,7 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<child>
<object class="GtkButton" id="button12">
<property name="label">gtk-ok</property>
<property name="use-action-appearance">True</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
@ -1408,8 +1410,8 @@ SolarFM is developed on Atom, git, and using Python 3+ with Gtk GObject introspe
<property name="default-height">830</property>
<property name="gravity">center</property>
<signal name="focus-out-event" handler="unset_keys_and_data" swapped="no"/>
<signal name="key-press-event" handler="global_key_press_controller" swapped="no"/>
<signal name="key-release-event" handler="global_key_release_controller" swapped="no"/>
<signal name="key-press-event" handler="on_global_key_press_controller" swapped="no"/>
<signal name="key-release-event" handler="on_global_key_release_controller" swapped="no"/>
<child>
<object class="GtkBox">
<property name="visible">True</property>