diff --git a/.gitignore b/.gitignore index 2e4e8fb..43cb491 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +# End of https://www.gitignore.io/api/linux,macos,windows,visualstudio,visualstudiocode + +.vscode \ No newline at end of file diff --git a/portable_computing_toolkit_installer/__init__.py b/portable_computing_toolkit_installer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/portable_computing_toolkit_installer/__main__.py b/portable_computing_toolkit_installer/__main__.py new file mode 100644 index 0000000..ce568c0 --- /dev/null +++ b/portable_computing_toolkit_installer/__main__.py @@ -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_()) diff --git a/portable_computing_toolkit_installer/resources/supported_tools.json b/portable_computing_toolkit_installer/resources/supported_tools.json new file mode 100644 index 0000000..3116ae3 --- /dev/null +++ b/portable_computing_toolkit_installer/resources/supported_tools.json @@ -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\"]" + } +} \ No newline at end of file diff --git a/portable_computing_toolkit_installer/ui/__init__.py b/portable_computing_toolkit_installer/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/portable_computing_toolkit_installer/ui/installer_wizard.py b/portable_computing_toolkit_installer/ui/installer_wizard.py new file mode 100644 index 0000000..86fc816 --- /dev/null +++ b/portable_computing_toolkit_installer/ui/installer_wizard.py @@ -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 diff --git a/portable_computing_toolkit_installer/ui/installer_wizard.ui b/portable_computing_toolkit_installer/ui/installer_wizard.ui new file mode 100644 index 0000000..658f399 --- /dev/null +++ b/portable_computing_toolkit_installer/ui/installer_wizard.ui @@ -0,0 +1,194 @@ + + + toolkit_installer + + + + 0 + 0 + 800 + 610 + + + + + 0 + 0 + + + + + 800 + 610 + + + + Portable Computing Toolkit Installer + + + + :/icons/toolbox.svg:/icons/toolbox.svg + + + QWizard::ClassicStyle + + + QWizard::IndependentPages|QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage|QWizard::NoCancelButtonOnLastPage + + + + Portable Computing Toolkit Installer + + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + <!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:'MS Shell Dlg 2'; font-size:8pt; 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;"><br /></p></body></html> + + + Qt::TextBrowserInteraction + + + false + + + false + + + + + + + + Portable CS Toolkit Installer License Agreement + + + + + + + + + + qrc:/resources/license.html + + + + false + + + + + + + + Install Location + + + Choose a drive or folder to install the portable toolkit to: + + + + + + true + + + + + + + Browse... + + + + + + + + + + + 300 + + + + Name + + + + + Homepage + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 0 + + + 100 + + + 0 + + + + + + + Download Progress + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + diff --git a/portable_computing_toolkit_installer/validators/__init__.py b/portable_computing_toolkit_installer/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/portable_computing_toolkit_installer/validators/location_validator.py b/portable_computing_toolkit_installer/validators/location_validator.py new file mode 100644 index 0000000..25836f5 --- /dev/null +++ b/portable_computing_toolkit_installer/validators/location_validator.py @@ -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)