-
-
Notifications
You must be signed in to change notification settings - Fork 411
wxGUI: Initial support for Jupyter-based workflows #5901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8cb8b82
1a718f3
6f2280f
4816bd0
d744c63
0b77318
c00740d
9fa46b4
09a6d9f
0169fa0
8d5acae
d2db982
2668c3f
a997e5a
039dd3b
e062d68
90610fa
b02f201
b932a13
c35b1be
a0f51e1
f569302
9461909
bce91a2
3a8f766
6825d39
1148d73
a6a9bf5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """ | ||
| @package jupyter_notebook.dialogs | ||
|
|
||
| @brief Integration of Jupyter Notebook to GUI. | ||
|
|
||
| Classes: | ||
| - dialog::JupyterStartDialog | ||
|
|
||
| (C) 2025 by the GRASS Development Team | ||
|
|
||
| This program is free software under the GNU General Public License | ||
| (>=v2). Read the file COPYING that comes with GRASS for details. | ||
|
|
||
| @author Linda Karlovska <linda.karlovska seznam.cz> | ||
| """ | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
|
|
||
| import wx | ||
|
|
||
| from grass.workflows.directory import get_default_jupyter_workdir | ||
|
|
||
|
|
||
| class JupyterStartDialog(wx.Dialog): | ||
| """Dialog for selecting Jupyter startup options.""" | ||
|
|
||
| def __init__(self, parent): | ||
| super().__init__(parent, title=_("Start Jupyter Notebook"), size=(500, 300)) | ||
|
|
||
| self.default_dir = get_default_jupyter_workdir() | ||
|
|
||
| self.selected_dir = self.default_dir | ||
| self.create_template = True | ||
|
|
||
| sizer = wx.BoxSizer(wx.VERTICAL) | ||
|
|
||
| # Working directory section | ||
| dir_box = wx.StaticBox(self, label=_("Notebook working directory")) | ||
| dir_sizer = wx.StaticBoxSizer(dir_box, wx.VERTICAL) | ||
|
|
||
| self.radio_default = wx.RadioButton( | ||
| self, | ||
| label=_("Use default: {}").format(self.default_dir), | ||
| style=wx.RB_GROUP, | ||
| ) | ||
| self.radio_custom = wx.RadioButton(self, label=_("Select another directory:")) | ||
|
|
||
| self.dir_picker = wx.DirPickerCtrl( | ||
| self, | ||
| message=_("Choose a working directory"), | ||
| style=wx.DIRP_USE_TEXTCTRL | wx.DIRP_DIR_MUST_EXIST, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why wx.DIRP_DIR_MUST_EXIST? I don't see why you shouldn't be able to create a new directory. |
||
| ) | ||
| self.dir_picker.Enable(False) | ||
|
|
||
| dir_sizer.Add(self.radio_default, 0, wx.ALL, 5) | ||
| dir_sizer.Add(self.radio_custom, 0, wx.ALL, 5) | ||
| dir_sizer.Add(self.dir_picker, 0, wx.EXPAND | wx.ALL, 5) | ||
| sizer.Add(dir_sizer, 0, wx.EXPAND | wx.ALL, 10) | ||
|
|
||
| # Template preference section | ||
| options_box = wx.StaticBox(self, label=_("Options")) | ||
| options_sizer = wx.StaticBoxSizer(options_box, wx.VERTICAL) | ||
|
|
||
| self.checkbox_template = wx.CheckBox(self, label=_("Create welcome notebook")) | ||
| self.checkbox_template.SetValue(True) | ||
| self.checkbox_template.SetToolTip( | ||
| _( | ||
| "If selected, a welcome notebook (welcome.ipynb) will be created,\n" | ||
| "but only if the selected directory contains no .ipynb files." | ||
| ) | ||
| ) | ||
| options_sizer.Add(self.checkbox_template, 0, wx.ALL, 5) | ||
|
|
||
| info = wx.StaticText( | ||
| self, | ||
| label=_( | ||
| "Note: The welcome notebook will be created only if the directory contains no .ipynb files." | ||
| ), | ||
| ) | ||
|
|
||
| options_sizer.Add(info, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 8) | ||
|
|
||
| sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) | ||
|
|
||
| btns = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL) | ||
| sizer.Add(btns, 0, wx.EXPAND | wx.ALL, 10) | ||
|
|
||
| self.SetSizer(sizer) | ||
|
|
||
| self.radio_default.Bind(wx.EVT_RADIOBUTTON, self.OnRadioToggle) | ||
| self.radio_custom.Bind(wx.EVT_RADIOBUTTON, self.OnRadioToggle) | ||
|
|
||
| self.Fit() | ||
| self.Layout() | ||
| self.SetMinSize(self.GetSize()) | ||
| self.CentreOnParent() | ||
|
|
||
| def OnRadioToggle(self, event): | ||
| """Enable/disable directory picker based on user choice.""" | ||
| self.dir_picker.Enable(self.radio_custom.GetValue()) | ||
|
|
||
| def GetValues(self): | ||
| """Return selected working directory and template preference.""" | ||
| if self.radio_custom.GetValue(): | ||
| path = Path(self.dir_picker.GetPath()) | ||
|
|
||
| if not os.access(path, os.W_OK) or not os.access(path, os.X_OK): | ||
| wx.MessageBox( | ||
| _("You do not have permission to write to the selected directory."), | ||
| _("Error"), | ||
| wx.ICON_ERROR, | ||
| ) | ||
| return None | ||
| self.selected_dir = path | ||
| else: | ||
| self.selected_dir = self.default_dir | ||
|
|
||
| return { | ||
| "directory": self.selected_dir, | ||
| "create_template": self.checkbox_template.GetValue(), | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| """ | ||
| @package jupyter_notebook.frame | ||
|
|
||
| @brief Manages the Jupyter frame widget for multi-window GUI | ||
|
|
||
| Classes: | ||
| - frame::JupyterFrame | ||
|
|
||
| (C) 2025-2026 by the GRASS Development Team | ||
|
|
||
| This program is free software under the GNU General Public License | ||
| (>=v2). Read the file COPYING that comes with GRASS for details. | ||
|
|
||
| @author Linda Karlovska <linda.karlovska seznam.cz> | ||
| """ | ||
|
|
||
| import os | ||
| import wx | ||
|
|
||
| from core import globalvar | ||
| from jupyter_notebook.panel import JupyterPanel | ||
|
|
||
|
|
||
| class JupyterFrame(wx.Frame): | ||
| """Main window for the Jupyter Notebook interface.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| parent, | ||
| giface, | ||
| workdir=None, | ||
| create_template=False, | ||
| id=wx.ID_ANY, | ||
| title=_("Jupyter Notebook"), | ||
| **kwargs, | ||
| ): | ||
| super().__init__(parent=parent, id=id, title=title, **kwargs) | ||
|
|
||
| self.SetName("JupyterFrame") | ||
|
|
||
| icon_path = os.path.join(globalvar.ICONDIR, "grass.ico") | ||
| self.SetIcon(wx.Icon(icon_path, wx.BITMAP_TYPE_ICO)) | ||
|
|
||
| self.statusbar = self.CreateStatusBar(number=1) | ||
|
|
||
| self.panel = JupyterPanel( | ||
| parent=self, | ||
| giface=giface, | ||
| workdir=workdir, | ||
| create_template=create_template, | ||
| statusbar=self.statusbar, | ||
| ) | ||
| self.panel.SetUpNotebookInterface() | ||
|
|
||
| sizer = wx.BoxSizer(wx.VERTICAL) | ||
| sizer.Add(self.panel, 1, wx.EXPAND) | ||
| self.SetSizer(sizer) | ||
|
|
||
| self.SetSize((800, 600)) | ||
|
|
||
| self.Bind(wx.EVT_CLOSE, self.panel.OnCloseWindow) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| """ | ||
| @package jupyter_notebook.notebook | ||
|
|
||
| @brief Manages the jupyter notebook widget. | ||
|
|
||
| Classes: | ||
| - page::JupyterAuiNotebook | ||
|
|
||
| (C) 2025 by the GRASS Development Team | ||
|
|
||
| This program is free software under the GNU General Public License | ||
| (>=v2). Read the file COPYING that comes with GRASS for details. | ||
|
|
||
| @author Linda Karlovska <linda.karlovska seznam.cz> | ||
| """ | ||
|
|
||
| import wx | ||
| from wx.lib.agw import aui | ||
|
|
||
| try: | ||
| import wx.html2 as html # wx.html2 is available in wxPython 4.0 and later | ||
| except ImportError as e: | ||
| raise RuntimeError(_("wx.html2 is required for Jupyter integration.")) from e | ||
|
|
||
| from gui_core.wrap import SimpleTabArt | ||
|
|
||
|
|
||
| class JupyterAuiNotebook(aui.AuiNotebook): | ||
| def __init__( | ||
| self, | ||
| parent, | ||
| agwStyle=aui.AUI_NB_DEFAULT_STYLE | ||
| | aui.AUI_NB_CLOSE_ON_ACTIVE_TAB | ||
| | aui.AUI_NB_TAB_EXTERNAL_MOVE | ||
| | aui.AUI_NB_BOTTOM | ||
| | wx.NO_BORDER, | ||
| ): | ||
| """ | ||
| Wrapper for the notebook widget that manages notebook pages. | ||
| """ | ||
| self.parent = parent | ||
| self.webview = None | ||
|
|
||
| super().__init__(parent=self.parent, id=wx.ID_ANY, agwStyle=agwStyle) | ||
|
|
||
| self.SetArtProvider(SimpleTabArt()) | ||
|
|
||
| self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.OnPageClose) | ||
|
|
||
| def _inject_javascript(self, event): | ||
| """ | ||
| Inject JavaScript into the Jupyter notebook page to hide top UI bars. | ||
|
|
||
| Works for: | ||
| - Jupyter Notebook 6 and older (classic interface) | ||
| - Jupyter Notebook 7+ (Jupyter Lab interface) | ||
|
|
||
| This is called once the WebView has fully loaded the Jupyter page. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but then the code uses interval to run the code, so this text should probably reflect that, possibly only before the JS code block , like "...but Jupyter UI may be (is?) create(ing?) the elements dynamically afterwards, so we need to wait for them to appear." |
||
| """ | ||
| webview = event.GetEventObject() | ||
| js = """ | ||
| var interval = setInterval(function() { | ||
| // --- Jupyter Notebook 7+ (new UI) --- | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't want the |
||
| var topPanel = document.getElementById('top-panel-wrapper'); | ||
| var menuPanel = document.getElementById('menu-panel-wrapper'); | ||
| if (topPanel) topPanel.style.display = 'none'; | ||
| if (menuPanel) menuPanel.style.display = 'none'; | ||
|
|
||
| // --- Jupyter Notebook 6 and older (classic UI) --- | ||
| var headerContainer = document.getElementById('header-container'); | ||
| var menubar = document.getElementById('menubar'); | ||
| if (headerContainer) headerContainer.style.display = 'none'; | ||
| if (menubar) menubar.style.display = 'none'; | ||
|
|
||
| // --- Stop once everything is hidden --- | ||
| if ((topPanel || headerContainer) && (menuPanel || menubar)) { | ||
| clearInterval(interval); | ||
| } | ||
| }, 500); | ||
| """ | ||
| webview.RunScript(js) | ||
|
Comment on lines
+60
to
+81
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest discussing this with Copilot or something. At least max tries would be nice. |
||
|
|
||
| def AddPage(self, url, title): | ||
| """ | ||
| Add a new aui notebook page with a Jupyter WebView. | ||
| :param url: URL of the Jupyter file (str). | ||
| :param title: Tab title (str). | ||
| """ | ||
| browser = html.WebView.New(self) | ||
| wx.CallAfter(browser.LoadURL, url) | ||
| wx.CallAfter(browser.Bind, html.EVT_WEBVIEW_LOADED, self._inject_javascript) | ||
| super().AddPage(browser, title) | ||
|
|
||
| def OnPageClose(self, event): | ||
| """Close the aui notebook page with confirmation dialog.""" | ||
| index = event.GetSelection() | ||
| title = self.GetPageText(index) | ||
|
|
||
| dlg = wx.MessageDialog( | ||
| self, | ||
| message=_("Really close page '{}'?").format(title), | ||
| caption=_("Close page"), | ||
| style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION, | ||
| ) | ||
|
|
||
| if dlg.ShowModal() != wx.ID_YES: | ||
| event.Veto() | ||
|
|
||
| dlg.Destroy() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a basically a starter of
jupyter notebook --no-browserwith JupyterServerInstance being the controller for it. So, it seems it wouldn't be difficult to add a button here to startjupyter notebookwith browser orjupyter lab. This would help users get the full Jupyter Notebook or Jupyter Lab experience without running these from the command line. Button in the GUI dialog:Cancel | Open Notebook in Browser | Open Lab in Browser | Open Integrated Notebook
For Open Notebook in Browser and Open Lab in Browser, the notebook tab shows only controls for the shutting down the server. Open Integrated Notebook is the current experience.
This may align well with something like a Run RStudio button. We already do things to make running RStudio easier from within a GRASS session on Windows (see
grass/mswindows), but in this case, we don't do anything for it in GUI (unlike Python scripts and now Jupyter notebooks).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notably, this would be still available even when the user does not have wx.html2.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great suggestion, I like it!!