diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 08952342..7b5c0281 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -20,7 +20,7 @@ class SVN_Operator: @staticmethod def update_file_list(context): repo = context.scene.svn.get_repo(context) - repo.update_file_filter(context) + repo.refresh_ui_lists(context) def execute_svn_command(self, context, command: List[str], use_cred=False) -> str: # Since a status update might already be being requested when an SVN operator is run, @@ -51,9 +51,8 @@ class SVN_Operator_Single_File(SVN_Operator): ret = self._execute(context) file = self.get_file(context) - if file: - Processes.start('Status') - redraw_viewport() + Processes.start('Status') + redraw_viewport() self.update_file_list(context) return ret @@ -107,7 +106,7 @@ class May_Modifiy_Current_Blend(SVN_Operator_Single_File, Warning_Operator): return current_blend and current_blend.svn_path == self.file_rel_path reload_file: BoolProperty( - name="Reload File", + name="Reload File (Keep UI)", description="Reload the file after the operation is completed. The UI layout will be preserved", default=False, ) @@ -133,6 +132,8 @@ class May_Modifiy_Current_Blend(SVN_Operator_Single_File, Warning_Operator): super().execute(context) if self.reload_file: bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath, load_ui=False) + else: + context.scene.svn.file_is_outdated = True return {'FINISHED'} @@ -178,7 +179,7 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator): missing_file_allowed = True - revision: IntProperty() + revision: IntProperty(default=0) def invoke(self, context, event): file_entry = context.scene.svn.get_repo( @@ -199,18 +200,24 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator): "Cancelled: You have local modifications to this file. You must revert or commit it first!") return {'CANCELLED'} - self.execute_svn_command( - context, - ["svn", "up", f"-r{self.revision}", - f"{self.file_rel_path}", "--accept", "postpone"], - use_cred=True - ) + self.svn_download_file_revision(context, self.file_rel_path, self.revision) self.report({'INFO'}, f"Checked out revision {self.revision} of {self.file_rel_path}") return {"FINISHED"} + def svn_download_file_revision(self, context, svn_file_path: str, revision=0): + commands = ["svn", "up", f"{self.file_rel_path}", "--accept", "postpone"] + if self.revision > 0: + commands.insert(2, f"-r{self.revision}") + + self.execute_svn_command( + context, + commands, + use_cred=True + ) + def set_predicted_file_status(self, repo, file_entry: "SVN_file"): file_entry['revision'] = self.revision latest_rev = repo.get_latest_revision_of_file(self.file_rel_path) @@ -230,13 +237,14 @@ class SVN_OT_restore_file(May_Modifiy_Current_Blend, Operator): missing_file_allowed = True - def _execute(self, context: Context) -> Set[str]: + def svn_revert(self, context, svn_file_path): self.execute_svn_command( context, - ["svn", "revert", f"{self.file_rel_path}"] + ["svn", "revert", f"{svn_file_path}"] ) - f = self.get_file(context) + def _execute(self, context: Context) -> Set[str]: + self.svn_revert(context, self.file_rel_path) return {"FINISHED"} def set_predicted_file_status(self, repo, file_entry: "SVN_file"): @@ -251,15 +259,35 @@ class SVN_OT_revert_file(SVN_OT_restore_file): missing_file_allowed = False - def _execute(self, context: Context) -> Set[str]: - super()._execute(context) - - return {"FINISHED"} - def get_warning_text(self, context) -> str: return "You will irreversibly and permanently lose the changes you've made to this file:\n " + self.file_rel_path +class SVN_OT_revert_and_update(SVN_OT_download_file_revision, SVN_OT_revert_file): + """Convenience operator for the "This file is outdated" warning message. Normally, these two operations should be done separately!""" + bl_idname = "svn.revert_and_update_file" + bl_label = "Revert And Update File" + bl_description = "A different version of this file was downloaded while it was open. This warning will persist until the file is updated and reloaded, or committed. Click to PERMANENTLY DISCARD local changes to this file and update it to the latest revision. Cannot be undone" + bl_options = {'INTERNAL'} + + missing_file_allowed = False + + def invoke(self, context, event): + return super(May_Modifiy_Current_Blend, self).invoke(context, event) + + def get_warning_text(self, context) -> str: + if self.get_file(context).status != 'normal': + return "You will irreversibly and permanently lose the changes you've made to this file:\n " + self.file_rel_path + else: + return "File will be updated to latest revision." + + def _execute(self, context: Context) -> Set[str]: + self.svn_revert(context, self.file_rel_path) + self.svn_download_file_revision(context, self.file_rel_path, self.revision) + + return {"FINISHED"} + + class SVN_OT_add_file(SVN_Operator_Single_File, Operator): bl_idname = "svn.add_file" bl_label = "Add File" @@ -305,6 +333,7 @@ class SVN_OT_trash_file(SVN_Operator_Single_File, Warning_Operator, Operator): bl_options = {'INTERNAL'} file_rel_path: StringProperty() + missing_file_allowed = False def get_warning_text(self, context): return "Are you sure you want to move this file to the recycle bin?\n " + self.file_rel_path @@ -369,6 +398,7 @@ class SVN_OT_resolve_conflict(May_Modifiy_Current_Blend, Operator): col.alert = True col.label(text="Choose which version of the file to keep.") col.row().prop(self, 'resolve_method', expand=True) + col.separator() if self.resolve_method == 'mine-full': col.label(text="Local changes will be kept.") col.label( @@ -411,15 +441,14 @@ class SVN_OT_cleanup(SVN_Operator, Operator): self.execute_svn_command(context, ["svn", "cleanup"]) repo.reload_svn_log(context) - Processes.kill('Status') - Processes.kill('Log') Processes.kill('Commit') Processes.kill('Update') Processes.kill('Authenticate') Processes.kill('Activate File') - Processes.start('Status') - Processes.start('Log') + Processes.restart('Status') + Processes.restart('Log') + Processes.restart('Redraw Viewport') self.report({'INFO'}, "SVN Cleanup complete.") @@ -428,13 +457,14 @@ class SVN_OT_cleanup(SVN_Operator, Operator): registry = [ SVN_OT_update_single, - SVN_OT_download_file_revision, - SVN_OT_revert_file, + SVN_OT_revert_and_update, SVN_OT_restore_file, - SVN_OT_unadd_file, + SVN_OT_revert_file, + SVN_OT_download_file_revision, SVN_OT_add_file, + SVN_OT_unadd_file, SVN_OT_trash_file, SVN_OT_remove_file, - SVN_OT_cleanup, SVN_OT_resolve_conflict, + SVN_OT_cleanup, ] diff --git a/scripts-blender/addons/blender_svn/operators/svn_checkout.py b/scripts-blender/addons/blender_svn/operators/svn_checkout.py index 03be4919..260251bb 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_checkout.py +++ b/scripts-blender/addons/blender_svn/operators/svn_checkout.py @@ -28,7 +28,6 @@ class SVN_OT_checkout_initiate(Operator): def execute(self, context): prefs = get_addon_prefs(context) - prefs.active_repo_mode = 'SELECTED_REPO' if self.create: prefs.repositories.add() prefs.active_repo_idx = len(prefs.repositories)-1 diff --git a/scripts-blender/addons/blender_svn/operators/svn_commit.py b/scripts-blender/addons/blender_svn/operators/svn_commit.py index 90a9a9cd..0ddace0c 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_commit.py +++ b/scripts-blender/addons/blender_svn/operators/svn_commit.py @@ -40,6 +40,8 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): bl_options = {'INTERNAL'} bl_property = "first_line" # Focus the text input box + popup_width = 600 + # The first line of the commit message needs to be an operator property in order # for us to be able to focus the input box automatically when the window pops up # (see bl_property above) @@ -92,7 +94,8 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): for f in repo.external_files: f.include_in_commit = False for f in self.get_committable_files(context): - f.include_in_commit = True + if not f.will_conflict: + f.include_in_commit = True return super().invoke(context, event) @@ -108,20 +111,30 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): row.label(text="Status") for file in files: row = layout.row() - row.prop(file, "include_in_commit", text=file.name) + split = row.split() + checkbox_ui = split.row() + status_ui = split.row() + checkbox_ui.prop(file, "include_in_commit", text=file.name) text = file.status_name icon = file.status_icon - if file == repo.current_blend_file and self.is_file_really_dirty: - split = row.split(factor=0.7) - row = split.row() - row.alert = True + if file.will_conflict: + # We don't want to conflict-resolve during a commit, it's + # confusing. User should resolve this as a separate step. + checkbox_ui.enabled = False + text = "Conflicting" + status_ui.alert = True + icon = 'ERROR' + elif file == repo.current_blend_file and self.is_file_really_dirty: + split = status_ui.split(factor=0.7) + status_ui = split.row() + status_ui.alert = True text += " but not saved!" icon = 'ERROR' op_row = split.row() op_row.alignment = 'LEFT' op_row.operator('svn.save_during_commit', icon='FILE_BLEND', text="Save") - row.label(text=text, icon=icon) + status_ui.label(text=text, icon=icon) row = layout.row() row.label(text="Commit message:") @@ -142,7 +155,6 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): def execute(self, context: Context) -> Set[str]: committable_files = self.get_committable_files(context) files_to_commit = [f for f in committable_files if f.include_in_commit] - prefs = get_addon_prefs(context) repo = context.scene.svn.get_repo(context) if not files_to_commit: diff --git a/scripts-blender/addons/blender_svn/operators/svn_update.py b/scripts-blender/addons/blender_svn/operators/svn_update.py index 161ad625..a066da13 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_update.py +++ b/scripts-blender/addons/blender_svn/operators/svn_update.py @@ -44,11 +44,14 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator): current_blend = repo.current_blend_file if self.revision == 0: if current_blend and current_blend.repos_status != 'none': + # If the current file will be modified, warn user. self.file_rel_path = current_blend.svn_path return context.window_manager.invoke_props_dialog(self, width=500) else: for f in repo.external_files: if f.status in ['modified', 'added', 'conflicted', 'deleted', 'missing', 'unversioned']: + # If user wants to check out an older version of the repo but + # there are uncommitted local changes to any files, warn user. return context.window_manager.invoke_props_dialog(self, width=500) return self.execute(context) diff --git a/scripts-blender/addons/blender_svn/prefs.py b/scripts-blender/addons/blender_svn/prefs.py index 955ee11a..1078f4c8 100644 --- a/scripts-blender/addons/blender_svn/prefs.py +++ b/scripts-blender/addons/blender_svn/prefs.py @@ -3,12 +3,13 @@ # (c) 2022, Blender Foundation - Demeter Dzadik from typing import Optional, Any, Set, Tuple, List +import platform import bpy from bpy.props import IntProperty, CollectionProperty, BoolProperty, EnumProperty from bpy.types import AddonPreferences -from .ui import ui_prefs +from .ui.ui_repo_list import draw_checkout, draw_repo_list from .repository import SVN_repository from .svn_info import get_svn_info import json @@ -19,8 +20,25 @@ from .threaded.background_process import Processes class SVN_addon_preferences(AddonPreferences): bl_idname = __package__ + is_svn_installed: BoolProperty( + name="Is SVN Installed", + description="Whether the `svn` command works at all in the user's command line. If not, user needs to install SVN", + default=False + ) + repositories: CollectionProperty(type=SVN_repository) + def init_repo_list(self): + # If we have any repository entries, make sure at least one is active. + self.sync_repo_info_file() + + if self.active_repo_idx == -1 and len(self.repositories) > 0: + self.active_repo_idx = 0 + elif self.active_repo_idx > len(self.repositories)-1: + self.active_repo_idx = 0 + else: + self.active_repo_idx = self.active_repo_idx + def init_repo(self, context, repo_path: Path or str): """Attempt to initialize a repository based on a directory. This means executing `svn info` in the repo_path to get the URL and root dir. @@ -40,70 +58,45 @@ class SVN_addon_preferences(AddonPreferences): repo = self.repositories.add() repo.initialize(root_dir, base_url) + self.active_repo_idx = len(self.repositories)-1 return repo - def update_active_repo_idx(self, context): - if self.idx_updating or len(self.repositories) == 0: - return - self.idx_updating = True - active_repo = self.active_repo - if self.active_repo_mode == 'CURRENT_BLEND': - scene_svn = context.scene.svn - scene_svn_idx = self.repositories.find(scene_svn.svn_directory) - if scene_svn_idx == -1: - self.idx_updating = False - return - self.active_repo_idx = scene_svn_idx - self.idx_updating = False - return - - if ( - active_repo and - not active_repo.authenticated and - not active_repo.auth_failed and - active_repo.is_cred_entered - ): - active_repo.authenticate(context) - - self.idx_updating = False - - def update_active_repo_mode(self, context): - if self.active_repo_mode == 'CURRENT_BLEND': - scene_svn = context.scene.svn - scene_svn_idx = self.repositories.find(scene_svn.svn_directory) - self.active_repo_idx = scene_svn_idx - checkout_mode: BoolProperty( name="Checkout In Progress", description="Internal flag to indicate that the user is currently trying to create a new checkout", default=False ) - active_repo_mode: EnumProperty( - name="Choose Repository", - description="Whether the add-on should communicate with the repository of the currently opened .blend file, or the repository selected in the list below", - items=[ - ('CURRENT_BLEND', "Current Blend", "Check if the current .blend file is in an SVN repository, and communicate with that if that is the case. The file list will display only the files of the repository of the current .blend file. If the current .blend is not in a repository, do nothing"), - ('SELECTED_REPO', "Selected Repo", - "Communicate with the selected repository") - ], - default='CURRENT_BLEND', - update=update_active_repo_mode - ) + def update_active_repo_idx(self, context): + if len(self.repositories) == 0: + return + active_repo = self.active_repo + + # Authenticate when switching repos. + if ( + active_repo and + not active_repo.auth_failed and + active_repo.is_cred_entered + ): + Processes.start('Redraw Viewport') + if active_repo.authenticated: + Processes.restart('Status') + else: + active_repo.authenticate() + else: + Processes.kill('Status') active_repo_idx: IntProperty( name="SVN Repositories", options=set(), update=update_active_repo_idx ) - idx_updating: BoolProperty( - name="Index is Updating", - description="Helper flag to avoid infinite looping update callbacks", - ) @property - def active_repo(self) -> SVN_repository: + def active_repo(self) -> Optional[SVN_repository]: + if not self.is_svn_installed: + return if 0 <= self.active_repo_idx <= len(self.repositories)-1: return self.repositories[self.active_repo_idx] @@ -163,7 +156,33 @@ class SVN_addon_preferences(AddonPreferences): self.load_repo_info_from_file() self.save_repo_info_to_file() - draw = ui_prefs.draw_prefs + def draw(self, context): + if not self.is_svn_installed: + draw_prefs_no_svn(self, context) + return + + if self.checkout_mode: + draw_checkout(self, context) + else: + draw_repo_list(self, context) + + +def draw_prefs_no_svn(self, context): + terminal, url = "terminal", "https://subversion.apache.org/packages.html" + system = platform.system() + if system == "Windows": + terminal = "command line (cmd.exe)" + url = "https://subversion.apache.org/packages.html#windows" + elif system == "Darwin": + terminal = "Mac terminal" + url = "https://subversion.apache.org/packages.html#osx" + + layout = self.layout + col = layout.column() + col.alert=True + col.label(text="Please ensure that Subversion (aka. SVN) is installed on your system.") + col.label(text=f"Typing `svn` into the {terminal} should yield a result.") + layout.operator("wm.url_open", icon='URL', text='Open Subversion Distribution Page').url=url registry = [ diff --git a/scripts-blender/addons/blender_svn/props.py b/scripts-blender/addons/blender_svn/props.py index 39a383ca..f3d97b34 100644 --- a/scripts-blender/addons/blender_svn/props.py +++ b/scripts-blender/addons/blender_svn/props.py @@ -3,10 +3,9 @@ # (c) 2022, Blender Foundation - Demeter Dzadik from .util import get_addon_prefs -from bpy.props import StringProperty, PointerProperty +from bpy.props import StringProperty, PointerProperty, BoolProperty from bpy.types import PropertyGroup import bpy -from pathlib import Path from typing import Optional, Dict, Any, List, Tuple, Set from . import wheels # This will load the dateutil and BAT wheel files. @@ -27,26 +26,16 @@ class SVN_scene_properties(PropertyGroup): description="Absolute directory path of the SVN repository's root in the file system", ) - def get_repo(self, context): - """Return the current repository. - Depending on preferences, this is either the repo the current .blend file is in, - or whatever repo is selected in the preferences UI. - """ + file_is_outdated: BoolProperty( + name="File Is Outdated", + description="Set to True when downloading a newer version of this file without reloading it, so that the warning in the UI can persist. This won't work in some cases involving multiple running Blender instances", + default=False + ) + + def get_repo(self, context) -> Optional['SVN_repository']: + """Return the active repository.""" prefs = get_addon_prefs(context) - - if prefs.active_repo_mode == 'CURRENT_BLEND': - return self.get_scene_repo(context) - else: - return prefs.active_repo - - def get_scene_repo(self, context) -> Optional['SVN_repository']: - if not self.svn_url or not self.svn_directory: - return - - prefs = get_addon_prefs(context) - for repo in prefs.repositories: - if (repo.url == self.svn_url) and (Path(repo.directory) == Path(self.svn_directory)): - return repo + return prefs.active_repo registry = [ diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index b920ca41..f33cf891 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -48,6 +48,10 @@ class SVN_file(PropertyGroup): default="none", options=set() ) + @property + def will_conflict(self): + return self.status != 'normal' and self.repos_status != 'none' + status_prediction_type: EnumProperty( name="Status Predicted By Process", items=[ @@ -68,18 +72,6 @@ class SVN_file(PropertyGroup): options=set() ) - @property - def absolute_path(self) -> Path: - """Return absolute path on the file system.""" - scene = self.id_data - svn = scene.svn - return Path(svn.svn_directory).joinpath(Path(self.svn_path)) - - @property - def relative_path(self) -> str: - """Return relative path with Blender's path conventions.""" - return bpy.path.relpath(self.absolute_path) - @property def is_outdated(self): return self.repos_status == 'modified' and self.status == 'normal' @@ -247,6 +239,7 @@ class SVN_repository(PropertyGroup): @property def is_valid_svn(self): dir_path = Path(self.directory) + # TODO: This property is checked pretty often, so we run `svn info` pretty often. Might not be a big deal, but maybe it's a bit overkill? root_dir, base_url = get_svn_info(self.directory) return ( dir_path.exists() and @@ -282,10 +275,10 @@ class SVN_repository(PropertyGroup): if get_addon_prefs(context).loading: return - self.authenticate(context) + self.authenticate() self.update_repo_info_file(context) - def authenticate(self, context): + def authenticate(self): self.auth_failed = False if self.is_valid_svn and self.is_cred_entered: Processes.start('Authenticate') @@ -305,9 +298,9 @@ class SVN_repository(PropertyGroup): ) @property - def is_cred_entered(self): + def is_cred_entered(self) -> bool: """Check if there's a username and password entered at all.""" - return self.username and self.password + return bool(self.username and self.password) authenticated: BoolProperty( name="Authenticated", @@ -316,7 +309,7 @@ class SVN_repository(PropertyGroup): ) auth_failed: BoolProperty( name="Authentication Failed", - description="Internal flag to mark whether the last entered credentials were denied by the repo", + description="Internal flag to mark whether the last entered credentials were rejected by the repo", default=False ) @@ -342,10 +335,6 @@ class SVN_repository(PropertyGroup): name="SVN Log", options=set() ) - log_active_index_filebrowser: IntProperty( - name="SVN Log", - options=set() - ) reload_svn_log = svn_log.reload_svn_log @@ -360,19 +349,13 @@ class SVN_repository(PropertyGroup): except IndexError: return None - @property - def active_log_filebrowser(self): - try: - return self.log[self.log_active_index_filebrowser] - except IndexError: - return None - def get_log_by_revision(self, revision: int) -> Tuple[int, SVN_log]: for i, log in enumerate(self.log): if log.revision_number == revision: return i, log def get_latest_revision_of_file(self, svn_path: str) -> int: + """Return the revision number of the last log entry that affects the given file.""" svn_path = str(svn_path) for log in reversed(self.log): for changed_file in log.changed_files: @@ -442,33 +425,43 @@ class SVN_repository(PropertyGroup): def update_active_file(self, context): """When user clicks on a different file, the latest log entry of that file - should become the active log entry.""" + should become the active log entry. + NOTE: Try to only trigger this on explicit user actions! + """ - latest_idx = self.get_latest_revision_of_file( + if self.external_files_active_index == self.prev_external_files_active_index: + return + self.prev_external_files_active_index = self.external_files_active_index + + latest_rev = self.get_latest_revision_of_file( self.active_file.svn_path) # SVN Revisions are not 0-indexed, so we need to subtract 1. - self.log_active_index = latest_idx-1 + self.log_active_index = latest_rev-1 space = context.space_data if space and space.type == 'FILE_BROWSER': - # Set the active file in the file browser to whatever was selected in the SVN Files panel. - self.log_active_index_filebrowser = latest_idx-1 - - space.params.directory = self.active_file.absolute_path.parent.as_posix().encode() + space.params.directory = Path(self.active_file.absolute_path).parent.as_posix().encode() space.params.filename = self.active_file.name.encode() space.deselect_all() - space.activate_file_by_relative_path( + # Set the active file in the file browser to whatever was selected + # in the SVN Files panel. + space.activate_file_by_relative_path( # This doesn't actually work, due to what I assume is a bug. relative_path=self.active_file.name) - Processes.start('Activate File') + Processes.start('Activate File') # This is my work-around. - # Filter out log entries that did not affect the selected file. + # Set the filter flag of the log entries based on whether they affect the active file or not. self.log.foreach_set( 'affects_active_file', [log_entry.changes_file(self.active_file) for log_entry in self.log] ) + prev_external_files_active_index: IntProperty( + name="Previous Active Index", + description="Internal value to avoid triggering the update callback unnecessarily", + options=set() + ) external_files_active_index: IntProperty( name="File List", description="Files tracked by SVN", @@ -513,28 +506,10 @@ class SVN_repository(PropertyGroup): return self.get_file_by_absolute_path(bpy.data.filepath) ### File List UIList filter properties ### - # Filtering properties are normally stored on the UIList, - # but then they cannot be accessed from anywhere else, - # since template_list() does not return the UIList instance. - # We need to be able to access them outside of drawing code, to be able to - # ensure that a filtered out entry can never be the active one. - - def force_good_active_index(self, context) -> bool: - """ - We want to avoid having the active file entry be invisible due to filtering. - If the active element is being filtered out, set the active element to - something that is visible. - """ - if len(self.external_files) == 0: - return - if not self.active_file.show_in_filelist: - for i, file in enumerate(self.external_files): - if file.show_in_filelist: - self.external_files_active_index = i - return - - def update_file_filter(self, context): - """Should run when any of the SVN file list search filters are changed.""" + def refresh_ui_lists(self, context): + """Refresh the file UI list based on filter settings. + Also triggers a refresh of the SVN UIList, through the update callback of + external_files_active_index.""" UI_LIST = bpy.types.UI_UL_list if self.file_search_filter: @@ -555,12 +530,20 @@ class SVN_repository(PropertyGroup): file.show_in_filelist = not file.has_default_status - self.force_good_active_index(context) + if len(self.external_files) == 0: + return + + # Make sure the active file isn't now being filtered out. + # If it is, change the active file to the first visible one. + for i, file in enumerate(self.external_files): + if file.show_in_filelist: + self.external_files_active_index = i + return file_search_filter: StringProperty( name="Search Filter", description="Only show entries that contain this string", - update=update_file_filter + update=refresh_ui_lists ) diff --git a/scripts-blender/addons/blender_svn/threaded/background_process.py b/scripts-blender/addons/blender_svn/threaded/background_process.py index 983a7a06..968336e4 100644 --- a/scripts-blender/addons/blender_svn/threaded/background_process.py +++ b/scripts-blender/addons/blender_svn/threaded/background_process.py @@ -44,7 +44,7 @@ class BackgroundProcess: # Displayed in the tooltip on mouse-hover in the error message when an error occurs. error_description = "SVN Error:" - debug = True + debug = False def debug_print(self, msg: str): if self.debug: @@ -82,7 +82,7 @@ class BackgroundProcess: def handle_error(self, context, error): self.output = "" self.error = error.stderr.decode() - self.is_running = False + self.stop() def process_output(self, context, prefs): """ @@ -113,7 +113,7 @@ class BackgroundProcess: repo = context.scene.svn.get_repo(context) if not repo: self.debug_print("Shutdown: Not in repo.") - self.is_running = False + self.stop() return prefs = get_addon_prefs(context) @@ -127,7 +127,7 @@ class BackgroundProcess: if self.needs_authentication and not repo.authenticated: self.debug_print("Shutdown: Authentication needed.") - self.is_running = False + self.stop() return if not self.thread or not self.thread.is_alive() and not self.output and not self.error: @@ -146,15 +146,14 @@ class BackgroundProcess: self.output = "" redraw_viewport() if self.repeat_delay == 0: - self.debug_print( - "Shutdown: Output was processed, repeat_delay==0.") - self.is_running = False + self.debug_print("Shutdown: Output was processed, repeat_delay==0.") + self.stop() return self.debug_print(f"Processed output. Waiting {self.repeat_delay}") return self.repeat_delay elif not self.thread and not self.thread.is_alive() and self.repeat_delay == 0: self.debug_print("Shutdown: Finished.\n") - self.is_running = False + self.stop() return self.debug_print(f"Tick delay: {self.tick_delay}") @@ -188,6 +187,7 @@ class BackgroundProcess: def stop(self): """Stop the process if it isn't running, by unregistering its timer function""" + self.debug_print("stop() function was called.") self.is_running = False if bpy.app.timers.is_registered(self.timer_function): # This won't work if the timer has returned None at any point, as that @@ -214,6 +214,7 @@ class ProcessManager: def processes(self): # I tried to implement this thing as a Singleton that inherits from the `dict` class, # I tried having the `processes` dict on the class level, + # I tried having it on the instance level, # and it just refuses to work properly. I add an instance to the dictionary, # I print it, I can see that it's there, I make sure it absolutely doesn't get removed, # but when I try to access it from anywhere, it's just empty. My mind is boggled. @@ -238,6 +239,8 @@ class ProcessManager: process = self.processes.get(proc_name, None) if process: process.start() + for key, value in kwargs.items(): + setattr(process, key, value) return else: for subcl in get_recursive_subclasses(BackgroundProcess): @@ -260,6 +263,12 @@ class ProcessManager: if process: process.stop() del self.processes[proc_name] + + def restart(self, proc_name: str): + """Destroy a process, then start it again. + Useful to skip the repeat_delay timer of infinite processes like Status or Log.""" + self.kill(proc_name) + self.start(proc_name) # I named this variable with title-case, to indicate that it's a Singleton. diff --git a/scripts-blender/addons/blender_svn/threaded/commit.py b/scripts-blender/addons/blender_svn/threaded/commit.py index af7c7eb2..80ffeab5 100644 --- a/scripts-blender/addons/blender_svn/threaded/commit.py +++ b/scripts-blender/addons/blender_svn/threaded/commit.py @@ -51,6 +51,8 @@ class BGP_SVN_Commit(BackgroundProcess): print(self.output) repo = context.scene.svn.get_repo(context) for f in repo.external_files: + if f == repo.current_blend_file: + context.scene.file_is_outdated = False if f.status_prediction_type == 'SVN_COMMIT': f.status_prediction_type = 'SKIP_ONCE' Processes.start('Log') diff --git a/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py b/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py index 7e395c9b..6816fb28 100644 --- a/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py +++ b/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py @@ -4,7 +4,6 @@ import subprocess from typing import List - def get_credential_commands(context) -> List[str]: repo = context.scene.svn.get_repo(context) assert (repo.is_cred_entered), "No username or password entered for this repository. The UI shouldn't have allowed you to get into a state where you can press an SVN operation button without having your credentials entered, so this is a bug!" @@ -29,6 +28,9 @@ def execute_svn_command(context, command: List[str], *, ignore_errors=False, pri SVN root. """ repo = context.scene.svn.get_repo(context) + if "svn" not in command: + command.insert(0, "svn") + if use_cred: command += get_credential_commands(context) @@ -46,3 +48,7 @@ def execute_svn_command(context, command: List[str], *, ignore_errors=False, pri print(f"Command returned error: {command}") print(err_msg) raise error + +def check_svn_installed(): + code, message = subprocess.getstatusoutput('svn') + return code != 127 \ No newline at end of file diff --git a/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py b/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py index f2911c2c..c9784a97 100644 --- a/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py +++ b/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py @@ -7,6 +7,7 @@ class BGP_SVN_Redraw_Viewport(BackgroundProcess): repeat_delay = 1 debug = False tick_delay = 1 + needs_authentication = False def tick(self, context, prefs): redraw_viewport() diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index 06f85ac8..f9468acf 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -4,7 +4,7 @@ from ..svn_info import get_svn_info from ..util import get_addon_prefs from .. import constants -from .execute_subprocess import execute_svn_command +from .execute_subprocess import execute_svn_command, check_svn_installed from .background_process import BackgroundProcess, Processes from bpy.types import Operator from bpy.props import StringProperty @@ -55,49 +55,44 @@ class SVN_OT_explain_status(Operator): @bpy.app.handlers.persistent -def init_svn_of_current_file(_scene=None): - """When opening or saving a .blend file: - - Initialize SVN Scene info - - Initialize Repository - - Try to authenticate +def ensure_svn_of_current_file(_scene=None): + """When opening or saving a .blend file, it's possible that the new .blend + is part of an SVN repository. If this is the case, do the following: + - Check if this file's repository is already in our database + - If not, create it + - Switch to that repo """ - context = bpy.context + prefs = get_addon_prefs(context) + prefs.is_svn_installed = check_svn_installed() + if not prefs.is_svn_installed: + return + scene_svn = context.scene.svn - prefs = get_addon_prefs(context) - prefs.sync_repo_info_file() + old_active_repo = prefs.active_repo + prefs.init_repo_list() - for repo in prefs.repositories: - # This would ideally only run when opening Blender for the first - # time, but there is no app handler for that, sadly. - repo.authenticated = False - repo.auth_failed = False + # If the file is unsaved, nothing more to do. + if not bpy.data.filepath: + scene_svn.svn_url = "" + return - if prefs.active_repo_mode == 'CURRENT_BLEND': - if not bpy.data.filepath: - scene_svn.svn_url = "" - return - - is_in_repo = set_scene_svn_info(context) - if not is_in_repo: - return - - repo = scene_svn.get_scene_repo(context) - if not repo: - repo = prefs.init_repo(context, scene_svn.svn_directory) - - for i, other_repo in enumerate(prefs.repositories): - if other_repo == repo: - prefs.active_repo_idx = i + # If file is not in a repo, nothing more to do. + is_in_repo = set_scene_svn_info(context) + if not is_in_repo: + return + # If file is in an existing repo, we should switch over to that repo. + for i, existing_repo in enumerate(prefs.repositories): + if ( existing_repo.url == scene_svn.svn_url and + existing_repo.directory == scene_svn.svn_directory and + existing_repo != old_active_repo + ): + prefs.active_repo_idx = i else: - repo = prefs.active_repo - if not repo: - return - - if repo.is_cred_entered: - repo.authenticate(context) + # If file is in a non-existing repo, initialize that repo. + prefs.init_repo(context, scene_svn.svn_directory) def set_scene_svn_info(context) -> bool: @@ -233,21 +228,6 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]): if not file_entry.exists: new_files_on_repo.add((file_entry.svn_path, repos_status)) - # if file_entry.status_prediction_type == 'SKIP_ONCE': - # # File status was predicted by a local svn file operation, - # # so we should ignore this status update and reset the flag. - # # The file status will be updated on the next status update. - # # This is because this status update was initiated before the file's - # # status was predicted, so the prediction is likely to be correct, - # # and the status we have here is likely to be outdated. - # file_entry.status_prediction_type = 'SKIPPED_ONCE' - # continue - # elif file_entry.status_prediction_type not in {'NONE', 'SKIPPED_ONCE'}: - # # We wait for `svn up/commit` background processes to finish and - # # set the predicted flag to SKIP_ONCE. Until then, we ignore status - # # updates on files that are being updated or committed. - # continue - if entry_existed and (file_entry.repos_status == 'none' and repos_status != 'none'): new_files_on_repo.add((file_entry.svn_path, repos_status)) @@ -277,8 +257,7 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]): if file_entry.svn_path not in svn_paths: repo.remove_file_entry(file_entry) - repo.update_file_filter(context) - repo.force_good_active_index(context) + repo.refresh_ui_lists(context) def get_repo_file_statuses(svn_status_str: str) -> Dict[str, Tuple[str, str, int]]: @@ -339,22 +318,22 @@ def mark_current_file_as_modified(_dummy1=None, _dummy2=None): def delayed_init_svn(delay=1): - bpy.app.timers.register(init_svn_of_current_file, first_interval=delay) + bpy.app.timers.register(ensure_svn_of_current_file, first_interval=delay) def register(): - bpy.app.handlers.load_post.append(init_svn_of_current_file) + bpy.app.handlers.load_post.append(ensure_svn_of_current_file) - bpy.app.handlers.save_post.append(init_svn_of_current_file) + bpy.app.handlers.save_post.append(ensure_svn_of_current_file) bpy.app.handlers.save_post.append(mark_current_file_as_modified) delayed_init_svn() def unregister(): - bpy.app.handlers.load_post.remove(init_svn_of_current_file) + bpy.app.handlers.load_post.remove(ensure_svn_of_current_file) - bpy.app.handlers.save_post.remove(init_svn_of_current_file) + bpy.app.handlers.save_post.remove(ensure_svn_of_current_file) bpy.app.handlers.save_post.remove(mark_current_file_as_modified) diff --git a/scripts-blender/addons/blender_svn/ui/__init__.py b/scripts-blender/addons/blender_svn/ui/__init__.py index 10daf702..716a8ec5 100644 --- a/scripts-blender/addons/blender_svn/ui/__init__.py +++ b/scripts-blender/addons/blender_svn/ui/__init__.py @@ -3,7 +3,7 @@ from . import ( ui_sidebar, ui_filebrowser, ui_log, - ui_prefs, + ui_repo_list, ui_outdated_warning, ui_context_menus ) @@ -13,7 +13,7 @@ modules = [ ui_sidebar, ui_filebrowser, ui_log, - ui_prefs, + ui_repo_list, ui_outdated_warning, ui_context_menus ] diff --git a/scripts-blender/addons/blender_svn/ui/ui_context_menus.py b/scripts-blender/addons/blender_svn/ui/ui_context_menus.py index 8082620b..6e509d4d 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_context_menus.py +++ b/scripts-blender/addons/blender_svn/ui/ui_context_menus.py @@ -3,26 +3,10 @@ import bpy from bpy.types import Context, UIList, Operator -from bpy.props import StringProperty +from bpy.props import StringProperty, BoolProperty from pathlib import Path -class SVN_OT_open_blend_file(Operator): - # This is needed because drawing a button for wm.open_mainfile in the UI - # directly simply does not work; Blender just opens a full-screen filebrowser, - # instead of opening the .blend file. Probably a bug. - bl_idname = "svn.open_blend_file" - bl_label = "Open Blend File" - bl_description = "Open Blend File" - bl_options = {'INTERNAL'} - - filepath: StringProperty() - - def execute(self, context): - bpy.ops.wm.open_mainfile(filepath=self.filepath, load_ui=False) - return {'FINISHED'} - - def check_context_match(context: Context, uilayout_type: str, bl_idname: str) -> bool: """For example, when right-clicking on a UIList, the uilayout_type will be `ui_list` and the bl_idname is that of the UIList being right-clicked. @@ -39,8 +23,17 @@ def svn_file_list_context_menu(self: UIList, context: Context) -> None: layout.separator() active_file = context.scene.svn.get_repo(context).active_file if active_file.name.endswith("blend"): - layout.operator("svn.open_blend_file", - text=f"Open {active_file.name}").filepath = active_file.absolute_path + op = layout.operator("wm.open_mainfile", + text=f"Open {active_file.name}") + op.filepath = active_file.absolute_path + op.display_file_selector = False + op.load_ui = True + op = layout.operator("wm.open_mainfile", + text=f"Open {active_file.name} (Keep UI)") + op.filepath = active_file.absolute_path + op.display_file_selector = False + op.load_ui = False + else: layout.operator("wm.path_open", text=f"Open {active_file.name}").filepath = str(Path(active_file.absolute_path)) @@ -53,12 +46,11 @@ def svn_log_list_context_menu(self: UIList, context: Context) -> None: if not check_context_match(context, 'ui_list', 'SVN_UL_log'): return - is_filebrowser = context.space_data.type == 'FILE_BROWSER' layout = self.layout layout.separator() repo = context.scene.svn.get_repo(context) - active_log = repo.active_log_filebrowser if is_filebrowser else repo.active_log + active_log = repo.active_log layout.operator("svn.update_all", text=f"Revert Repository To r{active_log.revision_number}").revision = active_log.revision_number layout.separator() @@ -73,5 +65,3 @@ def unregister(): bpy.types.UI_MT_list_item_context_menu.remove(svn_file_list_context_menu) bpy.types.UI_MT_list_item_context_menu.remove(svn_log_list_context_menu) - -registry = [SVN_OT_open_blend_file] diff --git a/scripts-blender/addons/blender_svn/ui/ui_file_list.py b/scripts-blender/addons/blender_svn/ui/ui_file_list.py index b6c460cb..2ff6c705 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_file_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_file_list.py @@ -114,6 +114,7 @@ class SVN_UL_file_list(UIList): explainer.status = status explainer.file_rel_path = file_entry.svn_path + @classmethod def cls_filter_items(cls, context, data, propname): """By moving all of this logic to a classmethod (and all the filter @@ -148,8 +149,10 @@ class SVN_UL_file_list(UIList): row.prop(self, 'show_file_paths', text="", toggle=True, icon="FILE_FOLDER") - row.prop(context.scene.svn.get_repo(context), - 'file_search_filter', text="") + + repo = context.scene.svn.get_repo(context) + if repo: + row.prop(repo, 'file_search_filter', text="") def draw_process_info(context, layout): @@ -183,18 +186,19 @@ def draw_process_info(context, layout): ", ".join([p.name for p in Processes.running_processes])) -def draw_repo_file_list(context, layout, repo): +def draw_file_list(context, layout): + prefs = get_addon_prefs(context) + repo = prefs.active_repo if not repo: return + if not repo.authenticated: + row = layout.row() + row.alert=True + row.label(text="Repository is not authenticated.", icon='ERROR') + return + main_col = layout.column() - main_col.enabled = False - status_proc = Processes.get('Status') - time_since_last_update = 1000 - if status_proc: - time_since_last_update = time.time() - status_proc.timestamp_last_update - if time_since_last_update < 30: - main_col.enabled = True main_row = main_col.row() split = main_row.split(factor=0.6) filepath_row = split.row() @@ -207,11 +211,6 @@ def draw_repo_file_list(context, layout, repo): ops_row.alignment = 'RIGHT' ops_row.label(text="Operations") - timer_row = main_row.row() - timer_row.alignment = 'RIGHT' - timer_row.operator("svn.custom_tooltip", icon='BLANK1', text="", - emboss=False).tooltip = "Time since last file status update: " + str(time_since_last_update) + 's' - row = main_col.row() row.template_list( "SVN_UL_file_list", diff --git a/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py b/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py index 7b2a5a25..15dabef5 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py +++ b/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py @@ -4,11 +4,10 @@ from bpy.types import Panel from bl_ui.space_filebrowser import FileBrowserPanel -from .ui_log import draw_svn_log -from .ui_file_list import draw_repo_file_list +from .ui_log import draw_svn_log, is_log_useful +from .ui_file_list import draw_file_list from ..util import get_addon_prefs - class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOLS' @@ -20,47 +19,37 @@ class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel): if not super().poll(context): return False - repo = context.scene.svn.get_repo(context) - if not repo: - return False - - return repo.is_filebrowser_directory_in_repo(context) + prefs = get_addon_prefs(context) + return prefs.active_repo and prefs.active_repo.authenticated def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False - # TODO: Get repository of the current file browser's directory. - prefs = get_addon_prefs(context) - if len(prefs.repositories) > 0: - repo = prefs.active_repo - draw_repo_file_list(context, layout, repo) + draw_file_list(context, layout) class FILEBROWSER_PT_SVN_log(FileBrowserPanel, Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOLS' bl_category = "Bookmarks" - bl_label = "SVN Log" + bl_parent_id = "FILEBROWSER_PT_SVN_files" + bl_label = "Revision History" @classmethod def poll(cls, context): if not super().poll(context): return False - repo = context.scene.svn.get_repo(context) - if not repo: - return False - - return repo.get_filebrowser_active_file(context) + return is_log_useful(context) def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False - draw_svn_log(context, layout, file_browser=True) + draw_svn_log(context, layout) registry = [ diff --git a/scripts-blender/addons/blender_svn/ui/ui_log.py b/scripts-blender/addons/blender_svn/ui/ui_log.py index c6d8d64e..ea6a10b0 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_log.py +++ b/scripts-blender/addons/blender_svn/ui/ui_log.py @@ -3,6 +3,7 @@ from bpy.props import IntProperty, BoolProperty from bpy.types import UIList, Panel, Operator +from ..util import get_addon_prefs class SVN_UL_log(UIList): @@ -21,9 +22,7 @@ class SVN_UL_log(UIList): num, auth, date, msg = layout_log_split(layout.row()) - is_filebrowser = context.space_data.type == 'FILE_BROWSER' - active_file = svn.get_filebrowser_active_file( - context) if is_filebrowser else svn.active_file + active_file = svn.active_file num.label(text=str(log_entry.revision_number)) if item.revision_number == active_file.revision: num.operator('svn.tooltip_log', text="", icon='LAYER_ACTIVE', @@ -88,8 +87,15 @@ class SVN_UL_log(UIList): toggle=True, icon='ALIGN_JUSTIFY') -def is_log_useful(context): - repo = context.scene.svn.get_repo(context) +def is_log_useful(context) -> bool: + """Return whether the log has any useful info to display.""" + + prefs = get_addon_prefs(context) + repo = prefs.active_repo + + if not repo or not repo.authenticated: + return False + if len(repo.log) == 0 or len(repo.external_files) == 0: return False active_file = repo.active_file @@ -121,7 +127,7 @@ class VIEW3D_PT_svn_log(Panel): layout.use_property_split = True layout.use_property_decorate = False - draw_svn_log(context, layout, file_browser=False) + draw_svn_log(context, layout) def layout_log_split(layout): @@ -140,23 +146,25 @@ def layout_log_split(layout): return num, auth, date, msg -def draw_svn_log(context, layout, file_browser: bool): +def draw_svn_log(context, layout): num, auth, date, msg = layout_log_split(layout.row()) num.label(text="Rev. #") auth.label(text="Author") date.label(text="Date") msg.label(text="Message") - repo = context.scene.svn.get_repo(context) + + prefs = get_addon_prefs(context) + repo = prefs.active_repo layout.template_list( "SVN_UL_log", "svn_log", repo, "log", repo, - "log_active_index_filebrowser" if file_browser else "log_active_index", + "log_active_index", ) - active_log = repo.active_log_filebrowser if file_browser else repo.active_log + active_log = repo.active_log if not active_log: return layout.label(text="Revision Date: " + active_log.revision_date) @@ -187,10 +195,7 @@ def execute_tooltip_log(self, context): repo = context.scene.svn.get_repo(context) tup = repo.get_log_by_revision(self.log_rev) if tup: - if context.area.type == 'FILE_BROWSER': - repo.log_active_index_filebrowser = tup[0] - else: - repo.log_active_index = tup[0] + repo.log_active_index = tup[0] return {'FINISHED'} diff --git a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py index 5b4a6cce..a57b3cea 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py +++ b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py @@ -17,9 +17,6 @@ def draw_outdated_file_warning(self, context): # If the current file is not in an SVN repository. return - if current_file.status == 'normal' and current_file.repos_status == 'none': - return - layout = self.layout row = layout.row() row.alert = True @@ -27,15 +24,14 @@ def draw_outdated_file_warning(self, context): if current_file.status == 'conflicted': row.operator('svn.resolve_conflict', text="SVN: This .blend file is conflicted.", icon='ERROR') - elif current_file.repos_status != 'none': - warning = row.operator( - 'svn.custom_tooltip', text="SVN: This .blend file is outdated.", icon='ERROR') - warning.tooltip = "The currently opened .blend file has a newer version available on the remote repository. This means any changes in this file will result in a conflict, and potential loss of data. See the SVN panel for info" + elif current_file.repos_status != 'none' or context.scene.svn.file_is_outdated: + op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file may be outdated.", icon='ERROR') + op.file_rel_path = repo.current_blend_file.svn_path def register(): - bpy.types.VIEW3D_HT_header.prepend(draw_outdated_file_warning) + bpy.types.TOPBAR_MT_editor_menus.append(draw_outdated_file_warning) def unregister(): - bpy.types.VIEW3D_HT_header.remove(draw_outdated_file_warning) + bpy.types.TOPBAR_MT_editor_menus.remove(draw_outdated_file_warning) diff --git a/scripts-blender/addons/blender_svn/ui/ui_prefs.py b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py similarity index 76% rename from scripts-blender/addons/blender_svn/ui/ui_prefs.py rename to scripts-blender/addons/blender_svn/ui/ui_repo_list.py index bee70ed8..d11ce989 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_prefs.py +++ b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py @@ -1,26 +1,25 @@ # SPDX-License-Identifier: GPL-2.0-or-later # (c) 2023, Blender Foundation - Demeter Dzadik -from pathlib import Path +import platform from bpy.types import UIList, Operator, Menu from bpy_extras.io_utils import ImportHelper from ..util import get_addon_prefs from .ui_log import draw_svn_log, is_log_useful -from .ui_file_list import draw_repo_file_list, draw_process_info +from .ui_file_list import draw_file_list, draw_process_info from ..threaded.background_process import Processes -import platform +from pathlib import Path class SVN_UL_repositories(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, context, layout, data, item, icon, active_data, active_propname + ): repo = item row = layout.row() - prefs = get_addon_prefs(context) - if prefs.active_repo_mode == 'CURRENT_BLEND' and repo != context.scene.svn.get_repo(context): - row.enabled = False row.label(text=repo.display_name) if not repo.dir_exists: @@ -30,6 +29,7 @@ class SVN_UL_repositories(UIList): class SVN_OT_repo_add(Operator, ImportHelper): """Add a repository to the list""" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_idname = "svn.repo_add" @@ -40,7 +40,16 @@ class SVN_OT_repo_add(Operator, ImportHelper): repos = prefs.repositories path = Path(self.filepath) + if not path.exists(): + # It's unlikely that a path that the user JUST BROWSED doesn't exist. + # So, this actually happens when the user leaves a filename in the + # file browser text box while trying to select the folder... + # Basically, Blender is dumb, and it will add that filename to the + # end of the browsed path. We need to discard that. + path = path.parent if path.is_file(): + # Maybe the user actually did select an existing file in the repo. + # We still want to discard the filename. path = path.parent existing_repos = repos[:] @@ -48,7 +57,9 @@ class SVN_OT_repo_add(Operator, ImportHelper): repo = prefs.init_repo(context, path) except Exception as e: self.report( - {'ERROR'}, "Failed to initialize repository. Ensure you have SVN installed, and that the selected directory is the root of a repository.") + {'ERROR'}, + "Failed to initialize repository. Ensure you have SVN installed, and that the selected directory is the root of a repository.", + ) print(e) return {'CANCELLED'} if not repo: @@ -65,6 +76,7 @@ class SVN_OT_repo_add(Operator, ImportHelper): class SVN_OT_repo_remove(Operator): """Remove a repository from the list""" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_idname = "svn.repo_remove" @@ -93,19 +105,98 @@ class SVN_MT_add_repo(Menu): def draw(self, context): layout = self.layout layout.operator( - "svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER') - layout.operator("svn.checkout_initiate", - text="Create New Checkout", icon='URL').create = True + "svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER' + ) + layout.operator( + "svn.checkout_initiate", text="Create New Checkout", icon='URL' + ).create = True -def draw_prefs(self, context): - if self.checkout_mode: - draw_prefs_checkout(self, context) - else: - draw_prefs_repos(self, context) +def draw_repo_list(self, context) -> None: + layout = self.layout + + auth_in_progress = False + auth_error = False + auth_proc = Processes.get('Authenticate') + if auth_proc: + auth_in_progress = auth_proc.is_running + auth_error = auth_proc.error + + repo_col = layout.column() + split = repo_col.row().split() + split.row().label(text="SVN Repositories:") + + # Secret debug toggle (invisible, to the right of the SVN Repositories label.) + row = split.row() + row.alignment = 'RIGHT' + row.prop(self, 'debug_mode', text="", icon='BLANK1', emboss=False) + + repo_col.enabled = not auth_in_progress + + list_row = repo_col.row() + col = list_row.column() + col.template_list( + "SVN_UL_repositories", + "svn_repo_list", + self, + "repositories", + self, + "active_repo_idx", + ) + + op_col = list_row.column() + op_col.menu('SVN_MT_add_repo', icon='ADD', text="") + op_col.operator('svn.repo_remove', icon='REMOVE', text="") + + if len(self.repositories) == 0: + return + if self.active_repo_idx - 1 > len(self.repositories): + return + if not self.active_repo: + return + + repo_col.prop(self.active_repo, 'display_name', icon='FILE_TEXT') + repo_col.prop(self.active_repo, 'url', icon='URL') + repo_col.prop(self.active_repo, 'username', icon='USER') + repo_col.prop(self.active_repo, 'password', icon='LOCKED') + + draw_process_info(context, layout.row()) + + if not self.active_repo.dir_exists: + draw_repo_error(layout, "Repository not found on file system.") + return + if not self.active_repo.is_valid_svn: + draw_repo_error(layout, "Directory is not an SVN repository.") + split = layout.split(factor=0.24) + split.row() + split.row().operator( + "svn.checkout_initiate", text="Create New Checkout", icon='URL' + ).create = False + return + if not self.active_repo.authenticated and not auth_in_progress and not auth_error: + draw_repo_error(layout, "Repository not authenticated. Enter your credentials.") + return + + if len(self.repositories) > 0 and self.active_repo.authenticated: + layout.separator() + layout.label(text="SVN Files: ") + draw_file_list(context, layout) + + if is_log_useful(context): + layout.separator() + layout.label(text="Revision History: ") + draw_svn_log(context, layout) -def draw_prefs_checkout(self, context): +def draw_repo_error(layout, message): + split = layout.split(factor=0.24) + split.row() + col = split.column() + col.alert = True + col.label(text=message, icon='ERROR') + + +def draw_checkout(self, context): def get_terminal_howto(): msg_windows = "If you don't, cancel this operation and toggle it using Window->Toggle System Console." msg_linux = "If you don't, quit Blender and re-launch it from a terminal." @@ -128,16 +219,22 @@ def draw_prefs_checkout(self, context): col.label(text=get_terminal_howto()) col.separator() col.label( - text="Downloading a repository can take a long time, and the UI will be locked.") + text="Downloading a repository can take a long time, and the UI will be locked." + ) col.label( - text="Without a terminal, you won't be able to track the progress of the checkout.") + text="Without a terminal, you won't be able to track the progress of the checkout." + ) col.separator() col = layout.column() col.label( - text="To interrupt the checkout, you can press Ctrl+C in the terminal.", icon='INFO') + text="To interrupt the checkout, you can press Ctrl+C in the terminal.", + icon='INFO', + ) col.label( - text="You can resume it by re-running this operation, or with the SVN Update button.", icon='INFO') + text="You can resume it by re-running this operation, or with the SVN Update button.", + icon='INFO', + ) col.separator() prefs = get_addon_prefs(context) @@ -150,7 +247,8 @@ def draw_prefs_checkout(self, context): row = col.row() row.alert = True row.label( - text="A repository at this filepath is already specified.", icon='ERROR') + text="A repository at this filepath is already specified.", icon='ERROR' + ) break col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER') @@ -163,7 +261,8 @@ def draw_prefs_checkout(self, context): sub.alert = True sub.label(text="A repository with this URL is already specified.") sub.label( - text="If you're sure you want to checkout another copy of the repo, feel free to proceed.") + text="If you're sure you want to checkout another copy of the repo, feel free to proceed." + ) break col.prop(repo, 'username', icon='USER') col.prop(repo, 'password', icon='LOCKED') @@ -173,101 +272,4 @@ def draw_prefs_checkout(self, context): op_row.operator('svn.checkout_cancel', text="Cancel", icon="X") -def draw_prefs_repos(self, context) -> None: - layout = self.layout - - row = layout.row() - row.use_property_split = True - row.prop(self, 'active_repo_mode', expand=True) - - auth_in_progress = False - auth_error = False - auth_proc = Processes.get('Authenticate') - if auth_proc: - auth_in_progress = auth_proc.is_running - auth_error = auth_proc.error - - if self.active_repo_mode == 'CURRENT_BLEND' and not context.scene.svn.get_repo(context): - split = layout.split(factor=0.4) - split.row() - split.row().label(text="Current file is not in a repository.") - return - - repo_col = layout.column() - split = repo_col.row().split() - split.row().label(text="SVN Repositories:") - row = split.row() - row.alignment = 'RIGHT' - row.prop(self, 'debug_mode') - - repo_col.enabled = not auth_in_progress - - list_row = repo_col.row() - col = list_row.column() - col.template_list( - "SVN_UL_repositories", - "svn_repo_list", - self, - "repositories", - self, - "active_repo_idx", - ) - - op_col = list_row.column() - op_col.menu('SVN_MT_add_repo', icon='ADD', text="") - op_col.operator('svn.repo_remove', icon='REMOVE', text="") - - if len(self.repositories) == 0: - return - if self.active_repo_idx-1 > len(self.repositories): - return - if not self.active_repo: - return - - repo_col.prop(self.active_repo, 'display_name', icon='FILE_TEXT') - repo_col.prop(self.active_repo, 'url', icon='URL') - repo_col.prop(self.active_repo, 'username', icon='USER') - repo_col.prop(self.active_repo, 'password', icon='LOCKED') - - draw_process_info(context, layout.row()) - - if not self.active_repo.dir_exists: - draw_repo_error(layout, "Repository not found on file system.") - return - if not self.active_repo.is_valid_svn: - draw_repo_error(layout, "Directory is not an SVN repository.") - split = layout.split(factor=0.24) - split.row() - split.row().operator("svn.checkout_initiate", - text="Create New Checkout", icon='URL').create = False - return - if not self.active_repo.authenticated and not auth_in_progress and not auth_error: - draw_repo_error( - layout, "Repository not authenticated. Enter your credentials.") - return - - if len(self.repositories) > 0 and self.active_repo.authenticated: - layout.separator() - layout.label(text="Files: ") - draw_repo_file_list(context, layout, self.active_repo) - - if is_log_useful(context): - layout.separator() - layout.label(text="Log: ") - draw_svn_log(context, layout, file_browser=False) - - -def draw_repo_error(layout, message): - split = layout.split(factor=0.24) - split.row() - col = split.column() - col.alert = True - col.label(text=message, icon='ERROR') - - -registry = [ - SVN_UL_repositories, - SVN_OT_repo_add, - SVN_OT_repo_remove, - SVN_MT_add_repo -] +registry = [SVN_UL_repositories, SVN_OT_repo_add, SVN_OT_repo_remove, SVN_MT_add_repo] diff --git a/scripts-blender/addons/blender_svn/ui/ui_sidebar.py b/scripts-blender/addons/blender_svn/ui/ui_sidebar.py index 1bc81a38..a0e47123 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_sidebar.py +++ b/scripts-blender/addons/blender_svn/ui/ui_sidebar.py @@ -4,7 +4,7 @@ from bpy.types import Panel from ..util import get_addon_prefs -from .ui_file_list import draw_repo_file_list, draw_process_info +from .ui_file_list import draw_file_list, draw_process_info class VIEW3D_PT_svn_credentials(Panel): @@ -16,12 +16,7 @@ class VIEW3D_PT_svn_credentials(Panel): @classmethod def poll(cls, context): - prefs = get_addon_prefs(context) - if prefs.active_repo_mode == 'CURRENT_BLEND': - repo = context.scene.svn.get_scene_repo(context) - else: - repo = context.scene.svn.get_repo(context) - + repo = context.scene.svn.get_repo(context) return repo and not repo.authenticated def draw(self, context): @@ -58,9 +53,8 @@ class VIEW3D_PT_svn_files(Panel): layout.use_property_split = True layout.use_property_decorate = False - repo = context.scene.svn.get_repo(context) draw_process_info(context, layout) - draw_repo_file_list(context, layout, repo) + draw_file_list(context, layout) registry = [