diff --git a/spinetoolbox/config.py b/spinetoolbox/config.py index 9b556add4..a31424bf6 100644 --- a/spinetoolbox/config.py +++ b/spinetoolbox/config.py @@ -18,7 +18,7 @@ LATEST_PROJECT_VERSION = 13 # For the Add/Update SpineOpt wizard -REQUIRED_SPINE_OPT_VERSION = "0.8.3" +REQUIRED_SPINE_OPT_VERSION = "0.9.0" # Invalid characters for directory names # NOTE: "." is actually valid in a directory name but this is diff --git a/spinetoolbox/helpers.py b/spinetoolbox/helpers.py index 771ba3c63..469d83aaa 100644 --- a/spinetoolbox/helpers.py +++ b/spinetoolbox/helpers.py @@ -1062,24 +1062,26 @@ def file_is_valid(parent, file_path, msgbox_title, extra_check=None): return True -def dir_is_valid(parent, dir_path, msgbox_title): +def dir_is_valid(parent, dir_path, msgbox_title, msg=None): """Checks that given path is a directory. Needed in - SettingsWdiget and KernelEditor because the QLineEdits + SettingsWidget and KernelEditor because the QLineEdits are editable. Returns True when dir_path is an empty string so that - we can use default values (e.g. from line edit place holder text) + we can use default values (e.g. from line edit placeholder text) Args: parent (QWidget): Parent widget for the message box dir_path (str): Directory path to check msgbox_title (str): Message box title + msg (str): Warning message Returns: bool: True if given path is an empty string or if path is an existing directory, False otherwise """ + if not msg: + msg = "Please select a valid directory" if dir_path == "": return True if not os.path.isdir(dir_path): - msg = "Please select a valid directory" # noinspection PyCallByClass, PyArgumentList QMessageBox.warning(parent, msgbox_title, msg) return False diff --git a/spinetoolbox/widgets/add_up_spine_opt_wizard.py b/spinetoolbox/widgets/add_up_spine_opt_wizard.py index 4b385b38a..e06bb90a5 100644 --- a/spinetoolbox/widgets/add_up_spine_opt_wizard.py +++ b/spinetoolbox/widgets/add_up_spine_opt_wizard.py @@ -13,7 +13,7 @@ """Classes for custom QDialogs for julia setup.""" from enum import IntEnum, auto from PySide6.QtCore import Qt, Slot -from PySide6.QtGui import QCursor +from PySide6.QtGui import QCursor, QTextCursor from PySide6.QtWidgets import ( QCheckBox, QFileDialog, @@ -72,6 +72,7 @@ def __init__(self, parent, julia_exe, julia_project): self.setPage(_PageId.ADD_UP_SPINE_OPT_AGAIN, AddUpSpineOptAgainPage(self)) self.setPage(_PageId.TOTAL_FAILURE, TotalFailurePage(self)) self.setStartId(_PageId.INTRO) + self.setOption(QWizard.WizardOption.NoCancelButtonOnLastPage) class IntroPage(QWizardPage): @@ -238,11 +239,10 @@ def initializePage(self): processing, code, process = { "add": ( "Installing", - 'using Pkg; pkg"registry add General https://github.com/spine-tools/SpineJuliaRegistry.git"; ' - 'pkg"add SpineOpt"', + 'using Pkg; Pkg.Registry.add("General"); Pkg.add("SpineOpt")', "installation", ), - "update": ("Updating", 'using Pkg; pkg"up SpineOpt"', "update"), + "update": ("Updating", 'using Pkg; Pkg.update("SpineOpt")', "update"), }[self.wizard().required_action] self.setTitle(f"{processing} SpineOpt") julia_exe = self.field("julia_exe") @@ -331,10 +331,23 @@ def __init__(self, parent): self.setTitle("Troubleshooting") msg = "Select your problem from the list." self._button1 = QRadioButton("None of the below") - self._button2 = QRadioButton("Installing SpineOpt fails with one of the following messages (or similar):") - msg2a = MonoSpaceFontTextBrowser(self) - msg2b = MonoSpaceFontTextBrowser(self) - msg2a.append( + self._button2 = QRadioButton("Installing SpineOpt fails with the following message (or similar):") + msg2 = MonoSpaceFontTextBrowser(self) + msg2.append( + """ + \u22ee
+ error: GitError(Code:ERROR, Class:SSL, Your Julia is built with a SSL/TLS engine that libgit2 + doesn't know how to configure to use a file or directory of certificate authority roots, + but your environment specifies one via the SSL_CERT_FILE variable. If you believe your + system's root certificates are safe to use, you can `export JULIA_SSL_CA_ROOTS_PATH=""` + in your environment to use those instead.
+ \u22ee + """ + ) + self._button3 = QRadioButton("Installing SpineOpt fails with one of the following messages (or similar):") + msg3a = MonoSpaceFontTextBrowser(self) + msg3b = MonoSpaceFontTextBrowser(self) + msg3a.append( """ \u22ee
Updating git-repo `https://github.com/spine-tools/SpineJuliaRegistry`
@@ -343,7 +356,7 @@ def __init__(self, parent): \u22ee """ ) - msg2b.append( + msg3b.append( """ \u22ee
Updating git-repo `https://github.com/spine-tools/SpineJuliaRegistry`
@@ -352,9 +365,9 @@ def __init__(self, parent): \u22ee """ ) - self._button3 = QRadioButton("On Windows 7, installing SpineOpt fails with the following message (or similar):") - msg3 = MonoSpaceFontTextBrowser(self) - msg3.append( + self._button4 = QRadioButton("On Windows 7, installing SpineOpt fails with the following message (or similar):") + msg4 = MonoSpaceFontTextBrowser(self) + msg4.append( """ \u22ee
Downloading artifact: OpenBLAS32
@@ -375,11 +388,14 @@ def __init__(self, parent): layout.addWidget(self._button1) layout.addStretch() layout.addWidget(self._button2) - layout.addWidget(msg2a) - layout.addWidget(msg2b) + layout.addWidget(msg2) layout.addStretch() layout.addWidget(self._button3) - layout.addWidget(msg3) + layout.addWidget(msg3a) + layout.addWidget(msg3b) + layout.addStretch() + layout.addWidget(self._button4) + layout.addWidget(msg4) layout.addStretch() button_view_log = QPushButton("View log") widget_view_log = QWidget() @@ -388,16 +404,24 @@ def __init__(self, parent): layout_view_log.addWidget(button_view_log) layout.addWidget(widget_view_log) layout.addStretch() + cursor = QTextCursor() + cursor.movePosition(QTextCursor.MoveOperation.Start, QTextCursor.MoveMode.MoveAnchor) + msg2.setTextCursor(cursor) # Scroll to the beginning of the document + msg3a.setTextCursor(cursor) + msg3b.setTextCursor(cursor) + msg4.setTextCursor(cursor) self.registerField("problem1", self._button1) self.registerField("problem2", self._button2) self.registerField("problem3", self._button3) + self.registerField("problem4", self._button4) self._button1.toggled.connect(lambda _: self.completeChanged.emit()) self._button2.toggled.connect(lambda _: self.completeChanged.emit()) self._button3.toggled.connect(lambda _: self.completeChanged.emit()) + self._button4.toggled.connect(lambda _: self.completeChanged.emit()) button_view_log.clicked.connect(self._show_log) def isComplete(self): - return self.field("problem1") or self.field("problem2") or self.field("problem3") + return self.field("problem1") or self.field("problem2") or self.field("problem3") or self.field("problem4") @Slot(bool) def _show_log(self, _=False): @@ -426,8 +450,11 @@ def initializePage(self): self._initialize_page_solution2() elif self.field("problem3"): self._initialize_page_solution3() + elif self.field("problem4"): + self._initialize_page_solution4() def _initialize_page_solution1(self): + self.setFinalPage(False) action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action] julia = self.field("julia_exe") env = self.field("julia_project") @@ -435,7 +462,6 @@ def _initialize_page_solution1(self): install_cmds = f""" julia> import Pkg
julia> Pkg.Registry.add("General")
- julia> Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/spine-tools/SpineJuliaRegistry"))
julia> Pkg.add("SpineOpt")
""" else: install_cmds = f""" @@ -443,7 +469,6 @@ def _initialize_page_solution1(self): julia> cd("{env}")
julia> Pkg.activate(".")
julia> Pkg.Registry.add("General")
- julia> Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/spine-tools/SpineJuliaRegistry"))
julia> Pkg.add("SpineOpt")
""" if not env: update_cmds = """ @@ -477,7 +502,42 @@ def _initialize_page_solution1(self): self.setButtonText(QWizard.WizardButton.CommitButton, action) def _initialize_page_solution2(self): - action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action] + self.setFinalPage(True) + julia = self.field("julia_exe") + env = self.field("julia_project") + self.setTitle("JULIA_SSL_CA_ROOTS_PATH environment variable missing") + description = ( + "

You are most likely running Toolbox in a Conda environment and the issue " + "you're facing is due to a missing environment variable. The simplest solution " + "is to open the Julia REPL from the Anaconda Prompt, add the environment variable, " + "and then install SpineOpt.

" + "

To do this, open your Anaconda prompt and start the Julia REPL using " + f"command:

{julia}

In the Julia REPL, enter the commands below (gray text, " + "not the green one). After entering the commands, SpineOpt should be installed. If you run into " + "other problems, please open an issue " + "with SpineOpt.

" + ) + if not env: + install_cmds = f""" + julia> using Pkg
+ julia> ENV["JULIA_SSL_CA_ROOTS_PATH"] = ""
+ julia> Pkg.Registry.add("General")
+ julia> Pkg.add("SpineOpt")
""" + else: + install_cmds = f""" + julia> using Pkg
+ julia> cd("{env}")
+ julia> Pkg.activate(".")
+ julia> ENV["JULIA_SSL_CA_ROOTS_PATH"] = ""
+ julia> Pkg.Registry.add("General")
+ julia> Pkg.add("SpineOpt")
""" + cmd_browser = MonoSpaceFontTextBrowser(self) + cmd_browser.append(install_cmds) + self.layout().addWidget(HyperTextLabel(description)) + self.layout().addWidget(cmd_browser) + + def _initialize_page_solution3(self): + self.setFinalPage(True) julia = self.field("julia_exe") self.setTitle("Reset Julia General Registry") description = ( @@ -491,16 +551,15 @@ def _initialize_page_solution2(self): ) cmds = f""" julia> import Pkg
- julia> Pkg.Registry.rm("SpineRegistry")
julia> Pkg.Registry.rm("General")
julia> Pkg.Registry.add()
""" cmd_browser = MonoSpaceFontTextBrowser(self) cmd_browser.append(cmds) self.layout().addWidget(HyperTextLabel(description)) self.layout().addWidget(cmd_browser) - self.setButtonText(QWizard.WizardButton.CommitButton, action) - def _initialize_page_solution3(self): + def _initialize_page_solution4(self): + self.setFinalPage(True) action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action] self.setTitle("Update Windows Management Framework") description = ( @@ -514,9 +573,10 @@ def _initialize_page_solution3(self): "

" ) self.layout().addWidget(HyperTextLabel(description)) - self.setButtonText(QWizard.WizardButton.CommitButton, action) def nextId(self): + if self.field("problem2") or self.field("problem3") or self.field("problem4"): + return -1 return _PageId.ADD_UP_SPINE_OPT_AGAIN diff --git a/spinetoolbox/widgets/custom_qtextbrowser.py b/spinetoolbox/widgets/custom_qtextbrowser.py index 06f94066e..a1a82b6b1 100644 --- a/spinetoolbox/widgets/custom_qtextbrowser.py +++ b/spinetoolbox/widgets/custom_qtextbrowser.py @@ -48,7 +48,7 @@ def __init__(self, parent): self._frame_format.setBorder(1) self._selected_frame_format = QTextFrameFormat(self._frame_format) palette = self.palette() - self._selected_frame_format.setBackground(QBrush(palette.color(QPalette.Highlight).darker())) + self._selected_frame_format.setBackground(QBrush(palette.color(QPalette.ColorRole.Highlight).darker())) self._executions_menu.aboutToShow.connect(self._populate_executions_menu) self._executions_menu.triggered.connect(self._select_execution) @@ -226,12 +226,13 @@ def set_item_log_selected(self, selected): frame.setFrameFormat(frame_format) -class MonoSpaceFontTextBrowser(CustomQTextBrowser): +class MonoSpaceFontTextBrowser(QTextBrowser): def __init__(self, parent): """ Args: parent (QWidget): Parent widget """ super().__init__(parent=parent) - font = QFontDatabase.systemFont(QFontDatabase.FixedFont) + self.setStyleSheet(TEXTBROWSER_SS) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) self.setFont(font) diff --git a/spinetoolbox/widgets/settings_widget.py b/spinetoolbox/widgets/settings_widget.py index 416992135..384471734 100644 --- a/spinetoolbox/widgets/settings_widget.py +++ b/spinetoolbox/widgets/settings_widget.py @@ -442,6 +442,12 @@ def _show_install_julia_wizard(self, _=False): def _show_add_up_spine_opt_wizard(self, _=False): """Opens the add/update SpineOpt wizard.""" use_julia_jupyter_console, julia_path, julia_project_path, julia_kernel = self._get_julia_settings() + if julia_project_path != "@." and not dir_is_valid( + self, + julia_project_path, + "Invalid Julia Project", + "Julia project must be an existing directory, @., or empty"): + return settings = QSettings("SpineProject", "AddUpSpineOptWizard") settings.setValue("appSettings/useJuliaKernel", use_julia_jupyter_console) settings.setValue("appSettings/juliaPath", julia_path)