Image
-- WARNING: This script requires AE_Utilities.lua of version 1.11 or later!


-- **************************************************
-- Provide Moho with the name of this script object
-- **************************************************

ScriptName = "AE_KeyTools"

-- **************************************************
-- General information about this script
-- **************************************************

AE_KeyTools = {}

function AE_KeyTools:Name()
	return self:Localize("Name")
end

function AE_KeyTools:Version()
	return "2.48"
end

function AE_KeyTools:Description()
	return self:Localize("Description")
end

function AE_KeyTools:Creator()
	return "A.Evseeva with a little help from Stan"
end

function AE_KeyTools:UILabel()
	return self:Localize("UILabel")
end


function AE_KeyTools:LoadPrefs(prefs)
	self.applyToChildLayers = prefs:GetBool("AE_KeyTools.applyToChildLayers", true)
	self.timelinevisibleOnly = prefs:GetBool("AE_KeyTools.timelinevisibleOnly", false)
	self.applyToRefs = prefs:GetBool("AE_KeyTools.applyToRefs", false)
	self.precalcCycles = prefs:GetBool("AE_KeyTools.precalcCycles", true)
	self.ignoreStringChannels = prefs:GetBool("AE_KeyTools.ignoreStringChannels", false)
	self.ignoreHiddenBones = prefs:GetBool("AE_KeyTools.ignoreHiddenBones", true)
	self.trackAnimOption = prefs:GetString("AE_KeyTools.trackAnimOption", "active")
	self.ifEqual = prefs:GetBool("AE_KeyTools.ifEqual", false)
	self.selectedBonesOnly = prefs:GetBool("AE_KeyTools.selectedBonesOnly", false)	
	self.useStepIntervals = prefs:GetBool("AE_KeyTools.useStepIntervals", false)
end

function AE_KeyTools:SavePrefs(prefs)
	prefs:SetBool("AE_KeyTools.applyToChildLayers", self.applyToChildLayers)
	prefs:SetBool("AE_KeyTools.timelinevisibleOnly", self.timelinevisibleOnly)
	prefs:SetBool("AE_KeyTools.applyToRefs", self.applyToRefs)	
	prefs:SetBool("AE_KeyTools.precalcCycles", self.precalcCycles)
	prefs:SetBool("AE_KeyTools.ignoreStringChannels", self.ignoreStringChannels)	
	prefs:SetBool("AE_KeyTools.ignoreHiddenBones", self.ignoreHiddenBones)	
	prefs:SetString("AE_KeyTools.trackAnimOption", self.trackAnimOption)	
	prefs:SetBool("AE_KeyTools.ifEqual", self.ifEqual)
	prefs:SetBool("AE_KeyTools.selectedBonesOnly", self.selectedBonesOnly)	
	prefs:SetBool("AE_KeyTools.useStepIntervals", self.useStepIntervals)
end

function AE_KeyTools:ResetPrefs()
	self.applyToChildLayers = true
	self.timelinevisibleOnly = false
	self.applyToRefs = false
	self.precalcCycles = true
	self.ignoreStringChannels = false
	self.ignoreHiddenBones = true
	self.trackAnimOption = "active"
	self.ifEqual = false
	self.selectedBonesOnly = false
	self.useStepIntervals = false
end

-- **************************************************
-- Is Relevant / Is Enabled
-- **************************************************

function AE_KeyTools:IsRelevant(moho)
	return true
end

function AE_KeyTools:IsEnabled(moho)
	return true
end

function AE_KeyTools:OnMouseDown(moho, mouseEvent)

end

-- **************************************************
-- Tool level variables to bind UI
-- **************************************************

AE_KeyTools.applyToChildLayers = true
AE_KeyTools.timelinevisibleOnly = false
AE_KeyTools.applyToRefs = false
AE_KeyTools.precalcCycles = true
AE_KeyTools.ignoreStringChannels = false
AE_KeyTools.ignoreHiddenBones = true
AE_KeyTools.fromFrame = 0
AE_KeyTools.toFrame = 0
AE_KeyTools.sourceFilePath = nil
AE_KeyTools.sourceRootLayerUUID = nil
AE_KeyTools.sourceRootLayerName = nil
AE_KeyTools.targetFilePath = nil
AE_KeyTools.storedValues = {}
AE_KeyTools.keyInterp = nil
AE_KeyTools.selectAllKeys = false
AE_KeyTools.trackAnimOption = "active"
AE_KeyTools.ifEqual = false
AE_KeyTools.selectedBonesOnly = false
AE_KeyTools.useStepIntervals = false
AE_KeyTools.regtab = {
  ["("] = "%(",
  [")"] = "%)",
  ["."] = "%.",
  ["%"] = "%%",
  ["+"] = "%+",
  ["-"] = "%-",
  ["*"] = "%*",
  ["?"] = "%?",
  ["["] = "%[",
  ["]"] = "%]",
  ["^"] = "%^",
  ["$"] = "%$",
}


-- **************************************************
-- Tool options - create and respond to tool's UI
-- **************************************************

AE_KeyTools.COPY			 		= MOHO.MSG_BASE 
AE_KeyTools.PASTE			 		= MOHO.MSG_BASE + 1
AE_KeyTools.ADD	 					= MOHO.MSG_BASE + 2
AE_KeyTools.APPLY_TO_REFS			= MOHO.MSG_BASE + 14
AE_KeyTools.APPLY_TO_CHILD_LAYERS	= MOHO.MSG_BASE + 3
AE_KeyTools.TIMELINEVISIBLE_ONLY	= MOHO.MSG_BASE + 27
AE_KeyTools.PRECALC_CYCLES	 		= MOHO.MSG_BASE + 4
AE_KeyTools.IGNORE_STRING_CHANNELS	= MOHO.MSG_BASE + 5
AE_KeyTools.IGNORE_HIDDEN_BONES		= MOHO.MSG_BASE + 16
AE_KeyTools.CLEANUP					= MOHO.MSG_BASE + 6
AE_KeyTools.SHOWANIM				= MOHO.MSG_BASE + 7
AE_KeyTools.HIDEANIM				= MOHO.MSG_BASE + 8
AE_KeyTools.SELECTKEY				= MOHO.MSG_BASE + 9
AE_KeyTools.DESELECTKEYS			= MOHO.MSG_BASE + 18
AE_KeyTools.PASTEKEYS 				= MOHO.MSG_BASE + 10
AE_KeyTools.COPYPASTEINTERP 		= MOHO.MSG_BASE + 15
AE_KeyTools.SHOWANIM1				= MOHO.MSG_BASE + 11
AE_KeyTools.NUDGERIGHT				= MOHO.MSG_BASE + 12
AE_KeyTools.NUDGERIGHTx10			= MOHO.MSG_BASE + 25
AE_KeyTools.NUDGELEFT				= MOHO.MSG_BASE + 13
AE_KeyTools.NUDGELEFTx10			= MOHO.MSG_BASE + 26
AE_KeyTools.ACTIVEANIM 				= MOHO.MSG_BASE + 19
AE_KeyTools.LONGANIM 				= MOHO.MSG_BASE + 20
AE_KeyTools.ANYANIM 				= MOHO.MSG_BASE + 21
AE_KeyTools.EQUALCHECK				= MOHO.MSG_BASE + 22
AE_KeyTools.SELECTED_BONES_ONLY		= MOHO.MSG_BASE + 23
AE_KeyTools.USE_STEP_INTERVALS		= MOHO.MSG_BASE + 24

-- constants to make menu
AE_KeyTools.PREVIOUS_KEY 			= MOHO.MSG_BASE + 100
AE_KeyTools.NEXT_KEY 				= MOHO.MSG_BASE + 101
AE_KeyTools.CURRENT_VALUES 			= MOHO.MSG_BASE + 102 
AE_KeyTools.ZEROFRAME_VALUES 		= MOHO.MSG_BASE + 103
AE_KeyTools.PREVIOUSMARKER_VALUES 	= MOHO.MSG_BASE + 104
AE_KeyTools.getFromKey = AE_KeyTools.PREVIOUS_KEY

--nudgearea menu
AE_KeyTools.NUDGEAREA_TOTAL			= MOHO.MSG_BASE + 30
AE_KeyTools.NUDGEAREA_VISIBLE		= MOHO.MSG_BASE + 31
AE_KeyTools.NUDGEAREA_SELECTEDKEYS	= MOHO.MSG_BASE + 32
AE_KeyTools.nudgeArea = AE_KeyTools.NUDGEAREA_TOTAL

--selectkeys menu
AE_KeyTools.SELECTKEYS_FRAME		= MOHO.MSG_BASE + 40
AE_KeyTools.SELECTKEYS_AREA			= MOHO.MSG_BASE + 41
AE_KeyTools.SELECTKEYS_TOTAL		= MOHO.MSG_BASE + 42
AE_KeyTools.selectKeys = AE_KeyTools.SELECTKEYS_FRAME
AE_KeyTools.SELECTKEYS_COLORED		= MOHO.MSG_BASE + 17


function AE_KeyTools:DoLayout(moho, layout)

	self.copy = LM.GUI.Button(self:Localize("COPY"), self.COPY)
	layout:AddChild(self.copy)
	self.copy:SetToolTip(self:Localize("CopyTooltip"))
	layout:AddPadding(-15)
	self.paste = LM.GUI.Button(self:Localize("PASTE"), self.PASTE)
	layout:AddChild(self.paste)
	self.paste:SetToolTip(self:Localize("PasteTooltip"))
	
	self.fromFrameField = LM.GUI.DynamicText(self:Localize("From") .. "000")
	layout:AddChild(self.fromFrameField)
	
	self.copypasteInterpButton = LM.GUI.Button("C", self.COPYPASTEINTERP)
	layout:AddChild(self.copypasteInterpButton)
	self.copypasteInterpButton:SetToolTip(self:Localize("CopyPasteInterpTooltip"))	
	
	self.pasteKeysButton = LM.GUI.ImageButton("ScriptResources/align_layers", self.Localize("PasteKeysTooltip"), false, self.PASTEKEYS, true)
	layout:AddChild(self.pasteKeysButton)
	self.pasteKeysButton:SetToolTip(self:Localize("PasteKeysTooltip"))

	self.setKey = LM.GUI.Button(self:Localize("ADD"), self.ADD)
	layout:AddChild(self.setKey)
	self.setKey:SetToolTip(self:Localize("AddTooltip"))
	
	self.getFromKeyMenu = LM.GUI.Menu("")
	self.getFromKeyMenu_popup = LM.GUI.PopupMenu(110, true)
	self.getFromKeyMenu_popup:SetMenu(self.getFromKeyMenu)
	self.getFromKeyMenu:AddItem(self:Localize("PreviousKey"), 0, self.PREVIOUS_KEY)
	self.getFromKeyMenu:AddItem(self:Localize("NextKey"), 0, self.NEXT_KEY)
	self.getFromKeyMenu:AddItem(self:Localize("CurrentValue"), 0, self.CURRENT_VALUES)
	self.getFromKeyMenu:AddItem(self:Localize("ZeroFrame"), 0, self.ZEROFRAME_VALUES)
	self.getFromKeyMenu:AddItem(self:Localize("PreviousMarker"), 0, self.PREVIOUSMARKER_VALUES)
	layout:AddChild(self.getFromKeyMenu_popup)
	self.getFromKeyMenu_popup:SetToolTip(self:Localize("MenuTooltip"))
	
	self.trackAnimMenu = LM.GUI.Menu("")
	self.trackAnimMenu:AddItem("Active animation only", 0,  self.ACTIVEANIM)
	self.trackAnimMenu:AddItem("Any animation after frame 1", 0,  self.LONGANIM)
	self.trackAnimMenu:AddItem("Any animation after frame 0", 0,  self.ANYANIM)	
	self.trackAnimPopup = LM.GUI.ImagePopupMenu("ScriptResources/ae_trackanimoptions", true, true)
	self.trackAnimPopup:SetMenu(self.trackAnimMenu)
	layout:AddChild(self.trackAnimPopup)
	
	self.equalCheck = LM.GUI.ImageButton("ScriptResources/ae_equal", "Set key even if value does not change", true, self.EQUALCHECK, true)
	layout:AddChild(self.equalCheck)
	
	self.includingsMenu = LM.GUI.Menu("Incl...")
	self.includingsMenu_popup = LM.GUI.PopupMenu(60, false)
	self.includingsMenu_popup:SetMenu(self.includingsMenu)
	self.includingsMenu:AddItem(self:Localize("ApplyToChildLayers"), 0, self.APPLY_TO_CHILD_LAYERS)	
	self.includingsMenu:AddItem("Timeline-visible only", 0, self.TIMELINEVISIBLE_ONLY)
	self.includingsMenu:AddItem(self:Localize("ApplyToRefs"), 0, self.APPLY_TO_REFS)	
	self.includingsMenu:AddItem(self:Localize("PrecalcCycles"), 0, self.PRECALC_CYCLES)	
	self.includingsMenu:AddItem(self:Localize("IgnoreStringChannels"), 0, self.IGNORE_STRING_CHANNELS)	
	self.includingsMenu:AddItem(self:Localize("IgnoreHiddenBones"), 0, self.IGNORE_HIDDEN_BONES)
	self.includingsMenu:AddItem("Selected bones only", 0, self.SELECTED_BONES_ONLY)
	self.includingsMenu:AddItem("Use step intervals", 0, self.USE_STEP_INTERVALS)
	layout:AddChild(self.includingsMenu_popup)
	self.includingsMenu_popup:SetToolTip(self:Localize("IncludingsMenuTooltip"))
	
	self.cleanButton = LM.GUI.Button(self:Localize("CLEANUP"), self.CLEANUP)
	layout:AddChild(self.cleanButton)
	self.cleanButton:SetToolTip(self:Localize("CleanupTooltip"))
	
	
	self.nudgeLeftButton = LM.GUI.ImageButton("curs_hresize_left", self:Localize("NudgeLeftTooltip"), false, self.NUDGELEFT, true)
	layout:AddChild(self.nudgeLeftButton)
	self.nudgeLeftButton:SetAlternateMessage(self.NUDGELEFTx10)
	self.nudgeLeftButton:SetToolTip(self:Localize("NudgeLeftTooltip"))
	layout:AddPadding(-15)
	self.nudgeRightButton = LM.GUI.ImageButton("curs_hresize_right", self:Localize("NudgeRightTooltip"), false, self.NUDGERIGHT, true)
	layout:AddChild(self.nudgeRightButton)
	self.nudgeRightButton:SetAlternateMessage(self.NUDGERIGHTx10)
	self.nudgeRightButton:SetToolTip(self:Localize("NudgeRightTooltip"))
	layout:AddPadding(-13)
	self.nudgeAreaMenu = LM.GUI.Menu("")
	self.nudgeAreaMenu_popup = LM.GUI.PopupMenu(70, true)
	self.nudgeAreaMenu_popup:SetMenu(self.nudgeAreaMenu)
	self.nudgeAreaMenu:AddItem(self:Localize("total"), 0, self.NUDGEAREA_TOTAL)
	self.nudgeAreaMenu:AddItem(self:Localize("visible"), 0, self.NUDGEAREA_VISIBLE)
	self.nudgeAreaMenu:AddItem(self:Localize("selected"), 0, self.NUDGEAREA_SELECTEDKEYS)
	layout:AddChild(self.nudgeAreaMenu_popup)
	self.nudgeAreaMenu_popup:SetToolTip("Filter keys to nudge")
	
	
	self.selectKeyButton = LM.GUI.Button("select", self.SELECTKEY)
	layout:AddChild(self.selectKeyButton)
	self.selectKeyButton:SetAlternateMessage(self.SELECTKEYS_COLORED)
	self.selectKeyButton:SetToolTip(self:Localize("selectKeyTooltip"))	
	layout:AddPadding(-13)
	self.selectKeysMenu = LM.GUI.Menu("")
	self.selectKeysMenu_popup = LM.GUI.PopupMenu(60, true)
	self.selectKeysMenu_popup:SetMenu(self.selectKeysMenu)
	self.selectKeysMenu:AddItem(self:Localize("frame"), 0, self.SELECTKEYS_FRAME)
	self.selectKeysMenu:AddItem(self:Localize("area"), 0, self.SELECTKEYS_AREA)
	self.selectKeysMenu:AddItem(self:Localize("total"), 0, self.SELECTKEYS_TOTAL)
	layout:AddChild(self.selectKeysMenu_popup)
	self.selectKeysMenu_popup:SetToolTip("Area to select keys within")	

	self.deselectKeysButton = LM.GUI.Button("deselect", self.DESELECTKEYS)
	--layout:AddChild(self.deselectKeysButton)	
	
	
	self.showAnimButton = LM.GUI.Button("V", self.SHOWANIM)
	layout:AddChild(self.showAnimButton)
	self.showAnimButton:SetToolTip(self:Localize("ShowAnimTooltip"))
	self.showAnimButton1 = LM.GUI.Button("v", self.SHOWANIM1)
	layout:AddPadding(-15)
	layout:AddChild(self.showAnimButton1)
	self.showAnimButton1:SetToolTip(self:Localize("ShowAnim1Tooltip"))	
	self.hideAnimButton = LM.GUI.Button("X", self.HIDEANIM)
	layout:AddPadding(-15)
	layout:AddChild(self.hideAnimButton)
	self.hideAnimButton:SetToolTip(self:Localize("HideAnimTooltip"))	
	
end

function AE_KeyTools:UpdateWidgets(moho)
	self.fromFrameField:SetValue(self:Localize("From") .. self.fromFrame)
	self.toFrame = moho.frame
	self.getFromKeyMenu:SetChecked(self.getFromKey, true)
	self.nudgeAreaMenu:SetChecked(self.nudgeArea, true)
	self.selectKeysMenu:SetChecked(self.selectKeys, true)
	
	self.trackAnimMenu:UncheckAll()
	if self.trackAnimOption == "active" then self.trackAnimMenu:SetChecked(self.ACTIVEANIM, true)
	elseif self.trackAnimOption == "long"  then self.trackAnimMenu:SetChecked(self.LONGANIM, true)
	elseif self.trackAnimOption == "any"  then self.trackAnimMenu:SetChecked(self.ANYANIM, true)
	end
	self.equalCheck:SetValue(self.ifEqual)
	
	self.includingsMenu:SetChecked(self.APPLY_TO_CHILD_LAYERS, self.applyToChildLayers)
	self.includingsMenu:SetChecked(self.TIMELINEVISIBLE_ONLY, self.timelinevisibleOnly)	
	self.includingsMenu:SetChecked(self.APPLY_TO_REFS, self.applyToRefs)
	self.includingsMenu:SetChecked(self.PRECALC_CYCLES, self.precalcCycles)
	self.includingsMenu:SetChecked(self.IGNORE_STRING_CHANNELS, self.ignoreStringChannels)	
	self.includingsMenu:SetChecked(self.IGNORE_HIDDEN_BONES, self.ignoreHiddenBones)	
	self.includingsMenu:SetChecked(self.SELECTED_BONES_ONLY, self.selectedBonesOnly)
	self.includingsMenu:SetChecked(self.USE_STEP_INTERVALS, self.useStepIntervals)
	
	if self.keyInterp then self.copypasteInterpButton:SetLabel("P", false) else  self.copypasteInterpButton:SetLabel("C", false) end
end

function AE_KeyTools:HandleMessage(moho, view, msg)
	if msg == self.COPY then
		local currentFrame = moho.frame
		self.fromFrame = currentFrame
		self.fromFrameField:SetValue(self:Localize("From") .. self.fromFrame)
		self.sourceFilePath = moho.document:Path()
		self.sourceRootLayerUUID = moho.layer:UUID()
		self.sourceRootLayerName = moho.layer:Name()
		self.sourceRootLayerPath = self:GetLayerPath(moho.layer)
		-- if red marker store values with params
		if MOHO.MohoGlobals.PlayEnd == -1 then 
			self:StoreValues(moho)
		else 
			self:StoreValues(moho, true, MOHO.MohoGlobals.PlayEnd)
		end
	elseif msg == self.PREVIOUS_KEY or msg == self.NEXT_KEY or msg == self.CURRENT_VALUES  or msg == self.ZEROFRAME_VALUES or msg == self.PREVIOUSMARKER_VALUES then
		self.getFromKey = self.getFromKeyMenu:FirstCheckedMsg()
	elseif msg == self.NUDGEAREA_TOTAL or msg == self.NUDGEAREA_VISIBLE or msg == self.NUDGEAREA_SELECTEDKEYS then
		self.nudgeArea = self.nudgeAreaMenu:FirstCheckedMsg()
	elseif msg == self.SELECTKEYS_FRAME or msg == self.SELECTKEYS_AREA or msg == self.SELECTKEYS_TOTAL then
		self.selectKeys = self.selectKeysMenu:FirstCheckedMsg()	
	elseif msg == self.APPLY_TO_CHILD_LAYERS then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.applyToChildLayers = self.includingsMenu:IsChecked(self.APPLY_TO_CHILD_LAYERS)
	elseif msg == self.TIMELINEVISIBLE_ONLY then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.timelinevisibleOnly = self.includingsMenu:IsChecked(self.TIMELINEVISIBLE_ONLY)		
	elseif msg == self.APPLY_TO_REFS then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.applyToRefs = self.includingsMenu:IsChecked(self.APPLY_TO_REFS)		
	elseif msg == self.PRECALC_CYCLES then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.precalcCycles = self.includingsMenu:IsChecked(self.PRECALC_CYCLES)
	elseif msg == self.IGNORE_STRING_CHANNELS then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.ignoreStringChannels = self.includingsMenu:IsChecked(self.IGNORE_STRING_CHANNELS)
	elseif msg == self.IGNORE_HIDDEN_BONES then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.ignoreHiddenBones = self.includingsMenu:IsChecked(self.IGNORE_HIDDEN_BONES)
	elseif msg == self.SELECTED_BONES_ONLY then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.selectedBonesOnly = self.includingsMenu:IsChecked(self.SELECTED_BONES_ONLY)
	elseif msg == self.USE_STEP_INTERVALS then
		self.includingsMenu:SetChecked(msg, not self.includingsMenu:IsChecked(msg))
		self.useStepIntervals = self.includingsMenu:IsChecked(self.USE_STEP_INTERVALS)	
	elseif msg == self.ACTIVEANIM then self.trackAnimOption = "active"
	elseif msg == self.LONGANIM then self.trackAnimOption = "long"
	elseif msg == self.ANYANIM then self.trackAnimOption = "any"
	elseif msg == self.EQUALCHECK then self.ifEqual = self.equalCheck:Value()
	elseif msg == self.PASTE then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()
		if moho.document:Path() ~= self.sourceFilePath then 
			self:RestoreValues(moho, self.sourceRootLayerPath, self:GetLayerPath(moho.layer))
		elseif moho.layer:UUID() ~= self.sourceRootLayerUUID then
			self:RestoreValues(moho, self.sourceRootLayerPath, self:GetLayerPath(moho.layer))			
		else
			self:CopyPasteFrames(moho)
		end
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()
	elseif msg == self.PASTEKEYS then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:PasteSelectedKeys(moho)
		moho:UpdateUI()		
	elseif msg == self.ADD then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:SetKey(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()
	elseif msg == self.CLEANUP then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:RemoveUnusedKeys(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()	
	elseif msg == self.SHOWANIM then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:ShowHideAnimLayers(moho,true,false)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()	
	elseif msg == self.SHOWANIM1 then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:ShowHideAnimLayers(moho,true,true)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()			
	elseif msg == self.HIDEANIM then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:ShowHideAnimLayers(moho,false)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()
	elseif msg == self.SELECTKEY then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:SelectKeys(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()
	elseif msg == self.SELECTKEYS_COLORED then self:SelectKeys_colored(moho)
	elseif msg == self.DESELECTKEYS then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:DeselectKeys(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()		
	elseif msg == self.NUDGELEFT or msg == self.NUDGELEFTx10 or msg == self.NUDGERIGHT or msg == self.NUDGERIGHTx10 then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		if msg == self.NUDGELEFT then self:NudgeKeys(moho,false) end
		if msg == self.NUDGELEFTx10 then self:NudgeKeys(moho,false, 10) end
		if msg == self.NUDGERIGHT then self:NudgeKeys(moho,true) end
		if msg == self.NUDGERIGHTx10 then self:NudgeKeys(moho,true, 10) end		
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()	
	elseif msg == self.COPYPASTEINTERP then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()
		self:CopyPasteInterp(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()		
	end
	
	
	
end

function AE_KeyTools:DrawMe(moho, view)
end

function AE_KeyTools:OnKeyDown(moho, keyEvent)
	--print("keyCode = ", keyEvent.keyCode)
	if keyEvent.altKey and keyEvent.shiftKey and keyEvent.keyCode == LM.GUI.KEY_DOWN then 
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()
		self:NudgeKeys(moho,true, 10)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()	
	elseif keyEvent.altKey and keyEvent.shiftKey and keyEvent.keyCode == LM.GUI.KEY_UP then 
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()
		self:NudgeKeys(moho,false, 10)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()
	elseif keyEvent.ctrlKey and keyEvent.key == "d" then 
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:DeselectKeys(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()			
	end
end

-- **************************************************
-- The guts of this script
-- **************************************************

function AE_KeyTools:CollectExcludeChannels(moho, layer)
	local exclude_channels = {}
	if moho:LayerAsBone(layer) then
		if self.ignoreHiddenBones or self.selectedBonesOnly then
			local skel = moho:LayerAsBone(layer):Skeleton()
			if skel:CountBones() > 0 then
				local maxChannel = layer:CountChannels()-1
				local chInfo = MOHO.MohoLayerChannel:new_local()
				local absNumber = 0
				for i=1, maxChannel do	
					layer:GetChannelInfo(i, chInfo)
					local name = chInfo.name:Buffer()
					if string.sub(name, 1, 5) == "Bone " and chInfo.subChannelCount == skel:CountBones() then
						for j=0, skel:CountBones()-1 do
							if(self.ignoreHiddenBones and skel:Bone(j).fHidden) or (self.selectedBonesOnly and not skel:Bone(j).fSelected) then													
								local hiddenChannel = layer:Channel(i,j,moho.document)
								table.insert(exclude_channels, tostring(hiddenChannel))
							end
						end
					end
					absNumber = absNumber + chInfo.subChannelCount
				end
			end
		end
	end

	return exclude_channels
end

function AE_KeyTools:CheckChannelForAddKey(moho, channel, channelFrame)
	if channel:HasKey(channelFrame) then return false end
	if self.ignoreStringChannels and channel:ChannelType() == MOHO.CHANNEL_STRING then return false end
	if not self.precalcCycles and self:IsCycled(moho,channel,channelFrame) > 0 then return false end
	if self.precalcCycles and self:IsCycled(moho,channel,channelFrame) > 0 then return true end
	--check track anim option menu and return false if condition not met
	if self.trackAnimOption == "active" and channel:Duration() < channelFrame then return false end
	if self.trackAnimOption == "long" and channel:Duration() <= 1 then return false end
	if self.trackAnimOption == "any" and channel:Duration() < 1 then return false end
	if self.getFromKey == self.PREVIOUS_KEY and (not self.useStepIntervals) and channel:GetKeyInterpModeByID(channel:GetClosestKeyID(channelFrame)) == MOHO.INTERP_STEP then return false end
	return true
end

function AE_KeyTools:SetKey(moho)
	local layersCollection = self:GetSelectedLayers(moho)
	local previousMarkerTime = moho.frame
	if self.getFromKey == self.PREVIOUSMARKER_VALUES then
		markerChannel = moho.document.fTimelineMarkers
		previousMarkerTime = markerChannel:GetClosestKeyID(moho.frame - 1)
		previousMarkerTime = markerChannel:GetKeyWhen(previousMarkerTime)
	end
	for id, layer in pairs(layersCollection) do
		if (not self.selectedBonesOnly) or moho:LayerAsBone(layer) then
			local exclude_channels = self:CollectExcludeChannels(moho, layer)

			local curFrame = layer:TotalTimingOffset() + moho.frame
			if curFrame == 0 then return end
			for subId, id, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
				if not(table.contains(exclude_channels, tostring(channel))) then 
					if self:CheckChannelForAddKey(moho, channel, curFrame) and
					chInfo.channelID ~= CHANNEL_DOC_MARKERS and chInfo.channelID ~= CHANNEL_LAYER_MARKERS then
						local derivedChannel = AE_Utilities:GetDerivedChannel(moho, channel)
						local fromFrame = curFrame
						local keyID = channel:GetClosestKeyID(curFrame)
						if self.getFromKey == self.NEXT_KEY then keyID = keyID + 1 end	
						if self.getFromKey == self.ZEROFRAME_VALUES then keyID = 0 end
						if self.getFromKey == self.PREVIOUSMARKER_VALUES then 
							keyID = channel:GetClosestKeyID(layer:TotalTimingOffset() + previousMarkerTime)
						end
						if keyID < channel:CountKeys() then
							if self.getFromKey ~= self.CURRENT_VALUES then fromFrame = channel:GetKeyWhen(keyID) end					
								if derivedChannel.AreDimensionsSplit and channel:AreDimensionsSplit() then
									for sss = 0, 2 do
										local subChannel = derivedChannel:DimensionChannel(sss)
										if subChannel then
											local val = subChannel:GetValue(fromFrame)
											local oldval = subChannel:GetValue(curFrame)
											if self.ifEqual or not AE_Utilities:IsEqualValues(subChannel, val, oldval) then
												subChannel:SetValue(curFrame, val)
											end
										end
									end
								else 
								
									local val = derivedChannel:GetValue(fromFrame)
									local oldval = derivedChannel:GetValue(curFrame)
									if self.ifEqual or not AE_Utilities:IsEqualValues(derivedChannel, val, oldval) then
										derivedChannel:SetValue(curFrame, val)
								end
								
							end
						end
					end
				end
			end
		end
	end
end

function AE_KeyTools:CopyPasteFrames(moho)
	local layersCollection = self:GetSelectedLayers(moho)
	for id, layer in pairs(layersCollection) do
		local fromFrame = layer:TotalTimingOffset() + self.fromFrame
		local toFrame = layer:TotalTimingOffset() + self.toFrame	
		for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
			if channel:Duration() > 0 and (not self.ignoreStringChannels or channel:ChannelType() ~= MOHO.CHANNEL_STRING) then				
				local derivedChannel = AE_Utilities:GetDerivedChannel(moho, channel)
				local val = derivedChannel:GetValue(fromFrame)
				if self.precalcCycles and self:IsCycled(moho,channel,fromFrame)>0 then
					val = self:GetCycledValue(moho, derivedChannel, fromFrame)
				end
				derivedChannel:SetValue(toFrame, val)
			end
		end
	end
end

function AE_KeyTools:TreatAsCicleKeyframe(moho, channel, keyFrame, keysToDelete)
	if channel:GetKeyInterpMode(keyFrame) ~= MOHO.INTERP_CYCLE then return false end
	local interp = MOHO.InterpSetting:new_local()
	channel:GetKeyInterp(keyFrame, interp)
	local startCicle = interp.val2
	if startCicle == -1 then startCicle = keyFrame - interp.val1 end
	for f = startCicle, keyFrame - 1 do		
		if channel:HasKey(f) then
			local isRealKey = true
			for i = #keysToDelete, 1, -1 do
				if keysToDelete[i] < startCicle then break end
				if keysToDelete[i] == f then
					isRealKey = false
					break
				end
			end
			if isRealKey then return true end
		end
	end
	return false
end

function AE_KeyTools:RemoveUnusedKeys(moho)
	local layersCollection = self:GetSelectedLayers(moho)
	for id, layer in pairs(layersCollection) do
		for subID, ID, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
			if channel:Duration() > 0 and (not self.ignoreStringChannels or channel:ChannelType() ~= MOHO.CHANNEL_STRING) then
				if chInfo.channelID ~= CHANNEL_LAYER_MARKERS and chInfo.channelID ~= CHANNEL_DOC_MARKERS then
					--print('processing ', chInfo.channelID)
					local keysToDelete = {}
					local derivedChannel = AE_Utilities:GetDerivedChannel(moho, channel)
					local val = derivedChannel:GetValue(0)
					local lastKeyFrame = 0
					for i=1, channel:CountKeys()-1 do
						local keyTime = channel:GetKeyWhen(i)
						--print('time=', keyTime, ' key is ', i, ' lastKeyFrame=', lastKeyFrame)
						local nextVal = derivedChannel:GetValue(keyTime)
						if not AE_Utilities:IsEqualValues(channel, val, nextVal) or 
						  (lastKeyFrame > 0 and channel:GetKeyInterpMode(lastKeyFrame)) == MOHO.INTERP_CYCLE or 
						  channel:GetKeyInterpModeByID(i) == MOHO.INTERP_POSE or
						  self:TreatAsCicleKeyframe(moho, channel, keyTime, keysToDelete) then 						  	
							-- if values are different
							--print('not equal')
							val = nextVal 
							lastKeyFrame = keyTime
						else 
							-- if values are the same
							--print('equal')
							if i< channel:CountKeys()-1 then
								local afterKey = i + 1
								local afterTime = channel:GetKeyWhen(i + 1)
								local afterVal = derivedChannel:GetValue(afterTime)
								if AE_Utilities:IsEqualValues(channel, nextVal, afterVal) then
									table.insert(keysToDelete, keyTime)
								end
							end
						end					
					end
					channel:ClearAfter(lastKeyFrame)
					for k, v in pairs(keysToDelete) do
						if v < lastKeyFrame then channel:DeleteKey(v) end
					end
				end
			end
		end
	end
end

function AE_KeyTools:ShowHideAnimLayers(moho,toShow, restrictFirsts)
	local minDuration = 0
	if restrictFirsts then minDuration = 1 end
	local theLayers = self:GetSelectedLayers(moho, true, true)
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		if not toShow then 
			if not layer:SecondarySelection() then layer:SetShownOnTimeline(false) end
		else
			if not moho.layer:IsGroupType() or layer:IsAncestorSelected() then
				local isAnimated = false
				local isFiltered = false
				for i, filteredLayer in pairs(theLayers) do 
					if filteredLayer == layer then 
						isFiltered = true 
						break
					end
				end
				if isFiltered then
					for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
						if channel:Duration() > minDuration then
							isAnimated = true
							break
						end
					end
				end
				layer:SetShownOnTimeline(isAnimated)
			end
		end			
	end
end

function AE_KeyTools:SelectKeys(moho)
	local filteredLayers = self:GetSelectedLayers(moho)
	
	local areaStart = MOHO.MohoGlobals.PlayStart
	if areaStart == -1 then areaStart = moho.document:StartFrame() end
	local areaEnd = MOHO.MohoGlobals.PlayEnd
	if areaEnd == -1 then areaEnd = moho.document:EndFrame() end
	
	for i, layer in pairs(filteredLayers) do
		local exclude_channels = self:CollectExcludeChannels(moho, layer)
		local layerFrame = moho.frame + layer:TotalTimingOffset()
		local layerAreaStart = areaStart + layer:TotalTimingOffset()
		local layerAreaEnd = areaEnd + layer:TotalTimingOffset()
		for subId, id, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
			if (not self.ignoreStringChannels or channel:ChannelType() ~= MOHO.CHANNEL_STRING) and 
				(not(table.contains(exclude_channels, tostring(channel)))) then
				if self.selectKeys == self.SELECTKEYS_TOTAL then			
					if channel:Duration() > 0 then
						if subId == 1 then table.contains(exclude_channels, tostring(channel), true) end
						for k=1, channel:CountKeys()-1 do
							channel:SetKeySelectedByID(k, true)
						end
					end			
				elseif self.selectKeys == self.SELECTKEYS_AREA then
					if channel:Duration() > 0 then
						if subId == 1 then table.contains(exclude_channels, tostring(channel), true) end
						for k=1, channel:CountKeys()-1 do
							local fr = channel:GetKeyWhen(k)
							if fr >= layerAreaStart and fr <= layerAreaEnd then
								channel:SetKeySelectedByID(k, true)
							end
						end	
					end
				else 
					if channel:HasKey(layerFrame) then
						channel:SetKeySelected(layerFrame, true)
					end
				end
			end
		end
	end
end

function AE_KeyTools:DeselectKeys(moho)
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
			for k = 0, channel:CountKeys()-1 do
				channel:SetKeySelectedByID(k, false)
			end
		end
	end
end


function AE_KeyTools:InterFileCopyPasteFrames(moho)
	self.targetFilePath = moho.document:Path()
	self.toFrame = moho.frame
	moho:FileOpen(self.sourceFilePath)
	local valuesCollection = {}
	for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, moho.layer) do
		local derivedChannel = AE_Utilities:GetDerivedChannel(moho, channel)
		if derivedChannel then
			local value = derivedChannel:GetValue(self.fromFrame + moho.layer:TotalTimingOffset())
			valuesCollection[subId] = {["val"] = value}	
		end
 
	end
	moho:FileOpen(self.targetFilePath)
	for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, moho.layer) do
		local derivedChannel = AE_Utilities:GetDerivedChannel(moho, channel)
		if derivedChannel and valuesCollection[subId] then
			local frame = self.toFrame + moho.layer:TotalTimingOffset()
			local value = derivedChannel:GetValue(frame)
			if not AE_Utilities:IsEqualValues(derivedChannel, value, valuesCollection[subId].val) then
				derivedChannel:SetValue(frame,  valuesCollection[subId].val)
			end
		end
	end
end

function AE_KeyTools:GetLayerPath(layer)
	local path = layer:Name()
	local nextLayer = layer
	while nextLayer:Parent() do
		nextLayer = nextLayer:Parent()
		path = nextLayer:Name() .. "\n" .. path
	end
	return path
end

function AE_KeyTools:Ordering2NameArray(moho, value, parentLayer, maptable)
	local valueArray = {}
	if value ~= "" then
		for token in string.gmatch(value, "[^|]+") do
			local name = nil
			if maptable then name = maptable[token] 
			else 
				local theLayer = AE_Utilities:GetLayerByUUID(moho, token)
				if theLayer then name = theLayer:Name() end
			end
			if name then table.insert(valueArray, name) end
		end
	else
		for i = 0, parentLayer:CountLayers()-1 do
			table.insert(valueArray, parentLayer:Layer(i):Name())
		end
	end
	return valueArray
end 

function AE_KeyTools:NameArray2ordering(moho, nameArray, parentLayer, maptable)
	local resultString = ""
	local zeroEqual = true
	if parentLayer then
		for i = 0, parentLayer:CountLayers() -1 do
			if parentLayer:Layer(i):Name() ~= nameArray[i + 1] then
				zeroEqual = false
				break
			end
		end
	end
	if zeroEqual then return "" end
	for i,v in pairs(nameArray) do
		local nextUUID = ""
		if maptable then nextUUID = maptable[v] 
		elseif parentLayer then
			for j=0, parentLayer:CountLayers()-1 do
				local nextChild = parentLayer:Layer(j)
				if nextChild:Name() == v then
					nextUUID = nextChild:UUID()
					break
				end
			end
		end
		if nextUUID and #nextUUID > 0 then
			if #resultString > 0 then resultString = resultString .. "|" end
			resultString = resultString .. nextUUID 
		end
	end
	return resultString
end

function AE_KeyTools:StoreValues(moho, saveAnimation, finishFrame)
	self.storedValues = {}
	local layersCollection = self:GetSelectedLayers(moho)
	for id, layer in pairs(layersCollection) do
		local path = self:GetLayerPath(layer)
		local layerInfo = {["name"] = layer:Name(), ["path"] = path, ["chans"] = {}}
		local skel = nil
		if layer:IsBoneType() then
			local boneLayer = moho:LayerAsBone(layer)
			if boneLayer then 
				skel = boneLayer:Skeleton()
			end
		end		
		for subId, id, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
			if (not chInfo.selectionBased) and chInfo.channelID ~= CHANNEL_LAYER_ALL and chInfo.channelID < CHANNEL_DOC_MARKERS then
				if subId == 0 then 
					layerInfo.chans[id] = {["vals"]={}, ["chantype"] = chInfo.channelID, ["numsubs"] = chInfo.subChannelCount}
				end

				local derivedChannel, channelType = AE_Utilities:GetDerivedChannel(moho, channel)
			
				if derivedChannel then
					local maptable = nil
					if chInfo.channelID == CHANNEL_LAYER_ORDER then
						maptable = {}
						local groupLayer = moho:LayerAsGroup(layer)
						if groupLayer then
							for nchild = 0, groupLayer:CountLayers()-1 do
								local nextChild = groupLayer:Layer(nchild)
								maptable[nextChild:UUID()] = nextChild:Name()
							end
						end
					end
					local value = derivedChannel:GetValue(self.fromFrame + moho.layer:TotalTimingOffset())
					if maptable then value = self:Ordering2NameArray(moho, value, moho:LayerAsGroup(layer), maptable) end
					layerInfo.chans[id].vals[subId] = {["val"] = value, ["chantype"] = channelType}
				
					if skel and string.match(chInfo.name:Buffer(), ".*Bone.*") then 
						if chInfo.subChannelCount == skel:CountBones() then
							layerInfo.chans[id].vals[subId].boneName = skel:Bone(subId):Name()
						end
					end					
					if saveAnimation and finishFrame > self.fromFrame then
						local keys = {}
						for k = 0, derivedChannel:CountKeys()-1 do
							local globalTime = derivedChannel:GetKeyWhen(k) - moho.layer:TotalTimingOffset()
							if globalTime >= self.fromFrame and globalTime <= finishFrame then
								local keyValue = derivedChannel:GetValueByID(k)
								if maptable then keyValue = self:Ordering2NameArray(moho, keyValue, moho:LayerAsGroup(layer), maptable) end
								local interp = MOHO.InterpSetting:new_local()
								derivedChannel:GetKeyInterpByID(k, interp)
								local newKey = {["globalTime"]=globalTime, ["val"] = keyValue, ["interp"] = interp}
								table.insert(keys, newKey)
							end
						end
						layerInfo.chans[id].vals[subId].keys = keys
					end
				end 
			end
		end
	table.insert(self.storedValues, layerInfo)
	end
end


function AE_KeyTools:RestoreValues(moho, oldRootPath, newRootPath)
	if #self.storedValues == 1 then return self:RestoreLayerValues(moho, moho.layer, self.storedValues[1].chans) end
	for i, obj in pairs(self.storedValues) do
		local sourcePath = obj.path
		if oldRootPath and newRootPath then
			sourcePath = obj.path:gsub(oldRootPath:gsub(".", self.regtab), newRootPath)		
		end	
		for id, layer in AE_Utilities:IterateAllLayers(moho) do
			local path = self:GetLayerPath(layer)
			if path == sourcePath then
				--print('ready to copy from '..string.gsub(sourcePath,'\n', ' ')..' to '..string.gsub(path,'\n', ' '))
				self:RestoreLayerValues(moho, layer, obj.chans)
			end
		end
	end
end

function AE_KeyTools:RestoreLayerValues(moho, layer, storedLayerValues)
	local skel = nil
	if layer:IsBoneType() then
		local boneLayer = moho:LayerAsBone(layer)
		skel = boneLayer:Skeleton()
	end
	for subId, id, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
		local derivedChannel, channelType = AE_Utilities:GetDerivedChannel(moho, channel)
		local storedChannel = storedLayerValues[id]
		if storedChannel then
			local storedSubChannel = nil
			local maptable = nil
			if chInfo.channelID == CHANNEL_LAYER_ORDER then
				maptable = {}
				local groupLayer = moho:LayerAsGroup(layer)
				if groupLayer then
					for nchild = 0, groupLayer:CountLayers()-1 do
						local nextChild = groupLayer:Layer(nchild)
						maptable[nextChild:Name()] = nextChild:UUID()
					end
				end
			end

			if storedChannel and storedChannel.chantype == chInfo.channelID then
				if storedChannel.numsubs > subId then
					storedSubChannel= storedChannel.vals[subId]
				end
			end
			if skel and string.match(chInfo.name:Buffer(), ".*Bone.*") and skel:Bone(subId) then
				local boneName = skel:Bone(subId):Name()
				if (not storedSubChannel) or storedSubChannel.boneName ~= boneName then
					storedSubChannel = nil
					for n,b in pairs(storedChannel.vals) do
						if b.boneName and b.boneName == boneName then
							storedSubChannel = b
						end
					end
				end
			end
			if derivedChannel and  storedSubChannel and --storedChannel.numsubs <= chInfo.subChannelCount and
			 storedSubChannel.chantype == channelType then
				local frame = self.toFrame + moho.layer:TotalTimingOffset()
				local value = derivedChannel:GetValue(frame)				
				if storedSubChannel.keys then 
					for k,v in pairs(storedSubChannel.keys) do
						frame = self.toFrame + moho.layer:TotalTimingOffset() + v.globalTime - self.fromFrame
						local val = v.val
						if maptable then val = self:NameArray2ordering(moho, val, moho:LayerAsGroup(layer), maptable) end
						if chInfo.channelID == CHANNEL_BONE_PARENT and val > -1 then				
							local name = storedChannel.vals[val].boneName
							local newID = -1
							if skel then 
								for b = 0, skel:CountBones()-1 do
									if skel:Bone(b):Name() == name then
										newID = b
										break
									end
								end
							end					
							val = newID
						end
						derivedChannel:SetValue(frame,  val)
						derivedChannel:SetKeyInterp(frame, v.interp)
					end
				else
					local val = storedSubChannel.val
					if maptable then val = self:NameArray2ordering(moho, val, moho:LayerAsGroup(layer), maptable) end
					if chInfo.channelID == CHANNEL_BONE_PARENT and val > -1 then				
						local name = storedChannel.vals[val].boneName
						local newID = -1
						if skel then 
							for b = 0, skel:CountBones()-1 do
								if skel:Bone(b):Name() == name then
									newID = b
									break
								end
							end
						end					
						val = newID
					end	
					if not AE_Utilities:IsEqualValues(derivedChannel, value, val ) then
						derivedChannel:SetValue(frame,  val)
					end
				end
			end
		end
		--layer:UpdateCurFrame()		
	end
	layer:UpdateCurFrame()
	
end

function AE_KeyTools:PasteSelectedKeys(moho)
	local firstKeyFrame = math.huge
	local keysCollection = {}
	local layersToUpdate = {}
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		if layer == moho.layer or layer:IsShownOnTimeline() or layer:SecondarySelection() then
			local foundKeys = false
			for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
				local derivedChannel = nil
				for k = 0, channel:CountKeys()-1 do
					if channel:IsKeySelectedByID(k) then
						if not derivedChannel then derivedChannel = AE_Utilities:GetDerivedChannel(moho, channel) end
						if derivedChannel then
							local keyTime = channel:GetKeyWhen(k)
							if (keyTime - layer:TotalTimingOffset()) < firstKeyFrame then firstKeyFrame = keyTime - layer:TotalTimingOffset() end
							local nextKey = {['channel'] = derivedChannel, ['time'] = keyTime, ['layer'] = layer}
							table.insert(keysCollection, nextKey)
							foundKeys = true
						end
					end
				end
			end
			if foundKeys then table.insert(layersToUpdate, layer) end
		end
	end

	if #keysCollection < 1 then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "No keys selected", "", "", "EXIT") end
	for k,v in pairs(keysCollection) do
		local val = v.channel:GetValue(v.time)
		local newTime = moho.frame + v.time - firstKeyFrame
		v.channel:SetValue(newTime, val)
	end
	moho.layer:UpdateCurFrame(true)
	for k,v in pairs(layersToUpdate) do v:UpdateCurFrame(true) end
end

function AE_KeyTools:DubbingChannel(moho, channelID) -- does not work as expected. Do not know, why
	if channelID > CHANNEL_BONE and channelID < CHANNEL_BONE_T then return true end
	if channelID > CHANNEL_BONE_T and channelID < CHANNEL_BONE_S then return true end
	if channelID > CHANNEL_BONE_S and channelID < CHANNEL_BONE_FLIPH then return true end
	return false
end

function AE_KeyTools:NudgeKeys(moho, toForward, step)
	if not step then step = 1 end
	local signedStep = step
	if not toForward then signedStep = -step end
	
	local layersCollection = self:GetSelectedLayers(moho)
	local channelsCollection = {}
	for id, layer in pairs(layersCollection) do
		if self.nudgeArea == self.NUDGEAREA_TOTAL or layer:IsShownOnTimeline() or layer == moho.layer then
			local curFrame = layer:TotalTimingOffset() + moho.frame
			for subId, channelId, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
				if (not self.ignoreStringChannels) or channel:ChannelType() ~= MOHO.CHANNEL_STRING then
					if chInfo.channelID ~= CHANNEL_DOC_MARKERS or self.nudgeArea == self.NUDGEAREA_TOTAL then
						if chInfo.selectionBased == false then
							if (self.nudgeArea ~= self.NUDGEAREA_SELECTEDKEYS and not toForward) and channel:HasKey(curFrame-step) then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Existing key in target frame", "", "", "EXIT") end				
							if channel.Duration and channel:Duration() > curFrame-1 or self.nudgeArea == self.NUDGEAREA_SELECTEDKEYS then 
								local nextChannel = {['channel'] = channel, ['curFrame'] = curFrame}
								local allwayCollected = false
								for i, item in pairs(channelsCollection) do
									if item.channel == channel then 
										allwayCollected = true
										break
									end
								end
								if not allwayCollected then table.insert(channelsCollection, nextChannel) end
							end
						end
					end
				end
			end
		end
	end
	local deleteOld = false
	if self.nudgeArea == self.NUDGEAREA_SELECTEDKEYS then
		for k,v in pairs(channelsCollection) do
			for kID = 0, v.channel:CountKeys()-1 do
				if v.channel:IsKeySelectedByID(kID) then
					if v.channel:HasKey(v.channel:GetKeyWhen(kID) + signedStep) and not deleteOld then
						local userAnswer = LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Existing key in target frame", "", "", "EXIT", "deselect problem keys and EXIT", "delete old")
						if userAnswer == 1 then
							for k1,v1 in pairs(channelsCollection) do
								for kID1 = 0, v1.channel:CountKeys()-1 do
									if v1.channel:IsKeySelectedByID(kID1) and v1.channel:HasKey(v1.channel:GetKeyWhen(kID1) + signedStep) then
										v1.channel:SetKeySelectedByID(kID1, false)
									end
								end
							end
						end
						if userAnswer == 2 then deleteOld = true else return end
					end
				end
			end
		end
	end

	for k,v in pairs(channelsCollection) do
		if self.nudgeArea == self.NUDGEAREA_SELECTEDKEYS then
			local startID = 0
			local endID = v.channel:CountKeys() - 1
			local iterator = 1
			if toForward then startID, endID, iterator = endID, startID, -1 end
			for kID = startID, endID, iterator do
				if v.channel:IsKeySelectedByID(kID) then
					local f = v.channel:GetKeyWhen(kID)
					if deleteOld and v.channel:HasKey(f + signedStep) then 
						v.channel:DeleteKey(f + signedStep)
					end
					v.channel:SetKeyWhen(kID, f + signedStep)
				end
			end
		else
			local lastKeyTime = v.channel:Duration()
			local curFrame = v.curFrame
			if toForward then
				for f = lastKeyTime, curFrame, -1 do
					if v.channel:HasKey(f) then
						local kID = v.channel:GetClosestKeyID(f)
						v.channel:SetKeyWhen(kID, f+step)
					end
				end
			else
				for f = curFrame, lastKeyTime do
					if v.channel:HasKey(f) then
						local kID = v.channel:GetClosestKeyID(f)
						v.channel:SetKeyWhen(kID, f-step)
					end				
				end
			end
		end
	end

	if toForward then moho:SetCurFrame(moho.frame + step) else moho:SetCurFrame(moho.frame - step) end
end

function AE_KeyTools:SelectKeys_colored(moho)
	moho.document:SetDirty()
	moho.document:PrepUndo(nil)
	
	local interp = MOHO.InterpSetting:new_local()
	local colorToSelect = -1
	local startFrame = MOHO.MohoGlobals.PlayStart
	if startFrame == -1 then startFrame = moho.document:StartFrame() end
	local endFrame = MOHO.MohoGlobals.PlayEnd
	if endFrame == -1 then endFrame = moho.document:EndFrame() end	
	
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		if layer == moho.layer or layer:IsShownOnTimeline() then
			for j, i, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
				if channel:Duration() > 0 then
					for f = startFrame, endFrame do
						if channel:HasKey(f) and channel:IsKeySelected(f) then
							channel:GetKeyInterp(f, interp)
							colorToSelect = interp.tags
							break
						end
					end
				end
			end
		end
	end
	
	if colorToSelect == -1 then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "First select any colored key to set the color") end
	
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		if layer == moho.layer or layer:IsShownOnTimeline() then
			for j, i, channel, chInfo in AE_Utilities:IterateAllChannels(moho, layer) do
				if channel:Duration() > 0 and chInfo.selectionBased == false then
					for k = 0, channel:CountKeys() -1 do
						local f = channel:GetKeyWhen(k)
						local globalF = f - layer:TotalTimingOffset()
						if (self.selectKeys == self.SELECTKEYS_AREA and (globalF < startFrame or globalF > endFrame)) or
						(self.selectKeys == self.SELECTKEYS_FRAME and globalF ~= moho.frame )then 
							channel:SetKeySelected(f, false)
						else
							channel:GetKeyInterp(f, interp)
							if interp.tags == colorToSelect then 
								channel:SetKeySelected(f, true)
							else
								channel:SetKeySelected(f, false)
							end
						end
					end
				end
			end
		end
	end	

	moho:UpdateUI()
	moho.layer:UpdateCurFrame()	
	
end


---------------------------------------------------------------------------------------------------------------------

function AE_KeyTools:GetSelectedLayers(moho, childrenAllwaysOn, forTimelineShow)
	local layersCollection = AE_Utilities:MohoListToTable(moho.document, moho.document.CountSelectedLayers, moho.document.GetSelectedLayer)
	if not self.applyToChildLayers and not childrenAllwaysOn then return layersCollection end
	table.sort(layersCollection, function(a,b) return (moho.document:LayerAbsoluteID(a)<moho.document:LayerAbsoluteID(b)) end)
	local childLayersCollection = {}
	for i, layer in AE_Utilities:IterateAllLayers(moho, moho.document:LayerAbsoluteID(layersCollection[1])) do
		if layer:SecondarySelection() then table.insert(childLayersCollection, layer)
		elseif not self.timelinevisibleOnly or layer:IsShownOnTimeline() or forTimelineShow then
			for j,parentLayer in pairs(layersCollection) do
				if AE_Utilities:IsAncestor(parentLayer, layer) then 
					table.insert(childLayersCollection, layer)
					break
				end
			end
		end
	end
	if not self.applyToRefs then
		for i, layer in pairs(childLayersCollection) do
			if layer:IsReferencedLayer() and not layer:IsReferenceExternal() then table.remove(childLayersCollection, i) end
		end
	end
	return childLayersCollection
end



function AE_KeyTools:GetCycledValue(moho, channel, frame)
	local keyID = self:IsCycled(moho,channel, frame)
	if keyID <= 0 then return channel:GetValue(frame) end
	local interp = MOHO.InterpSetting:new_local()
	channel:GetKeyInterpByID(keyID, interp)
	local keyTime = channel:GetKeyWhen(keyID)
	local absCycle = interp.val2 > 0 and interp.val2 or keyTime - interp.val1
	local timeDistance = frame - keyTime
	local referenceTime = timeDistance % (keyTime - absCycle + 1) + absCycle - 1
	local referenceValue = channel:GetValue(referenceTime)	
	if not interp:IsAdditiveCycle() then return referenceValue end
	local offset = channel:GetValue(keyTime) - channel:GetValue(absCycle - 1)
	local numOffsets = math.floor((timeDistance-1)/(keyTime - absCycle + 1))+1
	return (offset * numOffsets + referenceValue)
	
end

function AE_KeyTools:IsCycled(moho,channel,frame)
	if channel:HasKey(frame) then return 0 end
	local keyID = channel:GetClosestKeyID(frame)
	if channel:GetKeyInterpModeByID(keyID) == MOHO.INTERP_CYCLE then return keyID end
	if channel:GetKeyInterpModeByID(keyID) == MOHO.INTERP_NOISY then return keyID end
	if channel:GetKeyInterpModeByID(keyID) == MOHO.INTERP_BOUNCE then return keyID end
	if channel:GetKeyInterpModeByID(keyID) == MOHO.INTERP_ELASTIC then return keyID end
	return 0
end

function AE_KeyTools:CopyPasteInterp(moho)
	if self.keyInterp then
		for i, layer in AE_Utilities:IterateAllLayers(moho) do
			if layer == moho.layer or layer:IsShownOnTimeline() then
				for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
					for k = 0, channel:CountKeys()-1 do
						if channel:IsKeySelectedByID(k) then
							channel:SetKeyInterpByID(k,self.keyInterp)
						end
					end
				end
			end
		end	
		local d = moho.document		
		for i, channel in pairs({d.fCameraPanTilt, d.fCameraRoll, d.fCameraTrack, d.fCameraZoom}) do
			for k = 0, channel:CountKeys()-1 do
				if channel:IsKeySelectedByID(k) then
					channel:SetKeyInterpByID(k,self.keyInterp)
				end
			end
		end
		self.keyInterp = nil
		--self.copypasteInterpButton:SetImage("ScriptResources/smooth")
		self.copypasteInterpButton:SetLabel("C", false)
	else 
		for i, layer in AE_Utilities:IterateAllLayers(moho) do
			if layer == moho.layer or layer:IsShownOnTimeline() then
				for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
					for k = 0, channel:CountKeys()-1 do
						if channel:IsKeySelectedByID(k) then
							if self.keyInterp then
								self.keyInterp = nil
								return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Please, select only one key for copy", "", "", "EXIT")
							end
							self.keyInterp = MOHO.InterpSetting:new_local()
							channel:GetKeyInterpByID(k,self.keyInterp)
						end
					end
				end
			end
		end
		local d = moho.document
		local cameraChannels = {d.fCameraPanTilt, d.fCameraRoll, d.fCameraZoom}
		if d.fCameraTrack:AreDimensionsSplit() then
			for dim=0, 2 do table.insert(cameraChannels, d.fCameraTrack:DimensionChannel(dim)) end			
		else
			table.insert(cameraChannels, d.fCameraTrack)
		end
		for i, channel in pairs(cameraChannels) do
			for k = 0, channel:CountKeys()-1 do
				if channel:IsKeySelectedByID(k) then
					if self.keyInterp then
						self.keyInterp = nil
						return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Please, select only one key for copy", "", "", "EXIT")
					end
					self.keyInterp = MOHO.InterpSetting:new_local()
					channel:GetKeyInterpByID(k,self.keyInterp)
				end
			end		
		end
		--self.copypasteInterpButton:SetImage("ScriptResources/show_handles")
		self.copypasteInterpButton:SetLabel("P", false)
	end
end

function table.contains(t, element)
  if not t then return false end
  for _, value in pairs(t) do
    if value == element then
      return true
    end
  end
  return false
end

-- **************************************************
-- Localization
-- **************************************************
function AE_KeyTools:Localize(text)
	local fileWord = MOHO.Localize("/Menus/File/File=File")
	
	local phrase = {}
	
	phrase["Name"] = "Key Tool"
	phrase["Description"] = "Use Alt+Shift+UP/DOWN to nudge keys 10 frames at once"
	phrase["UILabel"] = "Key Tool"
	phrase["COPY"] = "COPY"
	phrase["PASTE"] = "PASTE"
	phrase["From"] = "From: "
	phrase["PasteKeysTooltip"] = "Paste selected keys"
	phrase["ADD"] = "ADD"
	phrase["NextKey"] = "Next key"
	phrase["PreviousKey"] = "Previous key"
	phrase["CurrentValue"] = "Current value"
	phrase["ZeroFrame"] = "Zero frame"
	phrase["PreviousMarker"] = "Previous Marker"
	phrase["ApplyToChildLayers"] = "Child layers"
	phrase["ApplyToRefs"] = "Referenced layers"	
	phrase["PrecalcCycles"] = "Cycles"
	phrase["IgnoreStringChannels"] = "Ignore string channels"
	phrase["IgnoreHiddenBones"] = "Ignore hidden bones"
	phrase["NudgeRightTooltip"] = "Nudge keys right, alt-click for x10"
	phrase["NudgeLeftTooltip"] = "Nudge keys left, alt-click for x10"
	phrase["CLEANUP"] = "CLEAN"
	phrase["CopyTooltip"] = "Define the source frame to copy values from"
	phrase["PasteTooltip"] = "Copy values from the source frame into current frame"
	phrase["AddTooltip"] = "Set keys at the current frame, taking current values or values from previous/next key"
	phrase["MenuTooltip"] = "Where to take the values for the ADD command"
	phrase["IncludingMenuTooltip"] = "Which layers and channels to include for all operations"	
	phrase["ChildLayersTooltip"] = "Include all sub-layers of the selected layers"
	phrase["RefsTooltip"] = "Include referenced layers"	
	phrase["CyclesTooltip"] = "Calculate values in frames affected by cycles"
	phrase["IgnoreStringsTooltip"] = "Do not use the Switch and Layer Order channels"
	phrase["CleanupTooltip"] = "Remove duplicate keys at the end of each channel"
	phrase["ShowAnimTooltip"] = "Show animated layers on timeline"
	phrase["ShowAnim1Tooltip"] = "Show layers with animation after first frame"	
	phrase["HideAnimTooltip"] = "Hide all non-selected layers from timeline"
	phrase["selectKeyTooltip"] = "Select all keys in current frame/area/total project; alt-click to select keys of same color"
	phrase["CopyPasteInterpTooltip"] = "Copy/paste key interpolation"
	
		
	if fileWord == "Файл" then
		phrase["Name"] = "Инструменты ключей"
		phrase["Description"] = "Инструменты ключей"
		phrase["UILabel"] = "Инструменты ключей"
		phrase["COPY"] = "КОПИРОВАТЬ"
		phrase["PASTE"] = "ВСТАВИТЬ"
		phrase["From"] = "Из: "
		phrase["PasteKeysTooltip"] = "Вставить выбранные ключи"
		phrase["ADD"] = "СОЗДАТЬ"
		phrase["NextKey"] = "След. ключ"
		phrase["PreviousKey"] = "Пред. ключ"
		phrase["CurrentValue"] = "Текущее значение"
		phrase["ZeroFrame"] = "Из нулевого кадра"
		phrase["PreviousMarker"] = "От предыдущего маркера"
		phrase["ApplyToChildLayers"] = "Дочерние слои"
		phrase["ApplyToRefs"] = "Референсы"
		phrase["PrecalcCycles"] = "Циклы"
		phrase["IgnoreStringChannels"] = "Игнорировать строчные каналы"
		phrase["IgnoreHiddenBones"] = "Игнорировать спрятанные кости"
		phrase["NudgeRightTooltip"] = "Сдвинуть ключи вправо"
		phrase["NudgeLeftTooltip"] = "Сдвинуть ключи влево"
		phrase["CLEANUP"] = "ОЧИСТИТЬ"
		phrase["CopyTooltip"] = "Сохранить номер исходного кадра"
		phrase["PasteTooltip"] = "Вставить ключи из исходного кадра"
		phrase["AddTooltip"] = "Создать ключи в текущем кадре, базируясь на предыдущем/сдледующем/текущем значении"
		phrase["MenuTooltip"] = "Откуда брать значения для SET"
		phrase["IncludingMenuTooltip"] = "Над какими слоями и каналами производить операции"
		phrase["ChildLayersTooltip"] = "Обрабатывать всю иерархию дочерних слоев каждого выбранного слоя"
		phrase["RefsTooltip"] = "Обрабатывать референсные слои"
		phrase["CyclesTooltip"] = "Вычислять значения в кадрах-источниках, на которые влияют циклы"
		phrase["IgnoreStringsTooltip"] = "Не учитывать каналы переключетелей и порядка слоев"
		phrase["CleanupTooltip"] = "Убрать одинаковые ключи в конце каждого канала"	
		phrase["ShowAnimTooltip"] = "Показать на таймлайне слои с анимацией"
		phrase["ShowAnim1Tooltip"] = "Показать на таймлайне слои с анимацией после первого кадра"
		phrase["HideAnimTooltip"] = "Убрать с таймлайна все невыбранные слои"
		phrase["selectKeyTooltip"] = "Выбрать все ключи в текущем кадре"
	end
	return phrase[text] or text;
end




Icon
AE Key Tools
Listed

Script type: Tool

Uploaded: Jul 14 2020, 08:28

Last modified: Jan 05 2023, 08:15

Multiple buttons combined to a tool for manipulating keys in character animation.
Setting keys, moving keys, copy and paste poses and so on, for any number of sublayers in skeleton hierarchy.
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 2123