Hi folks
Is it possible to use the SDK to create a plugin that creates a Collection Set that contains a number of Smart collections.
I'd also like to be able to select a Folder and have the plugin use the folder name as one of the parameters in the Smart collections and the title of the Collection Set
Cheers in advance
You may be interested in:
Folder Collections:
http://www.robcole.com/Rob/ProductsAndServices/FolderCollectionsLrPlug in
Download is source code format.
It uses dumb collections because there is no way to have smart collection content match folder content - I got close using smart collections, but ultimately had to rely on dumb collections and a background task.
Still, the original version used smart collections, and the code for it is still mostly in there...
Cheers,
Rob
Ok, I'm admitting defeat at an early stage ...
I think I've managed to return the array of selected sources from catalog:getActiveSources()
However I can't find any documentation on the actual structure of this array or how to access it
The docs for catalog:getActiveSources() say
Returned:
(array of LrCollection, LrCollectionSet, LrPublishedCollection, LrPublishedCollectionSet or LrFolder)
![]()
Hi John,
Try something like this:
local sources = catalog:getActiveSources()
for _notUsed, source in ipairs(sources) do
if source:type() == 'LrCollection' then
-- do something
elseif source:type() == 'LrCollectionSet' then
-- do something
elseif source:type() == 'LrPublishedCollection' then
-- do something
elseif source:type() == 'LrPublishedCollectionSet' then
-- do something
elseif source:type() == 'LrFolder' then
-- do something
end
end
Does this help?
-Don
Thanks Don, that helps enormously.
I'm still having trouble extracting the source names though. Guessing from source:type() I tried getting the names by trying source:name() or source:title() but it didn't work. Sorry about these possibly dumb questions, but I can't find any documentation on the structure of the array.
What object were you looking at? For example, the LrPublishedCollection's getName() method documentation includes:
This function must be called from within an asynchronous task started using LrTasks.
Did you do that?
-Don
I was looking at the object LrFolder in the returned array
I tried to get the name of the selected folder like this:
elseif source:type() == 'LrFolder' then
sourceName = source:getName()
end
/Adobe Lightroom 4 SDK/API Reference/modules/LrFolder.html#folder:getName
which says:
---------------------------------------------------------------------- --------------------
folder:getName() Retrieves the name of this folder.
First supported in version 3.0 of the Lightroom SDK.
(string) The name.
---------------------------------------------------------------------- --------------------
and mentions nothing about 'an asynchronous task started using LrTasks.' whatever that is ![]()
Stumped ![]()
Lots of stuff only works in an asynchronous task.
The first thing I do in response to a menu or button is create an asynchronous task to process it, with an error handler attached to the processing context (LrFunctionContext).
That way, I never have to worry about something needing to be done from an asynchronous task, because *everything* is being done from an asynchronous task.
If you want to make sure another instance of the task doesn't get started before the other is finished, you can use recursion guarding.
I don't use the LrRecursionGuard, but it's one possibility.
Rob
Where can I learn about how to create an asyncronous task?
This is where I am at so far. I'm able to show a dialog box that lists the types of active sources
I modified one of the SDK programmers guide tutorials thus:
ShowSources.lua
********************************************************************** **************************
local LrFunctionContext = import 'LrFunctionContext'
local LrBinding = import 'LrBinding'
local LrDialogs = import 'LrDialogs'
local LrView = import 'LrView'
local LrColor = import 'LrColor'
local LrTasks = import 'LrTasks'
local LrApplication = import 'LrApplication'
MyHWLibraryItem = {}
-- function to interrogate active sources array
function read_activeSources_array( sources )
source_type = "Active Sources:\n"
for i, source in ipairs( sources ) do
if source:type() == 'LrCollection' then
source_type = source_type .. i .. " is a Collection\n"
elseif source:type() == 'LrFolder' then
source_type = source_type .. i .. " is a folder\n"
end
--local sourceName = source:getName() -- this produces ***ERROR***
end
return source_type
end
-- ***ERROR*** "An internal error has occoured: We can only wait from within a task"
function MyHWLibraryItem.showCustomDialog()
-- body of show-dialog function
LrFunctionContext.callWithContext( "ShowSources",
function( context )
catalog = LrApplication.activeCatalog()
local path = catalog:getPath()
local sources = catalog:getActiveSources()
--string = LrTasks.startAsyncTask( read_activeSources_array( sources ) )
display_string = read_activeSources_array( sources )
-- create view hierarchy for dialog box
local f = LrView.osFactory() -- get the view factory object
local c = f:column { -- the root node
f:row { -- Display contents of active sources Array
f:static_text {
text_color = LrColor( 0, 0, 1 ),
alignment = "Left",
-- add title with binding later
title = display_string
},
},
}
local result = LrDialogs.presentModalDialog(
{
title = "Show Sources",
contents = c, -- the view hierarchy we defined
}
)
end )
end
MyHWLibraryItem.showCustomDialog()
********************************************************************** ********************
LrFunctionContext.postAsyncTaskWithContext is my personal favorite, since you can create the task and the processing context in one swipe that way.
Put that at the outermost level.
Then assign a cleanup, or error handler to the context, so errors don't just silently abort, or at least that's what used to happen to button handlers, unless Adobe changed that in Lr4 (I haven't checked).
I simply check/set a variable upon entry, then clear it in the cleanup handler to implement recursion guarding, which I have rolled into a set of classes which I use in my plugin framework.
You can see said framework (elare plugin framework) in any of my plugins (e.g. http://www.robcole.com/Rob/ProductsAndServices), all of which include source code, and you can use the framework by going here:
https://www.assembla.com/spaces/lrdevplugin/
R
Yeah, - what Don said too.
Just remember that recursion guarding can become an issue when everything is done asynchronously, since impatient users or those with twitchy fingers may get 3 or 4 of the same things going at once if not careful. Obviously, if there isn't enough time to click twice before the first/next (modal) dialog box comes up..., it's a non-problem. But if processing takes more than a fraction of a second... (Next up: LrProgressScope...).
Also, tasks don't abort when reloading the plugin, until they're finished, *if* they finish. It's possible to have multiple instances of the same task running with different environments...
I use LrShutdownPlugin to set a global shutdown flag and check it in async loops, so plugin reloads happen gracefully. (I wish LrTasks.sleep would automatically return a code upon a plugin reload / shutdown (like java...), but since it doesn't, I implemented my own sleeper in the framework).
PS - There is some new shutdown handling which I've yet to investigate. Since it would be ignored in Lr3, it's hardly worth using unless plugin is Lr4 only.
My apology in advance if this is too much too soon - maybe return to this thread and re-read if you start having problems...
Also, as Obewankanobee said: read the docs, Luke...
Rob
HURRAH! You guys Rock! Thanks ever so much. ![]()

I did
LrTasks.startAsyncTask(function()
MyHWLibraryItem.showCustomDialog()
end)
And then modified my function to
-- function to interrogate active sources array
function read_activeSources_array( sources )
source_type = "Active Sources:\n"
for i, source in ipairs( sources ) do
local sourceName = source:getName()
if source:type() == 'LrCollection' then
source_type = source_type .. i .. " is a Collection called " .. sourceName .. "\n"
elseif source:type() == 'LrFolder' then
source_type = source_type .. i .. " is a folder called " .. sourceName .. "\n"
--source_type = source_type .. sourceName .."\n"
end
end
return source_type
end
******************************************
Rob, errors seem pretty verbose in LR4.1 when something has gone wrong an error dialog appears everytime on the cide I've been mangling ![]()
I've bookmarked your site and the assembla site
Thanks again peeps ![]()
Rob Cole wrote:
Yeah, - what Don said too.
Just remember that recursion guarding can become an issue when everything is done asynchronously, since impatient users or those with twitchy fingers may get 3 or 4 of the same things going at once if not careful. Obviously, if there isn't enough time to click twice before the first/next (modal) dialog box comes up..., it's a non-problem. But if processing takes more than a fraction of a second... (Next up: LrProgressScope...).
Also, tasks don't abort when reloading the plugin, until they're finished, *if* they finish. It's possible to have multiple instances of the same task running with different environments...
I use LrShutdownPlugin to set a global shutdown flag and check it in async loops, so plugin reloads happen gracefully. (I wish LrTasks.sleep would automatically return a code upon a plugin reload / shutdown (like java...), but since it doesn't, I implemented my own sleeper in the framework).
PS - There is some new shutdown handling which I've yet to investigate. Since it would be ignored in Lr3, it's hardly worth using unless plugin is Lr4 only.
My apology in advance if this is too much too soon - maybe return to this thread and re-read if you start having problems...
Also, as Obewankanobee said: read the docs, Luke...
Rob
Cheers Rob ![]()
I am reading the docs as much as possible. I'm not finding them that intuitive to be honest, but I am trying to source the answers myself before coming here and shouting 'Help!
I hadn't even heard of LUA a few days ago so it is a bit of a jump in the deep end ![]()
John Spacey wrote:
HURRAH! You guys Rock! Thanks ever so much.
******************************************
Rob, errors seem pretty verbose in LR4.1 when something has gone wrong an error dialog appears everytime on the cide I've been mangling
I've bookmarked your site and the assembla site
Thanks again peeps
Congrats, and you're welcome.
In some cases, Lightroom provides the error handling, in other cases - it doesn't.
I no longer remember which cases it does, and which cases it doesn't, since I always provide my own.
One case I do remember is button handlers. You *must* handle errors in button handlers or they will just silently malfunction, or at least that's true for Lr3-.
Oh yeah, change handlers (ui property observers) and ui validator functions are 2 more cases that need error handlers (unless you don't mind silent failure upon error).
R
John Spacey wrote:
Cheers Rob
I am reading the docs as much as possible. I'm not finding them that intuitive to be honest, but I am trying to source the answers myself before coming here and shouting 'Help!
I hadn't even heard of LUA a few days ago so it is a bit of a jump in the deep end
Hi John,
Yeah, the docs are perhaps best digested along with a big dose of experience.
Enjoy (I think Lua is great).
Rob
I have a number of plugins that deal with collections which may have some good example source code:
FolderCollections
CollectionAgent
Stacker
SQLiteroom
LrFourB
PublishServiceAssistant
http://www.robcole.com/Rob/ProductsAndServices
R
Thanks Rob ![]()
I've already had a look in your FolderCollections.lua and there was some helpful code in there
I'll check out the others soon
tongiht I've written a recursive function that gets an activeSource's parent and then gets the parent's parent, and so on until the 'root' is reached. The recursive function adds the succesive parents to an Array. ( a table in LUA speak I believe
)
******************************************************
-- Function to create array of folder of collection parental structure
function MyHWLibraryItem.CreateParentStructure( source, parent_table, x )
local source_parent = source:getParent()
--add to parent table ( array )
parent_table[x] = source_parent
if parent_table[x] ~= nil then -- parent exists
x=x+1
-- call itself to interrogate further up the parent branch
parent_table = MyHWLibraryItem.CreateParentStructure( source_parent, parent_table, x )
else -- parent doesn't exist - top of branch
parent_table[x] = false
end
return parent_table
end
******************************************************
Interesting to note that I couldn't get this to work until I realised I couldn't assign an element of the array a value of "nil" ( which is returned when getParent() finds no parent ) and had to assign it a value of 'false' instead.
I think your Folder Collections plug deals with recreating path structures so I'll take another look at that shortly ![]()
Looks good John.
A couple lua-y things to note:
You can assign nil to an array, but:
* ipairs stops on the first nil.
* #tbl counts items (at positive integer indexes) up unitl the last nil.
I mean, assigning non-nil values (e.g. false) is probably the preferred approach in this case, but just so ya know:
#{ x=1 } == 0 -- is true
#{ 1, nil, 3 } == 3 -- true too
#{ 1, nil } == 1 -- also true
#{ x=1, 2 } == 1 -- true
{ x=1, 2 }[1] == 2 -- true
local x = {}
x[0] = 1
#x == 0 -- true
for i,v in ipairs{ 1, nil, 3 } do
local nothing = 1 -- executes only once
end
R
Here ya go - A huge thanks to you chaps for all your help!. ![]()
********************************************************************** ****************
local LrFunctionContext = import 'LrFunctionContext'
local LrBinding = import 'LrBinding'
local LrDialogs = import 'LrDialogs'
local LrView = import 'LrView'
local LrColor = import 'LrColor'
local LrTasks = import 'LrTasks'
local LrApplication = import 'LrApplication'
local LrLogger = import 'LrLogger'
local myLogger = LrLogger( 'libraryLogger' )
myLogger:enable( "logfile" ) -- or "print"
MyHWLibraryItem = {}
-- Logger function
function MyHWLibraryItem.outputToLog( message )
myLogger:trace( message )
end
-- Function to extract names from parent table
function MyHWLibraryItem.Path_String_From_Parent_Table( parent_table )
-- stuff goes in here
local path_string =""
for y, source in ipairs( parent_table ) do
if source ~= false then
parent = source:getName()
else
parent = "ROOT"
end
if y == #parent_table then
path_string = path_string .. parent .. "/.."
else
path_string = path_string .. parent .. "/"
end
end
return path_string
end
-- Function to create array of folder of collection parental structure
function MyHWLibraryItem.CreateParentStructure( source, parent_table, x )
local source_parent = source:getParent()
--add to parent table ( array )
parent_table[x] = source_parent
if parent_table[x] ~= nil then -- parent exists
x=x+1
-- call itself to interrogate further up the parent branch
parent_table = MyHWLibraryItem.CreateParentStructure( source_parent, parent_table, x )
else -- parent doesn't exist - top of branch
parent_table[x] = false
end
return parent_table
end
function MyHWLibraryItem.showCustomDialog()
-- body of show-dialog function
LrFunctionContext.callWithContext( "ShowSources",
function( context )
catalog = LrApplication.activeCatalog()
local source_type
local source_name
local source_parent
local source_parent_name
local collection_set
local path = catalog:getPath()
local sources = catalog:getActiveSources()
--local folders = catalog:getFolders()
source_type = "Active Sources:\n***********\n\n"
-- analyse source table
for i, source in ipairs( sources ) do
-- Create table for parent structure of source
local parent_table = {}
source_name = source:getName()
x=1 -- init counter for parent table
-- call recursive function
parent_table = MyHWLibraryItem.CreateParentStructure( source, parent_table, x )
-- now reverse the parent table
local y=1
local z = #parent_table
local parent_table_rev = {} -- create empty rev table
for a = z , 1, -1 do
parent_table_rev[y] = parent_table[z]
y=y+1 z=z-1
end
parent_table = parent_table_rev
-- end reverse parent table
-- create path string from parent_table
source_parent_name = MyHWLibraryItem.Path_String_From_Parent_Table( parent_table )
local name_and_parent_display = "\"" .. source_name .. "\" \n Parent(s) = " .. source_parent_name .."\n"
if source:type() == 'LrFolder' then
smart_coll_report = ""
-- ********* CREATE COLLECTION SETS and SMART COLLECTIONS *****************
-- create collection set hierarchy
for i = 1, #parent_table do
MyHWLibraryItem.outputToLog( "STEP = " .. i )
if i==1 then -- root
--do nothing
else
--create collection set
if parent_table[i-1] == false then
MyHWLibraryItem.outputToLog( "parent table = FALSE." )
parent = nil
else
--parent = parent_table[i-1]
MyHWLibraryItem.outputToLog( "parent name = " .. parent:getName() )
end
catalog:withWriteAccessDo( "Create Collection Set 2" , function( context )
parent = catalog:createCollectionSet( parent_table[i]:getName() , parent , true )
end)
MyHWLibraryItem.outputToLog( "collection set created. Name = " .. parent:getName() )
end
end
catalog:withWriteAccessDo( "Create Collection Set 1" , function( context )
-- Create new collection set
collection_set = catalog:createCollectionSet( source_name , parent )
if collection_set ~= nil then
smart_coll_report = smart_coll_report .. " Collection Set '"..source_name.."' created OK\n"
else
smart_coll_report = smart_coll_report .. " Collection Set '"..source_name.."' ALREADY EXISTS!\n"
end
-- create Smart Collections for ratings
if collection_set ~= nil then
local smart_collection_set_1 = catalog:createSmartCollection( "<2", {{criteria="rating",operation="<",value=2},{criteria="folder",operati on="all",value=source_name}}, collection_set )
local smart_collection_set_2 = catalog:createSmartCollection( "2+", {{criteria="rating",operation=">=",value=2},{criteria="folder",operat ion="all",value=source_name}}, collection_set )
local smart_collection_set_3 = catalog:createSmartCollection( "3+", {{criteria="rating",operation=">=",value=3},{criteria="folder",operat ion="all",value=source_name}}, collection_set )
local smart_collection_set_4 = catalog:createSmartCollection( "4+", {{criteria="rating",operation=">=",value=4},{criteria="folder",operat ion="all",value=source_name}}, collection_set )
if smart_collection_set_1 ~= nil then smart_coll_report = smart_coll_report .. " Smart Collection '<2' created OK\n" end
if smart_collection_set_2 ~= nil then smart_coll_report = smart_coll_report .. " Smart Collection '2+' created OK\n" end
if smart_collection_set_3 ~= nil then smart_coll_report = smart_coll_report .. " Smart Collection '3+' created OK\n" end
if smart_collection_set_4 ~= nil then smart_coll_report = smart_coll_report .. " Smart Collection '4+' created OK\n" end
end
end ) -- end of catalog:withWriteAccessDo FUNCTION
-- ********************************************************************* ***
-- create display string
source_type = source_type .. i .. " Folder: " .. name_and_parent_display .. smart_coll_report .. "\n"
elseif source:type() == 'LrCollectionSet' then
smart_coll_report = ""
source_type = source_type .. i .. " Collection Set: " .. name_and_parent_display .. smart_coll_report .. "\n"
elseif source:type() == 'LrCollection' then
smart_coll_report = ""
source_type = source_type .. i .. " Collection: " .. name_and_parent_display .. smart_coll_report .. "\n"
elseif source:type() == 'LrPublishedCollection' then
smart_coll_report = ""
source_type = source_type .. i .. " Published Collection: " .. name_and_parent_display .. smart_coll_report .. "\n"
elseif source:type() == 'LrPublishedCollectionSet' then
smart_coll_report = ""
source_type = source_type .. i .. " Published Collection Set: " .. name_and_parent_display .. smart_coll_report .. "\n"
end
end
display_string = source_type
-- create view hierarchy for dialog box
local f = LrView.osFactory() -- get the view factory object
local c = f:column { -- the root node
f:row { -- Display contents of active sources Array
f:static_text {
text_color = LrColor( 0, 0, 0 ),
alignment = "Left",
-- add title with binding later
title = display_string
},
},
}
local result = LrDialogs.presentModalDialog(
{
title = "Create Rating Smart Collection Sets",
contents = c, -- the view hierarchy we defined
}
)
end )
end
LrTasks.startAsyncTask(function()
MyHWLibraryItem.showCustomDialog()
end)
Hmmm interesting. Works fine here ....
What should happen is for any sources you select the routine will create an identical structure of collection sets and smart collections based upon ratings of pics in those sources as:
-- create Smart Collections for ratings
if collection_set ~= nil then
local smart_collection_set_1 = catalog:createSmartCollection( "<2", {{criteria="rating",operation="<",value=2},{criteria="folder",operati on="all",value=source_name}}, collection_set )
local smart_collection_set_2 = catalog:createSmartCollection( "2+", {{criteria="rating",operation=">=",value=2},{criteria="folder",operat ion="all",value=source_name}}, collection_set )
local smart_collection_set_3 = catalog:createSmartCollection( "3+", {{criteria="rating",operation=">=",value=3},{criteria="folder",operat ion="all",value=source_name}}, collection_set )
local smart_collection_set_4 = catalog:createSmartCollection( "4+", {{criteria="rating",operation=">=",value=4},{criteria="folder",operat ion="all",value=source_name}}, collection_set )
So for the red ticked slected folder source Dubau LR then the red encircled collection set/smart collection structure is created:
Thanks,
I tried it using a folder source and it worked. Previous test was using a collection as source.
Unfortunately, for me, the smart collection folder criteria casts too wide a net (sometimes includes photos from other folders). Same will be true for other users. This is why I changed FolderCollections to use dumb collections instead - there is no way to reliably define smart collection criteria to limit photos to those of a single corresponding folder, or at least no way that John Ellis or I could figure.
Otherwise, nice job!
Cheers,
Rob
Thanks Rob. ![]()
[i]"there is no way to reliably define smart collection criteria to limit photos to those of a single corresponding folder,"[/i]
In my working catalogues ( not the test catalogue shown in the screenshots ) all my folders have unique names starting with the Date and then a 2 digit number ( for multiple shoots on the same day ). then a decription i.e yyyy-mm-dd_xx_description
And then using smart collections which with a rule: "Folder -> Contains All - > yyyy-mm-dd_xx_description" you always ensure you only get photos from that folder
North America
Europe, Middle East and Africa
Asia Pacific