-- 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.24"
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.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)
end

function AE_KeyTools:SavePrefs(prefs)
	prefs:SetBool("AE_KeyTools.applyToChildLayers", self.applyToChildLayers)
	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)	
end

function AE_KeyTools:ResetPrefs()
	self.applyToChildLayers = true
	self.applyToRefs = false
	self.precalcCycles = true
	self.ignoreStringChannels = false
	self.ignoreHiddenBones = true
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.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.targetFilePath = nil
AE_KeyTools.storedValues = {}
AE_KeyTools.keyInterp = nil
AE_KeyTools.selectAllKeys = false


-- **************************************************
-- 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.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.SELECTALLKEYS			= MOHO.MSG_BASE + 17
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.NUDGELEFT				= MOHO.MSG_BASE + 13

-- 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


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"))
	
	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.ImageButton("ScriptResources/smooth", self.Localize("CopyPasteInterpTooltip"), false, self.COPYPASTEINTERP, true)
	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(120, 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.includingsMenu = LM.GUI.Menu("INCLUDE...")
	self.includingsMenu_popup = LM.GUI.PopupMenu(120, false)
	self.includingsMenu_popup:SetMenu(self.includingsMenu)
	self.includingsMenu:AddItem(self:Localize("ApplyToChildLayers"), 0, self.APPLY_TO_CHILD_LAYERS)	
	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)	
	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:SetToolTip(self:Localize("NudgeLeftTooltip"))
	self.nudgeRightButton = LM.GUI.ImageButton("curs_hresize_right", self:Localize("NudgeRightTooltip"), false, self.NUDGERIGHT, true)
	layout:AddChild(self.nudgeRightButton)
	self.nudgeRightButton:SetToolTip(self:Localize("NudgeRightTooltip"))
	
	
	self.selectKeyButton = LM.GUI.Button("select", self.SELECTKEY)
	layout:AddChild(self.selectKeyButton)
	self.selectKeyButton:SetToolTip(self:Localize("selectKeyTooltip"))	
	self.selectAllKeysCB = LM.GUI.CheckBox("All", self.SELECTALLKEYS)
	layout:AddChild(self.selectAllKeysCB)
	self.selectAllKeysCB:SetToolTip("All keys for any time (uncheck for current frame)")
	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:AddChild(self.showAnimButton1)
	self.showAnimButton1:SetToolTip(self:Localize("ShowAnim1Tooltip"))	
	self.hideAnimButton = LM.GUI.Button("X", self.HIDEANIM)
	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.includingsMenu:SetChecked(self.APPLY_TO_CHILD_LAYERS, self.applyToChildLayers)
	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.selectAllKeysCB:SetValue(self.selectAllKeys)
	
	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()
		-- 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.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.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.PASTE then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()
		if moho.document:Path() ~= self.sourceFilePath then 
			self:RestoreValues(moho)
		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.DESELECTKEYS then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:DeselectKeys(moho)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()		
	elseif msg == self.NUDGELEFT then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:NudgeKeys(moho,false)
		moho:UpdateUI()
		moho.layer:UpdateCurFrame()	
	elseif msg == self.SELECTALLKEYS then
		self.selectAllKeys = self.selectAllKeysCB:Value()
	elseif msg == self.NUDGERIGHT then
		moho.document:PrepMultiUndo()
		moho.document:SetDirty()		
		self:NudgeKeys(moho,true)
		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 self.ignoreHiddenBones and moho:LayerAsBone(layer) 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(skel:Bone(j).fHidden) 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
	return exclude_channels
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
		local exclude_channels = self:CollectExcludeChannels(moho, layer)

		local curFrame = layer:TotalTimingOffset() + moho.frame
		if curFrame == 0 then return end
		for subId, id, channel in AE_Utilities:IterateAllChannels(moho, layer) do
			if not(table.contains(exclude_channels, tostring(channel))) then 
				if (channel:Duration() > curFrame or (self:IsCycled(moho,channel,curFrame) > 0 and self.precalcCycles) or self.getFromKey == self.ZEROFRAME_VALUES or self.getFromKey == self.PREVIOUSMARKER_VALUES) 
				and (self.precalcCycles or self:IsCycled(moho,channel,curFrame) <= 0) and not channel:HasKey(curFrame) 
				and (not self.ignoreStringChannels or channel:ChannelType() ~= MOHO.CHANNEL_STRING)
				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() and ( self.getFromKey ~= self.PREVIOUS_KEY or channel:GetKeyInterpModeByID(keyID) ~= MOHO.INTERP_STEP) 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.getFromKey == self.CURRENT_VALUES 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.getFromKey == self.CURRENT_VALUES or not AE_Utilities:IsEqualValues(derivedChannel, val, oldval) then
									derivedChannel:SetValue(curFrame, val)
							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:RemoveUnusedKeys(moho)
	local layersCollection = self:GetSelectedLayers(moho)
	for id, layer in pairs(layersCollection) do
		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 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)
					local nextVal = derivedChannel:GetValue(keyTime)
					if not AE_Utilities:IsEqualValues(channel, val, nextVal) or 
					(laskKeyFrame and channel:GetKeyInterpModeByID(lastKeyFrame)) == MOHO.INTERP_CYCLE then
						val = nextVal 
						lastKeyFrame = keyTime
					else 
						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

function AE_KeyTools:ShowHideAnimLayers(moho,toShow, restrictFirsts)
	local minDuration = 0
	if restrictFirsts then minDuration = 1 end
	theLayers = self:GetSelectedLayers(moho)
	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)
	filteredLayers = self:GetSelectedLayers(moho)
	for i, layer in pairs(filteredLayers) do
		local exclude_channels = self:CollectExcludeChannels(moho, layer)
		local layerFrame = moho.frame + 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.selectAllKeys 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			
				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: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 value = derivedChannel:GetValue(self.fromFrame + moho.layer:TotalTimingOffset())
					layerInfo.chans[id].vals[subId] = {["val"] = value, ["chantype"] = channelType}
					if skel and (chInfo.channelID >= CHANNEL_BONE and chInfo.channelID < CHANNEL_SWITCH) 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)
								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)
	if #self.storedValues == 1 then return self:RestoreLayerValues(moho, moho.layer, self.storedValues[1].chans) end
	for i, obj in pairs(self.storedValues) do
		for id, layer in AE_Utilities:IterateAllLayers(moho) do
			local path = self:GetLayerPath(layer)
			if path == obj.path then
				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]
		local storedSubChannel = nil
		if storedChannel and storedChannel.chantype == chInfo.channelID then
			storedSubChannel= storedChannel.vals[subId]
			if skel and storedSubChannel and storedSubChannel.boneName and skel:Bone(subId-1) then
				local boneName = skel:Bone(subId):Name()
				if storedSubChannel.boneName ~= boneName then
					for n,b in pairs(storedChannel.vals) do
						if b.boneName and b.boneName == boneName then
							storedSubChannel = b
						end
					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
					derivedChannel:SetValue(frame,  v.val)
					derivedChannel:SetKeyInterp(frame, v.interp)
				end
			else
				if not AE_Utilities:IsEqualValues(derivedChannel, value, storedSubChannel.val) then
					derivedChannel:SetValue(frame,  storedSubChannel.val)
				end
			end
		end
	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 layersCollection = self:GetSelectedLayers(moho)
	local channelsCollection = {}
	for id, layer in pairs(layersCollection) do
		local curFrame = layer:TotalTimingOffset() + moho.frame
		for subId, channel, chInfo in AE_Utilities:IterateLayerSubChannels(moho, layer) do
			--if self:DubbingChannel(moho, chInfo.channelID) then
				if (not self.ignoreStringChannels) or channel:ChannelType() ~= MOHO.CHANNEL_STRING then
					if (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()>curFrame-1 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
	for k,v in pairs(channelsCollection) do
		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
	if toForward then moho:SetCurFrame(moho.frame + step) else moho:SetCurFrame(moho.frame - step) end
end

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

function AE_KeyTools:GetSelectedLayers(moho)
	local layersCollection = AE_Utilities:MohoListToTable(moho.document, moho.document.CountSelectedLayers, moho.document.GetSelectedLayer)
	if not self.applyToChildLayers 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
		for j,parentLayer in pairs(layersCollection) do
			if AE_Utilities:IsAncestor(parentLayer, layer) then
				table.insert(childLayersCollection, layer)
				break
			end
		end
	end
	if not self.applyToRefs then
		for i, layer in pairs(childLayersCollection) do
			if layer:IsReferencedLayer() 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
					local derivedChannel = nil
					for k = 0, channel:CountKeys()-1 do
						if channel:IsKeySelectedByID(k) then
							channel:SetKeyInterpByID(k,self.keyInterp)
						end
					end
				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
					local derivedChannel = nil
					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
		--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"
	phrase["NudgeLeftTooltip"] = "Nudge keys left"
	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"
	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: Nov 18 2020, 11:18

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: 27