Files
I38/scripts/shortcut_creator.py

375 lines
17 KiB
Python

#!/usr/bin/env python3
# This file is part of I38, licensed under the GNU GPL, version 3 or later.
import argparse
import threading
import gi
gi.require_version("Atk", "1.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GLib, Gtk
from shortcuts import (clipboard_target, create_shortcut, detect_wine_prefix, fetch_page_title,
installed_applications, parse_target, validate_wine_prefix)
class ApplicationDialog(Gtk.Dialog):
def __init__(self, parent):
super().__init__(title="Search for application", transient_for=parent, modal=True)
self.set_default_size(620, 420)
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL, "S_elect", Gtk.ResponseType.OK)
self.set_default_response(Gtk.ResponseType.OK)
box = self.get_content_area()
box.set_spacing(8)
box.set_border_width(12)
label = Gtk.Label.new_with_mnemonic("_Search applications:")
label.set_xalign(0)
self.searchEntry = Gtk.SearchEntry()
label.set_mnemonic_widget(self.searchEntry)
self.searchEntry.get_accessible().set_name("Search applications")
box.pack_start(label, False, False, 0)
box.pack_start(self.searchEntry, False, False, 0)
self.store = Gtk.ListStore(str, str, str)
for app in installed_applications():
self.store.append((app.get_display_name(), app.get_id(), app.get_filename()))
self.query = ""
self.filtered = self.store.filter_new()
self.filtered.set_visible_func(lambda model, row, _data:
model[row][0].casefold().startswith(self.query))
self.view = Gtk.TreeView(model=self.filtered)
self.view.set_enable_search(False)
self.view.get_accessible().set_name("Applications")
for index, title in enumerate(("Application", "Launcher")):
self.view.append_column(Gtk.TreeViewColumn(title, Gtk.CellRendererText(), text=index))
self.view.connect("row-activated", self.confirm_selection)
self.view.connect("key-press-event", self.on_list_key)
scroll = Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.add(self.view)
box.pack_start(scroll, True, True, 0)
self.countLabel = Gtk.Label(xalign=0)
box.pack_start(self.countLabel, False, False, 0)
self.searchEntry.connect("changed", self.on_search)
self.searchEntry.connect("activate", self.confirm_selection)
self.searchEntry.connect("key-press-event", self.on_search_key)
self.view.get_selection().connect("changed", self.on_selection)
self.on_search(self.searchEntry)
self.show_all()
self.searchEntry.grab_focus()
def on_search(self, entry):
self.query = entry.get_text().casefold()
self.filtered.refilter()
if len(self.filtered):
self.view.set_cursor(Gtk.TreePath.new_first())
self.countLabel.set_text("{} applications found.".format(len(self.filtered)))
self.on_selection(self.view.get_selection())
def on_selection(self, selection):
_model, row = selection.get_selected()
self.set_response_sensitive(Gtk.ResponseType.OK, row is not None)
def on_search_key(self, _entry, event):
if event.keyval == Gdk.KEY_Down:
self.view.grab_focus()
return True
return False
def on_list_key(self, _view, event):
character = Gdk.keyval_to_unicode(event.keyval)
if character >= 32 and not event.state & (Gdk.ModifierType.CONTROL_MASK |
Gdk.ModifierType.MOD1_MASK |
Gdk.ModifierType.SUPER_MASK):
self.searchEntry.grab_focus()
self.searchEntry.insert_text(chr(character), -1)
self.searchEntry.set_position(-1)
return True
if event.keyval == Gdk.KEY_BackSpace:
self.searchEntry.set_text(self.searchEntry.get_text()[:-1])
self.searchEntry.grab_focus()
self.searchEntry.set_position(-1)
return True
return False
def selected_path(self):
model, row = self.view.get_selection().get_selected()
return model[row][2] if row is not None else None
def confirm_selection(self, *_args):
if self.selected_path() is not None:
self.response(Gtk.ResponseType.OK)
class ShortcutWindow(Gtk.Window):
def __init__(self, fileManager=""):
super().__init__(title="I38 Create Desktop Shortcut")
self.fileManager = fileManager
self.target = None
self.requestNumber = 0
self.nameVersion = 0
self.closed = False
self.set_default_size(650, 300)
self.set_border_width(12)
self.connect("destroy", Gtk.main_quit)
self.connect("destroy", self.on_destroy)
self.connect("key-press-event", self.on_key)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.add(box)
instructions = Gtk.Label(label="Choose a source, then review the target and shortcut name.", xalign=0)
box.pack_start(instructions, False, False, 0)
for label, callback in (("Add from _Clipboard", self.on_clipboard),
("Browse for _File", self.on_file),
("Browse for F_older", self.on_folder),
("Search for _Application", self.on_application)):
button = Gtk.Button.new_with_mnemonic(label)
button.connect("clicked", callback)
box.pack_start(button, False, False, 0)
self.targetEntry = Gtk.Entry()
self.targetEntry.set_placeholder_text("/usr/bin/firefox, ~/Documents, or https://example.org/")
self.add_field(box, "_Target (path, program, or web URL):", self.targetEntry)
self.nameEntry = Gtk.Entry()
self.add_field(box, "Shortcut _name:", self.nameEntry)
self.wineBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.wineBox.set_no_show_all(True)
self.wineEntry = Gtk.Entry()
self.wineEntry.set_activates_default(True)
self.add_field(self.wineBox, "_Wine prefix (bottle) folder:", self.wineEntry)
wineButton = Gtk.Button.new_with_mnemonic("_Browse for Wine Prefix")
wineButton.connect("clicked", self.on_wine_prefix)
self.wineBox.pack_start(wineButton, False, False, 0)
box.pack_start(self.wineBox, False, False, 0)
self.targetEntry.connect("changed", self.on_target_changed)
self.targetEntry.connect("activate", self.on_target_activate)
self.nameEntry.connect("changed", self.on_name_changed)
self.nameEntry.set_activates_default(True)
self.terminalCheck = Gtk.CheckButton.new_with_mnemonic("Run program in a ter_minal")
self.terminalCheck.set_sensitive(False)
box.pack_start(self.terminalCheck, False, False, 0)
self.statusLabel = Gtk.Label(label="No source selected.", xalign=0)
self.statusLabel.set_line_wrap(True)
self.statusLabel.set_selectable(True)
self.statusLabel.get_accessible().set_name("Shortcut status")
box.pack_start(self.statusLabel, False, False, 0)
buttons = Gtk.ButtonBox(orientation=Gtk.Orientation.HORIZONTAL)
buttons.set_layout(Gtk.ButtonBoxStyle.END)
closeButton = Gtk.Button.new_with_mnemonic("C_lose")
closeButton.connect("clicked", lambda *_args: self.close())
self.createButton = Gtk.Button.new_with_mnemonic("C_reate Shortcut")
self.createButton.set_can_default(True)
self.createButton.connect("clicked", self.on_create)
self.createButton.set_sensitive(False)
buttons.add(closeButton)
buttons.add(self.createButton)
box.pack_start(buttons, False, False, 0)
self.set_default(self.createButton)
self.show_all()
box.get_children()[1].grab_focus()
def add_field(self, box, text, entry):
label = Gtk.Label.new_with_mnemonic(text)
label.set_xalign(0)
label.set_mnemonic_widget(entry)
box.pack_start(label, False, False, 0)
box.pack_start(entry, False, False, 0)
def show_message(self, message, error=False):
dialog = Gtk.MessageDialog(transient_for=self, modal=True,
message_type=Gtk.MessageType.ERROR if error else Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.CLOSE,
text="Could not create shortcut" if error else "Desktop shortcut created")
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
def on_target_changed(self, _entry):
self.requestNumber += 1
self.target = None
self.createButton.set_sensitive(False)
self.terminalCheck.set_active(False)
self.terminalCheck.set_sensitive(False)
self.wineBox.hide()
self.wineEntry.set_text("")
self.statusLabel.set_text("Press Enter to check the target, or choose a source above.")
def on_name_changed(self, _entry):
self.nameVersion += 1
self.createButton.set_sensitive(self.target is not None and bool(self.nameEntry.get_text().strip()))
def set_target(self, target):
self.targetEntry.set_text(target.value)
self.target = target
self.nameEntry.set_text(target.name)
self.terminalCheck.set_sensitive(target.kind in ("Program", "Windows program"))
self.statusLabel.set_text("{}: {}".format(target.kind, target.value))
self.nameEntry.grab_focus()
self.nameEntry.select_region(0, -1)
if target.kind == "Windows program":
winePrefix = detect_wine_prefix(target.value)
self.wineEntry.set_text(winePrefix or "")
self.wineBox.set_no_show_all(False)
self.wineBox.show_all()
self.wineBox.set_no_show_all(True)
if winePrefix:
self.statusLabel.set_text("Windows program: {}. Wine prefix: {}".format(target.value, winePrefix))
else:
self.statusLabel.set_text("Choose the Wine prefix (bottle) folder before creating this shortcut.")
self.wineEntry.grab_focus()
if target.kind == "Web link":
requestNumber = self.requestNumber
nameVersion = self.nameVersion
self.statusLabel.set_text("Looking up page title. You can edit the name or create the shortcut now.")
def lookup():
try:
title = fetch_page_title(target.value)
except Exception:
# Network, TLS, decoding, and malformed HTML failures all
# leave the usable hostname suggestion in place.
title = None
GLib.idle_add(self.apply_title, requestNumber, nameVersion, target, title)
threading.Thread(target=lookup, daemon=True).start()
def apply_title(self, requestNumber, nameVersion, target, title):
if self.closed or requestNumber != self.requestNumber:
return False
if nameVersion == self.nameVersion and title:
self.nameEntry.set_text(title)
self.nameEntry.select_region(0, -1)
self.statusLabel.set_text("Page title found." if title else
"Page title unavailable. Using the site hostname unless you edit the name.")
return False
def on_destroy(self, _widget):
self.closed = True
self.requestNumber += 1
def on_target_activate(self, _entry):
try:
self.set_target(parse_target(self.targetEntry.get_text()))
except (ValueError, OSError, GLib.Error) as error:
self.show_message(str(error), error=True)
def on_clipboard(self, _button):
self.requestNumber += 1
requestNumber = self.requestNumber
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.statusLabel.set_text("Reading clipboard…")
def received_text(_clipboard, text, _data):
if requestNumber != self.requestNumber:
return
try:
self.set_target(clipboard_target(text or "", "text"))
except (ValueError, UnicodeError, OSError, GLib.Error) as error:
self.show_message(str(error), error=True)
def received_contents(_clipboard, selection, targetType):
if requestNumber != self.requestNumber:
return
try:
self.set_target(clipboard_target(selection.get_data() or b"", targetType))
except (ValueError, UnicodeError, OSError, GLib.Error) as error:
self.show_message(str(error), error=True)
def received_targets(_clipboard, targets, _targetCount, _data):
if requestNumber != self.requestNumber:
return
names = {atom.name(): atom for atom in targets or []}
for targetType in ("x-special/gnome-copied-files", "text/uri-list", "application/x-kde4-urilist"):
if targetType in names:
clipboard.request_contents(names[targetType], received_contents, targetType)
return
clipboard.request_text(received_text, None)
clipboard.request_targets(received_targets, None)
def choose_path(self, folder):
self.requestNumber += 1
dialog = Gtk.FileChooserDialog(title="Choose folder" if folder else "Choose file or program",
transient_for=self,
action=Gtk.FileChooserAction.SELECT_FOLDER if folder else Gtk.FileChooserAction.OPEN)
dialog.add_buttons("_Cancel", Gtk.ResponseType.CANCEL, "_Select", Gtk.ResponseType.OK)
dialog.set_local_only(True)
if dialog.run() == Gtk.ResponseType.OK:
value = dialog.get_filename()
else:
value = None
dialog.destroy()
if value:
try:
self.set_target(parse_target(value))
except (ValueError, OSError, GLib.Error) as error:
self.show_message(str(error), error=True)
def on_file(self, _button):
self.choose_path(False)
def on_folder(self, _button):
self.choose_path(True)
def on_wine_prefix(self, _button):
dialog = Gtk.FileChooserDialog(title="Choose Wine prefix (bottle) folder",
transient_for=self, modal=True,
action=Gtk.FileChooserAction.SELECT_FOLDER)
dialog.add_buttons("_Cancel", Gtk.ResponseType.CANCEL, "_Select", Gtk.ResponseType.OK)
dialog.set_local_only(True)
dialog.set_show_hidden(True)
if dialog.run() == Gtk.ResponseType.OK:
self.wineEntry.set_text(dialog.get_filename())
dialog.destroy()
self.wineEntry.grab_focus()
def on_application(self, _button):
self.requestNumber += 1
try:
dialog = ApplicationDialog(self)
value = dialog.selected_path() if dialog.run() == Gtk.ResponseType.OK else None
dialog.destroy()
if value:
self.set_target(parse_target(value))
except (ValueError, OSError, GLib.Error) as error:
self.show_message(str(error), error=True)
def on_create(self, _button):
if self.target is None:
return
winePrefix = None
if self.target.kind == "Windows program":
try:
winePrefix = validate_wine_prefix(self.wineEntry.get_text())
except ValueError as error:
self.show_message(str(error), error=True)
self.wineEntry.grab_focus()
return
try:
destination = create_shortcut(self.target, self.nameEntry.get_text(), self.fileManager,
self.terminalCheck.get_active(), winePrefix=winePrefix)
except (ValueError, OSError, GLib.Error) as error:
self.show_message(str(error), error=True)
return
self.show_message("Created {}.\n\nOpen I38 Desktop to use it.".format(destination))
self.close()
def on_key(self, _widget, event):
if event.keyval == Gdk.KEY_Escape:
self.close()
return True
return False
def main():
parser = argparse.ArgumentParser(description="Create an I38 desktop shortcut")
parser.add_argument("--file-manager", default="", help="I38's selected file manager executable")
args = parser.parse_args()
GLib.set_application_name("I38 Create Desktop Shortcut")
ShortcutWindow(args.file_manager)
Gtk.main()
if __name__ == "__main__":
main()