File: __init__.py

package info (click to toggle)
python-pywebview 2.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 1,376 kB
  • sloc: python: 3,816; cs: 116; makefile: 3
file content (393 lines) | stat: -rwxr-xr-x 12,510 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
pywebview is a lightweight cross-platform wrapper around a webview component that allows to display HTML content in its
own dedicated window. Works on Windows, OS X and Linux and compatible with Python 2 and 3.

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

http://github.com/r0x0r/pywebview/
"""


import json
import logging
import os
import platform
import re
import sys
from threading import Event, Thread, current_thread
from uuid import uuid4
from functools import wraps

from webview.util import base_uri, parse_file_type, escape_string, transform_url, make_unicode, escape_line_breaks, inject_base_uri
from .js import css
from .localization import localization


logger = logging.getLogger('pywebview')
handler = logging.StreamHandler()
formatter = logging.Formatter('[pywebview] %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

log_level = logging.DEBUG if os.environ.get('PYWEBVIEW_LOG') == 'debug' else logging.INFO
logger.setLevel(log_level)


OPEN_DIALOG = 10
FOLDER_DIALOG = 20
SAVE_DIALOG = 30


class Config (dict):

    def __init__(self):
        self.use_qt = 'USE_QT' in os.environ
        self.use_win32 = 'USE_WIN32' in os.environ

        self.gui = 'qt' if 'KDE_FULL_SESSION' in os.environ else None
        self.gui = os.environ['PYWEBVIEW_GUI'].lower() \
            if 'PYWEBVIEW_GUI' in os.environ and os.environ['PYWEBVIEW_GUI'].lower() in ['qt', 'gtk', 'win32', 'cef'] \
            else None

    def __getitem__(self, key):
        return getattr(self, key.lower())

    def __setitem__(self, key, value):
        setattr(self, key.lower(), value)


config = Config()

_initialized = False
_webview_ready = Event()


def _initialize_imports():
    def import_gtk():
        global gui

        try:
            import webview.gtk as gui
            logger.debug('Using GTK')

            return True
        except (ImportError, ValueError) as e:
            logger.exception('GTK cannot be loaded')

            return False

    def import_qt():
        global gui

        try:
            import webview.qt as gui

            return True
        except ImportError as e:
            logger.exception('QT cannot be loaded')
            return False

    def import_cocoa():
        global gui

        try:
            import webview.cocoa as gui

            return True
        except ImportError:
            logger.exception('PyObjC cannot be loaded')

            return False

    def import_win32():
        global gui

        try:
            import webview.win32 as gui
            logger.debug('Using Win32')

            return True
        except ImportError as e:
            logger.exception('PyWin32 cannot be loaded')
            return False

    def import_winforms():
        global gui

        try:
            import webview.winforms as gui
            logger.debug('Using .NET')

            return True
        except ImportError as e:
            logger.exception('pythonnet cannot be loaded')
            return False

    def try_import(guis):
        while guis:
            import_func = guis.pop(0)

            if import_func():
                return True

        return False

    global _initialized

    if not _initialized:
        if platform.system() == 'Darwin':
            if config.gui == 'qt' or config.use_qt:
                guis = [import_qt, import_cocoa]
            else:
                guis = [import_cocoa, import_qt]

            if not try_import(guis):
                raise Exception('You must have either PyObjC (for Cocoa support) or Qt with Python bindings installed in order to use pywebview.')

        elif platform.system() == 'Linux' or platform.system() == 'OpenBSD':
            if config.gui == 'gtk' or config.gui != 'qt' and not config.use_qt:
                guis = [import_gtk, import_qt]
            else:
                guis = [import_qt, import_gtk]

            if not try_import(guis):
                raise Exception('You must have either QT or GTK with Python extensions installed in order to use pywebview.')

        elif platform.system() == 'Windows':
            if config.gui == 'win32' or config.use_win32:
                guis = [import_win32, import_winforms]
            else:
                guis = [import_winforms, import_win32]

            if not try_import(guis):
                raise Exception('You must have either pythonnet or pywin32 installed in order to use pywebview.')
        else:
            raise Exception('Unsupported platform. Only Windows, Linux, OS X, OpenBSD are supported.')

        _initialized = True


def _api_call(function):
    """
    Decorator to call a pywebview API, checking for _webview_ready and raisings
    appropriate Exceptions on failure.
    """
    @wraps(function)
    def wrapper(*args, **kwargs):
        try:
            _webview_ready.wait(5)
            return function(*args, **kwargs)
        except NameError:
            raise Exception('Create a web view window first, before invoking this function')
        except KeyError as e:
            try:
                uid = kwargs['uid']
            except KeyError:
                # uid not passed as a keyword arg, assumes it to be last in the arg list
                uid = args[-1]
            raise Exception('Cannot call function: No webview exists with uid: {}'.format(uid))
    return wrapper


def create_window(title, url=None, js_api=None, width=800, height=600,
                  resizable=True, fullscreen=False, min_size=(200, 100), strings={}, confirm_quit=False,
                  background_color='#FFFFFF', text_select=False, debug=False):
    """
    Create a web view window using a native GUI. The execution blocks after this function is invoked, so other
    program logic must be executed in a separate thread.
    :param title: Window title
    :param url: URL to load
    :param width: window width. Default is 800px
    :param height:window height. Default is 600px
    :param resizable True if window can be resized, False otherwise. Default is True
    :param fullscreen: True if start in fullscreen mode. Default is False
    :param min_size: a (width, height) tuple that specifies a minimum window size. Default is 200x100
    :param strings: a dictionary with localized strings
    :param confirm_quit: Display a quit confirmation dialog. Default is False
    :param background_color: Background color as a hex string that is displayed before the content of webview is loaded. Default is white.
    :param text_select: Allow text selection on page. Default is False.
    :return: The uid of the created window.
    """

    valid_color = r'^#(?:[0-9a-fA-F]{3}){1,2}$'
    if not re.match(valid_color, background_color):
        raise ValueError('{0} is not a valid hex triplet color'.format(background_color))

    # Check if starting up from main thread; if not, wait; finally raise exception
    if current_thread().name == 'MainThread':
        uid = 'master'

        if not _initialized:
            _initialize_imports()
            localization.update(strings)
    else:
        uid = 'child_' + uuid4().hex[:8]
        if not _webview_ready.wait(5):
            raise Exception('Call create_window from the main thread first')

    _webview_ready.clear()  # Make API calls wait while the new window is created
    gui.create_window(uid, make_unicode(title), transform_url(url),
                      width, height, resizable, fullscreen, min_size, confirm_quit,
                      background_color, debug, js_api, text_select, _webview_ready)

    if uid == 'master':
        _webview_ready.clear()
    else:
        return uid


@_api_call
def create_file_dialog(dialog_type=OPEN_DIALOG, directory='', allow_multiple=False, save_filename='', file_types=()):
    """
    Create a file dialog
    :param dialog_type: Dialog type: open file (OPEN_DIALOG), save file (SAVE_DIALOG), open folder (OPEN_FOLDER). Default
                        is open file.
    :param directory: Initial directory
    :param allow_multiple: Allow multiple selection. Default is false.
    :param save_filename: Default filename for save file dialog.
    :param file_types: Allowed file types in open file dialog. Should be a tuple of strings in the format:
        filetypes = ('Description (*.extension[;*.extension[;...]])', ...)
    :return: A tuple of selected files, None if cancelled.
    """
    if type(file_types) != tuple and type(file_types) != list:
        raise TypeError('file_types must be a tuple of strings')
    for f in file_types:
        parse_file_type(f)

    if not os.path.exists(directory):
        directory = ''

    return gui.create_file_dialog(dialog_type, directory, allow_multiple, save_filename, file_types)


@_api_call
def load_url(url, uid='master'):
    """
    Load a new URL into a previously created WebView window. This function must be invoked after WebView windows is
    created with create_window(). Otherwise an exception is thrown.
    :param url: url to load
    :param uid: uid of the target instance
    """
    gui.load_url(url, uid)


@_api_call
def load_html(content, base_uri=base_uri(), uid='master'):
    """
    Load a new content into a previously created WebView window. This function must be invoked after WebView windows is
    created with create_window(). Otherwise an exception is thrown.
    :param content: Content to load.
    :param base_uri: Base URI for resolving links. Default is the directory of the application entry point.
    :param uid: uid of the target instance
    """
    content = make_unicode(content)
    gui.load_html(content, base_uri, uid)


@_api_call
def load_css(stylesheet, uid='master'):
    code = css.src % stylesheet.replace('\n', '').replace('\r', '').replace('"', "'")
    gui.evaluate_js(code, uid)


@_api_call
def set_title(title, uid='master'):
    """
    Sets a new title of the window
    """
    gui.set_title(title, uid)


@_api_call
def get_current_url(uid='master'):
    """
    Get the URL currently loaded in the target webview
    :param uid: uid of the target instance
    """
    return gui.get_current_url(uid)


@_api_call
def destroy_window(uid='master'):
    """
    Destroy a web view window
    :param uid: uid of the target instance
    """
    gui.destroy_window(uid)


@_api_call
def toggle_fullscreen(uid='master'):
    """
    Toggle fullscreen mode
    :param uid: uid of the target instance
    """
    gui.toggle_fullscreen(uid)


@_api_call
def set_window_size(width, height, uid='master'):
    """
    Set Window Size
    :param width: desired width of target window
    :param height: desired height of target window
    :param uid: uid of the target instance
    """
    gui.set_window_size(width, height, uid)


@_api_call
def evaluate_js(script, uid='master'):
    """
    Evaluate given JavaScript code and return the result
    :param script: The JavaScript code to be evaluated
    :param uid: uid of the target instance
    :return: Return value of the evaluated code
    """
    escaped_script = 'JSON.stringify(eval("{0}"))'.format(escape_string(script))
    return gui.evaluate_js(escaped_script, uid)


def window_exists(uid='master'):
    """
    Check whether a webview with the given UID is up and running
    :param uid: uid of the target instance
    :return: True if the window exists, False otherwise
    """
    try:
        get_current_url(uid)
        return True
    except:
        return False


def webview_ready(timeout=None):
    """
    :param timeout: optional timeout
    :return: True when the last opened window is ready. False if the timeout is reached, when the timeout parameter is provided.
    Until then blocks the calling thread.
    """
    return _webview_ready.wait(timeout)


def _js_bridge_call(uid, api_instance, func_name, param):
    def _call():
        result = json.dumps(func(func_params))
        code = 'window.pywebview._returnValues["{0}"] = {{ isSet: true, value: {1}}}'.format(func_name, escape_line_breaks(result))
        evaluate_js(code, uid)

    func = getattr(api_instance, func_name, None)

    if func is not None:
        try:
            func_params = param if not param else json.loads(param)
            t = Thread(target=_call)
            t.start()
        except Exception as e:
            logger.exception('Error occurred while evaluating function {0}'.format(func_name))
    else:
        logger.error('Function {}() does not exist'.format(func_name))