#!/usr/bin/python3 # Install confirmation with details window import logging import os import pathlib import subprocess import sys import tempfile import webbrowser import gi gi.require_version('Gtk', '3.0') gi.require_version('WebKit2', '4.0') from pkg_resources import parse_version from gi.repository import Gtk, WebKit2 from keyman_config import _, secure_lookup from keyman_config.accelerators import bind_accelerator, init_accel from keyman_config.fcitx_util import is_fcitx_running from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir, get_keyman_dir) from keyman_config.install_kmp import (InstallError, InstallStatus, extract_kmp, get_metadata, install_kmp) from keyman_config.kmpmetadata import get_fonts from keyman_config.list_installed_kmp import get_kmp_version_user from keyman_config.uninstall_kmp import uninstall_kmp from keyman_config.welcome import WelcomeView def find_keyman_image(image_file): img_path = os.path.join(get_keyman_dir(InstallLocation.OS), "icons", image_file) if not os.path.isfile(img_path): img_path = os.path.join(get_keyman_dir(InstallLocation.Shared), "icons", image_file) if not os.path.isfile(img_path): img_path = os.path.join("keyman_config/icons/", image_file) if not os.path.isfile(img_path): img_path = image_file if not os.path.isfile(img_path): img_path = os.path.join("icons", image_file) return img_path class InstallKmpWindow(Gtk.Dialog): def __init__(self, kmpfile, online=False, viewkmp=None, language=None): logging.debug("InstallKmpWindow: kmpfile: %s", kmpfile) self.is_error = False self.kmpfile = kmpfile self.online = online self.viewwindow = viewkmp self.accelerators = None self.language = language keyboardid = os.path.basename(os.path.splitext(kmpfile)[0]) installed_kmp_ver = get_kmp_version_user(keyboardid) if installed_kmp_ver: logging.info("installed kmp version %s", installed_kmp_ver) windowtitle = _("Installing keyboard/package {keyboardid}").format(keyboardid=keyboardid) Gtk.Dialog.__init__(self, windowtitle, viewkmp) init_accel(self) self.set_border_width(12) mainhbox = Gtk.Box() with tempfile.TemporaryDirectory() as tmpdirname: try: extract_kmp(kmpfile, tmpdirname) except InstallError as e: self._handle_install_error(e, kmpfile) self.is_error = True return info, system, options, keyboards, files = get_metadata(tmpdirname) if not keyboards: # Likely not a keyboard .kmp file logging.info("%s is not a Keyman keyboard package" % kmpfile) dialog = Gtk.MessageDialog(viewkmp, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, _( "The file '{kmpfile}' is not a Keyman keyboard package!").format(kmpfile=kmpfile)) dialog.run() dialog.destroy() self.checkcontinue = False return if len(keyboards) > 0 and 'name' in keyboards[0]: self.kbname = keyboards[0]['name'] else: self.kbname = keyboardid self.checkcontinue = True if installed_kmp_ver and secure_lookup(info, 'version', 'description'): if secure_lookup(info, 'version', 'description') == installed_kmp_ver: dialog = Gtk.MessageDialog( viewkmp, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("Keyboard is installed already")) dialog.format_secondary_text( _("The {name} keyboard is already installed at version {version}. " "Do you want to uninstall then reinstall it?") .format(name=self.kbname, version=installed_kmp_ver)) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: logging.debug("QUESTION dialog closed by clicking YES button") uninstall_kmp(keyboardid) elif response == Gtk.ResponseType.NO: logging.debug("QUESTION dialog closed by clicking NO button") self.checkcontinue = False return else: try: logging.info("package version %s", secure_lookup(info, 'version', 'description')) logging.info("installed kmp version %s", installed_kmp_ver) if parse_version(secure_lookup(info, 'version', 'description')) <= parse_version(installed_kmp_ver): dialog = Gtk.MessageDialog( viewkmp, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("Keyboard is installed already")) dialog.format_secondary_text( _("The {name} keyboard is already installed with a newer version {installedversion}. " "Do you want to uninstall it and install the older version {version}?") .format(name=self.kbname, installedversion=installed_kmp_ver, version=secure_lookup(info, 'version', 'description'))) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: logging.debug("QUESTION dialog closed by clicking YES button") uninstall_kmp(keyboardid) elif response == Gtk.ResponseType.NO: logging.debug("QUESTION dialog closed by clicking NO button") self.checkcontinue = False return except: # noqa: E722 logging.warning("Exception uninstalling an old kmp, continuing") pass image = Gtk.Image() if secure_lookup(options, 'graphicFile'): image.set_from_file(os.path.join(tmpdirname, options['graphicFile'])) else: img_default = find_keyman_image("defaultpackage.gif") image.set_from_file(img_default) mainhbox.pack_start(image, False, False, 0) self.page1 = Gtk.Box() self.page1.set_border_width(12) grid = Gtk.Grid() self.page1.add(grid) label1 = Gtk.Label() label1.set_text(_("Keyboard layouts: ")) label1.set_halign(Gtk.Align.END) grid.add(label1) prevlabel = label1 label = Gtk.Label() keyboardlayout = "" for kb in keyboards: if 'name' in kb: if keyboardlayout != "": keyboardlayout = keyboardlayout + "\n" keyboardlayout = keyboardlayout + kb['name'] label.set_text(keyboardlayout) label.set_halign(Gtk.Align.START) label.set_selectable(True) grid.attach_next_to(label, label1, Gtk.PositionType.RIGHT, 1, 1) fonts = get_fonts(files) if fonts: label2 = Gtk.Label() # Fonts are optional label2.set_text(_("Fonts: ")) label2.set_halign(Gtk.Align.END) grid.attach_next_to(label2, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) prevlabel = label2 label = Gtk.Label() fontlist = "" for font in fonts: if 'description' in font: if fontlist != "": fontlist = fontlist + "\n" if font['description'][:5] == "Font ": fontdesc = font['description'][5:] else: fontdesc = font['description'] fontlist = fontlist + fontdesc label.set_text(fontlist) label.set_halign(Gtk.Align.START) label.set_selectable(True) grid.attach_next_to(label, label2, Gtk.PositionType.RIGHT, 1, 1) label3 = Gtk.Label() label3.set_text(_("Package version: ")) label3.set_halign(Gtk.Align.END) grid.attach_next_to(label3, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) prevlabel = label3 label = Gtk.Label() description = secure_lookup(info, 'version', 'description') if description: label.set_text(description) label.set_halign(Gtk.Align.START) label.set_selectable(True) grid.attach_next_to(label, label3, Gtk.PositionType.RIGHT, 1, 1) author = secure_lookup(info, 'author') if author: author = author label4 = Gtk.Label() label4.set_text(_("Author: ")) label4.set_halign(Gtk.Align.END) grid.attach_next_to(label4, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) prevlabel = label4 label = Gtk.Label() if secure_lookup(author, 'url') and secure_lookup(author, 'description'): label.set_markup( "" + author['description'] + "") elif secure_lookup(author, 'description'): label.set_text(author['description']) label.set_halign(Gtk.Align.START) label.set_selectable(True) grid.attach_next_to(label, label4, Gtk.PositionType.RIGHT, 1, 1) if secure_lookup(info, 'website'): label5 = Gtk.Label() # Website is optional and may be a mailto for the author label5.set_text(_("Website: ")) label5.set_halign(Gtk.Align.END) grid.attach_next_to(label5, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) prevlabel = label5 label = Gtk.Label() website_description = secure_lookup(info, 'website', 'description') if website_description: label.set_markup( "" + website_description + "") label.set_halign(Gtk.Align.START) label.set_selectable(True) grid.attach_next_to(label, label5, Gtk.PositionType.RIGHT, 1, 1) if secure_lookup(info, 'copyright'): label6 = Gtk.Label() label6.set_text(_("Copyright: ")) label6.set_halign(Gtk.Align.END) grid.attach_next_to(label6, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) label = Gtk.Label() copyright_description = secure_lookup(info, 'copyright', 'description') if copyright_description: label.set_text(copyright_description) label.set_halign(Gtk.Align.START) label.set_selectable(True) grid.attach_next_to(label, label6, Gtk.PositionType.RIGHT, 1, 1) self.page2 = Gtk.Box() webview = WebKit2.WebView() webview.connect("decide-policy", self.doc_policy) readmeFile = secure_lookup(options, 'readmeFile') if readmeFile: self.readme = readmeFile else: self.readme = "noreadme" readme_file = os.path.join(tmpdirname, self.readme) if os.path.isfile(readme_file): try: with open(readme_file, "r") as read_file: readme_data = read_file.read() readme_added = True readme_uri = pathlib.Path(readme_file).as_uri() logging.debug(readme_data) webview.load_html(readme_data, readme_uri) s = Gtk.ScrolledWindow() s.add(webview) self.page2.pack_start(s, True, True, 0) self.notebook = Gtk.Notebook() self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM) mainhbox.pack_start(self.notebook, True, True, 0) self.notebook.append_page( self.page1, Gtk.Label(_('Details'))) self.notebook.append_page( self.page2, Gtk.Label(_('README'))) except UnicodeDecodeError: readme_added = False else: readme_added = False if not readme_added: mainhbox.pack_start(self.page1, True, True, 0) self.get_content_area().pack_start(mainhbox, True, True, 0) hbox = Gtk.Box(spacing=6) self.get_content_area().pack_start(hbox, False, False, 0) button = Gtk.Button.new_with_mnemonic(_("_Install")) button.connect("clicked", self.on_install_clicked) hbox.pack_start(button, False, False, 0) button = Gtk.Button.new_with_mnemonic(_("_Cancel")) button.connect("clicked", self.on_cancel_clicked) hbox.pack_end(button, False, False, 0) bind_accelerator(self.accelerators, button, 'w') self.resize(800, 450) self.show_all() def run(self): if self.checkcontinue: return Gtk.Dialog.run(self) return Gtk.ResponseType.CANCEL def doc_policy(self, web_view, decision, decision_type): logging.info("Checking policy") logging.debug("received policy decision request of type: {0}".format(decision_type.value_name)) if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION: nav_action = decision.get_navigation_action() request = nav_action.get_request() uri = request.get_uri() logging.debug("nav request is for uri %s", uri) if self.readme not in uri: logging.debug("opening uri %s in webbrowser") webbrowser.open(uri) decision.ignore() return True return False def on_install_clicked(self, button): logging.info("Installing keyboard") try: result = install_kmp(self.kmpfile, self.online, language=self.language) if result: # If install_kmp returns a string, it is an instruction for the end user, # because for fcitx they will need to take extra steps to complete # installation themselves. dialog = Gtk.MessageDialog( self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, result) dialog.run() dialog.destroy() if is_fcitx_running(): subprocess.run(['fcitx5-configtool']) if self.viewwindow: self.viewwindow.refresh_installed_kmp() keyboardid = os.path.basename(os.path.splitext(self.kmpfile)[0]) welcome_file = os.path.join(get_keyboard_dir(InstallLocation.User, keyboardid), "welcome.htm") if os.path.isfile(welcome_file): uri_path = pathlib.Path(welcome_file).as_uri() logging.debug(uri_path) w = WelcomeView(self, uri_path, self.kbname, True) w.run() w.destroy() else: dialog = Gtk.MessageDialog( self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, _("Keyboard {name} installed").format(name=self.kbname)) dialog.run() dialog.destroy() except InstallError as e: self._handle_install_error(e, self.kbname) self.close() def on_cancel_clicked(self, button): logging.info("Cancel install keyboard") self.response(Gtk.ResponseType.CANCEL) def _handle_install_error(self, e, kbname): if e.status == InstallStatus.Abort: message = _("Keyboard {name} could not be installed.").format(name=kbname) \ + "\n\n" + _("Error Message:") + "\n %s" % (e.message) logging.error(message) message_type = Gtk.MessageType.ERROR else: message = _("Keyboard {name} could not be installed.").format(name=kbname) \ + "\n\n" + _("Warning Message:") + "\n %s" % (e.message) logging.warning(message) message_type = Gtk.MessageType.WARNING dialog = Gtk.MessageDialog( self, 0, message_type, Gtk.ButtonsType.OK, message) dialog.run() dialog.destroy() def main(argv): if len(sys.argv) != 2: logging.error("install_window.py ") sys.exit(2) name, ext = os.path.splitext(sys.argv[1]) if ext != ".kmp": logging.error("install_window.py Input file", sys.argv[1], "is not a kmp file.") logging.error("install_window.py ") sys.exit(2) if not os.path.isfile(sys.argv[1]): logging.error("install_window.py Keyman kmp file", sys.argv[1], "not found.") logging.error("install_window.py ") sys.exit(2) w = InstallKmpWindow(sys.argv[1]) w.run() w.destroy() if __name__ == "__main__": main(sys.argv[1:])