r0x0r / pywebview

Build GUI for your Python program with JavaScript, HTML, and CSS
https://pywebview.flowrl.com
BSD 3-Clause "New" or "Revised" License
4.71k stars 552 forks source link

Enabling copy-paste in CEF via cefpython browser_settings (proposal to make browser_settings configurable) #806

Closed Rolf-MP closed 2 years ago

Rolf-MP commented 2 years ago

Specification

Description

We have been trying to make copy-paste work with pywebview/CEF (for a local application).

To cut a long story short:

  1. The new asynchronous clipboard api / permissions model is not supported yet in CEF (and certainly not in v66) See: https://www.magpcss.org/ceforum/viewtopic.php?f=6&t=18097
  2. So we need to fall back to executeCommand('copy') and executeCommand('paste').
  3. In order to make the latter work we need to pass a browser_setting "dom_paste_disabled": False to cefpython.
  4. pywebview's cef.py does not provide this option.
  5. Adding a settings to the cef.CreatBrowserSync in create_browser() in cef.py per the "isolated" code section below resolved the issue.
def create_browser(window, handle, alert_func):
    def _create():
        real_url = 'data:text/html,{0}'.format(window.html) if window.html else window.real_url or 'data:text/html,{0}'.format(default_html)

        browser_settings = {
            "dom_paste_disabled": False
        }
        cef_browser = cef.CreateBrowserSync(window_info=window_info, settings=browser_settings, url=real_url)

        browser = Browser(window, handle, cef_browser)

        bindings = cef.JavascriptBindings()
        bindings.SetObject('external', browser.js_bridge)
        bindings.SetFunction('alert', alert_func)

        cef_browser.SetJavascriptBindings(bindings)
        cef_browser.SetClientHandler(LoadHandler())

        instances[window.uid] = browser
        window.shown.set()

    window_info = cef.WindowInfo()
    window_info.SetAsChild(handle)
    cef.PostTask(cef.TID_UI, _create)
  1. As there are many more relevant browser settings it seems obvious to opt for a similar construct as in place already for making the (application) settings and command_line_switches configurable.

Practicalities

Put quite a bit of effort in tracking the issue down but I am not an expert in pywebview. Solution is proposed - would rather leave the (simple) implementation to a core contributor.

r0x0r commented 2 years ago

You can set CEF settings using this code snippet

from webview.platforms.cef import settings
settings.update({
    'dom_paste_disabled': False
})

Are there any drawbacks of enabling this feature? If it is a sensible one, it can be enabled by default.

Rolf-MP commented 2 years ago

You can set CEF settings using this code snippet

from webview.platforms.cef import settings
settings.update({
    'dom_paste_disabled': False
})

Are there any drawbacks of enabling this feature? If it is a sensible one, it can be enabled by default.

Thank you Roman! Will give that a try. I do think it is a security issue to enable this by default. It should not be an issue for local usage of a trusted webapp - but enabling it for generic online browsing would be unwise.

Rolf-MP commented 2 years ago

The webview.platforms.cef import settings gives us access to the application settings whilst the 'dom_paste_disabled': False is a browser setting.

The suggested code actually throws an error as dom_paste_disabled is not a setting for the application.

The same idea could be reused to set browser specific settings actually (that was the suggested approach in the issue).

Rolf-MP commented 2 years ago

This flies:

  1. At the top of webview/platforms/cef.py add browser_settings = {}:
import json
import logging
import os
import shutil
import sys
import webbrowser

from ctypes import windll
from functools import wraps
from uuid import uuid1
from threading import Event
from cefpython3 import cefpython as cef
from copy import copy
from time import sleep

from webview.js.css import disable_text_select
from webview.js import dom
from webview import _debug, _user_agent
from webview.util import parse_api_js, default_html, js_bridge_call

sys.excepthook = cef.ExceptHook
instances = {}

logger = logging.getLogger(__name__)

settings = {}

browser_settings = {}

command_line_switches = {}
  1. Do the same as with settingsand command_line_switches to add the browser_settings to CreateBrowserSync:
def create_browser(window, handle, alert_func):
    def _create():
        real_url = 'data:text/html,{0}'.format(window.html) if window.html else window.real_url or 'data:text/html,{0}'.format(default_html)

        default_browser_settings = {}
        all_browser_settings = dict(default_browser_settings, **browser_settings)

        cef_browser = cef.CreateBrowserSync(window_info=window_info, settings=browser_settings, url=real_url)
        browser = Browser(window, handle, cef_browser)

        bindings = cef.JavascriptBindings()
        bindings.SetObject('external', browser.js_bridge)
        bindings.SetFunction('alert', alert_func)

        cef_browser.SetJavascriptBindings(bindings)
        cef_browser.SetClientHandler(LoadHandler())

        instances[window.uid] = browser
        window.shown.set()

    window_info = cef.WindowInfo()
    window_info.SetAsChild(handle)
    cef.PostTask(cef.TID_UI, _create)

This then works nicely (under "live demo" the url has some tests for old and new clipboard functionality): (Notice that the demo thinks cef supports the new asynchronous clipboard API - I suspect that this id because navigator.clipboard can be instantiated - methods such as writeTextfail however.)

import webview
my_url = 'https://whatwebcando.today/clipboard.html'

from webview.platforms.cef import browser_settings
browser_settings.update({
    'dom_paste_disabled': False
})

webview.create_window('Hello world', my_url)
webview.start(gui='cef', http_server=True, debug=True)
github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] commented 2 years ago

The message to post on the issue when closing it. If none provided, will not comment when closing an issue.