├── .github
├── ISSUE_TEMPLATE
└── PULL_REQUEST_TEMPLATE
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── JAK
├── Application.py
├── DevTools.py
├── IPC.py
├── KeyBindings.py
├── RequestInterceptor.py
├── Settings.py
├── Utils.py
├── WebEngine.py
├── Widgets.py
└── __init__.py
├── LICENSE
├── MANIFEST.in
├── README.md
├── bin
├── JAK
└── jak-cli
├── contributing.md
├── docs
├── Application.html
├── DevTools.html
├── IPC.html
├── KeyBindings.html
├── RequestInterceptor.html
├── Settings.html
├── Utils.html
├── WebEngine.html
├── Widgets.html
├── __init__.html
└── pycco.css
├── requirements.txt
├── setup.cfg
└── setup.py
/.github/ISSUE_TEMPLATE:
--------------------------------------------------------------------------------
1 | ## FEATURE REQUEST: Dear santa for this Christmas i will like?
2 |
3 | #### Detailed Description
4 |
5 |
6 |
7 | #### Context
8 |
9 |
10 |
11 |
12 | ## Possible Implementation
13 |
14 |
15 |
16 | =======================================================
17 |
18 |
19 | ## BUGS
20 |
21 |
22 |
23 | #### Context
24 |
25 |
26 |
27 | #### Expected Behavior
28 |
29 |
30 |
31 | #### Actual Behavior
32 |
33 |
34 |
35 | #### Possible Fix
36 |
37 |
38 |
39 | #### Steps to Reproduce
40 |
41 |
42 | 1.
43 | 2.
44 | 3.
45 | 4.
46 |
47 |
48 | #### Context
49 |
50 |
51 |
52 | #### Your Environment
53 |
54 |
55 |
56 | Pygi version:
57 | python version:
58 | Linux distro:
59 | Link to your project:
60 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE:
--------------------------------------------------------------------------------
1 | ## Reference to a related issue in your repository.
2 |
3 |
4 | ## Description of the changes proposed in the pull request
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *,cover
45 |
46 | # Translations
47 | *.mo
48 | *.pot
49 |
50 | # Django stuff:
51 | *.log
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # PyBuilder
57 | target/
58 |
59 | # JAKTesting
60 | jak
61 | index.html
62 | dist/
63 | test.py
64 | .idea/
65 | MANIFEST
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | language: python
3 | python:
4 | - '3.6'
5 | - "3.7"
6 | - "3.8"
7 | - "3.9"
8 | notifications:
9 | webhooks:
10 | urls:
11 | - https://webhooks.gitter.im/e/9da87f9fb77e9c13620d
12 | on_success: change
13 | on_failure: always
14 | on_start: never
15 | cache:
16 | - pip
17 | install:
18 | - pip install -r requirements.txt
19 | deploy:
20 | provider: pypi
21 | user: codesardine
22 | password:
23 | secure: etAjJfwFBg8/gzzV9ZkiBXQyDnDzG5QevKy/4SmGeXphl/USCIpBuXH0kxvVkE+DFv6OdiW2eiJk/xFP5LZpA6P9AgJtWmgqoKIkF/0oh/fRHzqwIXgYJJMQpq4ZrwmEEYYNuU5oQaz8wOu/g9EaJ2bo3ACXjsl0PsPyZv7+QBzoV6y3lfnuOYgfcxu3H0OYCfLz2cHER3747n5Fm/9OZZ2LpAgJks9a6AeYujrDNt+7GNIp8LT5W2VIdBCwz3RVazQwHpCHREgoGfVzRWz3Rj/hXUcQygY+ogEUjKMHHJfePuoX+MYP6MMYtvOtUH/P96ltO5kv8Bx8SDzet1HYV2fLNnIiuV5cTA4ChhLD1bdcS3qoom+gwJikY/zgc6gNz0qqzBwnTYJa/0ZcNf+Tw6NrO3Kkd0jE5dfx/di2xrTSyL7lbXxGSKo1ELlbgm+nPdegGp0aTje1Qe2SWFWqcqyB5eYc0B1/4V1m4iORaEsbUEEDLTm+N5PBXOxJ5lXgrvHmgz+N5CzbKYMHoAreuqFo3/vUDf7dVuSAHdnCfZ1N2x2YDZxrSn9gxpStTaE/XvtxM+1VNX55X8+cR/BmkS37avDKM1rASwTpCVwXnZCrRYMwFEBmTMQXjhg/uGX81RZ55Q8chiLTKELSFTRbDOIk87GEdm4019eR1fJJZB8=
24 | distributions: sdist bdist_wheel
25 | on:
26 | tags: true
27 | python: 3.8
28 | script: nosetests
29 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | I would love for you to contribute to JAK and help making it better! Here are some guidelines I would like you to follow:
2 |
3 | ### Developing
4 |
5 | * Join [Gitter chat](https://gitter.im/JustAnotherDesktopEnviroment/Lobby), it will make communication easier.
6 |
7 | * When contributing to this repository, please first discuss the change you wish to make via issue.
8 |
9 | * Try and follow pep8 when you can.
10 |
11 | * Make sure you test your changes.
12 |
13 | * Don't break existing functionality I try to maintain the Master Branch in a working order.
14 |
15 | ## Pull Request Process
16 |
17 | * Ensure you create separate pull requests on another branch for each issue or feature, that will accelerate the merging process without interfering with other issues, once I have tested the code I will merge back.
18 |
19 | ## Adding a wrapper
20 | clone the repository
21 | ```
22 | git checkout --orphan wrapper/my-wrapper-name-online
23 | git rm -rf .
24 | ```
25 | Commit your wrapper
26 | ```
27 | git push -u origin wrapper/my-wrapper-name-online
28 | ```
29 | All done send pull request
30 |
31 | ### Our Pledge
32 |
33 | In the interest of fostering an open and welcoming environment, we as
34 | contributors and maintainers pledge to making participation in our project and
35 | our community a harassment-free experience for everyone, regardless of age, body
36 | size, disability, ethnicity, gender identity and expression, level of experience,
37 | nationality, personal appearance, race, religion, or sexual identity and
38 | orientation.
39 |
40 | ### Our Standards
41 |
42 | Examples of behavior that contributes to creating a positive environment
43 | include:
44 |
45 | * Using welcoming and inclusive language
46 | * Being respectful of differing viewpoints and experiences
47 | * Gracefully accepting constructive criticism
48 | * Focusing on what is best for the community
49 | * Showing empathy towards other community members
50 |
51 | Examples of unacceptable behavior by participants include:
52 |
53 | * The use of sexualized language or imagery and unwelcome sexual attention or
54 | advances
55 | * Trolling, insulting/derogatory comments, and personal or political attacks
56 | * Public or private harassment
57 | * Publishing others' private information, such as a physical or electronic
58 | address, without explicit permission
59 | * Other conduct which could reasonably be considered inappropriate in a
60 | professional setting
61 |
62 | ### Our Responsibilities
63 |
64 | Project maintainers are responsible for clarifying the standards of acceptable
65 | behavior and are expected to take appropriate and fair corrective action in
66 | response to any instances of unacceptable behavior.
67 |
68 | Project maintainers have the right and responsibility to remove, edit, or
69 | reject comments, commits, code, wiki edits, issues, and other contributions
70 | that are not aligned to this Code of Conduct, or to ban temporarily or
71 | permanently any contributor for other behaviors that they deem inappropriate,
72 | threatening, offensive, or harmful.
73 |
74 | ### Scope
75 |
76 | This Code of Conduct applies both within project spaces and in public spaces
77 | when an individual is representing the project or its community. Examples of
78 | representing a project or community include using an official project e-mail
79 | address, posting via an official social media account, or acting as an appointed
80 | representative at an online or offline event. Representation of a project may be
81 | further defined and clarified by project maintainers.
82 |
83 | ### Enforcement
84 |
85 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
86 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
87 | complaints will be reviewed and investigated and will result in a response that
88 | is deemed necessary and appropriate to the circumstances. The project team is
89 | obligated to maintain confidentiality with regard to the reporter of an incident.
90 | Further details of specific enforcement policies may be posted separately.
91 |
92 | Project maintainers who do not follow or enforce the Code of Conduct in good
93 | faith may face temporary or permanent repercussions as determined by other
94 | members of the project's leadership.
95 |
96 | ### Attribution
97 |
98 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
99 | available at [http://contributor-covenant.org/version/1/4][version]
100 |
101 | [homepage]: http://contributor-covenant.org
102 | [version]: http://contributor-covenant.org/version/1/4/
103 |
--------------------------------------------------------------------------------
/JAK/Application.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 |
6 | import sys
7 | import subprocess
8 | from JAK.Utils import Instance, bindings, getScreenGeometry
9 | from JAK import Settings
10 | from JAK.Widgets import JWindow
11 | from JAK.WebEngine import JWebView
12 | if bindings() == "PyQt5":
13 | print("PyQt5 Bindings")
14 | from PyQt5.QtCore import Qt, QCoreApplication
15 | from PyQt5.QtWidgets import QApplication
16 | else:
17 | print("JAK_PREFERRED_BINDING environment variable not set, falling back to PySide2 Bindings.")
18 | from PySide2.QtCore import Qt, QCoreApplication
19 | from PySide2.QtWidgets import QApplication
20 |
21 |
22 | class JWebApp(QApplication):
23 | #### Imports: from JAK.Application import JWebApp
24 | def __init__(self, config=Settings.config(), **app_config):
25 | super(JWebApp, self).__init__(sys.argv)
26 | self.config = config
27 | self.setAAttribute(Qt.AA_UseHighDpiPixmaps)
28 | self.setAAttribute(Qt.AA_EnableHighDpiScaling)
29 | self.applicationStateChanged.connect(self._applicationStateChanged_cb)
30 |
31 | for key, value in app_config.items():
32 | if isinstance(value, dict):
33 | for subkey, subvalue in app_config[key].items():
34 | config[key][subkey] = subvalue
35 | else:
36 | config[key] = value
37 |
38 | if config["setAAttribute"]:
39 | for attr in config["setAAttribute"]:
40 | self.setAAttribute(attr)
41 |
42 | if config["remote-debug"] or "--remote-debug" in sys.argv:
43 | sys.argv.append("--remote-debugging-port=9000")
44 |
45 | if config["debug"] or "--dev" in sys.argv:
46 | print("Debugging On")
47 | if not config["debug"]:
48 | config["debug"] = True
49 | else:
50 | print("Production Mode On, use (--dev) for debugging")
51 |
52 | # Enable/Disable GPU acceleration
53 | if not config["disableGPU"]:
54 | # Virtual machine detection using SystemD
55 | detect_virtual_machine = subprocess.Popen(
56 | ["systemd-detect-virt"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
57 | )
58 | detect_nvidia_pci = subprocess.Popen(
59 | "lspci | grep -i --color 'vga\|3d\|2d'", stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
60 | shell=True
61 | )
62 | virtual = detect_virtual_machine.communicate()[-1]
63 | nvidia_pci = detect_nvidia_pci.communicate()[0].decode("utf-8").lower()
64 |
65 | if config["disableGPU"]:
66 | self.disable_opengl()
67 | print("Disabling GPU, Software Rendering explicitly activated")
68 | else:
69 | if virtual:
70 | # Detect virtual machine
71 | print(f"Virtual machine detected:{virtual}")
72 | self.disable_opengl()
73 |
74 | elif nvidia_pci:
75 | # Detect NVIDIA cards
76 | if "nvidia" in nvidia_pci:
77 | print("NVIDIA falling back to Software Rendering")
78 | self.disable_opengl()
79 | else:
80 | print(f"Virtual Machine:{virtual}")
81 |
82 | if not self.config['webview']['online'] and self.config['webview']['IPC']:
83 | if bindings() == "PyQt5":
84 | from PyQt5.QtWebEngineCore import QWebEngineUrlScheme
85 | else:
86 | from PySide2.QtWebEngineCore import QWebEngineUrlScheme
87 | QWebEngineUrlScheme.registerScheme(QWebEngineUrlScheme("ipc".encode()))
88 |
89 | def _applicationStateChanged_cb(self, event):
90 | view = Instance.retrieve("view")
91 | page = view.page()
92 | # TODO freeze view when inactive to save ram
93 | if event == Qt.ApplicationInactive:
94 | print("inactive")
95 | elif event == Qt.ApplicationActive:
96 | print("active")
97 |
98 | def disable_opengl(self):
99 | # Disable GPU acceleration
100 | # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/206307
101 | self.setAAttribute(Qt.AA_UseSoftwareOpenGL)
102 |
103 | def setAAttribute(self, attr):
104 | QCoreApplication.setAttribute(attr, True)
105 |
106 | def run(self):
107 | Instance.record("view", JWebView(self.config))
108 | win = Instance.auto("win", JWindow(self.config))
109 |
110 | if self.config['window']["transparent"]:
111 | from JAK.Utils import JavaScript
112 | JavaScript.css(
113 | "body, html {background-color:transparent !important;background-image:none !important;}", "JAK"
114 | )
115 |
116 | if self.config['webview']["addCSS"]:
117 | from JAK.Utils import JavaScript
118 | JavaScript.css(self.config['webview']["addCSS"], "user")
119 | print("Custom CSS detected")
120 |
121 | if self.config['webview']["runJavaScript"]:
122 | from JAK.Utils import JavaScript
123 | JavaScript.send(self.config['webview']["runJavaScript"])
124 | print("Custom JavaScript detected")
125 |
126 | if self.config['window']["fullScreen"]:
127 | screen = getScreenGeometry()
128 | win.resize(int(screen.width()), int(screen.height()))
129 | else:
130 | width, height = int(win.default_size("width")), int(win.default_size("height"))
131 | win.resize(width, height)
132 |
133 | win.setFocusPolicy(Qt.WheelFocus)
134 | win.show()
135 | win.setFocus()
136 | win.window_original_position = win.frameGeometry()
137 | self.exec_()
138 |
--------------------------------------------------------------------------------
/JAK/DevTools.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 |
6 | from JAK.Utils import bindings
7 | if bindings() == "PyQt5":
8 | from PyQt5.QtWebEngineWidgets import QWebEngineView
9 | from PyQt5.QtWidgets import QDockWidget
10 | else:
11 | from PySide2.QtWebEngineWidgets import QWebEngineView
12 | from PySide2.QtWidgets import QDockWidget
13 |
14 |
15 | class WebView(QWebEngineView):
16 |
17 | def __init__(self, parent=None):
18 | QWebEngineView.__init__(self, parent)
19 |
20 | def set_inspected_view(self, view=None):
21 | self.page().setInspectedPage(view.page() if view else None)
22 |
23 |
24 | class InspectorDock(QDockWidget):
25 |
26 | def __init__(self, parent=None):
27 | super().__init__(parent=parent)
28 | title = "Inspector"
29 | self.setWindowTitle(title)
30 |
--------------------------------------------------------------------------------
/JAK/IPC.py:
--------------------------------------------------------------------------------
1 | from JAK.Utils import Instance
2 |
3 |
4 | class Bind:
5 | """
6 | * Usage: from JAK import IPC
7 | * Create your own class and point to this one: IPC.Bind = MyOverrride
8 | """
9 | @staticmethod
10 | def listen(data):
11 | """
12 | * Do something with the data.
13 | * :param data:
14 | * :return: url output
15 | """
16 | raise NotImplementedError()
17 |
18 |
19 | class Communication:
20 | """
21 | Call python methods from JavaScript.
22 | """
23 | @staticmethod
24 | def send(url) -> None:
25 | if ":" in url:
26 | url = url.split(':')[1]
27 | if url.endswith("()"):
28 | eval(f"Bind.{url}")
29 | else:
30 | Bind.listen(url)
31 |
--------------------------------------------------------------------------------
/JAK/KeyBindings.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 |
6 | from JAK.Utils import Instance, bindings
7 | if bindings() == "PyQt5":
8 | from PyQt5.QtCore import Qt
9 | else:
10 | from PySide2.QtCore import Qt
11 |
12 |
13 | class KeyPress:
14 | """ #### Imports: from JAK.Keybindings import KeyPress """
15 |
16 | def __init__(self, event, config):
17 | # * self.win = QMainWindow Instance
18 | # * self.view = QTWebEngine Instance
19 | if event.type() == event.KeyPress:
20 | if event.key() == Qt.Key_F11:
21 | if config['webview']["online"] is True or config['window']["showHelpMenu"] is True:
22 | self.full_screen()
23 | elif event.key() == Qt.Key_F10:
24 | if config['webview']["online"] is True or config['window']["showHelpMenu"] is True:
25 | self.win = Instance.retrieve("win")
26 | self.win.corner_window()
27 |
28 | elif event.modifiers() == Qt.ControlModifier:
29 |
30 | if event.key() == Qt.Key_Minus:
31 | self._zoom_out()
32 |
33 | elif event.key() == Qt.Key_Equal:
34 | self._zoom_in()
35 |
36 | def _current_zoom(self):
37 | self.view = Instance.retrieve("view")
38 | return self.view.zoomFactor()
39 |
40 | def _zoom_in(self):
41 | new_zoom = self._current_zoom() * 1.5
42 | self.view.setZoomFactor(new_zoom)
43 | self._save_zoom()
44 |
45 | def _zoom_out(self):
46 | new_zoom = self._current_zoom() / 1.5
47 | self.view.setZoomFactor(new_zoom)
48 | self._save_zoom()
49 |
50 | # TODO only zoom to a certain lvl then reset
51 | def _reset_zoom(self):
52 | self.view.setZoomFactor(1)
53 |
54 | def _save_zoom(self):
55 | percent = int(self._current_zoom() * 100)
56 | print(f"Zoom:{percent}%")
57 | # TODO save zoom
58 |
59 | def full_screen(self):
60 | # TODO animate window resize
61 | self.win = Instance.retrieve("win")
62 | if self.win.isFullScreen():
63 | self.win.showNormal()
64 | self.win.hide_show_bar()
65 | else:
66 | self.win.showFullScreen()
67 | self.win.hide_show_bar()
68 |
69 |
--------------------------------------------------------------------------------
/JAK/RequestInterceptor.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 |
6 | from JAK.Utils import check_url_rules, bindings
7 | if bindings() == "PyQt5":
8 | from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo
9 | else:
10 | from PySide2.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo
11 |
12 |
13 | class Interceptor(QWebEngineUrlRequestInterceptor):
14 | #### Imports: from JAK.RequestInterceptor import Interceptor
15 |
16 | def __init__(self, config):
17 | self.config = config
18 | """
19 |
20 | * :param debug:bool:
21 | * :param block_rules:dict: URL's to block
22 | """
23 | super(Interceptor, self).__init__()
24 |
25 | def interceptRequest(self, info) -> None:
26 | """
27 | * All method calls to the profile on the main thread will block until execution of this function is finished.
28 | * :param info: QWebEngineUrlRequestInfo
29 | """
30 |
31 | if self.config['webview']["urlRules"] is not None:
32 | # If we have any URL's in the block dictionary
33 | url = info.requestUrl().toString()
34 | try:
35 | if check_url_rules("Block", url, self.config['webview']["urlRules"]["block"]):
36 | # block url's
37 | info.block(True)
38 | print(f"Blocked:{url}")
39 | except KeyError:
40 | pass
41 |
42 | if self.config["debug"]:
43 | url = info.requestUrl().toString()
44 | resource = info.resourceType()
45 | if resource == QWebEngineUrlRequestInfo.ResourceType.ResourceTypeMainFrame:
46 | print(f"Intercepted link:{url}")
47 |
48 | elif resource != QWebEngineUrlRequestInfo.ResourceType.ResourceTypeMainFrame:
49 | print(f"Intercepted resource:{url}")
50 |
--------------------------------------------------------------------------------
/JAK/Settings.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 | from JAK.Utils import bindings
6 | if bindings() == "PyQt5":
7 | from PyQt5.QtCore import Qt
8 | from PyQt5.QtWebEngineWidgets import QWebEngineSettings
9 | else:
10 | from PySide2.QtCore import Qt
11 | from PySide2.QtWebEngineWidgets import QWebEngineSettings
12 |
13 |
14 | def config():
15 | return {
16 | "debug": False,
17 | "remote-debug": False,
18 | "setAAttribute": (),
19 | "disableGPU": False,
20 | "window": {
21 | "title": "Jade Application Kit",
22 | "icon": None,
23 | "backgroundImage": None,
24 | "setFlags": Qt.Window,
25 | "setAttribute": (),
26 | "state": None,
27 | "fullScreen": False,
28 | "transparent": False,
29 | "toolbar": None,
30 | "menus": None,
31 | "SystemTrayIcon": False,
32 | "showHelpMenu": False,
33 | },
34 | "webview": {
35 | "webContents": "https://codesardine.github.io/Jade-Application-Kit",
36 | "online": False,
37 | "urlRules": None,
38 | "cookiesPath": None,
39 | "userAgent": None,
40 | "addCSS": None,
41 | "runJavaScript": None,
42 | "IPC": True,
43 | "MediaAudioVideoCapture": False,
44 | "MediaVideoCapture": False,
45 | "MediaAudioCapture": False,
46 | "Geolocation": False,
47 | "MouseLock": False,
48 | "DesktopVideoCapture": False,
49 | "DesktopAudioVideoCapture": False,
50 | "injectJavaScript": {
51 | "JavaScript": None,
52 | "name": "Application Script"
53 | },
54 | "webChannel": {
55 | "active": False,
56 | "sharedOBJ": None
57 | },
58 | "enabledSettings": (
59 | QWebEngineSettings.JavascriptCanPaste,
60 | QWebEngineSettings.FullScreenSupportEnabled,
61 | QWebEngineSettings.AllowWindowActivationFromJavaScript,
62 | QWebEngineSettings.LocalContentCanAccessRemoteUrls,
63 | QWebEngineSettings.JavascriptCanAccessClipboard,
64 | QWebEngineSettings.SpatialNavigationEnabled,
65 | QWebEngineSettings.TouchIconsEnabled
66 | ),
67 | "disabledSettings": (
68 | QWebEngineSettings.PlaybackRequiresUserGesture
69 | )
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/JAK/Utils.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 |
6 | import os
7 | import re
8 | import subprocess
9 | from pathlib import Path
10 | from PyQt5.QtWidgets import QApplication
11 |
12 | register = {}
13 |
14 |
15 | def create_desktop_entry(url, title, description, icon):
16 | entry_name = title.replace(" ", "-")
17 | filename = f"{entry_name}.desktop"
18 | user_entry_path = f"{str(Path.home())}/.local/share/applications"
19 | # system_entry_path = f"/usr/share/applications/{file}"
20 |
21 | template = f"""
22 | # Created with JAK url:https://github.com/codesardine/Jade-Application-Kit
23 | [Desktop Entry]
24 | Type=Application
25 | Version=1.0
26 | Name={title}
27 | Comment={description}
28 | Path=/usr/bin
29 | Exec=jak-cli --url {url} --title {title} --icon {icon} --online true
30 | Icon={icon}
31 | Terminal=false
32 | Categories=Network;
33 | """.strip()
34 |
35 | with open(f"{user_entry_path}/{filename}", 'w+') as file:
36 | file.write(template)
37 | print(f"Desktop entry created in:{user_entry_path}/{filename}")
38 |
39 | update_database = "update-desktop-database"
40 | if os.path.isfile(f"/usr/bin/{update_database}"):
41 | proc = subprocess.run(f"{update_database} {user_entry_path}", shell=True, check=True)
42 | if proc.returncode == 0:
43 | print("Database updated.")
44 | else:
45 | print("desktop-file-utils:Not installed\nDatabase not updated.")
46 |
47 |
48 | def getScreenGeometry():
49 | return QApplication.instance().desktop().screenGeometry()
50 |
51 |
52 | def bindings():
53 | environment_var = "JAK_PREFERRED_BINDING"
54 | try:
55 | preferred_bindings = os.environ[environment_var]
56 | return preferred_bindings
57 | except KeyError:
58 | user_config_path = f"{str(Path.home())}/.config/jak.conf"
59 | if os.path.isfile(user_config_path):
60 | config_file = user_config_path
61 | else:
62 | system_config_path = "/etc/jak.conf"
63 | config_file = system_config_path
64 | try:
65 | import configparser
66 | config = configparser.ConfigParser()
67 | config.read(config_file)
68 | preferred_bindings = config["bindings"][environment_var]
69 | return preferred_bindings
70 | except Exception as error:
71 | print(error)
72 |
73 |
74 | def get_current_path():
75 | return str(Path('.').absolute())
76 |
77 |
78 | def check_url_rules(request_type: str, url_request: str, url_rules: tuple) -> bool:
79 | """
80 | * Search logic for url rules, we can use regex or simple match the beginning of the domain.
81 | * :param request_type: WebWindowType
82 | * :return: function, checks against a list of urls
83 | """
84 | SCHEME = "https://"
85 |
86 | if request_type == "Block":
87 | url_rules=url_rules
88 |
89 | elif request_type == "WebBrowserTab":
90 | try:
91 | url_rules = url_rules["WebBrowserTab"]
92 | except KeyError:
93 | url_rules = ""
94 |
95 | elif request_type == "WebBrowserWindow":
96 | try:
97 | url_rules = url_rules["WebBrowserWindow"]
98 | except KeyError:
99 | url_rules = ""
100 |
101 | for rule in url_rules:
102 | pattern = re.compile(f"{SCHEME}{rule}")
103 | if url_request.startswith(f"{SCHEME}{rule}"):
104 | print(f"{SCHEME}{rule}:Method:startswith")
105 | return True
106 | elif re.search(pattern, url_request):
107 | print(f"{SCHEME}{rule}:Method:regex")
108 | return True
109 | return False
110 |
111 |
112 | class Instance:
113 | """
114 | #### :Imports: from JAK.Utils import Instance
115 | Add object instances in a dictionary, it can be used to point
116 | to references we don,t want to be garbage collected, for usage later
117 | """
118 |
119 | @staticmethod
120 | def get_instances() -> dict:
121 | """
122 | * :Usage: Instance.get_instances()
123 | """
124 | return register
125 |
126 | @staticmethod
127 | def record(name: str, _type: object) -> None:
128 | """
129 | * :Usage: Instance.record("name", object)
130 | * Should only be used once per instance
131 | """
132 | register[name] = _type
133 | print(f"Registering ['{name}'] Instance")
134 |
135 | @staticmethod
136 | def retrieve(name: str) -> object or str:
137 | """
138 | * :Usage: Instance.retrieve("name")
139 | """
140 | try:
141 | return register[name]
142 | except KeyError:
143 | print(f"Instance: ['{name}'] Not Present, to add it use -> Instance.record(['{name}', object])")
144 | return ""
145 |
146 | @staticmethod
147 | def auto(name: str, _type: object) -> object:
148 | """
149 | * :Usage: Instance.auto("name", object)
150 | * Automatically detects if an instance is active with that name and retrieves it.
151 | If not present, creates it creates a new one and retrieves it.
152 | * Should only be used once per instance
153 | """
154 | try:
155 | return register[name]
156 | except KeyError:
157 | register[name] = _type
158 | finally:
159 | print(f"Registering and Retrieving ['{name}'] Instance")
160 | return register[name]
161 |
162 |
163 | class JavaScript:
164 | """
165 | * Run javascript in the webview after load is complete Injects will be logged in the inspector
166 | * :Imports: from Jak.Utils import JavaScript
167 | * :Usage: JavaScript.log(msg)
168 | """
169 | @staticmethod
170 | def log(message: str) -> None:
171 | """
172 | * Outputs console.log() messages in the inspector
173 | * :param message: Log message
174 | """
175 | JavaScript.send(f"console.log('JAK log:{message}');")
176 |
177 | @staticmethod
178 | def css(styles: str, _type) -> None:
179 | """
180 | * Insert custom styles
181 | * :param styles: CSS -> a { color: red; }
182 | """
183 | javascript = f"""
184 | var style = document.createElement('style');
185 | style.type = 'text/css';
186 | style.classList.add('{_type}-custom-style');
187 | style.innerHTML = `{JavaScript._is_file_or_string(styles)}`;
188 | document.getElementsByTagName('head')[0].appendChild(style);
189 | """
190 | view = Instance.retrieve("view")
191 | view.page().loadFinished.connect(
192 | lambda: view.page().runJavaScript(javascript)
193 | )
194 |
195 | @staticmethod
196 | def alert(message: str) -> None:
197 | """
198 | * Triggers an alert message
199 | * :param message: your popcorn is ready enjoy
200 | """
201 | JavaScript.send(f"alert('{message}');")
202 | JavaScript.log(f"JAK Alert:[{message}]")
203 |
204 | @staticmethod
205 | def send(script: str) -> None:
206 | """
207 | * Send custom JavaScript
208 | """
209 | try:
210 | view = Instance.retrieve("view")
211 | view.page().runJavaScript(f"{JavaScript._is_file_or_string(script)}")
212 | except Exception as err:
213 | print(err)
214 |
215 | @staticmethod
216 | def inject(page, options: dict) -> None:
217 | if bindings() == "PyQt5":
218 | from PyQt5.QtWebEngineWidgets import QWebEngineScript
219 | else:
220 | from PySide2.QtWebEngineWidgets import QWebEngineScript
221 |
222 | script = QWebEngineScript()
223 | script.setName(options["name"])
224 | script.setWorldId(QWebEngineScript.MainWorld)
225 | script.setInjectionPoint(QWebEngineScript.DocumentCreation)
226 | script.setRunsOnSubFrames(True)
227 | script.setSourceCode(options["JavaScript"])
228 | print(f"Injecting JavaScript {options['name']}")
229 | page.profile().scripts().insert(script)
230 |
231 | @staticmethod
232 | def _is_file_or_string(script) -> str:
233 | """
234 | * Detect if is file or string, convert to string
235 | * :param script: file or string
236 | """
237 | if os.path.exists(script) and os.path.isfile(script):
238 | try:
239 | with open(script, "r") as file:
240 | string = file.read()
241 | return string
242 | except Exception as err:
243 | print(err)
244 | elif isinstance(script, str):
245 | return script
246 |
--------------------------------------------------------------------------------
/JAK/WebEngine.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 | import os
6 | from functools import lru_cache as cache
7 | from JAK.Utils import check_url_rules, get_current_path, bindings
8 | from JAK.Widgets import Dialog
9 | from JAK.RequestInterceptor import Interceptor
10 | if bindings() == "PyQt5":
11 | from PyQt5.QtCore import QUrl, Qt
12 | from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler
13 | from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage, QWebEngineSettings
14 | else:
15 | from PySide2.QtCore import QUrl, Qt
16 | from PySide2.QtWebEngineCore import QWebEngineUrlSchemeHandler
17 | from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage, QWebEngineSettings
18 |
19 |
20 | @cache(maxsize=5)
21 | def validate_url(self, url: str) -> None:
22 | """
23 | * Check if is a URL or HTML and if is valid
24 | * :param self: QWebEnginePage
25 | * :param web_contents: URL or HTML
26 | """
27 | if "!doctype" in url.lower():
28 | # Inject HTML
29 | base_url = get_current_path()
30 | self.setHtml(url, QUrl(f"file://{base_url}/"))
31 | print("Loading local HTML")
32 | else:
33 | if url.endswith(".html"):
34 | # HTML file
35 | if not url.startswith("/"):
36 | url = f"/{url}"
37 | url = f"file://{url}"
38 |
39 | elif "://" not in url:
40 | # HTML URL
41 | url = f"https://{url}"
42 |
43 | url = QUrl(url)
44 | if url.isValid():
45 | self.load(url)
46 | print(f"Loading URL:{url.toString()}")
47 |
48 |
49 | class IpcSchemeHandler(QWebEngineUrlSchemeHandler):
50 | def __init__(self):
51 | super().__init__()
52 |
53 | def requestStarted(self, request):
54 | url = request.requestUrl().toString()
55 | if url.startswith("ipc:"):
56 | # * Link's that starts with [ ipc:somefunction() ] trigger's the two way communication system between
57 | # HTML and Python, only if online is set to false
58 | from JAK.IPC import Communication
59 | Communication.send(url)
60 | return
61 |
62 |
63 | class JWebPage(QWebEnginePage):
64 | """ #### Imports: from JAK.WebEngine import JWebPage """
65 | def __init__(self, profile, webview, config):
66 | self.config = config
67 | super(JWebPage, self).__init__(profile, webview)
68 | self.featurePermissionRequested.connect(self._on_feature_permission_requested)
69 |
70 | def _open_in_browser(self) -> None:
71 | """ Open url in a external browser """
72 | print("Open above^ tab in Browser")
73 | from webbrowser import open_new_tab
74 | open_new_tab(self.url)
75 |
76 | def _dialog_open_in_browser(self) -> None:
77 | """ Opens a dialog to confirm if user wants to open url in external browser """
78 | msg = "Open In Your Browser"
79 | Dialog.question(self.parent(), self.title(), msg, self._open_in_browser)
80 |
81 | @cache(maxsize=10)
82 | def acceptNavigationRequest(self, url, _type, is_main_frame) -> bool:
83 | """
84 | * Decide if we navigate to a URL
85 | * :param url: QtCore.QUrl
86 | * :param _type: QWebEnginePage.NavigationType
87 | * :param is_main_frame:bool
88 | """
89 | self.url = url.toString()
90 | self.page = JWebPage(self.profile(), self.view(), self.config)
91 | # Redirect new tabs to same window
92 | self.page.urlChanged.connect(self._on_url_changed)
93 |
94 | if self.config['webview']["online"]:
95 | if _type == QWebEnginePage.WebWindowType.WebBrowserTab:
96 | if self.config['webview']["urlRules"]:
97 | # Check for URL rules on new tabs
98 | if self.url.startswith(self.config['webview']["urlRules"]["WebBrowserTab"]):
99 | self.open_window(self.url)
100 | return False
101 | elif check_url_rules("WebBrowserTab", self.url, self.config['webview']["urlRules"]):
102 | print(f"Redirecting WebBrowserTab^ to same window")
103 | return True
104 | else:
105 | print(f"Deny WebBrowserTab:{self.url}")
106 | # check against WebBrowserWindow list to avoid duplicate dialogs
107 | if not check_url_rules("WebBrowserWindow", self.url, self.config['webview']["urlRules"]):
108 | self._dialog_open_in_browser()
109 | return False
110 | else:
111 | return True
112 |
113 | elif _type == QWebEnginePage.WebBrowserBackgroundTab:
114 | print(f"WebBrowserBackgroundTab request:{self.url}")
115 | return True
116 |
117 | elif _type == QWebEnginePage.WebBrowserWindow:
118 | if self.config['webview']["urlRules"] and self.config['webview']["online"]:
119 | # Check URL rules on new windows
120 | if check_url_rules("WebBrowserWindow", self.url, self.config['webview']["urlRules"]):
121 | print(f"Deny WebBrowserWindow:{self.url}")
122 | self._dialog_open_in_browser()
123 | return False
124 | else:
125 | print(f"Allow WebBrowserWindow:{self.url}")
126 | return True
127 | else:
128 | return True
129 |
130 | elif _type == QWebEnginePage.WebDialog:
131 | return True
132 | return True
133 |
134 | def _on_feature_permission_requested(self, security_origin, feature):
135 |
136 | def grant_permission():
137 | self.setFeaturePermission(security_origin, feature, self.PermissionGrantedByUser)
138 |
139 | def deny_permission():
140 | self.setFeaturePermission(security_origin, feature, self.PermissionDeniedByUser)
141 |
142 | if feature == self.Notifications:
143 | grant_permission()
144 | elif feature == self.MediaAudioVideoCapture and self.config['webview']["MediaAudioVideoCapture"]:
145 | grant_permission()
146 | elif feature == self.MediaVideoCapture and self.config['webview']["MediaVideoCapture"]:
147 | grant_permission()
148 | elif feature == self.MediaAudioCapture and self.config['webview']["MediaAudioCapture"]:
149 | grant_permission()
150 | elif feature == self.Geolocation and self.config['webview']["Geolocation"]:
151 | grant_permission()
152 | elif feature == self.MouseLock and self.config['webview']["MouseLock"]:
153 | grant_permission()
154 | elif feature == self.DesktopVideoCapture and self.config['webview']["DesktopVideoCapture"]:
155 | grant_permission()
156 | elif feature == self.DesktopAudioVideoCapture and self.config['webview']["DesktopAudioVideoCapture"]:
157 | grant_permission()
158 | else:
159 | deny_permission()
160 |
161 | def open_window(self, url):
162 | """ Open a New Window"""
163 | # FIXME cookies path needs to be declared for this to work
164 | self.popup = JWebView(self.config)
165 | self.popup.page().windowCloseRequested.connect(self.popup.close)
166 | self.popup.show()
167 | print(f"Opening New Window^")
168 |
169 | @cache(maxsize=2)
170 | def createWindow(self, _type: object) -> QWebEnginePage:
171 | """
172 | * Redirect new window's or tab's to same window
173 | * :param _type: QWebEnginePage.WebWindowType
174 | """
175 | return self.page
176 |
177 | def _on_url_changed(self, url: str) -> None:
178 | url = url.toString()
179 | if url == "about:blank":
180 | return False
181 | else:
182 | validate_url(self, url)
183 |
184 |
185 | class JWebView(QWebEngineView):
186 | """ #### Imports: from JAK.WebEngine import JWebView """
187 | def __init__(self, config):
188 | self.config = config
189 | super(JWebView, self).__init__()
190 | self.setAttribute(Qt.WA_DeleteOnClose, True)
191 | self.profile = QWebEngineProfile.defaultProfile()
192 | self.webpage = JWebPage(self.profile, self, config)
193 | self.setPage(self.webpage)
194 | if config['webview']["injectJavaScript"]["JavaScript"]:
195 | self._inject_script(config['webview']["injectJavaScript"])
196 | self.interceptor = Interceptor(config)
197 |
198 | if config['webview']["userAgent"]:
199 | # Set user agent
200 | self.profile.setHttpUserAgent(config['webview']["userAgent"])
201 |
202 | if config["debug"]:
203 | self.settings().setAttribute(QWebEngineSettings.XSSAuditingEnabled, True)
204 | else:
205 | self.setContextMenuPolicy(Qt.PreventContextMenu)
206 |
207 | if config['window']["transparent"]:
208 | # Activates background transparency
209 | self.setAttribute(Qt.WA_TranslucentBackground)
210 | self.page().setBackgroundColor(Qt.transparent)
211 | print("Transparency detected")
212 |
213 | # * Set Engine options
214 | self.settings().setAttribute(self.config['webview']['disabledSettings'], False)
215 | for setting in self.config['webview']['enabledSettings']:
216 | self.settings().setAttribute(setting, True)
217 |
218 | if config['webview']["online"]:
219 | self.settings().setAttribute(QWebEngineSettings.DnsPrefetchEnabled, True)
220 | print("Engine online IPC and Bridge Disabled")
221 | self.page().profile().downloadRequested.connect(self._download_requested)
222 |
223 | # Set persistent cookies
224 | self.profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies)
225 |
226 | # set cookies on user folder
227 | if config['webview']["cookiesPath"]:
228 | # allow specific path per application.
229 | _cookies_path = f"{os.getenv('HOME')}/.jak/{config['webview']['cookiesPath']}"
230 | else:
231 | # use separate cookies database per application
232 | title = config['window']["title"].lower().replace(" ", "-")
233 | _cookies_path = f"{os.getenv('HOME')}/.jak/{title}"
234 |
235 | self.profile.setPersistentStoragePath(_cookies_path)
236 | print(f"Cookies PATH:{_cookies_path}")
237 | else:
238 | self.settings().setAttribute(QWebEngineSettings.ShowScrollBars, False)
239 | application_script = "const JAK = {};"
240 |
241 | if config['webview']["IPC"]:
242 | print("IPC Active:")
243 | self._ipc_scheme_handler = IpcSchemeHandler()
244 | self.profile.installUrlSchemeHandler('ipc'.encode(), self._ipc_scheme_handler)
245 | application_script += """JAK.IPC = function(backendFunction) {
246 | window.location.href = "ipc:" + backendFunction;
247 | };"""
248 |
249 | if config['webview']["webChannel"]["active"]:
250 | if bindings() == "PyQt5":
251 | from PyQt5.QtCore import QFile, QIODevice
252 | from PyQt5.QtWebChannel import QWebChannel
253 | else:
254 | from PySide2.QtCore import QFile, QIODevice
255 | from PySide2.QtWebChannel import QWebChannel
256 |
257 | webchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
258 | webchannel_js.open(QIODevice.ReadOnly)
259 | webchannel_js = bytes(webchannel_js.readAll()).decode('utf-8')
260 | webchannel_js += """new QWebChannel(qt.webChannelTransport, function (channel) {
261 | JAK.Bridge = channel.objects.Bridge;
262 | });"""
263 |
264 | application_script += webchannel_js
265 | self._inject_script({"JavaScript": application_script, "name": "JAK"})
266 | channel = QWebChannel(self.page())
267 | if config['webview']["webChannel"]["sharedOBJ"]:
268 | bridge_obj = config['webview']["webChannel"]["sharedOBJ"]
269 | else:
270 | raise NotImplementedError("QWebChannel shared QObject")
271 |
272 | channel.registerObject("Bridge", bridge_obj)
273 | self.page().setWebChannel(channel)
274 | print("WebChannel Active:")
275 | else:
276 | self._inject_script({"JavaScript": application_script, "name": "JAK"})
277 |
278 | self.profile.setRequestInterceptor(self.interceptor)
279 | print(self.profile.httpUserAgent())
280 | validate_url(self, config['webview']["webContents"])
281 |
282 | def _inject_script(self, script: dict):
283 | from JAK.Utils import JavaScript
284 | JavaScript.inject(self.page(), script)
285 |
286 | def dropEvent(self, *args):
287 | # disable drop event
288 | pass
289 |
290 | def _download_requested(self, download_item) -> None:
291 | """
292 | * If a download is requested call a save file dialog
293 | * :param download_item: file to be downloaded
294 | """
295 | if bindings() == "PyQt5":
296 | from PyQt5.QtWidgets import QFileDialog
297 | else:
298 | from PySide2.QtWidgets import QFileDialog
299 | dialog = QFileDialog(self)
300 | path = dialog.getSaveFileName(dialog, "Save File", download_item.path())
301 |
302 | if path[0]:
303 | download_item.setPath(path[0])
304 | print(f"downloading file to:( {download_item.path()} )")
305 | download_item.accept()
306 | self.download_item = download_item
307 | download_item.finished.connect(self._download_finished)
308 | else:
309 | print("Download canceled")
310 |
311 | def _download_finished(self) -> None:
312 | """
313 | Goes to previous page and pops an alert informing the user that the download is finish and were to find it
314 | """
315 | file_path = self.download_item.path()
316 | msg = f"File Downloaded to: {file_path}"
317 | Dialog.information(self, "Download Complete", msg)
318 |
--------------------------------------------------------------------------------
/JAK/Widgets.py:
--------------------------------------------------------------------------------
1 | #### Jade Application Kit
2 | # * https://codesardine.github.io/Jade-Application-Kit
3 | # * Vitor Lopes Copyright (c) 2016 - 2020
4 | # * https://vitorlopes.me
5 |
6 | import os
7 | from JAK.Utils import Instance, bindings, getScreenGeometry
8 | from JAK.KeyBindings import KeyPress
9 | if bindings() == "PyQt5":
10 | from PyQt5.QtCore import Qt, QSize, QUrl
11 | from PyQt5.QtGui import QIcon, QPixmap, QImage
12 | from PyQt5.QtWidgets import QMainWindow, QWidget, QMessageBox, QSystemTrayIcon,\
13 | QAction, QToolBar, QMenu, QMenuBar, QFileDialog, QLabel
14 | else:
15 | from PySide2.QtCore import Qt, QSize, QUrl
16 | from PySide2.QtGui import QIcon, QPixmap, QImage
17 | from PySide2.QtWidgets import QMainWindow, QWidget, QMessageBox, QSystemTrayIcon,\
18 | QAction, QToolBar, QMenu, QMenuBar, QFileDialog, QLabel
19 |
20 |
21 | class SystemTrayIcon(QSystemTrayIcon):
22 | def __init__(self, icon, app, config):
23 | self.config = config
24 | self.icon = icon
25 | super(SystemTrayIcon, self).__init__(icon, parent=app)
26 | self.setContextMenu(self.tray_menu())
27 | self.show()
28 |
29 | def tray_menu(self):
30 | """
31 | Create menu for the tray icon
32 | """
33 | self.menu = QMenu()
34 | for item in self.config['window']["SystemTrayIcon"]:
35 | try:
36 | self.action = QAction(f"{item['title']}", self)
37 | self.action.triggered.connect(item['action'])
38 | if item['icon']:
39 | self.action.setIcon(QIcon(QPixmap(item['icon'])))
40 | self.menu.addAction(self.action)
41 | except KeyError:
42 | pass
43 | return self.menu
44 |
45 |
46 | class JWindow(QMainWindow):
47 | """ #### Imports: from JAK.Widgets import JWindow """
48 | def __init__(self, config):
49 | super().__init__()
50 | self.config = config
51 | if config["window"]["backgroundImage"]:
52 | # Transparency must be set to True
53 | self.label = QLabel(self)
54 | self.setObjectName("JAKWindow")
55 | self.setBackgroundImage(config["window"]["backgroundImage"])
56 | self.video_corner = False
57 | self.center = getScreenGeometry().center()
58 | self.setWindowTitle(config['window']["title"])
59 | self.setWindowFlags(config['window']["setFlags"])
60 | self.setWAttribute(Qt.WA_DeleteOnClose)
61 | for attr in config['window']["setAttribute"]:
62 | self.setWAttribute(attr)
63 |
64 | if config['window']["state"]:
65 | self.setWindowState(config['window']["state"])
66 |
67 | if config['window']["icon"] and os.path.isfile(config['window']["icon"]):
68 | self.icon = QIcon(config['window']["icon"])
69 | else:
70 | print(f"icon not found: {config['window']['icon']}")
71 | print("loading default icon:")
72 | self.icon = QIcon.fromTheme("applications-internet")
73 |
74 | view = Instance.retrieve("view")
75 | if view:
76 | self.view = view
77 | self.setCentralWidget(self.view)
78 | self.view.iconChanged.connect(self._icon_changed)
79 | if config['webview']["online"]:
80 | self.view.page().titleChanged.connect(self.status_message)
81 |
82 | if config['window']["transparent"]:
83 | # Set Background Transparency
84 | self.setWAttribute(Qt.WA_TranslucentBackground)
85 | self.setAutoFillBackground(True)
86 |
87 | if config['webview']["online"]:
88 | # Do not display toolbar or system tray offline
89 | if config['window']["toolbar"]:
90 | self.toolbar = JToolbar(self, config['window']["toolbar"], self.icon, config['window']["title"])
91 | self.addToolBar(self.toolbar)
92 | self.setMenuBar(Menu(self, config['window']["menus"]))
93 | else:
94 | if config['window']["showHelpMenu"]:
95 | self.setMenuBar(Menu(self, config['window']["menus"]))
96 | self.view.page().titleChanged.connect(self.status_message)
97 |
98 | if config['window']["SystemTrayIcon"]:
99 | self.system_tray = SystemTrayIcon(self.icon, self, config)
100 |
101 | if config["debug"]:
102 | self.showInspector()
103 | self._set_icons()
104 |
105 | def setBackgroundImage(self, image):
106 | screen = getScreenGeometry()
107 | pixmap = QPixmap(QImage(image)).scaled(screen.width(), screen.height(), Qt.KeepAspectRatioByExpanding)
108 | self.label.setPixmap(pixmap)
109 | self.label.setGeometry(0, 0, screen.width(), self.label.sizeHint().height())
110 |
111 | def showInspector(self):
112 | from JAK.DevTools import WebView, InspectorDock
113 | self.inspector_dock = InspectorDock(self)
114 | self.inspector_view = WebView(parent=self)
115 | self.inspector_view.set_inspected_view(self.view)
116 | self.inspector_dock.setWidget(self.inspector_view)
117 | self.addDockWidget(Qt.TopDockWidgetArea, self.inspector_dock)
118 |
119 | def hideInspector(self):
120 | self.inspector_dock.hide()
121 |
122 | def setWAttribute(self, attr):
123 | self.setAttribute(attr, True)
124 |
125 | def keyPressEvent(self, event):
126 | KeyPress(event, self.config)
127 |
128 | def _set_icons(self):
129 | self.setWindowIcon(self.icon)
130 | if self.config['window']["SystemTrayIcon"]:
131 | self.system_tray.setIcon(self.icon)
132 |
133 | def _icon_changed(self):
134 | if not self.view.icon().isNull():
135 | self.icon = self.view.icon()
136 | self._set_icons()
137 |
138 | def status_message(self):
139 | # Show status message
140 | self.statusbar = self.statusBar()
141 | self.statusbar.showMessage(self.view.page().title(), 10000)
142 |
143 | def hide_show_bar(self):
144 | if self.isFullScreen() or self.video_corner:
145 | self.statusbar.hide()
146 | if self.config['window']["toolbar"]:
147 | self.toolbar.hide()
148 | else:
149 | self.statusbar.show()
150 | if self.config['window']["toolbar"]:
151 | self.toolbar.show()
152 |
153 | def default_size(self, size: str):
154 | # Set to 70% screen size
155 | screen = getScreenGeometry()
156 | if size == "width":
157 | return screen.width() * 2 / 3
158 | elif size == "height":
159 | return screen.height() * 2 / 3
160 |
161 | def set_window_to_defaults(self):
162 | self.window_original_position.moveCenter(self.center)
163 | self.move(self.window_original_position.topLeft())
164 | self.resize(self.default_size("width"), self.default_size("height"))
165 | self.hide_show_bar()
166 | self.setWindowFlags(Qt.Window)
167 | self.show()
168 |
169 | def set_window_to_corner(self):
170 | self.move(self.window_original_position.bottomRight())
171 | # Set to 30% screen size
172 | screen = getScreenGeometry()
173 | self.resize(screen.width() * 0.7 / 2, screen.height() * 0.7 / 2)
174 | self.hide_show_bar()
175 | self.setWindowFlags(Qt.SplashScreen | Qt.WindowStaysOnTopHint)
176 | self.show()
177 |
178 | def corner_window(self):
179 | if self.video_corner:
180 | self.video_corner = False
181 | self.set_window_to_defaults()
182 | else:
183 | self.video_corner = True
184 | if self.isFullScreen():
185 | self.showNormal()
186 | self.set_window_to_corner()
187 |
188 |
189 | class JToolbar(QToolBar):
190 | """ #### Imports: from JAK.Widgets import JToolbar """
191 | def __init__(self, parent, toolbar, icon, title):
192 | """
193 | * :param parent: Parent window
194 | * :param toolbar:dict
195 | * :param icon:str
196 | * :param title:str
197 | """
198 | super(JToolbar, self).__init__(parent)
199 | self.icon = icon
200 | self.setMovable(False)
201 | self.setContextMenuPolicy(Qt.PreventContextMenu)
202 | self.setIconSize(QSize(32, 32))
203 | self.about_title = "About"
204 |
205 | if toolbar:
206 | # If a dict is passed generate buttons from dict
207 | for btn in toolbar:
208 | try:
209 | if btn["icon"]:
210 | item = QAction(QIcon(btn["icon"]), btn["name"], self)
211 | except KeyError:
212 | item = QAction(btn["name"], self)
213 |
214 | item.triggered.connect(self._on_click(btn["url"]))
215 | self.addAction(item)
216 |
217 | def _on_click(self, url: str, title=""):
218 | view = Instance.retrieve("view")
219 | if url.startswith("https"):
220 | return lambda: view.setUrl(QUrl(url))
221 | else:
222 | msg = url
223 | return lambda: Dialog.information(self, title, msg)
224 |
225 |
226 | class Menu(QMenuBar):
227 |
228 | def __init__(self, parent, menus):
229 |
230 | super(Menu, self).__init__(parent)
231 | if menus:
232 | for menu in menus:
233 | if type(menu) is dict:
234 | title = self.addMenu(menu["title"])
235 | for entry in menu["entries"]:
236 | submenu = QAction(entry[0], self)
237 | title.addAction(submenu)
238 | print(entry[1])
239 | submenu.triggered.connect(self._on_click(entry[1]))
240 |
241 | help_menu = {"title": "Keyboard Shortcuts", "text": """
242 |
import sys
29 | import subprocess
30 | from JAK.Utils import Instance, bindings, getScreenGeometry
31 | from JAK import Settings
32 | from JAK.Widgets import JWindow
33 | from JAK.WebEngine import JWebView
34 | from JAK import __version__
35 | if bindings() == "PyQt5":
36 | print("PyQt5 Bindings")
37 | from PyQt5.QtCore import Qt, QCoreApplication, QRect
38 | from PyQt5.QtWidgets import QApplication
39 | from PyQt5.QtWebEngineWidgets import QWebEnginePage
40 | else:
41 | print("JAK_PREFERRED_BINDING environment variable not set, falling back to PySide2 Bindings.")
42 | from PySide2.QtCore import Qt, QCoreApplication
43 | from PySide2.QtWidgets import QApplication
class JWebApp(QApplication):
def __init__(self, config=Settings.config(), **app_config):
68 | super(JWebApp, self).__init__(sys.argv)
69 | self.config = config
70 | self.setAAttribute(Qt.AA_UseHighDpiPixmaps)
71 | self.setAAttribute(Qt.AA_EnableHighDpiScaling)
72 | self.applicationStateChanged.connect(self._applicationStateChanged_cb)
73 | for key, value in app_config.items():
74 | if isinstance(value, dict):
75 | for subkey, subvalue in app_config[key].items():
76 | config[key][subkey] = subvalue
77 | else:
78 | config[key] = value
79 |
80 | for attr in config["setAAttribute"]:
81 | self.setAAttribute(attr)
82 |
83 | if config["remote-debug"] or "--remote-debug" in sys.argv:
84 | sys.argv.append("--remote-debugging-port=9000")
85 |
86 | if config["debug"] or "--dev" in sys.argv:
87 | print("Debugging On")
88 | if not config["debug"]:
89 | config["debug"] = True
90 | else:
91 | print("Production Mode On, use (--dev) for debugging")
Enable/Disable GPU acceleration
101 | if not config["disableGPU"]:
Virtual machine detection using SystemD
113 | detect_virtual_machine = subprocess.Popen(
116 | ["systemd-detect-virt"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
117 | )
FIXME find a more reliable way of detecting NVIDIA cards
127 | detect_nvidia_pci = subprocess.Popen(
130 | "lspci | grep -i --color 'vga\|3d\|2d'", stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
131 | shell=True
132 | )
133 | virtual = detect_virtual_machine.communicate()
134 | nvidia_pci = detect_nvidia_pci.communicate()
135 | nvidia_pci = nvidia_pci[0].decode("utf-8").lower()
136 |
137 | if config["disableGPU"]:
138 | self.disable_opengl()
139 | print("Disabling GPU, Software Rendering explicitly activated")
140 | else:
141 | if virtual[-1]:
Detect virtual machine
151 | print(f"Virtual machine detected:{virtual}")
154 | self.disable_opengl()
155 |
156 | elif nvidia_pci:
Detect NVIDIA cards
166 | if "nvidia" in nvidia_pci:
169 | print("NVIDIA detected:Known bug - kernel rejected pushbuf")
170 | print("Falling back to Software Rendering")
171 | self.disable_opengl()
172 | else:
173 | print(f"Virtual Machine:{virtual[-1]}")
Desktop file must match application name in lowercase with dashes instead of white space.
183 | self.setDesktopFileName(f"{self.config['window']['title'].lower().replace(' ', '-')}.desktop")
186 | self.setOrganizationDomain(self.config['webview']['webContents'])
187 | self.setApplicationVersion(__version__)
188 | if not self.config['webview']['online'] and self.config['webview']['IPC']:
189 | if bindings() == "PyQt5":
190 | from PyQt5.QtWebEngineCore import QWebEngineUrlScheme
191 | else:
192 | from PySide2.QtWebEngineCore import QWebEngineUrlScheme
193 | QWebEngineUrlScheme.registerScheme(QWebEngineUrlScheme("ipc".encode()))
def _applicationStateChanged_cb(self, event):
206 | view = Instance.retrieve("view")
207 | page = view.page()
TODO freeze view when inactive to save ram
217 | if event == Qt.ApplicationInactive:
220 | print("inactive")
221 | elif event == Qt.ApplicationActive:
222 | print("active")
def disable_opengl(self):
Disable GPU acceleration 244 | https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/206307
245 | self.setAAttribute(Qt.AA_UseSoftwareOpenGL)
def setAAttribute(self, attr):
260 | QCoreApplication.setAttribute(attr, True)
def run(self):
273 | Instance.record("view", JWebView(self.config))
274 |
275 | if self.config['window']["transparent"]:
276 | from JAK.Utils import JavaScript
277 | JavaScript.css(
278 | "body, html {background-color:transparent !important;background-image:none !important;}", "JAK"
279 | )
280 |
281 | if self.config['webview']["addCSS"]:
282 | from JAK.Utils import JavaScript
283 | JavaScript.css(self.config['webview']["addCSS"], "user")
284 | print("Custom CSS detected")
285 |
286 | if self.config['webview']["runJavaScript"]:
287 | from JAK.Utils import JavaScript
288 | JavaScript.send(self.config['webview']["runJavaScript"])
289 | print("Custom JavaScript detected")
290 |
291 | win = Instance.auto("win", JWindow(self.config))
292 | if self.config['window']["fullScreen"]:
293 | screen = getScreenGeometry()
294 | win.resize(screen.width(), screen.height())
295 | else:
296 | win.resize(win.default_size("width"), win.default_size("height"))
297 |
298 | win.setFocusPolicy(Qt.WheelFocus)
299 | win.show()
300 | win.setFocus()
301 | win.window_original_position = win.frameGeometry()
302 | result = self.exec_()
303 | sys.exit(result)
304 |
305 |
from JAK.Utils import bindings
29 | if bindings() == "PyQt5":
30 | from PyQt5.QtCore import Qt
31 | from PyQt5.QtWebEngineWidgets import QWebEngineView
32 | from PyQt5.QtWidgets import QDockWidget
33 | else:
34 | from PySide2.QtCore import Qt
35 | from PySide2.QtWebEngineWidgets import QWebEngineView
36 | from PySide2.QtWidgets import QDockWidget
class WebView(QWebEngineView):
def __init__(self, parent=None):
61 | QWebEngineView.__init__(self, parent)
def set_inspected_view(self, view=None):
74 | self.page().setInspectedPage(view.page() if view else None)
class InspectorDock(QDockWidget):
def __init__(self, parent=None):
99 | super().__init__(parent=parent)
100 | title = "Inspector"
101 | self.setWindowTitle(title)
102 | self.setObjectName(title)
103 |
104 |
from JAK.Utils import Instance
class Bind:
@staticmethod
55 | def listen(data):
raise NotImplementedError()
Call python methods from JavaScript.
77 |class Communication:
@staticmethod
92 | def send(url) -> None:
93 | if ":" in url:
94 | url = url.split(':')[1]
95 | if url.endswith("()"):
96 | eval(f"Bind.{url}")
97 | else:
98 | Bind.listen(url)
99 |
100 |
from JAK.Utils import Instance, bindings
29 | if bindings() == "PyQt5":
30 | from PyQt5.QtCore import Qt
31 | else:
32 | from PySide2.QtCore import Qt
class KeyPress:
def __init__(self, event, config):
if event.type() == event.KeyPress:
84 | if event.key() == Qt.Key_F11:
85 | if config['webview']["online"] is True or config['window']["showHelpMenu"] is True:
86 | self.full_screen()
87 | elif event.key() == Qt.Key_F10:
88 | if config['webview']["online"] is True or config['window']["showHelpMenu"] is True:
89 | self.win = Instance.retrieve("win")
90 | self.win.corner_window()
91 |
92 | elif event.modifiers() == Qt.ControlModifier:
93 |
94 | if event.key() == Qt.Key_Minus:
95 | self._zoom_out()
96 |
97 | elif event.key() == Qt.Key_Equal:
98 | self._zoom_in()
def _current_zoom(self):
111 | self.view = Instance.retrieve("view")
112 | return self.view.zoomFactor()
def _zoom_in(self):
125 | new_zoom = self._current_zoom() * 1.5
126 | self.view.setZoomFactor(new_zoom)
127 | self._save_zoom()
def _zoom_out(self):
140 | new_zoom = self._current_zoom() / 1.5
141 | self.view.setZoomFactor(new_zoom)
142 | self._save_zoom()
TODO only zoom to a certain lvl then reset
152 | def _reset_zoom(self):
155 | self.view.setZoomFactor(1)
def _save_zoom(self):
168 | percent = int(self._current_zoom() * 100)
169 | print(f"Zoom:{percent}%")
TODO save zoom
179 | def full_screen(self):
TODO animate window resize
203 | self.win = Instance.retrieve("win")
206 | if self.win.isFullScreen():
207 | self.win.showNormal()
208 | self.win.hide_show_bar()
209 | else:
210 | self.win.showFullScreen()
211 | self.win.hide_show_bar()
212 |
213 |
from JAK.Utils import check_url_rules, bindings
29 | if bindings() == "PyQt5":
30 | from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo
31 | else:
32 | from PySide2.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo
class Interceptor(QWebEngineUrlRequestInterceptor):
def __init__(self, config):
72 | self.config = config
super(Interceptor, self).__init__()
def interceptRequest(self, info) -> None:
if self.config['webview']["urlRules"] is not None:
If we have any URL’s in the block dictionary
121 | url = info.requestUrl().toString()
124 | try:
125 | if check_url_rules("Block", url, self.config['webview']["urlRules"]["block"]):
block url’s
135 | info.block(True)
138 | print(f"Blocked:{url}")
139 | except KeyError:
140 | pass
141 |
142 | if self.config["debug"]:
143 | url = info.requestUrl().toString()
144 | resource = info.resourceType()
145 | if resource == QWebEngineUrlRequestInfo.ResourceType.ResourceTypeMainFrame:
146 | print(f"Intercepted link:{url}")
147 |
148 | elif resource != QWebEngineUrlRequestInfo.ResourceType.ResourceTypeMainFrame:
149 | print(f"Intercepted resource:{url}")
150 |
151 |
from JAK.Utils import bindings
29 | if bindings() == "PyQt5":
30 | from PyQt5.QtCore import Qt
31 | from PyQt5.QtWebEngineWidgets import QWebEngineSettings
32 | else:
33 | from PySide2.QtCore import Qt
34 | from PySide2.QtWebEngineWidgets import QWebEngineSettings
def config():
47 | return {
48 | "debug": False,
49 | "remote-debug": False,
50 | "setAAttribute": (),
51 | "disableGPU": False,
52 | "window": {
53 | "title": "Jade Application Kit",
54 | "icon": None,
55 | "backgroundImage": None,
56 | "setFlags": Qt.Window,
57 | "setAttribute": (),
58 | "state": None,
59 | "fullScreen": False,
60 | "transparent": False,
61 | "toolbar": None,
62 | "menus": None,
63 | "SystemTrayIcon": False,
64 | "showHelpMenu": False,
65 | },
66 | "webview": {
67 | "webContents": "https://codesardine.github.io/Jade-Application-Kit",
68 | "online": False,
69 | "urlRules": None,
70 | "cookiesPath": None,
71 | "userAgent": None,
72 | "addCSS": None,
73 | "runJavaScript": None,
74 | "IPC": True,
75 | "MediaAudioVideoCapture": False,
76 | "MediaVideoCapture": False,
77 | "MediaAudioCapture": False,
78 | "Geolocation": False,
79 | "MouseLock": False,
80 | "DesktopVideoCapture": False,
81 | "DesktopAudioVideoCapture": False,
82 | "injectJavaScript": {
83 | "JavaScript": None,
84 | "name": "Application Script"
85 | },
86 | "webChannel": {
87 | "active": False,
88 | "sharedOBJ": None
89 | },
90 | "enabledSettings": (
91 | QWebEngineSettings.JavascriptCanPaste,
92 | QWebEngineSettings.FullScreenSupportEnabled,
93 | QWebEngineSettings.AllowWindowActivationFromJavaScript,
94 | QWebEngineSettings.LocalContentCanAccessRemoteUrls,
95 | QWebEngineSettings.JavascriptCanAccessClipboard,
96 | QWebEngineSettings.SpatialNavigationEnabled,
97 | QWebEngineSettings.TouchIconsEnabled
98 | ),
99 | "disabledSettings": (
100 | QWebEngineSettings.PlaybackRequiresUserGesture
101 | )
102 | }
103 | }
104 |
105 |
__version__ = "v3.5.3"
24 | print(f"JAK {__version__}")
25 |
26 |