Repository: ynput/OpenPype
Issue: Maya: Update camera resets viewports currently looking through that camera

 ### Issue

Loading a camera and updating it resets any modelPanel to a different camera (because as the reference gets updated it temporarily isn't there and maya ends up resetting it).

**To reproduce:**

1. Load a camera
2. Set a viewport to look through the camera
3. Go to OpenPype > Manage.. and update the loaded camera to a new version
4. The viewport now currently isn't looking through the camera anymore.

**Expected behavior**

Less interuption for the user with their current scene would be nice - so basically no change to the viewports and their cameras except for the camera being updated.

**Additional context**

I have an existing fix for this outside of OpenPype here: https://github.com/Colorbleed/colorbleed-config/commit/2d1f60082c4360c6b4d011bbe49cf3c9a0f9855a

I'd just like to know whether anyone else also feels this is a nice enough user experience improvement to implement it.

[cuID:[OP-2261](https://app.clickup.com/t/1yyjjga)]

Changed files:
openpype/hosts/maya/plugins/load/load_reference.py

diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py
index d93702a16d..82c15ab899 100644
--- a/openpype/hosts/maya/plugins/load/load_reference.py
+++ b/openpype/hosts/maya/plugins/load/load_reference.py
@@ -1,4 +1,6 @@
 import os
+import difflib
+import contextlib
 from maya import cmds
 
 from openpype.settings import get_project_settings
@@ -8,7 +10,82 @@ from openpype.pipeline.create import (
     get_legacy_creator_by_name,
 )
 import openpype.hosts.maya.api.plugin
-from openpype.hosts.maya.api.lib import maintained_selection
+from openpype.hosts.maya.api.lib import (
+    maintained_selection,
+    get_container_members
+)
+
+
+@contextlib.contextmanager
+def preserve_modelpanel_cameras(container, log=None):
+    """Preserve camera members of container in the modelPanels.
+
+    This is used to ensure a camera remains in the modelPanels after updating
+    to a new version.
+
+    """
+
+    # Get the modelPanels that used the old camera
+    members = get_container_members(container)
+    old_cameras = set(cmds.ls(members, type="camera", long=True))
+    if not old_cameras:
+        # No need to manage anything
+        yield
+        return
+
+    panel_cameras = {}
+    for panel in cmds.getPanel(type="modelPanel"):
+        cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True),
+                      long=True)
+
+        # Often but not always maya returns the transform from the
+        # modelPanel as opposed to the camera shape, so we convert it
+        # to explicitly be the camera shape
+        if cmds.nodeType(cam) != "camera":
+            cam = cmds.listRelatives(cam,
+                                     children=True,
+                                     fullPath=True,
+                                     type="camera")[0]
+        if cam in old_cameras:
+            panel_cameras[panel] = cam
+
+    if not panel_cameras:
+        # No need to manage anything
+        yield
+        return
+
+    try:
+        yield
+    finally:
+        new_members = get_container_members(container)
+        new_cameras = set(cmds.ls(new_members, type="camera", long=True))
+        if not new_cameras:
+            return
+
+        for panel, cam_name in panel_cameras.items():
+            new_camera = None
+            if cam_name in new_cameras:
+                new_camera = cam_name
+            elif len(new_cameras) == 1:
+                new_camera = next(iter(new_cameras))
+            else:
+                # Multiple cameras in the updated container but not an exact
+                # match detected by name. Find the closest match
+                matches = difflib.get_close_matches(word=cam_name,
+                                                    possibilities=new_cameras,
+                                                    n=1)
+                if matches:
+                    new_camera = matches[0]  # best match
+                    if log:
+                        log.info("Camera in '{}' restored with "
+                                 "closest match camera: {} (before: {})"
+                                 .format(panel, new_camera, cam_name))
+
+            if not new_camera:
+                # Unable to find the camera to re-apply in the modelpanel
+                continue
+
+            cmds.modelPanel(panel, edit=True, camera=new_camera)
 
 
 class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
@@ -68,6 +145,9 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
 
             new_nodes = (list(set(nodes) - set(shapes)))
 
+            # if there are cameras, try to lock their transforms
+            self._lock_camera_transforms(new_nodes)
+
             current_namespace = pm.namespaceInfo(currentNamespace=True)
 
             if current_namespace != ":":
@@ -136,6 +216,15 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
     def switch(self, container, representation):
         self.update(container, representation)
 
+    def update(self, container, representation):
+        with preserve_modelpanel_cameras(container, log=self.log):
+            super(ReferenceLoader, self).update(container, representation)
+
+        # We also want to lock camera transforms on any new cameras in the
+        # reference or for a camera which might have changed names.
+        members = get_container_members(container)
+        self._lock_camera_transforms(members)
+
     def _post_process_rig(self, name, namespace, context, options):
 
         output = next((node for node in self if
@@ -168,3 +257,18 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
                 options={"useSelection": True},
                 data={"dependencies": dependency}
             )
+
+    def _lock_camera_transforms(self, nodes):
+        cameras = cmds.ls(nodes, type="camera")
+        if not cameras:
+            return
+
+        # Check the Maya version, lockTransform has been introduced since
+        # Maya 2016.5 Ext 2
+        version = int(cmds.about(version=True))
+        if version >= 2016:
+            for camera in cameras:
+                cmds.camera(camera, edit=True, lockTransform=True)
+        else:
+            self.log.warning("This version of Maya does not support locking of"
+                             " transforms of cameras.")