begin migrating code that can be reused from first iteration of this idea

This commit is contained in:
Brennen Raimer
2019-04-16 00:04:23 -04:00
parent cb0374e858
commit fb5131e480
9 changed files with 503 additions and 1 deletions

View File

@@ -0,0 +1,17 @@
import sys
from PyQt5 import QtWidgets
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")
# create a window
main_window = InstallerWizard()
main_window.show()
# execute the application
sys.exit(app.exec_())

View File

@@ -0,0 +1,135 @@
{
"Portable Apps Platform": {
"page": "https://portableapps.com/download",
"xpath": "//*[@id=\"node-58089\"]/div/div[1]/div/div/div/div/a"
},
"Git": {
"page": "https://git-scm.com/download/win",
"xpath": "//*[@id=\"main\"]/div/p[6]/strong/a"
},
"GitExtensions": {
"page": "https://github.com/gitextensions/gitextensions/releases/latest",
"xpath": "/html/body/div[4]/div/div/div[2]/div[1]/div[2]/div/div[2]/details/ul/li[1]/a",
"search": "GitExtensions-(\\d|\\.)+-Mono\\.zip$"
},
"WinPython": {
"page": "https://sourceforge.net/projects/winpython/",
"xpath": "//*[@id=\"pg_project\"]/div[5]/div[2]/div[1]/div/section/div[2]/div[3]/a[1]"
},
"MSYS2": {
"page": "http://www.msys2.org/",
"xpath": "//*[@id=\"downloads\"]/div[1]/a[2]"
},
"Java JDK": {
"page": "http://jdk.java.net/11/",
"xpath": "",
"search": ""
},
"SublimeText": {
"page": "https://www.sublimetext.com/3",
"xpath": "//*[@id=\"dl_win_64\"]/a[2]",
"search": "Sublime\\sText\\sBuild\\s(\\d|\\.)+\\sx64\\.zip$"
},
"Atom": {
"page": "https://github.com/atom/atom/releases/latest",
"xpath": "/html/body/div[4]/div/div/div[2]/div[1]/div[2]/div/div[2]/details/ul/li[11]/a",
"search": "atom-x64-windows\\.zip$"
},
".NET Core SDK": {
"page": "https://www.microsoft.com/net/download/dotnet-core/",
"xpath": "",
"search": ""
},
"Google Chrome": {
"page": "https://portableapps.com/apps/internet/google_chrome_portable",
"xpath": "//*[@id=\"node-57731\"]/div/div[1]/div/div/div/a"
},
"Mozilla Firefox": {
"page": "https://portableapps.com/apps/internet/firefox_portable",
"xpath": "//*[@id=\"node-58088\"]/div/div[1]/div/div/div/a"
},
"Cherrytree": {
"page": "https://portableapps.com/apps/office/cherrytree-portable",
"xpath": "//*[@id=\"node-58652\"]/div/div[1]/div/div/div/a"
},
"KeepNote": {
"page": "https://portableapps.com/apps/office/keepnote-portable",
"xpath": "//*[@id=\"node-58966\"]/div/div[1]/div/div/div/a"
},
"LibreOffice": {
"page": "https://portableapps.com/apps/office/libreoffice_portable",
"xpath": "//*[@id=\"node-54208\"]/div/div[1]/div/div/div[1]/a"
},
"RedNotebook": {
"page": "https://portableapps.com/apps/office/rednotebook_portable",
"xpath": "//*[@id=\"node-58725\"]/div/div[1]/div/div/div/a"
},
"Filezilla": {
"page": "https://portableapps.com/apps/internet/filezilla_portable",
"xpath": "//*[@id=\"node-57960\"]/div/div[1]/div/div/div/a"
},
"WinSCP": {
"page": "https://portableapps.com/apps/internet/winscp_portable",
"xpath": "//*[@id=\"node-58447\"]/div/div[1]/div/div/div/a"
},
"Cppcheck": {
"page": "https://portableapps.com/apps/development/cppcheck-portable",
"xpath": "//*[@id=\"node-58057\"]/div/div[1]/div/div/div/a"
},
"Frhed": {
"page": "https://portableapps.com/apps/development/frhed_portable",
"xpath": "//*[@id=\"node-58921\"]/div/div[1]/div/div/div/a"
},
"Geany": {
"page": "https://portableapps.com/apps/development/geany_portable",
"xpath": "//*[@id=\"node-58428\"]/div/div[1]/div/div/div/a"
},
"gVim": {
"page": "https://portableapps.com/apps/development/gvim_portable",
"xpath": "//*[@id=\"node-58871\"]/div/div[1]/div/div/div/a"
},
"Notepad++": {
"page": "https://portableapps.com/apps/development/notepadpp_portable",
"xpath": "//*[@id=\"node-57991\"]/div/div[1]/div/div/div/a"
},
"Peazip": {
"page": "https://portableapps.com/apps/utilities/peazip_portable",
"xpath": "//*[@id=\"node-58441\"]/div/div[1]/div/div/div/a"
},
"7-Zip": {
"page": "https://portableapps.com/apps/utilities/7-zip_portable",
"xpath": "//*[@id=\"node-57727\"]/div/div[1]/div/div/div/a"
},
"CubicExplorer": {
"page": "https://portableapps.com/apps/utilities/cubicexplorer_portable",
"xpath": "//*[@id=\"node-58914\"]/div/div[1]/div/div/div/a"
},
"Explorer++": {
"page": "https://portableapps.com/apps/utilities/explorerplusplus_portable",
"xpath": "//*[@id=\"node-58851\"]/div/div[1]/div/div/div/a"
},
"WinMerge": {
"page": "https://portableapps.com/apps/utilities/winmerge_portable",
"xpath": "//*[@id=\"node-58807\"]/div/div[1]/div/div/div/a"
},
"Command Prompt Portable": {
"page": "https://portableapps.com/apps/utilities/command_prompt_portable",
"xpath": "//*[@id=\"node-58853\"]/div/div[1]/div/div/div/a"
},
"Console": {
"page": "https://portableapps.com/apps/utilities/console_portable",
"xpath": "//*[@id=\"node-58901\"]/div/div[1]/div/div/div/a"
},
"KiTTY": {
"page": "https://portableapps.com/apps/internet/kitty-portable",
"xpath": "//*[@id=\"node-57811\"]/div/div[1]/div/div/div/a"
},
"PuTTY": {
"page": "https://portableapps.com/apps/internet/putty_portable",
"xpath": "//*[@id=\"node-58621\"]/div/div[1]/div/div/div/a"
},
"Microsoft Visual Studio Code": {
"page": "https://code.visualstudio.com/docs/?dv=win64user",
"xpath": "//*[@id=\"direct-link\"]"
}
}

View File

@@ -0,0 +1,87 @@
import platform
import subprocess
import tarfile
import tempfile
import webbrowser
import zipfile
from pathlib import Path
from queue import Queue
import psutil
from PyQt5 import QtCore, QtGui, QtWebEngineWidgets, QtWidgets
from PyQt5.uic import loadUi
try:
import win32api
import win32file
except ImportError:
if platform.system()=='Windows':
raise #require win32api and win32file on windows
from validators.location_validator import Location_Validator
class InstallerWizard(QtWidgets.QWizard):
def __init__(self, parent=None):
super().__init__(parent)
loadUi(Path(__file__).parent / "installer_wizard.ui", baseinstance=self)
self.button(QtWidgets.QWizard.NextButton).clicked.connect(self._requirements_check)
self.currentIdChanged.connect
self.intro.anchorClicked.connect(self._link_clicked)
self.license.anchorClicked.connect(self._link_clicked)
self.install_location.setValidator(Location_Validator(parent=self))
self.location_page.registerField("Location*", self.install_location)
self.browse_button.clicked.connect(self._link_clicked)
#Change button text on license page
self.license_page.setButtonText(QtWidgets.QWizard.NextButton, "Agree")
self.license_page.setButtonText(QtWidgets.QWizard.CancelButton, "Decline")
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:
message=QtWidgets.QMessageBox(icon=QtWidgets.QMessageBox.Critical,parent=self,text="Unable to launch external browser")
message.setDetailedText(str(e))
message.show()
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':
drives = [drive for drive in win32api.GetLogicalDriveStrings().split('\0') if drive]
dialog_location = drives.pop()
while (win32file.GetDriveType(dialog_location) == win32file.DRIVE_CDROM
or win32file.GetDriveType(dialog_location) == win32file.DRIVE_REMOTE):
dialog_location = drives.pop()
if dialog_location == "C:\\":
dialog_location = Path.home()
break
else:
dialog_location = Path.home()
selected_location = QtWidgets.QFileDialog.getExistingDirectory(parent=self,caption="Select Location",
directory=str(dialog_location),
options=(QtWidgets.QFileDialog.ShowDirsOnly))
self.__window.install_location.clear()
self.__window.install_location.insert(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)
"""
if not platform.system() == 'Windows' or int(platform.release()) < 7:
QtWidgets.QMessageBox(icon=QtWidgets.QMessageBox.Critical,parent=self,text="This toolkit requires Microsoft Windows 7 or newer").show()
self.restart()
return False
elif not platform.machine() == 'AMD64':
QtWidgets.QMessageBox(icon=QtWidgets.QMessageBox.Critical,parent=self,text='''Parts of this toolkit require a 64-bit processor.
Please open an issue on Github or Gitlab if you absolutely need it all 32-bit''').show()
self.restart()
return False
else:
return True

View File

@@ -0,0 +1,194 @@
<?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::IndependentPages|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>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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 CS 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="source">
<url>
<string>qrc:/resources/license.html</string>
</url>
</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="treeWidget">
<attribute name="headerDefaultSectionSize">
<number>300</number>
</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>

View File

@@ -0,0 +1,67 @@
from PyQt5 import QtCore, QtGui, QtWidgets
import psutil
import os
import pathlib
class Location_Validator(QtGui.QValidator):
def __init__(self, parent=None):
super().__init__(parent)
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() if (drive.device == pathlib.Path(location).anchor and drive.fstype == "NTFS")]
if not match:
QtWidgets.QMessageBox(
icon=QtWidgets.QMessageBox.Critical,
parent=self.parent(),
text=f"{pathlib.Path(location).anchor} is not an NTFS-formatted volume. Please reformat your drive to NTFS or pick an install path on a different volume"
).show()
return False
else:
return True
def validate(self, input_, pos):
"""Called by Portable_CS_Toolkit_Installer 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
"""
if input_.endswith(r":"):
return (
QtGui.QValidator.Intermediate,
input_,
pos,
) # bugfix. _onNTFSDrive triggers when typing the : in a drive name e.g. C:\
# because this class method is invoked via signal, it cannot handle python exceptions
# testing writability in Python is difficult and involves actually writing a temporary file
# to the directory, however, if that fails and raises an exception, it happens in Qt/C++ world
# and the python interpreter can't catch it, the program just crashes without any output,
# therefore, we will use Qt to do checking
input_ = os.path.expandvars(input_) # expand any environment variables in input
location = QtCore.QFileInfo(input_)
if location.exists() and location.isDir():
if input_ and not self._onNTFSDrive(input_):
# clear the location, reset cursor, but allow continued edits
return (QtGui.QValidator.Intermediate, "", 0)
test_file = QtCore.QTemporaryFile(input_ + r"\testXXXXXX.tmp")
if test_file.open(QtCore.QIODevice.ReadWrite):
test_file.close()
test_file.remove()
return (QtGui.QValidator.Acceptable, input_, pos)
else:
return (QtGui.QValidator.Intermediate, input_, pos)
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
return (QtGui.QValidator.Intermediate, input_, pos)