appimage-starter (#42)
create program launcher; reduced size of appimage; download of appimages working(?) Co-authored-by: Dominik Demuth <dominik.demuth@physik.tu-darmstadt.de> Reviewed-on: #42
This commit is contained in:
parent
6b71de8265
commit
0ec0021727
@ -42,19 +42,27 @@ AppDir:
|
|||||||
- python3.9-minimal
|
- python3.9-minimal
|
||||||
- python3-numpy
|
- python3-numpy
|
||||||
- python3-scipy
|
- python3-scipy
|
||||||
# - python3-matplotlib
|
|
||||||
# - python-matplotlib-data
|
|
||||||
- python3-bsddb3
|
- python3-bsddb3
|
||||||
- python3-h5py
|
- python3-h5py
|
||||||
- python3-pyqt5
|
- python3-pyqt5
|
||||||
- python3-pyqtgraph
|
- python3-pyqtgraph
|
||||||
- python3-requests
|
|
||||||
- python3-urllib3
|
|
||||||
# - python3-tk
|
|
||||||
exclude:
|
exclude:
|
||||||
|
# lots of qt stuff we do not use
|
||||||
|
- libqt5designer5
|
||||||
|
- libqt5help5
|
||||||
|
- libqt5network5
|
||||||
|
- libqt5sql5
|
||||||
|
- libqt5test5
|
||||||
|
- libqt5xml5
|
||||||
|
- qtbase5-dev-tools
|
||||||
|
- qtchooser
|
||||||
|
- pyqt5-dev-tools
|
||||||
|
- qtchooser
|
||||||
- libavahi-client3
|
- libavahi-client3
|
||||||
- libavahi-common-data
|
- libavahi-common-data
|
||||||
- libavahi-common3
|
- libavahi-common3
|
||||||
|
- libwacom2
|
||||||
|
- libwacom-common
|
||||||
after_bundle: |
|
after_bundle: |
|
||||||
echo "MONSTER SED FOLLOWING...(uncomment if needed for mpl-data)"
|
echo "MONSTER SED FOLLOWING...(uncomment if needed for mpl-data)"
|
||||||
# sed -i s,\'/usr/share/matplotlib/mpl-data\',"f\"\{os.environ.get\('APPDIR'\,'/'\)\}/usr/share/matplotlib/mpl-data\"", ${TARGET_APPDIR}/usr/lib/python3/dist-packages/matplotlib/__init__.py
|
# sed -i s,\'/usr/share/matplotlib/mpl-data\',"f\"\{os.environ.get\('APPDIR'\,'/'\)\}/usr/share/matplotlib/mpl-data\"", ${TARGET_APPDIR}/usr/lib/python3/dist-packages/matplotlib/__init__.py
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui'
|
# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui'
|
||||||
#
|
#
|
||||||
# Created by: PyQt5 UI code generator 5.15.7
|
# Created by: PyQt5 UI code generator 5.15.9
|
||||||
#
|
#
|
||||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||||
# run again. Do not edit this file unless you know what you are doing.
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
@ -358,6 +358,8 @@ class Ui_BaseWindow(object):
|
|||||||
self.actionBugs.setObjectName("actionBugs")
|
self.actionBugs.setObjectName("actionBugs")
|
||||||
self.actionShow_error_log = QtWidgets.QAction(BaseWindow)
|
self.actionShow_error_log = QtWidgets.QAction(BaseWindow)
|
||||||
self.actionShow_error_log.setObjectName("actionShow_error_log")
|
self.actionShow_error_log.setObjectName("actionShow_error_log")
|
||||||
|
self.actionCreate_starter = QtWidgets.QAction(BaseWindow)
|
||||||
|
self.actionCreate_starter.setObjectName("actionCreate_starter")
|
||||||
self.menuSave.addAction(self.actionSave)
|
self.menuSave.addAction(self.actionSave)
|
||||||
self.menuSave.addAction(self.actionExportGraphic)
|
self.menuSave.addAction(self.actionExportGraphic)
|
||||||
self.menuSave.addAction(self.action_save_fit_parameter)
|
self.menuSave.addAction(self.action_save_fit_parameter)
|
||||||
@ -422,6 +424,7 @@ class Ui_BaseWindow(object):
|
|||||||
self.menuOptions.addSeparator()
|
self.menuOptions.addSeparator()
|
||||||
self.menuOptions.addAction(self.action_colorcycle)
|
self.menuOptions.addAction(self.action_colorcycle)
|
||||||
self.menuOptions.addAction(self.actionConfiguration)
|
self.menuOptions.addAction(self.actionConfiguration)
|
||||||
|
self.menuOptions.addAction(self.actionCreate_starter)
|
||||||
self.menuView.addAction(self.actionTile)
|
self.menuView.addAction(self.actionTile)
|
||||||
self.menuView.addAction(self.actionCascade_windows)
|
self.menuView.addAction(self.actionCascade_windows)
|
||||||
self.menuWindow.addAction(self.actionNew_window)
|
self.menuWindow.addAction(self.actionNew_window)
|
||||||
@ -612,6 +615,7 @@ class Ui_BaseWindow(object):
|
|||||||
self.action_draw_object.setText(_translate("BaseWindow", "Draw objects..."))
|
self.action_draw_object.setText(_translate("BaseWindow", "Draw objects..."))
|
||||||
self.actionBugs.setText(_translate("BaseWindow", "Bugs! Problems! Wishes!"))
|
self.actionBugs.setText(_translate("BaseWindow", "Bugs! Problems! Wishes!"))
|
||||||
self.actionShow_error_log.setText(_translate("BaseWindow", "Show error log"))
|
self.actionShow_error_log.setText(_translate("BaseWindow", "Show error log"))
|
||||||
|
self.actionCreate_starter.setText(_translate("BaseWindow", "Create starter.."))
|
||||||
from ..data.datawidget.datawidget import DataWidget
|
from ..data.datawidget.datawidget import DataWidget
|
||||||
from ..data.integral_widget import IntegralWidget
|
from ..data.integral_widget import IntegralWidget
|
||||||
from ..data.point_select import PointSelectWidget
|
from ..data.point_select import PointSelectWidget
|
||||||
|
95
src/gui_qt/lib/starter.py
Normal file
95
src/gui_qt/lib/starter.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from shutil import copyfile
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from importlib.resources import path as resource_path
|
||||||
|
|
||||||
|
from nmreval.configs import config_paths
|
||||||
|
from nmreval.lib.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
def make_starter(app_file: str | None):
|
||||||
|
if app_file is not None:
|
||||||
|
make_starter_appimage(Path(app_file))
|
||||||
|
else:
|
||||||
|
make_starter_src()
|
||||||
|
|
||||||
|
|
||||||
|
def make_starter_appimage(app_file: Path):
|
||||||
|
new_path = Path.home() / '.local' / 'bin' / app_file.name
|
||||||
|
|
||||||
|
if app_file != new_path:
|
||||||
|
app_file.rename(new_path)
|
||||||
|
|
||||||
|
create_desktop_file(new_path)
|
||||||
|
|
||||||
|
|
||||||
|
def make_starter_src():
|
||||||
|
home = Path.home()
|
||||||
|
p = Path.home()
|
||||||
|
for p in Path(__file__).parents:
|
||||||
|
if p.stem == 'src':
|
||||||
|
break
|
||||||
|
elif p == home:
|
||||||
|
break
|
||||||
|
|
||||||
|
success = p != Path.home()
|
||||||
|
if success:
|
||||||
|
bin_path = p.with_name('bin') / 'evaluate.py'
|
||||||
|
success = bin_path.exists()
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
logger.warning('Location of evaluate.py could not be determined')
|
||||||
|
return False
|
||||||
|
|
||||||
|
create_desktop_file(bin_path)
|
||||||
|
|
||||||
|
|
||||||
|
def create_desktop_file(new_path: Path):
|
||||||
|
logo_path = config_paths() / 'logo.png'
|
||||||
|
if not logo_path.exists():
|
||||||
|
with resource_path('resources', 'logo.png') as fp:
|
||||||
|
copyfile(fp, logo_path)
|
||||||
|
desktop_entry = f"""\
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=NMReval
|
||||||
|
Comment=Best program ever (maybe)
|
||||||
|
Exec={new_path}
|
||||||
|
Icon={logo_path}
|
||||||
|
Type=Application
|
||||||
|
Terminal=false
|
||||||
|
Categories=Science;NumericalAnalysis;Physics;DataVisualization;Other;
|
||||||
|
NoDisplay=false
|
||||||
|
"""
|
||||||
|
file_name = 'pkm.vogel.nmreval.desktop'
|
||||||
|
with Path('~/.local/share/applications/', file_name).expanduser().open('w') as f:
|
||||||
|
f.write(desktop_entry)
|
||||||
|
|
||||||
|
desktop_dir = get_xkg_user_dirs('desktop')
|
||||||
|
if desktop_dir is not None:
|
||||||
|
desk_file = Path(desktop_dir, file_name)
|
||||||
|
with desk_file.open('w') as f:
|
||||||
|
f.write(desktop_entry)
|
||||||
|
|
||||||
|
desk_file.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def get_xkg_user_dirs(dir_type: str) -> str | None:
|
||||||
|
xdg_conf_home = os.getenv('XDG_CONFIG_HOME') or str(Path.home() / '.config')
|
||||||
|
|
||||||
|
with Path(xdg_conf_home, 'user-dirs.dirs').open('r') as f:
|
||||||
|
conf_string = '[XDG_USER_DIRS]\n' + f.read()
|
||||||
|
conf_string = re.sub(r'\$HOME', str(Path.home()), conf_string)
|
||||||
|
conf_string = re.sub('"', '', conf_string)
|
||||||
|
|
||||||
|
config = ConfigParser()
|
||||||
|
config.read_string(conf_string)
|
||||||
|
|
||||||
|
return config['XDG_USER_DIRS'].get(f'xdg_{dir_type}_dir')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
make_starter()
|
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import lru_cache
|
import os
|
||||||
|
import urllib.request
|
||||||
from os import getenv, stat
|
from os import getenv, stat
|
||||||
from os.path import exists
|
from os.path import exists
|
||||||
import hashlib
|
import hashlib
|
||||||
@ -8,8 +9,7 @@ import subprocess
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.error import HTTPError
|
||||||
import requests
|
|
||||||
from numpy import linspace
|
from numpy import linspace
|
||||||
from scipy.interpolate import interp1d
|
from scipy.interpolate import interp1d
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ class RdBuCMap:
|
|||||||
|
|
||||||
|
|
||||||
class UpdateDialog(QtWidgets.QDialog):
|
class UpdateDialog(QtWidgets.QDialog):
|
||||||
startDownload = QtCore.pyqtSignal(list)
|
startDownload = QtCore.pyqtSignal(tuple)
|
||||||
|
|
||||||
def __init__(self, filename: str = None, parent=None):
|
def __init__(self, filename: str = None, parent=None):
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
@ -74,7 +74,6 @@ class UpdateDialog(QtWidgets.QDialog):
|
|||||||
filename = getenv('APPIMAGE')
|
filename = getenv('APPIMAGE')
|
||||||
self._appfile = filename
|
self._appfile = filename
|
||||||
|
|
||||||
self.success = False
|
|
||||||
self.updater = Updater()
|
self.updater = Updater()
|
||||||
|
|
||||||
self.thread = QtCore.QThread(self)
|
self.thread = QtCore.QThread(self)
|
||||||
@ -144,24 +143,20 @@ class UpdateDialog(QtWidgets.QDialog):
|
|||||||
@QtCore.pyqtSlot()
|
@QtCore.pyqtSlot()
|
||||||
def update_appimage(self):
|
def update_appimage(self):
|
||||||
if self._appfile is None:
|
if self._appfile is None:
|
||||||
args = [self.updater.zsync_url]
|
args = (self.updater.zsync_url,)
|
||||||
else:
|
else:
|
||||||
# this breaks the download for some reason
|
# this breaks the download for some reason
|
||||||
args = ['-i', self._appfile, self.updater.zsync_url]
|
args = (self.updater.zsync_url, self._appfile)
|
||||||
|
|
||||||
args = [self.updater.zsync_url]
|
|
||||||
|
|
||||||
self.dialog_button.setEnabled(False)
|
self.dialog_button.setEnabled(False)
|
||||||
|
|
||||||
self.startDownload.emit(args)
|
self.startDownload.emit(args)
|
||||||
self.status.show()
|
self.status.show()
|
||||||
|
|
||||||
@QtCore.pyqtSlot(int)
|
@QtCore.pyqtSlot(int, str)
|
||||||
def finish_update(self, retcode: int):
|
def finish_update(self, retcode: int, file_loc: str):
|
||||||
# print('finished with', retcode)
|
|
||||||
self.success = retcode == 0
|
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
self.status.setText('Download complete.')
|
self.status.setText(f'Download complete.New AppImage lies in <p><em>{file_loc}</em>.</p>')
|
||||||
else:
|
else:
|
||||||
self.status.setText(f'Download failed :( with return code {retcode}.')
|
self.status.setText(f'Download failed :( with return code {retcode}.')
|
||||||
self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
|
self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
|
||||||
@ -171,44 +166,49 @@ class UpdateDialog(QtWidgets.QDialog):
|
|||||||
self.thread.quit()
|
self.thread.quit()
|
||||||
self.thread.wait()
|
self.thread.wait()
|
||||||
|
|
||||||
if self.success:
|
|
||||||
appname = self.updater.get_zsync()[2]
|
|
||||||
if self._appfile is not None:
|
|
||||||
appimage_path = appname
|
|
||||||
old_version = Path(self._appfile).rename(self._appfile+'.old')
|
|
||||||
appimage_path = Path(appimage_path).replace(self._appfile)
|
|
||||||
else:
|
|
||||||
appimage_path = Path().cwd() / appname
|
|
||||||
# rename to version-agnostic name
|
|
||||||
appimage_path = appimage_path.rename('NMReval.AppImage')
|
|
||||||
appimage_path.chmod(appimage_path.stat().st_mode | 73) # 73 = 0o111 = a+x
|
|
||||||
|
|
||||||
_ = QtWidgets.QMessageBox.information(self, 'Complete',
|
|
||||||
f'New AppImage available at<br>{appimage_path}')
|
|
||||||
|
|
||||||
super().closeEvent(evt)
|
super().closeEvent(evt)
|
||||||
|
|
||||||
|
|
||||||
class Downloader(QtCore.QObject):
|
class Downloader(QtCore.QObject):
|
||||||
started = QtCore.pyqtSignal()
|
started = QtCore.pyqtSignal()
|
||||||
finished = QtCore.pyqtSignal(int)
|
finished = QtCore.pyqtSignal(int, str)
|
||||||
progressChanged = QtCore.pyqtSignal(str)
|
progressChanged = QtCore.pyqtSignal(str)
|
||||||
|
|
||||||
@QtCore.pyqtSlot(list)
|
@QtCore.pyqtSlot(tuple)
|
||||||
def run_download(self, args: list[str]):
|
def run_download(self, args: tuple[str]):
|
||||||
logger.info(f'Download with args {args}')
|
status = 0
|
||||||
process = subprocess.Popen(['zsync'] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True)
|
appimage_location = args[0][:-6]
|
||||||
while True:
|
logger.info(f'Download {appimage_location}')
|
||||||
nextline = process.stdout.readline().strip()
|
if len(args) == 2:
|
||||||
if nextline:
|
new_file = Path(args[1])
|
||||||
self.progressChanged.emit(nextline)
|
else:
|
||||||
|
new_file = Path.home() / 'Downloads' / 'NMReval-latest-x86_64.AppImage'
|
||||||
|
|
||||||
# line = process.stderr.readline().strip()
|
if new_file.exists():
|
||||||
|
os.rename(new_file, new_file.with_suffix('.AppImage.old'))
|
||||||
|
|
||||||
if process.poll() is not None:
|
try:
|
||||||
break
|
with urllib.request.urlopen(appimage_location) as response:
|
||||||
|
with new_file.open('wb') as f:
|
||||||
|
f.write(response.read())
|
||||||
|
|
||||||
self.finished.emit(process.returncode)
|
new_file.chmod(0o755)
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.exception(f'Download failed with {e}')
|
||||||
|
status = 3
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f'Download failed with {e.args}')
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
if status != 0:
|
||||||
|
logger.warning('Download failed, restore previous AppImage')
|
||||||
|
try:
|
||||||
|
os.rename(new_file.with_suffix('.AppImage.old'), new_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
# zsync does not support https
|
||||||
|
|
||||||
|
self.finished.emit(status, str(new_file))
|
||||||
|
|
||||||
|
|
||||||
class Updater:
|
class Updater:
|
||||||
@ -220,7 +220,6 @@ class Updater:
|
|||||||
return f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
return f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@lru_cache(3)
|
|
||||||
def get_zsync():
|
def get_zsync():
|
||||||
url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
||||||
m_time_zsync = None
|
m_time_zsync = None
|
||||||
@ -228,11 +227,10 @@ class Updater:
|
|||||||
zsync_file = None
|
zsync_file = None
|
||||||
filename = None
|
filename = None
|
||||||
try:
|
try:
|
||||||
response = requests.get(url_zsync)
|
with urllib.request.urlopen(url_zsync) as response:
|
||||||
if response.status_code == 200:
|
zsync_file = response.read()
|
||||||
zsync_file = response.content
|
except HTTPError as e:
|
||||||
else:
|
logger.error(f'Request for zsync returned code {e}')
|
||||||
logger.error(f'Request for zsync returned code {response.status_code}')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f'Download of zsync failed with exception {e.args}')
|
logger.exception(f'Download of zsync failed with exception {e.args}')
|
||||||
|
|
||||||
@ -287,23 +285,3 @@ class Updater:
|
|||||||
return None, m_time_file, m_time_zsync
|
return None, m_time_file, m_time_zsync
|
||||||
else:
|
else:
|
||||||
return checksum_file != checksum_zsync, m_time_file, m_time_zsync
|
return checksum_file != checksum_zsync, m_time_file, m_time_zsync
|
||||||
|
|
||||||
|
|
||||||
def open_bug_report():
|
|
||||||
form_entries = {
|
|
||||||
'description': 'Please state the nature of the medical emergency.',
|
|
||||||
'title': 'Everything is awesome?',
|
|
||||||
'assign[0]': 'dominik',
|
|
||||||
'subscribers[0]': 'dominik',
|
|
||||||
'tag': 'nmreval',
|
|
||||||
'priority': 'normal',
|
|
||||||
'status': 'open',
|
|
||||||
}
|
|
||||||
full_url = 'https://chaos3.fkp.physik.tu-darmstadt.de/maniphest/task/edit/?'
|
|
||||||
|
|
||||||
for k, v in form_entries.items():
|
|
||||||
full_url += f'{k}={v}&'
|
|
||||||
full_url.replace(' ', '+')
|
|
||||||
|
|
||||||
import webbrowser
|
|
||||||
webbrowser.open(full_url)
|
|
||||||
|
@ -23,6 +23,7 @@ from ..io.fcbatchreader import QFCReader
|
|||||||
from ..io.filedialog import *
|
from ..io.filedialog import *
|
||||||
from ..lib import get_icon, make_action_icons
|
from ..lib import get_icon, make_action_icons
|
||||||
from ..lib.pg_objects import RegionItem
|
from ..lib.pg_objects import RegionItem
|
||||||
|
from ..lib.starter import make_starter
|
||||||
from ..math.evaluation import QEvalDialog
|
from ..math.evaluation import QEvalDialog
|
||||||
from ..math.interpol import InterpolDialog
|
from ..math.interpol import InterpolDialog
|
||||||
from ..math.mean_dialog import QMeanTimes
|
from ..math.mean_dialog import QMeanTimes
|
||||||
@ -30,7 +31,7 @@ from ..math.smooth import QSmooth
|
|||||||
from ..nmr.coupling_calc import QCoupCalcDialog
|
from ..nmr.coupling_calc import QCoupCalcDialog
|
||||||
from ..nmr.t1_from_tau import QRelaxCalc
|
from ..nmr.t1_from_tau import QRelaxCalc
|
||||||
from .._py.basewindow import Ui_BaseWindow
|
from .._py.basewindow import Ui_BaseWindow
|
||||||
from ..lib.utils import UpdateDialog, open_bug_report, Updater
|
from ..lib.utils import UpdateDialog, Updater
|
||||||
|
|
||||||
|
|
||||||
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
||||||
@ -1059,3 +1060,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
self.status.setText('Autosave...')
|
self.status.setText('Autosave...')
|
||||||
NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path)
|
NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path)
|
||||||
self.status.setText('')
|
self.status.setText('')
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(name='on_actionCreate_starter_triggered')
|
||||||
|
def create_starter(self):
|
||||||
|
make_starter(os.getenv('APPIMAGE'))
|
||||||
|
@ -17,9 +17,13 @@ def check_for_config(make=True):
|
|||||||
conf_path.mkdir(parents=True)
|
conf_path.mkdir(parents=True)
|
||||||
cwd = pathlib.Path(__file__).parent
|
cwd = pathlib.Path(__file__).parent
|
||||||
copyfile(cwd / 'models' / 'usermodels.py', conf_path / 'usermodels.py')
|
copyfile(cwd / 'models' / 'usermodels.py', conf_path / 'usermodels.py')
|
||||||
|
|
||||||
with resource_path('resources', 'Default.agr') as fp:
|
with resource_path('resources', 'Default.agr') as fp:
|
||||||
copyfile(fp, conf_path / 'Default.agr')
|
copyfile(fp, conf_path / 'Default.agr')
|
||||||
|
|
||||||
|
with resource_path('resources', 'logo.png') as fp:
|
||||||
|
copyfile(fp, conf_path / 'logo.png')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@ -263,6 +263,7 @@
|
|||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_colorcycle"/>
|
<addaction name="action_colorcycle"/>
|
||||||
<addaction name="actionConfiguration"/>
|
<addaction name="actionConfiguration"/>
|
||||||
|
<addaction name="actionCreate_starter"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuWindow">
|
<widget class="QMenu" name="menuWindow">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@ -999,6 +1000,11 @@
|
|||||||
<string>Show error log</string>
|
<string>Show error log</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionCreate_starter">
|
||||||
|
<property name="text">
|
||||||
|
<string>Create starter..</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
Loading…
Reference in New Issue
Block a user