getVideo/webview/platforms/gtk.py


Home Back

"""
(C) 2014-2019 Roman Sirokov and contributors
Licensed under BSD license

http://github.com/r0x0r/pywebview/
"""
import sys,os
import logging
import json
import webbrowser
try:
    from urllib.parse import unquote
except ImportError:
    from urllib import unquote

from uuid import uuid1
from threading import Event, Semaphore
from webview import _debug, _user_agent, OPEN_DIALOG, FOLDER_DIALOG, SAVE_DIALOG, parse_file_type, escape_string, windows
from webview.util import parse_api_js, default_html, js_bridge_call
from webview.js.css import disable_text_select
from webview.screen import Screen
from webview.window import FixPoint

logger = logging.getLogger('pywebview')

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('WebKit2', '4.0')

from gi.repository import Gtk as gtk
from gi.repository import Gdk
from gi.repository import GLib as glib
from gi.repository import WebKit2 as webkit


# version of WebKit2 older than 2.2 does not support returning a result of javascript, so we
# have to resort fetching a result via window title
webkit_ver = webkit.get_major_version(), webkit.get_minor_version(), webkit.get_micro_version()
old_webkit = webkit_ver[0] < 2 or webkit_ver[1] < 22

renderer = 'gtkwebkit2'

settings = {}

class BrowserView:
    instances = {}
    
    class JSBridge:
        def __init__(self, window):
            self.window = window
            self.uid = uuid1().hex[:8]

        def call(self, func_name, param, value_id):
            if param == 'undefined':
                param = None
            return js_bridge_call(self.window, func_name, param, value_id)
    
    
        
    
    def __init__(self, window):
        BrowserView.instances[window.uid] = self
        self.uid = window.uid
        self.pywebview_window = window

        self.is_fullscreen = False
        self.js_results = {}

        glib.threads_init()
        self.window = gtk.Window(title=window.title)
        
        if not window.icon == None:
           self.window.set_icon_from_file(window.icon)

        self.shown = window.events.shown
        self.loaded = window.events.loaded

        self.localization = window.localization

        self._last_width = window.initial_width
        self._last_height = window.initial_height

        if window.resizable:
            self.window.set_size_request(window.min_size[0], window.min_size[1])
            self.window.resize(window.initial_width, window.initial_height)
        else:
            self.window.set_size_request(window.initial_width, window.initial_height)

        if window.minimized:
            self.window.iconify()

        if window.initial_x is not None and window.initial_y is not None:
            self.move(window.initial_x, window.initial_y)
        else:
            self.window.set_position(gtk.WindowPosition.CENTER)

        self.window.set_resizable(window.resizable)

        # Set window background color
        style_provider = gtk.CssProvider()
        style_provider.load_from_data(
            'GtkWindow {{ background-color: {}; }}'.format(window.background_color).encode()
        )
        gtk.StyleContext.add_provider_for_screen(
            Gdk.Screen.get_default(),
            style_provider,
            gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        scrolled_window = gtk.ScrolledWindow()
        self.window.add(scrolled_window)

        if window.confirm_close:
            self.window.connect('delete-event', self.on_destroy)
        else:
            self.window.connect('delete-event', self.close_window)

        self.window.connect('window-state-event', self.on_window_state_change)
        self.window.connect('size-allocate', self.on_window_resize)

        self.js_bridge = BrowserView.JSBridge(window)
        self.text_select = window.text_select

        self.webview = webkit.WebView()
        self.webview.connect('notify::visible', self.on_webview_ready)
        self.webview.connect('load_changed', self.on_load_finish)
        self.webview.connect('notify::title', self.on_title_change)
        self.webview.connect('decide-policy', self.on_navigation)

        user_agent = settings.get('user_agent') or _user_agent
        if user_agent:
            self.webview.get_settings().props.user_agent = user_agent

        if window.frameless:
            self.window.set_decorated(False)
            if window.easy_drag:
                self.move_progress = False
                self.webview.connect('button-release-event', self.on_mouse_release)
                self.webview.connect('button-press-event', self.on_mouse_press)
                self.window.connect('motion-notify-event', self.on_mouse_move)

        if window.on_top:
            self.window.set_keep_above(True)

        self.transparent = window.transparent
        if window.transparent:
            configure_transparency(self.window)
            configure_transparency(self.webview)
            wvbg = self.webview.get_background_color()
            wvbg.alpha = 0.0
            self.webview.set_background_color(wvbg)

        if _debug['mode']:
            self.webview.get_settings().props.enable_developer_extras = True
        else:
            self.webview.connect('context-menu', lambda a,b,c,d: True) # Disable context menu

        self.webview.set_opacity(0.0)
        scrolled_window.add(self.webview)

        if window.real_url is not None:
            self.webview.load_uri(window.real_url)
        elif window.html:
            self.webview.load_html(window.html, '')
        else:
            self.webview.load_html(default_html, '')

        if window.fullscreen:
            self.toggle_fullscreen()

        iconpath=os.path.join(os.path.dirname(sys.argv[0]),"icon.png") 
        if os.path.exists(iconpath):
            self.window.set_icon_from_file(iconpath)
    
        
    def close_window(self, *data):
        should_cancel = self.pywebview_window.events.closing.set()

        if should_cancel:
            return

        for res in self.js_results.values():
            res['semaphore'].release()

        while gtk.events_pending():
            gtk.main_iteration()

        self.window.destroy()
        del BrowserView.instances[self.uid]

        if self.pywebview_window in windows:
            windows.remove(self.pywebview_window)

        self.pywebview_window.events.closed.set()

        if BrowserView.instances == {}:
            gtk.main_quit()

    def on_destroy(self, widget=None, *data):
        dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DialogFlags.MODAL & gtk.DialogFlags.DESTROY_WITH_PARENT,
                                          type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL,
                                          message_format=self.localization['global.quitConfirmation'])
        result = dialog.run()
        if result == gtk.ResponseType.OK:
            self.close_window()

        dialog.destroy()
        return True

    def on_window_state_change(self, window, window_state):
        if window_state.changed_mask == Gdk.WindowState.ICONIFIED:

            if Gdk.WindowState.ICONIFIED & window_state.new_window_state == Gdk.WindowState.ICONIFIED:
                self.pywebview_window.events.minimized.set()
            else:
                self.pywebview_window.events.restored.set()

        elif window_state.changed_mask == Gdk.WindowState.MAXIMIZED:

            if Gdk.WindowState.MAXIMIZED & window_state.new_window_state == Gdk.WindowState.MAXIMIZED:
                self.pywebview_window.events.maximized.set()
            else:
                self.pywebview_window.events.restored.set()

    def on_window_resize(self, window, allocation):

        if allocation.width != self._last_width or allocation.height != self._last_height:
            self._last_width = allocation.width
            self._last_height = allocation.height
            self.pywebview_window.events.resized.set(allocation.width, allocation.height)

    def on_webview_ready(self, arg1, arg2):
        # in webkit2 notify:visible fires after the window was closed and BrowserView object destroyed.
        # for a lack of better solution we check that BrowserView has 'webview_ready' attribute
        if 'shown' in dir(self):
            self.shown.set()


    def on_load_finish(self, webview, status):
        # Show the webview if it's not already visible
        if not webview.props.opacity:
            glib.idle_add(webview.set_opacity, 1.0)

        if status == webkit.LoadEvent.FINISHED:
            if not self.text_select:
                webview.run_javascript(disable_text_select)
            self._set_js_api()

    def on_title_change(self, webview, title):
        title = webview.get_title()

        try:
            js_data = json.loads(title)

            if 'type' not in js_data:
                return

            elif js_data['type'] == 'eval' and old_webkit:  # return result of evaluate_js
                unique_id = js_data['uid']
                result = js_data['result'] if 'result' in js_data else None

                js = self.js_results[unique_id]
                js['result'] = result
                js['semaphore'].release()

            elif js_data['type'] == 'invoke':  # invoke js api's function
                func_name = js_data['function']
                value_id = js_data['id']
                param = js_data['param'] if 'param' in js_data else None
                return_val = self.js_bridge.call(func_name, param, value_id)

                # Give back the return value to JS as a string
                code = 'pywebview._bridge.return_val = "{0}";'.format(escape_string(str(return_val)))
                webview.run_javascript(code)

        except ValueError: # Python 2
            return
        except json.JSONDecodeError: # Python 3
            return

    def on_navigation(self, webview, decision, decision_type):
        if type(decision) == webkit.NavigationPolicyDecision:
            uri = decision.get_request().get_uri()

            if decision.get_frame_name() == '_blank':
                webbrowser.open(uri, 2, True)
                decision.ignore()

    def on_mouse_release(self, sender, event):
        self.move_progress = False

    def on_mouse_press(self, _, event):
        self.point_diff = [x - y for x, y in zip(self.window.get_position(), [event.x_root, event.y_root])]
        self.move_progress = True

    def on_mouse_move(self, _, event):
        if self.move_progress:
            point = [x + y for x, y in zip((event.x_root, event.y_root), self.point_diff)]
            self.window.move(point[0], point[1])

    def show(self):
        self.window.show_all()

        if gtk.main_level() == 0:
            if self.pywebview_window.hidden:
                self.window.hide()
            gtk.main()
        else:
            glib.idle_add(self.window.show_all)

    def hide(self):
        glib.idle_add(self.window.hide)
    
    

    def destroy(self):
        self.window.emit('delete-event', Gdk.Event())

    def set_icon_from_file(self, path):
        """Set a icon for window """
        self.window.set_icon_from_file(path)
        
    def set_title(self, title):
        self.window.set_title(title)

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.window.unfullscreen()
        else:
            self.window.fullscreen()

        self.is_fullscreen = not self.is_fullscreen

    def resize(self, width, height, fix_point):
        if fix_point & FixPoint.NORTH and fix_point & FixPoint.WEST:
            self.window.set_gravity(Gdk.Gravity.NORTH_WEST)
        elif fix_point & FixPoint.NORTH and fix_point & FixPoint.EAST:
            self.window.set_gravity(Gdk.Gravity.NORTH_EAST)
        elif fix_point & FixPoint.SOUTH and fix_point & FixPoint.EAST:
            self.window.set_gravity(Gdk.Gravity.SOUTH_EAST)
        elif fix_point & FixPoint.SOUTH and fix_point & FixPoint.WEST:
            self.window.set_gravity(Gdk.Gravity.SOUTH_WEST)
        elif fix_point & FixPoint.SOUTH:
            self.window.set_gravity(Gdk.Gravity.SOUTH)
        elif fix_point & FixPoint.NORTH:
            self.window.set_gravity(Gdk.Gravity.NORTH)
        elif fix_point & FixPoint.WEST:
            self.window.set_gravity(Gdk.Gravity.WEST)
        elif fix_point & FixPoint.EAST:
            self.window.set_gravity(Gdk.Gravity.EAST)

        self.window.resize(width, height)

    def move(self, x, y):
        self.window.move(x, y)

    def minimize(self):
        glib.idle_add(self.window.iconify)

    def restore(self):
        def _restore():
            self.window.deiconify()
            self.window.present()

        glib.idle_add(_restore)

    def create_file_dialog(self, dialog_type, directory, allow_multiple, save_filename, file_types):
        if dialog_type == FOLDER_DIALOG:
            gtk_dialog_type = gtk.FileChooserAction.SELECT_FOLDER
            title = self.localization['linux.openFolder']
            button = gtk.STOCK_OPEN
        elif dialog_type == OPEN_DIALOG:
            gtk_dialog_type = gtk.FileChooserAction.OPEN
            if allow_multiple:
                title = self.localization['linux.openFiles']
            else:
                title = self.localization['linux.openFile']

            button = gtk.STOCK_OPEN
        elif dialog_type == SAVE_DIALOG:
            gtk_dialog_type = gtk.FileChooserAction.SAVE
            title = self.localization['global.saveFile']
            button = gtk.STOCK_SAVE

        dialog = gtk.FileChooserDialog(title, self.window, gtk_dialog_type,
                                       (gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL, button, gtk.ResponseType.OK))

        dialog.set_select_multiple(allow_multiple)
        dialog.set_current_folder(directory)
        self._add_file_filters(dialog, file_types)

        if dialog_type == SAVE_DIALOG:
            dialog.set_current_name(save_filename)

        response = dialog.run()

        if response == gtk.ResponseType.OK:
            if dialog_type == SAVE_DIALOG:
                file_name = dialog.get_filename()
            else:
                file_name = dialog.get_filenames()
        else:
            file_name = None

        dialog.destroy()

        return file_name

    def _add_file_filters(self, dialog, file_types):
        for s in file_types:
            description, extensions = parse_file_type(s)

            f = gtk.FileFilter()
            f.set_name(description)
            for e in extensions.split(';'):
                f.add_pattern(e)

            dialog.add_filter(f)

    def get_current_url(self):
        self.loaded.wait()
        uri = self.webview.get_uri()
        return uri if uri != 'about:blank' else None

    def load_url(self, url):
        self.loaded.clear()
        self.webview.load_uri(url)

    def load_html(self, content, base_uri):
        self.loaded.clear()
        self.webview.load_html(content, base_uri)

    def evaluate_js(self, script):
        def _evaluate_js():
            callback = None if old_webkit else _callback
            self.webview.run_javascript(script, None, callback, None)

        def _callback(webview, task, data):
            value = webview.run_javascript_finish(task)
            result = value.get_js_value().to_string() if value else None

            if unique_id in self.js_results:
                self.js_results[unique_id]['result'] = result

            result_semaphore.release()

        unique_id = uuid1().hex
        result_semaphore = Semaphore(0)
        self.js_results[unique_id] = {'semaphore': result_semaphore, 'result': None}

        if old_webkit:
            script = 'document.title = JSON.stringify({{"type": "eval", "uid": "{0}", "result": {1}}})'.format(unique_id, script)

        self.loaded.wait()
        glib.idle_add(_evaluate_js)
        result_semaphore.acquire()

        if not gtk.main_level():
            # Webview has been closed, don't proceed
            return None

        result = self.js_results[unique_id]['result']
        result = None if result == 'undefined' or result == 'null' or result is None else result if result == '' else json.loads(result)

        del self.js_results[unique_id]

        return result

    def _set_js_api(self):
        def create_bridge():
            self.webview.run_javascript(parse_api_js(self.js_bridge.window, 'gtk', uid=self.js_bridge.uid))
            self.loaded.set()

        glib.idle_add(create_bridge)


def create_window(window):
    def create():
        browser = BrowserView(window)
        browser.show()

    if window.uid == 'master':
        create()
    else:
        glib.idle_add(create)


def set_title(title, uid):
    def _set_title():
        BrowserView.instances[uid].set_title(title)
    glib.idle_add(_set_title)


def destroy_window(uid):
    def _destroy_window():
        BrowserView.instances[uid].close_window()
    glib.idle_add(_destroy_window)


def toggle_fullscreen(uid):
    def _toggle_fullscreen():
        BrowserView.instances[uid].toggle_fullscreen()
    glib.idle_add(_toggle_fullscreen)


def set_on_top(uid, top):
    def _set_on_top():
        BrowserView.instances[uid].window.set_keep_above(top)

    glib.idle_add(_set_on_top)


def resize(width, height, uid, fix_point):
    def _resize():
        BrowserView.instances[uid].resize(width, height, fix_point)
    glib.idle_add(_resize)


def move(x, y, uid):
    def _move():
        BrowserView.instances[uid].move(x, y)
    glib.idle_add(_move)


def hide(uid):
    glib.idle_add(BrowserView.instances[uid].hide)


def show(uid):
    glib.idle_add(BrowserView.instances[uid].show)


def minimize(uid):
    glib.idle_add(BrowserView.instances[uid].minimize)


def restore(uid):
    glib.idle_add(BrowserView.instances[uid].restore)


def get_current_url(uid):
    def _get_current_url():
        result['url'] = BrowserView.instances[uid].get_current_url()
        semaphore.release()

    result = {}
    semaphore = Semaphore(0)

    glib.idle_add(_get_current_url)
    semaphore.acquire()

    return result['url']


def load_url(url, uid):
    def _load_url():
        BrowserView.instances[uid].load_url(url)
    glib.idle_add(_load_url)


def load_html(content, base_uri, uid):
    def _load_html():
        BrowserView.instances[uid].load_html(content, base_uri)
    glib.idle_add(_load_html)


def create_file_dialog(dialog_type, directory, allow_multiple, save_filename, file_types, uid):
    i = BrowserView.instances[uid]
    file_name_semaphore = Semaphore(0)
    file_names = []

    def _create():
        result = i.create_file_dialog(dialog_type, directory, allow_multiple, save_filename, file_types)
        if result is None:
            file_names.append(None)
        else:
            file_names.append(tuple(result))

        file_name_semaphore.release()

    glib.idle_add(_create)
    file_name_semaphore.acquire()

    return file_names[0]


def evaluate_js(script, uid):
    return BrowserView.instances[uid].evaluate_js(script)


def get_position(uid):
    def _get_position():
        result['position'] = BrowserView.instances[uid].window.get_position()
        semaphore.release()

    result = {}
    semaphore = Semaphore(0)

    glib.idle_add(_get_position)
    semaphore.acquire()

    return result['position']


def get_size(uid):
    def _get_size():
        result['size'] = BrowserView.instances[uid].window.get_size()
        semaphore.release()

    result = {}
    semaphore = Semaphore(0)

    glib.idle_add(_get_size)
    semaphore.acquire()

    return result['size']

def get_screens():
    screen = Gdk.Screen.get_default()
    n = screen.get_n_monitors()
    geometries = [screen.get_monitor_geometry(i) for i in range(n)]
    screens = [Screen(geom.width, geom.height) for geom in geometries]

    return screens


def configure_transparency(c):
    c.set_visual(c.get_screen().get_rgba_visual())
    c.override_background_color(gtk.StateFlags.ACTIVE, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.BACKDROP, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.DIR_LTR, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.DIR_RTL, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.FOCUSED, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.INCONSISTENT, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.INSENSITIVE, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.PRELIGHT, Gdk.RGBA(0, 0, 0, 0))
    c.override_background_color(gtk.StateFlags.SELECTED, Gdk.RGBA(0, 0, 0, 0))
    transparentWindowStyleProvider = gtk.CssProvider()
    transparentWindowStyleProvider.load_from_data(b"""
        GtkWindow {
            background-color:rgba(0,0,0,0);
            background-image:none;
        }""")
    c.get_style_context().add_provider(transparentWindowStyleProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

Powered by Code, a simple repository browser by Fabio Di Matteo