Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Python scripts: handling PF_OPTION and PF_RADIO choices by name
#1
Handling PF_OPTION in scripts is always a bit of a problem when there are many of them or long list of choices. Here is a technique that has several advantages:
  • No choice is ever referenced by its actual integer equivalent.
  • The list of choices can be modified at will (insertions, reorders) without having to hunt the code for changes
  • Choices and their labels cannot be "desynchronized" as would happen with parallel lists
  • Code remains short
All is needed is this small function in the script (it uses the namedtuple from the standard collections module which is in all Python runtimes):

Code:
from collections import namedtuple

def createOptions(name,pairs):
    # namedtuple('FooType',['OPTION1',...,'OPTIONn','labels','labelTuples']
    optsclass=namedtuple(name+'Type',[symbol for symbol,label in pairs]+['labels','labelTuples'])
    # FooType(0,..,n-1,['Option 1',...,'Option N'],[('Option 1',0),...,('Option N',n-1)])
    opts=optsclass(*(
                    range(len(pairs))
                    +[[label for symbol,label in pairs]]
                    +[[(label,i) for i,(symbol,label) in enumerate(pairs)]]
                    ))
    return opts

Then you define your options as a list of  (name,label) tuples, where name is how you refer to that option in the code, and
label is how it appears in the PF_OPTION or PF_RADIO widget:

Code:
[('NONE','None'),('LINKED','Linked layers'),('TEXT','Text layers')]

To create the object that will carry the options, you call the function above, giving it a name, and the list of tuples, and keep the result is a variable:

Code:
MergeOptions=createOptions('Merge',[('NONE','None'),('LINKED','Linked layers'),('TEXT','Text layers')])

The name as passed to the function is not too important, it just needs to be unique among your various set of options. What really counts is the name of the variable in which you keep the object.

In the resulting variable:
  • The name of each tuple in the list is now an attribute: MergeOptions.NONE, MergeOptions.TEXT, MergeOptions.LINKED
  • Thees attributes have the value of their index in the list: MergeOptions.NONE==0, MergeOptions.TEXT==1, MergeOptions.LINKED==2
  • A labels attribute contains the list of labels: MergeOptions.labels==['None','Linked layers','Text layers']
  • A labelTuples attribute contains the list of (labels,value) tuples: MergeOptions.labelsTuples==[('None',0),('Linked layers',1),('Text layers',2)]
Given this, you can define your PF_OPTION like this:

Code:
(PF_OPTION, 'mergeLayers', 'Merge layers',MergeOptions.TEXT,MergeOptions.labels)

or your PF_RADIO like this:

Code:
(PF_RADIO, 'mergeLayers', 'Merge layers',MergeOptions.TEXT,MergeOptions.labelTuples)

To test an option in the code you just use its name:

Code:
if merge==MergeOptions.TEXT:

or even:

Code:
if merge in [MergeOptions.TEXT,MergeOptions.LINKED]:

Happy coding.
Reply
#2
Thanks!
Reply
#3
I'm rather new to python.

Can you please give me an example how to use this convention with PF_RADIO?

I suspect there is a way to use the existing data structure to give PF_RADIO the pairs it needs the way it wants them, but I don't have a clue how to do it.

Thanks.
Reply
#4
(08-15-2018, 10:19 PM)lordcayleth Wrote: I'm rather new to python.

Can you please give me an example how to use this convention with PF_RADIO?

I suspect there is a way to use the existing data structure to give PF_RADIO the pairs it needs the way it wants them, but I don't have a clue how to do it.

Thanks.

For radio button the function to create the symbols and labels is a bit different:

For instance, taking $random_script:
Code:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from gimpfu import *


from collections import namedtuple

def createButtons(name,pairs):
    optsclass=namedtuple(name+'Type',[symbol for symbol,label in pairs]+['labels'])
    opts=optsclass(*(range(len(pairs))+[[(label,i) for i,(symbol,label) in enumerate(pairs)]]))
    return opts

Directions=createButtons('Directions',[('UP','Up'),('DOWN','Down'),('LEFT','Left'),('RIGHT','Right')])

def celenicorn_auto_mirror(image, drawable, direction = Directions.UP) :
    image.undo_group_start()
    width = image.width
    height = image.height
    newLayer = drawable.copy()
    image.add_layer(newLayer, 0)

    dirParms={
        Directions.UP   : (1, width, height * 2, 0, height, 0, 0),
        Directions.DOWN : (1, width, height * 2, 0, 0, 0, height),
        Directions.LEFT : (0, width * 2, height, width, 0, 0, 0),
        Directions.RIGHT: (0, width * 2, height, 0, 0, width, 0)
        }

    vhflip, sizex, sizey, ofsx, ofsy, movx, movy=dirParms[direction]
    pdb.gimp_image_resize(image, sizex, sizey, ofsx, ofsy)
    pdb.gimp_drawable_transform_flip_simple(newLayer, vhflip, True, 0, False)
    pdb.gimp_layer_set_offsets(newLayer, movx, movy)
    image.flatten()

    image.undo_group_end()
    
register(
        "celenicorn_auto_mirror",
        "Mirror the image in a specific direction.",
        "Mirror the image in a specific direction.",
        "Arlo Horner",
        "Arlo Horner",
        "2018",
        "Cosmic Auto Mirror",
        "*",
        [
            (PF_IMAGE,   'image', 'Input image', None),
            (PF_DRAWABLE,'layer', 'Input layer', None),
            (PF_RADIO, "direction", "Mirror towards which direction: ", 0,Directions.labels)
        ],
        [],
        celenicorn_auto_mirror,
        menu="<Image>/Celenicorn")
main()

Independently of the use of the Directions object, you will note that the registration is a bit different. There are now explicit Image/Drawable parameters. When the filter is called as usual, Gimp automatically fills them with the current image and drawable and the dialog only shows widgets for the 3rd parameter and next. But if you use Filters>Re-run|Re-show (that now work...) these parameters have matching widgets in the dialog.

EDIT: updated initial post with function that creates an object usable with both PF_OPTIONS and PF_RADIO.

Incidentally, PF_RADIO is seldom used because it takes a lot of vertical space.
Reply
#5
(08-15-2018, 11:17 PM)Ofnuts Wrote:
Code:
def createButtons(name,pairs):
    optsclass=namedtuple(name+'Type',[symbol for symbol,label in pairs]+['labels'])
    opts=optsclass(*(range(len(pairs))+[[(label,i) for i,(symbol,label) in enumerate(pairs)]]))
    return opts

You've been super helpful, and I thank you a lot.  I have a new respect for the power and flexibility of python, too.

I only have one more question. What would I look up to gain a better understanding of how the for/in syntax works in this code?  I'm quite curious about it, but I don't even know what terminology it would fall under.
Reply
#6
Generating a list by [expression(something) for something in some_iteration] is called a "comprehension". Tremendously useful.

If you want to really "grok" python, there are several Youtube videos with Ned Batchelder. He goes into some very important (and often overlooked) concepts.

Python-forum.io is also a a good place to ask general Python questions.
Reply


Forum Jump: