text: # -= ml_utilities.py =-
  #                __   by Morgan Loomis
  #     ____ ___  / /  http://morganloomis.com
  #    / __ `__ \/ /  Revision 34
  #   / / / / / / /  2019-03-07
  #  /_/ /_/ /_/_/  _________
  #               /_________/
  # 
  #     ______________
  # - -/__ License __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
  # 
  # Copyright 2018 Morgan Loomis
  # 
  # Permission is hereby granted, free of charge, to any person obtaining a copy of 
  # this software and associated documentation files (the "Software"), to deal in 
  # the Software without restriction, including without limitation the rights to use, 
  # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 
  # Software, and to permit persons to whom the Software is furnished to do so, 
  # subject to the following conditions:
  # 
  # The above copyright notice and this permission notice shall be included in all 
  # copies or substantial portions of the Software.
  # 
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 
  # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  # 
  #     ___________________
  # - -/__ Installation __/- - - - - - - - - - - - - - - - - - - - - - - - - - 
  # 
  # Copy this file into your maya scripts directory, for example:
  #     C:/Documents and Settings/user/My Documents/maya/scripts/ml_utilities.py
  # 
  # Run the tool in a python shell or shelf button by importing the module, 
  # and then calling the primary function:
  # 
  #     import ml_utilities
  #     ml_utilities._showHelpCommand()
  # 
  # 
  #     __________________
  # - -/__ Description __/- - - - - - - - - - - - - - - - - - - - - - - - - - - 
  # 
  # A collection of support functions that are required by several of the tools in
  # this library. The individual tools will tell you if this script is required.
  # 
  #     ____________
  # - -/__ Usage __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
  # 
  # ml_utilities isn't a stand alone tool, and so it isn't meant to be used
  # directly. However, you can certainly call these functions if they seem useful in
  # your own scripts.
  # 
  # 
  #                                                             __________
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /_ Enjoy! _/- - -
  
  __author__ = 'Morgan Loomis'
  __license__ = 'MIT'
  __revision__ = 35
  
  import maya.cmds as mc
  import maya.mel as mm
  from maya import OpenMaya
  from functools import partial
  import shutil, os, re, sys, math
  
  #declare some variables
  WEBSITE_URL = 'http://morganloomis.com'
  TOOL_URL = WEBSITE_URL+'/tool/'
  ICON_URL = WEBSITE_URL+'/icons/'
  GITHUB_ROOT_URL = 'https://raw.githubusercontent.com/morganloomis/ml_tools/master/scripts/'
  
  #try to add to the iconpath if there is an icons folder in this directory
  THIS_DIR = os.path.dirname(__file__)
  ICON_PATH = os.path.join(THIS_DIR,'icons').replace('\\','/')
  if os.path.isdir(ICON_PATH) and ICON_PATH not in os.environ['XBMLANGPATH']:
      os.environ['XBMLANGPATH'] = os.pathsep.join((os.environ['XBMLANGPATH'],ICON_PATH))
  
  MAYA_VERSION = mm.eval('getApplicationVersionAsFloat')
  
  def _showHelpCommand(url):
      '''
      This just returns the maya command for launching a web page, since that gets called a few times
      '''
      return 'import maya.cmds;maya.cmds.showHelp("'+url+'",absolute=True)'
  
  
  def main():
      '''
      This just launches the online help and serves as a placeholder for the default function for this script.
      '''
      mc.showHelp(TOOL_URL+'ml_utilities/', absolute=True)
  
  
  def upToDateCheck(revision, prompt=True):
      '''
      This is a check that can be run by scripts that import ml_utilities to make sure they
      have the correct version.
      '''
  
      if not '__revision__' in locals():
          return
  
      if revision > __revision__:
          if prompt and mc.optionVar(query='ml_utilities_revision') < revision:
              result = mc.confirmDialog( title='Module Out of Date',
                                         message='Your version of ml_utilities may be out of date for this tool. Without the latest file you may encounter errors.',
                                         button=['Download Latest Revision','Ignore', "Don't Ask Again"],
                                         defaultButton='Download Latest Revision', cancelButton='Ignore', dismissString='Ignore' )
  
              if result == 'Download Latest Revision':
                  mc.showHelp(GITHUB_ROOT_URL+'ml_utilities.py', absolute=True)
              elif result == "Don't Ask Again":
                  mc.optionVar(intValue=('ml_utilities_revision', revision))
          return False
      return True
  
  
  def castToTime(time):
      '''
      Maya's keyframe commands are finnicky about how lists of times or indicies are formatted.
      '''
      if isinstance(time, (list, tuple)):
          return [(x,) for x in time]
      return (time,)
  
  
  def constrain(source, destination, translate=True, rotate=True, scale=False, maintainOffset=False):
      '''
      Constrain two objects, even if they have some locked attributes.
      '''
  
      transAttr = None
      rotAttr = None
      scaleAttr = None
  
      if translate:
          transAttr = mc.listAttr(destination, keyable=True, unlocked=True, string='translate*')
      if rotate:
          rotAttr = mc.listAttr(destination, keyable=True, unlocked=True, string='rotate*')
      if scale:
          scaleAttr = mc.listAttr(destination, keyable=True, unlocked=True, string='scale*')
  
      rotSkip = list()
      transSkip = list()
  
      for axis in ['x','y','z']:
          if transAttr and not 'translate'+axis.upper() in transAttr:
              transSkip.append(axis)
          if rotAttr and not 'rotate'+axis.upper() in rotAttr:
              rotSkip.append(axis)
  
      if not transSkip:
          transSkip = 'none'
      if not rotSkip:
          rotSkip = 'none'
  
      constraints = list()
      if rotAttr and transAttr and rotSkip == 'none' and transSkip == 'none':
          constraints.append(mc.parentConstraint(source, destination, maintainOffset=maintainOffset))
      else:
          if transAttr:
              constraints.append(mc.pointConstraint(source, destination, skip=transSkip, maintainOffset=maintainOffset))
          if rotAttr:
              constraints.append(mc.orientConstraint(source, destination, skip=rotSkip, maintainOffset=maintainOffset))
  
      return constraints
  
  
  def createAnimLayer(nodes=None, name=None, namePrefix='', override=True):
      '''
      Create an animation layer, add nodes, and select it.
      '''
  
      #if there's no layer name, generate one
      if not name:
          if namePrefix:
              namePrefix+='_'
          if nodes:
              shortNodes = mc.ls(nodes, shortNames=True)
              shortNodes = [x.rpartition(':')[-1] for x in shortNodes]
              #if there's just one node, use it's name minus the namespace
              if len(shortNodes) == 1:
                  name = namePrefix+shortNodes[0]
              else:
                  #try to find the longest common substring
                  commonString = longestCommonSubstring(shortNodes)
                  if commonString:
                      name = commonString
                  elif ':' in nodes[0]:
                      #otherwise use the namespace if it has one
                      name = nodes[0].rpartition(':')[-1]
          if not name:
              if not namePrefix:
                  namePrefix = 'ml_'
              name = namePrefix+'animLayer'
  
      layer = mc.animLayer(name, override=override)
  
      #add the nodes to the layer
      if nodes:
          sel = mc.ls(sl=True)
          mc.select(nodes)
          mc.animLayer(layer, edit=True, addSelectedObjects=True)
          if sel:
              mc.select(sel)
          else:
              mc.select(clear=True)
  
      #select the layer
      selectAnimLayer(layer)
      return layer
  
  
  def selectAnimLayer(animLayer=None):
      '''
      Select only the specified animation layer
      '''
      #deselect all layers
      for each in mc.ls(type='animLayer'):
          mc.animLayer(each, edit=True, selected=False, preferred=False)
      if animLayer:
          mc.animLayer(animLayer, edit=True, selected=True, preferred=True)
  
  
  def getSelectedAnimLayers():
      '''
      Return the names of the layers which are selected
      '''
      layers = list()
      for each in mc.ls(type='animLayer'):
          if mc.animLayer(each, query=True, selected=True):
              layers.append(each)
      return layers
  
  
  def createHotkey(command, name, description='', python=True):
      '''
      Open up the hotkey editor to create a hotkey from the specified command
      '''
  
      if MAYA_VERSION > 2015:
          print "Creating hotkeys currently doesn't work in the new hotkey editor."
          print "Here's the command, you'll have to make the hotkey yourself (sorry):"
          print command
          OpenMaya.MGlobal.displayWarning("Couldn't create hotkey, please see script editor for details...")
          return
  
      mm.eval('hotkeyEditor')
      mc.textScrollList('HotkeyEditorCategoryTextScrollList', edit=True, selectItem='User')
      mm.eval('hotkeyEditorCategoryTextScrollListSelect')
      mm.eval('hotkeyEditorCreateCommand')
  
      mc.textField('HotkeyEditorNameField', edit=True, text=name)
      mc.textField('HotkeyEditorDescriptionField', edit=True, text=description)
  
      if python:
          if MAYA_VERSION < 2013:
              command = 'python("'+command+'");'
          else: #2013 or above
              mc.radioButtonGrp('HotkeyEditorLanguageRadioGrp', edit=True, select=2)
  
      mc.scrollField('HotkeyEditorCommandField', edit=True, text=command)
  
  
  def createShelfButton(command, label='', name=None, description='',
                        image=None, #the default image is a circle
                        labelColor=(1, 0.5, 0),
                        labelBackgroundColor=(0, 0, 0, 0.5),
                        backgroundColor=None
                        ):
      '''
      Create a shelf button for the command on the current shelf
      '''
      #some good default icons:
      #menuIconConstraints - !
      #render_useBackground - circle
      #render_volumeShader - black dot
      #menuIconShow - eye
  
      gShelfTopLevel = mm.eval('$temp=$gShelfTopLevel')
      if not mc.tabLayout(gShelfTopLevel, exists=True):
          OpenMaya.MGlobal.displayWarning('Shelf not visible.')
          return
  
      if not name:
          name = label
  
      if not image:
          image = getIcon(name)
      if not image:
          image = 'render_useBackground'
  
      shelfTab = mc.shelfTabLayout(gShelfTopLevel, query=True, selectTab=True)
      shelfTab = gShelfTopLevel+'|'+shelfTab
  
      #add additional args depending on what version of maya we're in
      kwargs = {}
      if MAYA_VERSION >= 2009:
          kwargs['commandRepeatable'] = True
      if MAYA_VERSION >= 2011:
          kwargs['overlayLabelColor'] = labelColor
          kwargs['overlayLabelBackColor'] = labelBackgroundColor
          if backgroundColor:
              kwargs['enableBackground'] = bool(backgroundColor)
              kwargs['backgroundColor'] = backgroundColor
  
      return mc.shelfButton(parent=shelfTab, label=name, command=command,
                            imageOverlayLabel=label, image=image, annotation=description,
                            width=32, height=32, align='center', **kwargs)
  
  
  def deselectChannels():
      '''
      Deselect selected channels in the channelBox
      by clearing selection and then re-selecting
      '''
  
      if not getSelectedChannels():
          return
      sel = mc.ls(sl=True)
      mc.select(clear=True)
      mc.evalDeferred(partial(mc.select,sel))
  
  
  def formLayoutGrid(form, controls, offset=1):
      '''
      Controls should be a list of lists, and this will arrange them in a grid
      '''
  
      kwargs = {'edit':True, 'attachPosition':[]}
      rowInc = 100/len(controls)
      colInc = 100/len(controls[0])
      position = {'left':0,'right':100,'top':0,'bottom':100}
  
      for r,row in enumerate(controls):
          position['top'] = r*rowInc
          position['bottom'] = (r+1)*rowInc
          for c,ctrl in enumerate(row):
              position['left'] = c*colInc
              position['right'] = (c+1)*colInc
              for k in position.keys():
                  kwargs['attachPosition'].append((ctrl, k, offset, position[k]))
  
      mc.formLayout(form, **kwargs)
  
  
  def frameRange(start=None, end=None):
      '''
      Returns the frame range based on the highlighted timeslider,
      or otherwise the playback range.
      '''
  
      if not start and not end:
          gPlayBackSlider = mm.eval('$temp=$gPlayBackSlider')
          if mc.timeControl(gPlayBackSlider, query=True, rangeVisible=True):
              frameRange = mc.timeControl(gPlayBackSlider, query=True, rangeArray=True)
              start = frameRange[0]
              end = frameRange[1]-1
          else:
              start = mc.playbackOptions(query=True, min=True)
              end = mc.playbackOptions(query=True, max=True)
  
      return start,end
  
  
  def getChannelFromAnimCurve(curve, plugs=True):
      '''
      Finding the channel associated with a curve has gotten really complicated since animation layers.
      This is a recursive function which walks connections from a curve until an animated channel is found.
      '''
  
  
      #we need to save the attribute for later.
      attr = ''
      if '.' in curve:
          curve, attr = curve.split('.')
  
      nodeType = mc.nodeType(curve)
      if nodeType.startswith('animCurveT') or nodeType.startswith('animBlendNode'):
          source = mc.listConnections(curve+'.output', source=False, plugs=plugs)
          if not source and nodeType=='animBlendNodeAdditiveRotation':
              #if we haven't found a connection from .output, then it may be a node that uses outputX, outputY, etc.
              #get the proper attribute by using the last letter of the input attribute, which should be X, Y, etc.
              #if we're not returning plugs, then we wont have an attr suffix to use, so just use X.
              attrSuffix = 'X'
              if plugs:
                  attrSuffix = attr[-1]
  
              source = mc.listConnections(curve+'.output'+attrSuffix, source=False, plugs=plugs)
          if source:
              nodeType = mc.nodeType(source[0])
              if nodeType.startswith('animCurveT') or nodeType.startswith('animBlendNode'):
                  return getChannelFromAnimCurve(source[0], plugs=plugs)
              return source[0]
  
  
  def getCurrentCamera():
      '''
      Returns the camera that you're currently looking through.
      If the current highlighted panel isn't a modelPanel,
      '''
  
      panel = mc.getPanel(withFocus=True)
  
      if mc.getPanel(typeOf=panel) != 'modelPanel':
          #just get the first visible model panel we find, hopefully the correct one.
          for p in mc.getPanel(visiblePanels=True):
              if mc.getPanel(typeOf=p) == 'modelPanel':
                  panel = p
                  mc.setFocus(panel)
                  break
  
      if mc.getPanel(typeOf=panel) != 'modelPanel':
          OpenMaya.MGlobal.displayWarning('Please highlight a camera viewport.')
          return False
  
      camShape = mc.modelEditor(panel, query=True, camera=True)
      if not camShape:
          return False
  
      camNodeType = mc.nodeType(camShape)
      if mc.nodeType(camShape) == 'transform':
          return camShape
      elif mc.nodeType(camShape) in ['camera','stereoRigCamera']:
          return mc.listRelatives(camShape, parent=True, path=True)[0]
  
  
  def getFrameRate():
      '''
      Return an int of the current frame rate
      '''
      currentUnit = mc.currentUnit(query=True, time=True)
      if currentUnit == 'film':
          return 24
      if currentUnit == 'show':
          return 48
      if currentUnit == 'pal':
          return 25
      if currentUnit == 'ntsc':
          return 30
      if currentUnit == 'palf':
          return 50
      if currentUnit == 'ntscf':
          return 60
      if 'fps' in currentUnit:
          return int(currentUnit.substitute('fps',''))
  
      return 1
  
  
  def getFrameRateInSeconds():
  
      return 1.0/getFrameRate()
  
  
  def getDistanceInMeters():
  
      unit = mc.currentUnit(query=True, linear=True)
  
      if unit == 'mm':
          return 1000
      elif unit == 'cm':
          return 100
      elif unit == 'km':
          return 0.001
      elif unit == 'in':
          return 39.3701
      elif unit == 'ft':
          return 3.28084
      elif unit == 'yd':
          return 1.09361
      elif unit == 'mi':
          return 0.000621371
  
      return 1
  
  
  def getHoldTangentType():
      '''
      Returns the best in and out tangent type for creating a hold with the current tangent settings.
      '''
      try:
          tangentType = mc.keyTangent(query=True, g=True, ott=True)[0]
      except:
          return 'auto','auto'
      if tangentType=='linear':
          return 'linear','linear'
      if tangentType=='step':
          return 'linear','step'
      if tangentType == 'plateau' or tangentType == 'spline' or MAYA_VERSION < 2012:
          return 'plateau','plateau'
      return 'auto','auto'
  
  
  def getIcon(name):
      '''
      Check if an icon name exists, and return with proper extension.
      Otherwise return standard warning icon.
      '''
  
      ext = '.png'
      if MAYA_VERSION < 2011:
          ext = '.xpm'
  
      if not name.endswith('.png') and not name.endswith('.xpm'):
          name+=ext
  
      for each in os.environ['XBMLANGPATH'].split(os.pathsep):
          #on some linux systems each path ends with %B, for some reason
          iconPath = os.path.abspath(each.replace('%B',''))
          iconPath = os.path.join(iconPath,name)
          if os.path.exists(iconPath):
              return name
  
      return None
  
  
  def getIconPath():
      '''
      Find the icon path
      '''
  
      appDir = os.environ['MAYA_APP_DIR']
      for each in os.environ['XBMLANGPATH'].split(os.pathsep):
          #on some linux systems each path ends with %B, for some reason
          iconPath = each.replace('%B','')
          if iconPath.startswith(appDir):
              iconPath = os.path.abspath(iconPath)
              if os.path.exists(iconPath):
                  return iconPath
  
  
  def getModelPanel():
      '''Return the active or first visible model panel.'''
      
      panel = mc.getPanel(withFocus=True)
  
      if mc.getPanel(typeOf=panel) != 'modelPanel':
          #just get the first visible model panel we find, hopefully the correct one.
          panels = getModelPanels()
          if panels:
              panel = panels[0]
              mc.setFocus(panel)
      
      if mc.getPanel(typeOf=panel) != 'modelPanel':
          OpenMaya.MGlobal.displayWarning('Please highlight a camera viewport.')
          return None
      return panel
      
      
  def getModelPanels():
      '''Return all the model panels visible so you can operate on them.'''
      panels = []
      for p in mc.getPanel(visiblePanels=True):
          if mc.getPanel(typeOf=p) == 'modelPanel':
              panels.append(p)
      return panels
  
  
  def getNamespace(node):
      '''Returns the namespace of a node with simple string parsing.'''
  
      if not ':' in node:
          return ''
      return node.rsplit('|',1)[-1].rsplit(':',1)[0] + ':'
  
  
  def getNucleusHistory(node):
      
      history = mc.listHistory(node, levels=0)
      if history:
          dynamics = mc.ls(history, type='hairSystem')
          if dynamics:
              nucleus = mc.listConnections(dynamics[0]+'.startFrame', source=True, destination=False, type='nucleus')
              if nucleus:
                  return nucleus[0]
      return None
  
  
  def getRoots(nodes):
  
      objs = mc.ls(nodes, long=True)
      tops = []
      namespaces = []
      parent = None
      for obj in objs:
          namespace = getNamespace(obj)
          if namespace in namespaces:
              #we've already done this one
              continue
          parent = mc.listRelatives(obj, parent=True, pa=True)
          top = obj
          if not namespace:
              while parent:
                  top = parent[0]
                  parent = mc.listRelatives(top, parent=True, pa=True)
              
              tops.append(top)
          
          else:
              namespaces.append(namespace)
              while parent and parent[0].rsplit('|',1)[-1].startswith(namespace):
                  top = parent[0]
                  parent = mc.listRelatives(top, parent=True, pa=True)
              
              tops.append(top)
      return tops
  
  
  def getSelectedChannels():
      '''
      Return channels that are selected in the channelbox
      '''
  
      if not mc.ls(sl=True):
          return
      gChannelBoxName = mm.eval('$temp=$gChannelBoxName')
      sma = mc.channelBox(gChannelBoxName, query=True, sma=True)
      ssa = mc.channelBox(gChannelBoxName, query=True, ssa=True)
      sha = mc.channelBox(gChannelBoxName, query=True, sha=True)
  
      channels = list()
      if sma:
          channels.extend(sma)
      if ssa:
          channels.extend(ssa)
      if sha:
          channels.extend(sha)
  
      return channels
  
  
  def getSkinCluster(mesh):
      '''
      Return the first skinCluster affecting this mesh.
      '''
  
      if mc.nodeType(mesh) in ('mesh','nurbsSurface','nurbsCurve'):
          shapes = [mesh]
      else:
          shapes = mc.listRelatives(mesh, shapes=True, path=True)
  
      for shape in shapes:
          history = mc.listHistory(shape, groupLevels=True, pruneDagObjects=True)
          if not history:
              continue
          skins = mc.ls(history, type='skinCluster')
          if skins:
              return skins[0]
      return None
  
  
  def listAnimCurves(objOrAttrs):
      '''
      This lists connections to all types of animNodes
      '''
  
      animNodes = list()
  
      tl = mc.listConnections(objOrAttr, s=True, d=False, type='animCurveTL')
      ta = mc.listConnections(objOrAttr, s=True, d=False, type='animCurveTA')
      tu = mc.listConnections(objOrAttr, s=True, d=False, type='animCurveTU')
  
      if tl:
          animNodes.extend(tl)
      if ta:
          animNodes.extend(ta)
      if tu:
          animNodes.extend(tu)
  
      return animNodes
  
  
  def longestCommonSubstring(data):
      '''
      Returns the longest string that is present in the list of strings.
      '''
      substr = ''
      if len(data) > 1 and len(data[0]) > 0:
          for i in range(len(data[0])):
              for j in range(len(data[0])-i+1):
                  if j > len(substr):
                      find = data[0][i:i+j]
                      if len(data) < 1 and len(find) < 1:
                          continue
                      found = True
                      for k in range(len(data)):
                          if find not in data[k]:
                              found = False
                      if found:
                          substr = data[0][i:i+j]
      return substr
  
  
  def matchBake(source=None, destination=None, bakeOnOnes=False, maintainOffset=False, preserveTangentWeight=True, translate=True, rotate=True, start=None, end=None):
  
      if not source and not destination:
          sel = mc.ls(sl=True)
          if len(sel) != 2:
              OpenMaya.MGlobal.displayWarning('Select exactly 2 objects')
              return
          source = [sel[0]]
          destination = [sel[1]]
  
      #save for reset:
      resetTime = mc.currentTime(query=True)
  
      #frame range
      if start == None or end == None:
          start, end = frameRange()
  
      attributes = list()
      if translate:
          attributes.extend(['translateX','translateY','translateZ'])
      if rotate:
          attributes.extend(['rotateX','rotateY','rotateZ'])
  
      if not attributes:
          OpenMaya.MGlobal.displayWarning('No attributes to bake!')
          return
  
      duplicates = {}
      keytimes = {}
      constraint = list()
      itt = {}
      ott = {}
      weighted = {}
      itw = {}
      otw = {}
      #initialize allKeyTimes with start and end frames, since they may not be keyed
      allKeyTimes = [start,end]
      for s,d in zip(source,destination):
  
          #duplicate the destination
          dup = mc.duplicate(d, name='temp#', parentOnly=True)[0]
          for a in attributes:
              mc.setAttr(dup+'.'+a, lock=False, keyable=True)
  
          constraint.append(mc.parentConstraint(s, dup, maintainOffset=maintainOffset))
  
          #cut keys on destination
          mc.cutKey(d, attribute=attributes, time=(start,end))
  
          #set up our data dictionaries
          duplicates[d] = dup
          keytimes[d] = {}
          itt[d] = {}
          ott[d] = {}
          weighted[d] = {}
          itw[d] = {}
          otw[d] = {}
  
          #if we're baking on ones, we don't need keytimes
          if not bakeOnOnes:
              for a in attributes:
                  currKeytimes = mc.keyframe(s, attribute=a, time=(start,end), query=True, timeChange=True)
                  if not currKeytimes:
                      continue
  
                  keytimes[d][a] = currKeytimes
                  allKeyTimes.extend(currKeytimes)
  
                  #errors in maya 2016.5?
                  try:
                      itt[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, inTangentType=True)
                      ott[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, outTangentType=True)
                  except RuntimeError as err:
                      itt[d][a] = ['auto'] * len(currKeytimes)
                      ott[d][a] = ['auto'] * len(currKeytimes)
  
                  if preserveTangentWeight and mc.keyTangent(s, attribute=a, query=True, weightedTangents=True)[0]:
                      weighted[d][a] = True
                      itw[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, inWeight=True)
                      otw[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, outWeight=True)
  
                  #change fixed tangents to spline, because we can't set fixed tangents
                  for i, each in enumerate(itt[d][a]):
                      if each == 'fixed':
                          itt[d][a][i] = 'spline'
  
                  for i, each in enumerate(ott[d][a]):
                      if each == 'fixed':
                          ott[d][a][i] = 'spline'
  
                  #add the start and end frames and tangents if they're not keyed
                  if not start in keytimes[d][a]:
                      keytimes[d][a].insert(0,start)
                      itt[d][a].insert(0,'spline')
                      ott[d][a].insert(0,'spline')
                      if a in weighted[d]:
                          itw[d][a].insert(0, 1.0)
                          otw[d][a].insert(0, 1.0)
                  if not end in keytimes[d][a]:
                      keytimes[d][a].append(end)
                      itt[d][a].append('spline')
                      ott[d][a].append('spline')
                      if a in weighted[d]:
                          itw[d][a].append(1.0)
                          otw[d][a].append(1.0)
  
                  #reverse these, because we want to pop but start from the beginning
                  itt[d][a].reverse()
                  ott[d][a].reverse()
                  if a in weighted[d]:
                      itw[d][a].reverse()
                      otw[d][a].reverse()
  
  
      if bakeOnOnes:
          allKeyTimes = range(int(start), int(end)+1)
      else:
          allKeyTimes = list(set(allKeyTimes))
          allKeyTimes.sort()
  
      with UndoChunk():
          #if 
          with IsolateViews():
              for frame in allKeyTimes:
                  #cycle through all the frames
                  mc.currentTime(frame, edit=True)
                  for d in destination:
                      weightedSet = False
                      for a in attributes:
                          try:
                              v = mc.getAttr(duplicates[d]+'.'+a)
                              if bakeOnOnes:
                                  mc.setKeyframe(d, attribute=a, time=frame,
                                                 value=v,
                                                 itt='spline',
                                                 ott='spline')
                                  mc.setAttr(d+'.'+a, v)
                              elif a in keytimes[d] and frame in keytimes[d][a]:
                                  #tangent types line up with keytimes
                                  mc.setKeyframe(d, attribute=a, time=frame,
                                                 value=v,
                                                 itt=itt[d][a].pop(),
                                                 ott=ott[d][a].pop()
                                                 )
                                  mc.setAttr(d+'.'+a, v)
  
                          except:
                              pass
  
              #this was breaking the tangents inside the other loop, so run it after.
              if not bakeOnOnes and preserveTangentWeight:
                  for d in destination:
                      for a in attributes:
                          if a in weighted[d]:
                              mc.keyTangent(d, attribute=a, edit=True, weightedTangents=True)
                              for frame in keytimes[d][a]:
                                  mc.keyTangent(d, attribute=a, time=(frame,), edit=True, absolute=True, inWeight=itw[d][a].pop(), outWeight=otw[d][a].pop())
  
          #reset time and selection
          mc.currentTime(resetTime, edit=True)
          mc.select(destination, replace=True)
  
      mc.delete(duplicates.values())
      if rotate:
          mc.filterCurve(mc.listConnections(destination,type='animCurve'))
      if bakeOnOnes:
          mc.keyTangent(destination, attribute=attributes, itt='spline', ott='spline')
  
  def message(msg, position='midCenterTop'):
      
      OpenMaya.MGlobal.displayWarning(msg)
      fadeTime = min(len(message)*100, 2000)
      mc.inViewMessage( amg=msg, pos=position, fade=True, fadeStayTime=fadeTime, dragKill=True)
  
  
  def minimizeRotationCurves(obj):
      '''
      Sets rotation animation to the value closest to zero.
      '''
  
      rotateCurves = mc.keyframe(obj, attribute=('rotateX','rotateY', 'rotateZ'), query=True, name=True)
  
      if not rotateCurves or len(rotateCurves) < 3:
          return
  
      keyTimes = mc.keyframe(rotateCurves, query=True, timeChange=True)
      tempFrame = sorted(keyTimes)[0] - 1
  
      #set a temp frame
      mc.setKeyframe(rotateCurves, time=(tempFrame,), value=0)
  
      #euler filter
      mc.filterCurve(rotateCurves)
  
      #delete temp key
      mc.cutKey(rotateCurves, time=(tempFrame,))
  
  
  def renderShelfIcon(name='tmp', width=32, height=32):
      '''
      This renders a shelf-sized icon and hopefully places it in your icon directory
      '''
      imageName=name
  
      #getCamera
      cam = getCurrentCamera()
  
      #save these values for resetting
      currentRenderer = mc.getAttr('defaultRenderGlobals.currentRenderer')
      imageFormat = mc.getAttr('defaultRenderGlobals.imageFormat')
  
      mc.setAttr('defaultRenderGlobals.currentRenderer', 'mayaSoftware', type='string')
  
      imageFormat = 50 #XPM
      if MAYA_VERSION >= 2011:
          imageFormat = 32 #PNG
  
      mc.setAttr('defaultRenderGlobals.imageFormat', imageFormat)
      mc.setAttr('defaultRenderGlobals.imfkey', 'xpm', type='string')
      #here's the imageName
      mc.setAttr('defaultRenderGlobals.imageFilePrefix', imageName, type='string')
  
      mc.setAttr(cam+'.backgroundColor', 0.8,0.8,0.8, type='double3')
      #need to reset this afterward
  
      image = mc.render(cam, xresolution=width, yresolution=height)
      base = os.path.basename(image)
  
      #here we attempt to move the rendered icon to a more generalized icon location
      newPath = getIconPath()
      if newPath:
          newPath = os.path.join(newPath, base)
          shutil.move(image, newPath)
          image = newPath
  
      #reset
      mc.setAttr('defaultRenderGlobals.currentRenderer', currentRenderer, type='string')
      mc.setAttr('defaultRenderGlobals.imageFormat', imageFormat)
  
      return image
  
  
  def setAnimValue(plug, value, tangentType=None):
      '''
      Sets key if the channel is keyed, otherwise setAttr
      '''
  
      if mc.keyframe(plug, query=True, name=True):
          mc.setKeyframe(plug, value=value)
          if tangentType:
              time = mc.currentTime(query=True)
              itt = tangentType
              ott = tangentType
              if tangentType == 'step':
                  itt = 'linear'
              mc.keyTangent(plug, time=(time,), edit=True, itt=itt, ott=ott)
  
      mc.setAttr(plug, value)
  
  
  class Dragger(object):
  
      def __init__(self,
                   name = 'mlDraggerContext',
                   title = 'Dragger',
                   defaultValue=0,
                   minValue=None,
                   maxValue=None,
                   multiplier=0.01,
                   cursor='hand'
                   ):
  
          self.multiplier = multiplier
          self.defaultValue = defaultValue
          self.minValue = minValue
          self.maxValue = maxValue
          #self.cycleCheck = mc.cycleCheck(query=True, evaluation=True)
  
          self.draggerContext = name
          if not mc.draggerContext(self.draggerContext, exists=True):
              self.draggerContext = mc.draggerContext(self.draggerContext)
  
          mc.draggerContext(self.draggerContext, edit=True,
                            pressCommand=self.press,
                            dragCommand=self.drag,
                            releaseCommand=self.release,
                            cursor=cursor,
                            drawString=title,
                            undoMode='all'
                            )
  
  
      def press(self, *args):
          '''
          Be careful overwriting the press method in child classes, because of the undoInfo openChunk
          '''
  
          self.anchorPoint = mc.draggerContext(self.draggerContext, query=True, anchorPoint=True)
          self.button = mc.draggerContext(self.draggerContext, query=True, button=True)
  
          # This turns off the undo queue until we're done dragging, so we can undo it.
          mc.undoInfo(openChunk=True)
  
  
      def drag(self, *args):
          '''
          This is what is actually run during the drag, updating the coordinates and calling the
          placeholder drag functions depending on which button is pressed.
          '''
  
          self.dragPoint = mc.draggerContext(self.draggerContext, query=True, dragPoint=True)
  
          #if this doesn't work, try getmodifier
          self.modifier = mc.draggerContext(self.draggerContext, query=True, modifier=True)
  
          self.x = ((self.dragPoint[0] - self.anchorPoint[0]) * self.multiplier) + self.defaultValue
          self.y = ((self.dragPoint[1] - self.anchorPoint[1]) * self.multiplier) + self.defaultValue
  
          if self.minValue is not None and self.x < self.minValue:
              self.x = self.minValue
          if self.maxValue is not None and self.x > self.maxValue:
              self.x = self.maxValue
  
          #dragString
          if self.modifier == 'control':
              if self.button == 1:
                  self.dragControlLeft(*args)
              elif self.button == 2:
                  self.dragControlMiddle(*args)
          elif self.modifier == 'shift':
              if self.button == 1:
                  self.dragShiftLeft(*args)
              elif self.button == 2:
                  self.dragShiftMiddle(*args)
          else:
              if self.button == 1:
                  self.dragLeft()
              elif self.button == 2:
                  self.dragMiddle()
  
          mc.refresh()
  
      def release(self, *args):
          '''
          Be careful overwriting the release method in child classes. Not closing the undo chunk leaves maya in a sorry state.
          '''
          # close undo chunk and turn cycle check back on
          mc.undoInfo(closeChunk=True)
          #mc.cycleCheck(evaluation=self.cycleCheck)
          mm.eval('SelectTool')
  
      def drawString(self, message):
          '''
          Creates a string message at the position of the pointer.
          '''
          mc.draggerContext(self.draggerContext, edit=True, drawString=message)
  
      def dragLeft(self,*args):
          '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
          pass
  
      def dragMiddle(self,*args):
          '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
          pass
  
      def dragControlLeft(self,*args):
          '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
          pass
  
      def dragControlMiddle(self,*args):
          '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
          pass
  
      def dragShiftLeft(self,*args):
          '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
          pass
  
      def dragShiftMiddle(self,*args):
          '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
          pass
  
      #no drag right, because that is monopolized by the right click menu
      #no alt drag, because that is used for the camera
  
      def setTool(self):
          mc.setToolTo(self.draggerContext)
  
  
  class IsolateViews():
      '''
      Isolates selection with nothing selected for all viewports
      This speeds up any process that causes the viewport to refresh,
      such as baking or changing time.
      '''
  
      def __enter__(self):
  
          if MAYA_VERSION >= 2016.5:
              if not mc.ogs(query=True, pause=True):
                  mc.ogs(pause=True)
          else:
              self.sel = mc.ls(sl=True)
              self.modelPanels = mc.getPanel(type='modelPanel')
  
              #unfortunately there's no good way to know what's been isolated, so in this case if a view is isolated, skip it.
              self.skip = []
              for each in self.modelPanels:
                  if mc.isolateSelect(each, query=True, state=True):
                      self.skip.append(each)
  
              self.isolate(True)
  
              mc.select(clear=True)
  
          self.resetAutoKey = mc.autoKeyframe(query=True, state=True)
          mc.autoKeyframe(state=False)
  
  
      def __exit__(self, *args):
  
          #reset settings
          mc.autoKeyframe(state=self.resetAutoKey)
  
          if MAYA_VERSION >= 2016.5:
              if mc.ogs(query=True, pause=True):
                  mc.ogs(pause=True)
          else:
              if self.sel:
                  mc.select(self.sel)
  
              self.isolate(False)
  
  
      def isolate(self, state):
  
          mc.select(clear=True)
          for each in self.modelPanels:
              if not each in self.skip:
                  mc.isolateSelect(each, state=state)
  
  
  class KeySelection(object):
      '''
  
      '''
  
      def __init__(self, *args):
  
          #if args are passed in, this has been called from and out of date script. Warn and fail.
          if args:
              print ''
              print "Because of an update to ml_utilities, the tool you're trying to run is deprecated and needs to be updated as well."
              print "Please visit http://morganloomis.com/tools and download the latest version of this tool."
              OpenMaya.MGlobal.displayError('Tool out of date. See script editor for details.')
              return
  
          self.shortestTime = getFrameRate()/6000.0
  
          #node variables
          self.nodeSelection = mc.ls(sl=True)
          self._nodes = list()
          self._curves = list()
          self._channels = list()
  
          #time variables
          self.currentTime = mc.currentTime(query=True)
          self._time = None
          self._timeRangeStart = None
          self._timeRangeEnd = None
  
          #keyframe command variables
          self.selected = False
  
          #other housekeeping
          self._curvesCulled = False
  
  
      @property
      def curves(self):
          '''
          The keySelections's animation curve list.
          '''
  
          # if self._curves is False or None, then it has been initialized and curves haven't been found.
          if self._curves == []:
  
              #find anim curves connected to channels or nodes
              for each in (self._channels, self._nodes):
                  if not each:
                      continue
                  # this will only return time based keyframes, not driven keys
                  self._curves = mc.keyframe(each, time=(':',), query=True, name=True)
  
                  if self._curves:
                      self._curvesCulled = False
                      break
              if not self._curves:
                  self._curves = False
  
          # need to remove curves which are unkeyable
          # supposedly referenced keys are keyable in 2013, I'll need to test that and update
          if self._curves and not self._curvesCulled:
              remove = list()
              for c in self._curves:
                  if mc.referenceQuery(c, isNodeReferenced=True):
                      remove.append(c)
                  else:
                      plug = mc.listConnections('.'.join((c,'output')), source=False, plugs=True)
                      if plug:
                          if not mc.getAttr(plug, keyable=True) and not mc.getAttr(plug, settable=True):
                              remove.append(c)
              if remove:
                  for r in remove:
                      self._curves.remove(r)
              self._curvesCulled = True
  
          return self._curves
  
  
      @property
      def channels(self):
          '''
          The keySelection's channels list.
          '''
  
          if not self._channels:
  
              if self._curves:
                  for c in self._curves:
                      self._channels.append(getChannelFromAnimCurve(c))
              elif self._nodes:
                  for obj in self._nodes:
                      keyable = mc.listAttr(obj, keyable=True, unlocked=True, hasData=True, settable=True)
                      if keyable:
                          for attr in keyable:
                              self._channels.append('.'.join((obj, attr)))
  
          return self._channels
  
      @property
      def nodes(self):
          '''
          The keySelection's node list.
          '''
  
          if not self._nodes:
  
              if self._curves:
                  self._nodes = list()
                  for c in self._curves:
                      n = getChannelFromAnimCurve(c, plugs=False)
                      if not n in self._nodes:
                          self._nodes.append(n)
              elif self._channels:
                  for each in self._channels:
                      n = each.split('.')[0]
                      if not n in self._nodes:
                          self._nodes.append(n)
  
          return self._nodes
  
      @property
      def args(self):
          '''
          This will return channels, curves or nodes in instances where we don't care which.
          It wont waste time converting from one to the other.
          '''
          if self._channels:
              return self._channels
          if self._curves:
              return self._curves
          if self._nodes:
              return self._nodes
          return None
  
  
      @property
      def time(self):
          '''
          The keySelection's time, formatted for maya's various keyframe command arguments.
          '''
          if self._time:
              if isinstance(self._time, list):
                  return tuple(self._time)
              elif isinstance(self._time, float) or isinstance(self._time, int):
                  return (self._time,)
              return self._time
          elif self._timeRangeStart and self._timeRangeEnd:
              return (self._timeRangeStart,self._timeRangeEnd)
          elif self._timeRangeStart:
              return (str(self._timeRangeStart)+':',)
          elif self._timeRangeEnd:
              return (':'+str(self._timeRangeEnd),)
          elif self.selected:
              #if keys are selected, get their times
              timeList = self.keyframe(query=True, timeChange=True)
              return tuple(set(timeList))
          return (':',)
  
  
      @property
      def times(self):
          '''
          This returns an expanded list of times, which is synced with the curve list.
          '''
          timeList = list()
          theTime = self.time
          for c in self.curves:
              curveTime = tuple(mc.keyframe(c, time=(theTime,), query=True, timeChange=True))
              if len(curveTime) == 1:
                  curveTime = (curveTime[0],)
              timeList.append(curveTime)
          return timeList
  
      @property
      def values(self):
          valueList = list()
          for c in self.curves:
              curveValues = mc.keyframe(c, query=True, valueChange=True)
              if len(curveValues) == 1:
                  curveValues = (curveValues[0],)
              valueList.append(curveValues)
          return valueList
  
  
      @property
      def initialized(self):
          '''
          Basically just tells if the object has been sucessfully initialized.
          '''
          return bool(self.args)
  
  
      def selectedObjects(self):
          '''
          Initializes the keySelection object with selected objects.
          Returns True if successful.
          '''
  
          if not self.nodeSelection:
              return False
  
          self._nodes = self.nodeSelection
          return True
  
  
      def selectedChannels(self):
          '''
          Initializes the keySelection object with selected channels.
          Returns True if successful.
          '''
  
          chanBoxChan = getSelectedChannels()
  
          if not chanBoxChan:
              return False
  
          #channels may be on shapes, include shapes in the list
          nodes = self.nodeSelection
          shapes = mc.listRelatives(self.nodeSelection, shapes=True, path=True)
          if shapes:
              nodes.extend(shapes)
              nodes = list(set(nodes))
  
          for obj in nodes:
              for attr in chanBoxChan:
                  if mc.attributeQuery(attr, node=obj, exists=True):
                      self._channels.append('.'.join((obj,attr)))
  
          if not self._channels:
              return False
  
          return True
  
  
      def selectedLayers(self, includeLayerWeight=True):
          '''
          This affects keys on all layers that the node belongs to.
          If includeLayerWeight, the keys on the layer's weight attribute will be affected also.
          '''
          layers = getSelectedAnimLayers()
          curves = list()
          for layer in layers:
              layerCurves = mc.animLayer(layer, query=True, animCurves=True)
              if layerCurves:
                  curves.extend(layerCurves)
              if includeLayerWeight:
                  weightCurve = mc.keyframe(layer+'.weight', query=True, name=True)
                  if weightCurve:
                      curves.append(weightCurve[0])
          self._curves = curves
          #we only want to use curves, since nodes or channels wont accurately represent all layers
          self._nodes = None
          self._channels = None
  
  
      def visibleInGraphEditor(self):
          '''
          Initializes the keySelection object with curves visibile in graph editor.
          Returns True if successful.
          '''
  
  
          if not 'graphEditor1' in mc.getPanel(visiblePanels=True):
              return False
  
          graphVis = mc.selectionConnection('graphEditor1FromOutliner', query=True, obj=True)
  
          if not graphVis:
              return False
  
          for each in graphVis:
              try:
                  self._curves.extend(mc.keyframe(each, query=True, name=True))
              except StandardError:
                  pass
  
  
          if not self._curves:
              return False
  
          return True
  
  
      def selectedKeys(self):
          '''
          Initializes the keySelection object with selected keyframes.
          Returns True if successful.
          '''
  
          selectedCurves = mc.keyframe(query=True, name=True, selected=True)
  
          if not selectedCurves:
              return False
          self._curves = selectedCurves
          self.selected = True
          return True
  
  
      def keyedChannels(self, includeShapes=False):
          '''
          Initializes the keySelection object with keyed channels.
          Returns True if successful.
          '''
  
          if not self.nodeSelection:
              return False
  
          self._nodes = self.nodeSelection
          if includeShapes:
              shapes = mc.listRelatives(self.nodeSelection, shapes=True, path=True)
              if shapes:
                  self._nodes.extend(shapes)
  
          #since self.curves is a property, it is actually finding curves from self._nodes
          if not self.curves:
              #if we don't find curves, reset nodes and fail
              self._nodes = None
              return False
  
          #reset self._nodes, otherwise they'll take priority over curves.
          self._nodes = None
  
          return True
  
  
      def keyedInHierarchy(self, includeRoot=True):
          '''
          Initializes the keySelection object with all the animation curves in the hierarchy.
          Returns True if successful.
          '''
  
          if not self.nodeSelection:
              return False
  
          tops = getRoots(self.nodeSelection)
  
          if not tops:
              #if we haven't been sucessful, we're done
              return False
  
          nodes = mc.listRelatives(tops, pa=True, type='transform', ad=True)
          if not nodes:
              nodes = list()
  
          if includeRoot:
              nodes.extend(tops)
  
          if not nodes:
              return False
  
          #now that we've determined the hierarchy, lets find keyed nodes
          #for node in nodes:
          # this will only return time based keyframes, not driven keys
          self._curves = mc.keyframe(nodes, time=(':',), query=True, name=True)
  
          #nodes or channels can be acessed by the node or channel property
          if not self._curves:
              return False
  
          return True
  
  
      def scene(self):
          '''
          Initializes the keySelection object with all animation curves in the scene.
          Returns True if successful.
          '''
  
          tl = mc.ls(type='animCurveTL')
          ta = mc.ls(type='animCurveTA')
          tu = mc.ls(type='animCurveTU')
  
          if tl:
              self._curves.extend(tl)
          if ta:
              self._curves.extend(ta)
          if tu:
              self._curves.extend(tu)
  
          if not self._curves:
              return False
  
          return True
  
  
      def selectedFrameRange(self):
          '''
          Sets the keySelection time to the selected frame range, returns false if frame range not selected.
          '''
  
          gPlayBackSlider = mm.eval('$temp=$gPlayBackSlider')
          if mc.timeControl(gPlayBackSlider, query=True, rangeVisible=True):
              self._timeRangeStart, self._timeRangeEnd = mc.timeControl(gPlayBackSlider, query=True, rangeArray=True)
              return True
          return False
  
  
      def frameRange(self):
          '''
          Sets the keySelection time to the selected frame range, or the current frame range.
          '''
          #this is selected range in the time slider
          self._timeRangeStart, self._timeRangeEnd = frameRange()
  
  
      def toEnd(self, includeCurrent=False):
          '''
          Sets the keySelection time to the range from the current frame to the last frame.
          Option to include the current frame.
          '''
  
          t = self.currentTime
          if not includeCurrent:
              t+=self.shortestTime
          self._timeRangeStart = t
  
  
      def fromBeginning(self, includeCurrent=False):
          '''
          Sets the keySelection time to the range from the first frame to the current frame.
          Option to include the current frame.
          '''
  
          t = self.currentTime
          if not includeCurrent:
              t-=self.shortestTime
          self._timeRangeEnd = t
  
  
      def keyRange(self):
          '''
          Sets the keySelection time range to the range of keys in the keySelection.
          '''
  
          keyTimes = self.getSortedKeyTimes()
  
          if not keyTimes or keyTimes[0] == keyTimes[-1]:
              return
  
          self._timeRangeStart = keyTimes[0]
          self._timeRangeEnd = keyTimes[-1]
  
  
      def currentFrame(self):
          '''
          Sets the keySelection time to the current frame.
          '''
          self._time = self.currentTime
  
  
      def previousKey(self):
          '''
          Sets the keySelection time to the previous key from the current frame.
          '''
          self._time = self.findKeyframe(which='previous')
  
  
      def nextKey(self):
          '''
          Sets the keySelection time to the next key from the current frame.
          '''
          self._time = self.findKeyframe(which='next')
  
  
      def setKeyframe(self, deleteSubFrames=False, **kwargs):
          '''
          Wrapper for the setKeyframe command. Curve and time arguments will be provided based on
          how this object was intitialized, otherwise usage is the same as maya's setKeyframe command.
          Option to delete sub-frames after keying.
          '''
  
          if not 'time' in kwargs:
              #still not sure about how I want to do this, but we need a discrete time.
              #if time is a string set to current time
              if isinstance(self.time, tuple) and (isinstance(self.time[0], str) or len(self.time)>1):
                  kwargs['time'] = mc.currentTime(query=True)
              else:
                  kwargs['time'] = self.time
  
          if 'insert' in kwargs and kwargs['insert'] == True:
              #setKeyframe fails if insert option is used but there's no keyframes on the channels.
              #key any curves with insert, then key everything again without it
  
              if self.curves:
                  mc.setKeyframe(self.curves, **kwargs)
              kwargs['insert'] = False
  
          #want to try setting keys on nodes first, since certain setKeyframe arguments wont work
          #as expected with channels
          if self._nodes:
              mc.setKeyframe(self.nodes, **kwargs)
              self._curves = mc.keyframe(self.nodes, query=True, name=True)
          else:
              mc.setKeyframe(self.channels, **kwargs)
              self._curves = mc.keyframe(self.channels, query=True, name=True)
  
          #there's a new selection of curves, so reset the member variables
          self._channels = list()
          self._nodes = list()
          self._time = kwargs['time']
  
          if deleteSubFrames:
              #remove nearby sub-frames
              #this breaks at higher frame ranges because maya doesn't keep enough digits
              #this value is also different for different frame rates
              if self.currentTime % 1 == 0 and -9999 < self.currentTime < 9999:
                  #the distance that keys can be is independent of frame rate, so we have to convert based on the frame rate.
                  tol = self.shortestTime
                  self.cutKey(time=(self.currentTime+tol, self.currentTime+0.5))
                  self.cutKey(time=(self.currentTime-0.5, self.currentTime-tol))
  
  
      def keyframe(self,**kwargs):
          '''
          Wrapper for the keyframe command. Curve and time arguments will be provided based on
          how this object was intitialized, otherwise usage is the same as maya's keyframe command.
          '''
          if self.selected:
              #it's important that selection test first, becuase it's called by the time property
              kwargs['sl'] = True
          elif not 'time' in kwargs:
              kwargs['time'] = self.time
  
          return mc.keyframe(self.curves, **kwargs)
  
  
      def cutKey(self, includeSubFrames=False, **kwargs):
          '''
          Wrapper for the cutKey command. Curve and time arguments will be provided based on
          how this object was intitialized, otherwise usage is the same as maya's cutKey command.
          Option to delete sub-frames.
          '''
  
          if not 'includeUpperBound' in kwargs:
              kwargs['includeUpperBound'] = False
  
          if self.selected:
              mc.cutKey(sl=True, **kwargs)
              return
  
          if not 'time' in kwargs:
              if includeSubFrames:
                  kwargs['time'] = (round(self.time[0])-0.5, round(self.time[-1])+0.5)
              else:
                  kwargs['time'] = self.time
          mc.cutKey(self.curves, **kwargs)
  
  
      def copyKey(self, **kwargs):
          '''
  
          '''
  
          if not 'includeUpperBound' in kwargs:
              kwargs['includeUpperBound'] = False
  
          if self.selected:
              mc.copyKey(sl=True, **kwargs)
              return
  
          if not 'time' in kwargs:
              kwargs['time'] = self.time
  
          mc.copyKey(self.args, **kwargs)
  
  
      def pasteKey(self, option='replaceCompletely', **kwargs):
          '''
  
          '''
          mc.pasteKey(self.args, option=option, **kwargs)
  
  
      def selectKey(self,**kwargs):
          '''
          Wrapper for maya's selectKey command.
          '''
  
          if not 'time' in kwargs:
              kwargs['time'] = self.time
          mc.selectKey(self.curves, **kwargs)
  
  
      def moveKey(self, frames):
          '''
          A wrapper for keyframe -edit -timeChange
          '''
  
          if not frames:
              return
  
          self.keyframe(edit=True, relative=True, timeChange=frames)
  
  
      def scaleKey(self, timePivot=0, **kwargs):
          '''
          Wrapper for maya's scaleKey command.
          '''
  
          if not 'time' in kwargs:
              kwargs['time'] = self.time
  
          if timePivot == 'current':
              timePivot = self.currentTime
  
          mc.scaleKey(self.curves, timePivot=timePivot, **kwargs)
  
  
      def tangentType(self, **kwargs):
          '''
          Wrapper for maya's tangentType command.
          '''
          if not 'time' in kwargs:
              kwargs['time'] = self.time
          mc.tangentType(self.curves, **kwargs)
  
  
      def keyTangent(self, **kwargs):
          '''
          Wrapper for maya's keyTangent command.
          '''
          if not 'time' in kwargs:
              kwargs['time'] = self.time
          mc.keyTangent(self.curves, **kwargs)
  
  
      def findKeyframe(self, which='next', loop=False, roundFrame=False, **kwargs):
          '''
          This is similar to maya's findKeyframe, but operates on the keySelection and has options
          for rounding and looping.
          '''
  
          if which not in ('next','previous','first','last'):
              return
  
          if not roundFrame:
              if not loop or which == 'first' or which == 'last':
                  #if there's not special options, just use default maya command for speed
                  return mc.findKeyframe(self.args, which=which, **kwargs)
  
          keyTimes = self.getSortedKeyTimes()
  
          #if we don't find any, we're done
          if not keyTimes:
              return
  
          tolerence = 0.0
          if roundFrame:
              tolerence = 0.5
  
          if which == 'previous':
              findTime = keyTimes[-1]
              for x in reversed(keyTimes):
                  if self.currentTime - x > tolerence:
                      findTime=x
                      break
          elif which == 'next':
              findTime = keyTimes[0]
              for x in keyTimes:
                  if x - self.currentTime > tolerence:
                      findTime=x
                      break
          elif which == 'first':
              findTime = keyTimes[0]
          elif which == 'last':
              findTime = keyTimes[-1]
  
          if roundFrame:
              #round to nearest frame, if that option is selected
              findTime = round(findTime)
  
          return findTime
  
  
      def getSortedKeyTimes(self):
          '''
          Returns a list of the key times in order without duplicates.
          '''
  
          keyTimes = self.keyframe(query=True, timeChange=True)
          if not keyTimes:
              return
          return sorted(list(set(keyTimes)))
  
  
  
  class MlUi(object):
      '''
      Window template for consistency
      '''
  
      def __init__(self, name, title, width=400, height=200, info='', menu=True, module=None):
  
          self.name = name
          self.title = title
          self.width = width
          self.height = height
          self.info = info
          self.menu = menu
  
          self.module = module
          if not module or module == '__main__':
              self.module = self.name
  
          #look for icon
          self.icon = getIcon(name)
  
  
      def __enter__(self):
          self.buildWindow()
          return self
  
      def __exit__(self, *args):
          self.finish()
  
      def buildWindow(self):
          '''
          Initialize the UI
          '''
  
          if mc.window(self.name, exists=True):
              mc.deleteUI(self.name)
  
          mc.window(self.name, title='ml :: '+self.title, iconName=self.title, width=self.width, height=self.height, menuBar=self.menu)
  
  
          if self.menu:
              self.createMenu()
  
          self.form = mc.formLayout()
          self.column = mc.columnLayout(adj=True)
  
  
          mc.rowLayout( numberOfColumns=2, columnWidth2=(34, self.width-34), adjustableColumn=2,
                        columnAlign2=('right','left'),
                        columnAttach=[(1, 'both', 0), (2, 'both', 8)] )
  
          #if we can find an icon, use that, otherwise do the text version
          if self.icon:
              mc.iconTextStaticLabel(style='iconOnly', image1=self.icon)
          else:
              mc.text(label=' _ _ |\n| | | |')
  
          if not self.menu:
              mc.popupMenu(button=1)
              mc.menuItem(label='Help', command=(_showHelpCommand(TOOL_URL+self.name+'/')))
  
          mc.text(label=self.info)
          mc.setParent('..')
          mc.separator(height=8, style='single', horizontal=True)
  
  
      def finish(self):
          '''
          Finalize the UI
          '''
  
          mc.setParent(self.form)
  
          frame = mc.frameLayout(labelVisible=False)
          mc.helpLine()
  
          mc.formLayout( self.form, edit=True,
                         attachForm=((self.column, 'top', 0), (self.column, 'left', 0),
                                     (self.column, 'right', 0), (frame, 'left', 0),
                                     (frame, 'bottom', 0), (frame, 'right', 0)),
                         attachNone=((self.column, 'bottom'), (frame, 'top')) )
  
          mc.showWindow(self.name)
          mc.window(self.name, edit=True, width=self.width, height=self.height)
  
  
      def createMenu(self, *args):
          '''
          Create the main menu for the UI
          '''
  
          #generate shelf label by removing ml_
          shelfLabel = self.name.replace('ml_','')
          module = self.module
          if not module:
              module = self.name
  
          #if icon exists, use that
          argString = ''
          if not self.icon:
              argString = ', label="'+shelfLabel+'"'
  
          mc.menu(label='Tools')
          mc.menuItem(label='Add to shelf',
                      command='import ml_utilities;ml_utilities.createShelfButton("import '+module+';'+module+'.ui()", name="'+self.name+'", description="Open the UI for '+self.name+'."'+argString+')')
          if not self.icon:
              mc.menuItem(label='Get Icon',
                          command=(_showHelpCommand(ICON_URL+self.name+'.png')))
          mc.menuItem(label='Get More Tools!',
                      command=(_showHelpCommand(WEBSITE_URL+'/tools/')))
          mc.setParent( '..', menu=True )
  
          mc.menu(label='Help')
          mc.menuItem(label='About', command=self.about)
          mc.menuItem(label='Documentation', command=(_showHelpCommand(TOOL_URL+self.name+'/')))
          mc.menuItem(label='Python Command Documentation', command=(_showHelpCommand(TOOL_URL+'#\%5B\%5B'+self.name+'\%20Python\%20Documentation\%5D\%5D')))
          mc.menuItem(label='Submit a Bug or Request', command=(_showHelpCommand(WEBSITE_URL+'/about/')))
  
          mc.setParent( '..', menu=True )
  
  
      def about(self, *args):
          '''
          This pops up a window which shows the revision number of the current script.
          '''
  
          text='by Morgan Loomis\n\n'
          try:
              __import__(self.module)
              module = sys.modules[self.module]
              text = text+'Revision: '+str(module.__revision__)+'\n'
          except StandardError:
              pass
          try:
              text = text+'ml_utilities Rev: '+str(__revision__)+'\n'
          except StandardError:
              pass
  
          mc.confirmDialog(title=self.name, message=text, button='Close')
  
  
      def buttonWithPopup(self, label=None, command=None, annotation='', shelfLabel='', shelfIcon='render_useBackground', readUI_toArgs={}):
          '''
          Create a button and attach a popup menu to a control with options to create a shelf button or a hotkey.
          The argCommand should return a kwargs dictionary that can be used as args for the main command.
          '''
  
          if self.icon:
              shelfIcon = self.icon
  
          if annotation and not annotation.endswith('.'):
              annotation+='.'
  
          button = mc.button(label=label, command=command, annotation=annotation+' Or right click for more options.')
  
          mc.popupMenu()
          self.shelfMenuItem(command=command, annotation=annotation, shelfLabel=shelfLabel, shelfIcon=shelfIcon)
          self.hotkeyMenuItem(command=command, annotation=annotation)
          return button
  
  
      def shelfMenuItem(self, command=None, annotation='', shelfLabel='', shelfIcon='menuIconConstraints', menuLabel='Create Shelf Button'):
          '''
          This creates a menuItem that can be attached to a control to create a shelf menu with the given command
          '''
          pythonCommand = 'import '+self.name+';'+self.name+'.'+command.__name__+'()'
  
          mc.menuItem(label=menuLabel,
                      command='import ml_utilities;ml_utilities.createShelfButton(\"'+pythonCommand+'\", \"'+shelfLabel+'\", \"'+self.name+'\", description=\"'+annotation+'\", image=\"'+shelfIcon+'\")',
                      enableCommandRepeat=True,
                      image=shelfIcon)
  
  
      def hotkeyMenuItem(self, command=None, annotation='', menuLabel='Create Hotkey'):
          '''
          This creates a menuItem that can be attached to a control to create a hotkey with the given command
          '''
          melCommand = 'import '+self.name+';'+self.name+'.'+command.__name__+'()'
          mc.menuItem(label=menuLabel,
                      command='import ml_utilities;ml_utilities.createHotkey(\"'+melCommand+'\", \"'+self.name+'\", description=\"'+annotation+'\")',
                      enableCommandRepeat=True,
                      image='commandButton')
  
  
      def selectionField(self, label='', annotation='', channel=False, text=''):
          '''
          Create a field with a button that adds the selection to the field.
          '''
          field = mc.textFieldButtonGrp(label=label, text=text,
                                        buttonLabel='Set Selected')
          mc.textFieldButtonGrp(field, edit=True, buttonCommand=partial(self._populateSelectionField, channel, field))
          return field
  
  
      def _populateSelectionField(self, channel, field, *args):
  
          selectedChannels = None
          if channel:
              selectedChannels = getSelectedChannels()
              if not selectedChannels:
                  raise RuntimeError('Please select an attribute in the channelBox.')
              if len(selectedChannels) > 1:
                  raise RuntimeError('Please select only one attribute.')
  
          sel = mc.ls(sl=True)
          if not sel:
              raise RuntimeError('Please select a node.')
          if len(sel) > 1:
              raise RuntimeError('Please select only one node.')
  
          selection = sel[0]
          if selectedChannels:
              selection = selection+'.'+selectedChannels[0]
  
          mc.textFieldButtonGrp(field, edit=True, text=selection)
  
  
      def selectionList(self, channel=False, **kwargs):
          tsl = mc.textScrollList(**kwargs)
          mc.button(label='Append Selected', command=partial(self._populateSelectionList, channel, tsl))
          return tsl
  
  
      def _populateSelectionList(self, channel, control, *args):
  
          selectedChannels = None
          if channel:
              selectedChannels = getSelectedChannels()
              if not selectedChannels:
                  raise RuntimeError('Please select an attribute in the channelBox.')
              if len(selectedChannels) > 1:
                  raise RuntimeError('Please select only one attribute.')
  
          sel = mc.ls(sl=True)
          if not sel:
              raise RuntimeError('Please select a node.')
          if len(sel) > 1:
              raise RuntimeError('Please select only one node.')
  
          selection = sel[0]
          if selectedChannels:
              selection = selection+'.'+selectedChannels[0]
  
          mc.textScrollList(control, edit=True, append=[selection])
  
  
      class ButtonWithPopup():
  
          def __init__(self, label=None, name=None, command=None, annotation='', shelfLabel='', shelfIcon='render_useBackground', readUI_toArgs={}, **kwargs):
              '''
              The fancy part of this object is the readUI_toArgs argument.
              '''
  
              self.uiArgDict = readUI_toArgs
              self.name = name
              self.command = command
              self.kwargs = kwargs
  
              self.annotation = annotation
              self.shelfLabel = shelfLabel
              self.shelfIcon = shelfIcon
  
              if annotation and not annotation.endswith('.'):
                  annotation+='.'
  
              button = mc.button(label=label, command=self.runCommand, annotation=annotation+' Or right click for more options.')
  
              mc.popupMenu()
              mc.menuItem(label='Create Shelf Button', command=self.createShelfButton, image=shelfIcon)
  
              mc.menuItem(label='Create Hotkey',
                          command=self.createHotkey, image='commandButton')
  
  
          def readUI(self):
              '''
              This reads the UI elements and turns them into arguments saved in a kwargs style member variable
              '''
  
              if self.uiArgDict:
                  #this is some fanciness to read the values of UI elements and generate or run the resulting command
                  #keys represent the argument names, the values are UI elements
                  for k in self.uiArgDict.keys():
  
                      uiType = mc.objectTypeUI(self.uiArgDict[k])
                      value = None
                      if uiType == 'rowGroupLayout':
                          controls = mc.layout(self.uiArgDict[k], query=True, childArray=True)
                          if 'check1' in controls:
                              value = mc.checkBoxGrp(self.uiArgDict[k], query=True, value1=True)
                          elif 'radio1' in controls:
                              #this will be a 1 based index, we want to return formatted button name?
                              value = mc.radioButtonGrp(self.uiArgDict[k], query=True, select=True)-1
                          elif 'slider' in controls:
                              try:
                                  value = mc.floatSliderGrp(self.uiArgDict[k], query=True, value=True)
  
                              except StandardError:
                                  pass
                              try:
                                  value = mc.intSliderGrp(self.uiArgDict[k], query=True, value=True)
  
                              except StandardError:
                                  pass
                          elif 'field1' in controls:
                              value = mc.floatFieldGrp(self.uiArgDict[k], query=True, value1=True)
                          elif 'OptionMenu' in controls:
                              value = mc.optionMenuGrp(self.uiArgDict[k], query=True, select=True)
                      else:
                          OpenMaya.MGlobal.displayWarning('Cannot read '+uiType+' UI element: '+self.uiArgDict[k])
                          continue
  
                      self.kwargs[k] = value
  
  
          def runCommand(self, *args):
              '''
              This compiles the kwargs and runs the command directly
              '''
              self.readUI()
              self.command(**self.kwargs)
  
  
          def stringCommand(self):
              '''
              This takes the command
              '''
  
              cmd = 'import '+self.name+'\n'+self.name+'.'+self.command.__name__+'('
  
              comma = False
              for k,v in self.kwargs.items():
                  value = v
                  if isinstance(v, str):
                      value = "'"+value+"'"
                  else:
                      value = str(value)
  
                  if comma:
                      cmd+=', '
                  cmd = cmd+k+'='+value
  
                  comma = True
  
              cmd+=')'
  
              return cmd
  
  
          def createShelfButton(self,*args):
              '''
              Builds the command and creates a shelf button out of it
              '''
              self.readUI()
              pythonCommand = self.stringCommand()
              createShelfButton(pythonCommand, self.shelfLabel, self.name, description=self.annotation, image=self.shelfIcon)
  
  
          def createHotkey(self, annotation='', menuLabel='Create Hotkey'):
              '''
              Builds the command and prompts to create a hotkey.
              '''
  
              self.readUI()
              pythonCommand = self.stringCommand()
              createHotkey(pythonCommand, self.name, description=self.annotation)
  
  
  class SkipUndo():
      '''
      Skips adding the encapsulated commands to the undo queue, so that you
      cannot undo them.
      '''
  
      def __enter__(self):
          '''
          Turn off undo
          '''
          mc.undoInfo(stateWithoutFlush=False)
  
      def __exit__(self,*args):
          '''
          Turn on undo
          '''
          mc.undoInfo(stateWithoutFlush=True)
  
  
  class UndoChunk():
      '''
      In versions of maya before 2011, python doesn't always undo properly, so in
      some cases we have to manage the undo queue ourselves.
      '''
  
      def __init__(self, force=False):
          self.force = force
  
      def __enter__(self):
          '''open the undo chunk'''
          if self.force or MAYA_VERSION < 2011:
              self.force = True
              mc.undoInfo(openChunk=True)
  
      def __exit__(self, *args):
          '''close the undo chunk'''
          if self.force:
              mc.undoInfo(closeChunk=True)
  
  
  class Vector:
  
      def __init__(self, x=0, y=0, z=0):
          '''
          Initialize the vector with 3 values, or else
          '''
  
          if self._isCompatible(x):
              x = x[0]
              y = x[1]
              z = x[2]
          self.x = x
          self.y = y
          self.z = z
  
      def __repr__(self):
          return 'Vector({0:.2f}, {1:.2f}, {2:.2f})'.format(*self)
  
      #iterator methods
      def __iter__(self):
          return iter((self.x, self.y, self.z))
  
      def __getitem__(self, key):
          return (self.x, self.y, self.z)[key]
  
      def __setitem__(self, key, value):
          [self.x, self.y, self.z][key] = value
  
      def __len__(self):
          return 3
  
      def _isCompatible(self, other):
          '''
          Return true if the provided argument is a vector
          '''
          if isinstance(other,(Vector,list,tuple)) and len(other)==3:
              return True
          return False
  
  
      def __add__(self, other):
  
          if not self._isCompatible(other):
              raise TypeError('Can only add to another vector of the same dimension.')
  
          return Vector(*[a+b for a,b in zip(self,other)])
  
  
      def __sub__(self, other):
  
          if not self._isCompatible(other):
              raise TypeError('Can only subtract another vector of the same dimension.')
  
          return Vector(*[a-b for a,b in zip(self,other)])
  
  
      def __mul__(self, other):
  
          if self._isCompatible(other):
              return Vector(*[a*b for a,b in zip(self,other)])
          elif isinstance(other, (float,int,long)):
              return Vector(*[x*float(other) for x in self])
          else:
              raise TypeError("Can't multiply {} with {}".format(self, other))
  
  
      def __div__(self, other):
          if isinstance(other, (float,int,long)):
              return Vector(*[x/float(other) for x in self])
          else:
              raise TypeError("Can't divide {} by {}".format(self, other))
  
  
      def magnitude(self):
          return math.sqrt(sum([x**2 for x in self]))
  
  
      def normalize(self):
          d = self.magnitude()
          if d:
              self.x /= d
              self.y /= d
              self.z /= d
          return self
  
  
      def normalized(self):
          d = self.magnitude()
          if d:
              return self/d
          return self
  
  
      def dot(self, other):
          if not self._isCompatible(other):
              raise TypeError('Can only perform dot product with another Vector object of equal dimension.')
          return sum([a*b for a,b in zip(self,other)])
  
  
      def cross(self, other):
          if not self._isCompatible(other):
              raise TypeError('Can only perform cross product with another Vector object of equal dimension.')
          return Vector(self.y * other.z - self.z * other.y,
                         -self.x * other.z + self.z * other.x,
                         self.x * other.y - self.y * other.x)
  
  
  
  #      ______________________
  # - -/__ Revision History __/- - - - - - - - - - - - - - - - - - - - - - - -
  #
  # Revision 1: : First publish
  #
  # Revision 2: 2011-05-04 : Fixed error in frameRange.
  #
  # Revision 3: 2012-05-31 : Adding Menu and Icon update to UI, adding KeyframeSelection object, and a few random utility functions.
  #
  # Revision 4: 2012-06-01 : Fixing bug with UI icons
  #
  # Revision 5: 2012-07-23 : Expanding and bug fixing Keyselection, added SkipUndo, minor bug fixes.
  #
  # Revision 6: 2012-07-23 : KeySelection bug fix
  #
  # Revision 7: 2012-08-07 : Minor bug with Keyselection, adding functions.
  #
  # Revision 8: 2012-11-06 : Backwards incompatable update to KeySelection, and several new functions.
  #
  # Revision 9: 2013-10-29 : Update to support more options for goToKeyframe, update to preserve isolateSelect.
  #
  # Revision 10: 2014-03-01 : adding category, updating contact.
  #
  # Revision 11: 2014-03-08 : Fixed keySelection bug with keyed channels.
  #
  # Revision 12: 2015-04-27 : updated for ml_puppet support
  #
  # Revision 13: 2015-05-13 : UI function updates
  #
  # Revision 14: 2015-05-16 : minor update to setAnimValue
  #
  # Revision 15: 2015-05-18 : Small bugfix in matchBake
  #
  # Revision 16: 2016-02-29 : Support for animCurveEditor and fixing some old hotkey bugs.
  #
  # Revision 17: 2016-03-23 : keySelection bug fix.
  #
  # Revision 18: 2016-05-05 : Update matchBake to support tangent weights.
  #
  # Revision 19: 2016-06-02 : temp patching hotkey issue with > 2015
  #
  # Revision 20: 2016-07-31 : Update to MlUi to support subclassing.
  #
  # Revision 21: 2016-07-31 : MlUi bug fixes.
  #
  # Revision 21: 2016-08-11 : windows support for icons
  #
  # Revision 22: 2016-10-01 : changing frameRange to return consistent results when returning timeline or selection.
  #
  # Revision 23: 2016-10-12 : Tangent bug fixes for 2016.5
  #
  # Revision 24: 2016-10-31 : Adding selection field to mlUI
  #
  # Revision 25: 2016-11-21 : silly icon path bug
  #
  # Revision 26: 2016-12-05 : Adding getSkinCluster
  #
  # Revision 27: 2016-12-10 : Adding Vector class to remove euclid dependency
  #
  # Revision 28: 2017-03-20 : bug fix and support for ml_puppet
  #
  # Revision 29: 2017-04-25 : matchBake support input frames
  #
  # Revision 30: 2017-06-13 : unify version test, isolate view update
  #
  # Revision 31: 2017-06-30 : getCamera support for stereo camera
  #
  # Revision 32: 2018-02-17 : Updating license to MIT.
  #
  # Revision 33: 2018-07-18 : getNamespace bug
  #
  # Revision 34: 2019-03-07 : github URL update