From 71441a6b81059e62c5930d93c441c206dd4121b0 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sat, 14 Aug 2021 21:40:42 +0200 Subject: [PATCH] Greeting screen! Update notifier system! --- LogarithmPlotter/config.py | 89 +++++++++++++ LogarithmPlotter/logarithmplotter.py | 93 +++++++++----- .../eu/ad5001/LogarithmPlotter/AppMenuBar.qml | 16 ++- .../ad5001/LogarithmPlotter/GreetScreen.qml | 117 ++++++++++++++++++ .../LogarithmPlotter/LogarithmPlotter.qml | 26 +++- .../eu/ad5001/LogarithmPlotter/Settings.qml | 2 - .../icons/settings/update.svg | 1 + LogarithmPlotter/update.py | 81 ++++++++++++ 8 files changed, 388 insertions(+), 37 deletions(-) create mode 100644 LogarithmPlotter/config.py create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/GreetScreen.qml create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/icons/settings/update.svg create mode 100644 LogarithmPlotter/update.py diff --git a/LogarithmPlotter/config.py b/LogarithmPlotter/config.py new file mode 100644 index 0000000..0e0a2bf --- /dev/null +++ b/LogarithmPlotter/config.py @@ -0,0 +1,89 @@ +""" + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 Ad5001 + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +""" + +from os import path, environ, makedirs +from platform import system +from json import load, dumps + +DEFAULT_SETTINGS = { + "check_for_updates": True, + "last_install_greet": "0", + "lang": "en" +} + +# Create config directory +CONFIG_PATH = { + "Linux": path.join(environ["XDG_CONFIG_HOME"], "LogarithmPlotter") if "XDG_CONFIG_HOME" in environ else path.join(path.expanduser("~"), ".config", "LogarithmPlotter"), + "Windows": path.join(path.expandvars('%APPDATA%'), "LogarithmPlotter", "config"), + "Darwin": path.join(path.expanduser("~"), "Library", "Application Support", "LogarithmPlotter"), +}[system()] + +CONFIG_FILE = path.join(CONFIG_PATH, "config.json") +CONFIG = DEFAULT_SETTINGS + +def init(): + """ + Initializes the config and loads all possible settings from the file if needs be. + """ + makedirs(CONFIG_PATH, exist_ok=True) + + if path.exists(CONFIG_FILE): + cfg_data = load(open(CONFIG_FILE, 'r', -1, 'utf8')) + for setting_name in cfg_data: + setSetting(setting_name, cfg_data[setting_name]) + +def save(): + """ + Saves the config to the path. + """ + write_file = open(CONFIG_FILE, 'w', -1, 'utf8') + write_file.write(dumps(CONFIG)) + write_file.close() + +def getSetting(namespace): + """ + Returns a setting from a namespace. + E.g: if the config is {"test": {"foo": 1}}, you can access the "foo" setting + by using the "test.foo" namespace. + """ + names = namespace.split(".") + setting = CONFIG + for name in names: + if name in setting: + setting = setting[name] + else: + # return namespace # Return original name + raise ValueError('Setting ' + namespace + ' doesn\'t exist. Debug: ', setting, name) + return setting + +def setSetting(namespace, data): + """ + Sets a setting at a namespace with data. + E.g: if the config is {"test": {"foo": 1}}, you can access the "foo" setting + by using the "test.foo" namespace. + """ + names = namespace.split(".") + setting = CONFIG + for name in names: + if name != names[-1]: + if name in setting: + setting = setting[name] + else: + raise ValueError('Setting {} doesn\'t exist. Debug: {}, {}'.format(namespace, setting, name)) + else: + setting[name] = data diff --git a/LogarithmPlotter/logarithmplotter.py b/LogarithmPlotter/logarithmplotter.py index 687ffb8..f1354bd 100644 --- a/LogarithmPlotter/logarithmplotter.py +++ b/LogarithmPlotter/logarithmplotter.py @@ -26,15 +26,21 @@ from PySide2.QtCore import Qt, QObject, Signal, Slot, Property from PySide2.QtGui import QIcon, QImage, QKeySequence from PySide2 import __version__ as PySide2_version -import os -import tempfile +from tempfile import mkstemp +from os import getcwd, chdir, environ, path, remove from platform import release as os_release from json import dumps, loads from sys import platform, argv, version as sys_version -import webbrowser +from webbrowser import open as openWeb + +# Create the temporary file for saving copied screenshots +tmpfile = mkstemp(suffix='.png')[1] +pwd = getcwd() + +from . import config, __VERSION__ +from .update import check_for_updates +config.init() -tempfile = tempfile.mkstemp(suffix='.png')[1] -pwd = os.getcwd() def get_linux_theme(): des = { @@ -43,8 +49,8 @@ def get_linux_theme(): "lxqt": "fusion", "mate": "fusion", } - if "XDG_SESSION_DESKTOP" in os.environ: - return des[os.environ["XDG_SESSION_DESKTOP"]] if os.environ["XDG_SESSION_DESKTOP"] in des else "fusion" + if "XDG_SESSION_DESKTOP" in environ: + return des[environ["XDG_SESSION_DESKTOP"]] if environ["XDG_SESSION_DESKTOP"] in des else "fusion" else: # Android return "Material" @@ -53,22 +59,22 @@ class Helper(QObject): @Slot(str, str) def write(self, filename, filedata): - os.chdir(pwd) - if os.path.exists(os.path.dirname(os.path.realpath(filename))): + chdir(pwd) + if path.exists(path.dirname(path.realpath(filename))): if filename.split(".")[-1] == "lpf": # Add header to file filedata = "LPFv1" + filedata - f = open(os.path.realpath(filename), 'w', -1, 'utf8') + f = open(path.realpath(filename), 'w', -1, 'utf8') f.write(filedata) f.close() - os.chdir(os.path.dirname(os.path.realpath(__file__))) + chdir(path.dirname(path.realpath(__file__))) @Slot(str, result=str) def load(self, filename): - os.chdir(pwd) + chdir(pwd) data = '{}' - if os.path.exists(os.path.realpath(filename)): - f = open(os.path.realpath(filename), 'r', -1, 'utf8') + if path.exists(path.realpath(filename)): + f = open(path.realpath(filename), 'r', -1, 'utf8') data = f.read() f.close() try: @@ -84,25 +90,40 @@ class Helper(QObject): QMessageBox.warning(None, 'LogarithmPlotter', 'Could not open file "{}":\n{}'.format(filename, e), QMessageBox.Ok) # Cannot parse file else: QMessageBox.warning(None, 'LogarithmPlotter', 'Could not open file: "{}"\nFile does not exist.'.format(filename), QMessageBox.Ok) # Cannot parse file - os.chdir(os.path.dirname(os.path.realpath(__file__))) + chdir(path.dirname(path.realpath(__file__))) return data @Slot(result=str) def gettmpfile(self): - global tempfile - return tempfile + global tmpfile + return tmpfile @Slot() def copyImageToClipboard(self): - global tempfile + global tmpfile clipboard = QApplication.clipboard() - clipboard.setImage(QImage(tempfile)) + clipboard.setImage(QImage(tmpfile)) @Slot(result=str) def getVersion(self): - from . import __VERSION__ return __VERSION__ + @Slot(str, result=str) + def getSetting(self, namespace): + return config.getSetting(namespace) + + @Slot(str, result=bool) + def getSettingBool(self, namespace): + return config.getSetting(namespace) + + @Slot(str, str) + def setSetting(self, namespace, value): + return config.setSetting(namespace, value) + + @Slot(str, bool) + def setSettingBool(self, namespace, value): + return config.setSetting(namespace, value) + @Slot(result=str) def getDebugInfos(self): """ @@ -112,12 +133,12 @@ class Helper(QObject): @Slot(str) def openUrl(self, url): - webbrowser.open(url) + openWeb(url) def run(): - os.chdir(os.path.dirname(os.path.realpath(__file__))) + chdir(path.dirname(path.realpath(__file__))) - os.environ["QT_QUICK_CONTROLS_STYLE"] = { + environ["QT_QUICK_CONTROLS_STYLE"] = { "linux": get_linux_theme(), "freebsd": get_linux_theme(), "win32": "universal" if os_release == "10" else "fusion", @@ -129,35 +150,41 @@ def run(): print("Loaded dependencies in " + str((dep_time - start_time)*1000) + "ms.") icon_fallbacks = QIcon.fallbackSearchPaths(); - icon_fallbacks.append(os.path.realpath(os.path.join(os.getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons"))) - icon_fallbacks.append(os.path.realpath(os.path.join(os.getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons", "settings"))) - icon_fallbacks.append(os.path.realpath(os.path.join(os.getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons", "settings", "custom"))) + icon_fallbacks.append(path.realpath(path.join(getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons"))) + icon_fallbacks.append(path.realpath(path.join(getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons", "settings"))) + icon_fallbacks.append(path.realpath(path.join(getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons", "settings", "custom"))) QIcon.setFallbackSearchPaths(icon_fallbacks); app = QApplication(argv) app.setApplicationName("LogarithmPlotter") app.setOrganizationName("Ad5001") - app.setWindowIcon(QIcon(os.path.realpath(os.path.join(os.getcwd(), "logarithmplotter.svg")))) + app.setWindowIcon(QIcon(path.realpath(path.join(getcwd(), "logarithmplotter.svg")))) engine = QQmlApplicationEngine() helper = Helper() engine.rootContext().setContextProperty("Helper", helper) engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv) engine.rootContext().setContextProperty("StartTime", dep_time) - engine.addImportPath(os.path.realpath(os.path.join(os.getcwd(), "qml"))) - engine.load(os.path.realpath(os.path.join(os.getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "LogarithmPlotter.qml"))) + engine.addImportPath(path.realpath(path.join(getcwd(), "qml"))) + engine.load(path.realpath(path.join(getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "LogarithmPlotter.qml"))) - os.chdir(pwd) - if len(argv) > 0 and os.path.exists(argv[-1]) and argv[-1].split('.')[-1] in ['json', 'lgg', 'lpf']: + chdir(pwd) + if len(argv) > 0 and path.exists(argv[-1]) and argv[-1].split('.')[-1] in ['json', 'lgg', 'lpf']: engine.rootObjects()[0].loadDiagram(argv[-1]) - os.chdir(os.path.dirname(os.path.realpath(__file__))) + chdir(path.dirname(path.realpath(__file__))) if not engine.rootObjects(): print("No root object") exit(-1) + + # Check for updates + if config.getSetting("check_for_updates"): + check_for_updates(__VERSION__, engine.rootObjects()[0]) + exit_code = app.exec_() - os.remove(tempfile) + remove(tmpfile) + config.save() exit(exit_code) if __name__ == "__main__": diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/AppMenuBar.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/AppMenuBar.qml index 33718bd..9cdb42f 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/AppMenuBar.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/AppMenuBar.qml @@ -17,7 +17,6 @@ */ import QtQuick 2.12 -//import QtQuick.Controls 2.12 import eu.ad5001.MixedMenu 1.1 import "js/objects.js" as Objects import "js/historylib.js" as HistoryLib @@ -54,6 +53,7 @@ MenuBar { icon.name: 'application-exit' } } + Menu { title: qsTr("&Edit") Action { @@ -80,6 +80,7 @@ MenuBar { icon.name: 'edit-copy' } } + Menu { title: qsTr("&Create") // Services repeater @@ -100,6 +101,19 @@ MenuBar { } } } + + Menu { + title: qsTr("&Settings") + Action { + id: checkForUpdatesMenuSetting + text: qsTr("Check for updates on startup") + checkable: true + checked: Helper.getSettingBool("check_for_updates") + onTriggered: Helper.setSettingBool("check_for_updates", checked) + icon.name: 'update' + } + } + Menu { title: qsTr("&Help") Action { diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/GreetScreen.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/GreetScreen.qml new file mode 100644 index 0000000..22cdb20 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/GreetScreen.qml @@ -0,0 +1,117 @@ + +/** + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 Ad5001 + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Popup { + id: greetingPopup + x: (parent.width-width)/2 + y: Math.max(20, (parent.height-height)/2) + width: 600 + height: Math.min(parent.height-40, 500) + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + Item { + id: welcome + height: logo.height + width: logo.width + 10 + welcomeText.width + anchors.top: parent.top + anchors.topMargin: 50 + anchors.horizontalCenter: parent.horizontalCenter + + Image { + id: logo + source: "icons/logarithmplotter.svg" + sourceSize.width: 48 + sourceSize.height: 48 + width: 48 + height: 48 + } + + Label { + id: welcomeText + anchors.verticalCenter: parent.verticalCenter + anchors.left: logo.right + anchors.leftMargin: 10 + //width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: 32 + text: "Welcome to LogarithmPlotter" + } + } + + Label { + id: versionText + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: welcome.bottom + anchors.topMargin: 10 + //width: parent.width + wrapMode: Text.WordWrap + width: implicitWidth + font.pixelSize: 18 + font.italic: true + text: "Version " + Helper.getVersion() + } + + Label { + id: helpText + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: versionText.bottom + anchors.topMargin: 40 + //width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: 14 + width: parent.width - 50 + text: "Take a few seconds to configure LogarithmPlotter.\nThese settings can always be changed at any time from the \"Settings\" menu." + } + + CheckBox { + id: checkForUpdatesSetting + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: helpText.bottom + anchors.topMargin: 10 + checked: Helper.getSettingBool("check_for_updates") + text: 'Check for updates on startup (requires online connectivity)' + onClicked: { + Helper.setSettingBool("check_for_updates", checked) + checkForUpdatesMenuSetting.checked = checked + } + } + + Button { + text: "Done" + font.pixelSize: 20 + anchors.bottom: parent.bottom + anchors.bottomMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + onClicked: greetingPopup.close() + } + + Timer { + running: Helper.getSetting("last_install_greet") != Helper.getVersion() + repeat: false + interval: 50 + onTriggered: greetingPopup.open() + } + + onClosed: Helper.setSetting("last_install_greet", Helper.getVersion()) +} diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml index 123973e..2b27a0a 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml @@ -18,6 +18,7 @@ import QtQml 2.12 import QtQuick.Controls 2.12 +import eu.ad5001.MixedMenu 1.1 import QtQuick.Layouts 1.12 import QtQuick 2.12 // Auto loading all objects. @@ -37,12 +38,15 @@ ApplicationWindow { SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active } SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled } - History { id: history } menuBar: appMenu.trueItem + GreetScreen {} + AppMenuBar {id: appMenu} + History { id: history } + About {id: about} Alert { @@ -271,4 +275,24 @@ ApplicationWindow { Helper.copyImageToClipboard() alert.show("Copied plot screenshot to clipboard!") } + + function showAlert(alertText) { + // This function is called from the backend and is used to show alerts from there. + alert.show(alertText) + } + + + Menu { + id: updateMenu + title: qsTr("&Update") + Action { + text: qsTr("&Update LogarithmPlotter") + icon.name: 'update' + onTriggered: Helper.openUrl("https://dev.apps.ad5001.eu/logarithmplotter") + } + } + + function showUpdateMenu() { + appMenu.addMenu(updateMenu) + } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml index eee9cdf..8c81bde 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Settings.qml @@ -43,8 +43,6 @@ ScrollView { property bool showygrad: true Column { - //height: 30*12 //30*Math.max(1, Math.ceil(7 / columns)) - //columns: Math.floor(width / settingWidth) spacing: 10 width: parent.width bottomPadding: 20 diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/icons/settings/update.svg b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/icons/settings/update.svg new file mode 100644 index 0000000..6dd9014 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/icons/settings/update.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LogarithmPlotter/update.py b/LogarithmPlotter/update.py new file mode 100644 index 0000000..5e6748b --- /dev/null +++ b/LogarithmPlotter/update.py @@ -0,0 +1,81 @@ +""" + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 Ad5001 + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +""" + +from PySide2.QtCore import Qt, QRunnable, QThreadPool, QThread, QObject, Signal +from urllib.request import urlopen +from urllib.error import HTTPError, URLError + +class UpdateInformation(QObject): + got_update_info = Signal(bool, str, bool) + +class UpdateCheckerRunnable(QRunnable): + def __init__(self, current_version, callback): + QRunnable.__init__(self) + self.current_version = current_version + self.callback = callback + + def run(self): + msg_text = "Unknown update error." + show_alert = True + update_available = False + try: + # Fetching version + r = urlopen("https://api.ad5001.eu/update/v1/LogarithmPlotter") + lines = r.readlines() + r.close() + # Parsing version + version = "".join(map(chr, lines[0])).strip() # Converts byte to string. + version_tuple = version.split(".") + is_version_newer = False + if "dev" in self.current_version: + # We're on a dev version + current_version_tuple = self.current_version.split(".")[:-1] # Removing the dev0+git bit. + is_version_newer = version_tuple >= current_version_tuple # If equals, that means we got out of testing phase. + else: + current_version_tuple = self.current_version.split(".") + is_version_newer = version_tuple > current_version_tuple + if is_version_newer: + msg_text = "An update for LogarithPlotter (v" + version + ") is available." + update_available = True + else: + show_alert = False + + except HTTPError as e: + msg_text = "Could not fetch update information: Server error " + str(e.code) + "." + except URLError as e: + msg_text = "Could not fetch update information: Could not connect to the update server. " + str(e.reason) + "." + print(msg_text) + self.callback.got_update_info.emit(show_alert, msg_text,update_available) + +def check_for_updates(current_version, window): + """ + Checks for updates in the background, and sends an alert with information. + """ + + def cb(show_alert, msg_text, update_available): + pass + if show_alert: + window.showAlert(msg_text) + if update_available: + window.showUpdateMenu() + + update_info = UpdateInformation() + update_info.got_update_info.connect(cb) + + runnable = UpdateCheckerRunnable(current_version, update_info) + QThreadPool.globalInstance().start(runnable)