2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
+23
View File
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from . import (
ui_file_list,
ui_sidebar,
ui_filebrowser,
ui_log,
ui_repo_list,
ui_outdated_warning,
ui_context_menus
)
modules = [
ui_file_list,
ui_sidebar,
ui_filebrowser,
ui_log,
ui_repo_list,
ui_outdated_warning,
ui_context_menus
]
@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from bpy.types import Context, UIList, Operator
from bpy.props import StringProperty, BoolProperty
from pathlib import Path
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.
"""
uilayout = getattr(context, uilayout_type, None)
return uilayout and uilayout.bl_idname == bl_idname
def svn_file_list_context_menu(self: UIList, context: Context) -> None:
if not check_context_match(context, 'ui_list', 'SVN_UL_file_list'):
return
layout = self.layout
layout.separator()
repo = context.scene.svn.get_repo(context)
active_file = repo.active_file
file_abs_path = repo.get_file_abspath(active_file)
if active_file.name.endswith("blend"):
op = layout.operator("wm.open_mainfile",
text=f"Open {active_file.name}")
op.filepath = str(file_abs_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 = str(file_abs_path)
op.display_file_selector = False
op.load_ui = False
else:
layout.operator("wm.path_open",
text=f"Open {active_file.name}").filepath = str(file_abs_path)
layout.operator("wm.path_open",
text=f"Open Containing Folder").filepath = Path(file_abs_path).parent.as_posix()
layout.separator()
def svn_log_list_context_menu(self: UIList, context: Context) -> None:
if not check_context_match(context, 'ui_list', 'SVN_UL_log'):
return
layout = self.layout
layout.separator()
repo = context.scene.svn.get_repo(context)
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()
def register():
bpy.types.UI_MT_list_item_context_menu.append(svn_file_list_context_menu)
bpy.types.UI_MT_list_item_context_menu.append(svn_log_list_context_menu)
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)
@@ -0,0 +1,233 @@
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from bpy.types import UIList
from bpy.props import BoolProperty
from .. import constants
from ..util import get_addon_prefs, dots
from ..threaded.background_process import Processes
class SVN_UL_file_list(UIList):
# Value that indicates that this item has passed the filter process successfully. See rna_ui.c.
UILST_FLT_ITEM = 1 << 30
show_file_paths: BoolProperty(
name="Show File Paths",
description="Show file paths relative to the SVN root, instead of just the file name"
)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
# As long as there are any items, always draw the filters.
self.use_filter_show = True
if self.layout_type != 'DEFAULT':
raise NotImplemented
file_entry = item
prefs = get_addon_prefs(context)
main_row = layout.row()
split = main_row.split(factor=0.6)
filepath_ui = split.row()
split = split.split(factor=0.4)
status_ui = split.row(align=True)
ops_ui = split.row(align=True)
ops_ui.alignment = 'RIGHT'
ops_ui.enabled = file_entry.status_prediction_type == 'NONE' and not prefs.is_busy
if self.show_file_paths:
filepath_ui.prop(file_entry, 'name', text="",
emboss=False, icon=file_entry.file_icon)
else:
filepath_ui.label(text=file_entry.file_name, icon=file_entry.file_icon)
statuses = [file_entry.status]
# SVN operations
ops = []
if file_entry.status in ['missing', 'deleted']:
ops.append(ops_ui.operator(
'svn.restore_file', text="", icon='LOOP_BACK'))
if file_entry.status == 'missing':
ops.append(ops_ui.operator(
'svn.remove_file', text="", icon='TRASH'))
elif file_entry.status == 'added':
ops.append(ops_ui.operator(
'svn.unadd_file', text="", icon='REMOVE'))
elif file_entry.status == 'unversioned':
ops.append(ops_ui.operator('svn.add_file', text="", icon='ADD'))
ops.append(ops_ui.operator(
'svn.trash_file', text="", icon='TRASH'))
elif file_entry.status == 'modified':
ops.append(ops_ui.operator(
'svn.revert_file', text="", icon='LOOP_BACK'))
if file_entry.repos_status == 'modified':
# The file isn't actually `conflicted` yet, by SVN's definition,
# but it will be as soon as we try to commit or update.
# I think it's better to let the user know in advance.
statuses.append('conflicted')
# Updating the file will create an actual conflict.
ops.append(ops_ui.operator(
'svn.update_single', text="", icon='IMPORT'))
elif file_entry.status == 'conflicted':
ops.append(ops_ui.operator('svn.resolve_conflict',
text="", icon='TRACKING_CLEAR_FORWARDS'))
elif file_entry.status in ['incomplete', 'obstructed']:
ops.append(ops_ui.operator(
'svn.cleanup', text="", icon='BRUSH_DATA'))
elif file_entry.status == 'none':
if file_entry.repos_status == 'added':
# From user POV it makes a bit more sense to call a file that doesn't
# exist yet "added" instead of "outdated".
statuses.append('added')
ops.append(ops_ui.operator(
'svn.update_single', text="", icon='IMPORT'))
elif file_entry.status == 'normal' and file_entry.repos_status == 'modified':
# From user POV, this file is outdated, not 'normal'.
statuses = ['none']
ops.append(ops_ui.operator(
'svn.update_single', text="", icon='IMPORT'))
elif file_entry.status in ['normal', 'external', 'ignored']:
pass
else:
print("Unknown file status: ", file_entry.svn_path,
file_entry.status, file_entry.repos_status)
for op in ops:
if hasattr(op, 'file_rel_path'):
op.file_rel_path = file_entry.svn_path
# Populate the status icons.
for status in statuses:
icon = constants.SVN_STATUS_DATA[status][0]
explainer = status_ui.operator(
'svn.explain_status', text="", icon=icon, emboss=False)
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
properties to the addon preferences) we can find a visible entry
from other UI code, allowing us to avoid situations where the active
element becomes hidden."""
flt_neworder = []
list_items = getattr(data, propname)
flt_flags = [file.show_in_filelist *
cls.UILST_FLT_ITEM for file in list_items]
helper_funcs = bpy.types.UI_UL_list
# This list should ALWAYS be sorted alphabetically.
flt_neworder = helper_funcs.sort_items_by_name(list_items, "name")
repo = context.scene.svn.get_repo(context)
if not repo:
return flt_flags, flt_neworder
return flt_flags, flt_neworder
def filter_items(self, context, data, propname):
return type(self).cls_filter_items(context, data, propname)
def draw_filter(self, context, layout):
"""Custom filtering UI.
Toggles are stored in addon preferences, see cls_filter_items().
"""
main_row = layout.row()
row = main_row.row(align=True)
row.prop(self, 'show_file_paths', text="",
toggle=True, icon="FILE_FOLDER")
repo = context.scene.svn.get_repo(context)
if repo:
row.prop(repo, 'file_search_filter', text="")
def draw_process_info(context, layout):
prefs = get_addon_prefs(context)
process_message = ""
any_error = False
col = layout.column()
for process in Processes.processes.values():
if process.name not in {'Commit', 'Update', 'Log', 'Status', 'Authenticate'}:
continue
if process.error:
row = col.row()
row.alert = True
warning = row.operator(
'svn.clear_error', text=f"SVN {process.name}: Error Occurred. Hover to view", icon='ERROR')
warning.process_id = process.name
any_error = True
break
if process.is_running:
message = process.get_ui_message(context)
if message:
message = message.replace("...", dots())
process_message = f"SVN: {message}"
if not any_error and process_message:
col.label(text=process_message)
if prefs.debug_mode:
col.label(text="Processes: " +
", ".join([p.name for p in Processes.running_processes]))
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_row = main_col.row()
split = main_row.split(factor=0.6)
filepath_row = split.row()
filepath_row.label(text=" Filepath")
status_row = split.row()
status_row.label(text=" Status")
ops_row = main_row.row()
ops_row.alignment = 'RIGHT'
ops_row.label(text="Operations")
row = main_col.row()
row.template_list(
"SVN_UL_file_list",
"svn_file_list",
repo,
"external_files",
repo,
"external_files_active_index",
)
col = row.column()
col.separator()
col.operator("svn.commit", icon='EXPORT', text="")
col.operator("svn.update_all", icon='IMPORT', text="").revision = 0
col.separator()
col.operator("svn.cleanup", icon='BRUSH_DATA', text="")
registry = [
SVN_UL_file_list,
]
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from bpy.types import Panel
from bl_ui.space_filebrowser import FileBrowserPanel
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'
bl_category = "Bookmarks"
bl_label = "SVN Files"
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
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
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_parent_id = "FILEBROWSER_PT_SVN_files"
bl_label = "Revision History"
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
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)
registry = [
FILEBROWSER_PT_SVN_files,
FILEBROWSER_PT_SVN_log
]
+279
View File
@@ -0,0 +1,279 @@
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from bpy.props import IntProperty, BoolProperty
from bpy.types import UIList, Panel, Operator
from ..util import get_addon_prefs
class SVN_UL_log(UIList):
show_all_logs: BoolProperty(
name='Show All Logs',
description='Show the complete SVN Log, instead of only entries that affected the currently selected file',
default=False
)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if self.layout_type != 'DEFAULT':
raise NotImplemented
svn = data
log_entry = item
num, auth, date, msg = layout_log_split(layout.row())
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',
emboss=False).log_rev = log_entry.revision_number
elif log_entry.changes_file(active_file):
get_older = num.operator(
'svn.download_file_revision', text="", icon='IMPORT', emboss=False)
get_older.revision = log_entry.revision_number
get_older.file_rel_path = active_file.svn_path
auth.label(text=log_entry.revision_author)
date.label(text=log_entry.revision_date_simple)
commit_msg = log_entry.commit_message
commit_msg = commit_msg.split(
"\n")[0] if "\n" in commit_msg else commit_msg
commit_msg = commit_msg[:50] + \
"..." if len(commit_msg) > 52 else commit_msg
msg.alignment = 'LEFT'
msg.operator("svn.display_commit_message", text=commit_msg,
emboss=False).log_rev = log_entry.revision_number
def filter_items(self, context, data, propname):
"""Custom filtering functionality:
- Always sort by descending revision number
- Allow searching for various criteria
"""
svn = data
log_entries = getattr(data, propname)
# Start off with all entries flagged as visible.
flt_flags = [self.bitflag_filter_item] * len(log_entries)
# Always sort by descending revision number
flt_neworder = sorted(range(len(log_entries)),
key=lambda i: log_entries[i].revision_number)
flt_neworder.reverse()
if not self.show_all_logs:
flt_flags = [
log_entry.affects_active_file * self.bitflag_filter_item
for log_entry in log_entries
]
if self.filter_name:
# Filtering: Allow comma-separated keywords.
# ALL keywords must be found somewhere in the log entry for it to show up.
filter_words = [word.strip().lower()
for word in self.filter_name.split(",")]
for idx, log_entry in enumerate(log_entries):
for filter_word in filter_words:
if filter_word not in log_entry.text_to_search:
flt_flags[idx] = 0
break
return flt_flags, flt_neworder
def draw_filter(self, context, layout):
"""Custom filtering UI.
"""
main_row = layout.row()
main_row.prop(self, 'filter_name', text="")
main_row.prop(self, 'show_all_logs', text="",
toggle=True, icon='ALIGN_JUSTIFY')
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
if active_file.status in ['unversioned', 'added']:
return False
any_visible = any([file.show_in_filelist for file in repo.external_files])
if not any_visible:
return False
return True
class VIEW3D_PT_svn_log(Panel):
"""Display the revision history of the selected file."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'SVN'
bl_label = 'Revision History'
bl_parent_id = "VIEW3D_PT_svn_files"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, 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)
def layout_log_split(layout):
main = layout.split(factor=0.4)
num_and_auth = main.row()
date_and_msg = main.row()
num_and_auth_split = num_and_auth.split(factor=0.5)
num = num_and_auth_split.row()
auth = num_and_auth_split.row()
date_and_msg_split = date_and_msg.split(factor=0.3)
date = date_and_msg_split.row()
msg = date_and_msg_split.row()
return num, auth, date, msg
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")
prefs = get_addon_prefs(context)
repo = prefs.active_repo
layout.template_list(
"SVN_UL_log",
"svn_log",
repo,
"log",
repo,
"log_active_index",
)
active_log = repo.active_log
if not active_log:
return
layout.label(text="Revision Date: " + active_log.revision_date)
layout.label(
text=f"Files changed in revision `r{active_log.revision_number}`:")
col = layout.column(align=True)
row = col.row()
split = row.split(factor=0.80)
split.label(text=" Filepath")
row = split.row()
row.alignment = 'RIGHT'
row.label(text="Action")
for f in active_log.changed_files:
row = col.row()
split = row.split(factor=0.90)
split.prop(f, 'name', emboss=False, text="", icon=f.file_icon)
row = split.row()
row.alignment = 'RIGHT'
row.operator('svn.explain_status', text="",
icon=f.status_icon, emboss=False).status = f.status
def execute_tooltip_log(self, context):
"""Set the index on click, to act as if this operator button was
click-through in the UIList."""
repo = context.scene.svn.get_repo(context)
tup = repo.get_log_by_revision(self.log_rev)
if tup:
repo.log_active_index = tup[0]
return {'FINISHED'}
class SVN_OT_log_tooltip(Operator):
bl_idname = "svn.tooltip_log"
bl_label = "" # Don't want the first line of the tooltip on mouse hover.
# bl_description = "An operator to be drawn in the log list, that can display a dynamic tooltip"
bl_options = {'INTERNAL'}
log_rev: IntProperty(
description="Revision number of the log entry to show in the tooltip"
)
@classmethod
def description(cls, context, properties):
return "This is the currently checked out version of the file"
execute = execute_tooltip_log
class SVN_OT_log_show_commit_msg(Operator):
bl_idname = "svn.display_commit_message"
bl_label = "" # Don't want the first line of the tooltip on mouse hover.
# bl_description = "Show the currently active commit, using a dynamic tooltip"
bl_options = {'INTERNAL'}
log_rev: IntProperty(
description="Revision number of the log entry to show in the tooltip"
)
@classmethod
def description(cls, context, properties):
log_entry = context.scene.svn.get_repo(context).get_log_by_revision(properties.log_rev)[
1]
commit_msg = log_entry.commit_message
# Prettify the tooltips.
pretty_msg = ""
for line in commit_msg.split("\n"):
# Remove leading/trailing whitespace
line = line.strip()
# Add punctuation mark
if not (line.endswith(".") or line.endswith("!") or line.endswith("?")):
line = line + "."
# Split long lines into several
limit = 300
if len(line) > limit:
words = line.split(" ")
sub_lines = []
new_line = ""
for word in words:
if len(new_line) + len(word) < limit:
new_line += " "+word
else:
sub_lines.append(new_line)
new_line = word
else:
sub_lines.append(new_line)
line = "\n".join(sub_lines)
pretty_msg += "\n"+line
# Remove last period because Blender adds it.
if pretty_msg.endswith("."):
pretty_msg = pretty_msg[:-1]
return pretty_msg
execute = execute_tooltip_log
registry = [
VIEW3D_PT_svn_log,
SVN_UL_log,
SVN_OT_log_tooltip,
SVN_OT_log_show_commit_msg
]
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
def draw_outdated_file_warning(self, context):
repo = context.scene.svn.get_repo(context)
current_file = None
if not repo:
return
try:
current_file = repo.current_blend_file
except ValueError:
# This can happen if the svn_directory property wasn't updated yet (not enough time has passed since opening the file)
pass
if not current_file:
# If the current file is not in an SVN repository.
return
layout = self.layout
row = layout.row()
row.alert = True
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' 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.TOPBAR_MT_editor_menus.append(draw_outdated_file_warning)
def unregister():
bpy.types.TOPBAR_MT_editor_menus.remove(draw_outdated_file_warning)
@@ -0,0 +1,276 @@
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
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_file_list, draw_process_info
from ..threaded.background_process import Processes
from pathlib import Path
class SVN_UL_repositories(UIList):
def draw_item(
self, context, layout, data, item, icon, active_data, active_propname
):
repo = item
row = layout.row()
row.label(text=repo.display_name)
if not repo.dir_exists:
row.alert = True
row.prop(repo, 'directory', text="")
class SVN_OT_repo_add(Operator, ImportHelper):
"""Add a repository to the list"""
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
bl_idname = "svn.repo_add"
bl_label = "Add Repository"
def execute(self, context):
prefs = get_addon_prefs(context)
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[:]
try:
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.",
)
print(e)
return {'CANCELLED'}
if not repo:
self.report({'ERROR'}, "Failed to initialize repository.")
return {'CANCELLED'}
if repo in existing_repos:
self.report({'INFO'}, "Repository already present.")
else:
self.report({'INFO'}, "Repository added.")
prefs.active_repo_idx = repos.find(repo.directory)
prefs.save_repo_info_to_file()
return {'FINISHED'}
class SVN_OT_repo_remove(Operator):
"""Remove a repository from the list"""
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
bl_idname = "svn.repo_remove"
bl_label = "Remove Repository"
@classmethod
def poll(cls, context):
return len(get_addon_prefs(context).repositories) > 0
def execute(self, context):
prefs = get_addon_prefs(context)
active_index = prefs.active_repo_idx
repos = prefs.repositories
prefs.repositories.remove(prefs.active_repo_idx)
to_index = min(active_index, len(repos) - 1)
prefs.active_repo_idx = to_index
prefs.save_repo_info_to_file()
return {'FINISHED'}
class SVN_MT_add_repo(Menu):
bl_idname = "SVN_MT_add_repo"
bl_label = "Add Repo"
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
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_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."
msg_mac = msg_linux
system = platform.system()
if system == "Windows":
return msg_windows
elif system == "Linux":
return msg_linux
elif system == "Darwin":
return msg_mac
layout = self.layout
col = layout.column()
col.alert = True
col.label(text="IMPORTANT! ", icon='ERROR')
col.label(text="Make sure you have Blender's terminal open!")
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."
)
col.label(
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',
)
col.label(
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)
repo = prefs.repositories[-1]
col.prop(repo, 'directory')
for other_repo in prefs.repositories:
if other_repo == repo:
continue
if other_repo.directory == repo.directory:
row = col.row()
row.alert = True
row.label(
text="A repository at this filepath is already specified.", icon='ERROR'
)
break
col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER')
col.prop(repo, 'url', icon='URL')
for other_repo in prefs.repositories:
if other_repo == repo:
continue
if other_repo.url == repo.url:
sub = col.column()
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."
)
break
col.prop(repo, 'username', icon='USER')
col.prop(repo, 'password', icon='LOCKED')
op_row = layout.row()
op_row.operator('svn.checkout_finalize', text="Checkout", icon='CHECKMARK')
op_row.operator('svn.checkout_cancel', text="Cancel", icon="X")
registry = [SVN_UL_repositories, SVN_OT_repo_add, SVN_OT_repo_remove, SVN_MT_add_repo]
@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from bpy.types import Panel
from .ui_file_list import draw_file_list, draw_process_info
class VIEW3D_PT_svn_credentials(Panel):
"""Prompt the user to enter their username and password for the remote repository of the current .blend file."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'SVN'
bl_label = 'SVN Credentials'
@classmethod
def poll(cls, context):
repo = context.scene.svn.get_repo(context)
return repo and not repo.authenticated
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
col = layout.column(align=True)
repo = context.scene.svn.get_repo(context)
row = col.row()
row.prop(repo, 'display_name', text="Repo Name", icon='FILE_TEXT')
url = row.operator('svn.custom_tooltip', text="", icon='URL')
url.tooltip = repo.url
url.copy_on_click = True
col.prop(repo, 'username', icon='USER')
col.prop(repo, 'password', icon='UNLOCKED')
draw_process_info(context, layout)
class VIEW3D_PT_svn_files(Panel):
"""Display a list of files in the SVN repository of the current .blend file."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'SVN'
bl_label = 'SVN Files'
@classmethod
def poll(cls, context):
repo = context.scene.svn.get_repo(context)
return repo and repo.authenticated
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
draw_process_info(context, layout)
draw_file_list(context, layout)
registry = [
VIEW3D_PT_svn_credentials,
VIEW3D_PT_svn_files,
]