From 31477f3c4903abe86530c77a3859e0309fd7a36c Mon Sep 17 00:00:00 2001 From: Christophe Gueret Date: Sun, 19 Apr 2026 09:05:23 +0100 Subject: [PATCH 1/2] Show storage and load Library dynamically Inform the user of the storage location in the settings. Create a Library navigation page based on the storage location, will enable re-creating it dynamically when the location changes --- scriptorium/dialogs/preferences.blp | 7 +- scriptorium/dialogs/preferences.py | 8 ++ scriptorium/views/editor.blp | 3 +- scriptorium/views/editor.py | 18 ++-- scriptorium/views/library.blp | 1 + scriptorium/views/library.py | 73 +++++++++----- scriptorium/views/write/navigation.py | 14 ++- scriptorium/views/write/page.py | 1 + scriptorium/window.blp | 2 +- scriptorium/window.py | 135 ++++++++++++-------------- 10 files changed, 153 insertions(+), 109 deletions(-) diff --git a/scriptorium/dialogs/preferences.blp b/scriptorium/dialogs/preferences.blp index e18ece6..19ccb50 100644 --- a/scriptorium/dialogs/preferences.blp +++ b/scriptorium/dialogs/preferences.blp @@ -23,12 +23,17 @@ template $ScrptPreferencesDialog: Adw.PreferencesDialog { title: _("Restore Previous Session"); subtitle: _("Open the last project when starting Scriptorium"); } + + Adw.ActionRow projects_directory { + title: _("Data directory"); + subtitle: _(""); + } } } Adw.PreferencesPage { title: _("Editor"); - icon-name: "settings-symbolic"; + icon-name: "edit-symbolic"; Adw.PreferencesGroup { title: _("Preview"); diff --git a/scriptorium/dialogs/preferences.py b/scriptorium/dialogs/preferences.py index 820873d..69ddb9e 100644 --- a/scriptorium/dialogs/preferences.py +++ b/scriptorium/dialogs/preferences.py @@ -45,6 +45,7 @@ class ScrptPreferencesDialog(Adw.PreferencesDialog): editor_line_height = Gtk.Template.Child() font_dialog_button = Gtk.Template.Child() editor_underline_style = Gtk.Template.Child() + projects_directory = Gtk.Template.Child() def __init__(self): """Create a new instance of the class.""" @@ -62,6 +63,13 @@ def __init__(self): Gio.SettingsBindFlags.DEFAULT ) + settings.bind( + "manuscripts-folder", + self.projects_directory, + "subtitle", + Gio.SettingsBindFlags.DEFAULT + ) + settings.bind( "editor-line-height", self.editor_line_height, diff --git a/scriptorium/views/editor.blp b/scriptorium/views/editor.blp index 34a8b87..cf88727 100644 --- a/scriptorium/views/editor.blp +++ b/scriptorium/views/editor.blp @@ -4,7 +4,8 @@ using Adw 1; template $ScrptEditorView: Adw.NavigationPage { title: _("Editor"); tag: "editor"; - unrealize => $on_editorview_closed(); + realize => $on_scrpteditorview_realize(); + unrealize => $on_scrpteditorview_unrealize(); child: Adw.ToolbarView { top-bar-style: raised_border; diff --git a/scriptorium/views/editor.py b/scriptorium/views/editor.py index f09f2dc..5911e70 100644 --- a/scriptorium/views/editor.py +++ b/scriptorium/views/editor.py @@ -53,7 +53,7 @@ class ScrptEditorView(Adw.NavigationPage): plan_page = Gtk.Template.Child() file_filter_image = Gtk.Template.Child() - def __init__(self): + def __init__(self, project: Project): """Create a new instance of the editor.""" super().__init__() @@ -114,16 +114,20 @@ def __init__(self): ) group.add_action(action) - def connect_to_project(self, project: Project): - # Keep track of the project the editor is associated to + # Keep track of the project for this editor window self.project = project - self.write_page.connect_to_project(project) - self.publish_page.connect_to_project(project) - self.plan_page.connect_to_project(project) + @Gtk.Template.Callback() + def on_scrpteditorview_realize(self, _editorview): + """Called when the editor widgets are all created.""" + if self.project is not None: + logger.info("Editor is open, connect the tabs to the manuscript") + self.plan_page.connect_to_project(self.project) + self.write_page.connect_to_project(self.project) + self.publish_page.connect_to_project(self.project) @Gtk.Template.Callback() - def on_editorview_closed(self, _editorview): + def on_scrpteditorview_unrealize(self, _editorview): """Handle a request to close the editor.""" if self.project is not None: logger.info("Editor is closed, saving the manuscript") diff --git a/scriptorium/views/library.blp b/scriptorium/views/library.blp index 6ef1370..c4881c9 100644 --- a/scriptorium/views/library.blp +++ b/scriptorium/views/library.blp @@ -6,6 +6,7 @@ template $ScrptLibraryView: Adw.NavigationPage { tag: "library"; shown => $on_scrptlibraryview_shown(); + realize => $on_scrptlibraryview_realize(); child: Adw.ToolbarView { [top] diff --git a/scriptorium/views/library.py b/scriptorium/views/library.py index 2afb862..d8a44c5 100644 --- a/scriptorium/views/library.py +++ b/scriptorium/views/library.py @@ -20,6 +20,8 @@ from gi.repository import Adw, GObject, Gio, Gtk from gi.repository import GLib +from scriptorium.views import ScrptEditorView + from scriptorium.globals import BASE from scriptorium.models import Library, Project from scriptorium.dialogs import ScrptAddDialog @@ -38,8 +40,6 @@ class ScrptLibraryView(Adw.NavigationPage): # The Library is the data model holding the list of projects library: Library = Library() - selected_project = GObject.Property(type=Project) - # The base path of all the manuscripts manuscripts_base_path = GObject.Property(type=str) @@ -55,8 +55,10 @@ class ScrptLibraryView(Adw.NavigationPage): identifier = Gtk.Template.Child() - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, manuscripts_base_path): + super().__init__() + + self.manuscripts_base_path = manuscripts_base_path # Connect an instance of the theme button to the menu popover = self.win_menu.get_popover() @@ -83,6 +85,7 @@ def __init__(self, **kwargs): group.add_action(action) # Signal to the list model to detect when content is available + # this is useful when a new Manuscript is created self.library.projects.connect( "items-changed", self.on_grid_content_changed @@ -98,16 +101,25 @@ def __init__(self, **kwargs): @Gtk.Template.Callback() def on_scrptlibraryview_shown(self, _src): - """Update the selected project to None.""" + """ + Called when the window becomes visible. This is likely to happen + when the editor is closed so we use the callback to unset the selected + manuscript. + """ + logger.info("Shown") # Disable the current selection selection_model = self.projects_grid.get_model() selection_model.set_selected(Gtk.INVALID_LIST_POSITION) - # Set the window to no project opened - window = self.props.root - if window is not None: - window.project = None + @Gtk.Template.Callback() + def on_scrptlibraryview_realize(self, _src): + logger.info(f"Opening library at {self.manuscripts_base_path}") + + # Connect the library to the folder + self.library.open_folder(self.manuscripts_base_path) + + self.open_last_project() @Gtk.Template.Callback() def on_add_manuscript_clicked(self, _button): @@ -142,20 +154,40 @@ def on_grid_content_changed(self, list_model, _position, _added, _removed): def on_selection_changed(self, selection_model, position, n_items): """ - Called when a manuscript is selected + Called when a manuscript is selected in the list. """ # Get the selected project - selected_item = selection_model.get_selected_item() - if selected_item is not None: - selected_project = selection_model.get_selected_item() + selected_project = selection_model.get_selected_item() + + # Keep track of the last manuscript selected (this could be None) + logger.info(f"Set last selected project to {selected_project}") + settings = Gio.Settings(schema_id="io.github.cgueret.Scriptorium") + settings.set_string( + "last-manuscript-name", + selected_project.identifier if selected_project is not None else "" + ) + + selected_project = selection_model.get_selected_item() + if selected_project is not None: logger.info(f"Selected project {selected_project.identifier}") if not selected_project.can_be_opened: self.migrate_dialog.choose(self) #selection_model.set_selected(Gtk.INVALID_LIST_POSITION) else: # Open the project - window = self.props.root - window.project = selected_project + self._open_project(selected_project) + + def _open_project(self, project): + """ + Open a selected project and remember it as the last project selected + """ + # If we did select something, open the editor + if project is not None: + logger.info(f"\"{project.title}\": create and open editor") + + # Create an editor navigation page and push it to the navigation + editor_page = ScrptEditorView(project) + self.get_parent().push(editor_page) def open_last_project(self): """Check if we need to open the last project.""" @@ -182,15 +214,6 @@ def open_last_project(self): if model[index].can_be_opened: model.select_item(index, True) - def on_projects_base_path_changed(self, window, parameter): - base_path = window.get_property(parameter.name) - logger.info(f"Opening library at {base_path}") - - # Connect the library to the folder - self.library.open_folder(base_path) - - self.open_last_project() - @Gtk.Template.Callback() def on_migrate_dialog_response(self, _dialog, response): """Handle a response to migrating a project.""" @@ -211,7 +234,7 @@ def on_migrate_dialog_response(self, _dialog, response): window.inform("Project successfuly migrated!") # Open the project right away - window.project = selected_project + self._open_project(selected_project) else: window.inform("Something went wrong. See logs for details") diff --git a/scriptorium/views/write/navigation.py b/scriptorium/views/write/navigation.py index 837fac1..cb68893 100644 --- a/scriptorium/views/write/navigation.py +++ b/scriptorium/views/write/navigation.py @@ -16,9 +16,11 @@ # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later +from gettext import gettext as _ + import logging -from gi.repository import Gio, Graphene, Gtk, Adw +from gi.repository import Gio, Graphene, Gtk, Adw, GLib from scriptorium.globals import BASE from scriptorium.models import Chapter, Manuscript, Scene @@ -76,6 +78,7 @@ def on_list_item_bind(self, _, list_item): def connect_to(self, project): """Connect the navigation to the project and its contents.""" + logger.info(f"Connect navigation to {project}") # Turn the content into a tree, instance of Chapter may have children roots = Gio.ListStore.new(Manuscript) @@ -102,6 +105,7 @@ def connect_to(self, project): # append items at the end of the manuscript by default. Users who want # to position items directly can use the context actions instead manuscript_id = project.manuscript.identifier + logger.info(f"Connect navigation to {manuscript_id}") menu = Gio.Menu() menu.append( label=_("Add new Scene"), @@ -112,7 +116,11 @@ def connect_to(self, project): detailed_action=f"editor.add_resource(('Chapter', '{manuscript_id}'))" ) self.add_menu.set_menu_model(menu) - self.add_menu.set_detailed_action_name( - f"editor.add_resource(('Scene', '{manuscript_id}'))" + self.add_menu.connect( + "clicked", + lambda _: self.activate_action( + "editor.add_resource", + GLib.Variant("(ss)", ("Scene", manuscript_id)) + ) ) diff --git a/scriptorium/views/write/page.py b/scriptorium/views/write/page.py index 924fd26..f6ef48f 100644 --- a/scriptorium/views/write/page.py +++ b/scriptorium/views/write/page.py @@ -60,6 +60,7 @@ class WritePage(Adw.Bin): def __init__(self): """Create an instance of the editor.""" super().__init__() + # By default we have no active scene self.active_scene = None diff --git a/scriptorium/window.blp b/scriptorium/window.blp index 0df8f4f..e614fa6 100644 --- a/scriptorium/window.blp +++ b/scriptorium/window.blp @@ -8,11 +8,11 @@ template $ScrptWindow: Adw.ApplicationWindow { content: Adw.ToastOverlay toast_overlay { child: Adw.NavigationView navigation { - $ScrptLibraryView library_panel {} }; }; close-request => $on_close_request(); + realize => $on_scrptwindow_realize(); ShortcutController { Shortcut { diff --git a/scriptorium/window.py b/scriptorium/window.py index 97f90c9..a4da41e 100644 --- a/scriptorium/window.py +++ b/scriptorium/window.py @@ -21,8 +21,9 @@ from gi.repository import Gtk, Adw, GObject, Gdk, Gio, GLib from pathlib import Path -# It seems Builder won't find the widgets unless we import them? -from scriptorium.views import ScrptEditorView +# Needed here for some weird reason, Builder does not find it otherwise +# TODO: idea: maybe instantiate the libraryview and reset it on folder change +from scriptorium.views import ScrptLibraryView from scriptorium.models import Project from scriptorium.globals import BASE @@ -38,17 +39,25 @@ class ScrptWindow(Adw.ApplicationWindow): __gtype_name__ = 'ScrptWindow' navigation = Gtk.Template.Child() - library_panel = Gtk.Template.Child() toast_overlay = Gtk.Template.Child() # This is a pointer to the currently open project, defaults to None - project = GObject.Property(type=Project, default=None) + #project = GObject.Property( + # type=Project, + # default=None + #) # The base path of all the manuscripts - projects_base_path = GObject.Property(type=str) + manuscripts_folder = GObject.Property( + type=str, + default=None + ) # This is the identifier of the manuscript that was last opened - last_manuscript_name = GObject.Property(type=str, default=None) + last_manuscript_name = GObject.Property( + type=str, + default=None + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -58,9 +67,11 @@ def __init__(self, **kwargs): css_provider.load_from_file( Gio.File.new_for_uri(f"resource:/{BASE}/style.css") ) - Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) # Load custom icons theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) @@ -70,17 +81,26 @@ def __init__(self, **kwargs): self.settings = Gio.Settings(schema_id="io.github.cgueret.Scriptorium") # Bind the settings related to the window - self.settings.bind("window-width", self, "default-width", + self.settings.bind( + "window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT ) - self.settings.bind("window-height", self, "default-height", + self.settings.bind( + "window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT ) - self.settings.bind("window-maximized", self, "maximized", + self.settings.bind( + "window-maximized", self, "maximized", Gio.SettingsBindFlags.DEFAULT ) - self.settings.bind("last-manuscript-name", self, "last-manuscript-name", + # Bindings related to projects management + self.settings.bind( + "last-manuscript-name", self, "last-manuscript-name", + Gio.SettingsBindFlags.DEFAULT + ) + self.settings.bind( + "manuscripts-folder", self, "manuscripts-folder", Gio.SettingsBindFlags.DEFAULT ) @@ -97,23 +117,34 @@ def __init__(self, **kwargs): # The library is where a project is selected by the user. We keep an # eye on actions there - self.connect( - 'notify::project', - self.on_project_changed - ) - - # Connect a callback in the library to keep an eye on projects base path - self.connect( - 'notify::projects-base-path', - self.library_panel.on_projects_base_path_changed - ) + #self.connect( + # 'notify::project', + # self.on_project_changed + #) # Open the default data directory # (TODO Implement the setting for data folder) - projects_path = Path(GLib.get_user_data_dir()) / Path('manuscripts') - if not projects_path.exists(): - projects_path.mkdir() - self.projects_base_path = projects_path.resolve() + #projects_path = Path(self.manuscripts_folder) / Path('manuscripts') + #if not projects_path.exists(): + # projects_path.mkdir() + #self.projects_base_path = projects_path.resolve() + + @Gtk.Template.Callback() + def on_scrptwindow_realize(self, window): + """Called with the window is created.""" + + # See if we have a manuscript folder path set + # If not, use the app base directory + if not self.manuscripts_folder or self.manuscripts_folder == "": + folder = Path(GLib.get_user_data_dir()) / "manuscripts" + folder.mkdir(exist_ok=True) + self.manuscripts_folder = folder + + # Inform the user of the data folder + logger.info(f'Data location: {self.manuscripts_folder}') + + # Open the library at this location + self._open_library() @Gtk.Template.Callback() def on_close_request(self, event): @@ -121,51 +152,13 @@ def on_close_request(self, event): # Save the name of the last edited project def _open_library(self): - # Get a reference to the library panel - self.library_panel.connect('notify::selected-project', - self.on_selected_project_changed) - - # Set the data folder - manuscript_path = Path(GLib.get_user_data_dir()) / Path('manuscripts') - if not manuscript_path.exists(): - manuscript_path.mkdir() - logger.info(f'Data location: {manuscript_path}') - self.library_panel.set_property('manuscripts_base_path', - manuscript_path.resolve()) - - #last_opened = self.settings.get_string("last-manuscript-name") - #logger.info(f"Trigger selection for last opened: {last_opened}") - - #manuscripts_model = self._library_panel.manuscripts_grid.get_model() - #if self.settings.get_boolean("open-last-project"): - # if len(manuscripts_model) > 0: - # index = 0 - # for i in range(len(manuscripts_model)): - # if manuscripts_model[i].identifier == last_opened: - # index = i - # manuscripts_model.select_item(index, True) - - def on_project_changed(self, _navigation, _other): - """Handle a change in the selected project.""" - logger.info(f"Change currently edited project to {self.project}") - - # If we did select something, open the editor - if self.project is not None: - logger.info(f"\"{self.project.title}\": create and open editor") - - # Create an editor navigation page and push it to the stack - editor_page = ScrptEditorView() - if not self.project.is_opened: - self.project.open() - editor_page.connect_to_project(self.project) - self.navigation.push(editor_page) - - # Keep track of the last manuscript selected - settings = Gio.Settings(schema_id="io.github.cgueret.Scriptorium") - settings.set_string( - "last-manuscript-name", - self.project.identifier if self.project is not None else "" - ) + """Create a library panel and add it to the navigation.""" + + # Create a library panel + library_panel = ScrptLibraryView(self.manuscripts_folder) + + # Add it to the navigation + self.navigation.push(library_panel) def close_editor(self, editor_view): self.navigation.pop() From cf92d0aba229e914ec1627a689798326729d26c4 Mon Sep 17 00:00:00 2001 From: Christophe Gueret Date: Sun, 19 Apr 2026 22:07:27 +0100 Subject: [PATCH 2/2] Added dialog to select storage location Now users can decide where the manuscripts are stored --- scriptorium/dialogs/preferences.blp | 13 ++++++++++ scriptorium/dialogs/preferences.py | 40 +++++++++++++++++++++++++---- scriptorium/window.py | 35 ++++++++++++++----------- 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/scriptorium/dialogs/preferences.blp b/scriptorium/dialogs/preferences.blp index 19ccb50..6eb95e4 100644 --- a/scriptorium/dialogs/preferences.blp +++ b/scriptorium/dialogs/preferences.blp @@ -27,6 +27,14 @@ template $ScrptPreferencesDialog: Adw.PreferencesDialog { Adw.ActionRow projects_directory { title: _("Data directory"); subtitle: _(""); + + [suffix] + Gtk.Button projects_directory_button { + icon-name: "folder-symbolic"; + tooltip-text: _("Change storage location"); + valign: center; + clicked => $on_projects_directory_button_clicked(); + } } } } @@ -100,3 +108,8 @@ template $ScrptPreferencesDialog: Adw.PreferencesDialog { } } } + +Gtk.FileDialog projects_directory_dialog { + title: _("Select storage location"); + modal: true; +} diff --git a/scriptorium/dialogs/preferences.py b/scriptorium/dialogs/preferences.py index 69ddb9e..d900d02 100644 --- a/scriptorium/dialogs/preferences.py +++ b/scriptorium/dialogs/preferences.py @@ -17,7 +17,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later """Dialog to select scenes in Scriptorium.""" -from gi.repository import Adw, Gtk, Gio, Pango +from gi.repository import Adw, Gtk, Gio, Pango, GLib from scriptorium.globals import BASE from scriptorium.utils import html_to_buffer from gettext import gettext as _ @@ -27,9 +27,9 @@ logger = logging.getLogger(__name__) PLACEHOLDER_TEXT = _( -"

This is a placeholder text to select" +"

This is a placeholder text to select " "the font of the scene editor.

" -"

You can also see how annotations are shown and how words" +"

You can also see how annotations are shown and how words " "with an emphasis or noted as strong will appear.

" ) UNDERLINE_OPTIONS = ["single", "double", "dashed"] @@ -39,13 +39,19 @@ class ScrptPreferencesDialog(Adw.PreferencesDialog): __gtype_name__ = "ScrptPreferencesDialog" + # Setting to open the last project on application launch open_last_project = Gtk.Template.Child() + + # Settings for the editor text_view = Gtk.Template.Child() font_dialog_button = Gtk.Template.Child() editor_line_height = Gtk.Template.Child() - font_dialog_button = Gtk.Template.Child() editor_underline_style = Gtk.Template.Child() - projects_directory = Gtk.Template.Child() + + # Information about the storage location for manuscripts + projects_directory: Adw.ActionRow = Gtk.Template.Child() + projects_directory_button: Gtk.Button = Gtk.Template.Child() + projects_directory_dialog: Gtk.FileDialog = Gtk.Template.Child() def __init__(self): """Create a new instance of the class.""" @@ -119,3 +125,27 @@ def on_underline_style_selected(self, _combo, _value): UNDERLINE_OPTIONS[selected_value] ) + @Gtk.Template.Callback() + def on_projects_directory_button_clicked(self, _button): + """Handle a click to select storage location.""" + def on_folder_selected(dialog, result): + try: + folder = dialog.select_folder_finish(result) + except GLib.Error as e: + logger.info(f"Error when changing data folder: {e}") + return + self.projects_directory.set_subtitle(folder.get_path()) + + # Configure the dialog to open the current storage value + self.projects_directory_dialog.set_initial_folder( + Gio.File.new_for_path(self.projects_directory.get_subtitle()) + ) + + # Open the dialog + self.projects_directory_dialog.select_folder( + parent=self.get_root(), + cancellable=None, + callback=on_folder_selected, + ) + + diff --git a/scriptorium/window.py b/scriptorium/window.py index a4da41e..bc094ab 100644 --- a/scriptorium/window.py +++ b/scriptorium/window.py @@ -117,10 +117,10 @@ def __init__(self, **kwargs): # The library is where a project is selected by the user. We keep an # eye on actions there - #self.connect( - # 'notify::project', - # self.on_project_changed - #) + self.connect( + 'notify::manuscripts-folder', + self.on_manuscripts_folder_changed + ) # Open the default data directory # (TODO Implement the setting for data folder) @@ -143,23 +143,16 @@ def on_scrptwindow_realize(self, window): # Inform the user of the data folder logger.info(f'Data location: {self.manuscripts_folder}') - # Open the library at this location - self._open_library() - - @Gtk.Template.Callback() - def on_close_request(self, event): - logger.info("Window close requested") - # Save the name of the last edited project - - def _open_library(self): - """Create a library panel and add it to the navigation.""" - # Create a library panel library_panel = ScrptLibraryView(self.manuscripts_folder) # Add it to the navigation self.navigation.push(library_panel) + @Gtk.Template.Callback() + def on_close_request(self, event): + logger.info("Window close requested") + def close_editor(self, editor_view): self.navigation.pop() @@ -167,3 +160,15 @@ def inform(self, message: str): toast = Adw.Toast.new(title=message) toast.set_timeout(3) self.toast_overlay.add_toast(toast) + + def on_manuscripts_folder_changed(self, _src, _value): + """Called when the manuscripts storage location is changed.""" + self.inform("Data folder changed") + + # Create a library panel + library_panel = ScrptLibraryView(self.manuscripts_folder) + + # Replace all the other panels by that one + self.navigation.replace(pages=[library_panel]) + +