Compare commits
35 Commits
master
...
initial_de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8bd206856 | ||
|
|
fdf8e7d0a2 | ||
|
|
76d1121206 | ||
|
|
806941b3de | ||
|
|
ea96dd3669 | ||
|
|
9280a7b7cf | ||
|
|
7ce680b2fd | ||
|
|
20801e9872 | ||
|
|
9946330fa5 | ||
|
|
1c39435ded | ||
|
|
4c48a6b827 | ||
|
|
602273a219 | ||
|
|
cb491c7bbd | ||
|
|
77a13e2753 | ||
|
|
e1237985b8 | ||
|
|
2571e097e2 | ||
|
|
1f426ea6ae | ||
|
|
f4c5cbee37 | ||
|
|
79da5c9c1c | ||
|
|
ce8da72876 | ||
|
|
c63bc4f61c | ||
|
|
0e8db03be5 | ||
|
|
1ccdb845cc | ||
|
|
7c71483c8d | ||
|
|
9d7a092f38 | ||
|
|
9a3987ff55 | ||
|
|
cd8a2c2ce7 | ||
|
|
ecfb8032d9 | ||
|
|
0fc5e5cecf | ||
|
|
9291d870b9 | ||
|
|
87d3bcdc46 | ||
|
|
cbccf82671 | ||
|
|
dc3f3ecb18 | ||
|
|
32c32306d6 | ||
|
|
fb5131e480 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -531,4 +531,6 @@ healthchecksdb
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# End of https://www.gitignore.io/api/linux,macos,windows,visualstudio,visualstudiocode
|
||||
# End of https://www.gitignore.io/api/linux,macos,windows,visualstudio,visualstudiocode
|
||||
|
||||
.vscode
|
||||
0
portable_computing_toolkit_installer/__init__.py
Normal file
0
portable_computing_toolkit_installer/__init__.py
Normal file
22
portable_computing_toolkit_installer/__main__.py
Normal file
22
portable_computing_toolkit_installer/__main__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
from ui.installer_wizard import InstallerWizard
|
||||
|
||||
if __name__ == "__main__":
|
||||
# create the QApplication that will manage this window
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName("Portable Computing Toolkit Installer")
|
||||
try:
|
||||
app.setStyle(QtWidgets.QStyleFactory.create('WindowsVista'))
|
||||
except:
|
||||
app.setStyle(QtWidgets.QStyleFactory.create('Fusion'))
|
||||
app.setWindowIcon(QtGui.QIcon(str(Path(__file__).parent/"resources/icons/system-software-installer-2.png")))
|
||||
|
||||
# create a window
|
||||
main_window = InstallerWizard()
|
||||
main_window.show()
|
||||
|
||||
# execute the application
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1,491 @@
|
||||
[
|
||||
{
|
||||
"name": "Portable Apps Platform",
|
||||
"page": "https://portableapps.com/download",
|
||||
"category": "",
|
||||
"depends on": "Portable Apps Platform",
|
||||
"homepage": "https://portableapps.com",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Git",
|
||||
"page": "https://git-scm.com/download/win",
|
||||
"category": "development.tools.source code management",
|
||||
"homepage": "https://git-scm.com",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GitExtensions",
|
||||
"page": "https://github.com/gitextensions/gitextensions/releases/latest",
|
||||
"category": "development.tools.source code management",
|
||||
"depends on": "Git",
|
||||
"homepage": "http://gitextensions.github.io/",
|
||||
"icon url": "https://raw.githubusercontent.com/gitextensions/gitextensions/master/Logo/git-extensions-logo-64px.png",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WinPython",
|
||||
"page": "https://sourceforge.net/projects/winpython/",
|
||||
"category": "development.languages.python",
|
||||
"homepage": "https://winpython.github.io/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "MSYS2",
|
||||
"page": "https://www.msys2.org/",
|
||||
"category": "development.languages.c and c++",
|
||||
"homepage": "https://www.msys2.org",
|
||||
"icon url": "https://avatars1.githubusercontent.com/u/6759993?s=200&v=4",
|
||||
"ntfs_required": true,
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Win-builds",
|
||||
"page": "http://win-builds.org/doku.php/download_and_installation_from_windows",
|
||||
"category": "development.languages.c and c++",
|
||||
"homepage": "http://win-builds.org/doku.php/start",
|
||||
"ntfs_required": false,
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Java JDK",
|
||||
"page": "https://jdk.java.net/",
|
||||
"category": "development.languages.java",
|
||||
"versions": {
|
||||
"stable": {
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"version": "",
|
||||
"page": ""
|
||||
},
|
||||
"preview": {
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"version": "",
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
"homepage": "https://jdk.java.net",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SublimeText",
|
||||
"page": "https://www.sublimetext.com/3",
|
||||
"category": "development.tools.text editors and integrated development environments",
|
||||
"homepage": "https://www.sublimetext.com",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Atom",
|
||||
"page": "https://github.com/atom/atom/releases/latest",
|
||||
"category": "development.tools.text editors and integrated development environments",
|
||||
"homepage": "https://atom.io",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core SDK",
|
||||
"page": "https://dotnet.microsoft.com/download/dotnet-core",
|
||||
"category": "development.languages.C#",
|
||||
"homepage": "https://dotnet.microsoft.com",
|
||||
"versions": {
|
||||
"stable": {
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"version": "",
|
||||
"page": ""
|
||||
},
|
||||
"preview": {
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"version": "",
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Node.js",
|
||||
"page": "https://nodejs.org/en/",
|
||||
"category": "development.languages.javascript",
|
||||
"homepage": "https://nodejs.org",
|
||||
"versions": {
|
||||
"stable": {
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"version": "",
|
||||
"page": ""
|
||||
},
|
||||
"preview": {
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"version": "",
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Google Chrome",
|
||||
"page": "https://portableapps.com/apps/internet/google_chrome_portable",
|
||||
"category": "internet.browsers",
|
||||
"homepage": "https://google.com/chrome",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mozilla Firefox",
|
||||
"page": "https://portableapps.com/apps/internet/firefox_portable",
|
||||
"category": "internet.browsers",
|
||||
"homepage": "https://www.mozilla.org/en-US/firefox/new/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Cherrytree",
|
||||
"page": "https://portableapps.com/apps/office/cherrytree-portable",
|
||||
"category": "office.notes",
|
||||
"homepage": "https://www.giuspen.com/cherrytree/",
|
||||
"icon url": "https://www.giuspen.com/icons_softw/cherrytree.png",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "KeepNote",
|
||||
"page": "https://portableapps.com/apps/office/keepnote-portable",
|
||||
"category": "office.notes",
|
||||
"homepage": "http://keepnote.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "LibreOffice",
|
||||
"page": "https://portableapps.com/apps/office/libreoffice_portable",
|
||||
"category": "office.suite",
|
||||
"homepage": "https://libreoffice.org",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "RedNotebook",
|
||||
"page": "https://portableapps.com/apps/office/rednotebook_portable",
|
||||
"category": "office.notes",
|
||||
"homepage": "https://rednotebook.sourceforge.io/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Filezilla",
|
||||
"page": "https://portableapps.com/apps/internet/filezilla_portable",
|
||||
"category": "internet.file transfer",
|
||||
"homepage": "https://filezilla-project.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WinSCP",
|
||||
"page": "https://portableapps.com/apps/internet/winscp_portable",
|
||||
"category": "internet.file transfer",
|
||||
"homepage": "https://winscp.net/eng/index.php",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Cppcheck",
|
||||
"page": "https://portableapps.com/apps/development/cppcheck-portable",
|
||||
"category": "development.languages.c and c++",
|
||||
"homepage": "http://cppcheck.sourceforge.net/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"page": "https://portableapps.com/apps/development/frhed_portable",
|
||||
"category": "development.tools",
|
||||
"homepage": "http://frhed.sourceforge.net/en/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
},
|
||||
"name": "Frhed"
|
||||
},
|
||||
{
|
||||
"name": "Geany",
|
||||
"page": "https://portableapps.com/apps/development/geany_portable",
|
||||
"category": "development.tools.text editors and integrated development environments",
|
||||
"homepage": "https://geany.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gVim",
|
||||
"page": "https://portableapps.com/apps/development/gvim_portable",
|
||||
"category": "development.tools.text editors and integrated development environments",
|
||||
"homepage": "https://www.vim.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Notepad++",
|
||||
"page": "https://portableapps.com/apps/development/notepadpp_portable",
|
||||
"category": "development.tools.text editors and integrated development environments",
|
||||
"homepage": "https://notepad-plus-plus.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Peazip",
|
||||
"page": "https://portableapps.com/apps/utilities/peazip_portable",
|
||||
"category": "utilities.archiving",
|
||||
"homepage": "http://www.peazip.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7-Zip",
|
||||
"page": "https://portableapps.com/apps/utilities/7-zip_portable",
|
||||
"category": "utilities.archiving",
|
||||
"homepage": "https://7-zip.org",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Explorer++",
|
||||
"page": "https://portableapps.com/apps/utilities/explorerplusplus_portable",
|
||||
"category": "utilities.file browsers",
|
||||
"homepage": "https://explorerplusplus.com/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WinMerge",
|
||||
"page": "https://portableapps.com/apps/utilities/winmerge_portable",
|
||||
"category": "development.tools.file comparison",
|
||||
"homepage": "http://winmerge.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Command Prompt Portable",
|
||||
"page": "https://portableapps.com/apps/utilities/command_prompt_portable",
|
||||
"category": "utilities.terminals",
|
||||
"homepage": "https://portableapps.com/apps/utilities/command_prompt_portable",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Console",
|
||||
"page": "https://portableapps.com/apps/utilities/console_portable",
|
||||
"category": "utilities.terminals",
|
||||
"homepage": "https://sourceforge.net/projects/console/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Terminus",
|
||||
"page": "https://github.com/Eugeny/terminus/releases/latest",
|
||||
"category": "utilities.terminals",
|
||||
"homepage": "https://eugeny.github.io/terminus/",
|
||||
"icon url": "https://raw.githubusercontent.com/Eugeny/terminus/master/app/assets/tray.png",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "KiTTY",
|
||||
"page": "https://portableapps.com/apps/internet/kitty-portable",
|
||||
"category": "utilities.terminals.remote",
|
||||
"homepage": "http://www.9bis.net/kitty/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PuTTY",
|
||||
"page": "https://portableapps.com/apps/internet/putty_portable",
|
||||
"category": "utilities.terminals.remote",
|
||||
"homepage": "https://www.chiark.greenend.org.uk/~sgtatham/putty/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Microsoft Visual Studio Code",
|
||||
"page": "https://code.visualstudio.com/docs/?dv=win64user",
|
||||
"category": "development.tools.text editors and integrated development environments",
|
||||
"depends on": "Git",
|
||||
"homepage": "https://code.visualstudio.com",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Umbrello",
|
||||
"page": "https://download.kde.org/stable/umbrello/latest/win64/",
|
||||
"category": "development.tools",
|
||||
"homepage": "https://umbrello.kde.org/",
|
||||
"icon url": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Umbrello-icon.svg/128px-Umbrello-icon.svg.png",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "QOwnNotes",
|
||||
"page": "https://portableapps.com/apps/office/qownnotes-portable",
|
||||
"category": "office.notes",
|
||||
"homepage": "https://www.qownnotes.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SageMath",
|
||||
"page": "http://mirrors.mit.edu/sage/win/index.html",
|
||||
"category": "education.mathmatics",
|
||||
"homepage": "http://www.sagemath.org/",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GNU Octave",
|
||||
"page": "https://www.gnu.org/software/octave/",
|
||||
"category": "education.mathmatics",
|
||||
"homepage": "https://www.gnu.org/software/octave/",
|
||||
"icon url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Gnu-octave-logo.svg/240px-Gnu-octave-logo.svg.png",
|
||||
"search": {
|
||||
"selector": "",
|
||||
"filename regex": "",
|
||||
"text regex": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
30
portable_computing_toolkit_installer/tests/test_json.py
Normal file
30
portable_computing_toolkit_installer/tests/test_json.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
json_url = "https://raw.githubusercontent.com/norweeg/portable-computing-toolkit-installer/initial_dev/portable_computing_toolkit_installer/resources/supported_tools.json"
|
||||
|
||||
class TestJSON(object):
|
||||
def test_json_url_valid(self):
|
||||
global json_url
|
||||
parsed = urlparse(json_url)
|
||||
assert len(parsed) == 6
|
||||
assert Path(parsed.path).suffix == ".json"
|
||||
|
||||
def test_json_accessible(self):
|
||||
global json_url
|
||||
json_file = urlopen(json_url)
|
||||
assert json_file.code < 400
|
||||
json_file.close()
|
||||
|
||||
def test_json_valid(self):
|
||||
global json_url
|
||||
with urlopen(json_url) as json_file:
|
||||
json_data = json.load(json_file)
|
||||
assert json_data
|
||||
|
||||
|
||||
26
portable_computing_toolkit_installer/tests/test_pages.py
Normal file
26
portable_computing_toolkit_installer/tests/test_pages.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import json
|
||||
from urllib.request import urlopen
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from parsel import Selector
|
||||
from w3lib.url import canonicalize_url
|
||||
|
||||
import pytest
|
||||
|
||||
json_url = "https://raw.githubusercontent.com/norweeg/portable-computing-toolkit-installer/initial_dev/portable_computing_toolkit_installer/resources/supported_tools.json"
|
||||
|
||||
class TestPages(object):
|
||||
def test_pages_accessible(self):
|
||||
global json_url
|
||||
with urlopen(json_url) as json_file:
|
||||
json_data = json.load(json_file)
|
||||
with requests.Session() as session:
|
||||
for page in [canonicalize_url(item["homepage"]) for item in json_data]:
|
||||
try:
|
||||
response = session.get(page, allow_redirects = True, verify = False)
|
||||
except:
|
||||
response = session.get(page.replace("http", "https", 1), allow_redirects = True, verify = False)
|
||||
assert response.status_code < 400
|
||||
html = response.text
|
||||
assert html
|
||||
assert Selector(text = html, type="html").getall()
|
||||
0
portable_computing_toolkit_installer/ui/__init__.py
Normal file
0
portable_computing_toolkit_installer/ui/__init__.py
Normal file
248
portable_computing_toolkit_installer/ui/downloader.py
Normal file
248
portable_computing_toolkit_installer/ui/downloader.py
Normal file
@@ -0,0 +1,248 @@
|
||||
import platform
|
||||
import re
|
||||
import webbrowser
|
||||
from collections import deque, namedtuple
|
||||
from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from parsel import Selector
|
||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWebEngineWidgets, QtWidgets
|
||||
from PyQt5.uic import loadUi
|
||||
|
||||
try:
|
||||
import win32api
|
||||
except ImportError:
|
||||
if platform.system()=='Windows':
|
||||
raise #require win32api on windows
|
||||
|
||||
|
||||
|
||||
class Downloader(QtWidgets.QMainWindow):
|
||||
|
||||
download_progress = QtCore.pyqtSignal(str, int, int)
|
||||
LoadedPage = namedtuple("LoadedPage", ("success", "html"))
|
||||
|
||||
def __init__(self, download_queue, install_ready, download_errors, download_directory, parent = None, flags = None):
|
||||
super().__init__(parent, flags)
|
||||
loadUi(Path(__file__).parent / "downloader.ui", baseinstance = self)
|
||||
self.hide()
|
||||
#create an off-the-record profile, isolated from other Downloader objects
|
||||
new_page = QtWebEngineWidgets.QtWebEnginePage(QtWebEngineWidgets.QWebEngineProfile(self.web_view))
|
||||
self.web_view.setPage(new_page)
|
||||
self._load_result = Queue(maxsize=1)
|
||||
#set application style and decorate pushbuttons
|
||||
self._style = QtWidgets.QApplication.instance().style()
|
||||
self.setWindowIcon(QtWidgets.QApplication.instance().windowIcon())
|
||||
self.back_button.setIcon(self._style.standardIcon(self._style.SP_ArrowBack))
|
||||
self.forward_button.setIcon(self._style.standardIcon(self._style.SP_ArrowForward))
|
||||
self.reload_stop_button.setIcon(self._style.standardIcon(self._style.SP_BrowserReload))
|
||||
self.home_button.setIcon(self._style.standardIcon(self._style.SP_ArrowUp))
|
||||
#connect signals to make useable as a browser
|
||||
self.web_view.urlChanged.connect(lambda url: self.address_bar.setText(url.toString()))
|
||||
self.web_view.loadStarted.connect(self._reload_becomes_stop)
|
||||
self.web_view.loadFinished.connect(self._stop_becomes_reload)
|
||||
self.address_bar.returnPressed.connect(lambda: self.web_view.load(QtCore.QUrl(self.address_bar.text())))
|
||||
#connect cancel actions to handlers
|
||||
self.actionCancel.triggered.connect(self._cancel_manual_search)
|
||||
self.actionCancel_and_Open_Issue_on_Github.triggered.connect(self._cancel_manual_search_open_issue)
|
||||
#set attributes
|
||||
self._download_queue = download_queue
|
||||
self._install_ready = install_ready
|
||||
self._download_errors = download_errors
|
||||
self._download_directory = download_directory
|
||||
|
||||
|
||||
def _reload_becomes_stop(self):
|
||||
"""Turns the reload button into a stop button.
|
||||
"""
|
||||
self._reload_stop_button.clicked.disconnect()
|
||||
self._reload_stop_button.clicked.connect(self.web_view.stop)
|
||||
self._reload_stop_button.setToolTip("Stop")
|
||||
self._reload_stop_button.setIcon(self._style.standardIcon(self._style.SP_BrowserStop))
|
||||
self._check_history()
|
||||
|
||||
def _stop_becomes_reload(self, ok):
|
||||
"""Turns a temporary stop button back into a reload button
|
||||
|
||||
Args:
|
||||
ok (bool): Received from signal indicating load was stopped without error (True) or because of error (False). Ignored.
|
||||
"""
|
||||
self._reload_stop_button.clicked.disconnect()
|
||||
self._reload_stop_button.clicked.connect(self.web_view.reload)
|
||||
self._reload_stop_button.setToolTip("Reload")
|
||||
self._reload_stop_button.setIcon(self._style.standardIcon(self._style.SP_BrowserReload))
|
||||
self._check_history()
|
||||
|
||||
def _check_history(self):
|
||||
"""Checks if the current web view has history to navigate forward or backward to. Disables or enables the forward/backward buttons accordingly
|
||||
"""
|
||||
if self.web_view.history().canGoBack():
|
||||
self.back_button.setEnabled(True)
|
||||
else:
|
||||
self.back_button.setEnabled(False)
|
||||
|
||||
if self.web_view.history().canGoForward():
|
||||
self.forward_button.setEnabled(True)
|
||||
else:
|
||||
self.forward_button.setEnabled(False)
|
||||
|
||||
def _cancel_manual_search(self):
|
||||
"""Action handler for the cancel action. Puts the received tool info into errors queue and immediately exits
|
||||
"""
|
||||
self._download_errors.put(self._tool_info)
|
||||
self.close()
|
||||
self.deleteLater()
|
||||
QtCore.QThread.currentThread().exit(1)
|
||||
|
||||
def _cancel_manual_search_open_issue(self):
|
||||
"""Action handler for the cancel and open a bug report. Same as cancel, except it will open the github issues page in the default browser first
|
||||
"""
|
||||
self._download_errors.put(self._tool_info)
|
||||
webbrowser.open_new_tab("https://github.com/norweeg/portable-computing-toolkit-installer/issues/new")
|
||||
self.close()
|
||||
self.deleteLater()
|
||||
QtCore.QThread.currentThread().exit(1)
|
||||
|
||||
def begin_manual_search(self):
|
||||
self.web_view.history().clear()
|
||||
self._check_history()
|
||||
self.show()
|
||||
self.web_view.load(QtCore.QUrl(self._home_page))
|
||||
|
||||
def begin_auto_search(self):
|
||||
"""Begins the search process by getting tool info from a download queue and finalizes some GUI elements (in case manual search is necessary)
|
||||
"""
|
||||
try:
|
||||
tool_info = self._download_queue.get(timeout = 1)
|
||||
except Empty:
|
||||
QtCore.QThread.currentThread().quit()
|
||||
else:
|
||||
self._tool_info = tool_info
|
||||
self._tool_name = tool_info["name"]
|
||||
self._download_page_selector = deque(tool_info["search"]["selector"])
|
||||
self._home_page = tool_info["homepage"]
|
||||
#name window from attribute
|
||||
self.setWindowTitle(f"Find {self._tool_name}")
|
||||
#get homepage from attributes and set home button to load it
|
||||
self.home_button.clicked.connect(lambda: self.web_view.load(QtCore.QUrl(self._home_page)))
|
||||
self.web_view.loadFinished.connect(lambda status: self._load_result.put(Downloader.LoadedPage(status, self.web_view.page().toHtml())))
|
||||
try:
|
||||
self._download_file(self._find_installer_url(self._tool_info["page"]))
|
||||
except:
|
||||
self.begin_manual_search()
|
||||
|
||||
def _find_installer_url(self, url):
|
||||
"""Finds the URL of the next page to navigate to or the item to download using the search selectors from a tool info dictionary. The fully-rendered
|
||||
HTML will be scraped, in case the desired URL is rendered by JavaScript
|
||||
|
||||
Args:
|
||||
url (str): The URL of the page to be scraped
|
||||
"""
|
||||
#load the url with a browser that will render page, including urls generated by JavaScript and get result
|
||||
self.web_view.load(QtCore.QUrl(url))
|
||||
load_result = self._load_result.get()
|
||||
#if page loaded successfully, parse the results, otherwise return nothing
|
||||
if load_result.success:
|
||||
page = Selector(text = load_result.html)
|
||||
self._load_result.task_done()
|
||||
else:
|
||||
self._load_result.task_done()
|
||||
return
|
||||
'''if tool has a download page selector, we need to follow one or more links from the "page" url to find the download page to search
|
||||
portableapps.com loves to make you do this'''
|
||||
if self._download_page_selector:
|
||||
next_page_query = self._download_page_selector.popleft()
|
||||
return self._find_installer_url(urljoin(url, page.css(next_page_query).attrib["href"]))
|
||||
#get all links in the current page and search for the tool's installer file using a regular expression
|
||||
for link in [urljoin(url, anchor.attrib["href"]) for anchor in page.xpath("//a")]:
|
||||
if re.search(self._tool_info["search"]["filename regex"], link):
|
||||
#the link matched the regular expression, our download URL is found!
|
||||
return link
|
||||
#if nothing was found by now, returns nothing. automated search has failed
|
||||
return
|
||||
|
||||
def _download_file(self, url):
|
||||
"""Initiates a download of url
|
||||
|
||||
Args:
|
||||
url (str): The URL of the file to be downloaded
|
||||
"""
|
||||
#no url means autosearch has failed
|
||||
assert url
|
||||
try:
|
||||
self.web_view.loadFinished.disconnect()
|
||||
except:
|
||||
pass
|
||||
self.web_view.page().profile().downloadRequested.connect(self._process_download)
|
||||
self.web_view.load(url)
|
||||
|
||||
@QtCore.pyqtSlot("QWebEngineDownloadItem*")
|
||||
def _process_download(self, download_item):
|
||||
self._tool_info["download url"] = download_item.url().toString()
|
||||
self._tool_info["mimetype"] = download_item.mimeType()
|
||||
suggested_path = Path(download_item.path())
|
||||
filename = suggested_path.name
|
||||
download_item.setPath(str(Path(self._download_directory/filename)))
|
||||
self._tool_info["filename"] = Path(download_item.path())
|
||||
download_item.downloadProgress.connect(lambda x, y: self.download_progress.emit(self._tool_name, x, y))
|
||||
download_item.stateChanged.connect(self._download_interrupted)
|
||||
download_item.finished(self._check_finished)
|
||||
download_item.accept()
|
||||
|
||||
@QtCore.pyqtSlot("QWebEngineDownloadItem::DownloadState")
|
||||
def _download_interrupted(self, state):
|
||||
"""Receives the QWebEngineDownloadItem's stateChanged signal and, if the state is "interrupted", trigger a failure
|
||||
"""
|
||||
if state == QtWebEngineWidgets.QWebEngineDownloadItem.DownloadInterrupted:
|
||||
sender = self.sender()
|
||||
self._tool_info["error reason"] = sender.interruptReasonString()
|
||||
sender.cancel()
|
||||
self._tool_info["filename"].unlink()
|
||||
self._tool_info.pop("filename", None)
|
||||
self._download_errors.put(self._tool_info)
|
||||
self.close()
|
||||
self.deleteLater()
|
||||
QtCore.QThread.currentThread().exit(1)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def _check_finished(self):
|
||||
"""Receives the QWebEngineDownloadItem's finished signal, verifies success, unblocks the file an triggers a success
|
||||
"""
|
||||
#get the downloaditem that sent this signal
|
||||
sender = self.sender()
|
||||
#if the download item is completed, unblock the file on windows, then close and exit this thread
|
||||
if sender.state() == QtWebEngineWidgets.QWebEngineDownloadItem.DownloadCompleted:
|
||||
if platform.system() == 'Windows':
|
||||
self._unblock_file()
|
||||
#check if the total bytes in the download is known. -1 means not known.
|
||||
if sender.totalBytes() == -1:
|
||||
#if not known, signal that this download has completed, since the progress thusfar has been ignored
|
||||
self.download_progress.emit(self._tool_name, 1, 1)
|
||||
self._install_ready.put(self._tool_info)
|
||||
self.close()
|
||||
self.deleteLater()
|
||||
QtCore.QThread.currentThread().quit()
|
||||
|
||||
def _unblock_file(self):
|
||||
"""Removes the "downladed from internet" Zone Identifier. Windows will prevent executables that have this set from executing.
|
||||
Contents of zip files inherit this from the .zip they come from
|
||||
"""
|
||||
try:
|
||||
win32api.DeleteFile(str(self._tool_info["filename"])+r":Zone.Identifier")
|
||||
except win32api.error:
|
||||
#just ignore the error if the above Zone Identifier is not set
|
||||
pass
|
||||
|
||||
class DownloadWorker(QtCore.QRunnable):
|
||||
def __init__(self, download_queue, install_ready, download_error, download_directory, wizard):
|
||||
super().__init__()
|
||||
self._download_queue = download_queue
|
||||
self._install_ready = install_ready
|
||||
self._download_directory = download_directory
|
||||
self._wizard = wizard
|
||||
|
||||
def run(self):
|
||||
downloader = Downloader(self.download_queue, self._install_ready, self._download_error, self._download_directory)
|
||||
downloader.download_progress.connect(self._wizard.track_progress)
|
||||
downloader.begin_auto_search()
|
||||
182
portable_computing_toolkit_installer/ui/downloader.ui
Normal file
182
portable_computing_toolkit_installer/ui/downloader.ui
Normal file
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="back_button">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Back</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="forward_button">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Forward</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="reload_stop_button">
|
||||
<property name="toolTip">
|
||||
<string>Reload</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="home_button">
|
||||
<property name="toolTip">
|
||||
<string>Home</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="address_bar">
|
||||
<property name="placeholderText">
|
||||
<string>Enter a URL</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWebEngineView" name="web_view">
|
||||
<property name="url">
|
||||
<url>
|
||||
<string>about:blank</string>
|
||||
</url>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuActions">
|
||||
<property name="title">
|
||||
<string>Actions</string>
|
||||
</property>
|
||||
<addaction name="actionCancel"/>
|
||||
<addaction name="actionCancel_and_Open_Issue_on_Github"/>
|
||||
</widget>
|
||||
<addaction name="menuActions"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<action name="actionCancel">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCancel_and_Open_Issue_on_Github">
|
||||
<property name="text">
|
||||
<string>Cancel and Open Issue on Github</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QWebEngineView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>back_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>web_view</receiver>
|
||||
<slot>back()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>34</x>
|
||||
<y>51</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>399</x>
|
||||
<y>324</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>forward_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>web_view</receiver>
|
||||
<slot>forward()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>72</x>
|
||||
<y>51</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>399</x>
|
||||
<y>324</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>reload_stop_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>web_view</receiver>
|
||||
<slot>reload()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>110</x>
|
||||
<y>51</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>399</x>
|
||||
<y>324</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
382
portable_computing_toolkit_installer/ui/installer_wizard.py
Executable file
382
portable_computing_toolkit_installer/ui/installer_wizard.py
Executable file
@@ -0,0 +1,382 @@
|
||||
import platform
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
import json
|
||||
from collections import deque
|
||||
from itertools import product
|
||||
from functools import partial
|
||||
from threading import Lock
|
||||
|
||||
import psutil
|
||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWebEngineWidgets, QtWidgets
|
||||
from PyQt5.uic import loadUi
|
||||
|
||||
try:
|
||||
import win32file
|
||||
except ImportError:
|
||||
if platform.system()=='Windows':
|
||||
raise #require win32file on windows
|
||||
|
||||
from validators.location_validator import Location_Validator
|
||||
from .menu_iterator import MenuIterator
|
||||
from .downloader import DownloadWorker, Downloader
|
||||
|
||||
class InstallerWizard(QtWidgets.QWizard):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
loadUi(Path(__file__).parent / "installer_wizard.ui", baseinstance=self)
|
||||
self.setWindowIcon(QtWidgets.QApplication.instance().windowIcon())
|
||||
self.intro_page.validatePage = self._requirements_check
|
||||
self.selection_page.initializePage = self._load_tools
|
||||
self.intro.anchorClicked.connect(self._link_clicked)
|
||||
self.license.anchorClicked.connect(self._link_clicked)
|
||||
self.selection_menu.itemClicked.connect(self._enforce_dependencies)
|
||||
self._location_validator = Location_Validator(self)
|
||||
try:
|
||||
self.install_location.inputRejected.connect(self.install_location.clear)
|
||||
except AttributeError:
|
||||
pass #inputRejected signal added in Qt 5.12, I have 5.9
|
||||
self.button(self.BackButton).clicked.connect(lambda x: self._location_validator.reset_state())
|
||||
self.install_location.setValidator(self._location_validator)
|
||||
self.location_page.registerField("Location*", self.install_location)
|
||||
self.browse_button.clicked.connect(self._select_location)
|
||||
self.selection_menu.itemClicked.connect(self._open_homepage)
|
||||
self.selection_menu.itemCollapsed.connect(lambda x: self.selection_menu.resizeColumnToContents(0))
|
||||
self.selection_menu.itemExpanded.connect(lambda x: self.selection_menu.resizeColumnToContents(0))
|
||||
self.selection_page.setCommitPage(True)
|
||||
self.progress_page.setFinalPage(True)
|
||||
#Change button text on license page
|
||||
self.license_page.setButtonText(QtWidgets.QWizard.NextButton, "Agree")
|
||||
self.license_page.setButtonText(QtWidgets.QWizard.CancelButton, "Decline")
|
||||
self._network_manager = QtNetwork.QNetworkAccessManager(parent=QtWidgets.QApplication.instance())
|
||||
self._thread_pool = QtCore.QThreadPool.globalInstance()
|
||||
self._project_page=Path("https://github.com/norweeg/portable-computing-toolkit-installer/")
|
||||
self._progress_update_lock = Lock()
|
||||
|
||||
@QtCore.pyqtSlot("QUrl")
|
||||
def _link_clicked(self, address):
|
||||
"""Receives anchorClicked signal from the intro or license text browsers and opens the address in an external web browser.
|
||||
Opens an error message window if it is not able to do so.
|
||||
|
||||
Arguments:
|
||||
address {QtCore.QUrl} -- The URL from the received signal
|
||||
"""
|
||||
try:
|
||||
webbrowser.open(address.toString())
|
||||
except webbrowser.Error as e:
|
||||
self._display_error("Unable to launch external browser",e)
|
||||
except Exception as e:
|
||||
self._display_error("An error has occurred",e)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def _select_location(self):
|
||||
"""Opens a QFileDialog in either the root of the most-recently connected non-network, non-cd drive or the user's home directory,
|
||||
if no suitable external device is found
|
||||
"""
|
||||
if platform.system() == 'Windows':
|
||||
#get all drives read-writable drives except C:\
|
||||
drives = [drive for drive in psutil.disk_partitions(all = True) if drive.device not in ("","C:\\") and "rw" in drive.opts]
|
||||
if drives: #drives other than C:\ exist, are likely installation locations
|
||||
dialog_location = Path(drives[0].mountpoint)
|
||||
else: #C:\ was only drive, so start in user home
|
||||
dialog_location = Path.home()
|
||||
else:
|
||||
dialog_location = Path.home()
|
||||
|
||||
selected_location = QtWidgets.QFileDialog.getExistingDirectory(parent=self,caption="Select Location",
|
||||
directory=str(dialog_location),
|
||||
options=(QtWidgets.QFileDialog.ShowDirsOnly))
|
||||
self.install_location.setText(selected_location)
|
||||
|
||||
def _requirements_check(self):
|
||||
"""Checks that the minimum system requirements are met. Opens error message and resets the wizard if they are not.
|
||||
Minimum Requirements: Windows 7 or greater (64-bit)
|
||||
"""
|
||||
process_dialog = QtWidgets.QProgressDialog("Checking System Requirements...", str(), 0, 100, self, QtCore.Qt.Dialog)
|
||||
process_dialog.setAutoClose(True)
|
||||
process_dialog.setAutoReset(True)
|
||||
process_dialog.setCancelButton(None)
|
||||
if self._network_manager.networkAccessible():
|
||||
request = QtNetwork.QNetworkRequest(QtCore.QUrl("https://www.gnu.org/licenses/gpl-3.0-standalone.html"))
|
||||
request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute, QtNetwork.QNetworkRequest.AlwaysNetwork)
|
||||
request.setAttribute(QtNetwork.QNetworkRequest.FollowRedirectsAttribute, True)
|
||||
reply = self._network_manager.get(request)
|
||||
reply.downloadProgress.connect(lambda received, total: process_dialog.setMaximum(total))
|
||||
reply.downloadProgress.connect(lambda received, total: process_dialog.setValue(received))
|
||||
process_dialog.show()
|
||||
else:
|
||||
self._display_error("This installer requires an active internet connection, but you do not appear to be online")
|
||||
return False
|
||||
|
||||
if not platform.system() == 'Windows' or int(platform.release()) < 7:
|
||||
if QtWidgets.QMessageBox.question(self, "", "This toolkit requires Microsoft Windows 7 or newer to be used. Continue anyway?") in (QtWidgets.QMessageBox.No, 0):
|
||||
reply.abort()
|
||||
process_dialog.cancel()
|
||||
return False
|
||||
|
||||
if not platform.machine().lower() in ('amd64', 'x86_64'):
|
||||
if QtWidgets.QMessageBox.question(self, "", "Parts of this toolkit require a 64-bit processor. Continue anyway?") in (QtWidgets.QMessageBox.No, 0):
|
||||
reply.abort()
|
||||
process_dialog.cancel()
|
||||
return False
|
||||
|
||||
while reply.isRunning():
|
||||
QtWidgets.QApplication.instance().processEvents()
|
||||
QtCore.QThread.currentThread().msleep(100)
|
||||
if not reply.error():
|
||||
self.license.setHtml(bytearray(reply.readAll()).decode("utf-8"))
|
||||
return True
|
||||
else:
|
||||
self._display_error(f"Encountered an error while testing Internet connectivity:\n\n{reply.errorString()}")
|
||||
return False
|
||||
|
||||
def _display_error(self, message, details = None):
|
||||
"""Displays an error message.
|
||||
|
||||
Arguments:
|
||||
message {str} -- The contents of the error message to be displayed
|
||||
|
||||
Keyword Arguments:
|
||||
details {Any} -- Any string-printable object that will be set in the more details, typically an Exception object (default: {None})
|
||||
"""
|
||||
message=QtWidgets.QMessageBox(icon=QtWidgets.QMessageBox.Critical,parent=self,text=message)
|
||||
if details:
|
||||
message.setDetailedText(str(details))
|
||||
message.show()
|
||||
return message
|
||||
|
||||
def _load_tools(self):
|
||||
"""Loads tool data from supported_tools.json hosted on the project Github page
|
||||
"""
|
||||
process_dialog = QtWidgets.QProgressDialog("Loading tool information...", str(), 0, 100, self, QtCore.Qt.Dialog)
|
||||
process_dialog.setAutoClose(True)
|
||||
process_dialog.setAutoReset(True)
|
||||
process_dialog.setCancelButton(None)
|
||||
try: #clear data from this page before reloading
|
||||
self.__tools__ = None
|
||||
self._icon_cache = {item.text(0):item.icon(0) for item in list(MenuIterator(self.selection_menu, QtWidgets.QTreeWidgetItemIterator.NoChildren))}
|
||||
self.selection_menu.clear()
|
||||
except NameError:
|
||||
pass
|
||||
request = QtNetwork.QNetworkRequest(QtCore.QUrl("https://raw.githubusercontent.com/norweeg/portable-computing-toolkit-installer/initial_dev/portable_computing_toolkit_installer/resources/supported_tools.json"))
|
||||
request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute, QtNetwork.QNetworkRequest.AlwaysNetwork)
|
||||
request.setAttribute(QtNetwork.QNetworkRequest.FollowRedirectsAttribute, True)
|
||||
reply = self._network_manager.get(request)
|
||||
reply.downloadProgress.connect(lambda received, total: process_dialog.setMaximum(total))
|
||||
reply.downloadProgress.connect(lambda received, total: process_dialog.setValue(received))
|
||||
process_dialog.show()
|
||||
while reply.isRunning():
|
||||
QtWidgets.QApplication.instance().processEvents()
|
||||
QtCore.QThread.currentThread().msleep(100)
|
||||
if not reply.error():
|
||||
try:
|
||||
self.__tools__ = json.loads(bytearray(reply.readAll()))
|
||||
except json.JSONDecodeError as e:
|
||||
self._display_error(f"Unable to decode {request.url.toString()}", e).accepted.connect(self.back)
|
||||
else:
|
||||
self._populate_menu()
|
||||
else:
|
||||
self._display_error(f"Encountered an error while loading supported tools from {request.url().toString()}", reply.errorString()).finished.connect(lambda x: self.back())
|
||||
|
||||
def _populate_menu(self):
|
||||
"""Populates the selection menu with items loaded from supported_tools.json
|
||||
"""
|
||||
process_dialog = QtWidgets.QProgressDialog("Loading tool information...", str(), 0, len(self.__tools__)-1, self, QtCore.Qt.Dialog)
|
||||
process_dialog.setAutoClose(True)
|
||||
process_dialog.setAutoReset(True)
|
||||
process_dialog.setCancelButton(None)
|
||||
process_dialog.show()
|
||||
for tool in sorted(self.__tools__, key = lambda x: x["category"] + f".{x['name']}"):
|
||||
placement = deque([level.capitalize().strip() for level in tool["category"].split(".") if level] + [tool["name"]])
|
||||
root = self.selection_menu.invisibleRootItem()
|
||||
self._add_menu_item(placement, root, tool)
|
||||
process_dialog.setValue(process_dialog.value() + 1)
|
||||
|
||||
for item in [self.selection_menu.topLevelItem(i) for i in range(self.selection_menu.topLevelItemCount())]:
|
||||
item.setExpanded(True)
|
||||
|
||||
def _add_menu_item(self, placement, root, tool):
|
||||
current_level = placement.popleft()
|
||||
children = [root.child(i) for i in range(root.childCount())]
|
||||
|
||||
if any([item is child for item, child in product(self.selection_menu.findItems(current_level,
|
||||
QtCore.Qt.MatchFixedString|QtCore.Qt.MatchCaseSensitive|QtCore.Qt.MatchRecursive), children)]):
|
||||
root = list(filter(lambda child: child.text(0) == current_level, children)).pop()
|
||||
self._add_menu_item(placement, root, tool)
|
||||
else:
|
||||
new_item = QtWidgets.QTreeWidgetItem(root, QtWidgets.QTreeWidgetItem.Type)
|
||||
new_item.setText(0, current_level)
|
||||
new_item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled)
|
||||
new_item.setCheckState(0, QtCore.Qt.Unchecked)
|
||||
if not placement: #all items popped from placement
|
||||
new_item.setFlags(new_item.flags()|QtCore.Qt.ItemNeverHasChildren)
|
||||
new_item.setData(1, QtCore.Qt.DecorationRole, QtGui.QIcon(str(Path(__file__).parent.parent/"resources/icons/internet-web-browser-4.png")))
|
||||
new_item.setData(1, QtCore.Qt.ToolTipRole, tool["homepage"])
|
||||
try:
|
||||
new_item.setIcon(0, self._icon_cache[new_item.text(0)])
|
||||
except (AttributeError, KeyError):
|
||||
try:
|
||||
self._get_icon(new_item, tool["icon url"])
|
||||
except KeyError:
|
||||
self._get_icon(new_item)
|
||||
try:
|
||||
if tool["depends on"] == current_level: #tool depends on itself makes it mandatory
|
||||
new_item.setFlags(new_item.flags()^QtCore.Qt.ItemIsUserCheckable)
|
||||
new_item.setCheckState(0, QtCore.Qt.Checked)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
new_item.setFlags(new_item.flags()|QtCore.Qt.ItemIsAutoTristate)
|
||||
new_item.setExpanded(False)
|
||||
new_item.addChild(self._add_menu_item(placement, new_item, tool))
|
||||
return new_item
|
||||
|
||||
@QtCore.pyqtSlot("QTreeWidgetItem*", "int")
|
||||
def _open_homepage(self, item, column):
|
||||
"""Receives the itemClicked signal from the selection_menu QTreeWidget and if the user has clicked a homepage, opens it in a new browser tab.
|
||||
|
||||
Arguments:
|
||||
item {QTreeWidgetItem} -- The item that was clicked
|
||||
column {int} -- The index of the column of the item that was clicked
|
||||
"""
|
||||
try:
|
||||
#if the item has a valid URL, open it, otherwise do nothing
|
||||
if column and QtCore.QUrl(item.data(column, QtCore.Qt.ToolTipRole), QtCore.QUrl.StrictMode).scheme().startswith("http"):
|
||||
webbrowser.open_new_tab(item.data(column, QtCore.Qt.ToolTipRole))
|
||||
except:
|
||||
pass
|
||||
|
||||
def _get_icon(self, tree_item, icon_url = None):
|
||||
"""Given a QTreeWidgetItem, this will initiate a request to download an icon for it. If icon_url is specified, that will be fetched, otherwise
|
||||
the favicon of the tree item's homepage.
|
||||
|
||||
Arguments:
|
||||
tree_item {QTreeWidgetItem} -- a tree item with a specified homepage to set an icon on
|
||||
|
||||
Keyword Arguments:
|
||||
icon_url {str} -- A string representing the url of the icon to fetch instead of the favicon of the homepage (default: {None})
|
||||
"""
|
||||
if not icon_url:
|
||||
icon_url = QtCore.QUrl(f"http://www.google.com/s2/favicons?domain={QtCore.QUrl(tree_item.data(1, QtCore.Qt.ToolTipRole)).toString()}")
|
||||
icon_request = QtNetwork.QNetworkRequest(QtCore.QUrl(icon_url))
|
||||
icon_request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute, QtNetwork.QNetworkRequest.AlwaysNetwork)
|
||||
icon_request.setAttribute(QtNetwork.QNetworkRequest.FollowRedirectsAttribute, True)
|
||||
icon_reply = self._network_manager.get(icon_request)
|
||||
#when download is complete, validates request returned without error and sets icon
|
||||
icon_reply.finished.connect(partial(self._set_icon, tree_item, icon_reply))
|
||||
|
||||
def _set_icon(self, tree_item, reply):
|
||||
"""Validates a network request and sets its result as the icon of a QTreeWidgetItem.
|
||||
|
||||
Arguments:
|
||||
tree_item {QTreeWidgetItem} -- A tree widget item to set the icon of
|
||||
request {QNetworkReply} -- The results of the HTTP GET request whose data will be used as the icon for tree_item
|
||||
"""
|
||||
if not reply.error():
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.loadFromData(reply.readAll())
|
||||
tree_item.setIcon(0, QtGui.QIcon(pixmap))
|
||||
|
||||
def _get_selections(self):
|
||||
"""Returns a subset of the supported tools whose corresponding item was selected by the user.
|
||||
"""
|
||||
return list(MenuIterator(self.selection_menu, QtWidgets.QTreeWidgetItemIterator.Checked|QtWidgets.QTreeWidgetItemIterator.NoChildren))
|
||||
|
||||
@QtCore.pyqtSlot("QTreeWidgetItem*","int")
|
||||
def _enforce_dependencies(self, item, column):
|
||||
if column: #ignore everything not in column 0 (the names)
|
||||
return
|
||||
elif item.childCount(): #recurse through child items til no children
|
||||
for child in [item.child(i) for i in range(item.childCount())]:
|
||||
self._enforce_dependencies(child, column)
|
||||
else:
|
||||
#if item is checked, look for dependent items and check them too
|
||||
tools_by_name = {tool["name"]:tool for tool in self.__tools__}
|
||||
if item.checkState(0) == QtCore.Qt.Checked:
|
||||
try:
|
||||
dependencies = tools_by_name[item.text(0)]["depends on"].split(",")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for dependency in dependencies:
|
||||
for dependant_item in self.selection_menu.findItems(dependency, QtCore.Qt.MatchFixedString|QtCore.Qt.MatchCaseSensitive|QtCore.Qt.MatchRecursive):
|
||||
dependant_item.setCheckState(0, QtCore.Qt.Checked)
|
||||
else: #item was unchecked
|
||||
#find all checked items and build a set of their dependencies
|
||||
selected = self._get_selections()
|
||||
dependencies_of_selected = set()
|
||||
names_of_dependant_items = []
|
||||
for selected_item in selected:
|
||||
try:
|
||||
dependencies_of_selected |= set((tools_by_name[selected_item.text(0)]["depends on"]).split(","))
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if item.text(0) in (tools_by_name[selected_item.text(0)]["depends on"]).split(","):
|
||||
names_of_dependant_items.append(selected_item.text(0))
|
||||
#if the item that was just unchecked is a dependency of another selected item
|
||||
if item.text(0) in dependencies_of_selected:
|
||||
#build a gramatically correct error (minus oxford comma (sorry!!))
|
||||
if len(names_of_dependant_items) > 1:
|
||||
for name in names_of_dependant_items[1:-1]:
|
||||
name = f", {name}"
|
||||
names_of_dependant_items[-1] = f" and {names_of_dependant_items[-1]}"
|
||||
depend_text = "depend"
|
||||
else:
|
||||
depend_text = "depends"
|
||||
#display the error and disallow unchecking item while something that depends on it is selected
|
||||
self._display_error(f"{str().join(names_of_dependant_items)} {depend_text} on {item.text(0)}")
|
||||
item.setCheckState(0, QtCore.Qt.Checked)
|
||||
|
||||
def _begin_downloads(self, download_queue = None, install_ready = None, download_directory = None):
|
||||
if download_queue:
|
||||
pass
|
||||
else:
|
||||
self._download_queue = Queue()
|
||||
|
||||
if install_ready:
|
||||
pass
|
||||
else:
|
||||
self._install_ready = Queue()
|
||||
|
||||
self._download_error = Queue()
|
||||
|
||||
if download_directory:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self._download_directory = QtCore.QTemporaryDir()
|
||||
except Exception as e:
|
||||
self._display_error(str(e), self._download_directory.errorString()).accepted.connect(lambda: self.back())
|
||||
|
||||
if self._download_queue.empty():
|
||||
tools_by_name = {tool["name"]:tool for tool in self.__tools__}
|
||||
self._download_progress = {}
|
||||
for selected_tool in self._get_selections():
|
||||
self._download_queue.put_nowait(tools_by_name[selected_tool.text(0)])
|
||||
self.progress_bar.setRange(0, self._download_queue.qsize())
|
||||
|
||||
while self._thread_pool.activeThreadCount() < self._thread_pool.maxThreadCount() and not self._download_queue.empty():
|
||||
self._thread_pool.tryStart(DownloadWorker(self._download_queue, self._install_ready, self._download_error, self._download_directory, self))
|
||||
QtCore.QThread.currentThread().msleep(250)
|
||||
|
||||
assert self._download_error.empty()
|
||||
|
||||
|
||||
@QtCore.pyqtSlot(str, int, int)
|
||||
def track_progress(self, tool_name, bytes_received, bytes_total):
|
||||
if tool_name not in [tool["name"] for tool in self.__tools__]:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
self._download_progress[tool_name] = bytes_received/bytes_total
|
||||
except ZeroDivisionError:
|
||||
self._download_progress[tool_name] = 0
|
||||
self._progress_update_lock.acquire()
|
||||
self.progress_bar.setValue(sum(self._download_progress.values()))
|
||||
self._progress_update_lock.release()
|
||||
230
portable_computing_toolkit_installer/ui/installer_wizard.ui
Executable file
230
portable_computing_toolkit_installer/ui/installer_wizard.ui
Executable file
@@ -0,0 +1,230 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>toolkit_installer</class>
|
||||
<widget class="QWizard" name="toolkit_installer">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>610</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>800</width>
|
||||
<height>610</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Portable Computing Toolkit Installer</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>:/icons/toolbox.svg</normaloff>:/icons/toolbox.svg</iconset>
|
||||
</property>
|
||||
<property name="wizardStyle">
|
||||
<enum>QWizard::ClassicStyle</enum>
|
||||
</property>
|
||||
<property name="options">
|
||||
<set>QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage|QWizard::NoCancelButtonOnLastPage</set>
|
||||
</property>
|
||||
<widget class="QWizardPage" name="intro_page">
|
||||
<property name="title">
|
||||
<string>Portable Computing Toolkit Installer</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="intro">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<property name="html">
|
||||
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;">
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small;">The portable computing toolkit installer sets up and combines the </span><a href="https://portableapps.com/"><span style=" font-size:small; text-decoration: underline; color:#000080;">PortableApps.com</span></a><span style=" font-size:small;"> framework with various tools for writing code. The goal of this project is to provide a platform for anyone who has computing needs but does not own their on PC to be able to have a private workspace on public/shared computers.</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small; font-weight:600;">Requirements:</span></p>
|
||||
<ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" background-color:transparent;" style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%;"><span style=" font-size:small;">A PC runninging Windows 7 (64-bit) or later </span></li>
|
||||
<li style=" background-color:transparent;" style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%;"><span style=" font-size:small;">An </span><a href="https://lifehacker.com/how-to-erase-and-format-a-hard-drive-1525128357"><span style=" font-size:small; text-decoration: underline; color:#000080;">NTFS-Formatted</span></a><span style=" font-size:small;"> USB storage device </span></li>
|
||||
<li style=" background-color:transparent;" style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%;"><span style=" font-size:small;">An active Internet connection </span></li></ul>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small; font-weight:600;">Contributing and bug reporting:</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small;">Source code for this installer is available on </span><a href="https://github.com/norweeg/portable-computing-toolkit-installer"><span style=" font-size:small; text-decoration: underline; color:#000080;">Github</span></a><span style=" font-size:small;">. This installer was originally a learning exercise for myself to learn python, so I welcome you to help improve it!</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small;">This installer scrapes the web for the URLs to the installers for the software. I don't have control over the homepages of the tools in this toolkit, so if they fail to download, it's likely that the download page has changed and this tool needs to be updated to reflect those changes. If you find the installer is unable to download one of the tools, please </span><a href="https://github.com/norweeg/portable-computing-toolkit-installer/issues"><span style=" font-size:small; text-decoration: underline; color:#000080;">open an issue</span></a><span style=" font-size:small;"> on Github</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small;">This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small;">This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; line-height:115%; background-color:transparent;"><span style=" font-size:small;">Copyright © 2019 </span><a href="https://github.com/norweeg"><span style=" font-size:small; text-decoration: underline; color:#000080;">Brennen Raimer</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextBrowserInteraction</set>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="license_page">
|
||||
<property name="title">
|
||||
<string>Portable Computing Toolkit Installer License Agreement</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextBrowser" name="license">
|
||||
<property name="html">
|
||||
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;">
|
||||
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p></body></html></string>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="location_page">
|
||||
<property name="title">
|
||||
<string>Install Location</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Choose a drive or folder to install the portable toolkit to:</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="install_location">
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="browse_button">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="selection_page">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="selection_menu">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerDefaultSectionSize">
|
||||
<number>300</number>
|
||||
</attribute>
|
||||
<attribute name="headerShowSortIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Homepage</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="progress_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progress_bar">
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="progress_label">
|
||||
<property name="text">
|
||||
<string>Download Progress</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
17
portable_computing_toolkit_installer/ui/menu_iterator.py
Normal file
17
portable_computing_toolkit_installer/ui/menu_iterator.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
|
||||
class MenuIterator(QtWidgets.QTreeWidgetItemIterator):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
value = self.value()
|
||||
if not value:
|
||||
raise StopIteration
|
||||
else:
|
||||
self += 1
|
||||
return value
|
||||
@@ -0,0 +1,100 @@
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
import psutil
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from threading import Lock, Condition, Event
|
||||
|
||||
class Location_Validator(QtGui.QValidator):
|
||||
_disposition = QtGui.QValidator.Intermediate
|
||||
_pos = 0
|
||||
_location = ""
|
||||
_ntfs_location_selected = False
|
||||
_previous_location = None
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
@property
|
||||
def ntfs_location_selected(self):
|
||||
return Location_Validator._ntfs_location_selected
|
||||
|
||||
def _onNTFSDrive(self, location):
|
||||
"""Tests if location is a path on an NTFS-formatted drive. Returns True if path is on NTFS-formatted drive.
|
||||
|
||||
Arguments:
|
||||
location {str} -- A string representing a folder path
|
||||
"""
|
||||
match = [drive for drive in psutil.disk_partitions(all = True)
|
||||
if (Path(drive.mountpoint) in Path(location).parents or Path(drive.mountpoint) == Path(location))
|
||||
and drive.fstype.lower() == "ntfs"]
|
||||
if match:
|
||||
self._ntfs_location_selected = True
|
||||
return True
|
||||
#elif platform.system() == "Linux":
|
||||
# pass
|
||||
else:
|
||||
if QtWidgets.QApplication.instance().activeModalWidget():
|
||||
return False
|
||||
self._ntfs_location_selected = False
|
||||
return not QtWidgets.QMessageBox.question(self.parent(), "", f"{Path(location)} is not an NTFS-formatted volume. Tools which require a NTFS filesystem to function will not be available. Continue anyway?") in (QtWidgets.QMessageBox.No, 0)
|
||||
|
||||
def validate(self, input_, pos):
|
||||
"""Called by InstallerWizard via QtWidget.QLineEdit.TextChanged signal to validate installation location input of user
|
||||
|
||||
Arguments:
|
||||
input_ {str} -- The text entered in the installation location QLineEdit widget
|
||||
pos {int} -- The current location of the cursor in the QLineEdit widget
|
||||
"""
|
||||
#there is a very obnoxious, nearly 10 year old bug in QLineEdit that causes validate to be called gratitously and rapidly especially on
|
||||
#non-Windows platforms where it gets called every time the window focus changes. The bug has been in Qt since at least Qt4 and it doesn't
|
||||
#look like anyone cares to fix it, so I did my best to supress gratitious input checking and modal dialog opening
|
||||
if not input_.strip():
|
||||
_previous_location = input_
|
||||
return (QtGui.QValidator.Intermediate, input_, pos)
|
||||
|
||||
elif input_.strip() == self._previous_location:
|
||||
return (self._disposition, self._previous_location, self._pos)
|
||||
|
||||
elif input_.strip().endswith(r":"):
|
||||
#if nothing entered or location ends with : e.g. C: which will cause _onNTFSDrive to trigger unwanted messages
|
||||
self._location = input_
|
||||
self._disposition = QtGui.QValidator.Intermediate
|
||||
else:
|
||||
expanded_input = os.path.expandvars(input_.strip()) # expand any environment variables in input
|
||||
location = QtCore.QFileInfo(expanded_input)
|
||||
if location.exists() and location.isDir():
|
||||
if not self._onNTFSDrive(expanded_input):
|
||||
# clear the location, reset cursor, but allow continued edits
|
||||
self._location = ""
|
||||
self._disposition = QtGui.QValidator.Intermediate
|
||||
else:
|
||||
test_file = QtCore.QTemporaryFile(f"{Path(expanded_input)/'testXXXXXX.tmp'}")
|
||||
|
||||
if test_file.open(QtCore.QIODevice.ReadWrite):
|
||||
test_file.close()
|
||||
test_file.remove()
|
||||
self._location = input_
|
||||
self._disposition = QtGui.QValidator.Acceptable
|
||||
else:
|
||||
self._location = input_
|
||||
self._disposition = QtGui.QValidator.Intermediate
|
||||
else:
|
||||
# I wanted to return invalid, but that would prevent input of anything not instantly acceptable
|
||||
# or editing something acceptable to something else acceptable, passing through an invalid on the way
|
||||
self._location = input_
|
||||
self._disposition = QtGui.QValidator.Intermediate
|
||||
|
||||
self._pos = pos
|
||||
self._previous_location = self._location
|
||||
|
||||
return (self._disposition, self._location, self._pos,)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def reset_state(self):
|
||||
"""Resets the internal state of the Location_Validator, including the previously validated location
|
||||
"""
|
||||
self._disposition = QtGui.QValidator.Intermediate
|
||||
self._pos = 0
|
||||
self._location = ""
|
||||
self._ntfs_location_selected = False
|
||||
self._previous_location = None
|
||||
Reference in New Issue
Block a user