From e702eff9f7dda569f50069a2c8767302af96ae9a Mon Sep 17 00:00:00 2001 From: David Peterson Date: Fri, 4 Jun 2021 14:17:01 +1000 Subject: [PATCH 1/6] Fixes #2719. Uses the videoImage instead of content frame to create the grid. --- src/plugins/finalcutpro/viewer/overlays.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/finalcutpro/viewer/overlays.lua b/src/plugins/finalcutpro/viewer/overlays.lua index d8d80ef83..73bcad79c 100644 --- a/src/plugins/finalcutpro/viewer/overlays.lua +++ b/src/plugins/finalcutpro/viewer/overlays.lua @@ -213,9 +213,9 @@ function mod.show() -------------------------------------------------------------------------------- mod.hide() - local fcpFrame = fcp.viewer:contentsUI() - if fcpFrame then - local frame = fcpFrame:attributeValue("AXFrame") + local videoImage = fcp.viewer.videoImage + if videoImage then + local frame = videoImage:frame() if frame then -------------------------------------------------------------------------------- -- New Canvas: From 1eb176914188b7aae7807f49c11be54898a4dd95 Mon Sep 17 00:00:00 2001 From: David Peterson Date: Fri, 4 Jun 2021 17:16:30 +1000 Subject: [PATCH 2/6] Initial Custom Overlay support code. Not quite functional yet. --- .../finalcutpro/viewer/CustomOverlay.lua | 90 +++++++++++++++++++ .../cp/apple/finalcutpro/viewer/Viewer.lua | 45 ++++++++++ src/extensions/cp/tools/init.lua | 14 +++ 3 files changed, 149 insertions(+) create mode 100644 src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua diff --git a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua new file mode 100644 index 000000000..e4a4352b3 --- /dev/null +++ b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua @@ -0,0 +1,90 @@ +--- === cp.apple.finalcutpro.viewer.CustomOverlay === +--- +--- Represents a "Custom Overlay" value that can be enabled/disabled from FCP. +local require = require + +local app = require "cp.app" +local Set = require "cp.collect.Set" +local prop = require "cp.prop" +local tools = require "cp.tools" + +local fs = require "hs.fs" +local class = require "middleclass" +local lazy = require "cp.lazy" + +local pathToAbsolute = fs.pathToAbsolute +local getNameAndExtensionFromFile = tools.getNameAndExtensionFromFile +local insert = table.insert + +local CustomOverlay = class("cp.apple.finalcutpro.viewer.CustomOverlay"):include(lazy) + +--- cp.apple.finalcutpro.ALLOWED_IMPORT_IMAGE_EXTENSIONS -> table +--- Constant +--- Table of image file extensions Final Cut Pro can import. +CustomOverlay.static.ALLOWED_IMAGE_EXTENSIONS = Set("bmp", "gif", "jpeg", "jpg", "png", "psd", "raw", "tga", "tiff", "tif") + +--- cp.apple.finalcutpro.viewer.CustomOverlay.userOverlaysPath() -> string +--- Function +--- Returns the absolute path to the location where Custom Overlay images are stored for the current user. +function CustomOverlay.static.userOverlaysPath() + return pathToAbsolute("~/Library/Application Support/ProApps/Custom Overlays") +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.userOverlays +--- Constant +--- Contains the current list of `CustomOverlay`s available. +CustomOverlay.static.userOverlays = prop(function() + local overlays = {} + + for file in fs.dir(CustomOverlay.userOverlaysPath()) do + local name, ext = getNameAndExtensionFromFile(file) + if ext and CustomOverlay.ALLOWED_IMAGE_EXTENSIONS:has(ext) then + insert(overlays, CustomOverlay(name, ext)) + end + end + + return overlays +end) + +--- cp.apple.finalcutpro.viewer.CustomOverlay(name, extension) -> CustomOverlay +--- Constructor +--- Initializes a `CustomOverlay` with the specified name and file extension. +--- +--- Parameters: +--- * name - The overlay name. +--- * extension - The overlay file extension. +--- +--- Returns: +--- * the new `CustomOverlay`. +function CustomOverlay:initialize(name, extension) + self.name = name + self.extension = extension +end + +--- cp.apple.viewer.CustomOverlay.name +--- Field +--- The name of the overlay, as appears in the Viewer's 'Choose Custom Overlay' menu. + +--- cp.apple.viewer.CustomOverlay.extension +--- Field +--- The file extension of the overlay. + +--- cp.apple.finalcutpro.viewer.CustomOverlay.fileName +--- Field +--- The filename with extension. +function CustomOverlay.lazy.value:fileName() + return self.name .. "." .. self.extension +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.filePath +--- Field +--- The absolute path to the overlay file. +function CustomOverlay.lazy.value:filePath() + return CustomOverlay.userOverlaysPath() .. "/" .. self.fileName +end + +function CustomOverlay:__tostring() + return self.name +end + +return CustomOverlay \ No newline at end of file diff --git a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua index 094c36f40..f8a649b22 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua @@ -473,6 +473,51 @@ function Viewer.lazy.prop:framerate() return self.infoBar.framerate end +----------------------------------------------------------------------- +-- +-- OVERLAYS: +-- +----------------------------------------------------------------------- + +-- FFCustomOverlaySelectedCanvas = "Test.png", +-- FFCustomOverlaySelectedCanvas_Opacity = 50, +-- FFCustomOverlaySelectedViewer = "Test.png", +-- FFCustomOverlaySelectedViewer_Opacity = 50, +-- FFPlayerDisplayedCustomOverlayCanvas = 1, +-- FFPlayerDisplayedCustomOverlayViewer = 1, +-- ["YouTube UI.png_Opacity_Canvas"] = 50, +-- ["YouTube UI.png_Opacity_Viewer"] = 50, + +local VIEWER_OVERLAY_POSTFIX = "Canvas" +local EVENT_VIEWER_OVERLAY_POSTFIX = "Viewer" + +function Viewer:_overlayPostfix() + return self:isEventViewer() and EVENT_VIEWER_OVERLAY_POSTFIX or VIEWER_OVERLAY_POSTFIX +end + +function Viewer.lazy.prop:overlayEnabled() + -- TODO: Trigger "View > Show in Viewer > Show Custom Overlay" to actually toggle, to avoid UI update lag + local postfix = self:_overlayPostfix() + return self:app().preferences:prop("FFPlayerDisplayedCustomOverlay"..postfix):mutate( + function(original) return original() == 1 end, + function(newValue, original) original(newValue == 1) end + ) +end + +function Viewer.lazy.prop:overlayFileName() + local postfix = self:_overlayPostfix() + return self:app().preferences:prop("FFCustomOverlaySelected"..postfix) +end + +function Viewer.lazy.prop:overlayOpacity() + local postfix = self:_overlayPostfix() + return self:app().preferences:prop("FFCustomOverlaySelected"..postfix.."_Opacity") +end + +function Viewer.lazy.prop:overlay() + -- TODO: Figuring out the best way to handle this... +end + ----------------------------------------------------------------------- -- -- BROWSER UI: diff --git a/src/extensions/cp/tools/init.lua b/src/extensions/cp/tools/init.lua index 69451dda9..ea38d47c5 100644 --- a/src/extensions/cp/tools/init.lua +++ b/src/extensions/cp/tools/init.lua @@ -1446,6 +1446,20 @@ function tools.getFileExtensionFromPath(path) end end +--- cp.tools.getNameAndExtensionFromFile(file) -> string, string | nil +--- Function +--- Extracts the name and the extension for the provided file name (eg. "foo.bar" -> "foo", "bar"). +--- Does not remove any preceding path values from the string first. +--- +--- Parameters: +--- * file - The `string` for the file name (eg. "image.jpg"). +--- +--- Returns: +--- * The name and extension strings, or `nil` if the file has no extension. +function tools.getNameAndExtensionFromFile(file) + return string.match(file, "^(.*)%.([^%.]+)$") +end + --- cp.tools.removeFilenameFromPath(string) -> string --- Function --- Removes the filename from a path. From a6e62af80a6dea22e96c32dd828e2f310edd875f Mon Sep 17 00:00:00 2001 From: David Peterson Date: Sat, 5 Jun 2021 03:32:42 +1000 Subject: [PATCH 3/6] * Updated CustomOverlay/Viewer with additional options. --- .../finalcutpro/viewer/CustomOverlay.lua | 189 +++++++++++++++++- .../cp/apple/finalcutpro/viewer/Viewer.lua | 60 +++--- 2 files changed, 215 insertions(+), 34 deletions(-) diff --git a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua index e4a4352b3..25848f1a7 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua @@ -3,7 +3,7 @@ --- Represents a "Custom Overlay" value that can be enabled/disabled from FCP. local require = require -local app = require "cp.app" +local fcpApp = require "cp.apple.finalcutpro.app" local Set = require "cp.collect.Set" local prop = require "cp.prop" local tools = require "cp.tools" @@ -23,11 +23,13 @@ local CustomOverlay = class("cp.apple.finalcutpro.viewer.CustomOverlay"):include --- Table of image file extensions Final Cut Pro can import. CustomOverlay.static.ALLOWED_IMAGE_EXTENSIONS = Set("bmp", "gif", "jpeg", "jpg", "png", "psd", "raw", "tga", "tiff", "tif") +local userOverlaysPath = pathToAbsolute("~/Library/Application Support/ProApps/Custom Overlays") + --- cp.apple.finalcutpro.viewer.CustomOverlay.userOverlaysPath() -> string --- Function --- Returns the absolute path to the location where Custom Overlay images are stored for the current user. function CustomOverlay.static.userOverlaysPath() - return pathToAbsolute("~/Library/Application Support/ProApps/Custom Overlays") + return userOverlaysPath end --- cp.apple.finalcutpro.viewer.CustomOverlay.userOverlays @@ -37,15 +39,128 @@ CustomOverlay.static.userOverlays = prop(function() local overlays = {} for file in fs.dir(CustomOverlay.userOverlaysPath()) do - local name, ext = getNameAndExtensionFromFile(file) - if ext and CustomOverlay.ALLOWED_IMAGE_EXTENSIONS:has(ext) then - insert(overlays, CustomOverlay(name, ext)) + local overlay = CustomOverlay.forFileName(file) + if overlay then + insert(overlays, overlay) end end return overlays end) +local VIEWER_PREFIX = "Canvas" +local EVENT_VIEWER_PREFIX = "Viewer" + +--- cp.apple.finalcutpro.viewer.CustomOverlay.viewerEnabled +--- Constant +--- Is `true` if the `Viewer` `CustomOverlay` is enabled. +CustomOverlay.static.viewerEnabled = fcpApp.preferences:prop("FFPlayerDisplayedCustomOverlay"..VIEWER_PREFIX):mutate( + -- TODO: Trigger "View > Show in Viewer > Show Custom Overlay" to actually toggle, to avoid UI update lag + function(original) return original() == 1 end, + function(newValue, original) original:set(newValue and 1 or 0) end +) + +--- cp.apple.finalcutpro.viewer.CustomOverlay.viewerFileName +--- Constant +--- The `Viewer` `CustomOverlay` file name. +CustomOverlay.static.viewerFileName = fcpApp.preferences:prop("FFCustomOverlaySelected"..VIEWER_PREFIX):mutate( + function(original) + return original() + end, + function(value, original) + original:set(nil) + original:set(value) + end +) + +--- cp.apple.finalcutpro.viewer.CustomOverlay.viewerOpacity +--- Constant +--- The `Viewer` `CustomOverlay` opacity. +CustomOverlay.static.viewerOpacity = fcpApp.preferences:prop("FFCustomOverlaySelected"..VIEWER_PREFIX.."_Opacity") + +--- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerEnabled +--- Constant +--- Is `true` if the `EventViewer` `CustomOverlay` is enabled. +CustomOverlay.static.eventViewerEnabled = fcpApp.preferences:prop("FFPlayerDisplayedCustomOverlay"..EVENT_VIEWER_PREFIX):mutate( + -- TODO: Trigger "View > Show in Event Viewer > Show Custom Overlay" to actually toggle, to avoid UI update lag + function(original) return original() == 1 end, + function(newValue, original) original:set(newValue and 1 or 0) end +) + +--- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerFileName +--- Constant +--- The `EventViewer` `CustomOverlay` file name. +CustomOverlay.static.eventViewerFileName = fcpApp.preferences:prop("FFCustomOverlaySelected"..EVENT_VIEWER_PREFIX):mutate( + function(original) + return original() + end, + function(value, original) + original:set(nil) + original:set(value) + end +) + +--- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerOpacity +--- Constant +--- The `EventViewer` `CustomOverlay` opacity. +CustomOverlay.static.eventViewerOpacity = fcpApp.preferences:prop("FFCustomOverlaySelected"..EVENT_VIEWER_PREFIX.."_Opacity") + +--- cp.apple.finalcutpro.viewer.CustomOverlay.viewerOverlay +--- Constant +--- The `Viewer` `CustomOverlay`. +CustomOverlay.static.viewerOverlay = CustomOverlay.viewerFileName:mutate( + function(original) + local fileName = original() + return CustomOverlay.forFileName(fileName) + end, + function(value, original) + local filePath = value and value.filePath + if filePath and not pathToAbsolute(filePath) then + original:set(value.fileName) + else + original:set(nil) + end + end +) + +--- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerOverlay +--- Constant +--- The `EventViewer` `CustomOverlay`. +CustomOverlay.static.eventViewerOverlay = CustomOverlay.eventViewerFileName:mutate( + function(original) + local fileName = original() + return CustomOverlay.forFileName(fileName) + end, + function(value, original) + local filePath = value and value.filePath + if filePath and not pathToAbsolute(filePath) then + original:set(value.fileName) + else + original:set(nil) + end + end +) + +--- cp.apple.finalcutpro.viewer.CustomOverlay.forFileName(fileName) -> CustomOverlay | nil +--- Constructor +--- If a supported file with the provided `fileName` exists in the `Custom Overlays` folder, return a new `CustomOverlay` +--- that describes it. +--- +--- Parameters: +--- * fileName - The simple file name (eg. "My Overlay.png") +--- +--- Returns: +--- * The `CustomOverlay`, or `nil` if the file does not exist, or is not one of the supported formats. +function CustomOverlay.static.forFileName(fileName) + local name, ext = getNameAndExtensionFromFile(fileName) + if ext and CustomOverlay.ALLOWED_IMAGE_EXTENSIONS:has(ext) + and pathToAbsolute(CustomOverlay.userOverlaysPath().."/"..fileName) + then + return CustomOverlay(name, ext) + end + return nil +end + --- cp.apple.finalcutpro.viewer.CustomOverlay(name, extension) -> CustomOverlay --- Constructor --- Initializes a `CustomOverlay` with the specified name and file extension. @@ -61,11 +176,11 @@ function CustomOverlay:initialize(name, extension) self.extension = extension end ---- cp.apple.viewer.CustomOverlay.name +--- cp.apple.finalcutpro.viewer.CustomOverlay.name --- Field --- The name of the overlay, as appears in the Viewer's 'Choose Custom Overlay' menu. ---- cp.apple.viewer.CustomOverlay.extension +--- cp.apple.finalcutpro.viewer.CustomOverlay.extension --- Field --- The file extension of the overlay. @@ -83,6 +198,66 @@ function CustomOverlay.lazy.value:filePath() return CustomOverlay.userOverlaysPath() .. "/" .. self.fileName end +--- cp.apple.finalcutpro.viewer.CustomOverlay.viewerEnabled +--- Field +--- Indicates if this `CustomOverlay` is currently selected for the `Viewer`. It may not be visible if `CustomOverlay.viewerEnabled()` is not `true`. +function CustomOverlay.lazy.prop:viewerEnabled() + return CustomOverlay.viewerFileName:mutate( + function(original) + return original() == self.fileName + end, + function(value, original) + if value then + original:set(self.fileName) + else + -- only clear it if currently set to this file name. + if original() == self.fileName then + original:set(nil) + end + end + end + ) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerEnabled +--- Field +--- Indicates if this `CustomOverlay` is currently enabled for the `EventViewer`. It may not be visible if `CustomOverlay.eventViewerEnabled()` is not `true`. +function CustomOverlay.lazy.prop:eventViewerEnabled() + return CustomOverlay.eventViewerFileName:mutate( + function(original) + return original() == self.fileName + end, + function(value, original) + if value then + original:set(self.fileName) + else + -- only clear it if currently set to this file name. + if original() == self.fileName then + original:set(nil) + end + end + end + ) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.viewerOpacity +--- Field +--- The opacity of the overlay in the `Viewer`, if enabled. +function CustomOverlay.lazy.prop:viewerOpacity() + return fcpApp.preferences:prop(self.fileName.."_Opacity_"..VIEWER_PREFIX) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerOpacity +--- Field +--- The opacity of the overlay in the `EventViewer`, if enabled. +function CustomOverlay.lazy.prop:eventViewerOpacity() + return fcpApp.preferences:prop(self.fileName.."_Opacity_"..EVENT_VIEWER_PREFIX) +end + +function CustomOverlay:__eq(other) + return self.name == other.name and self.extension == other.extension +end + function CustomOverlay:__tostring() return self.name end diff --git a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua index f8a649b22..c89199009 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua @@ -20,6 +20,7 @@ local prop = require "cp.prop" local SplitGroup = require "cp.ui.SplitGroup" local ControlBar = require "cp.apple.finalcutpro.viewer.ControlBar" +local CustomOverlay = require "cp.apple.finalcutpro.viewer.CustomOverlay" local InfoBar = require "cp.apple.finalcutpro.viewer.InfoBar" local PrimaryWindow = require "cp.apple.finalcutpro.main.PrimaryWindow" local SecondaryWindow = require "cp.apple.finalcutpro.main.SecondaryWindow" @@ -479,43 +480,48 @@ end -- ----------------------------------------------------------------------- --- FFCustomOverlaySelectedCanvas = "Test.png", --- FFCustomOverlaySelectedCanvas_Opacity = 50, --- FFCustomOverlaySelectedViewer = "Test.png", --- FFCustomOverlaySelectedViewer_Opacity = 50, --- FFPlayerDisplayedCustomOverlayCanvas = 1, --- FFPlayerDisplayedCustomOverlayViewer = 1, --- ["YouTube UI.png_Opacity_Canvas"] = 50, --- ["YouTube UI.png_Opacity_Viewer"] = 50, - -local VIEWER_OVERLAY_POSTFIX = "Canvas" -local EVENT_VIEWER_OVERLAY_POSTFIX = "Viewer" - -function Viewer:_overlayPostfix() - return self:isEventViewer() and EVENT_VIEWER_OVERLAY_POSTFIX or VIEWER_OVERLAY_POSTFIX -end - +--- cp.apple.finalcutpro.viewer.Viewer.overlayEnabled +--- Field +--- Specifies if the custom overlay is enabled. function Viewer.lazy.prop:overlayEnabled() - -- TODO: Trigger "View > Show in Viewer > Show Custom Overlay" to actually toggle, to avoid UI update lag - local postfix = self:_overlayPostfix() - return self:app().preferences:prop("FFPlayerDisplayedCustomOverlay"..postfix):mutate( - function(original) return original() == 1 end, - function(newValue, original) original(newValue == 1) end - ) + if self:isEventViewer() then + return CustomOverlay.eventViewerEnabled + else + return CustomOverlay.viewerEnabled + end end +--- cp.apple.finalcutpro.viewer.Viewer.overlayFileName +--- Field +--- Specifies if the custom overlay file name. function Viewer.lazy.prop:overlayFileName() - local postfix = self:_overlayPostfix() - return self:app().preferences:prop("FFCustomOverlaySelected"..postfix) + if self:isEventViewer() then + return CustomOverlay.eventViewerFileName + else + return CustomOverlay.viewerFileName + end end +--- cp.apple.finalcutpro.viewer.Viewer.overlayOpacity +--- Field +--- Specifies if custom overlay's opacity setting. function Viewer.lazy.prop:overlayOpacity() - local postfix = self:_overlayPostfix() - return self:app().preferences:prop("FFCustomOverlaySelected"..postfix.."_Opacity") + if self:isEventViewer() then + return CustomOverlay.eventViewerOpacity + else + return CustomOverlay.viewerOpacity + end end +--- cp.apple.finalcutpro.viewer.Viewer.overlay +--- Field +--- The current `CustomOverlay` instance. May be `nil` if none is specified. function Viewer.lazy.prop:overlay() - -- TODO: Figuring out the best way to handle this... + if self:isEventViewer() then + return CustomOverlay.eventViewerOverlay + else + return CustomOverlay.viewerOverlay + end end ----------------------------------------------------------------------- From 85b1501eb86c19cbad4f1b0a7b4ea718218d87af Mon Sep 17 00:00:00 2001 From: David Peterson Date: Sun, 6 Jun 2021 02:42:18 +1000 Subject: [PATCH 4/6] * added `force` functions, which set the current overlay to a non-existent file and then updates the the new value. This doesn't work when disabling the overlay, which still requires a viewer update to refresh. * Simplified the API down to a Viewer/EventViewer 'overlay enabled' function, and retrieving the selected `overlay` for the particular viewer. * While the whole API can be accessed via CustomOverlay, most will provbably access via the Viewer props (isOverlayEnabled/overlay/userOverlays) --- .../finalcutpro/viewer/CustomOverlay.lua | 202 +++++++++++++++--- .../cp/apple/finalcutpro/viewer/Viewer.lua | 38 ++-- 2 files changed, 180 insertions(+), 60 deletions(-) diff --git a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua index 25848f1a7..43718cda4 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua @@ -9,6 +9,7 @@ local prop = require "cp.prop" local tools = require "cp.tools" local fs = require "hs.fs" +local inspect = require "hs.inspect" local class = require "middleclass" local lazy = require "cp.lazy" @@ -18,6 +19,19 @@ local insert = table.insert local CustomOverlay = class("cp.apple.finalcutpro.viewer.CustomOverlay"):include(lazy) +--- cp.apple.finalcutpro.viewer.CustomOverlay.is(thing) -> boolean +--- Function +--- Checks if the provided `thing` is a `CustomOverlay` instance. +--- +--- Parameters: +--- * thing - The thing to check. +--- +--- Returns: +--- * `true` if it is a `CustomOverlay` instance, otherwise `false`. +function CustomOverlay.static.is(thing) + return type(thing) == "table" and thing.isInstanceOf ~= nil and thing:isInstanceOf(CustomOverlay) +end + --- cp.apple.finalcutpro.ALLOWED_IMPORT_IMAGE_EXTENSIONS -> table --- Constant --- Table of image file extensions Final Cut Pro can import. @@ -32,7 +46,7 @@ function CustomOverlay.static.userOverlaysPath() return userOverlaysPath end ---- cp.apple.finalcutpro.viewer.CustomOverlay.userOverlays +--- cp.apple.finalcutpro.viewer.CustomOverlay.userOverlays --- Constant --- Contains the current list of `CustomOverlay`s available. CustomOverlay.static.userOverlays = prop(function() @@ -51,14 +65,39 @@ end) local VIEWER_PREFIX = "Canvas" local EVENT_VIEWER_PREFIX = "Viewer" ---- cp.apple.finalcutpro.viewer.CustomOverlay.viewerEnabled +local FAKE_IMAGE = "asdfghjklasdfghjklasdfghjklasdfghjkl.png" + +-- Getting the image on-screen to update without it playing/scrubbing/etc requires the file name to be modified, +-- so we set it to a non-existent image and back to force it. Doesn't work for all situations unfortunately. +local function forceUpdate(fileName) + local currentFileName = fileName:get() + fileName:set(FAKE_IMAGE) + fileName:set(currentFileName) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.forceViewerUpdate() +--- Function +--- Forces the current `Viewer` overlay to update. May cause a flicker. +--- NOTE: In general, most changes will force an update automatically anyway. +function CustomOverlay.static.forceViewerUpdate() + forceUpdate(CustomOverlay.viewerFileName) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.forceEventViewerUpdate() +--- Function +--- Forces the current `Viewer` overlay to update. May cause a flicker. +--- NOTE: In general, most changes will force an update automatically anyway. +function CustomOverlay.static.forceEventViewerUpdate() + forceUpdate(CustomOverlay.eventViewerFileName) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.isSelectedOnViewer --- Constant --- Is `true` if the `Viewer` `CustomOverlay` is enabled. -CustomOverlay.static.viewerEnabled = fcpApp.preferences:prop("FFPlayerDisplayedCustomOverlay"..VIEWER_PREFIX):mutate( - -- TODO: Trigger "View > Show in Viewer > Show Custom Overlay" to actually toggle, to avoid UI update lag +CustomOverlay.static.isEnabledOnViewer = fcpApp.preferences:prop("FFPlayerDisplayedCustomOverlay"..VIEWER_PREFIX):mutate( function(original) return original() == 1 end, function(newValue, original) original:set(newValue and 1 or 0) end -) +):watch(CustomOverlay.forceViewerUpdate) --- cp.apple.finalcutpro.viewer.CustomOverlay.viewerFileName --- Constant @@ -73,19 +112,20 @@ CustomOverlay.static.viewerFileName = fcpApp.preferences:prop("FFCustomOverlaySe end ) ---- cp.apple.finalcutpro.viewer.CustomOverlay.viewerOpacity ---- Constant ---- The `Viewer` `CustomOverlay` opacity. -CustomOverlay.static.viewerOpacity = fcpApp.preferences:prop("FFCustomOverlaySelected"..VIEWER_PREFIX.."_Opacity") +-- NOTE: Disabled since although the fields exist, the don't appear to do anything useful. Perhaps legacy? +-- --- cp.apple.finalcutpro.viewer.CustomOverlay.viewerOpacity +-- --- Constant +-- --- The `Viewer` `CustomOverlay` opacity. +-- CustomOverlay.static.viewerOpacity = fcpApp.preferences:prop("FFCustomOverlaySelected"..VIEWER_PREFIX.."_Opacity") +-- :watch(CustomOverlay.forceViewerUpdate) ---- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerEnabled +--- cp.apple.finalcutpro.viewer.CustomOverlay.isEnabledOnEventViewer --- Constant --- Is `true` if the `EventViewer` `CustomOverlay` is enabled. -CustomOverlay.static.eventViewerEnabled = fcpApp.preferences:prop("FFPlayerDisplayedCustomOverlay"..EVENT_VIEWER_PREFIX):mutate( - -- TODO: Trigger "View > Show in Event Viewer > Show Custom Overlay" to actually toggle, to avoid UI update lag +CustomOverlay.static.isEnabledOnEventViewer = fcpApp.preferences:prop("FFPlayerDisplayedCustomOverlay"..EVENT_VIEWER_PREFIX):mutate( function(original) return original() == 1 end, function(newValue, original) original:set(newValue and 1 or 0) end -) +):watch(CustomOverlay.forceEventViewerUpdate) --- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerFileName --- Constant @@ -95,15 +135,34 @@ CustomOverlay.static.eventViewerFileName = fcpApp.preferences:prop("FFCustomOver return original() end, function(value, original) - original:set(nil) + original:set(FAKE_IMAGE) original:set(value) end ) ---- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerOpacity ---- Constant ---- The `EventViewer` `CustomOverlay` opacity. -CustomOverlay.static.eventViewerOpacity = fcpApp.preferences:prop("FFCustomOverlaySelected"..EVENT_VIEWER_PREFIX.."_Opacity") +-- attempts to return an overlay based on the value. May be a `CustomOverlay`, or a `string` matching the name or filename of the overlay. +local function findOverlay(value) + if value == nil then + return nil + end + + if CustomOverlay.is(value) then + return value + elseif type(value) == "string" then + local overlay = CustomOverlay.forFileName(value) + if not overlay then + return CustomOverlay.forName(value) + end + return overlay + end +end + +-- NOTE: Disabled since although the opacity fields exist, the don't appear to do anything useful. Perhaps legacy? +-- --- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerOpacity +-- --- Constant +-- --- The `EventViewer` `CustomOverlay` opacity. +-- CustomOverlay.static.eventViewerOpacity = fcpApp.preferences:prop("FFCustomOverlaySelected"..EVENT_VIEWER_PREFIX.."_Opacity") +-- :watch(CustomOverlay.forceEventViewerUpdate) --- cp.apple.finalcutpro.viewer.CustomOverlay.viewerOverlay --- Constant @@ -114,11 +173,16 @@ CustomOverlay.static.viewerOverlay = CustomOverlay.viewerFileName:mutate( return CustomOverlay.forFileName(fileName) end, function(value, original) - local filePath = value and value.filePath - if filePath and not pathToAbsolute(filePath) then - original:set(value.fileName) - else + if value == nil then original:set(nil) + return + end + + local overlay = findOverlay(value) + if CustomOverlay.is(overlay) then + original:set(overlay.fileName) + else + error(string.format("Expected a CustomOverlay or a string with the image name or the simple name, but got: %s", inspect(value)), 3) end end ) @@ -132,11 +196,16 @@ CustomOverlay.static.eventViewerOverlay = CustomOverlay.eventViewerFileName:muta return CustomOverlay.forFileName(fileName) end, function(value, original) - local filePath = value and value.filePath - if filePath and not pathToAbsolute(filePath) then - original:set(value.fileName) - else + if value == nil then original:set(nil) + return + end + + local overlay = findOverlay(value) + if CustomOverlay.is(overlay) then + original:set(overlay.fileName) + else + error(string.format("Expected a CustomOverlay or a string with the image name or the simple name, but got: %s", inspect(value)), 3) end end ) @@ -152,6 +221,9 @@ CustomOverlay.static.eventViewerOverlay = CustomOverlay.eventViewerFileName:muta --- Returns: --- * The `CustomOverlay`, or `nil` if the file does not exist, or is not one of the supported formats. function CustomOverlay.static.forFileName(fileName) + if not fileName then + return nil + end local name, ext = getNameAndExtensionFromFile(fileName) if ext and CustomOverlay.ALLOWED_IMAGE_EXTENSIONS:has(ext) and pathToAbsolute(CustomOverlay.userOverlaysPath().."/"..fileName) @@ -161,6 +233,28 @@ function CustomOverlay.static.forFileName(fileName) return nil end +--- cp.apple.finalcutpro.viewer.CustomOverlay.forName(name) -> CustomOverlay | nil +--- Constructor +--- If a supported file with the provided `name`, as it appears in the FCP menu, exists in the `Custom Overlays` folder, +--- return a new `CustomOverlay` that describes it. +--- +--- Parameters: +--- * name - The simple file name (eg. "My Overlay") +--- +--- Returns: +--- * The `CustomOverlay`, or `nil` if the file does not exist, or is not one of the supported formats. +function CustomOverlay.static.forName(name) + if not name then + return nil + end + for _,o in ipairs(CustomOverlay.userOverlays()) do + if o.name == name then + return o + end + end + return nil +end + --- cp.apple.finalcutpro.viewer.CustomOverlay(name, extension) -> CustomOverlay --- Constructor --- Initializes a `CustomOverlay` with the specified name and file extension. @@ -198,10 +292,48 @@ function CustomOverlay.lazy.value:filePath() return CustomOverlay.userOverlaysPath() .. "/" .. self.fileName end ---- cp.apple.finalcutpro.viewer.CustomOverlay.viewerEnabled +--- cp.apple.finalcutpro.viewer.CustomOverlay.isEnabledOnViewer +--- Field +--- If `true`, the `CustomOverlay` is enabled on the `Viewer`. +function CustomOverlay.lazy.prop:isEnabledOnViewer() + return prop( + function() + return CustomOverlay.isEnabledOnViewer() and self:isSelectedOnViewer() + end, + function(enabled) + if enabled then + self:isSelectedOnViewer(true) + CustomOverlay.isEnabledOnViewer(true) + elseif self:isSelectedOnViewer() then + CustomOverlay.isEnabledOnViewer(false) + end + end + ) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.isEnabledOnEventViewer +--- Field +--- If `true`, the `CustomOverlay` is enabled on the `EventViewer`. +function CustomOverlay.lazy.prop:isEnabledOnEventViewer() + return prop( + function() + return CustomOverlay.isEnabledOnEventViewer() and self:isSelectedOnEventViewer() + end, + function(enabled) + if enabled then + self:isSelectedOnEventViewer(true) + CustomOverlay.isEnabledOnEventViewer(true) + elseif self:isSelectedOnEventViewer() then + CustomOverlay.isEnabledOnEventViewer(false) + end + end + ) +end + +--- cp.apple.finalcutpro.viewer.CustomOverlay.isSelectedOnViewer --- Field ---- Indicates if this `CustomOverlay` is currently selected for the `Viewer`. It may not be visible if `CustomOverlay.viewerEnabled()` is not `true`. -function CustomOverlay.lazy.prop:viewerEnabled() +--- Indicates if this `CustomOverlay` is currently selected for the `Viewer`. It may not be visible if `CustomOverlay.isSelectedOnViewer()` is not `true`. +function CustomOverlay.lazy.prop:isSelectedOnViewer() return CustomOverlay.viewerFileName:mutate( function(original) return original() == self.fileName @@ -219,10 +351,10 @@ function CustomOverlay.lazy.prop:viewerEnabled() ) end ---- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerEnabled +--- cp.apple.finalcutpro.viewer.CustomOverlay.isSelectedOnEventViewer --- Field ---- Indicates if this `CustomOverlay` is currently enabled for the `EventViewer`. It may not be visible if `CustomOverlay.eventViewerEnabled()` is not `true`. -function CustomOverlay.lazy.prop:eventViewerEnabled() +--- Indicates if this `CustomOverlay` is currently enabled for the `EventViewer`. It may not be visible if `CustomOverlay.isSelectedOnEventViewer()` is not `true`. +function CustomOverlay.lazy.prop:isSelectedOnEventViewer() return CustomOverlay.eventViewerFileName:mutate( function(original) return original() == self.fileName @@ -244,14 +376,16 @@ end --- Field --- The opacity of the overlay in the `Viewer`, if enabled. function CustomOverlay.lazy.prop:viewerOpacity() - return fcpApp.preferences:prop(self.fileName.."_Opacity_"..VIEWER_PREFIX) + return fcpApp.preferences:prop(self.fileName.."_Opacity_"..VIEWER_PREFIX, 100) + :watch(CustomOverlay.forceViewerUpdate) end --- cp.apple.finalcutpro.viewer.CustomOverlay.eventViewerOpacity --- Field --- The opacity of the overlay in the `EventViewer`, if enabled. function CustomOverlay.lazy.prop:eventViewerOpacity() - return fcpApp.preferences:prop(self.fileName.."_Opacity_"..EVENT_VIEWER_PREFIX) + return fcpApp.preferences:prop(self.fileName.."_Opacity_"..EVENT_VIEWER_PREFIX, 100) + :watch(CustomOverlay.forceEventViewerUpdate) end function CustomOverlay:__eq(other) diff --git a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua index c89199009..6bc31bdd2 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua @@ -480,42 +480,21 @@ end -- ----------------------------------------------------------------------- ---- cp.apple.finalcutpro.viewer.Viewer.overlayEnabled +--- cp.apple.finalcutpro.viewer.Viewer.isOverlayEnabled --- Field --- Specifies if the custom overlay is enabled. -function Viewer.lazy.prop:overlayEnabled() +function Viewer.lazy.prop:isOverlayEnabled() if self:isEventViewer() then - return CustomOverlay.eventViewerEnabled + return CustomOverlay.isEnabledOnEventViewer else - return CustomOverlay.viewerEnabled - end -end - ---- cp.apple.finalcutpro.viewer.Viewer.overlayFileName ---- Field ---- Specifies if the custom overlay file name. -function Viewer.lazy.prop:overlayFileName() - if self:isEventViewer() then - return CustomOverlay.eventViewerFileName - else - return CustomOverlay.viewerFileName - end -end - ---- cp.apple.finalcutpro.viewer.Viewer.overlayOpacity ---- Field ---- Specifies if custom overlay's opacity setting. -function Viewer.lazy.prop:overlayOpacity() - if self:isEventViewer() then - return CustomOverlay.eventViewerOpacity - else - return CustomOverlay.viewerOpacity + return CustomOverlay.isEnabledOnViewer end end --- cp.apple.finalcutpro.viewer.Viewer.overlay --- Field --- The current `CustomOverlay` instance. May be `nil` if none is specified. +--- May also be specified even if the overlay for the `Viewer` isn't enabled. function Viewer.lazy.prop:overlay() if self:isEventViewer() then return CustomOverlay.eventViewerOverlay @@ -524,6 +503,13 @@ function Viewer.lazy.prop:overlay() end end +--- cp.apple.finalcutpro.viewer.Viewer.userOverlays +--- Constant +--- Contains the current list of `CustomOverlay`s available. +function Viewer.lazy.prop.userOverlays() + return CustomOverlay.userOverlays +end + ----------------------------------------------------------------------- -- -- BROWSER UI: From 68ec80b2850387070db6667005bc46b7e8017b0d Mon Sep 17 00:00:00 2001 From: David Peterson Date: Sun, 6 Jun 2021 02:54:55 +1000 Subject: [PATCH 5/6] * Stickler issues --- .../cp/apple/finalcutpro/viewer/CustomOverlay.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua index 43718cda4..07de46071 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/CustomOverlay.lua @@ -77,7 +77,7 @@ end --- cp.apple.finalcutpro.viewer.CustomOverlay.forceViewerUpdate() --- Function ---- Forces the current `Viewer` overlay to update. May cause a flicker. +--- Forces the current `Viewer` overlay to update. May cause a flicker. --- NOTE: In general, most changes will force an update automatically anyway. function CustomOverlay.static.forceViewerUpdate() forceUpdate(CustomOverlay.viewerFileName) @@ -85,7 +85,7 @@ end --- cp.apple.finalcutpro.viewer.CustomOverlay.forceEventViewerUpdate() --- Function ---- Forces the current `Viewer` overlay to update. May cause a flicker. +--- Forces the current `Viewer` overlay to update. May cause a flicker. --- NOTE: In general, most changes will force an update automatically anyway. function CustomOverlay.static.forceEventViewerUpdate() forceUpdate(CustomOverlay.eventViewerFileName) @@ -225,7 +225,7 @@ function CustomOverlay.static.forFileName(fileName) return nil end local name, ext = getNameAndExtensionFromFile(fileName) - if ext and CustomOverlay.ALLOWED_IMAGE_EXTENSIONS:has(ext) + if ext and CustomOverlay.ALLOWED_IMAGE_EXTENSIONS:has(ext) and pathToAbsolute(CustomOverlay.userOverlaysPath().."/"..fileName) then return CustomOverlay(name, ext) From a2a4948e152dea95676ce759a2682068ffc46311 Mon Sep 17 00:00:00 2001 From: David Peterson Date: Fri, 11 Jun 2021 20:52:07 +1000 Subject: [PATCH 6/6] * Initial checkin for Update Overlays to use the Viewer.videoImage for more accurate display #2719* * Currently very broken * Partially working: - Draggable Guides as multiple canvases * Not working: - Overlays - Transparency on anything * Also, some wicked bad lag when initially loading CP (on my machine). Still determining if this is due to code in this branch or CP overall. --- .../cp/apple/finalcutpro/viewer/Viewer.lua | 4 +- src/extensions/cp/prop/init.lua | 3 + src/plugins/core/menu/manager/init.lua | 3 + src/plugins/finalcutpro/viewer/overlays.lua | 1738 ----------------- .../finalcutpro/viewer/overlays/Bar.lua | 111 ++ .../finalcutpro/viewer/overlays/Dot.lua | 181 ++ .../viewer/overlays/DraggableGuide.lua | 311 +++ .../finalcutpro/viewer/overlays/GridLayer.lua | 80 + .../finalcutpro/viewer/overlays/Overlay.lua | 10 + .../viewer/overlays/OverlayLayer.lua | 44 + .../finalcutpro/viewer/overlays/init.lua | 1350 +++++++++++++ .../finalcutpro/viewer/overlays/menus.lua | 82 + 12 files changed, 2178 insertions(+), 1739 deletions(-) delete mode 100644 src/plugins/finalcutpro/viewer/overlays.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/Bar.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/Dot.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/DraggableGuide.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/GridLayer.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/Overlay.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/OverlayLayer.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/init.lua create mode 100644 src/plugins/finalcutpro/viewer/overlays/menus.lua diff --git a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua index 6bc31bdd2..9113f4ef3 100644 --- a/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua +++ b/src/extensions/cp/apple/finalcutpro/viewer/Viewer.lua @@ -283,7 +283,9 @@ end --- Field --- The `Image` for the video content. function Viewer.lazy.value:videoImage() - return Image(self, self.videoImageUI) + local videoImage = Image(self, self.videoImageUI) + videoImage.frame:monitor(self.frame) + return videoImage end --- cp.apple.finalcutpro.viewer.Viewer.infoBar diff --git a/src/extensions/cp/prop/init.lua b/src/extensions/cp/prop/init.lua index e261ab121..48f1233dc 100644 --- a/src/extensions/cp/prop/init.lua +++ b/src/extensions/cp/prop/init.lua @@ -693,6 +693,9 @@ local NOTHING = {} --- Notes: --- * You can watch immutable values. Wrapped `cp.prop` instances may not be immutable, and any changes to them will cause watchers to be notified up the chain. function prop.mt:watch(watchFn, notifyNow, uncloned) + if watchFn == nil then + error("the watchFn was nil", 2) + end if not self._watchers then self._watchers = {} end diff --git a/src/plugins/core/menu/manager/init.lua b/src/plugins/core/menu/manager/init.lua index 5eee14a56..c26a2d241 100644 --- a/src/plugins/core/menu/manager/init.lua +++ b/src/plugins/core/menu/manager/init.lua @@ -183,6 +183,9 @@ function mod.generateMenuTable() return mod.rootSection:generateMenuTable() end +-- Makes the `section` API accessible outside the menu manger. +mod.section = section + local plugin = { id = "core.menu.manager", group = "core", diff --git a/src/plugins/finalcutpro/viewer/overlays.lua b/src/plugins/finalcutpro/viewer/overlays.lua deleted file mode 100644 index 73bcad79c..000000000 --- a/src/plugins/finalcutpro/viewer/overlays.lua +++ /dev/null @@ -1,1738 +0,0 @@ ---- === plugins.finalcutpro.viewer.overlays === ---- ---- Final Cut Pro Viewer Overlays. - -local require = require - -local log = require "hs.logger".new "overlays" - -local hs = _G.hs - -local canvas = require "hs.canvas" -local dialog = require "hs.dialog" -local eventtap = require "hs.eventtap" -local fs = require "hs.fs" -local geometry = require "hs.geometry" -local hid = require "hs.hid" -local image = require "hs.image" -local menubar = require "hs.menubar" -local mouse = require "hs.mouse" -local timer = require "hs.timer" - -local axutils = require "cp.ui.axutils" -local config = require "cp.config" -local cpDialog = require "cp.dialog" -local deferred = require "cp.deferred" -local fcp = require "cp.apple.finalcutpro" -local i18n = require "cp.i18n" -local tools = require "cp.tools" - -local Do = require "cp.rx.go.Do" - -local capslock = hid.capslock -local doAfter = timer.doAfter -local events = eventtap.event.types - -local mod = {} - --- STILLS_FOLDER -> string --- Constant --- Folder name for Stills Cache. -local STILLS_FOLDER = "Still Frames" - --- DEFAULT_COLOR -> string --- Constant --- Default Colour Setting. -local DEFAULT_COLOR = "#FFFFFF" - --- DEFAULT_ALPHA -> number --- Constant --- Default Alpha Setting. -local DEFAULT_ALPHA = 50 - --- DEFAULT_GRID_SPACING -> number --- Constant --- Default Grid Spacing Setting. -local DEFAULT_GRID_SPACING = 20 - --- DEFAULT_STILLS_LAYOUT -> number --- Constant --- Default Stills Layout Setting. -local DEFAULT_STILLS_LAYOUT = "Left Vertical" - --- DEFAULT_LETTERBOX_HEIGHT -> number --- Constant --- Default Letterbox Height -local DEFAULT_LETTERBOX_HEIGHT = 70 - --- FCP_COLOR_BLUE -> string --- Constant --- Apple's preferred blue colour in Final Cut Pro. -local FCP_COLOR_BLUE = "#5760e7" - --- CROSS_HAIR_LENGTH -> number --- Constant --- Cross Hair Length -local CROSS_HAIR_LENGTH = 100 - --- plugins.finalcutpro.viewer.overlays.NUMBER_OF_MEMORIES -> number --- Constant --- Number of Stills Memories Available. -mod.NUMBER_OF_MEMORIES = 5 - --- plugins.finalcutpro.viewer.overlays.NUMBER_OF_DRAGGABLE_GUIDES -> number --- Constant --- Number of Draggable Guides Available. -mod.NUMBER_OF_DRAGGABLE_GUIDES = 5 - ---- plugins.finalcutpro.viewer.overlays.enableViewerRightClick ---- Variable ---- Allow the user to right click on the top of the viewer to access the menu? -mod.enableViewerRightClick = config.prop("fcpx.ViewerOverlay.EnableViewerRightClick", false) - ---- plugins.finalcutpro.viewer.overlays.disabled ---- Variable ---- Are all the Viewer Overlay's disabled? -mod.disabled = config.prop("fcpx.ViewerOverlay.MasterDisabled", false) - ---- plugins.finalcutpro.viewer.overlays.crossHairEnabled ---- Variable ---- Is Viewer Cross Hair Enabled? -mod.crossHairEnabled = config.prop("fcpx.ViewerOverlay.CrossHair.Enabled", false) - ---- plugins.finalcutpro.viewer.overlays.letterboxEnabled ---- Variable ---- Is Viewer Letterbox Enabled? -mod.letterboxEnabled = config.prop("fcpx.ViewerOverlay.Letterbox.Enabled", false) - ---- plugins.finalcutpro.viewer.overlays.letterboxHeight ---- Variable ---- Letterbox Height -mod.letterboxHeight = config.prop("fcpx.ViewerOverlay.Letterbox.Height", DEFAULT_LETTERBOX_HEIGHT) - ---- plugins.finalcutpro.viewer.overlays.basicGridEnabled ---- Variable ---- Is Viewer Grid Enabled? -mod.basicGridEnabled = config.prop("fcpx.ViewerOverlay.BasicGrid.Enabled", false) - ---- plugins.finalcutpro.viewer.overlays.crossHairColor ---- Variable ---- Viewer Grid Color as HTML value -mod.crossHairColor = config.prop("fcpx.ViewerOverlay.CrossHair.Color", DEFAULT_COLOR) - ---- plugins.finalcutpro.viewer.overlays.crossHairAlpha ---- Variable ---- Viewer Grid Alpha -mod.crossHairAlpha = config.prop("fcpx.ViewerOverlay.CrossHair.Alpha", DEFAULT_ALPHA) - ---- plugins.finalcutpro.viewer.overlays.gridColor ---- Variable ---- Viewer Grid Color as HTML value -mod.gridColor = config.prop("fcpx.ViewerOverlay.Grid.Color", DEFAULT_COLOR) - ---- plugins.finalcutpro.viewer.overlays.gridAlpha ---- Variable ---- Viewer Grid Alpha -mod.gridAlpha = config.prop("fcpx.ViewerOverlay.Grid.Alpha", DEFAULT_ALPHA) - ---- plugins.finalcutpro.viewer.overlays.customGridColor ---- Variable ---- Viewer Custom Grid Color as HTML value -mod.customGridColor = config.prop("fcpx.ViewerOverlay.Grid.CustomColor", nil) - ---- plugins.finalcutpro.viewer.overlays.gridSpacing ---- Variable ---- Viewer Custom Grid Color as HTML value -mod.gridSpacing = config.prop("fcpx.ViewerOverlay.Grid.Spacing", DEFAULT_GRID_SPACING) - ---- plugins.finalcutpro.viewer.overlays.activeMemory ---- Variable ---- Viewer Custom Grid Color as HTML value -mod.activeMemory = config.prop("fcpx.ViewerOverlay.ActiveMemory", 0) - ---- plugins.finalcutpro.viewer.overlays.stillsLayout ---- Variable ---- Stills layout. -mod.stillsLayout = config.prop("fcpx.ViewerOverlay.StillsLayout", DEFAULT_STILLS_LAYOUT) - ---- plugins.finalcutpro.viewer.overlays.draggableGuideEnabled ---- Variable ---- Is Viewer Grid Enabled? -mod.draggableGuideEnabled = config.prop("fcpx.ViewerOverlay.DraggableGuide.Enabled", {}) - ---- plugins.finalcutpro.viewer.overlays.guidePosition ---- Variable ---- Guide Position. -mod.guidePosition = config.prop("fcpx.ViewerOverlay.GuidePosition", {}) - ---- plugins.finalcutpro.viewer.overlays.guideColor ---- Variable ---- Viewer Guide Color as HTML value -mod.guideColor = config.prop("fcpx.ViewerOverlay.Guide.Color", {}) - ---- plugins.finalcutpro.viewer.overlays.guideAlpha ---- Variable ---- Viewer Guide Alpha -mod.guideAlpha = config.prop("fcpx.ViewerOverlay.Guide.Alpha", {}) - ---- plugins.finalcutpro.viewer.overlays.customGuideColor ---- Variable ---- Viewer Custom Guide Color as HTML value -mod.customGuideColor = config.prop("fcpx.ViewerOverlay.Guide.CustomColor", {}) - ---- plugins.finalcutpro.viewer.overlays.customCrossHairColor ---- Variable ---- Viewer Custom Cross Hair Color as HTML value -mod.customCrossHairColor = config.prop("fcpx.ViewerOverlay.CrossHair.CustomColor", {}) - ---- plugins.finalcutpro.viewer.overlays.capslock ---- Variable ---- Toggle Viewer Overlays with Caps Lock. -mod.capslock = config.prop("fcpx.ViewerOverlay.CapsLock", false):watch(function(enabled) - if not enabled then - if mod._capslockEventTap then - mod._capslockEventTap:stop() - mod._capslockEventTap = nil - end - end -end) - ---- plugins.finalcutpro.viewer.overlays.show() -> none ---- Function ---- Show's the Viewer Grid. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.show() - - -------------------------------------------------------------------------------- - -- First, we must destroy any existing canvas: - -------------------------------------------------------------------------------- - mod.hide() - - local videoImage = fcp.viewer.videoImage - if videoImage then - local frame = videoImage:frame() - if frame then - -------------------------------------------------------------------------------- - -- New Canvas: - -------------------------------------------------------------------------------- - mod._canvas = canvas.new(frame) - - -------------------------------------------------------------------------------- - -- Still Frames: - -------------------------------------------------------------------------------- - local stillsLayout = mod.stillsLayout() - local activeMemory = mod.activeMemory() - if activeMemory ~= 0 then - local memory = mod.getMemory(activeMemory) - if memory then - if stillsLayout == "Left Vertical" then - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = 0, y = 0, h = "100%", w = "50%"}, - action = "clip", - }) - elseif stillsLayout == "Right Vertical" then - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = frame.w/2, y = 0, h = "100%", w = frame.w/2}, - action = "clip", - }) - elseif stillsLayout == "Top Horizontal" then - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = 0, y = 0, h = "50%", w = "100%"}, - action = "clip", - }) - elseif stillsLayout == "Bottom Horizontal" then - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = 0, y = frame.h/2, h = "50%", w = "100%"}, - action = "clip", - }) - end - mod._canvas:appendElements({ - type = "image", - frame = { x = 0, y = 0, h = "100%", w = "100%"}, - action = "fill", - image = memory, - imageScaling = "scaleProportionally", - imageAlignment = "topLeft", - }) - if stillsLayout ~= "Full Frame" then - mod._canvas:appendElements({ - type = "resetClip", - }) - end - end - end - - -------------------------------------------------------------------------------- - -- Cross Hair: - -------------------------------------------------------------------------------- - if mod.crossHairEnabled() then - - local length = CROSS_HAIR_LENGTH - - local crossHairColor = mod.crossHairColor() - local crossHairAlpha = mod.crossHairAlpha() / 100 - - local fillColor - if crossHairColor == "CUSTOM" and mod.customCrossHairColor() then - fillColor = mod.customCrossHairColor() - fillColor.alpha = crossHairAlpha - else - fillColor = { hex = crossHairColor, alpha = crossHairAlpha } - end - - -------------------------------------------------------------------------------- - -- Horizontal Bar: - -------------------------------------------------------------------------------- - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = (frame.w / 2) - (length/2), y = frame.h / 2, h = 1, w = length}, - fillColor = fillColor, - action = "fill", - }) - - -------------------------------------------------------------------------------- - -- Vertical Bar: - -------------------------------------------------------------------------------- - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = frame.w / 2, y = (frame.h / 2) - (length/2), h = length, w = 1}, - fillColor = fillColor, - action = "fill", - }) - - end - - -------------------------------------------------------------------------------- - -- Basic Grid: - -------------------------------------------------------------------------------- - local gridColor = mod.gridColor() - local gridAlpha = mod.gridAlpha() / 100 - local gridSpacing = mod.gridSpacing() - local fillColor - if gridColor == "CUSTOM" and mod.customGridColor() then - fillColor = mod.customGridColor() - fillColor.alpha = gridAlpha - else - fillColor = { hex = gridColor, alpha = gridAlpha } - end - if mod.basicGridEnabled() then - -------------------------------------------------------------------------------- - -- Add Vertical Lines: - -------------------------------------------------------------------------------- - for i=1, frame.w, frame.w/gridSpacing do - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = i, y = 0, h = frame.h, w = 1}, - fillColor = fillColor, - action = "fill", - }) - end - - -------------------------------------------------------------------------------- - -- Add Horizontal Lines: - -------------------------------------------------------------------------------- - for i=1, frame.h, frame.w/gridSpacing do - mod._canvas:appendElements({ - type = "rectangle", - frame = { x = 0, y = i, h = 1, w = frame.w}, - fillColor = fillColor, - action = "fill", - }) - end - end - - -------------------------------------------------------------------------------- - -- Draggable Guides: - -------------------------------------------------------------------------------- - local draggableGuideEnabled = false - for i=1, mod.NUMBER_OF_DRAGGABLE_GUIDES do - if mod.getDraggableGuideEnabled(i) then - - draggableGuideEnabled = true - - local guidePosition = mod.getGuidePosition(i) - local guideColor = mod.getGuideColor(i) - local guideAlpha = mod.getGuideAlpha(i) / 100 - local customGuideColor = mod.getCustomGuideColor(i) - - if guideColor == "CUSTOM" and customGuideColor then - fillColor = customGuideColor - fillColor.alpha = guideAlpha - else - fillColor = { hex = guideColor, alpha = guideAlpha } - end - - local savedX, savedY - if guidePosition.x and guidePosition.y then - savedX = guidePosition.x - savedY = guidePosition.y - else - savedX = frame.w/2 - savedY = frame.h/2 - end - mod._canvas:appendElements({ - id = "dragVertical" .. i, - action = "stroke", - closed = false, - coordinates = { { x = savedX, y = 0 }, { x = savedX, y = frame.h } }, - strokeColor = fillColor, - strokeWidth = 2, - type = "segments", - }) - mod._canvas:appendElements({ - id = "dragHorizontal" .. i, - action = "stroke", - closed = false, - coordinates = { { x = 0, y = savedY }, { x = frame.w, y = savedY } }, - strokeColor = fillColor, - strokeWidth = 2, - type = "segments", - }) - mod._canvas:appendElements({ - id = "dragCentreKill" .. i, - action = "fill", - center = { x = savedX, y = savedY }, - radius = 8, - fillColor = fillColor, - type = "circle", - compositeRule = "clear", - }) - mod._canvas:appendElements({ - id = "dragCentre" .. i, - action = "fill", - center = { x = savedX, y = savedY }, - radius = 8, - fillColor = fillColor, - type = "circle", - trackMouseDown = true, - trackMouseUp = true, - trackMouseMove = true, - }) - end - end - - -------------------------------------------------------------------------------- - -- Letterbox: - -------------------------------------------------------------------------------- - if mod.letterboxEnabled() then - - local letterboxHeight = mod.letterboxHeight() - mod._canvas:appendElements({ - id = "topLetterbox", - type = "rectangle", - frame = { x = 0, y = 0, h = letterboxHeight, w = "100%"}, - fillColor = { hex = "#000000", alpha = 1 }, - action = "fill", - trackMouseDown = true, - trackMouseUp = true, - trackMouseMove = true, - }) - - mod._canvas:appendElements({ - id = "bottomLetterbox", - type = "rectangle", - frame = { x = 0, y = frame.h - letterboxHeight, h = letterboxHeight, w = "100%"}, - fillColor = { hex = "#000000", alpha = 1 }, - action = "fill", - trackMouseDown = true, - trackMouseUp = true, - trackMouseMove = true, - }) - - end - - -------------------------------------------------------------------------------- - -- Mouse Actions for Canvas: - -------------------------------------------------------------------------------- - if draggableGuideEnabled or mod.letterboxEnabled() then - mod._canvas:clickActivating(false) - mod._canvas:canvasMouseEvents(true, true, true, true) - mod._canvas:mouseCallback(function(_, event, id) - -------------------------------------------------------------------------------- - -- Draggable Guides: - -------------------------------------------------------------------------------- - if draggableGuideEnabled then - for i=1, mod.NUMBER_OF_DRAGGABLE_GUIDES do - -------------------------------------------------------------------------------- - -- Reset Guide on Double Click: - -------------------------------------------------------------------------------- - if id == "dragCentre" .. i and event == "mouseUp" then - if mod._draggableGuideDoubleClick then - - local newX = frame.w/2 - local newY = frame.h/2 - - mod._canvas["dragCentre" .. i].center = { x = newX, y = newY} - mod._canvas["dragCentreKill" .. i].center = {x = newX, y = newY } - mod._canvas["dragVertical" .. i].coordinates = { { x = newX, y = 0 }, { x = newX, y = frame.h } } - mod._canvas["dragHorizontal" .. i].coordinates = { { x = 0, y = newY }, { x = frame.w, y = newY } } - mod.setGuidePosition(i, {x=newX, y=newY}) - - mod._draggableGuideDoubleClick = false - else - mod._draggableGuideDoubleClick = true - doAfter(eventtap.doubleClickInterval(), function() - mod._draggableGuideDoubleClick = false - end) - end - end - - if id == "dragCentre" .. i and event == "mouseDown" then - if not mod._mouseMoveTracker then - mod._mouseMoveTracker = {} - end - if mod._mouseMoveLetterboxTracker then - mod._mouseMoveLetterboxTracker:stop() - mod._mouseMoveLetterboxTracker = nil - end - if not mod._mouseMoveTracker[i] then - mod._mouseMoveTracker[i] = eventtap.new({ events.leftMouseDragged, events.leftMouseUp }, function(e) - if e:getType() == events.leftMouseUp then - mod._mouseMoveTracker[i]:stop() - mod._mouseMoveTracker[i] = nil - else - Do(function() - if mod._canvas then - local mousePosition = mouse.absolutePosition() - local canvasTopLeft = mod._canvas:topLeft() - local newX = mousePosition.x - canvasTopLeft.x - local newY = mousePosition.y - canvasTopLeft.y - local viewerFrame = geometry.new(frame) - if geometry.new(mousePosition):inside(viewerFrame) then - mod._canvas["dragCentre" .. i].center = { x = newX, y = newY} - mod._canvas["dragCentreKill" .. i].center = {x = newX, y = newY } - mod._canvas["dragVertical" .. i].coordinates = { { x = newX, y = 0 }, { x = newX, y = frame.h } } - mod._canvas["dragHorizontal" .. i].coordinates = { { x = 0, y = newY }, { x = frame.w, y = newY } } - mod.setGuidePosition(i, {x=newX, y=newY}) - end - end - end):After(0) - end - end, false):start() - end - end - end - end - - -------------------------------------------------------------------------------- - -- Letterbox: - -------------------------------------------------------------------------------- - if mod.letterboxEnabled() then - if id == "topLetterbox" or id == "bottomLetterbox" and event == "mouseDown" then - if not mod._mouseMoveLetterboxTracker then - mod._mouseMoveLetterboxTracker = eventtap.new({ events.leftMouseDragged, events.leftMouseUp }, function(e) - if e:getType() == events.leftMouseUp then - mod._mouseMoveLetterboxTracker:stop() - mod._mouseMoveLetterboxTracker = nil - else - Do(function() - if mod._canvas then - local mousePosition = mouse.absolutePosition() - local canvasTopLeft = mod._canvas:topLeft() - local letterboxHeight = mousePosition.y - canvasTopLeft.y - local viewerFrame = geometry.new(frame) - if geometry.new(mousePosition):inside(viewerFrame) and letterboxHeight > 10 and letterboxHeight < (frame.h/2) then - mod._canvas["topLetterbox"].frame = { x = 0, y = 0, h = letterboxHeight, w = "100%"} - mod._canvas["bottomLetterbox"].frame = { x = 0, y = frame.h - letterboxHeight, h = letterboxHeight, w = "100%"} - mod.letterboxHeight(letterboxHeight) - end - end - end):After(0) - end - end, false):start() - end - end - end - end) - end - - -------------------------------------------------------------------------------- - -- Add Border: - -------------------------------------------------------------------------------- - mod._canvas:appendElements({ - id = "border", - type = "rectangle", - action = "stroke", - strokeColor = { hex = FCP_COLOR_BLUE }, - strokeWidth = 5, - }) - - -------------------------------------------------------------------------------- - -- Show the Canvas: - -------------------------------------------------------------------------------- - mod._canvas:level("status") - mod._canvas:show() - end - else - mod.hide() - end -end - ---- plugins.finalcutpro.viewer.overlays.hide() -> none ---- Function ---- Hides the Viewer Grid. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.hide() - if mod._canvas then - mod._canvas:delete() - mod._canvas = nil - end -end - ---- plugins.finalcutpro.viewer.overlays.draggableGuidesEnabled() -> boolean ---- Function ---- Are any draggable guides enabled? ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * `true` if at least one draggable guide is enabled otherwise `false` -function mod.draggableGuidesEnabled() - local draggableGuideEnabled = mod.draggableGuideEnabled() - if draggableGuideEnabled then - for id=1, mod.NUMBER_OF_DRAGGABLE_GUIDES do - if draggableGuideEnabled[tostring(id)] == true then - return true - end - end - end - return false -end - --- areAnyOverlaysEnabled() -> boolean --- Function --- Are any Final Cut Pro Viewer Overlays Enabled? --- --- Parameters: --- * None --- --- Returns: --- * `true` if any Viewer Overlays are enabled otherwise `false` -local function areAnyOverlaysEnabled() - return mod.basicGridEnabled() == true - or mod.draggableGuidesEnabled() == true - or mod.activeMemory() ~= 0 - or mod.crossHairEnabled() == true - or mod.letterboxEnabled() == true - or false -end - --- generateMenu -> table --- Function --- Returns a table with the Overlay menu. --- --- Parameters: --- * None --- --- Returns: --- * A table containing the overlay menu. -local function generateMenu() - return { - -------------------------------------------------------------------------------- - -- - -- ENABLE OVERLAYS: - -- - -------------------------------------------------------------------------------- - { title = i18n("enable") .. " " .. i18n("overlays"), checked = not mod.capslock() and not mod.disabled(), fn = function() mod.disabled:toggle(); mod.update() end, disabled = mod.capslock() }, - { title = i18n("toggleOverlaysWithCapsLock"), checked = mod.capslock(), fn = function() mod.capslock:toggle(); mod.update() end }, - { title = "-", disabled = true }, - -------------------------------------------------------------------------------- - -- - -- DRAGGABLE GUIDES: - -- - -------------------------------------------------------------------------------- - { title = string.upper(i18n("guides")) .. ":", disabled = true }, - { title = " " .. i18n("draggableGuides"), menu = { - { title = i18n("enableAll"), fn = mod.enableAllDraggableGuides }, - { title = i18n("disableAll"), fn = mod.disableAllDraggableGuides }, - { title = "-", disabled = true }, - { title = i18n("guide") .. " 1", menu = { - { title = i18n("enable"), checked = mod.getDraggableGuideEnabled(1), fn = function() mod.toggleDraggableGuide(1); mod.update(); end }, - { title = i18n("reset"), fn = function() mod.resetDraggableGuide(1) end }, - { title = i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.getGuideColor(1) == "#000000", fn = function() mod.setGuideColor(1, "#000000") end }, - { title = i18n("white"), checked = mod.getGuideColor(1) == "#FFFFFF", fn = function() mod.setGuideColor(1, "#FFFFFF") end }, - { title = i18n("yellow"), checked = mod.getGuideColor(1) == "#F4D03F", fn = function() mod.setGuideColor(1, "#F4D03F") end }, - { title = i18n("red"), checked = mod.getGuideColor(1) == "#FF5733", fn = function() mod.setGuideColor(1, "#FF5733") end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.getGuideColor(1) == "CUSTOM", fn = function() mod.setCustomGuideColor(1) end}, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.getGuideAlpha(1) == 10, fn = function() mod.setGuideAlpha(1, 10) end }, - { title = "20%", checked = mod.getGuideAlpha(1) == 20, fn = function() mod.setGuideAlpha(1, 20) end }, - { title = "30%", checked = mod.getGuideAlpha(1) == 30, fn = function() mod.setGuideAlpha(1, 30) end }, - { title = "40%", checked = mod.getGuideAlpha(1) == 40, fn = function() mod.setGuideAlpha(1, 40) end }, - { title = "50%", checked = mod.getGuideAlpha(1) == 50, fn = function() mod.setGuideAlpha(1, 50) end }, - { title = "60%", checked = mod.getGuideAlpha(1) == 60, fn = function() mod.setGuideAlpha(1, 60) end }, - { title = "70%", checked = mod.getGuideAlpha(1) == 70, fn = function() mod.setGuideAlpha(1, 70) end }, - { title = "80%", checked = mod.getGuideAlpha(1) == 80, fn = function() mod.setGuideAlpha(1, 80) end }, - { title = "90%", checked = mod.getGuideAlpha(1) == 90, fn = function() mod.setGuideAlpha(1, 90) end }, - { title = "100%", checked = mod.getGuideAlpha(1) == 100, fn = function() mod.setGuideAlpha(1, 100) end }, - }}, - }}, - }}, - { title = i18n("guide") .. " 2", menu = { - { title = i18n("enable"), checked = mod.getDraggableGuideEnabled(2), fn = function() mod.toggleDraggableGuide(2); mod.update(); end }, - { title = i18n("reset"), fn = function() mod.resetDraggableGuide(2) end }, - { title = i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.getGuideColor(2) == "#000000", fn = function() mod.setGuideColor(2, "#000000") end }, - { title = i18n("white"), checked = mod.getGuideColor(2) == "#FFFFFF", fn = function() mod.setGuideColor(2, "#FFFFFF") end }, - { title = i18n("yellow"), checked = mod.getGuideColor(2) == "#F4D03F", fn = function() mod.setGuideColor(2, "#F4D03F") end }, - { title = i18n("red"), checked = mod.getGuideColor(2) == "#FF5733", fn = function() mod.setGuideColor(2, "#FF5733") end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.getGuideColor(2) == "CUSTOM", fn = function() mod.setCustomGuideColor(2) end}, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.getGuideAlpha(2) == 10, fn = function() mod.setGuideAlpha(2, 10) end }, - { title = "20%", checked = mod.getGuideAlpha(2) == 20, fn = function() mod.setGuideAlpha(2, 20) end }, - { title = "30%", checked = mod.getGuideAlpha(2) == 30, fn = function() mod.setGuideAlpha(2, 30) end }, - { title = "40%", checked = mod.getGuideAlpha(2) == 40, fn = function() mod.setGuideAlpha(2, 40) end }, - { title = "50%", checked = mod.getGuideAlpha(2) == 50, fn = function() mod.setGuideAlpha(2, 50) end }, - { title = "60%", checked = mod.getGuideAlpha(2) == 60, fn = function() mod.setGuideAlpha(2, 60) end }, - { title = "70%", checked = mod.getGuideAlpha(2) == 70, fn = function() mod.setGuideAlpha(2, 70) end }, - { title = "80%", checked = mod.getGuideAlpha(2) == 80, fn = function() mod.setGuideAlpha(2, 80) end }, - { title = "90%", checked = mod.getGuideAlpha(2) == 90, fn = function() mod.setGuideAlpha(2, 90) end }, - { title = "100%", checked = mod.getGuideAlpha(2) == 100, fn = function() mod.setGuideAlpha(2, 100) end }, - }}, - }}, - }}, - { title = i18n("guide") .. " 3", menu = { - { title = i18n("enable"), checked = mod.getDraggableGuideEnabled(3), fn = function() mod.toggleDraggableGuide(3); mod.update(); end }, - { title = i18n("reset"), fn = function() mod.resetDraggableGuide(3) end }, - { title = i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.getGuideColor(3) == "#000000", fn = function() mod.setGuideColor(3, "#000000") end }, - { title = i18n("white"), checked = mod.getGuideColor(3) == "#FFFFFF", fn = function() mod.setGuideColor(3, "#FFFFFF") end }, - { title = i18n("yellow"), checked = mod.getGuideColor(3) == "#F4D03F", fn = function() mod.setGuideColor(3, "#F4D03F") end }, - { title = i18n("red"), checked = mod.getGuideColor(3) == "#FF5733", fn = function() mod.setGuideColor(3, "#FF5733") end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.getGuideColor(3) == "CUSTOM", fn = function() mod.setCustomGuideColor(3) end}, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.getGuideAlpha(3) == 10, fn = function() mod.setGuideAlpha(3, 10) end }, - { title = "20%", checked = mod.getGuideAlpha(3) == 20, fn = function() mod.setGuideAlpha(3, 20) end }, - { title = "30%", checked = mod.getGuideAlpha(3) == 30, fn = function() mod.setGuideAlpha(3, 30) end }, - { title = "40%", checked = mod.getGuideAlpha(3) == 40, fn = function() mod.setGuideAlpha(3, 40) end }, - { title = "50%", checked = mod.getGuideAlpha(3) == 50, fn = function() mod.setGuideAlpha(3, 50) end }, - { title = "60%", checked = mod.getGuideAlpha(3) == 60, fn = function() mod.setGuideAlpha(3, 60) end }, - { title = "70%", checked = mod.getGuideAlpha(3) == 70, fn = function() mod.setGuideAlpha(3, 70) end }, - { title = "80%", checked = mod.getGuideAlpha(3) == 80, fn = function() mod.setGuideAlpha(3, 80) end }, - { title = "90%", checked = mod.getGuideAlpha(3) == 90, fn = function() mod.setGuideAlpha(3, 90) end }, - { title = "100%", checked = mod.getGuideAlpha(3) == 100, fn = function() mod.setGuideAlpha(3, 100) end }, - }}, - }}, - }}, - { title = i18n("guide") .. " 4", menu = { - { title = i18n("enable"), checked = mod.getDraggableGuideEnabled(4), fn = function() mod.toggleDraggableGuide(4); mod.update(); end }, - { title = i18n("reset"), fn = function() mod.resetDraggableGuide(4) end }, - { title = i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.getGuideColor(4) == "#000000", fn = function() mod.setGuideColor(4, "#000000") end }, - { title = i18n("white"), checked = mod.getGuideColor(4) == "#FFFFFF", fn = function() mod.setGuideColor(4, "#FFFFFF") end }, - { title = i18n("yellow"), checked = mod.getGuideColor(4) == "#F4D03F", fn = function() mod.setGuideColor(4, "#F4D03F") end }, - { title = i18n("red"), checked = mod.getGuideColor(4) == "#FF5733", fn = function() mod.setGuideColor(4, "#FF5733") end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.getGuideColor(4) == "CUSTOM", fn = function() mod.setCustomGuideColor(4) end}, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.getGuideAlpha(4) == 10, fn = function() mod.setGuideAlpha(4, 10) end }, - { title = "20%", checked = mod.getGuideAlpha(4) == 20, fn = function() mod.setGuideAlpha(4, 20) end }, - { title = "30%", checked = mod.getGuideAlpha(4) == 30, fn = function() mod.setGuideAlpha(4, 30) end }, - { title = "40%", checked = mod.getGuideAlpha(4) == 40, fn = function() mod.setGuideAlpha(4, 40) end }, - { title = "50%", checked = mod.getGuideAlpha(4) == 50, fn = function() mod.setGuideAlpha(4, 50) end }, - { title = "60%", checked = mod.getGuideAlpha(4) == 60, fn = function() mod.setGuideAlpha(4, 60) end }, - { title = "70%", checked = mod.getGuideAlpha(4) == 70, fn = function() mod.setGuideAlpha(4, 70) end }, - { title = "80%", checked = mod.getGuideAlpha(4) == 80, fn = function() mod.setGuideAlpha(4, 80) end }, - { title = "90%", checked = mod.getGuideAlpha(4) == 90, fn = function() mod.setGuideAlpha(4, 90) end }, - { title = "100%", checked = mod.getGuideAlpha(4) == 100, fn = function() mod.setGuideAlpha(4, 100) end }, - }}, - }}, - }}, - { title = i18n("guide") .. " 5", menu = { - { title = i18n("enable"), checked = mod.getDraggableGuideEnabled(5), fn = function() mod.toggleDraggableGuide(5); mod.update(); end }, - { title = i18n("reset"), fn = function() mod.resetDraggableGuide(5) end }, - { title = i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.getGuideColor(5) == "#000000", fn = function() mod.setGuideColor(5, "#000000") end }, - { title = i18n("white"), checked = mod.getGuideColor(5) == "#FFFFFF", fn = function() mod.setGuideColor(5, "#FFFFFF") end }, - { title = i18n("yellow"), checked = mod.getGuideColor(5) == "#F4D03F", fn = function() mod.setGuideColor(5, "#F4D03F") end }, - { title = i18n("red"), checked = mod.getGuideColor(5) == "#FF5733", fn = function() mod.setGuideColor(5, "#FF5733") end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.getGuideColor(5) == "CUSTOM", fn = function() mod.setCustomGuideColor(5) end}, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.getGuideAlpha(5) == 10, fn = function() mod.setGuideAlpha(5, 10) end }, - { title = "20%", checked = mod.getGuideAlpha(5) == 20, fn = function() mod.setGuideAlpha(5, 20) end }, - { title = "30%", checked = mod.getGuideAlpha(5) == 30, fn = function() mod.setGuideAlpha(5, 30) end }, - { title = "40%", checked = mod.getGuideAlpha(5) == 40, fn = function() mod.setGuideAlpha(5, 40) end }, - { title = "50%", checked = mod.getGuideAlpha(5) == 50, fn = function() mod.setGuideAlpha(5, 50) end }, - { title = "60%", checked = mod.getGuideAlpha(5) == 60, fn = function() mod.setGuideAlpha(5, 60) end }, - { title = "70%", checked = mod.getGuideAlpha(5) == 70, fn = function() mod.setGuideAlpha(5, 70) end }, - { title = "80%", checked = mod.getGuideAlpha(5) == 80, fn = function() mod.setGuideAlpha(5, 80) end }, - { title = "90%", checked = mod.getGuideAlpha(5) == 90, fn = function() mod.setGuideAlpha(5, 90) end }, - { title = "100%", checked = mod.getGuideAlpha(5) == 100, fn = function() mod.setGuideAlpha(5, 100) end }, - }}, - }}, - }}, - }}, - -------------------------------------------------------------------------------- - -- - -- CROSS HAIR: - -- - -------------------------------------------------------------------------------- - { title = " " .. i18n("crossHair"), menu = { - { title = i18n("enable"), checked = mod.crossHairEnabled(), fn = function() mod.crossHairEnabled:toggle(); mod.update(); end }, - { title = i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.crossHairColor() == "#000000", fn = function() mod.crossHairColor("#000000"); mod.update() end }, - { title = i18n("white"), checked = mod.crossHairColor() == "#FFFFFF", fn = function() mod.crossHairColor("#FFFFFF"); mod.update() end }, - { title = i18n("yellow"), checked = mod.crossHairColor() == "#F4D03F", fn = function() mod.crossHairColor("#F4D03F"); mod.update() end }, - { title = i18n("red"), checked = mod.crossHairColor() == "#FF5733", fn = function() mod.crossHairColor("#FF5733"); mod.update() end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.crossHairColor() == "CUSTOM", fn = function() mod.setCustomCrossHairColor(); mod.update() end}, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.crossHairAlpha() == 10, fn = function() mod.crossHairAlpha(10); mod.update() end }, - { title = "20%", checked = mod.crossHairAlpha() == 20, fn = function() mod.crossHairAlpha(20); mod.update() end }, - { title = "30%", checked = mod.crossHairAlpha() == 30, fn = function() mod.crossHairAlpha(30); mod.update() end }, - { title = "40%", checked = mod.crossHairAlpha() == 40, fn = function() mod.crossHairAlpha(40); mod.update() end }, - { title = "50%", checked = mod.crossHairAlpha() == 50, fn = function() mod.crossHairAlpha(50); mod.update() end }, - { title = "60%", checked = mod.crossHairAlpha() == 60, fn = function() mod.crossHairAlpha(60); mod.update() end }, - { title = "70%", checked = mod.crossHairAlpha() == 70, fn = function() mod.crossHairAlpha(70); mod.update() end }, - { title = "80%", checked = mod.crossHairAlpha() == 80, fn = function() mod.crossHairAlpha(80); mod.update() end }, - { title = "90%", checked = mod.crossHairAlpha() == 90, fn = function() mod.crossHairAlpha(90); mod.update() end }, - { title = "100%", checked = mod.crossHairAlpha() == 100, fn = function() mod.crossHairAlpha(100); mod.update() end }, - }}, - }}, - }}, - { title = "-", disabled = true }, - { title = string.upper(i18n("mattes")) .. ":", disabled = true }, - { title = " " .. i18n("letterbox"), menu = { - { title = i18n("enable"), checked = mod.letterboxEnabled(), fn = function() mod.letterboxEnabled:toggle(); mod.update(); end }, - }}, - { title = "-", disabled = true }, - -------------------------------------------------------------------------------- - -- - -- STILL FRAMES: - -- - -------------------------------------------------------------------------------- - { title = string.upper(i18n("stillFrames")) .. ":", disabled = true }, - { title = " " .. i18n("view"), menu = { - { title = i18n("memory") .. " 1", checked = mod.activeMemory() == 1, fn = function() mod.viewMemory(1) end, disabled = not mod.getMemory(1) }, - { title = i18n("memory") .. " 2", checked = mod.activeMemory() == 2, fn = function() mod.viewMemory(2) end, disabled = not mod.getMemory(2) }, - { title = i18n("memory") .. " 3", checked = mod.activeMemory() == 3, fn = function() mod.viewMemory(3) end, disabled = not mod.getMemory(3) }, - { title = i18n("memory") .. " 4", checked = mod.activeMemory() == 4, fn = function() mod.viewMemory(4) end, disabled = not mod.getMemory(4) }, - { title = i18n("memory") .. " 5", checked = mod.activeMemory() == 5, fn = function() mod.viewMemory(5) end, disabled = not mod.getMemory(5) }, - }}, - { title = " " .. i18n("save"), menu = { - { title = i18n("memory") .. " 1", fn = function() mod.saveMemory(1) end }, - { title = i18n("memory") .. " 2", fn = function() mod.saveMemory(2) end }, - { title = i18n("memory") .. " 3", fn = function() mod.saveMemory(3) end }, - { title = i18n("memory") .. " 4", fn = function() mod.saveMemory(4) end }, - { title = i18n("memory") .. " 5", fn = function() mod.saveMemory(5) end }, - }}, - { title = " " .. i18n("import"), menu = { - { title = i18n("memory") .. " 1", fn = function() mod.importMemory(1) end }, - { title = i18n("memory") .. " 2", fn = function() mod.importMemory(2) end }, - { title = i18n("memory") .. " 3", fn = function() mod.importMemory(3) end }, - { title = i18n("memory") .. " 4", fn = function() mod.importMemory(4) end }, - { title = i18n("memory") .. " 5", fn = function() mod.importMemory(5) end }, - }}, - { title = " " .. i18n("delete"), menu = { - { title = i18n("memory") .. " 1", fn = function() mod.deleteMemory(1) end, disabled = not mod.getMemory(1) }, - { title = i18n("memory") .. " 2", fn = function() mod.deleteMemory(2) end, disabled = not mod.getMemory(2) }, - { title = i18n("memory") .. " 3", fn = function() mod.deleteMemory(3) end, disabled = not mod.getMemory(3) }, - { title = i18n("memory") .. " 4", fn = function() mod.deleteMemory(4) end, disabled = not mod.getMemory(4) }, - { title = i18n("memory") .. " 5", fn = function() mod.deleteMemory(5) end, disabled = not mod.getMemory(5) }, - }}, - { title = " " .. i18n("appearance"), menu = { - { title = i18n("fullFrame"), checked = mod.stillsLayout() == "Full Frame", fn = function() mod.stillsLayout("Full Frame"); mod.update() end }, - { title = "-", disabled = true }, - { title = i18n("leftVertical"), checked = mod.stillsLayout() == "Left Vertical", fn = function() mod.stillsLayout("Left Vertical"); mod.update() end }, - { title = i18n("rightVertical"), checked = mod.stillsLayout() == "Right Vertical", fn = function() mod.stillsLayout("Right Vertical"); mod.update() end }, - { title = "-", disabled = true }, - { title = i18n("topHorizontal"), checked = mod.stillsLayout() == "Top Horizontal", fn = function() mod.stillsLayout("Top Horizontal"); mod.update() end }, - { title = i18n("bottomHorizontal"), checked = mod.stillsLayout() == "Bottom Horizontal", fn = function() mod.stillsLayout("Bottom Horizontal"); mod.update() end }, - }}, - { title = "-", disabled = true }, - -------------------------------------------------------------------------------- - -- - -- GRID OVERLAY: - -- - -------------------------------------------------------------------------------- - { title = string.upper(i18n("gridOverlay")) .. ":", disabled = true }, - { title = " " .. i18n("enable"), checked = mod.basicGridEnabled(), fn = function() mod.basicGridEnabled:toggle(); mod.update(); end }, - { title = " " .. i18n("appearance"), menu = { - { title = " " .. i18n("color"), menu = { - { title = i18n("black"), checked = mod.gridColor() == "#000000", fn = function() mod.setGridColor("#000000") end }, - { title = i18n("white"), checked = mod.gridColor() == "#FFFFFF", fn = function() mod.setGridColor("#FFFFFF") end }, - { title = i18n("yellow"), checked = mod.gridColor() == "#F4D03F", fn = function() mod.setGridColor("#F4D03F") end }, - { title = i18n("red"), checked = mod.gridColor() == "#FF5733", fn = function() mod.setGridColor("#FF5733") end }, - { title = "-", disabled = true }, - { title = i18n("custom"), checked = mod.gridColor() == "CUSTOM" and mod.customGridColor(), fn = mod.setCustomGridColor }, - }}, - { title = " " .. i18n("opacity"), menu = { - { title = "10%", checked = mod.gridAlpha() == 10, fn = function() mod.setGridAlpha(10) end }, - { title = "20%", checked = mod.gridAlpha() == 20, fn = function() mod.setGridAlpha(20) end }, - { title = "30%", checked = mod.gridAlpha() == 30, fn = function() mod.setGridAlpha(30) end }, - { title = "40%", checked = mod.gridAlpha() == 40, fn = function() mod.setGridAlpha(40) end }, - { title = "50%", checked = mod.gridAlpha() == 50, fn = function() mod.setGridAlpha(50) end }, - { title = "60%", checked = mod.gridAlpha() == 60, fn = function() mod.setGridAlpha(60) end }, - { title = "70%", checked = mod.gridAlpha() == 70, fn = function() mod.setGridAlpha(70) end }, - { title = "80%", checked = mod.gridAlpha() == 80, fn = function() mod.setGridAlpha(80) end }, - { title = "90%", checked = mod.gridAlpha() == 90, fn = function() mod.setGridAlpha(90) end }, - { title = "100%", checked = mod.gridAlpha() == 100, fn = function() mod.setGridAlpha(100) end }, - }}, - { title = " " .. i18n("segments"), menu = { - { title = "5", checked = mod.gridSpacing() == 5, fn = function() mod.setGridSpacing(5) end }, - { title = "10", checked = mod.gridSpacing() == 10, fn = function() mod.setGridSpacing(10) end }, - { title = "15", checked = mod.gridSpacing() == 15, fn = function() mod.setGridSpacing(15) end }, - { title = "20", checked = mod.gridSpacing() == 20, fn = function() mod.setGridSpacing(20) end }, - { title = "25", checked = mod.gridSpacing() == 25, fn = function() mod.setGridSpacing(25) end }, - { title = "30", checked = mod.gridSpacing() == 30, fn = function() mod.setGridSpacing(30) end }, - { title = "35", checked = mod.gridSpacing() == 35, fn = function() mod.setGridSpacing(35) end }, - { title = "40", checked = mod.gridSpacing() == 40, fn = function() mod.setGridSpacing(40) end }, - { title = "45", checked = mod.gridSpacing() == 45, fn = function() mod.setGridSpacing(45) end }, - { title = "50", checked = mod.gridSpacing() == 50, fn = function() mod.setGridSpacing(50) end }, - { title = "55", checked = mod.gridSpacing() == 55, fn = function() mod.setGridSpacing(55) end }, - { title = "60", checked = mod.gridSpacing() == 60, fn = function() mod.setGridSpacing(60) end }, - { title = "65", checked = mod.gridSpacing() == 65, fn = function() mod.setGridSpacing(65) end }, - { title = "70", checked = mod.gridSpacing() == 70, fn = function() mod.setGridSpacing(70) end }, - { title = "75", checked = mod.gridSpacing() == 75, fn = function() mod.setGridSpacing(70) end }, - { title = "80", checked = mod.gridSpacing() == 80, fn = function() mod.setGridSpacing(80) end }, - { title = "85", checked = mod.gridSpacing() == 85, fn = function() mod.setGridSpacing(85) end }, - { title = "90", checked = mod.gridSpacing() == 90, fn = function() mod.setGridSpacing(90) end }, - { title = "95", checked = mod.gridSpacing() == 95, fn = function() mod.setGridSpacing(95) end }, - { title = "100", checked = mod.gridSpacing() == 100, fn = function() mod.setGridSpacing(100) end }, - }}, - }}, - { title = "-", disabled = true }, - { title = i18n("reset") .. " " .. i18n("overlays"), fn = function() mod.resetOverlays() end }, - } -end - --- contextualMenu(event) -> none --- Function --- Builds the Final Cut Pro Overlay contextual menu. --- --- Parameters: --- * event - The `hs.eventtap` event --- --- Returns: --- * None -local function contextualMenu(event) - local ui = fcp.viewer:UI() - local topBar = ui and axutils.childFromTop(ui, 1) - if topBar then - local barFrame = topBar:attributeValue("AXFrame") - local location = event:location() and geometry.point(event:location()) - if barFrame and location and location:inside(geometry.rect(barFrame)) then - if mod._menu then - mod._menu:delete() - mod._menu = nil - end - mod._menu = menubar.new() - mod._menu:setMenu(generateMenu()) - mod._menu:removeFromMenuBar() - mod._menu:popupMenu(location, true) - end - end -end - -mod._lastValue = false - ---- plugins.finalcutpro.viewer.overlays.update() -> none ---- Function ---- Updates the Viewer Grid. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.update() - -------------------------------------------------------------------------------- - -- Wrap this in a timer to (maybe?) avoid notification lockups: - -------------------------------------------------------------------------------- - doAfter(0, function() - -------------------------------------------------------------------------------- - -- If Final Cut Pro is Front Most & Viewer is Showing: - -------------------------------------------------------------------------------- - if fcp.isFrontmost() - and fcp.viewer:isShowing() - and not fcp.isModalDialogOpen() - and not fcp.fullScreenWindow:isShowing() - and not fcp.commandEditor:isShowing() - and not fcp.preferencesWindow:isShowing() - then - -------------------------------------------------------------------------------- - -- Start the Mouse Watcher: - -------------------------------------------------------------------------------- - if mod.enableViewerRightClick() then - if mod._eventtap then - if not mod._eventtap:isEnabled() then - mod._eventtap:start() - end - else - mod._eventtap = eventtap.new({events.rightMouseUp}, contextualMenu) - mod._eventtap:start() - end - else - if mod._eventtap then - mod._eventtap:stop() - mod._eventtap = nil - end - end - - -------------------------------------------------------------------------------- - -- Start the Caps Lock Watcher: - -------------------------------------------------------------------------------- - if mod.capslock() then - if mod._capslockEventTap then - if not mod._capslockEventTap:isEnabled() then - mod._capslockEventTap:start() - end - else - mod._capslockEventTap = eventtap.new({events.flagsChanged}, function(event) - local keycode = event:getKeyCode() - if keycode == 57 then - mod.update() - end - end) - mod._capslockEventTap:start() - end - else - if mod._capslockEventTap then - mod._capslockEventTap:stop() - mod._capslockEventTap = nil - end - end - - -------------------------------------------------------------------------------- - -- Toggle Overall Visibility: - -------------------------------------------------------------------------------- - if areAnyOverlaysEnabled() == true then - if mod.capslock() == true then - -------------------------------------------------------------------------------- - -- Caps Lock Mode: - -------------------------------------------------------------------------------- - if capslock.get() == true then - mod.show() - else - mod.hide() - end - else - -------------------------------------------------------------------------------- - -- "Enable Overlays" Toggle: - -------------------------------------------------------------------------------- - if mod.disabled() == true then - mod.hide() - else - mod.show() - end - end - else - -------------------------------------------------------------------------------- - -- No Overlays Enabled: - -------------------------------------------------------------------------------- - mod.hide() - end - else - -------------------------------------------------------------------------------- - -- Otherwise hide the grid: - -------------------------------------------------------------------------------- - mod.hide() - - -------------------------------------------------------------------------------- - -- Destroy the Mouse Watcher: - -------------------------------------------------------------------------------- - if mod._eventtap then - mod._eventtap:stop() - mod._eventtap = nil - end - - -------------------------------------------------------------------------------- - -- Destroy the Caps Lock Watcher: - -------------------------------------------------------------------------------- - if mod._capslockEventTap then - mod._capslockEventTap:stop() - mod._capslockEventTap = nil - end - - -------------------------------------------------------------------------------- - -- Destroy the Mouse Move Letterbox Tracker: - -------------------------------------------------------------------------------- - if mod._mouseMoveLetterboxTracker then - mod._mouseMoveLetterboxTracker:stop() - mod._mouseMoveLetterboxTracker = nil - end - - -------------------------------------------------------------------------------- - -- Destroy any Mouse Move Trackers: - -------------------------------------------------------------------------------- - if mod._mouseMoveTracker then - for i, _ in pairs(mod._mouseMoveTracker) do - mod._mouseMoveTracker[i]:stop() - mod._mouseMoveTracker[i] = nil - end - end - end - end) -end - ---- plugins.finalcutpro.viewer.overlays.getStillsFolderPath() -> string | nil ---- Function ---- Gets the stills folder path. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * The stills folder path as a string or `nil` if an error occurs. -function mod.getStillsFolderPath() - local userConfigRootPath = config.userConfigRootPath - if userConfigRootPath then - if not tools.doesDirectoryExist(userConfigRootPath) then - fs.mkdir(userConfigRootPath) - end - local path = userConfigRootPath .. "/" .. STILLS_FOLDER .. "/" - if not tools.doesDirectoryExist(path) then - fs.mkdir(path) - end - return tools.doesDirectoryExist(path) and path - else - return nil - end -end - ---- plugins.finalcutpro.viewer.overlays.deleteMemory() -> none ---- Function ---- Deletes a memory. ---- ---- Parameters: ---- * id - An identifier in the form of a number. ---- ---- Returns: ---- * None -function mod.deleteMemory(id) - local path = mod.getStillsFolderPath() - if path then - local imagePath = path .. "/memory" .. id .. ".png" - if tools.doesFileExist(imagePath) then - os.remove(imagePath) - local activeMemory = mod.activeMemory() - if activeMemory == id then - mod.activeMemory(0) - mod.update() - end - end - end -end - ---- plugins.finalcutpro.viewer.overlays.saveMemory() -> none ---- Function ---- Saves a still frame to file. ---- ---- Parameters: ---- * id - An identifier in the form of a number. ---- ---- Returns: ---- * None -function mod.saveMemory(id) - local viewer = fcp.viewer:contentsUI() - local result = false - if viewer then - local path = mod.getStillsFolderPath() - if path then - local snapshot = axutils.snapshot(viewer, path .. "/memory" .. id .. ".png") - if snapshot then - result = true - end - else - log.ef("Could not create Cache Folder.") - end - else - log.ef("Could not find Viewer.") - end - if not result then - dialog.displayErrorMessage("Could not save still frame.") - end -end - ---- plugins.finalcutpro.viewer.overlays.importMemory() -> none ---- Function ---- Import a file to memory. ---- ---- Parameters: ---- * id - An identifier in the form of a number. ---- ---- Returns: ---- * None -function mod.importMemory(id) - local disabled = mod.disabled() - mod.disabled(true) - mod.update() - local allowedImageType = {"PDF", "com.adobe.pdf", "BMP", "com.microsoft.bmp", "JPEG", "JPEG2", "jpg", "public.jpeg", "PICT", "com.apple.pict", "PNG", "public.png", "PSD", "com.adobe.photoshop-image", "TIFF", "public.tiff"} - local path = cpDialog.displayChooseFile("Please select a file to import", allowedImageType) - local stillsFolderPath = mod.getStillsFolderPath() - if path and stillsFolderPath then - local importedImage = image.imageFromPath(path) - if importedImage then - importedImage:saveToFile(stillsFolderPath .. "/memory" .. id .. ".png") - local activeMemory = mod.activeMemory() - if activeMemory == id then - mod.activeMemory(0) - end - end - end - mod.disabled(disabled) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.getMemory(id) -> image | nil ---- Function ---- Gets an image from memory. ---- ---- Parameters: ---- * id - The ID of the memory you want to retrieve. ---- ---- Returns: ---- * The memory as a `hs.image` or `nil` if the memory could not be retrieved. -function mod.getMemory(id) - local path = mod.getStillsFolderPath() - if path then - local imagePath = path .. "/memory" .. id .. ".png" - if tools.doesFileExist(imagePath) then - local result = image.imageFromPath(imagePath) - if result then - return result - end - end - end - return nil -end - ---- plugins.finalcutpro.viewer.overlays.viewMemory(id) -> none ---- Function ---- View a memory. ---- ---- Parameters: ---- * id - The ID of the memory you want to retrieve. ---- ---- Returns: ---- * None -function mod.viewMemory(id) - local activeMemory = mod.activeMemory() - if activeMemory == id then - mod.activeMemory(0) - else - local result = mod.getMemory(id) - if result then - mod.activeMemory(id) - end - end - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.setGridSpacing(value) -> none ---- Function ---- Sets Grid Spacing. ---- ---- Parameters: ---- * value - The value you want to set. ---- ---- Returns: ---- * None -function mod.setGridSpacing(value) - mod.gridSpacing(value) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.setGridAlpha(value) -> none ---- Function ---- Sets Grid Alpha. ---- ---- Parameters: ---- * value - The value you want to set. ---- ---- Returns: ---- * None -function mod.setGridAlpha(value) - mod.gridAlpha(value) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.setGridColor(value) -> none ---- Function ---- Sets Grid Color. ---- ---- Parameters: ---- * value - The value you want to set. ---- ---- Returns: ---- * None -function mod.setGridColor(value) - mod.gridColor(value) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.setCustomGridColor() -> none ---- Function ---- Pops up a Color Dialog box allowing the user to select a custom colour for grid lines. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.setCustomGridColor() - dialog.color.continuous(false) - dialog.color.callback(function(color, closed) - if closed then - mod.gridColor("CUSTOM") - mod.customGridColor(color) - mod.update() - fcp:launch() - end - end) - dialog.color.show() - hs.focus() -end - ---- plugins.finalcutpro.viewer.overlays.setCustomCrossHairColor() -> none ---- Function ---- Pops up a Color Dialog box allowing the user to select a custom colour for cross hairs. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.setCustomCrossHairColor() - dialog.color.continuous(false) - dialog.color.callback(function(color, closed) - if closed then - mod.crossHairColor("CUSTOM") - mod.customCrossHairColor(color) - mod.update() - fcp:launch() - end - end) - dialog.color.show() - hs.focus() -end - ---- plugins.finalcutpro.viewer.overlays.getGuidePosition() -> none ---- Function ---- Get Guide Position. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.setGuidePosition(id, value) - id = tostring(id) - local guidePosition = mod.guidePosition() - guidePosition[id] = value - mod.guidePosition(guidePosition) -end - ---- plugins.finalcutpro.viewer.overlays.getGuidePosition() -> none ---- Function ---- Get Guide Position. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.getGuidePosition(id) - id = tostring(id) - local guidePosition = mod.guidePosition() - return guidePosition and guidePosition[id] or {} -end - ---- plugins.finalcutpro.viewer.overlays.getGuideAlpha() -> none ---- Function ---- Get Guide Alpha. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.getGuideAlpha(id) - id = tostring(id) - local guideAlpha = mod.guideAlpha() - return guideAlpha and guideAlpha[id] or DEFAULT_ALPHA -end - ---- plugins.finalcutpro.viewer.overlays.getGuideColor(id) -> none ---- Function ---- Get Guide Color. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.getGuideColor(id) - id = tostring(id) - local guideColor = mod.guideColor() - return guideColor and guideColor[id] or DEFAULT_COLOR -end - ---- plugins.finalcutpro.viewer.overlays.getCustomGuideColor(id) -> none ---- Function ---- Get Custom Guide Color. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.getCustomGuideColor(id) - id = tostring(id) - local customGuideColor = mod.customGuideColor() - return customGuideColor and customGuideColor[id] -end - ---- plugins.finalcutpro.viewer.overlays.setGuideAlpha(value) -> none ---- Function ---- Sets Guide Alpha. ---- ---- Parameters: ---- * id - The ID of the guide. ---- * value - The value you want to set. ---- ---- Returns: ---- * None -function mod.setGuideAlpha(id, value) - id = tostring(id) - local guideAlpha = mod.guideAlpha() - guideAlpha[id] = value - mod.guideAlpha(guideAlpha) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.setGuideColor(value) -> none ---- Function ---- Sets Guide Color. ---- ---- Parameters: ---- * id - The ID of the guide. ---- * value - The value you want to set. ---- ---- Returns: ---- * None -function mod.setGuideColor(id, value) - id = tostring(id) - local guideColor = mod.guideColor() - guideColor[id] = value - mod.guideColor(guideColor) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.setCustomGuideColor() -> none ---- Function ---- Pops up a Color Dialog box allowing the user to select a custom colour for guide lines. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.setCustomGuideColor(id) - id = tostring(id) - dialog.color.continuous(false) - dialog.color.callback(function(color, closed) - if color and closed then - local guideColor = mod.guideColor() - guideColor[id] = "CUSTOM" - mod.guideColor(guideColor) - - local customGuideColor = mod.customGuideColor() - customGuideColor[id] = color - mod.customGuideColor(customGuideColor) - - mod.update() - fcp:launch() - end - end) - dialog.color.show() - hs.focus() -end - ---- plugins.finalcutpro.viewer.overlays.enableAllDraggableGuides() -> none ---- Function ---- Enable all draggable guides. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.enableAllDraggableGuides() - local draggableGuideEnabled = mod.draggableGuideEnabled() - for id=1, 5 do - draggableGuideEnabled[tostring(id)] = true - end - mod.draggableGuideEnabled(draggableGuideEnabled) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.disableAllDraggableGuides() -> none ---- Function ---- Disable all draggable guides. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.disableAllDraggableGuides() - local draggableGuideEnabled = mod.draggableGuideEnabled() - for id=1, 5 do - draggableGuideEnabled[tostring(id)] = false - end - mod.draggableGuideEnabled(draggableGuideEnabled) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.getDraggableGuideEnabled(id) -> none ---- Function ---- Get Guide Enabled. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.getDraggableGuideEnabled(id) - id = tostring(id) - local draggableGuideEnabled = mod.draggableGuideEnabled() - return draggableGuideEnabled and draggableGuideEnabled[id] and draggableGuideEnabled[id] == true -end - ---- plugins.finalcutpro.viewer.overlays.toggleDraggableGuide(id) -> none ---- Function ---- Toggle Guide Enabled. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.toggleDraggableGuide(id) - id = tostring(id) - local draggableGuideEnabled = mod.draggableGuideEnabled() - if draggableGuideEnabled[id] and draggableGuideEnabled[id] == true then - draggableGuideEnabled[id] = false - else - draggableGuideEnabled[id] = true - end - mod.draggableGuideEnabled(draggableGuideEnabled) - mod.update() -end - ---- plugins.finalcutpro.viewer.overlays.resetDraggableGuide(id) -> none ---- Function ---- Reset a specific Draggable Guide. ---- ---- Parameters: ---- * id - The ID of the guide. ---- ---- Returns: ---- * None -function mod.resetDraggableGuide(id) - - id = tostring(id) - - -------------------------------------------------------------------------------- - -- Reset Color: - -------------------------------------------------------------------------------- - local guideColor = mod.guideColor() - guideColor[id] = nil - mod.guideColor(guideColor) - - -------------------------------------------------------------------------------- - -- Reset Alpha: - -------------------------------------------------------------------------------- - local guideAlpha = mod.guideAlpha() - guideAlpha[id] = nil - mod.guideAlpha(guideAlpha) - - -------------------------------------------------------------------------------- - -- Reset Position: - -------------------------------------------------------------------------------- - local guidePosition = mod.guidePosition() - guidePosition[id] = nil - mod.guidePosition(guidePosition) - - mod.update() - -end - ---- plugins.finalcutpro.viewer.overlays.resetOverlays() -> none ---- Function ---- Resets all overlays to their default values. ---- ---- Parameters: ---- * None ---- ---- Returns: ---- * None -function mod.resetOverlays() - mod.crossHairEnabled(false) - mod.letterboxEnabled(false) - mod.letterboxHeight(DEFAULT_LETTERBOX_HEIGHT) - mod.basicGridEnabled(false) - mod.crossHairColor(DEFAULT_COLOR) - mod.crossHairAlpha(DEFAULT_ALPHA) - mod.gridColor(DEFAULT_COLOR) - mod.gridAlpha(DEFAULT_ALPHA) - mod.customGridColor(nil) - mod.gridSpacing(DEFAULT_GRID_SPACING) - mod.activeMemory(0) - mod.stillsLayout(DEFAULT_STILLS_LAYOUT) - mod.draggableGuideEnabled({}) - mod.guidePosition({}) - mod.guideColor({}) - mod.guideAlpha({}) - mod.customGuideColor({}) - mod.customCrossHairColor({}) - mod.update() -end - --- updater -> cp.deferred --- Variable --- A deferred timer that triggers the update function. -local updater = deferred.new(0.1):action(mod.update) - --- deferredUpdate -> none --- Function --- Triggers the update function. --- --- Parameters: --- * None --- --- Returns: --- * None -local function deferredUpdate() - updater() -end - -local plugin = { - id = "finalcutpro.viewer.overlays", - group = "finalcutpro", - dependencies = { - ["finalcutpro.commands"] = "fcpxCmds", - ["finalcutpro.menu.manager"] = "menu", - } -} - -function plugin.init(deps) - -------------------------------------------------------------------------------- - -- Only load plugin if Final Cut Pro is supported: - -------------------------------------------------------------------------------- - if not fcp:isSupported() then return end - - -------------------------------------------------------------------------------- - -- Setup the system menu: - -------------------------------------------------------------------------------- - deps.menu.viewer - :addMenu(10001, function() return i18n("overlay") end) - :addItems(999, function() - return { - { title = i18n("enableViewerContextualMenu"), fn = function() mod.enableViewerRightClick:toggle(); mod.update() end, checked = mod.enableViewerRightClick() }, - { title = "-", disabled = true }, - } - end) - :addItems(1000, generateMenu) - - -------------------------------------------------------------------------------- - -- Update Canvas when Final Cut Pro is shown/hidden: - -------------------------------------------------------------------------------- - fcp.isFrontmost:watch(deferredUpdate) - fcp.isModalDialogOpen:watch(deferredUpdate) - fcp.fullScreenWindow.isShowing:watch(deferredUpdate) - fcp.commandEditor.isShowing:watch(deferredUpdate) - fcp.preferencesWindow.isShowing:watch(deferredUpdate) - - -------------------------------------------------------------------------------- - -- Update Canvas one second after going full-screen: - -------------------------------------------------------------------------------- - fcp.primaryWindow.isFullScreen:watch(function() doAfter(1, mod.update) end) - - -------------------------------------------------------------------------------- - -- Update Canvas when Final Cut Pro's Viewer is resized or moved: - -------------------------------------------------------------------------------- - fcp.viewer.frame:watch(function(value) - if value then - deferredUpdate() - end - end) - - -------------------------------------------------------------------------------- - -- Setup Commands: - -------------------------------------------------------------------------------- - if deps.fcpxCmds then - deps.fcpxCmds - :add("cpViewerBasicGrid") - :whenActivated(function() mod.basicGridEnabled:toggle(); mod.update() end) - - for i=1, mod.NUMBER_OF_DRAGGABLE_GUIDES do - deps.fcpxCmds - :add("cpViewerDraggableGuide" .. i) - :whenActivated(function() mod.toggleDraggableGuide(i); mod.update() end) - :titled(i18n("cpViewerDraggableGuide_title") .. " " .. i) - end - - deps.fcpxCmds - :add("cpToggleAllViewerOverlays") - :whenActivated(function() mod.disabled:toggle(); mod.update() end) - - for i=1, mod.NUMBER_OF_MEMORIES do - deps.fcpxCmds - :add("cpSaveStillsFrame" .. i) - :whenActivated(function() mod.saveMemory(i) end) - :titled(i18n("saveCurrentFrameToStillsMemory") .. " " .. i) - - deps.fcpxCmds - :add("cpViewStillsFrame" .. i) - :whenActivated(function() mod.viewMemory(i) end) - :titled(i18n("viewStillsMemory") .. " " .. i) - end - end - - return mod -end - -function plugin.postInit() - -------------------------------------------------------------------------------- - -- Update the Canvas on initial boot: - -------------------------------------------------------------------------------- - if mod.update then - mod.update() - end -end - -return plugin diff --git a/src/plugins/finalcutpro/viewer/overlays/Bar.lua b/src/plugins/finalcutpro/viewer/overlays/Bar.lua new file mode 100644 index 000000000..5cb27ceb6 --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/Bar.lua @@ -0,0 +1,111 @@ +-- === finalcutpro.viewer.overlays.Bar === +-- +-- Represents a bar on a `DraggableGuide`. + +local require = require + +local canvas = require "hs.canvas" + +local prop = require "cp.prop" + +local class = require "middleclass" + +local Bar = class("finalcutpro.viewer.overlays.Bar") + +local FILLER_ID = "filler" + +-- finalcutpro.viewer.overlays.Bar(frame, color, isEnabled, isActive) +-- Constructor +-- Initializes a `Bar` with the provided `frame` and `color` `cp.prop` values. +-- +-- Parameters: +-- * frame: The frame for the bar. +-- * color: The `cp.drawing.color` to fill the bar with. +-- * isEnabled: If `true`, the bar will be loaded, ready to show. +-- * isActive: If `true`, the bar will be visible. +function Bar:initialize(frame, color, isEnabled, isActive) + prop.bind(self) { + frame = frame, + color = color, + isEnabled = isEnabled, + isActive = isActive, + } + + self.isEnabled:watch(function(enabled) + if not enabled then + self:_killOverlay() + end + end, true) + + self.isActive:watch(function(active) + self:update() + end, true) + + self.frame:watch(function(frame) + local overlay = self:overlay() + if overlay then + overlay:frame(frame) + end + self:update() + end, true) + + self.color:watch(function(color) + local overlay = self:overlay() + if self._overlay then + local filler = self._overlay[FILLER_ID] + filler.fillColor = color + filler.strokeColor = color + end + self:update() + end, true) +end + +function Bar:overlay() + if self._overlay then + return self._overlay + end + + local frame = self:frame() + local color = self:color() + + if not frame or not color then + return nil + end + + self._overlay = canvas.new(frame) + :level(canvas.windowLevels.status + 5) + :appendElements({ + id = FILLER_ID, + type = "rectangle", + strokeColor = color, + fillColor = color, + }) + + return self._overlay +end + +function Bar:update() + local overlay = self:overlay() + if not overlay then + return + end + + if self:isActive() then + overlay:show() + else + overlay:hide() + end +end + +function Bar:_killOverlay() + if self._overlay then + self._overlay:delete() + self._overlay = nil + end +end + +function Bar:__gc() + self:_killOverlay() +end + +return Bar \ No newline at end of file diff --git a/src/plugins/finalcutpro/viewer/overlays/Dot.lua b/src/plugins/finalcutpro/viewer/overlays/Dot.lua new file mode 100644 index 000000000..558550b3e --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/Dot.lua @@ -0,0 +1,181 @@ +-- === finalcutpro.viewer.overlays.Dot === +-- +-- Represents a target dot on a `DraggableGuide`. + +local require = require + +-- local log = require "hs.logger" .new "Dot" +-- local inspect = require "hs.inspect" + +local deferred = require "cp.deferred" +local prop = require "cp.prop" + +local canvas = require "hs.canvas" +local eventtap = require "hs.eventtap" +local timer = require "hs.timer" + +local class = require "middleclass" + +local secondsSinceEpoch = timer.secondsSinceEpoch +local eventTypes = eventtap.event.types + +local Dot = class("finalcutpro.viewer.overlays.Dot") + +local DOT_ID = "dot" + +-- finalcutpro.viewer.overlays.Dot(frame, target, color, isEnabled, isActive) +-- Constructor +-- Initializes a `Dot` with the provided `frame` and `color` `cp.prop` values. +-- +-- Parameters: +-- * frame: The frame for the dot. +-- * target: The actual target, which may be different to the center of the frame. +-- * color: The `cp.drawing.color` to fill the bar with. +-- * isEnabled: If `true`, the bar will be loaded, ready to show. +-- * isActive: If `true`, the bar will be visible. +function Dot:initialize(frame, target, color, isEnabled, isActive) + prop.bind(self) { + frame = frame, + target = target, + color = color, + isEnabled = isEnabled, + isActive = isActive, + } + + self.isEnabled:watch(function(enabled) + if not enabled then + self:_killOverlay() + end + end, true) + + self.isActive:watch(function(active) + self:update() + end, true) + + self.frame:watch(function(frame) + local overlay = self:overlay() + if overlay and frame then + overlay:frame(frame) + end + self:update() + end, true) + + self.target:watch(function(target) + self:update() + end) + + self.color:watch(function(color) + local overlay = self:overlay() + if overlay and color then + local filler = self._overlay[DOT_ID] + filler.fillColor = color + filler.strokeColor = color + self:update() + end + end, true) +end + + +function Dot:overlay() + if self._overlay then + return self._overlay + end + + local frame = self:frame() + -- local target = self:target() + local color = self:color() + + if not frame or not color then + return nil + end + + -- log.df("isEnabled: creating overlay, targetting %s", inspect(target)) + self._overlay = canvas.new(frame) + :level(canvas.windowLevels.status + 10) + :appendElements( + { + id = DOT_ID, + type = "rectangle", + strokeColor = self:color(), + fillColor = self:color(), + } + ) + :clickActivating(false) + :canvasMouseEvents(true, true, false, false) + :mouseCallback(function(canvas, event, id, x, y) + self:_onMouseEvent(event) + end) + + return self._overlay +end + +function Dot:update() + local isActive = self:isActive() + local overlay = self:overlay() + if not overlay then + return + end + + if isActive then + -- log.df("update: showing the overlay...") + overlay:show() + else + -- log.df("update: hiding the overlay...") + overlay:hide() + end +end + +function Dot:_onMouseEvent(event) + -- log.df("_onMouseEvent: %s", inspect(event)) + if event == "mouseUp" then + local prevClick = self._previousClick + local now = secondsSinceEpoch() + if prevClick and now - prevClick <= eventtap.doubleClickInterval() then + self._previousClick = nil + self.target:set(nil) + else + self._previousClick = now + end + elseif event == "mouseDown" then + local location + local updateTarget = deferred.new(0.01):action(function() + self:target(location) + end) + + self:_killMouseTracker() + + self._mouseMoveTracker = eventtap.new( + { eventTypes.leftMouseDragged, eventTypes.leftMouseUp }, + function(e) + if e:getType() == eventTypes.leftMouseUp and self._mouseMoveTracker then + self:_killMouseTracker() + else + location = e:location() + updateTarget() + end + end, + false + ):start() + end +end + +function Dot:_killOverlay() + if self._overlay then + self._overlay:delete() + self._overlay = nil + end +end + +function Dot:_killMouseTracker() + if self._mouseMoveTracker then + self._mouseMoveTracker:stop() + self._mouseMoveTracker = nil + end +end + +function Dot:__gc() + self:_killOverlay() + self:_killMouseTracker() +end + +return Dot \ No newline at end of file diff --git a/src/plugins/finalcutpro/viewer/overlays/DraggableGuide.lua b/src/plugins/finalcutpro/viewer/overlays/DraggableGuide.lua new file mode 100644 index 000000000..0252fed4e --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/DraggableGuide.lua @@ -0,0 +1,311 @@ +-- === plugins.finalcutpro.viewer.overlays.DraggableGuide === +-- +-- A draggable guide, visible over the main Viewer. + +local require = require + +local log = require "hs.logger" .new "DraggableGuide" +local inspect = require "hs.inspect" + +local Bar = require "Bar" +local Dot = require "Dot" + +local config = require "cp.config" +local fcp = require "cp.apple.finalcutpro" + +local color = require "hs.drawing.color" +local geometry = require "hs.geometry" + +local class = require "middleclass" +local lazy = require "cp.lazy" + +local format = string.format +local min, max = math.min, math.max + +local DraggableGuide = class("finalcutpro.viewer.overlays.DraggableGuide"):include(lazy) + +-- DEFAULT_COLOR -> string +-- Constant +-- Default Colour Setting. +local DEFAULT_COLOR = color.asRGB({ hex = "#FFFFFF" }) + +local BAR_THICKNESS = 2 +local DOT_RADIUS = 6 + +local CONFIG_PREFIX = "finalcutpro.viewer.overlays.DraggableGuide" + +local function logInspect(label, value) + -- log.df("%s: %s", label, inspect(value)) + return value +end + +local function named(id, name) + return format("%s.%s.%s", CONFIG_PREFIX, id, name) +end + +-- creates a `cp.prop` which wraps the passed `cp.prop` containing an `{x,y,w,h}` table to and from a ` hs.geometry.rect`. +local function asRect(frameProp) + return frameProp:mutate( + function(original) + local value = original() + return value and geometry.rect(value) or nil + end, + function(value, original) + if value and value.table then + -- it's a geometry rect. + original:set(value.table) + else + original:set(value) + end + end + ) +end + +-- creates a `cp.prop` which wraps the passed `cp.prop` containing an `{x,y,w,h}` table to and from a ` hs.geometry.rect`. +local function asPoint(pointProp) + return pointProp:mutate( + function(original) + local value = original() + return value and geometry.point(value) or nil + end, + function(value, original) + if value and value.table then + original:set(value.table) + else + original:set(value) + end + end + ) +end + +-- determines if FCP is available to have the guides overlaid on. +local fcpAvailable = fcp.isFrontmost + :AND(fcp.isModalDialogOpen:NOT()) + :AND(fcp.fullScreenWindow.isShowing:NOT()) + :AND(fcp.preferencesWindow.isShowing:NOT()) + +--- plugins.finalcutpro.viewer.DraggableGuide.active +--- Variable +--- If set to `true`, any enabled `DraggableGuide`s will be visible when FCPX is available. +DraggableGuide.static.active = config.prop(named("global", "active")) + +-- plugins.finalcutpro.viewer.DraggableGuide(viewer, id) -> DraggableGuide +-- Constructor +-- Constructs a new `DraggableGuide` on the nominated `Viewer` with the specified id. +-- +-- Parameter: +-- * viewer - The `cp.apple.finalcutpro.viewer.Viewer` the guide is attached to. +-- * id - The id of the guide (eg `1`.) +-- +-- Returns: +-- * The new `DraggableGuide`. +function DraggableGuide:initialize(viewer, id) + self.viewer = viewer + self.id = id + + self.verticalBar = Bar(self._verticalBarFrame, self.colorWithAlpha, self.isEnabled, self.isActive) + self.horizontalBar = Bar(self._horizontalBarFrame, self.colorWithAlpha, self.isEnabled, self.isActive) + self.dot = Dot(self._dotFrame, self.target, self.colorWithAlpha, self.isEnabled, self.isActive) + + self.isActive:update() +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.frame +-- Field +-- The outer frame of the guide. +function DraggableGuide.lazy.prop:frame() + return asRect(self.viewer.videoImage.frame) +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.isActive +-- Field +-- Indicates if the guide should be shown on-screen. +function DraggableGuide.lazy.prop:isActive() + return self.isEnabled:AND(fcpAvailable):AND(self.viewer.isShowing) +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.isEnabled +-- Field +-- If `true`, the guide is enabled. It may not be visible however, since that depends on whether the Viewer is visible and not obscured. +function DraggableGuide.lazy.prop:isEnabled() + return config.prop(named(self.id, "enabled"), false) +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.position +-- Field +-- A `table` containing an `x` and `y` value representing the position of the guide as a percentage of the `Viewer` frame width and height. +-- +-- For example: `guide:position({ x = 0.5, y = 0.5 })` puts the guide in the center of the Viewer. +function DraggableGuide.lazy.prop:position() + return asPoint(config.prop(named(self.id, "position"), { x = 0.5, y = 0.5 })) +end + +-- clamp(value, minValue, maxValue) -> number +-- Function +-- Returns the value, clamped betwen the provide min and max values. +-- +-- Parameters: +-- * value - The original value. +-- * minValue - The minimum allowed value. +-- * maxValue - The maximum allowed value. +-- +-- Returns: +-- * The clamped value. +local function clamp(value, minValue, maxValue) + return max(min(value, maxValue), minValue) +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.target +-- Field +-- The current position of the center of the guide in absolute x/y coordinates. +function DraggableGuide.lazy.prop:target() + return self.position:mutate( + function(original) + local position = original() + local frame = self:frame() + + return logInspect("target", position and frame and position:fromUnitRect(self:frame())) + end, + function(value, original) + if not value then + original:set(nil) + return + end + + local frame = self:frame() + if not frame then + original:set(nil) + return + end + + local position = { + x = clamp((value.x - frame.x)/frame.w, 0, 1), + y = clamp((value.y - frame.y)/frame.h, 0, 1) + } + original:set(position) + end + ) + :monitor(self.frame) +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.color +-- Field +-- The color of the guide as a hex string (eg. "#FFFFFF"). +function DraggableGuide.lazy.prop:color() + return config.prop(named(self.id, "color"), DEFAULT_COLOR):mutate( + function(original) + local value = original() + if type(value) == "string" then + value = { hex = value } + end + return value and color.asRGB(value) + end, + function(value, original) + if type(value) == "string" then + value = { hex = value } + end + if type(value) == "table" then + value = color.asRGB(value) + original({ red = value.red, green = value.green, blue = value.blue }) -- ignoring alpha + else + original(nil) + end + end + ) +end + +-- plugins.finalcutpro.viewer.overlays.DraggableGuide.alpha +-- Field +-- The transparancy value of the guide, a percentage from `0.0` to `1.0`. +function DraggableGuide.lazy.prop:alpha() + return config.prop(named(self.id, "alpha"), 1.0) +end + +function DraggableGuide.lazy.prop:colorWithAlpha() + return self.color:mutate(function(original) + local color = original() + color.alpha = self:alpha() + return color + end) + :monitor(self.alpha) +end + +-- plugins.finalcutpro.viewer.overlay.DraggableGuide:reset() +-- Method +-- Resets the position and color properties of the guide. +function DraggableGuide:reset() + self.position:set(nil) + self.alpha:set(nil) + self.color:set(nil) + self.customColor:set(nil) +end + +function DraggableGuide:update() + +end + +-- plugins.finalcutpro.viewer.overlay.DraggableGuide._verticalBarFrame +-- Field +-- The frame for the vertical bar. +function DraggableGuide.lazy.prop:_verticalBarFrame() + return self.target:mutate(function(original) + local target = original() + local frame = self:frame() + if not target or not frame then + return nil + end + + return logInspect("_verticalBarFrame", + geometry.rect { x = target.x - BAR_THICKNESS/2, y = frame.y, w = BAR_THICKNESS, h = frame.h } + :intersect(frame) + ) + end) + :monitor(self.frame) +end + +-- plugins.finalcutpro.viewer.overlay.DraggableGuide._horizontalBarFrame +-- Field +-- The frame for the horizontal bar. +function DraggableGuide.lazy.prop:_horizontalBarFrame() + return self.target:mutate(function(original) + local target = original() + local frame = self:frame() + if not target or not frame then + return nil + end + + return logInspect("_horizontalBarFrame", + geometry.rect { x = frame.x, y = target.y - BAR_THICKNESS/2, w = frame.w, h = BAR_THICKNESS } + :intersect(frame) + ) + end) + :monitor(self.frame) +end + +-- plugins.finalcutpro.viewer.overlay.DraggableGuide._dotFrame +-- Field +-- The frame for the dot. +function DraggableGuide.lazy.prop:_dotFrame() + return self.target:mutate(function(original) + local target = original() + local frame = self:frame() + if not target or not frame then + log.df("_dotFrame: no target or viewer frame available") + return nil + end + + local dotFrame = geometry.rect { + x = target.x - DOT_RADIUS, + y = target.y - DOT_RADIUS, + w = DOT_RADIUS*2, + h = DOT_RADIUS*2 + } + + return logInspect("_dotFrame", + dotFrame:intersect(frame) + ) + end) + :monitor(self.frame) +end + +return DraggableGuide \ No newline at end of file diff --git a/src/plugins/finalcutpro/viewer/overlays/GridLayer.lua b/src/plugins/finalcutpro/viewer/overlays/GridLayer.lua new file mode 100644 index 000000000..48353b72b --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/GridLayer.lua @@ -0,0 +1,80 @@ +local require = require +local AudioConfiguration = require "AudioConfiguration" +local SearchField = require "SearchField" + +local menus = require "menus" +local OverlayLayer = require "OverlayLayer" + +local config = require "cp.config" +local i18n = require "cp.i18n" + +local insert = table.insert + +local GridLayer = OverlayLayer:subclass("finalcutpro.viewer.overlays.Overlay") + +GridLayer.static.sizes = { 2, 3, 4, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 } + +function GridLayer:initialize(overlay) + OverlayLayer.initialize(self, overlay) +end + +function GridLayer.lazy.prop:isEnabled() + config.prop("finalcutpro.viewer.overlays.GridLayer.enabled") + :watch(function() self.overlay:update() end) +end + +function GridLayer.lazy.prop:color() + return config.prop("finalcutpro.viewer.overlays.GridLayer.color") + :watch(function() self.overlay:update() end) +end + +function GridLayer.lazy.prop:alpha() + return config.prop("finalcutpro.viewer.overlays.GridLayer.alpha") +end + +function GridLayer.lazy.prop:spacing() + return config.prop("finalcutpro.viewer.overlays.GridLayer.spacing") +end + +local function generateSpacingMenu(title, spacingProp) + local currentSpacing = spacingProp() + local menu = {} + + for i = 5,100,5 do + insert(menu, { + title = tostring(i), + checked = currentSpacing == i, + fn = function() spacingProp(i) end + }) + end + + return { title = title, menu = menu } +end + +function GridLayer:generateAppearanceMenu(menu) + -- colors + menu:addItem(menus.generateColorMenu(" ".. i18n("color"), self.color)) + menu:addItem(menus.generateAlphaMenu(" "..i18n("opacity"), self.alpha)) + menu:addItem(generateSpacingMenu(" "..i18n("segments"), self.spacing)) + + return menu +end + +function GridLayer:buildMenu(section) + + section:addHeading(i18n("gridOverlay")) + + section:addItem(function() + return { title = " " .. i18n("enable"), checked = self:isEnabled(), fn = function() self.isEnabled:toggle() end } + end) + + self:generateAppearanceMenu(section:addMenu(" "..i18n("appearance"))) + + section:addSeparator() + + section:addItem(function() + return { title = i18n("reset") .. " " .. i18n("overlays"), fn = function() self:resetOverlays() end } + end) +end + +return GridLayer \ No newline at end of file diff --git a/src/plugins/finalcutpro/viewer/overlays/Overlay.lua b/src/plugins/finalcutpro/viewer/overlays/Overlay.lua new file mode 100644 index 000000000..0eaa1254a --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/Overlay.lua @@ -0,0 +1,10 @@ +local require = require + +local class = require "metaclass" +local lazy = require "cp.lazy" + +local Overlay = class("finalcutpro.viewer.overlays.Overlay"):include(lazy) + + + +return Overlay \ No newline at end of file diff --git a/src/plugins/finalcutpro/viewer/overlays/OverlayLayer.lua b/src/plugins/finalcutpro/viewer/overlays/OverlayLayer.lua new file mode 100644 index 000000000..355caf8aa --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/OverlayLayer.lua @@ -0,0 +1,44 @@ +local require = require + +local class = require "metaclass" +local lazy = require "cp.lazy" + +local OverlayLayer = class("finalcutpro.viewer.overlays.OverlayLayer"):include(lazy) + +-- finalcutpro.viewer.overlays.OverlayLayer(overlay) -> OverlayLayer +-- Constructor +-- Should not be called directly, but called by implementing classes. +-- +-- Parameters: +-- * overlay - the `Overlay` the layer belongs to. +-- +-- Returns: +-- * The initialized `OverlayLayer`. +function OverlayLayer:initialize(overlay) + self.overlay = overlay +end + +-- finalcutpro.viewer.overlays.OverlayLayer:buildMenu(section) +-- Method +-- Passed a `core.menu.manager.section` into which any addtional menu +-- items/sections/menus should be added into to allow configuration of the layer. +-- +-- Parameters: +-- * section - The `core.menu.manager.section`. +function OverlayLayer.buildMenu(_) +end + +-- finalcutpro.viewer.overlays.OverlayLayer:drawOn(overlay) +-- Method +-- Draws on the provided `overlay` `hs.canvas` instance. +-- +-- Parameters: +-- * overlay - The `hs.canvas` to draw on. +-- +-- Returns: +-- * None +function OverlayLayer.drawOn(_) + error("Unimplemented `drawOn` method.") +end + +return OverlayLayer \ No newline at end of file diff --git a/src/plugins/finalcutpro/viewer/overlays/init.lua b/src/plugins/finalcutpro/viewer/overlays/init.lua new file mode 100644 index 000000000..e391ba7b9 --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/init.lua @@ -0,0 +1,1350 @@ +--- === plugins.finalcutpro.viewer.overlays === +--- +--- Final Cut Pro Viewer Overlays. + +local require = require + +local log = require "hs.logger".new "overlays" + +local hs = _G.hs + +local DraggableGuide = require "DraggableGuide" +local menus = require "menus" + +local canvas = require "hs.canvas" +local dialog = require "hs.dialog" +local eventtap = require "hs.eventtap" +local fs = require "hs.fs" +local geometry = require "hs.geometry" +local hid = require "hs.hid" +local image = require "hs.image" +local menubar = require "hs.menubar" +local mouse = require "hs.mouse" +local timer = require "hs.timer" + +local axutils = require "cp.ui.axutils" +local config = require "cp.config" +local cpDialog = require "cp.dialog" +local deferred = require "cp.deferred" +local fcp = require "cp.apple.finalcutpro" +local i18n = require "cp.i18n" +local tools = require "cp.tools" + +local Do = require "cp.rx.go.Do" + +local capslock = hid.capslock +local doAfter = timer.doAfter +local events = eventtap.event.types +local insert = table.insert +local format = string.format + +local mod = {} + + +--- plugins.finalcutpro.viewer.overlays.update() -> none +--- Function +--- Updates the Viewer Grid. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.update() + -------------------------------------------------------------------------------- + -- Wrap this in a timer to (maybe?) avoid notification lockups: + -------------------------------------------------------------------------------- + doAfter(0, function() + -------------------------------------------------------------------------------- + -- If Final Cut Pro is Front Most & Viewer is Showing: + -------------------------------------------------------------------------------- + if fcp.isFrontmost() + and fcp.viewer:isShowing() + and not fcp.isModalDialogOpen() + and not fcp.fullScreenWindow:isShowing() + and not fcp.commandEditor:isShowing() + and not fcp.preferencesWindow:isShowing() + then + -------------------------------------------------------------------------------- + -- Start the Mouse Watcher: + -------------------------------------------------------------------------------- + if mod.enableViewerRightClick() then + if mod._eventtap then + if not mod._eventtap:isEnabled() then + mod._eventtap:start() + end + else + mod._eventtap = eventtap.new({events.rightMouseUp}, mod._contextualMenu) + mod._eventtap:start() + end + else + if mod._eventtap then + mod._eventtap:stop() + mod._eventtap = nil + end + end + + -------------------------------------------------------------------------------- + -- Start the Caps Lock Watcher: + -------------------------------------------------------------------------------- + if mod.capslock() then + if mod._capslockEventTap then + if not mod._capslockEventTap:isEnabled() then + mod._capslockEventTap:start() + end + else + mod._capslockEventTap = eventtap.new({events.flagsChanged}, function(event) + local keycode = event:getKeyCode() + if keycode == 57 then + mod.update() + end + end) + mod._capslockEventTap:start() + end + else + if mod._capslockEventTap then + mod._capslockEventTap:stop() + mod._capslockEventTap = nil + end + end + + -------------------------------------------------------------------------------- + -- Toggle Overall Visibility: + -------------------------------------------------------------------------------- + if mod._areAnyOverlaysEnabled() == true then + if mod.capslock() == true then + -------------------------------------------------------------------------------- + -- Caps Lock Mode: + -------------------------------------------------------------------------------- + if capslock.get() == true then + mod.show() + else + mod.hide() + end + else + -------------------------------------------------------------------------------- + -- "Enable Overlays" Toggle: + -------------------------------------------------------------------------------- + if mod.disabled() == true then + mod.hide() + else + mod.show() + end + end + else + -------------------------------------------------------------------------------- + -- No Overlays Enabled: + -------------------------------------------------------------------------------- + mod.hide() + end + else + -------------------------------------------------------------------------------- + -- Otherwise hide the grid: + -------------------------------------------------------------------------------- + mod.hide() + + -------------------------------------------------------------------------------- + -- Destroy the Mouse Watcher: + -------------------------------------------------------------------------------- + if mod._eventtap then + mod._eventtap:stop() + mod._eventtap = nil + end + + -------------------------------------------------------------------------------- + -- Destroy the Caps Lock Watcher: + -------------------------------------------------------------------------------- + if mod._capslockEventTap then + mod._capslockEventTap:stop() + mod._capslockEventTap = nil + end + + -------------------------------------------------------------------------------- + -- Destroy the Mouse Move Letterbox Tracker: + -------------------------------------------------------------------------------- + if mod._mouseMoveLetterboxTracker then + mod._mouseMoveLetterboxTracker:stop() + mod._mouseMoveLetterboxTracker = nil + end + + -------------------------------------------------------------------------------- + -- Destroy any Mouse Move Trackers: + -------------------------------------------------------------------------------- + if mod._mouseMoveTracker then + for i, _ in pairs(mod._mouseMoveTracker) do + mod._mouseMoveTracker[i]:stop() + mod._mouseMoveTracker[i] = nil + end + end + end + end) +end + +-- STILLS_FOLDER -> string +-- Constant +-- Folder name for Stills Cache. +local STILLS_FOLDER = "Still Frames" + +-- DEFAULT_COLOR -> string +-- Constant +-- Default Colour Setting. +local DEFAULT_COLOR = "#FFFFFF" + +-- DEFAULT_ALPHA -> number +-- Constant +-- Default Alpha Setting. +local DEFAULT_ALPHA = 50 + +-- DEFAULT_GRID_SPACING -> number +-- Constant +-- Default Grid Spacing Setting. +local DEFAULT_GRID_SPACING = 20 + +-- DEFAULT_STILLS_LAYOUT -> number +-- Constant +-- Default Stills Layout Setting. +local DEFAULT_STILLS_LAYOUT = "Left Vertical" + +-- DEFAULT_LETTERBOX_HEIGHT -> number +-- Constant +-- Default Letterbox Height +local DEFAULT_LETTERBOX_HEIGHT = 70 + +-- FCP_COLOR_BLUE -> string +-- Constant +-- Apple's preferred blue colour in Final Cut Pro. +local FCP_COLOR_BLUE = "#5760e7" + +-- CROSS_HAIR_LENGTH -> number +-- Constant +-- Cross Hair Length +local CROSS_HAIR_LENGTH = 100 + +-- plugins.finalcutpro.viewer.overlays.NUMBER_OF_MEMORIES -> number +-- Constant +-- Number of Stills Memories Available. +mod.NUMBER_OF_MEMORIES = 5 + +-- plugins.finalcutpro.viewer.overlays.NUMBER_OF_DRAGGABLE_GUIDES -> number +-- Constant +-- Number of Draggable Guides Available. +mod.NUMBER_OF_DRAGGABLE_GUIDES = 5 + +--- plugins.finalcutpro.viewer.overlays.enableViewerRightClick +--- Variable +--- Allow the user to right click on the top of the viewer to access the menu? +mod.enableViewerRightClick = config.prop("fcpx.ViewerOverlay.EnableViewerRightClick", false) + +--- plugins.finalcutpro.viewer.overlays.disabled +--- Variable +--- Are all the Viewer Overlay's disabled? +mod.disabled = config.prop("fcpx.ViewerOverlay.MasterDisabled", false) +:watch(mod.update) + +--- plugins.finalcutpro.viewer.overlays.crossHairEnabled +--- Variable +--- Is Viewer Cross Hair Enabled? +mod.crossHairEnabled = config.prop("fcpx.ViewerOverlay.CrossHair.Enabled", false) +:watch(mod.update) + +--- plugins.finalcutpro.viewer.overlays.letterboxEnabled +--- Variable +--- Is Viewer Letterbox Enabled? +mod.letterboxEnabled = config.prop("fcpx.ViewerOverlay.Letterbox.Enabled", false) + +--- plugins.finalcutpro.viewer.overlays.letterboxHeight +--- Variable +--- Letterbox Height +mod.letterboxHeight = config.prop("fcpx.ViewerOverlay.Letterbox.Height", DEFAULT_LETTERBOX_HEIGHT) + +--- plugins.finalcutpro.viewer.overlays.basicGridEnabled +--- Variable +--- Is Viewer Grid Enabled? +mod.basicGridEnabled = config.prop("fcpx.ViewerOverlay.BasicGrid.Enabled", false) + +--- plugins.finalcutpro.viewer.overlays.crossHairColor +--- Variable +--- Viewer Grid Color as HTML value +mod.crossHairColor = config.prop("fcpx.ViewerOverlay.CrossHair.Color", DEFAULT_COLOR) + +--- plugins.finalcutpro.viewer.overlays.crossHairAlpha +--- Variable +--- Viewer Grid Alpha +mod.crossHairAlpha = config.prop("fcpx.ViewerOverlay.CrossHair.Alpha", DEFAULT_ALPHA) + +--- plugins.finalcutpro.viewer.overlays.gridColor +--- Variable +--- Viewer Grid Color as HTML value +mod.gridColor = config.prop("fcpx.ViewerOverlay.Grid.Color", DEFAULT_COLOR) + +--- plugins.finalcutpro.viewer.overlays.gridAlpha +--- Variable +--- Viewer Grid Alpha +mod.gridAlpha = config.prop("fcpx.ViewerOverlay.Grid.Alpha", DEFAULT_ALPHA) + +--- plugins.finalcutpro.viewer.overlays.customGridColor +--- Variable +--- Viewer Custom Grid Color as HTML value +mod.customGridColor = config.prop("fcpx.ViewerOverlay.Grid.CustomColor", nil) + +--- plugins.finalcutpro.viewer.overlays.gridSpacing +--- Variable +--- Viewer Custom Grid Color as HTML value +mod.gridSpacing = config.prop("fcpx.ViewerOverlay.Grid.Spacing", DEFAULT_GRID_SPACING) + +--- plugins.finalcutpro.viewer.overlays.activeMemory +--- Variable +--- Viewer Custom Grid Color as HTML value +mod.activeMemory = config.prop("fcpx.ViewerOverlay.ActiveMemory", 0) + +--- plugins.finalcutpro.viewer.overlays.stillsLayout +--- Variable +--- Stills layout. +mod.stillsLayout = config.prop("fcpx.ViewerOverlay.StillsLayout", DEFAULT_STILLS_LAYOUT) + +mod.draggableGuides = { + DraggableGuide(fcp.viewer, 1), + DraggableGuide(fcp.viewer, 2), + DraggableGuide(fcp.viewer, 3), + DraggableGuide(fcp.viewer, 4), + DraggableGuide(fcp.viewer, 5), +} + +--- plugins.finalcutpro.viewer.overlays.customGuideColor +--- Variable +--- Viewer Custom Guide Color as HTML value +mod.customGuideColor = config.prop("fcpx.ViewerOverlay.Guide.CustomColor", {}) + +--- plugins.finalcutpro.viewer.overlays.customCrossHairColor +--- Variable +--- Viewer Custom Cross Hair Color as HTML value +mod.customCrossHairColor = config.prop("fcpx.ViewerOverlay.CrossHair.CustomColor", {}) + +--- plugins.finalcutpro.viewer.overlays.capslock +--- Variable +--- Toggle Viewer Overlays with Caps Lock. +mod.capslock = config.prop("fcpx.ViewerOverlay.CapsLock", false):watch(function(enabled) + if not enabled then + if mod._capslockEventTap then + mod._capslockEventTap:stop() + mod._capslockEventTap = nil + end + end +end) + +-- addStillsFrame(overlay, frame, stillsLayout, activeMemory) +-- Function +-- Adds the active stills frame to the overlay, if it is available and enabled. +local function addStillsFrame(overlay, frame, stillsLayout, activeMemory) + if activeMemory ~= 0 then + local memory = mod.getMemory(activeMemory) + if memory then + local clipFrame = { x = 0, y = 0, h = "100%", w = "100%" } + if stillsLayout == "Left Vertical" then + clipFrame.w = "50%" + elseif stillsLayout == "Right Vertical" then + clipFrame.x = frame.w/2 + clipFrame.w = frame.w/2 + elseif stillsLayout == "Top Horizontal" then + clipFrame.h = "50%" + elseif stillsLayout == "Bottom Horizontal" then + clipFrame.y = frame.h/2 + clipFrame.h = "50%" + end + + overlay:appendElements({ + type = "rectangle", + frame = clipFrame, + action = "clip", + }) + + overlay:appendElements({ + type = "image", + frame = { x = 0, y = 0, h = "100%", w = "100%"}, + action = "fill", + image = memory, + imageScaling = "scaleProportionally", + imageAlignment = "topLeft", + }) + + if stillsLayout ~= "Full Frame" then + overlay:appendElements({ + type = "resetClip", + }) + end + end + end +end + +local function addCrossHair(overlay, frame) + if mod.crossHairEnabled() then + + local length = CROSS_HAIR_LENGTH + + local crossHairColor = mod.crossHairColor() + local crossHairAlpha = mod.crossHairAlpha() / 100 + + local fillColor + if crossHairColor == "CUSTOM" and mod.customCrossHairColor() then + fillColor = mod.customCrossHairColor() + fillColor.alpha = crossHairAlpha + else + fillColor = { hex = crossHairColor, alpha = crossHairAlpha } + end + + -------------------------------------------------------------------------------- + -- Horizontal Bar: + -------------------------------------------------------------------------------- + overlay:appendElements({ + type = "rectangle", + frame = { x = (frame.w / 2) - (length/2), y = frame.h / 2, h = 1, w = length}, + fillColor = fillColor, + action = "fill", + }) + + -------------------------------------------------------------------------------- + -- Vertical Bar: + -------------------------------------------------------------------------------- + overlay:appendElements({ + type = "rectangle", + frame = { x = frame.w / 2, y = (frame.h / 2) - (length/2), h = length, w = 1}, + fillColor = fillColor, + action = "fill", + }) + end +end + +local function addBasicGrid(overlay, frame, fillColor) + if mod.basicGridEnabled() then + local gridSpacing = mod.gridSpacing() + -------------------------------------------------------------------------------- + -- Add Vertical Lines: + -------------------------------------------------------------------------------- + for i=1, frame.w, frame.w/gridSpacing do + overlay:appendElements({ + type = "rectangle", + frame = { x = i, y = 0, h = frame.h, w = 1}, + fillColor = fillColor, + action = "fill", + }) + end + + -------------------------------------------------------------------------------- + -- Add Horizontal Lines: + -------------------------------------------------------------------------------- + for i=1, frame.h, frame.w/gridSpacing do + overlay:appendElements({ + type = "rectangle", + frame = { x = 0, y = i, h = 1, w = frame.w}, + fillColor = fillColor, + action = "fill", + }) + end + end +end + +local function addLeterbox(overlay, frame) + if mod.letterboxEnabled() then + + local letterboxHeight = mod.letterboxHeight() + overlay:appendElements({ + id = "topLetterbox", + type = "rectangle", + frame = { x = 0, y = 0, h = letterboxHeight, w = "100%"}, + fillColor = { hex = "#000000", alpha = 1 }, + action = "fill", + trackMouseDown = true, + trackMouseUp = true, + trackMouseMove = true, + }) + + overlay:appendElements({ + id = "bottomLetterbox", + type = "rectangle", + frame = { x = 0, y = frame.h - letterboxHeight, h = letterboxHeight, w = "100%"}, + fillColor = { hex = "#000000", alpha = 1 }, + action = "fill", + trackMouseDown = true, + trackMouseUp = true, + trackMouseMove = true, + }) + + end +end + +local function onLetterboxMouse(id, event, frame) + if id == "topLetterbox" or id == "bottomLetterbox" and event == "mouseDown" then + if not mod._mouseMoveLetterboxTracker then + mod._mouseMoveLetterboxTracker = eventtap.new({ events.leftMouseDragged, events.leftMouseUp }, function(e) + if e:getType() == events.leftMouseUp then + mod._mouseMoveLetterboxTracker:stop() + mod._mouseMoveLetterboxTracker = nil + else + Do(function() + if mod._overlay then + local mousePosition = mouse.absolutePosition() + local canvasTopLeft = mod._overlay:topLeft() + local letterboxHeight = mousePosition.y - canvasTopLeft.y + local viewerFrame = geometry.new(frame) + if geometry.new(mousePosition):inside(viewerFrame) and letterboxHeight > 10 and letterboxHeight < (frame.h/2) then + mod._overlay["topLetterbox"].frame = { x = 0, y = 0, h = letterboxHeight, w = "100%"} + mod._overlay["bottomLetterbox"].frame = { x = 0, y = frame.h - letterboxHeight, h = letterboxHeight, w = "100%"} + mod.letterboxHeight(letterboxHeight) + end + end + end):After(0) + end + end, false):start() + end + end +end + +local function addBorder(overlay) + overlay:appendElements({ + id = "border", + type = "rectangle", + action = "stroke", + strokeColor = { hex = FCP_COLOR_BLUE }, + strokeWidth = 5, + }) +end + +function mod._fillColor() + local gridColor = mod.gridColor() + local gridAlpha = mod.gridAlpha() / 100 + local fillColor + if gridColor == "CUSTOM" and mod.customGridColor() then + fillColor = mod.customGridColor() + fillColor.alpha = gridAlpha + else + fillColor = { hex = gridColor, alpha = gridAlpha } + end + return fillColor +end + +--- plugins.finalcutpro.viewer.overlays.show() -> none +--- Function +--- Show's the Viewer Grid. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.show() + + -------------------------------------------------------------------------------- + -- First, we must destroy any existing canvas: + -------------------------------------------------------------------------------- + mod.hide() + + local videoImage = fcp.viewer.videoImage + if videoImage then + local frame = videoImage:frame() + if frame then + -------------------------------------------------------------------------------- + -- New Canvas: + -------------------------------------------------------------------------------- + mod._overlay = canvas.new(frame) + + -------------------------------------------------------------------------------- + -- Still Frames: + -------------------------------------------------------------------------------- + addStillsFrame(mod._overlay, frame, mod.stillsLayout(), mod.activeMemory()) + + -------------------------------------------------------------------------------- + -- Cross Hair: + -------------------------------------------------------------------------------- + addCrossHair(mod._overlay, frame) + + -------------------------------------------------------------------------------- + -- Basic Grid: + -------------------------------------------------------------------------------- + + local fillColor = mod._fillColor() + + addBasicGrid(mod._overlay, frame, fillColor) + + -------------------------------------------------------------------------------- + -- Letterbox: + -------------------------------------------------------------------------------- + addLeterbox(mod._overlay, frame) + + -------------------------------------------------------------------------------- + -- Mouse Actions for Canvas: + -------------------------------------------------------------------------------- + if mod.letterboxEnabled() then + mod._overlay:clickActivating(false) + mod._overlay:canvasMouseEvents(true, true, true, true) + mod._overlay:mouseCallback(function(_, event, id) + + -------------------------------------------------------------------------------- + -- Letterbox: + -------------------------------------------------------------------------------- + if mod.letterboxEnabled() then + onLetterboxMouse(id, event, frame) + end + end) + end + + -------------------------------------------------------------------------------- + -- Add Border: + -------------------------------------------------------------------------------- + addBorder(mod._overlay) + + -------------------------------------------------------------------------------- + -- Show the Canvas: + -------------------------------------------------------------------------------- + mod._overlay:level("status") + -- TODO: Save the overlay to file + -- mod._overlay:show() + end + else + mod.hide() + end +end + +--- plugins.finalcutpro.viewer.overlays.hide() -> none +--- Function +--- Hides the Viewer Grid. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.hide() + if mod._overlay then + mod._overlay:delete() + mod._overlay = nil + end +end + +--- plugins.finalcutpro.viewer.overlays.draggableGuidesEnabled() -> boolean +--- Function +--- Are any draggable guides enabled? +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * `true` if at least one draggable guide is enabled otherwise `false` +function mod.draggableGuidesEnabled() + for _,guide in ipairs(mod.draggableGuides) do + if guide:isEnabled() then + return true + end + end + return false +end + +-- _areAnyOverlaysEnabled() -> boolean +-- Function +-- Are any Final Cut Pro Viewer Overlays Enabled? +-- +-- Parameters: +-- * None +-- +-- Returns: +-- * `true` if any Viewer Overlays are enabled otherwise `false` +function mod._areAnyOverlaysEnabled() + return mod.basicGridEnabled() == true + or mod.draggableGuidesEnabled() == true + or mod.activeMemory() ~= 0 + or mod.crossHairEnabled() == true + or mod.letterboxEnabled() == true + or false +end + +-------------------------------------------------------------------------------- +-- MENUS +-------------------------------------------------------------------------------- + +local function generateSeparatorMenuItem() + return { title = "-", disabled = true } +end + +local colorOptions = { + { title = i18n("black"), color = "#000000" }, + { title = i18n("white"), color = "#FFFFFF" }, + { title = i18n("yellow"), color = "#F4D03F"}, + { title = i18n("red"), color = "#FF5733"}, +} + +local function setCustomColor(colorProp, customColorProp) + dialog.color.continuous(false) + dialog.color.callback(function(color, closed) + if closed then + colorProp("CUSTOM") + customColorProp(color) + mod.update() + fcp:launch() + end + end) + dialog.color.show() + hs.focus() +end + +local function generateSpacingMenu(title, spacingProp) + local currentSpacing = spacingProp() + local menu = {} + + for i = 5,100,5 do + insert(menu, { + title = tostring(i), + checked = currentSpacing == i, + fn = function() spacingProp(i) end + }) + end + + return { title = title, menu = menu } +end + +local function generateDraggableGuidesMenu() + local menu = { + { title = i18n("enableAll"), fn = mod.enableAllDraggableGuides }, + { title = i18n("disableAll"), fn = mod.disableAllDraggableGuides }, + { title = "-", disabled = true }, + } + + local guideText, enableText, resetText = i18n("guide"), i18n("enable"), i18n("reset") + local appearanceText, colorText, opacityText = i18n("appearance"), i18n("color"), i18n("opacity") + + for _,guide in ipairs(mod.draggableGuides) do + local guideMenu = {} + + insert(guideMenu, { title = enableText, checked = guide:isEnabled(), fn = function() guide.isEnabled:toggle(); mod.update(); end }) + insert(guideMenu, { title = resetText, fn = function() guide:reset() end }) + + insert(guideMenu, { title = appearanceText, menu = { + menus.generateColorMenu(" "..colorText, guide.color), + menus.generateAlphaMenu(" "..opacityText, guide.alpha), + }}) + + insert(menu, { title = format("%s %d", guideText, guide.id), menu = guideMenu }) + end + + return menu +end + +local function generateCrossHairMenu() + local menu = {} + + insert(menu, { title = i18n("enable"), checked = mod.crossHairEnabled(), fn = function() mod.crossHairEnabled:toggle() end }) + insert(menu, { i18n("appearance"), menu = { + menus.generateColorMenu(" "..i18n("color"), mod.crossHairColor), + menus.generateAlphaMenu(" "..i18n("opacity"), mod.crossHairAlpha), + }}) + + return menu +end + +local function generateLetterboxMattesMenu() + return { + { title = i18n("enable"), checked = mod.letterboxEnabled(), fn = function() mod.letterboxEnabled:toggle(); mod.update(); end }, + } +end + +local function generateStillFrameViewMenu() + local menu = {} + local activeMemory = mod.activeMemory() + local memoryText = i18n("memory") + for i = 1, mod.NUMBER_OF_MEMORIES do + insert(menu, { + title = format("%s %d", memoryText, i), + checked = activeMemory == i, + fn = function() mod.viewMemory(i) end, + disabled = not mod.getMemory(1), + }) + end + return menu +end + +local function generateStillFrameSaveMenu() + local menu = {} + local memoryText = i18n("memory") + for i = 1, mod.NUMBER_OF_MEMORIES do + insert(menu, { + title = format("%s %d", memoryText, i), + fn = function() mod.saveMemory(i) end, + }) + end + return menu +end + +local function generateStillFrameImportMenu() + local menu = {} + local memoryText = i18n("memory") + for i = 1, mod.NUMBER_OF_MEMORIES do + insert(menu, { + title = format("%s %d", memoryText, i), + fn = function() mod.importMemory(i) end, + }) + end + return menu +end + +local function generateStillFrameDeleteMenu() + local menu = {} + local memoryText = i18n("memory") + for i = 1, mod.NUMBER_OF_MEMORIES do + insert(menu, { + title = format("%s %d", memoryText, i), + fn = function() mod.deleteMemory(1) end, + disabled = not mod.getMemory(i), + }) + end + return menu +end + +local function generateStillFrameAppearanceMenu() + return { + { title = i18n("fullFrame"), checked = mod.stillsLayout() == "Full Frame", fn = function() mod.stillsLayout("Full Frame"); mod.update() end }, + { title = "-", disabled = true }, + { title = i18n("leftVertical"), checked = mod.stillsLayout() == "Left Vertical", fn = function() mod.stillsLayout("Left Vertical"); mod.update() end }, + { title = i18n("rightVertical"), checked = mod.stillsLayout() == "Right Vertical", fn = function() mod.stillsLayout("Right Vertical"); mod.update() end }, + { title = "-", disabled = true }, + { title = i18n("topHorizontal"), checked = mod.stillsLayout() == "Top Horizontal", fn = function() mod.stillsLayout("Top Horizontal"); mod.update() end }, + { title = i18n("bottomHorizontal"), checked = mod.stillsLayout() == "Bottom Horizontal", fn = function() mod.stillsLayout("Bottom Horizontal"); mod.update() end }, + } +end + +local function generateGridOverlayAppearanceMenu() + local menu = {} + + -- colors + insert(menu, menus.generateColorMenu(" ".. i18n("color"), mod.gridColor)) + insert(menu, menus.generateAlphaMenu(" "..i18n("opacity"), mod.gridAlpha)) + insert(menu, generateSpacingMenu(" "..i18n("segments"), mod.gridSpacing)) + + return menu +end + +-- generateMenu -> table +-- Function +-- Returns a table with the Overlay menu. +-- +-- Parameters: +-- * None +-- +-- Returns: +-- * A table containing the overlay menu. +local function generateMenu() + local menu = {} + + -------------------------------------------------------------------------------- + -- + -- ENABLE OVERLAYS: + -- + -------------------------------------------------------------------------------- + insert(menu, { title = i18n("enable") .. " " .. i18n("overlays"), checked = not mod.capslock() and not mod.disabled(), fn = function() mod.disabled:toggle() end, disabled = mod.capslock() }) + insert(menu, { title = i18n("toggleOverlaysWithCapsLock"), checked = mod.capslock(), fn = function() mod.capslock:toggle(); mod.update() end }) + + insert(menu, generateSeparatorMenuItem() ) + + -- GUIDES SECTION: + insert(menu, { title = string.upper(i18n("guides")) .. ":", disabled = true }) + + -------------------------------------------------------------------------------- + -- + -- DRAGGABLE GUIDES: + -- + -------------------------------------------------------------------------------- + insert(menu, { title = " " .. i18n("draggableGuides"), menu = generateDraggableGuidesMenu() }) + + -------------------------------------------------------------------------------- + -- + -- CROSS HAIR: + -- + -------------------------------------------------------------------------------- + insert(menu, { title = " " .. i18n("crossHair"), menu = generateCrossHairMenu()}) + + insert(menu, generateSeparatorMenuItem()) + + -- MATTES SECTION: + insert(menu, { title = string.upper(i18n("mattes")) .. ":", disabled = true }) + insert(menu, { title = " " .. i18n("letterbox"), menu = generateLetterboxMattesMenu()}) + + insert(menu, generateSeparatorMenuItem()) + + -------------------------------------------------------------------------------- + -- + -- STILL FRAMES: + -- + -------------------------------------------------------------------------------- + insert(menu, { title = string.upper(i18n("stillFrames")) .. ":", disabled = true }) + + insert(menu, { title = " " .. i18n("view"), menu = generateStillFrameViewMenu() }) + insert(menu, { title = " " .. i18n("save"), menu = generateStillFrameSaveMenu() }) + insert(menu, { title = " " .. i18n("import"), menu = generateStillFrameImportMenu() }) + insert(menu, { title = " " .. i18n("delete"), menu = generateStillFrameDeleteMenu() }) + insert(menu, { title = " " .. i18n("appearance"), menu = generateStillFrameAppearanceMenu() }) + + insert(menu, generateSeparatorMenuItem()) + + -------------------------------------------------------------------------------- + -- + -- GRID OVERLAY: + -- + -------------------------------------------------------------------------------- + insert(menu, { title = string.upper(i18n("gridOverlay")) .. ":", disabled = true }) + + insert(menu, { title = " " .. i18n("enable"), checked = mod.basicGridEnabled(), fn = function() mod.basicGridEnabled:toggle(); mod.update(); end }) + + insert(menu, { title = " " .. i18n("appearance"), menu = generateGridOverlayAppearanceMenu() }) + + insert(menu, generateSeparatorMenuItem()) + + insert(menu, { title = i18n("reset") .. " " .. i18n("overlays"), fn = function() mod.resetOverlays() end }) + + return menu +end + +-- _contextualMenu(event) -> none +-- Function +-- Builds the Final Cut Pro Overlay contextual menu. +-- +-- Parameters: +-- * event - The `hs.eventtap` event +-- +-- Returns: +-- * None +function mod._contextualMenu(event) + local ui = fcp.viewer:UI() + local topBar = ui and axutils.childFromTop(ui, 1) + if topBar then + local barFrame = topBar:attributeValue("AXFrame") + local location = event:location() and geometry.point(event:location()) + if barFrame and location and location:inside(geometry.rect(barFrame)) then + if mod._menu then + mod._menu:delete() + mod._menu = nil + end + mod._menu = menubar.new() + mod._menu:setMenu(generateMenu()) + mod._menu:removeFromMenuBar() + mod._menu:popupMenu(location, true) + end + end +end + +mod._lastValue = false + +--- plugins.finalcutpro.viewer.overlays.getStillsFolderPath() -> string | nil +--- Function +--- Gets the stills folder path. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The stills folder path as a string or `nil` if an error occurs. +function mod.getStillsFolderPath() + local userConfigRootPath = config.userConfigRootPath + if userConfigRootPath then + if not tools.doesDirectoryExist(userConfigRootPath) then + fs.mkdir(userConfigRootPath) + end + local path = userConfigRootPath .. "/" .. STILLS_FOLDER .. "/" + if not tools.doesDirectoryExist(path) then + fs.mkdir(path) + end + return tools.doesDirectoryExist(path) and path + else + return nil + end +end + +--- plugins.finalcutpro.viewer.overlays.deleteMemory() -> none +--- Function +--- Deletes a memory. +--- +--- Parameters: +--- * id - An identifier in the form of a number. +--- +--- Returns: +--- * None +function mod.deleteMemory(id) + local path = mod.getStillsFolderPath() + if path then + local imagePath = path .. "/memory" .. id .. ".png" + if tools.doesFileExist(imagePath) then + os.remove(imagePath) + local activeMemory = mod.activeMemory() + if activeMemory == id then + mod.activeMemory(0) + mod.update() + end + end + end +end + +--- plugins.finalcutpro.viewer.overlays.saveMemory() -> none +--- Function +--- Saves a still frame to file. +--- +--- Parameters: +--- * id - An identifier in the form of a number. +--- +--- Returns: +--- * None +function mod.saveMemory(id) + local videoImage = fcp.viewer.videoImage + local result = false + if videoImage then + local path = mod.getStillsFolderPath() + if path then + local snapshot = videoImage:snapshot(path .. "/memory" .. id .. ".png") + if snapshot then + result = true + end + else + log.ef("Could not create Cache Folder.") + end + else + log.ef("Could not find Viewer.") + end + if not result then + dialog.displayErrorMessage("Could not save still frame.") + end +end + +--- plugins.finalcutpro.viewer.overlays.importMemory() -> none +--- Function +--- Import a file to memory. +--- +--- Parameters: +--- * id - An identifier in the form of a number. +--- +--- Returns: +--- * None +function mod.importMemory(id) + local disabled = mod.disabled() + mod.disabled(true) + local allowedImageType = {"PDF", "com.adobe.pdf", "BMP", "com.microsoft.bmp", "JPEG", "JPEG2", "jpg", "public.jpeg", "PICT", "com.apple.pict", "PNG", "public.png", "PSD", "com.adobe.photoshop-image", "TIFF", "public.tiff"} + local path = cpDialog.displayChooseFile("Please select a file to import", allowedImageType) + local stillsFolderPath = mod.getStillsFolderPath() + if path and stillsFolderPath then + local importedImage = image.imageFromPath(path) + if importedImage then + importedImage:saveToFile(stillsFolderPath .. "/memory" .. id .. ".png") + local activeMemory = mod.activeMemory() + if activeMemory == id then + mod.activeMemory(0) + end + end + end + mod.disabled(disabled) +end + +--- plugins.finalcutpro.viewer.overlays.getMemory(id) -> image | nil +--- Function +--- Gets an image from memory. +--- +--- Parameters: +--- * id - The ID of the memory you want to retrieve. +--- +--- Returns: +--- * The memory as a `hs.image` or `nil` if the memory could not be retrieved. +function mod.getMemory(id) + local path = mod.getStillsFolderPath() + if path then + local imagePath = path .. "/memory" .. id .. ".png" + if tools.doesFileExist(imagePath) then + local result = image.imageFromPath(imagePath) + if result then + return result + end + end + end + return nil +end + +--- plugins.finalcutpro.viewer.overlays.viewMemory(id) -> none +--- Function +--- View a memory. +--- +--- Parameters: +--- * id - The ID of the memory you want to retrieve. +--- +--- Returns: +--- * None +function mod.viewMemory(id) + local activeMemory = mod.activeMemory() + if activeMemory == id then + mod.activeMemory(0) + else + local result = mod.getMemory(id) + if result then + mod.activeMemory(id) + end + end + mod.update() +end + +--- plugins.finalcutpro.viewer.overlays.setGridSpacing(value) -> none +--- Function +--- Sets Grid Spacing. +--- +--- Parameters: +--- * value - The value you want to set. +--- +--- Returns: +--- * None +function mod.setGridSpacing(value) + mod.gridSpacing(value) + mod.update() +end + +--- plugins.finalcutpro.viewer.overlays.setGridAlpha(value) -> none +--- Function +--- Sets Grid Alpha. +--- +--- Parameters: +--- * value - The value you want to set. +--- +--- Returns: +--- * None +function mod.setGridAlpha(value) + mod.gridAlpha(value) + mod.update() +end + +--- plugins.finalcutpro.viewer.overlays.setGridColor(value) -> none +--- Function +--- Sets Grid Color. +--- +--- Parameters: +--- * value - The value you want to set. +--- +--- Returns: +--- * None +function mod.setGridColor(value) + mod.gridColor(value) + mod.update() +end + +--- plugins.finalcutpro.viewer.overlays.setCustomGridColor() -> none +--- Function +--- Pops up a Color Dialog box allowing the user to select a custom colour for grid lines. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.setCustomGridColor() + setCustomColor(mod.gridColor, mod.customGridColor) +end + +--- plugins.finalcutpro.viewer.overlays.setCustomCrossHairColor() -> none +--- Function +--- Pops up a Color Dialog box allowing the user to select a custom colour for cross hairs. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.setCustomCrossHairColor() + dialog.color.continuous(false) + dialog.color.callback(function(color, closed) + if closed then + mod.crossHairColor("CUSTOM") + mod.customCrossHairColor(color) + mod.update() + fcp:launch() + end + end) + dialog.color.show() + hs.focus() +end + +--- plugins.finalcutpro.viewer.overlays.enableAllDraggableGuides() -> none +--- Function +--- Enable all draggable guides. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.enableAllDraggableGuides() + for _,guide in ipairs(mod.draggableGuides) do + guide:isEnabled(true) + end + mod.update() +end + +--- plugins.finalcutpro.viewer.overlays.disableAllDraggableGuides() -> none +--- Function +--- Disable all draggable guides. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.disableAllDraggableGuides() + for _,guide in ipairs(mod.draggableGuides) do + guide:isEnabled(false) + end + mod.update() +end + +--- plugins.finalcutpro.visible.overlay.resetAllDraggableGuides() -> none +--- Function +--- Resets all draggable guides to default settings. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.resetAllDraggableGuides() + for _,guide in ipairs(mod.draggableGuides) do + guide:reset() + end +end + +--- plugins.finalcutpro.viewer.overlays.resetOverlays() -> none +--- Function +--- Resets all overlays to their default values. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function mod.resetOverlays() + mod.crossHairEnabled(false) + mod.letterboxEnabled(false) + mod.letterboxHeight(DEFAULT_LETTERBOX_HEIGHT) + mod.basicGridEnabled(false) + mod.crossHairColor(DEFAULT_COLOR) + mod.crossHairAlpha(DEFAULT_ALPHA) + mod.gridColor(DEFAULT_COLOR) + mod.gridAlpha(DEFAULT_ALPHA) + mod.customGridColor(nil) + mod.gridSpacing(DEFAULT_GRID_SPACING) + mod.activeMemory(0) + mod.stillsLayout(DEFAULT_STILLS_LAYOUT) + mod.resetAllDraggableGuides() + mod.customCrossHairColor({}) + mod.update() +end + +-- updater -> cp.deferred +-- Variable +-- A deferred timer that triggers the update function. +local updater = deferred.new(0.1):action(mod.update) + +-- deferredUpdate -> none +-- Function +-- Triggers the update function. +-- +-- Parameters: +-- * None +-- +-- Returns: +-- * None +local function deferredUpdate() + updater() +end + +local plugin = { + id = "finalcutpro.viewer.overlays", + group = "finalcutpro", + dependencies = { + ["finalcutpro.commands"] = "fcpxCmds", + ["finalcutpro.menu.manager"] = "menu", + } +} + +function plugin.init(deps) + -------------------------------------------------------------------------------- + -- Only load plugin if Final Cut Pro is supported: + -------------------------------------------------------------------------------- + if not fcp:isSupported() then return end + + -------------------------------------------------------------------------------- + -- Setup the system menu: + -------------------------------------------------------------------------------- + deps.menu.viewer + :addMenu(10001, function() return i18n("overlay") end) + :addItems(999, function() + return { + { title = i18n("enableViewerContextualMenu"), fn = function() mod.enableViewerRightClick:toggle(); mod.update() end, checked = mod.enableViewerRightClick() }, + { title = "-", disabled = true }, + } + end) + :addItems(1000, generateMenu) + + -------------------------------------------------------------------------------- + -- Update Canvas when Final Cut Pro is shown/hidden: + -------------------------------------------------------------------------------- + fcp.isFrontmost:watch(deferredUpdate) + fcp.isModalDialogOpen:watch(deferredUpdate) + fcp.fullScreenWindow.isShowing:watch(deferredUpdate) + fcp.commandEditor.isShowing:watch(deferredUpdate) + fcp.preferencesWindow.isShowing:watch(deferredUpdate) + + -------------------------------------------------------------------------------- + -- Update Canvas one second after going full-screen: + -------------------------------------------------------------------------------- + fcp.primaryWindow.isFullScreen:watch(function() doAfter(1, mod.update) end) + + -------------------------------------------------------------------------------- + -- Update Canvas when Final Cut Pro's Viewer is resized or moved: + -------------------------------------------------------------------------------- + fcp.viewer.frame:watch(function(value) + if value then + deferredUpdate() + end + end) + + -------------------------------------------------------------------------------- + -- Setup Commands: + -------------------------------------------------------------------------------- + if deps.fcpxCmds then + deps.fcpxCmds + :add("cpViewerBasicGrid") + :whenActivated(function() mod.basicGridEnabled:toggle(); mod.update() end) + + for i=1, mod.NUMBER_OF_DRAGGABLE_GUIDES do + deps.fcpxCmds + :add("cpViewerDraggableGuide" .. i) + :whenActivated(function() mod.draggableGuides[i].isEnabled:toggle() end) + :titled(i18n("cpViewerDraggableGuide_title") .. " " .. i) + end + + deps.fcpxCmds + :add("cpToggleAllViewerOverlays") + :whenActivated(function() mod.disabled:toggle() end) + + for i=1, mod.NUMBER_OF_MEMORIES do + deps.fcpxCmds + :add("cpSaveStillsFrame" .. i) + :whenActivated(function() mod.saveMemory(i) end) + :titled(i18n("saveCurrentFrameToStillsMemory") .. " " .. i) + + deps.fcpxCmds + :add("cpViewStillsFrame" .. i) + :whenActivated(function() mod.viewMemory(i) end) + :titled(i18n("viewStillsMemory") .. " " .. i) + end + end + + return mod +end + +function plugin.postInit() + -------------------------------------------------------------------------------- + -- Update the Canvas on initial boot: + -------------------------------------------------------------------------------- + if mod.update then + mod.update() + end +end + +return plugin diff --git a/src/plugins/finalcutpro/viewer/overlays/menus.lua b/src/plugins/finalcutpro/viewer/overlays/menus.lua new file mode 100644 index 000000000..740a31f6e --- /dev/null +++ b/src/plugins/finalcutpro/viewer/overlays/menus.lua @@ -0,0 +1,82 @@ +local require = require + +local hs = _G.hs + +local i18n = require "cp.i18n" + +local dialog = require "hs.dialog" +local color = require "hs.drawing.color" + +local insert = table.insert +local format = string.format + +local menus = {} + +function menus.generateSeparatorMenuItem() + return { title = "-", disabled = true } +end + +local colorOptions = { + { title = i18n("black"), color = color.asRGB({ hex = "#000000" }) }, + { title = i18n("white"), color = color.asRGB({ hex = "#FFFFFF" }) }, + { title = i18n("yellow"), color = color.asRGB({ hex = "#F4D03F" }) }, + { title = i18n("red"), color = color.asRGB({ hex = "#FF5733" }) }, +} + +local function setCustomColor(colorProp) + dialog.color.continuous(false) + dialog.color.callback(function(color, closed) + if closed then + colorProp(color) + end + end) + dialog.color.show() + hs.focus() +end + +local function colorsEqual(left, right) + if left == nil then + return right == nil + elseif right == nil then + return false + end + + return left.red == right.red + and left.green == right.green + and left.blue == right.blue +end + +function menus.generateColorMenu(title, colorProp) + local currentColor = colorProp() + local menu = {} + + local foundColor = false + + for _,option in ipairs(colorOptions) do + local isOption = colorsEqual(currentColor, option.color) + foundColor = foundColor or isOption + insert(menu, { title = option.title, checked = isOption, fn = function() colorProp(option.color) end }) + end + + insert(menu, menus.generateSeparatorMenuItem() ) + insert(menu, { title = i18n("custom"), checked = not foundColor, fn = function() setCustomColor(colorProp) end }) + + return { title = title, menu = menu } +end + +function menus.generateAlphaMenu(title, alphaProp) + local currentAlpha = alphaProp() + local menu = {} + + for i = 10,100,10 do + insert(menu, { + title = format("%d%%", i), + checked = currentAlpha == i, + fn = function() alphaProp(i) end, + }) + end + + return { title = title, menu = menu } +end + +return menus \ No newline at end of file