diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 7723436..e0f80a8 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -85,6 +85,7 @@ def register(): type=preferences.SessionUser ) bpy.types.WindowManager.user_index = bpy.props.IntProperty() + bpy.types.WindowManager.server_index = bpy.props.IntProperty() bpy.types.TOPBAR_MT_file_import.append(operators.menu_func_import) @@ -111,5 +112,6 @@ def unregister(): del bpy.types.ID.uuid del bpy.types.WindowManager.online_users del bpy.types.WindowManager.user_index + del bpy.types.WindowManager.server_index environment.unregister() diff --git a/multi_user/operators.py b/multi_user/operators.py index c638967..bb21f7b 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -28,6 +28,7 @@ import string import sys import time import traceback +from uuid import uuid4 from datetime import datetime from operator import itemgetter from pathlib import Path @@ -146,12 +147,123 @@ def on_connection_end(reason="none"): # OPERATORS -class SessionStartOperator(bpy.types.Operator): - bl_idname = "session.start" - bl_label = "start" +class SessionConnectOperator(bpy.types.Operator): + bl_idname = "session.connect" + bl_label = "connect" bl_description = "connect to a net server" - host: bpy.props.BoolProperty(default=False) + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + global deleyables + + settings = utils.get_preferences() + runtime_settings = context.window_manager.session + users = bpy.data.window_managers['WinMan'].online_users + admin_pass = settings.admin_password + server_pass = settings.server_password if settings.server_password else None + + users.clear() + deleyables.clear() + + logger = logging.getLogger() + if len(logger.handlers) == 1: + formatter = logging.Formatter( + fmt='%(asctime)s CLIENT %(levelname)-8s %(message)s', + datefmt='%H:%M:%S' + ) + + start_time = datetime.now().strftime('%Y_%m_%d_%H-%M-%S') + log_directory = os.path.join( + settings.cache_directory, + f"multiuser_{start_time}.log") + + os.makedirs(settings.cache_directory, exist_ok=True) + + handler = logging.FileHandler(log_directory, mode='w') + logger.addHandler(handler) + + for handler in logger.handlers: + if isinstance(handler, logging.NullHandler): + continue + + handler.setFormatter(formatter) + + bpy_protocol = bl_types.get_data_translation_protocol() + + # Check if supported_datablocks are up to date before starting the + # the session + for dcc_type_id in bpy_protocol.implementations.keys(): + if dcc_type_id not in settings.supported_datablocks: + logging.info(f"{dcc_type_id} not found, \ + regenerate type settings...") + settings.generate_supported_types() + + + if bpy.app.version[1] >= 91: + python_binary_path = sys.executable + else: + python_binary_path = bpy.app.binary_path_python + + repo = Repository( + rdp=bpy_protocol, + username=settings.username) + + # Join a session + if not runtime_settings.admin: + utils.clean_scene() + # regular session, no admin_password needed nor server_password + admin_pass = None + server_pass = None + + try: + porcelain.remote_add( + repo, + 'origin', + settings.ip, + settings.port, + server_password=server_pass, + admin_password=admin_pass) + session.connect( + repository= repo, + timeout=settings.connection_timeout, + server_password=server_pass, + admin_password=admin_pass + ) + except Exception as e: + self.report({'ERROR'}, str(e)) + logging.error(str(e)) + + # Background client updates service + deleyables.append(timers.ClientUpdate()) + deleyables.append(timers.DynamicRightSelectTimer()) + deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate)) + + session_update = timers.SessionStatusUpdate() + session_user_sync = timers.SessionUserSync() + session_background_executor = timers.MainThreadExecutor(execution_queue=background_execution_queue) + session_listen = timers.SessionListenTimer(timeout=0.001) + + session_listen.register() + session_update.register() + session_user_sync.register() + session_background_executor.register() + + deleyables.append(session_background_executor) + deleyables.append(session_update) + deleyables.append(session_user_sync) + deleyables.append(session_listen) + deleyables.append(timers.AnnotationUpdates()) + + return {"FINISHED"} + + +class SessionHostOperator(bpy.types.Operator): + bl_idname = "session.host" + bl_label = "host" + bl_description = "host server" @classmethod def poll(cls, context): @@ -213,64 +325,38 @@ class SessionStartOperator(bpy.types.Operator): username=settings.username) # Host a session - if self.host: - if settings.init_method == 'EMPTY': - utils.clean_scene() + if settings.init_method == 'EMPTY': + utils.clean_scene() - runtime_settings.is_host = True - runtime_settings.internet_ip = environment.get_ip() + runtime_settings.is_host = True + runtime_settings.internet_ip = environment.get_ip() - try: - # Init repository - for scene in bpy.data.scenes: - porcelain.add(repo, scene) + try: + # Init repository + for scene in bpy.data.scenes: + porcelain.add(repo, scene) - porcelain.remote_add( - repo, - 'origin', - '127.0.0.1', - settings.port, - server_password=server_pass, - admin_password=admin_pass) - session.host( - repository= repo, - remote='origin', - timeout=settings.connection_timeout, - server_password=server_pass, - admin_password=admin_pass, - cache_directory=settings.cache_directory, - server_log_level=logging.getLevelName( - logging.getLogger().level), - ) - except Exception as e: - self.report({'ERROR'}, repr(e)) - logging.error(f"Error: {e}") - traceback.print_exc() - # Join a session - else: - if not runtime_settings.admin: - utils.clean_scene() - # regular session, no admin_password needed nor server_password - admin_pass = None - server_pass = None - - try: - porcelain.remote_add( - repo, - 'origin', - settings.ip, - settings.port, - server_password=server_pass, - admin_password=admin_pass) - session.connect( - repository= repo, - timeout=settings.connection_timeout, - server_password=server_pass, - admin_password=admin_pass - ) - except Exception as e: - self.report({'ERROR'}, str(e)) - logging.error(str(e)) + porcelain.remote_add( + repo, + 'origin', + '127.0.0.1', + settings.port, + server_password=server_pass, + admin_password=admin_pass) + session.host( + repository= repo, + remote='origin', + timeout=settings.connection_timeout, + server_password=server_pass, + admin_password=admin_pass, + cache_directory=settings.cache_directory, + server_log_level=logging.getLevelName( + logging.getLogger().level), + ) + except Exception as e: + self.report({'ERROR'}, repr(e)) + logging.error(f"Error: {e}") + traceback.print_exc() # Background client updates service deleyables.append(timers.ClientUpdate()) @@ -838,27 +924,41 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): class SessionPresetServerAdd(bpy.types.Operator): """Add a server to the server list preset""" bl_idname = "session.preset_server_add" - bl_label = "add server preset" - bl_description = "add the current server to the server preset list" + bl_label = "Add server preset" + bl_description = "add a server to the server preset list" bl_options = {"REGISTER"} - name : bpy.props.StringProperty(default="server_preset") - + target_server_name: bpy.props.StringProperty(default="None") + @classmethod def poll(cls, context): return True def invoke(self, context, event): + settings = utils.get_preferences() + + settings.server_name = "" + settings.ip = "127.0.0.1" + settings.port = 5555 + settings.server_password = "" + settings.admin_password = "" + assert(context) return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout - - col = layout.column() settings = utils.get_preferences() - - col.prop(settings, "server_name", text="server name") + + row = layout.row() + row.prop(settings, "server_name", text="Server name") + row = layout.row(align = True) + row.prop(settings, "ip", text="IP+port") + row.prop(settings, "port", text="") + row = layout.row() + row.prop(settings, "server_password", text="Server password") + row = layout.row() + row.prop(settings, "admin_password", text="Admin password") def execute(self, context): assert(context) @@ -868,14 +968,13 @@ class SessionPresetServerAdd(bpy.types.Operator): existing_preset = settings.server_preset.get(settings.server_name) new_server = existing_preset if existing_preset else settings.server_preset.add() - new_server.name = settings.server_name + new_server.name = str(uuid4()) + new_server.server_name = settings.server_name new_server.server_ip = settings.ip new_server.server_port = settings.port new_server.server_server_password = settings.server_password new_server.server_admin_password = settings.admin_password - settings.server_preset_interface = settings.server_name - if new_server == existing_preset : self.report({'INFO'}, "Server '" + settings.server_name + "' override") else : @@ -884,6 +983,65 @@ class SessionPresetServerAdd(bpy.types.Operator): return {'FINISHED'} +class SessionPresetServerEdit(bpy.types.Operator): + """Edit a server to the server list preset""" + bl_idname = "session.preset_server_edit" + bl_label = "Edit server preset" + bl_description = "Edit a server from the server preset list" + bl_options = {"REGISTER"} + + target_server_name: bpy.props.StringProperty(default="None") + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + settings = utils.get_preferences() + settings_active_server = settings.server_preset.get(self.target_server_name) + + if settings_active_server : + settings.server_name = settings_active_server.server_name + settings.ip = settings_active_server.server_ip + settings.port = settings_active_server.server_port + settings.server_password = settings_active_server.server_server_password + settings.admin_password = settings_active_server.server_admin_password + + assert(context) + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + settings = utils.get_preferences() + + row = layout.row() + row.prop(settings, "server_name", text="Server name") + row = layout.row(align = True) + row.prop(settings, "ip", text="IP+port") + row.prop(settings, "port", text="") + row = layout.row() + row.prop(settings, "server_password", text="Server password") + row = layout.row() + row.prop(settings, "admin_password", text="Admin password") + + def execute(self, context): + assert(context) + + settings = utils.get_preferences() + settings_active_server = settings.server_preset.get(self.target_server_name) + + server = settings_active_server if settings_active_server else settings.server_preset.add() + server.server_name = settings.server_name + server.server_ip = settings.ip + server.server_port = settings.port + server.server_server_password = settings.server_password + server.server_admin_password = settings.admin_password + + self.report({'INFO'}, "Server '" + settings.server_name + "' override") + + return {'FINISHED'} + + class SessionPresetServerRemove(bpy.types.Operator): """Remove a server to the server list preset""" bl_idname = "session.preset_server_remove" @@ -891,6 +1049,8 @@ class SessionPresetServerRemove(bpy.types.Operator): bl_description = "remove the current server from the server preset list" bl_options = {"REGISTER"} + target_server_name: bpy.props.StringProperty(default="None") + @classmethod def poll(cls, context): return True @@ -899,8 +1059,7 @@ class SessionPresetServerRemove(bpy.types.Operator): assert(context) settings = utils.get_preferences() - - settings.server_preset.remove(settings.server_preset.find(settings.server_preset_interface)) + settings.server_preset.remove(settings.server_preset.find(self.target_server_name)) return {'FINISHED'} @@ -911,7 +1070,8 @@ def menu_func_import(self, context): classes = ( - SessionStartOperator, + SessionConnectOperator, + SessionHostOperator, SessionStopOperator, SessionPropertyRemoveOperator, SessionSnapUserOperator, @@ -928,6 +1088,7 @@ classes = ( SessionStopAutoSaveOperator, SessionPurgeOperator, SessionPresetServerAdd, + SessionPresetServerEdit, SessionPresetServerRemove, ) diff --git a/multi_user/preferences.py b/multi_user/preferences.py index ef43913..8177270 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -17,6 +17,7 @@ import random import logging +from uuid import uuid4 import bpy import string import re @@ -35,12 +36,14 @@ HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9] DEFAULT_PRESETS = { "localhost" : { + "server_name": "localhost", "server_ip": "localhost", "server_port": 5555, "admin_password": "admin", "server_password": "" }, "public session" : { + "server_name": "public session", "server_ip": "51.75.71.183", "server_port": 5555, "admin_password": "", @@ -112,7 +115,8 @@ class ReplicatedDatablock(bpy.types.PropertyGroup): auto_push: bpy.props.BoolProperty(default=True) icon: bpy.props.StringProperty() -class ServerPreset(bpy.types.PropertyGroup): +class ServerPreset(bpy.types.PropertyGroup): # TODO: self.uuid = uuid if uuid else str(uuid4()) + server_name: bpy.props.StringProperty() server_ip: bpy.props.StringProperty() server_port: bpy.props.IntProperty(default=5555) server_server_password: bpy.props.StringProperty(default="", subtype = "PASSWORD") @@ -190,6 +194,10 @@ class SessionPrefs(bpy.types.AddonPreferences): description="Custom name of the server", default='localhost', ) + server_index: bpy.props.IntProperty( + name="server_index", + description="index of the server", + ) server_password: bpy.props.StringProperty( name="server_password", description='Session password', @@ -540,7 +548,9 @@ class SessionPrefs(bpy.types.AddonPreferences): if existing_preset : continue new_server = self.server_preset.add() - new_server.name = preset_name + new_server.name = str(uuid4()) + new_server.server_name = preset_data.get('server_name') + new_server.server_index = preset_data.get('server_index') new_server.server_ip = preset_data.get('server_ip') new_server.server_port = preset_data.get('server_port') new_server.server_password = preset_data.get('server_password',None) diff --git a/multi_user/ui.py b/multi_user/ui.py index 5f5de88..f96f2be 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -16,6 +16,8 @@ # ##### END GPL LICENSE BLOCK ##### +from logging import log +import logging import bpy import bpy.utils.previews @@ -103,7 +105,37 @@ class SESSION_PT_settings(bpy.types.Panel): # STATE INITIAL if not session \ or (session and session.state == STATE_INITIAL): - pass + layout = self.layout + settings = get_preferences() + server_preset = settings.server_preset + selected_server = context.window_manager.server_index if context.window_manager.server_index<=len(server_preset)-1 else 0 + active_server_name = server_preset[selected_server].name if len(server_preset)>=1 else "" + is_server_selected = True if active_server_name else False # TODO : issues when removing the lowest server in the list + + # Create a simple row. + row = layout.row() + box = row.box() + split = box.split(factor=0.7) + split.label(text="Server") + split.label(text="Online") + + row = layout.row() + layout.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index") # TODO: change port to server_index + + row = layout.row() # TODO : active server in template + row.operator("session.preset_server_add", text="Add") # TODO : add conditions (need a name, etc..) + add a checkbox for password without creating preferences + col = row.column() + col.enabled = is_server_selected + col.operator("session.preset_server_edit", text="Edit").target_server_name = active_server_name + col = row.column() + col.enabled = is_server_selected + col.operator("session.preset_server_remove", text="Remove").target_server_name = active_server_name + + row = layout.row() + row.operator("session.host", text="Host") # TODO : add a pop-up for admin and server password ? + col = row.column() + col.enabled =is_server_selected + col.operator("session.connect", text="Connect") else: progress = session.state_progress row = layout.row() @@ -139,99 +171,6 @@ class SESSION_PT_settings(bpy.types.Panel): layout.row().operator("session.stop", icon='QUIT', text="Exit") -class SESSION_PT_settings_network(bpy.types.Panel): - bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel" - bl_label = "Network" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel' - - @classmethod - def poll(cls, context): - return not session \ - or (session and session.state == 0) - - def draw_header(self, context): - self.layout.label(text="", icon='URL') - - def draw(self, context): - layout = self.layout - - runtime_settings = context.window_manager.session - settings = get_preferences() - - # USER SETTINGS - row = layout.row() - row.prop(runtime_settings, "session_mode", expand=True) - row = layout.row() - - col = row.row(align=True) - col.prop(settings, "server_preset_interface", text="") - col.operator("session.preset_server_add", icon='ADD', text="") - col.operator("session.preset_server_remove", icon='REMOVE', text="") - - row = layout.row() - box = row.box() - - if runtime_settings.session_mode == 'HOST': - row = box.row() - row.label(text="Port:") - row.prop(settings, "port", text="") - row = box.row() - row.label(text="Start from:") - row.prop(settings, "init_method", text="") - row = box.row() - row.label(text="Admin password:") - row.prop(settings, "admin_password", text="") - row = box.row() - row.operator("session.start", text="HOST").host = True - else: - row = box.row() - row.prop(settings, "ip", text="IP") - row = box.row() - row.label(text="Port:") - row.prop(settings, "port", text="") - - row = box.row() - row.prop(runtime_settings, "admin", text='Connect as admin', icon='DISCLOSURE_TRI_DOWN' if runtime_settings.admin - else 'DISCLOSURE_TRI_RIGHT') - if runtime_settings.admin: - row = box.row() - row.label(text="Password:") - row.prop(settings, "admin_password", text="") - row = box.row() - row.operator("session.start", text="CONNECT").host = False - -class SESSION_PT_settings_user(bpy.types.Panel): - bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel" - bl_label = "User info" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel' - - @classmethod - def poll(cls, context): - return not session \ - or (session and session.state == 0) - - def draw_header(self, context): - self.layout.label(text="", icon='USER') - - def draw(self, context): - layout = self.layout - - runtime_settings = context.window_manager.session - settings = get_preferences() - - row = layout.row() - # USER SETTINGS - row.prop(settings, "username", text="name") - - row = layout.row() - row.prop(settings, "client_color", text="color") - row = layout.row() - - class SESSION_PT_advanced_settings(bpy.types.Panel): bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel" bl_label = "Advanced" @@ -446,57 +385,6 @@ class SESSION_UL_users(bpy.types.UIList): split.label(text=scene_current) split.label(text=ping) - -class SESSION_PT_presence(bpy.types.Panel): - bl_idname = "MULTIUSER_MODULE_PT_panel" - bl_label = "Presence overlay" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel' - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - return not session \ - or (session and session.state in [STATE_INITIAL, STATE_ACTIVE]) - - def draw_header(self, context): - self.layout.prop(context.window_manager.session, - "enable_presence", text="",icon='OVERLAY') - - def draw(self, context): - layout = self.layout - - settings = context.window_manager.session - pref = get_preferences() - layout.active = settings.enable_presence - - row = layout.row() - row.prop(settings, "presence_show_selected",text="Selected Objects") - - row = layout.row(align=True) - row.prop(settings, "presence_show_user", text="Users camera") - row.prop(settings, "presence_show_mode", text="Users mode") - - col = layout.column() - if settings.presence_show_mode or settings.presence_show_user: - row = col.column() - row.prop(pref, "presence_text_distance", expand=True) - - row = col.column() - row.prop(settings, "presence_show_far_user", text="Users on different scenes") - - col.prop(settings, "presence_show_session_status") - if settings.presence_show_session_status : - split = layout.split() - text_pos = split.column(align=True) - text_pos.active = settings.presence_show_session_status - text_pos.prop(pref, "presence_hud_hpos", expand=True) - text_pos.prop(pref, "presence_hud_vpos", expand=True) - text_scale = split.column() - text_scale.active = settings.presence_show_session_status - text_scale.prop(pref, "presence_hud_scale", expand=True) - def draw_property(context, parent, property_uuid, level=0): settings = get_preferences() runtime_settings = context.window_manager.session @@ -673,13 +561,36 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel): text_scale.active = settings.presence_show_session_status text_scale.prop(pref, "presence_hud_scale", expand=True) + +class SESSION_UL_network(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag): + settings = get_preferences() + server_name = '-' + server_status = 'BLANK1' + server_private = 'BLANK1' + + server_name = item.server_name + + split = layout.split(factor=0.7) + # Session with/without password + # TODO : ping lock server + if settings.server_password != None: + server_private = 'LOCKED' + split.label(text=server_name, icon=server_private) + else: + split.label(text=server_name) + + # Session status + # TODO : if session online : vert else rouge + # TODO : ping + from multi_user import icons + server_status = icons.icons_col["session_status_offline"] + split.label(icon_value=server_status.icon_id) classes = ( SESSION_UL_users, + SESSION_UL_network, SESSION_PT_settings, - SESSION_PT_settings_user, - SESSION_PT_settings_network, - SESSION_PT_presence, SESSION_PT_advanced_settings, SESSION_PT_user, SESSION_PT_repository,