From 45fcb129e3fc32ba46cab000aed0fb7734f0a0d5 Mon Sep 17 00:00:00 2001 From: Dylan Sandall Date: Mon, 22 Jun 2026 21:00:36 -0700 Subject: [PATCH] Add git-native terminology toggle Adds an opt-in 'Use git terminology' preference that relabels the UI with the underlying git terms (Iteration->Commit, Project->Repository, Reviewed->Staged, Current Files->Working Tree, etc.). - SettingsRepository owns the toggle: git_terminology_enabled() / set_git_terminology_enabled() on the domain port and FreeCADSettingsRepository, reusing the repo's existing cache. - utils.term(cad, git): selects the CAD-friendly or git phrase per the toggle. Both args are translate()/QT_TRANSLATE_NOOP literals. - Display sites pass both variants through term() at the call site. - Command labels are built at Initialize (before the container exists), so they read the pref via settings_repo.read_git_terminology_enabled(); everything else uses the container-cached repo. - preferences page: 'Use git terminology' checkbox under a Terminology group. - tests: settings-repo toggle + cache, term() selection, preferences wiring. Design notes: - No runtime regex remap. The earlier approach ran ~20 regex substitutions on every translated string; term() does a single boolean check and returns one of two prepared strings, so untoggled paths cost nothing and toggled paths do no scanning. - No inline single-word substitution (f-string or %1 placeholder). Each terminology variant is written as a full translate()/QT_TRANSLATE_NOOP literal at its display site, for two reasons: Qt's lupdate can only extract static literals, so both variants stay translatable; and sentences are never fragmented around an inserted word, which would break grammar/agreement in inflected locales. The near-duplicate CAD/git phrases are the deliberate cost of keeping every user-facing string translatable at the point it is shown. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AY7KZvGbYkCem92P3oVkQd --- .../src/content/docs/user-guide/concepts.md | 2 + .../application/actions/result_models.py | 1 + .../history_wb/domain/settings/repository.py | 13 + freecad/history_wb/entrypoints/commands.py | 122 +++- freecad/history_wb/entrypoints/workbench.py | 11 +- .../infrastructure/freecad/settings_repo.py | 34 +- .../resources/translations/History.ts | 549 ++++++++++++++---- .../author_configuration_handler.py | 26 +- .../commit_iteration_handler.py | 16 +- .../initialize_repository_handler.py | 26 +- .../ui/presenters/presentation_models.py | 45 +- .../presenters/workbench_command_presenter.py | 23 +- freecad/history_wb/ui/registry.py | 8 +- .../history_wb/ui/views/diff_panel/dialogs.py | 80 ++- .../ui/views/document_diff/document_row.py | 43 +- .../ui/views/document_diff/node_row.py | 8 +- .../ui/views/document_diff/summary_bar.py | 22 +- .../ui/views/history/history_list.py | 46 +- freecad/history_wb/ui/views/history/panel.py | 16 +- .../ui/views/history/repository_header.py | 17 +- .../ui/views/settings_preferences_page.py | 23 +- freecad/history_wb/utils.py | 44 +- .../test_open_all_documents_command.py | 1 + .../freecad/test_settings_repo.py | 26 + tests/unit/test_utils.py | 18 +- .../views/test_settings_preferences_page.py | 11 + 26 files changed, 973 insertions(+), 258 deletions(-) diff --git a/docs-static-site/src/content/docs/user-guide/concepts.md b/docs-static-site/src/content/docs/user-guide/concepts.md index 9d9da45..f22b953 100644 --- a/docs-static-site/src/content/docs/user-guide/concepts.md +++ b/docs-static-site/src/content/docs/user-guide/concepts.md @@ -9,6 +9,8 @@ Understanding the concepts laid out here will help you understand how to use the Note: if you are already knowledgeable in git, just review git term next to each heading to familiarize yourself with the terminology mapping, and you'll be ready to rock and roll. +Prefer the git terms everywhere? Enable **Edit → Preferences → History → Terminology → "Use git terminology"** to relabel the interface (Iteration → Commit, Project → Repository, Reviewed → Staged, and so on). Restart FreeCAD for the change to apply to every label. + ## FreeCAD Terminology FreeCAD stores files in the `.FCStd` format, and represents a single **Document**. From the workbench's perspective, a FreeCAD "Document" and "file" mean the same thing. diff --git a/freecad/history_wb/application/actions/result_models.py b/freecad/history_wb/application/actions/result_models.py index a7bc6d8..72b631b 100644 --- a/freecad/history_wb/application/actions/result_models.py +++ b/freecad/history_wb/application/actions/result_models.py @@ -70,6 +70,7 @@ def has_any(self) -> bool: """Return True when any issue exists on either side or general bucket.""" return self.old_snapshot is not None or self.new_snapshot is not None or bool(self.general) + @dataclass(frozen=True) class DocumentDiffResult: """Application-level diff result for one FCStd document.""" diff --git a/freecad/history_wb/domain/settings/repository.py b/freecad/history_wb/domain/settings/repository.py index a86b8e0..96321d7 100644 --- a/freecad/history_wb/domain/settings/repository.py +++ b/freecad/history_wb/domain/settings/repository.py @@ -59,6 +59,19 @@ def get_settings(self) -> Settings: """ ... + def git_terminology_enabled(self) -> bool: + """Return whether the git-native terminology display toggle is on. + + Controls whether the UI relabels CAD-friendly vocabulary (Iteration, + Project, Reviewed, ...) with the underlying git terms (Commit, + Repository, Staged, ...). Independent of diff computation. + """ + ... + + def set_git_terminology_enabled(self, enabled: bool) -> None: + """Persist the git-native terminology display toggle.""" + ... + class SettingsPersistenceRepository(Protocol): """Interface for raw diff settings persistence state access.""" diff --git a/freecad/history_wb/entrypoints/commands.py b/freecad/history_wb/entrypoints/commands.py index 1df8bd2..3471253 100644 --- a/freecad/history_wb/entrypoints/commands.py +++ b/freecad/history_wb/entrypoints/commands.py @@ -10,7 +10,7 @@ from ..qt import QtCore from ..resources import ICONPATH -from ..utils import Log, translate +from ..utils import Log, term, translate if TYPE_CHECKING: @@ -92,8 +92,14 @@ class _CommitCommand: def GetResources(self) -> CommandResources: """Return FreeCAD command metadata for UI integration.""" return { - "MenuText": QtCore.QT_TRANSLATE_NOOP("HistoryCommit", "Save Iteration"), - "ToolTip": QtCore.QT_TRANSLATE_NOOP("HistoryCommit", "Save reviewed changes as an iteration"), + "MenuText": term( + QtCore.QT_TRANSLATE_NOOP("HistoryCommit", "Save Iteration"), + QtCore.QT_TRANSLATE_NOOP("HistoryCommit", "Commit"), + ), + "ToolTip": term( + QtCore.QT_TRANSLATE_NOOP("HistoryCommit", "Save reviewed changes as an iteration"), + QtCore.QT_TRANSLATE_NOOP("HistoryCommit", "Commit the staged changes"), + ), "Pixmap": os.path.join(ICONPATH, "Commit.svg"), } @@ -117,14 +123,27 @@ class _RefreshRepositoryCommand: def GetResources(self) -> CommandResources: """Return FreeCAD command metadata for UI integration.""" return { - "MenuText": QtCore.QT_TRANSLATE_NOOP("HistoryRefreshRepository", "Refresh Project"), - "ToolTip": QtCore.QT_TRANSLATE_NOOP( - "HistoryRefreshRepository", - "Refresh the detected project and reload iterations.\n" - "Open at least one FreeCAD document " - "located within a project before running this command.\n" - "How it works: open FreeCAD " - "documents are checked one by one until one is found to be located within a project.", + "MenuText": term( + QtCore.QT_TRANSLATE_NOOP("HistoryRefreshRepository", "Refresh Project"), + QtCore.QT_TRANSLATE_NOOP("HistoryRefreshRepository", "Refresh Repository"), + ), + "ToolTip": term( + QtCore.QT_TRANSLATE_NOOP( + "HistoryRefreshRepository", + "Refresh the detected project and reload iterations.\n" + "Open at least one FreeCAD document " + "located within a project before running this command.\n" + "How it works: open FreeCAD " + "documents are checked one by one until one is found to be located within a project.", + ), + QtCore.QT_TRANSLATE_NOOP( + "HistoryRefreshRepository", + "Refresh the detected repository and reload commits.\n" + "Open at least one FreeCAD document " + "located within a repository before running this command.\n" + "How it works: open FreeCAD " + "documents are checked one by one until one is found to be located within a repository.", + ), ), "Pixmap": os.path.join(ICONPATH, "RefreshRepository.svg"), } @@ -146,10 +165,19 @@ class _InitializeGitRepositoryCommand: def GetResources(self) -> CommandResources: """Return FreeCAD command metadata for UI integration.""" return { - "MenuText": QtCore.QT_TRANSLATE_NOOP("HistoryInitializeGitRepository", "Initialize Project"), - "ToolTip": QtCore.QT_TRANSLATE_NOOP( - "HistoryInitializeGitRepository", - "Initialize a new project in the selected directory", + "MenuText": term( + QtCore.QT_TRANSLATE_NOOP("HistoryInitializeGitRepository", "Initialize Project"), + QtCore.QT_TRANSLATE_NOOP("HistoryInitializeGitRepository", "Initialize Repository"), + ), + "ToolTip": term( + QtCore.QT_TRANSLATE_NOOP( + "HistoryInitializeGitRepository", + "Initialize a new project in the selected directory", + ), + QtCore.QT_TRANSLATE_NOOP( + "HistoryInitializeGitRepository", + "Initialize a new repository in the selected directory", + ), ), "Pixmap": os.path.join(ICONPATH, "CreateGitRepository.svg"), } @@ -173,13 +201,25 @@ class _OpenAllDocumentsInRepositoryCommand: def GetResources(self) -> CommandResources: """Return FreeCAD command metadata for UI integration.""" return { - "MenuText": QtCore.QT_TRANSLATE_NOOP( - "HistoryOpenAllDocumentsInRepository", - "Open All Documents in Project", + "MenuText": term( + QtCore.QT_TRANSLATE_NOOP( + "HistoryOpenAllDocumentsInRepository", + "Open All Documents in Project", + ), + QtCore.QT_TRANSLATE_NOOP( + "HistoryOpenAllDocumentsInRepository", + "Open All Documents in Repository", + ), ), - "ToolTip": QtCore.QT_TRANSLATE_NOOP( - "HistoryOpenAllDocumentsInRepository", - "Open every .FCStd file found in the project. Useful for generating en masse.", + "ToolTip": term( + QtCore.QT_TRANSLATE_NOOP( + "HistoryOpenAllDocumentsInRepository", + "Open every .FCStd file found in the project. Useful for generating en masse.", + ), + QtCore.QT_TRANSLATE_NOOP( + "HistoryOpenAllDocumentsInRepository", + "Open every .FCStd file found in the repository. Useful for generating en masse.", + ), ), "Pixmap": os.path.join(ICONPATH, "OpenAllDocuments.svg"), } @@ -202,8 +242,14 @@ def Activated(self) -> None: repo = ui_registry.application_state.git_repository if repo is None: dialog_view.show_warning_message( - translate("History", "No Project"), - translate("History", "No project detected. Open a FreeCAD document in a project first."), + term( + translate("History", "No Project"), + translate("History", "No Repository"), + ), + term( + translate("History", "No project detected. Open a FreeCAD document in a project first."), + translate("History", "No repository detected. Open a FreeCAD document in a repository first."), + ), ) return @@ -216,10 +262,19 @@ class _UpdateGitIgnoreCommand: def GetResources(self) -> CommandResources: """Return FreeCAD command metadata for UI integration.""" return { - "MenuText": QtCore.QT_TRANSLATE_NOOP("HistoryUpdateGitIgnore", "Edit Ignored Files"), - "ToolTip": QtCore.QT_TRANSLATE_NOOP( - "HistoryUpdateGitIgnore", - "Edit project ignored files list (.gitignore)", + "MenuText": term( + QtCore.QT_TRANSLATE_NOOP("HistoryUpdateGitIgnore", "Edit Ignored Files"), + QtCore.QT_TRANSLATE_NOOP("HistoryUpdateGitIgnore", "Edit .gitignore"), + ), + "ToolTip": term( + QtCore.QT_TRANSLATE_NOOP( + "HistoryUpdateGitIgnore", + "Edit project ignored files list (.gitignore)", + ), + QtCore.QT_TRANSLATE_NOOP( + "HistoryUpdateGitIgnore", + "Edit repository ignored files list (.gitignore)", + ), ), "Pixmap": os.path.join(ICONPATH, "GitIgnore.svg"), } @@ -314,7 +369,10 @@ class _CloseDiffWindowsCommand: def GetResources(self) -> CommandResources: """Return FreeCAD command metadata for UI integration.""" return { - "MenuText": QtCore.QT_TRANSLATE_NOOP("HistoryCloseDiffWindows", "Close Comparison Windows"), + "MenuText": term( + QtCore.QT_TRANSLATE_NOOP("HistoryCloseDiffWindows", "Close Comparison Windows"), + QtCore.QT_TRANSLATE_NOOP("HistoryCloseDiffWindows", "Close Diff Windows"), + ), "ToolTip": QtCore.QT_TRANSLATE_NOOP( "HistoryCloseDiffWindows", "Close every document starting with 'Diff_' without saving", @@ -339,7 +397,13 @@ def Activated(self) -> None: def register_commands() -> None: - """Register the Diff Workbench commands with FreeCAD.""" + """Register the Diff Workbench commands with FreeCAD. + + Command labels resolve the git-terminology toggle inside GetResources via + term(), so no wrapper is needed. FreeCAD queries GetResources at Initialize + (before the container exists); term() reads the toggle directly from + preferences in that case. + """ import FreeCADGui as Gui # pylint: disable=import-error Gui.addCommand("HistoryConfigureAuthorCommand", _ConfigureAuthorCommand()) diff --git a/freecad/history_wb/entrypoints/workbench.py b/freecad/history_wb/entrypoints/workbench.py index 1d3f0d8..bc470d1 100644 --- a/freecad/history_wb/entrypoints/workbench.py +++ b/freecad/history_wb/entrypoints/workbench.py @@ -14,7 +14,7 @@ from ..qt import QtCore, QtGui, QtWidgets from ..resources import ICONPATH -from ..utils import Log, set_logger, translate +from ..utils import Log, set_logger, term, translate _PREFERENCES_REGISTRY_ATTR = "_history_wb_preference_pages" @@ -57,7 +57,13 @@ class HistoryWorkbench(Gui.Workbench): def __init__(self): super().__init__() self.MenuText = cast(str, QtCore.QT_TRANSLATE_NOOP("Workbench", "History")) - self.ToolTip = cast(str, QtCore.QT_TRANSLATE_NOOP("Workbench", "Track project iterations and history")) + self.ToolTip = cast( + str, + term( + QtCore.QT_TRANSLATE_NOOP("Workbench", "Track project iterations and history"), + QtCore.QT_TRANSLATE_NOOP("Workbench", "Track repository commits and history"), + ), + ) self._subwindow = None # Store reference to MDI subwindow def GetClassName(self) -> str: @@ -232,6 +238,7 @@ def _on_subwindow_closed(self) -> None: # Clear panel-scoped presenters; application state survives for command access from ..ui.registry import ui_registry + ui_registry.clear_presenters() def _focus_diff_panel_deferred(self) -> None: diff --git a/freecad/history_wb/infrastructure/freecad/settings_repo.py b/freecad/history_wb/infrastructure/freecad/settings_repo.py index 955a690..ba3581d 100644 --- a/freecad/history_wb/infrastructure/freecad/settings_repo.py +++ b/freecad/history_wb/infrastructure/freecad/settings_repo.py @@ -41,6 +41,25 @@ def SetInt(self, key: str, value: int) -> None: ... MIN_FLOAT_PRECISION = 0 MAX_FLOAT_PRECISION = 12 +# Single source of truth for where the History workbench preferences live. +PARAM_GROUP_PATH = "User parameter:BaseApp/Preferences/Mod/History" +KEY_GIT_TERMINOLOGY = "GitTerminology" + + +def read_git_terminology_enabled() -> bool: + """Read the git-terminology toggle directly from FreeCAD preferences. + + Used during command registration, which happens at workbench Initialize + before the application container (and its cached settings repository) + exists. Returns False when FreeCAD is unavailable (e.g. unit tests). + """ + try: + import FreeCAD as App # pylint: disable=import-error + + return bool(App.ParamGet(PARAM_GROUP_PATH).GetBool(KEY_GIT_TERMINOLOGY, False)) + except (ImportError, AttributeError): + return False + class FreeCADSettingsRepository: """Settings repository implementation using FreeCAD's Parameter system. @@ -67,8 +86,9 @@ class FreeCADSettingsRepository: def __init__(self, ctx: FreeCadContext) -> None: self._ctx = ctx - self._group_path = "User parameter:BaseApp/Preferences/Mod/History" + self._group_path = PARAM_GROUP_PATH self._cached_settings: Settings | None = None + self._cached_git_terminology: bool | None = None def _get_group(self) -> _ParamGroup: return cast(_ParamGroup, self._ctx.app.ParamGet(self._group_path)) @@ -232,3 +252,15 @@ def get_settings(self) -> Settings: self._cached_settings = self._build_settings_from_state(self.get_persistence_state()) return self._cached_settings + + def git_terminology_enabled(self) -> bool: + """Return the git-terminology display toggle, caching the lookup.""" + if self._cached_git_terminology is None: + self._cached_git_terminology = self._get_group().GetBool(KEY_GIT_TERMINOLOGY, False) + + return self._cached_git_terminology + + def set_git_terminology_enabled(self, enabled: bool) -> None: + """Persist the git-terminology display toggle and refresh the cache.""" + self._get_group().SetBool(KEY_GIT_TERMINOLOGY, enabled) + self._cached_git_terminology = enabled diff --git a/freecad/history_wb/resources/translations/History.ts b/freecad/history_wb/resources/translations/History.ts index 899ed1e..82329e6 100644 --- a/freecad/history_wb/resources/translations/History.ts +++ b/freecad/history_wb/resources/translations/History.ts @@ -4,67 +4,110 @@ History - + - - + + No Project + + + + + + No Repository + + + + + + + + No repository detected. Open a FreeCAD document in a repository first. + + - - + + Save Iteration Failed - + + + + Commit Failed + + + + Name and email are required to save iteration - + + Name and email are required to commit + + + + Git identity could not be saved - + Could not save git identity for all projects. Uncheck the global option to save it only for this project. - + + Could not save git identity for all repositories. Uncheck the global option to save it only for this repository. + + + + Configure Author - + Enter the name and email you'd like to use for your git identity, which is used for authoring project iterations. - - Configure globally for all projects + + Enter the name and email you'd like to use for your git identity, which is used for authoring repository commits. - Name: + Configure globally for all projects + Configure globally for all repositories + + + + + Name: + + + + Email: - + Global configuration option disabled because global config file not writable. - + This operation will overwrite the current file(s) on disk with the selected saved copies. All open FreeCAD documents will be closed and reopened to ensure links are updated. @@ -75,49 +118,64 @@ Saved history will not be affected. - + Restore All - + Which files would you like to restore? - + Restore only the FreeCAD files changed in the selected iteration. Other files on disk are left unchanged. - + + Restore only the FreeCAD files changed in the selected commit. Other files on disk are left unchanged. + + + + Restore all saved FreeCAD files to their state in this history entry. Saved FreeCAD files that did not exist in this entry will be removed. Files that have not been saved to history will be kept. - + + Edit .gitignore + + + + Listed FreeCAD files - + + Enter commit notes (subject and optional body)... + + + + All FreeCAD files - - + + OK - - - - - - + + + + + + Cancel @@ -127,38 +185,64 @@ Saved history will not be affected. - + + No Staged Files + + + + There are no reviewed files to save. - + + There are no staged files to commit. + + + + + Commit notes cannot be empty + + + + Git commit failed - + Empty Notes - + Iteration notes cannot be empty - + Save Iteration - + + + Commit + + + + Enter iteration notes: - + + Enter commit notes: + + + + Enter iteration notes (subject and optional body)... @@ -168,62 +252,97 @@ Saved history will not be affected. - + No open documents are available for project initialization. Please open at least one saved document in the root location you'd like to initialize a new project. - + + No open documents are available for repository initialization. Please open at least one saved document in the root location you'd like to initialize a new repository. + + + + Initialization Failed + + + Repository Initialized + + - + Unknown error occurred - + Initialized project: %1 - + + Initialized repository: %1 + + + + Project Initialized - + Initialize Project - + + Initialize Repository + + + + Choose a directory to initialize based on currently open documents. The selected directory will be the root of your project: - + + Choose a directory to initialize based on currently open documents. The selected directory will be the root of your repository: + + + + Already inside project - + + Already inside repository + + + + All listed directories are already inside projects. - + + All listed directories are already inside repositories. + + + + Initialize - - - - + + + + No project detected. Open a FreeCAD document in a project first. @@ -233,7 +352,7 @@ Saved history will not be affected. - + Edit Ignored Files @@ -243,12 +362,12 @@ Saved history will not be affected. - + Save - + Update the ignored files list. Lines starting with a "#" are considered comments. Click <a href="%1">here</a> to learn about the full syntax. @@ -263,82 +382,137 @@ Saved history will not be affected. - + Cannot find previous snapshot. Tree comparison cannot be generated. - + + Cannot find previous snapshot. Tree diff cannot be generated. + + + + No snapshot available for this document. Tree comparison cannot be generated. - + + No snapshot available for this document. Tree diff cannot be generated. + + + + Click to open the document and generate a comparison. + Click to open the document and generate a diff. + + + + The old snapshot is invalid, so a tree comparison cannot be generated. - + + The old snapshot is invalid, so a tree diff cannot be generated. + + + + The selected snapshot is invalid, so a tree comparison cannot be generated. - + + The selected snapshot is invalid, so a tree diff cannot be generated. + + + + File changed on disk but no parametric changes detected. - + Diff computation failed - + Mark All Files Reviewed - - Remove document(s) from Reviewed. The current file(s) stay unchanged and will not be saved in the next iteration until reviewed again. + + Mark All Files Staged + Remove document(s) from Reviewed. The current file(s) stay unchanged and will not be saved in the next iteration until reviewed again. + + + + + Remove document(s) from Staged. The current file(s) stay unchanged and will not be saved in the next commit until staged again. + + + + Remove All Files From Reviewed - + + Remove All Files From Staged + + + + Restore All Reviewed Files - + + Restore All Staged Files + + + + Restore All Files From Iteration - + + Restore All Files From Commit + + + + Copy Iteration ID to Clipboard - + + Copy Commit ID to Clipboard + + + + Modified file count - + Added file count - + Deleted file count @@ -348,19 +522,31 @@ Saved history will not be affected. - + + + Mark All Staged + + + + Choose which files to restore from the selected iteration. Current files on disk can be overwritten or removed. Saved history will not be affected. - + + Choose which files to restore from the selected commit. +Current files on disk can be overwritten or removed. +Saved history will not be affected. + + + + Remove All - + Remove document(s) from Reviewed. The current file(s) stay unchanged. They will not be saved in the next iteration until reviewed again. @@ -378,26 +564,46 @@ They will not be saved in the next iteration until reviewed again. - + Unnamed Document - + + Remove document(s) from Staged. +The current file(s) stay unchanged. +They will not be saved in the next commit until staged again. + + + + + Reviewed - + + + Staged + + + + Remove - + Restore the selected file. This overwrites %1 on disk with a copy of the file as it was saved in the selected iteration. THE CURRENT FILE WILL BE OVERWRITTEN BY THIS OPERATION. +Saved history will not be affected. + + + + + Restore the selected file. +This overwrites %1 on disk with a copy of the file as it was saved in the selected commit. +THE CURRENT FILE WILL BE OVERWRITTEN BY THIS OPERATION. Saved history will not be affected. @@ -412,45 +618,85 @@ Saved history will not be affected. - + + Open 3D diff + + + + Iterations - + Refresh Project and Iterations - + + No commits to display. + + + + + Commits + + + + Current Files Area + Working Tree + + + + Reviewed Area - + + Staging Area + + + + No iterations to display. - + Yesterday - + No project detected - + + No repository detected + + + + Project: %1 + + + Repository: %1 + + + + + Refresh Repository and Commits + + Property @@ -467,7 +713,7 @@ Saved history will not be affected. - + Properties @@ -477,7 +723,7 @@ Saved history will not be affected. - + Settings apply only during tree comparisons. Saved tree snapshots are unaffected by these settings. @@ -522,27 +768,42 @@ Saved history will not be affected. - + + Use git terminology (Commit, Repository, Staged, …) + + + + + Relabel the interface with the underlying git terms instead of the CAD-friendly defaults. Takes full effect after restarting FreeCAD. + + + + + Terminology + + + + Use default exclusion list - + Use custom exclusion list - + History - - - - + + + + Restore @@ -555,12 +816,17 @@ Saved history will not be affected. HistoryCloseDiffWindows - + Close Comparison Windows - + + Close Diff Windows + + + + Close every document starting with 'Diff_' without saving @@ -568,15 +834,25 @@ Saved history will not be affected. HistoryCommit - + Save Iteration - + + Commit + + + + Save reviewed changes as an iteration + + + Commit the staged changes + + HistoryConfigureAuthorCommand @@ -594,38 +870,58 @@ Saved history will not be affected. HistoryInitializeGitRepository - + Initialize Project - + + Initialize Repository + + + + Initialize a new project in the selected directory + + + Initialize a new repository in the selected directory + + HistoryOpenAllDocumentsInRepository - + Open All Documents in Project - + + Open All Documents in Repository + + + + Open every .FCStd file found in the project. Useful for generating en masse. + + + Open every .FCStd file found in the repository. Useful for generating en masse. + + HistoryOpenDiffWindow - + Open History Panel - + Open history panel view @@ -633,12 +929,12 @@ Saved history will not be affected. HistoryRecomputeActiveDocument - + Recompute Active Document - + Recompute the active document @@ -646,12 +942,12 @@ Saved history will not be affected. HistoryRecomputeAllOpenDocuments - + Recompute All - + Recompute every open document @@ -659,45 +955,72 @@ Saved history will not be affected. HistoryRefreshRepository - + Refresh Project - + + Refresh Repository + + + + Refresh the detected project and reload iterations. Open at least one FreeCAD document located within a project before running this command. How it works: open FreeCAD documents are checked one by one until one is found to be located within a project. + + + Refresh the detected repository and reload commits. +Open at least one FreeCAD document located within a repository before running this command. +How it works: open FreeCAD documents are checked one by one until one is found to be located within a repository. + + HistoryUpdateGitIgnore - + Edit Ignored Files - + + Edit .gitignore + + + + Edit project ignored files list (.gitignore) + + + Edit repository ignored files list (.gitignore) + + Workbench - - - + + + History - + Track project iterations and history + + + Track repository commits and history + + diff --git a/freecad/history_wb/ui/presenters/git_repository/author_configuration_handler.py b/freecad/history_wb/ui/presenters/git_repository/author_configuration_handler.py index 93ee456..c9d2e97 100644 --- a/freecad/history_wb/ui/presenters/git_repository/author_configuration_handler.py +++ b/freecad/history_wb/ui/presenters/git_repository/author_configuration_handler.py @@ -14,7 +14,7 @@ from ....application.actions.git_config.get_git_identity import GetGitIdentityAction from ....application.actions.git_config.save_git_identity import SaveGitIdentityAction from ....domain.git.models import GitIdentity, GitRepository -from ....utils import translate +from ....utils import term, translate from ...views.diff_panel.dialogs import GitConfigDialogResult @@ -65,8 +65,11 @@ def execute(self, repo: GitRepository) -> bool: if not dialog_result.author_name or not dialog_result.author_email: self._show_warning_message( - translate("History", "Save Iteration Failed"), - translate("History", "Name and email are required to save iteration"), + term(translate("History", "Save Iteration Failed"), translate("History", "Commit Failed")), + term( + translate("History", "Name and email are required to save iteration"), + translate("History", "Name and email are required to commit"), + ), ) return False @@ -81,16 +84,23 @@ def execute(self, repo: GitRepository) -> bool: # Local save failed; give up if not dialog_result.should_save_globally: self._show_error_message( - translate("History", "Save Iteration Failed"), + term(translate("History", "Save Iteration Failed"), translate("History", "Commit Failed")), translate("History", "Git identity could not be saved"), ) return False # Global save failed; retry with local-only option - retry_message = translate( - "History", - "Could not save git identity for all projects. " - "Uncheck the global option to save it only for this project.", + retry_message = term( + translate( + "History", + "Could not save git identity for all projects. " + "Uncheck the global option to save it only for this project.", + ), + translate( + "History", + "Could not save git identity for all repositories. " + "Uncheck the global option to save it only for this repository.", + ), ) initial_values = dialog_result diff --git a/freecad/history_wb/ui/presenters/git_repository/commit_iteration_handler.py b/freecad/history_wb/ui/presenters/git_repository/commit_iteration_handler.py index c1a10fd..db0be0b 100644 --- a/freecad/history_wb/ui/presenters/git_repository/commit_iteration_handler.py +++ b/freecad/history_wb/ui/presenters/git_repository/commit_iteration_handler.py @@ -13,7 +13,7 @@ from ....application.actions.git_history.get_staged_file_paths import GetStagedFilePathsAction from ....application.actions.git_workflow.commit_staging import CommitStagingAction from ....domain.git.models import GitRepository -from ....utils import translate +from ....utils import term, translate from .author_configuration_handler import AuthorConfigurationHandler @@ -56,8 +56,11 @@ def execute(self, repo: GitRepository) -> bool: staged_result = self._get_staged_file_paths_action.execute(repo) if not staged_result.is_success or not staged_result.data: self._show_info_message( - translate("History", "No Reviewed Files"), - translate("History", "There are no reviewed files to save."), + term(translate("History", "No Reviewed Files"), translate("History", "No Staged Files")), + term( + translate("History", "There are no reviewed files to save."), + translate("History", "There are no staged files to commit."), + ), ) return False @@ -76,7 +79,10 @@ def execute(self, repo: GitRepository) -> bool: if not trimmed_message: self._show_warning_message( translate("History", "Empty Notes"), - translate("History", "Iteration notes cannot be empty"), + term( + translate("History", "Iteration notes cannot be empty"), + translate("History", "Commit notes cannot be empty"), + ), ) return False @@ -85,7 +91,7 @@ def execute(self, repo: GitRepository) -> bool: return True self._show_error_message( - translate("History", "Save Iteration Failed"), + term(translate("History", "Save Iteration Failed"), translate("History", "Commit Failed")), result.message or translate("History", "Git commit failed"), ) return False diff --git a/freecad/history_wb/ui/presenters/git_repository/initialize_repository_handler.py b/freecad/history_wb/ui/presenters/git_repository/initialize_repository_handler.py index 72aaaf8..4d1cc4b 100644 --- a/freecad/history_wb/ui/presenters/git_repository/initialize_repository_handler.py +++ b/freecad/history_wb/ui/presenters/git_repository/initialize_repository_handler.py @@ -7,7 +7,7 @@ GetGitRepositoryInitCandidatesAction, ) from ....application.actions.git_repo.initialize_git_repository import InitializeGitRepositoryAction -from ....utils import Log, translate +from ....utils import Log, term, translate from ...state import ApplicationState @@ -37,11 +37,19 @@ def execute(self) -> bool: if not candidates_result.is_success: self._show_info_message( translate("History", "No Directories Available"), - translate( - "History", - "No open documents are available for project initialization. " - "Please open at least one saved document in the root location " - "you'd like to initialize a new project.", + term( + translate( + "History", + "No open documents are available for project initialization. " + "Please open at least one saved document in the root location " + "you'd like to initialize a new project.", + ), + translate( + "History", + "No open documents are available for repository initialization. " + "Please open at least one saved document in the root location " + "you'd like to initialize a new repository.", + ), ), ) return False @@ -61,10 +69,12 @@ def execute(self) -> bool: repository = init_result.data self._application_state.git_repository = repository - success_template = translate("History", "Initialized project: %1") + success_template = term( + translate("History", "Initialized project: %1"), translate("History", "Initialized repository: %1") + ) success_message = success_template.replace("%1", repository.absolute_path) self._show_info_message( - translate("History", "Project Initialized"), + term(translate("History", "Project Initialized"), translate("History", "Repository Initialized")), success_message, ) Log.info(f"Initialized git repository at {repository.absolute_path}") diff --git a/freecad/history_wb/ui/presenters/presentation_models.py b/freecad/history_wb/ui/presenters/presentation_models.py index afaa97a..ea78994 100644 --- a/freecad/history_wb/ui/presenters/presentation_models.py +++ b/freecad/history_wb/ui/presenters/presentation_models.py @@ -8,6 +8,7 @@ from ...domain.diff.models import DiffState from ...qt import QtCore, QtGui from ...resources import get_icon_path +from ...utils import term __all__ = [ @@ -42,9 +43,15 @@ def __init__(self) -> None: super().__init__( tooltip=cast( str, - QtCore.QT_TRANSLATE_NOOP( - "History", - "Cannot find previous snapshot. Tree comparison cannot be generated.", + term( + QtCore.QT_TRANSLATE_NOOP( + "History", + "Cannot find previous snapshot. Tree comparison cannot be generated.", + ), + QtCore.QT_TRANSLATE_NOOP( + "History", + "Cannot find previous snapshot. Tree diff cannot be generated.", + ), ), ), icon=QtGui.QIcon(str(get_icon_path("DocumentStatusOldSnapshotMissing.svg"))), @@ -59,8 +66,13 @@ def __init__(self) -> None: super().__init__( tooltip=cast( str, - QtCore.QT_TRANSLATE_NOOP( - "History", "No snapshot available for this document. Tree comparison cannot be generated." + term( + QtCore.QT_TRANSLATE_NOOP( + "History", "No snapshot available for this document. Tree comparison cannot be generated." + ), + QtCore.QT_TRANSLATE_NOOP( + "History", "No snapshot available for this document. Tree diff cannot be generated." + ), ), ), icon=QtGui.QIcon(str(get_icon_path("DocumentStatusSnapshotMissing.svg"))), @@ -75,7 +87,10 @@ def __init__(self) -> None: super().__init__( tooltip=cast( str, - QtCore.QT_TRANSLATE_NOOP("History", "Click to open the document and generate a comparison."), + term( + QtCore.QT_TRANSLATE_NOOP("History", "Click to open the document and generate a comparison."), + QtCore.QT_TRANSLATE_NOOP("History", "Click to open the document and generate a diff."), + ), ), icon=QtGui.QIcon(str(get_icon_path("OpenDocument.svg"))), ) @@ -89,8 +104,13 @@ def __init__(self) -> None: super().__init__( tooltip=cast( str, - QtCore.QT_TRANSLATE_NOOP( - "History", "The old snapshot is invalid, so a tree comparison cannot be generated." + term( + QtCore.QT_TRANSLATE_NOOP( + "History", "The old snapshot is invalid, so a tree comparison cannot be generated." + ), + QtCore.QT_TRANSLATE_NOOP( + "History", "The old snapshot is invalid, so a tree diff cannot be generated." + ), ), ), icon=QtGui.QIcon(str(get_icon_path("DocumentStatusInvalidSnapshot.svg"))), @@ -105,8 +125,13 @@ def __init__(self) -> None: super().__init__( tooltip=cast( str, - QtCore.QT_TRANSLATE_NOOP( - "History", "The selected snapshot is invalid, so a tree comparison cannot be generated." + term( + QtCore.QT_TRANSLATE_NOOP( + "History", "The selected snapshot is invalid, so a tree comparison cannot be generated." + ), + QtCore.QT_TRANSLATE_NOOP( + "History", "The selected snapshot is invalid, so a tree diff cannot be generated." + ), ), ), icon=QtGui.QIcon(str(get_icon_path("DocumentStatusInvalidSnapshot.svg"))), diff --git a/freecad/history_wb/ui/presenters/workbench_command_presenter.py b/freecad/history_wb/ui/presenters/workbench_command_presenter.py index 36666a4..55579b7 100644 --- a/freecad/history_wb/ui/presenters/workbench_command_presenter.py +++ b/freecad/history_wb/ui/presenters/workbench_command_presenter.py @@ -28,7 +28,7 @@ from ...application.actions.git_workflow.commit_staging import CommitStagingAction from ...domain.git.models import GitRepository from ...qt import QtCore -from ...utils import Log, translate +from ...utils import Log, term, translate from ..state import ApplicationState from ..views.diff_panel.dialog_view import DialogView from ..views.diff_panel.dialogs import GitConfigDialogResult @@ -131,8 +131,11 @@ def configure_author(self) -> bool: repo = self._application_state.git_repository if repo is None: self._show_warning_message( - translate("History", "No Project"), - translate("History", "No project detected. Open a FreeCAD document in a project first."), + term(translate("History", "No Project"), translate("History", "No Repository")), + term( + translate("History", "No project detected. Open a FreeCAD document in a project first."), + translate("History", "No repository detected. Open a FreeCAD document in a repository first."), + ), ) return False @@ -147,8 +150,11 @@ def save_iteration(self) -> bool: repo = self._application_state.git_repository if repo is None: self._show_warning_message( - translate("History", "No Project"), - translate("History", "No project detected. Open a FreeCAD document in a project first."), + term(translate("History", "No Project"), translate("History", "No Repository")), + term( + translate("History", "No project detected. Open a FreeCAD document in a project first."), + translate("History", "No repository detected. Open a FreeCAD document in a repository first."), + ), ) return False @@ -167,8 +173,11 @@ def update_gitignore(self) -> None: repo = self._application_state.git_repository if repo is None: self._show_warning_message( - translate("History", "No Project"), - translate("History", "No project detected. Open a FreeCAD document in a project first."), + term(translate("History", "No Project"), translate("History", "No Repository")), + term( + translate("History", "No project detected. Open a FreeCAD document in a project first."), + translate("History", "No repository detected. Open a FreeCAD document in a repository first."), + ), ) return diff --git a/freecad/history_wb/ui/registry.py b/freecad/history_wb/ui/registry.py index b1dc608..da66b50 100644 --- a/freecad/history_wb/ui/registry.py +++ b/freecad/history_wb/ui/registry.py @@ -62,18 +62,14 @@ def workbench_command_presenter(self) -> "WorkbenchCommandPresenter": RuntimeError: If not initialized """ if self._workbench_command_presenter is None: - raise RuntimeError( - "Workbench command presenter not initialized. Workbench must be activated first." - ) + raise RuntimeError("Workbench command presenter not initialized. Workbench must be activated first.") return self._workbench_command_presenter def register_application_state(self, state: "ApplicationState") -> None: """Register application state.""" self._application_state = state - def register_workbench_command_presenter( - self, presenter: "WorkbenchCommandPresenter" - ) -> None: + def register_workbench_command_presenter(self, presenter: "WorkbenchCommandPresenter") -> None: """Register workbench command presenter.""" self._workbench_command_presenter = presenter diff --git a/freecad/history_wb/ui/views/diff_panel/dialogs.py b/freecad/history_wb/ui/views/diff_panel/dialogs.py index 40bc787..fb2a9ea 100644 --- a/freecad/history_wb/ui/views/diff_panel/dialogs.py +++ b/freecad/history_wb/ui/views/diff_panel/dialogs.py @@ -6,7 +6,7 @@ from ....domain.git.models import GitRepositoryInitCandidate from ....qt import QtWidgets -from ....utils import translate +from ....utils import term, translate from ..widgets.buttons import make_dialog_button_box @@ -22,15 +22,22 @@ class GitConfigDialogResult: def show_save_iteration_dialog(parent: QtWidgets.QWidget) -> str | None: """Show Save Iteration dialog and return notes when accepted.""" dialog = QtWidgets.QDialog(parent) - dialog.setWindowTitle(translate("History", "Save Iteration")) + dialog.setWindowTitle(term(translate("History", "Save Iteration"), translate("History", "Commit"))) dialog.setSizeGripEnabled(True) layout = QtWidgets.QVBoxLayout(dialog) - label = QtWidgets.QLabel(translate("History", "Enter iteration notes:")) + label = QtWidgets.QLabel( + term(translate("History", "Enter iteration notes:"), translate("History", "Enter commit notes:")) + ) layout.addWidget(label) text_edit = QtWidgets.QPlainTextEdit(dialog) - text_edit.setPlaceholderText(translate("History", "Enter iteration notes (subject and optional body)...")) + text_edit.setPlaceholderText( + term( + translate("History", "Enter iteration notes (subject and optional body)..."), + translate("History", "Enter commit notes (subject and optional body)..."), + ) + ) text_edit.setTabStopDistance(40) text_edit.setMinimumHeight(100) text_edit.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) @@ -65,10 +72,17 @@ def show_configure_author_dialog( layout = QtWidgets.QVBoxLayout(dialog) layout.addWidget( QtWidgets.QLabel( - translate( - "History", - "Enter the name and email you'd like to use for your git identity, " - "which is used for authoring project iterations.", + term( + translate( + "History", + "Enter the name and email you'd like to use for your git identity, " + "which is used for authoring project iterations.", + ), + translate( + "History", + "Enter the name and email you'd like to use for your git identity, " + "which is used for authoring repository commits.", + ), ), dialog, ) @@ -84,7 +98,10 @@ def show_configure_author_dialog( name_edit = QtWidgets.QLineEdit(dialog) email_edit = QtWidgets.QLineEdit(dialog) remember_checkbox = QtWidgets.QCheckBox( - translate("History", "Configure globally for all projects"), + term( + translate("History", "Configure globally for all projects"), + translate("History", "Configure globally for all repositories"), + ), dialog, ) @@ -176,9 +193,17 @@ def show_restore_scope_dialog(parent: QtWidgets.QWidget) -> str | None: layout.addWidget(title_label) listed = QtWidgets.QRadioButton(translate("History", "Listed FreeCAD files")) listed_desc = QtWidgets.QLabel( - translate( - "History", - "Restore only the FreeCAD files changed in the selected iteration. Other files on disk are left unchanged.", + term( + translate( + "History", + "Restore only the FreeCAD files changed in the selected iteration. " + "Other files on disk are left unchanged.", + ), + translate( + "History", + "Restore only the FreeCAD files changed in the selected commit. " + "Other files on disk are left unchanged.", + ), ) ) listed_desc.setWordWrap(True) @@ -245,15 +270,24 @@ def show_init_repository_dialog( ) -> str | None: """Show repository initialization dialog and return selected directory.""" dialog = QtWidgets.QDialog(parent) - dialog.setWindowTitle(translate("History", "Initialize Project")) + dialog.setWindowTitle( + term(translate("History", "Initialize Project"), translate("History", "Initialize Repository")) + ) dialog.setSizeGripEnabled(True) layout = QtWidgets.QVBoxLayout(dialog) layout.addWidget( QtWidgets.QLabel( - translate( - "History", - "Choose a directory to initialize based on currently open documents. " - "The selected directory will be the root of your project:", + term( + translate( + "History", + "Choose a directory to initialize based on currently open documents. " + "The selected directory will be the root of your project:", + ), + translate( + "History", + "Choose a directory to initialize based on currently open documents. " + "The selected directory will be the root of your repository:", + ), ) ) ) @@ -272,7 +306,10 @@ def show_init_repository_dialog( if not candidate.is_available: reason_label = QtWidgets.QLabel( - translate("History", "Already inside project"), + term( + translate("History", "Already inside project"), + translate("History", "Already inside repository"), + ), dialog, ) reason_label.setEnabled(False) @@ -284,7 +321,10 @@ def show_init_repository_dialog( if first_available_button is not None: first_available_button.setChecked(True) else: - no_available_text = translate("History", "All listed directories are already inside projects.") + no_available_text = term( + translate("History", "All listed directories are already inside projects."), + translate("History", "All listed directories are already inside repositories."), + ) layout.addWidget(QtWidgets.QLabel(no_available_text)) button_layout = QtWidgets.QHBoxLayout() @@ -316,7 +356,7 @@ def show_init_repository_dialog( def show_gitignore_editor_dialog(parent: QtWidgets.QWidget, content: str) -> str | None: """Show gitignore editor dialog. Returns edited content on accept, None on cancel.""" dialog = QtWidgets.QDialog(parent) - dialog.setWindowTitle(translate("History", "Edit Ignored Files")) + dialog.setWindowTitle(term(translate("History", "Edit Ignored Files"), translate("History", "Edit .gitignore"))) dialog.setMinimumWidth(680) dialog.setMinimumHeight(460) diff --git a/freecad/history_wb/ui/views/document_diff/document_row.py b/freecad/history_wb/ui/views/document_diff/document_row.py index 21db450..6cad513 100644 --- a/freecad/history_wb/ui/views/document_diff/document_row.py +++ b/freecad/history_wb/ui/views/document_diff/document_row.py @@ -5,7 +5,7 @@ from functools import partial from ....qt import QtCore, QtWidgets -from ....utils import translate +from ....utils import term, translate from ...presenters.presentation_models import DiffTreePresentation from ..history.models import HistorySelection from ..widgets.buttons import make_row_action_button @@ -16,11 +16,19 @@ STAGE_BUTTON_WIDTH = 90 REMOVE_BUTTON_WIDTH = 90 RESTORE_BUTTON_WIDTH = 90 -REMOVE_REVIEWED_TOOLTIP = translate( - "History", - "Remove document(s) from Reviewed.\n" - "The current file(s) stay unchanged.\n" - "They will not be saved in the next iteration until reviewed again.", +REMOVE_REVIEWED_TOOLTIP = term( + translate( + "History", + "Remove document(s) from Reviewed.\n" + "The current file(s) stay unchanged.\n" + "They will not be saved in the next iteration until reviewed again.", + ), + translate( + "History", + "Remove document(s) from Staged.\n" + "The current file(s) stay unchanged.\n" + "They will not be saved in the next commit until staged again.", + ), ) @@ -98,7 +106,7 @@ def _is_commit_selected(self) -> bool: def _add_stage_button(self, layout: QtWidgets.QHBoxLayout) -> None: """Add + Reviewed button for one working-tree document row.""" self._stage_button = make_row_action_button( - text=translate("History", "+ Reviewed"), + text=term(translate("History", "+ Reviewed"), translate("History", "+ Staged")), width=STAGE_BUTTON_WIDTH, on_clicked=partial(self.stage_requested.emit, self._diff.git_path), ) @@ -117,12 +125,21 @@ def _add_remove_from_reviewed_button(self, layout: QtWidgets.QHBoxLayout) -> Non def _add_restore_button(self, layout: QtWidgets.QHBoxLayout) -> None: """Add Restore button for reviewed or commit-backed document rows.""" - tooltip = translate( - "History", - "Restore the selected file.\n" - "This overwrites %1 on disk with a copy of the file as it was saved in the selected iteration.\n" - "THE CURRENT FILE WILL BE OVERWRITTEN BY THIS OPERATION.\n" - "Saved history will not be affected.", + tooltip = term( + translate( + "History", + "Restore the selected file.\n" + "This overwrites %1 on disk with a copy of the file as it was saved in the selected iteration.\n" + "THE CURRENT FILE WILL BE OVERWRITTEN BY THIS OPERATION.\n" + "Saved history will not be affected.", + ), + translate( + "History", + "Restore the selected file.\n" + "This overwrites %1 on disk with a copy of the file as it was saved in the selected commit.\n" + "THE CURRENT FILE WILL BE OVERWRITTEN BY THIS OPERATION.\n" + "Saved history will not be affected.", + ), ).replace("%1", self._top_level_text) restore_button = make_row_action_button( text=translate("History", "Restore"), diff --git a/freecad/history_wb/ui/views/document_diff/node_row.py b/freecad/history_wb/ui/views/document_diff/node_row.py index 86889e5..5d4a0db 100644 --- a/freecad/history_wb/ui/views/document_diff/node_row.py +++ b/freecad/history_wb/ui/views/document_diff/node_row.py @@ -4,7 +4,7 @@ from ....qt import QtCore, QtGui, QtWidgets from ....resources import get_icon_path -from ....utils import translate +from ....utils import term, translate from ..widgets.buttons import make_tool_button from ..widgets.styles import ( DIFF_ROW_CONTAINER_OBJECT_NAME, @@ -54,7 +54,7 @@ def _setup_ui(self) -> None: icon_size = QtCore.QSize(TREE_ITEM_ICON_SIZE, TREE_ITEM_ICON_SIZE) button = make_tool_button( - tooltip=translate("History", "Open 3D comparison"), + tooltip=term(translate("History", "Open 3D comparison"), translate("History", "Open 3D diff")), icon=QtGui.QIcon(str(get_icon_path("VisualDiff.svg"))), width=TREE_ITEM_HEIGHT, height=TREE_ITEM_HEIGHT, @@ -63,7 +63,5 @@ def _setup_ui(self) -> None: icon_size=icon_size, tool_button_style=QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly, ) - button.clicked.connect( - lambda checked=False: self.visual_diff_requested.emit(self._git_path, self._node_path) - ) + button.clicked.connect(lambda checked=False: self.visual_diff_requested.emit(self._git_path, self._node_path)) layout.addWidget(button) diff --git a/freecad/history_wb/ui/views/document_diff/summary_bar.py b/freecad/history_wb/ui/views/document_diff/summary_bar.py index e59f1a1..b79f252 100644 --- a/freecad/history_wb/ui/views/document_diff/summary_bar.py +++ b/freecad/history_wb/ui/views/document_diff/summary_bar.py @@ -4,7 +4,7 @@ from ....qt import QtCore, QtGui, QtWidgets from ....resources import get_icon_path -from ....utils import translate +from ....utils import term, translate from ..widgets.buttons import make_tool_button from ..widgets.styles import TREE_ITEM_HEIGHT from .summary_state import SummaryButtonState, SummaryCounts @@ -53,7 +53,7 @@ def _setup_ui(self) -> None: layout.addLayout(self._summary_layout) self._stage_all_button = make_tool_button( - text=translate("History", "+ Mark All Reviewed"), + text=term(translate("History", "+ Mark All Reviewed"), translate("History", "+ Mark All Staged")), width=STAGE_ALL_BUTTON_WIDTH, height=TREE_ITEM_HEIGHT, ) @@ -64,11 +64,19 @@ def _setup_ui(self) -> None: self._restore_all_button = make_tool_button( text=translate("History", "Restore All"), - tooltip=translate( - "History", - "Choose which files to restore from the selected iteration.\n" - "Current files on disk can be overwritten or removed.\n" - "Saved history will not be affected.", + tooltip=term( + translate( + "History", + "Choose which files to restore from the selected iteration.\n" + "Current files on disk can be overwritten or removed.\n" + "Saved history will not be affected.", + ), + translate( + "History", + "Choose which files to restore from the selected commit.\n" + "Current files on disk can be overwritten or removed.\n" + "Saved history will not be affected.", + ), ), height=TREE_ITEM_HEIGHT, ) diff --git a/freecad/history_wb/ui/views/history/history_list.py b/freecad/history_wb/ui/views/history/history_list.py index 85998b0..1485ce5 100644 --- a/freecad/history_wb/ui/views/history/history_list.py +++ b/freecad/history_wb/ui/views/history/history_list.py @@ -3,7 +3,7 @@ from typing import cast from ....qt import QtCore, QtGui, QtWidgets -from ....utils import translate +from ....utils import term, translate from .models import HistorySelection @@ -138,7 +138,9 @@ def _show_working_tree_context_menu(self, pos: QtCore.QPoint) -> None: """Show Current Files Area bulk-review context action.""" menu = QtWidgets.QMenu(self) menu.setToolTipsVisible(True) - action = menu.addAction(translate("History", "Mark All Files Reviewed")) + action = menu.addAction( + term(translate("History", "Mark All Files Reviewed"), translate("History", "Mark All Files Staged")) + ) selected_action = menu.exec(self.mapToGlobal(pos)) if selected_action == action: @@ -146,15 +148,29 @@ def _show_working_tree_context_menu(self, pos: QtCore.QPoint) -> None: def _show_reviewed_context_menu(self, pos: QtCore.QPoint, selection: HistorySelection) -> None: """Show Reviewed Area context menu actions.""" - tooltip = translate( - "History", - "Remove document(s) from Reviewed. The current file(s) stay unchanged " - "and will not be saved in the next iteration until reviewed again.", + tooltip = term( + translate( + "History", + "Remove document(s) from Reviewed. The current file(s) stay unchanged " + "and will not be saved in the next iteration until reviewed again.", + ), + translate( + "History", + "Remove document(s) from Staged. The current file(s) stay unchanged " + "and will not be saved in the next commit until staged again.", + ), ) menu = QtWidgets.QMenu(self) menu.setToolTipsVisible(True) - action = menu.addAction(translate("History", "Remove All Files From Reviewed")) - restore_action = menu.addAction(translate("History", "Restore All Reviewed Files")) + action = menu.addAction( + term( + translate("History", "Remove All Files From Reviewed"), + translate("History", "Remove All Files From Staged"), + ) + ) + restore_action = menu.addAction( + term(translate("History", "Restore All Reviewed Files"), translate("History", "Restore All Staged Files")) + ) action.setToolTip(tooltip) action.setStatusTip(tooltip) selected_action = menu.exec(self.mapToGlobal(pos)) @@ -168,8 +184,18 @@ def _show_reviewed_context_menu(self, pos: QtCore.QPoint, selection: HistorySele def _show_commit_context_menu(self, pos: QtCore.QPoint, selection: HistorySelection) -> None: """Show commit-row context actions.""" menu = QtWidgets.QMenu(self) - restore_action = menu.addAction(translate("History", "Restore All Files From Iteration")) - copy_id_action = menu.addAction(translate("History", "Copy Iteration ID to Clipboard")) + restore_action = menu.addAction( + term( + translate("History", "Restore All Files From Iteration"), + translate("History", "Restore All Files From Commit"), + ) + ) + copy_id_action = menu.addAction( + term( + translate("History", "Copy Iteration ID to Clipboard"), + translate("History", "Copy Commit ID to Clipboard"), + ) + ) selected_action = menu.exec(self.mapToGlobal(pos)) if selected_action == restore_action: diff --git a/freecad/history_wb/ui/views/history/panel.py b/freecad/history_wb/ui/views/history/panel.py index fde9416..3551bc0 100644 --- a/freecad/history_wb/ui/views/history/panel.py +++ b/freecad/history_wb/ui/views/history/panel.py @@ -1,10 +1,11 @@ """File responsibility: History panel facade composing repository header and history list.""" + from datetime import datetime from ....application.actions.result_models import SnapshotSummary from ....domain.git.models import GitCommit, GitRepository from ....qt import QtCore, QtWidgets -from ....utils import translate +from ....utils import term, translate from .formatters import format_snapshot_timestamp from .history_list import HistoryList from .history_row import create_commit_history_item, create_no_iterations_history_item, create_special_history_item @@ -59,7 +60,10 @@ def show_commits(self, commits: list[GitCommit], show_special_items: bool = True self._add_special_items() if not show_special_items and not commits: - no_iterations_text = translate("History", "No iterations to display.") + no_iterations_text = term( + translate("History", "No iterations to display."), + translate("History", "No commits to display."), + ) item, widget = create_no_iterations_history_item(no_iterations_text) self._add_list_item(item, widget) self._restore_history_selection(previous_selection) @@ -88,7 +92,9 @@ def _setup_ui(self) -> None: self._repository_header = RepositoryHeader(self) self._history_list = HistoryList(self) - history_placeholder = QtWidgets.QLabel(translate("History", "Iterations")) + history_placeholder = QtWidgets.QLabel( + term(translate("History", "Iterations"), translate("History", "Commits")) + ) history_placeholder.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) layout = QtWidgets.QVBoxLayout(self) @@ -135,13 +141,13 @@ def _on_save_iteration_requested(self) -> None: def _add_special_items(self) -> None: """Insert Current Files Area and Reviewed Area pseudo-rows.""" working_tree_item, working_tree_widget = create_special_history_item( - translate("History", "Current Files Area"), + term(translate("History", "Current Files Area"), translate("History", "Working Tree")), HistorySelection(item_kind="WORKING_TREE", commit_hash=None), ) self._add_list_item(working_tree_item, working_tree_widget) staging_item, staging_widget = create_special_history_item( - translate("History", "Reviewed Area"), + term(translate("History", "Reviewed Area"), translate("History", "Staging Area")), HistorySelection(item_kind="STAGING", commit_hash=None), ) self._add_list_item(staging_item, staging_widget) diff --git a/freecad/history_wb/ui/views/history/repository_header.py b/freecad/history_wb/ui/views/history/repository_header.py index d1b8d0b..33c02b3 100644 --- a/freecad/history_wb/ui/views/history/repository_header.py +++ b/freecad/history_wb/ui/views/history/repository_header.py @@ -5,7 +5,7 @@ from ....domain.git.models import GitRepository from ....qt import QtCore, QtGui, QtWidgets from ....resources import get_icon_path -from ....utils import translate +from ....utils import term, translate from ..widgets.buttons import make_tool_button from ..widgets.styles import HEADER_ICON_BUTTON_STYLE, REPOSITORY_LABEL_EMPTY_STYLE, REPOSITORY_LABEL_LINK_STYLE @@ -60,14 +60,18 @@ def show_repository(self, repo: GitRepository | None) -> None: # Null repository means workbench currently has no project context. if repo is None: self._current_repository_path = None - self._repository_label.setText(translate("History", "No project detected")) + self._repository_label.setText( + term(translate("History", "No project detected"), translate("History", "No repository detected")) + ) self._repository_label.setToolTip("") self._repository_label.setCursor(QtCore.Qt.CursorShape.ArrowCursor) self._repository_label.setStyleSheet(REPOSITORY_LABEL_EMPTY_STYLE) return self._current_repository_path = repo.absolute_path - text = translate("History", "Project: %1").replace("%1", repo.name) + text = term(translate("History", "Project: %1"), translate("History", "Repository: %1")).replace( + "%1", repo.name + ) self._repository_label.setText(text) self._repository_label.setToolTip(repo.absolute_path) self._repository_label.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) @@ -80,7 +84,10 @@ def _setup_ui(self) -> None: self._repository_label.setStyleSheet(REPOSITORY_LABEL_EMPTY_STYLE) self._refresh_button = make_tool_button( - tooltip=translate("History", "Refresh Project and Iterations"), + tooltip=term( + translate("History", "Refresh Project and Iterations"), + translate("History", "Refresh Repository and Commits"), + ), style=HEADER_ICON_BUTTON_STYLE, icon_size=QtCore.QSize(24, 24), tool_button_style=QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly, @@ -90,7 +97,7 @@ def _setup_ui(self) -> None: self._refresh_button.clicked.connect(self.refresh_requested.emit) self._save_iteration_button = make_tool_button( - tooltip=translate("History", "Save Iteration"), + tooltip=term(translate("History", "Save Iteration"), translate("History", "Commit")), style=HEADER_ICON_BUTTON_STYLE, icon_size=QtCore.QSize(24, 24), tool_button_style=QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly, diff --git a/freecad/history_wb/ui/views/settings_preferences_page.py b/freecad/history_wb/ui/views/settings_preferences_page.py index 9ca0c00..362e734 100644 --- a/freecad/history_wb/ui/views/settings_preferences_page.py +++ b/freecad/history_wb/ui/views/settings_preferences_page.py @@ -20,7 +20,7 @@ serialize_list_lines, ) from ...qt import QtWidgets -from ...utils import Log, translate +from ...utils import Log, git_terminology_enabled, set_git_terminology_enabled, translate @dataclass(frozen=True) @@ -116,6 +116,24 @@ def __init__( precision_group = QtWidgets.QGroupBox(translate("History", "Numeric comparison"), self.form) precision_group.setLayout(precision_layout) root_layout.addWidget(precision_group) + + self._git_terminology_checkbox = QtWidgets.QCheckBox( + translate("History", "Use git terminology (Commit, Repository, Staged, …)"), + self.form, + ) + self._git_terminology_checkbox.setToolTip( + translate( + "History", + "Relabel the interface with the underlying git terms instead of the " + "CAD-friendly defaults. Takes full effect after restarting FreeCAD.", + ) + ) + terminology_layout = QtWidgets.QVBoxLayout() + terminology_layout.addWidget(self._git_terminology_checkbox) + terminology_group = QtWidgets.QGroupBox(translate("History", "Terminology"), self.form) + terminology_group.setLayout(terminology_layout) + root_layout.addWidget(terminology_group) + root_layout.addStretch(1) self._excluded_types_default_radio = self._excluded_types_controls.default_radio @@ -147,6 +165,7 @@ def loadSettings(self) -> None: # noqa: N802 self._apply_list_state(self._excluded_properties_controls, state.excluded_properties) self._apply_by_type_state(self._excluded_by_type_controls, state.excluded_properties_by_type) self._float_precision_spin.setValue(state.float_precision) + self._git_terminology_checkbox.setChecked(git_terminology_enabled()) self._is_loading = False self._sync_visibility() @@ -173,6 +192,8 @@ def saveSettings(self) -> None: # noqa: N802 Log.error(result.message or "Failed to save diff settings state") return self._loaded_state = state_to_save + # Terminology preference is independent of the diff Settings model. + set_git_terminology_enabled(self._git_terminology_checkbox.isChecked()) def _bind_signals(self) -> None: self._excluded_types_controls.custom_radio.toggled.connect( diff --git a/freecad/history_wb/utils.py b/freecad/history_wb/utils.py index fd5db2b..bd9cdba 100644 --- a/freecad/history_wb/utils.py +++ b/freecad/history_wb/utils.py @@ -6,11 +6,14 @@ """ import traceback -from typing import Protocol +from typing import Protocol, TypeVar from .qt import QtCore +_Term = TypeVar("_Term") + + class LoggerProtocol(Protocol): """Logger protocol for consistent logging interface. @@ -215,6 +218,42 @@ def translate(context: str, text: str) -> str: return text +def git_terminology_enabled() -> bool: + """Return whether the git-native terminology display toggle is on. + + Resolves through the application container's cached settings repository. + Command labels are built at workbench Initialize, before the container + exists; in that case the toggle is read directly from FreeCAD preferences. + """ + from ._container import get_container + + try: + return get_container().settings_repo.git_terminology_enabled() + except RuntimeError: + # Container not built yet (e.g. command registration at Initialize). + from .infrastructure.freecad.settings_repo import read_git_terminology_enabled + + return read_git_terminology_enabled() + + +def set_git_terminology_enabled(enabled: bool) -> None: + """Persist the git-native terminology display toggle.""" + from ._container import get_container + + get_container().settings_repo.set_git_terminology_enabled(enabled) + + +def term(cad: _Term, git: _Term) -> _Term: + """Pick the git-native phrase when the terminology toggle is on, else CAD. + + Both arguments must be passed as the result of ``translate(...)`` or + ``QT_TRANSLATE_NOOP(...)`` literals so translation extraction (lupdate) + still records both variants. Generic so it preserves ``str`` from + ``translate()`` and the ``object`` returned by ``QT_TRANSLATE_NOOP``. + """ + return git if git_terminology_enabled() else cad + + __all__ = [ "LoggerProtocol", "StdoutLogger", @@ -224,4 +263,7 @@ def translate(context: str, text: str) -> str: "format_float", "float_values_equal", "translate", + "git_terminology_enabled", + "set_git_terminology_enabled", + "term", ] diff --git a/tests/unit/entrypoints/test_open_all_documents_command.py b/tests/unit/entrypoints/test_open_all_documents_command.py index bb57b07..261899d 100644 --- a/tests/unit/entrypoints/test_open_all_documents_command.py +++ b/tests/unit/entrypoints/test_open_all_documents_command.py @@ -22,6 +22,7 @@ def test_activated_no_repository_shows_warning( """When no repository in UI state, warning popup is shown.""" mock_container = MagicMock() mock_container.translate.side_effect = lambda _ctx, text: text + mock_container.settings_repo.git_terminology_enabled.return_value = False mock_get_container.return_value = mock_container mock_ui_registry.application_state.git_repository = None diff --git a/tests/unit/infrastructure/freecad/test_settings_repo.py b/tests/unit/infrastructure/freecad/test_settings_repo.py index ac2cbc8..0f88ff7 100644 --- a/tests/unit/infrastructure/freecad/test_settings_repo.py +++ b/tests/unit/infrastructure/freecad/test_settings_repo.py @@ -11,6 +11,7 @@ ) from freecad.history_wb.domain.freecad_ports import FreeCadContext from freecad.history_wb.infrastructure.freecad.settings_repo import ( + KEY_GIT_TERMINOLOGY, MAX_FLOAT_PRECISION, MIN_FLOAT_PRECISION, FreeCADSettingsRepository, @@ -81,6 +82,31 @@ def test_default_mode_returns_config_defaults(self) -> None: assert settings.excluded_properties_by_type == EXCLUDED_PROPERTIES_BY_TYPE assert settings.float_precision == FLOAT_PRECISION + def test_git_terminology_defaults_to_disabled(self) -> None: + repo, _ = _make_repo() + + assert repo.git_terminology_enabled() is False + + def test_set_git_terminology_persists_and_reads_back(self) -> None: + repo, group = _make_repo() + + repo.set_git_terminology_enabled(True) + + assert group.GetBool(KEY_GIT_TERMINOLOGY, False) is True + assert repo.git_terminology_enabled() is True + + def test_git_terminology_is_cached_until_set(self) -> None: + repo, group = _make_repo() + + # First read caches the default; a direct param change is not observed. + assert repo.git_terminology_enabled() is False + group.SetBool(KEY_GIT_TERMINOLOGY, True) + assert repo.git_terminology_enabled() is False + + # set refreshes the cache. + repo.set_git_terminology_enabled(True) + assert repo.git_terminology_enabled() is True + def test_custom_mode_returns_parsed_line_values(self) -> None: repo, group = _make_repo() diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 505eecc..108b69f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,10 +1,12 @@ -# File responsibility: Unit tests for float comparison helper functions. +# File responsibility: Unit tests for utility helper functions. """Unit tests for utility functions in the Diff Workbench.""" +from unittest.mock import patch + import pytest from freecad.history_wb.domain.config import FLOAT_PRECISION -from freecad.history_wb.utils import float_values_equal +from freecad.history_wb.utils import float_values_equal, term class TestFloatValuesEqual: @@ -36,3 +38,15 @@ def test_float_values_equal_uses_default_precision(self) -> None: """Test that float_values_equal uses the configured precision.""" assert float_values_equal(1.0, 1.0 + 1e-8, FLOAT_PRECISION) is True assert float_values_equal(1.0, 1.1, FLOAT_PRECISION) is False + + +class TestTerm: + """Tests for the git-terminology phrase selector.""" + + def test_returns_cad_phrase_when_disabled(self) -> None: + with patch("freecad.history_wb.utils.git_terminology_enabled", return_value=False): + assert term("Save Iteration", "Commit") == "Save Iteration" + + def test_returns_git_phrase_when_enabled(self) -> None: + with patch("freecad.history_wb.utils.git_terminology_enabled", return_value=True): + assert term("Save Iteration", "Commit") == "Commit" diff --git a/tests/unit/ui/views/test_settings_preferences_page.py b/tests/unit/ui/views/test_settings_preferences_page.py index 4ef7196..08fb611 100644 --- a/tests/unit/ui/views/test_settings_preferences_page.py +++ b/tests/unit/ui/views/test_settings_preferences_page.py @@ -4,6 +4,9 @@ from __future__ import annotations from dataclasses import replace +from unittest.mock import MagicMock, patch + +import pytest from freecad.history_wb.application.actions.result_models import Result from freecad.history_wb.domain.config import EXCLUDED_TYPES @@ -61,6 +64,14 @@ def _ensure_qapplication() -> None: class TestDiffSettingsPreferencesPage: + @pytest.fixture(autouse=True) + def _fake_container(self): + """Provide a container so the git-terminology toggle read/write resolves.""" + container = MagicMock() + container.settings_repo.git_terminology_enabled.return_value = False + with patch("freecad.history_wb._container.get_container", return_value=container): + yield container + def test_preference_page_loads_current_mode_and_values_into_controls(self) -> None: _ensure_qapplication()