1

I am trying to add a procedure to pop-up a modal dialog inside a plug-in. Its purpose is to query a response at designated steps within the control-flow of the plug-in (not just acquire parameters at its start).

I have tried using gtk - I get a dialog but it is asynchronous - the plugin continues execution. It needs to operate as a synchronous function.

I have tried registering a plugin in order to take advantage of the gimpfu start-up dialogue for same. By itself, it works; it shows up in the procedural db when queried. But I never seem to be able to actually invoke it from within another plug-in - its either an execution error or wrong number of arguments no matter how many permutations I try.

[Reason behind all of this nonsense: I have written a lot of extension Python scripts for PaintShopPro. I have written a App package (with App.Do, App.Constants, Environment and the like that lets me begin to port those scripts to GIMP -- yes it is perverse, and yes sometimes the code just has to be rewritten, but for a lot of what I actual use in the PSP.API it is sufficient.

However, debugging and writing the module rhymes with witch. So. I am trying to add emulation of psp's "SetExecutionMode" (ie interactive). If set, the intended behavior is that the App.Do() method will "pause" after/before it runs the applicable psp emulation code by popping up a simple message dialog.]

Chrys G
  • 99
  • 10
  • *"its either an execution error or wrong number of arguments no matter how many permutations I try"* Can you add to your question a small plugin and the code you use to call it? – xenoid Sep 27 '19 at 20:58
  • I have given up on the attempt to register a procedure and then invoke it to recursively invoke the gimpfu start-up dialogue for a registered plug-in. -- Rewrote the gtk dialogue approach and it is working. Will post the code after I clean it up and make it more terse. – Chrys G Sep 30 '19 at 05:40

1 Answers1

1

A simple modal dialogue within a gimp python-fu plug-in can be implemented via gtk's Dialog interface, specifically gtk.MessageDialog. A generic dialog can be created via

queryDialogue = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT \ gtk.MESSAGE_QUESTION, \ gtk.BUTTONS_OK_CANCEL, "")

Once the dialog has been shown, a synchronous response may be obtained from it

queryDialogue.show() response = queryDialogue.run() queryDialogue.hide()

The above assumes that the dialog is not created and thence destroyed after each use.

In the use case (mentioned in the question) of a modal dialog to manage single stepping through a pspScript in gimp via an App emulator package, the dialogue message contents need to be customized for each use. [Hence, the "" for the message argument in the Constructor. [more below]]

In addition, the emulator must be able to accept a [cancel] response to 'get out of Dodge' - ie quit the entire plug-in (gracefully). I could not find a gimpfu interface for the latter, (and do not want to kill the app entirely via gimp.exit()). Hence, this is accomplished by raising a custom Exception class [appTerminate] within the App pkg and catching the exception in the outer-most scope of the plugin. When caught, then, the plug-in returns (exits).[App.Do() can not return a value to indicate continue/exit/etc, because the pspScripts are to be included verbatim.]

The following is an abbreviated skeleton of the solution -

  • a plug-in incorporating (in part) a pspScript
  • the App.py pkg supplying the environment and App.Do() to support the pspScript
  • a Map.py pkg supporting how pspScripts use dot-notation for parameters

App.py demonstrates creation, customization and use of a modal dialog - App.doContinue() displays the dialogue illustrating how it can be customized on each use. App._parse() parses the pspScript (excerpt showing how it determines to start/stop single-step via the dialogue) App._exec() implements the pspScript commands (excerpt showing how it creates the dialogue, identifies the message widget for later customization, and starts/stops its use)

# App.py   (abbreviated)
#
import gimp
import gtk
import Map         # see https://stackoverflow.com/questions/2352181/how-to-   use-a-dot-to-access-members-of-dictionary
from Map import *
pdb = gimp.pdb

isDialogueAvailable = False
queryDialogue = None
queryMessage  = None

Environment = Map({'executionMode' : 1 })

_AutoActionMode   = Map({'Match' : 0})
_ExecutionMode    = Map({'Default' : 0}, Silent=1, Interactive=2)
Constants = Map({'AutoActionMode' : _AutoActionMode},       ExecutionMode=_ExecutionMode ) # etc... 

class appTerminate(Exception): pass

def Do(eNvironment, procedureName, options = {}):
    global appTerminate
    img = gimp.image_list()[0]
    lyr = pdb.gimp_image_get_active_layer(img)

    parsed = _parse(img, lyr, procedureName, options)
    if eNvironment.executionMode == Constants.ExecutionMode.Interactive:
        resp = doContinue(procedureName, parsed.detail)
        if resp == -5:          # OK
            print procedureName # log to stdout
            if parsed.valid:
                if parsed.isvalid:
                    _exec(img, lyr, procedureName, options, parsed, eNvironment)
                else:
                    print "invalid args"
            else:
                print "invalid procedure"
        elif resp == -6:        # CANCEL
            raise appTerminate, "script cancelled"
            pass  # terminate plugin
        else:
            print procedureName + " skipped"
            pass  # skip execution, continue
    else:
        _exec(img, lyr, procedureName, options, parsed, eNvironment)
    return

def doContinue(procedureName, details):
    global queryMessage, querySkip, queryDialogue
    # - customize the dialog -
    if details == "":
        msg  = "About to execute procedure \n    "+procedureName+ "\n\nContinue?"
    else:
        msg  = "About to execute procedure \n    "+procedureName+ "\n\nDetails - \n" + details +"\n\nContinue?"
    queryMessage.set_text(msg)
    queryDialogue.show()
    resp = queryDialogue.run()       # get modal response
    queryDialogue.hide()
    return resp

def _parse(img, lyr, procedureName, options):
    # validate and interpret App.Do options' semantics vz gimp
    if procedureName == "Selection":
        isValid=True
        # ...
        # parsed = Map({'valid' : True}, isvalid=True, start=Start, width=Width, height=Height, channelOP=ChannelOP ...
        # /Selection
    # ...
    elif procedureName == "SetExecutionMode":
        generalOptions = options['GeneralSettings']
        newMode = generalOptions['ExecutionMode']
        if newMode == Constants.ExecutionMode.Interactive:
            msg = "set mode interactive/single-step"
        else:
            msg = "set mode silent/run"
        parsed = Map({'valid' : True}, isvalid=True, detail=msg, mode=newMode)
        # /SetExecutionMode
    else:
        parsed = Map({'valid' : False})

    return parsed

def _exec(img, lyr, procedureName, options, o, eNvironment):
    global isDialogueAvailable, queryMessage, queryDialogue
    #
    try:
        # -------------------------------------------------------------------------------------------------------------------     
        if procedureName == "Selection":
            # pdb.gimp_rect_select(img, o.start[0], o.start[1], o.width, o.height, o.channelOP, ...
            # /Selection
        # ...
        elif procedureName == "SetExecutionMode":
            generalOptions = options['GeneralSettings']
            eNvironment.executionMode = generalOptions['ExecutionMode']
            if eNvironment.executionMode == Constants.ExecutionMode.Interactive:
                if isDialogueAvailable:
                    queryDialogue.destroy()   # then clean-up and refresh

                isDialogueAvailable = True
                queryDialogue = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "")
                queryDialogue.set_title("psp/APP.Do Emulator")
                queryDialogue.set_size_request(450, 180)

                aqdContent = queryDialogue.children()[0]
                aqdHeader  = aqdContent.children()[0]
                aqdMsgBox  = aqdHeader.children()[1]
                aqdMessage = aqdMsgBox.children()[0]
                queryMessage = aqdMessage

            else:
                if isDialogueAvailable:
                    queryDialogue.destroy()
                    isDialogueAvailable = False
            # /SetExecutionMode

        else:   # should not get here (should have been screened by parse)
            raise AssertionError,  "unimplemented PSP procedure: " + procedureName
    except:
        raise AssertionError, "App.Do("+procedureName+") generated an exception:\n" + sys.exc_info()
    return 

A skeleton of the plug-in itself. This illustrates incorporating a pspScript which includes a request for single-step/interactive execution mode, and thus the dialogues. It catches the terminate exception raised via the dialogue, and then terminates.

def generateWebImageSet(dasImage, dasLayer, title, mode):
    try:
        img = dasImage.duplicate()
        # ...
        bkg   = img.layers[-1]
        frameWidth = 52
        start = bkg.offsets
        end   = (start[0]+bkg.width, start[1]+frameWidth)

        # pspScript: (snippet included verbatim)

        # SetExecutionMode / begin interactive single-step through pspScript
        App.Do( Environment, 'SetExecutionMode', {
                            'GeneralSettings': {
                                'ExecutionMode': App.Constants.ExecutionMode.Interactive
                                }
                            })
        # Selection
        App.Do( Environment, 'Selection', {
                    'General' : {
                        'Mode' : 'Replace',
                        'Antialias' : False,
                        'Feather'   : 0
                        },
                    'Start': start,
                    'End':   end
                    })      
        # Promote           
        App.Do( Environment, 'SelectPromote' )
        # und_so_weiter  ...

    except App.appTerminate:
        raise AssertionError, "script cancelled"
    # /generateWebImageSet

# _generateFloatingCanvasSetWeb.register -----------------------------------------
#               
def generateFloatingCanvasSetWeb(dasImage, dasLayer, title):
    mode="FCSW" 
    generateWebImageSet(dasImage, dasLayer, title, mode)

register(
        "generateFloatingCanvasSetWeb",
        "Generate Floating- Frame GW Canvas Image Set for Web Page",
        "Generate Floating- Frame GW Canvas Image Set for Web Page",
        "C G",
        "C G",
        "2019",
        "<Image>/Image/Generate Web Imagesets/Floating-Frame Gallery-Wrapped Canvas Imageset...",
        "*",
        [
          ( PF_STRING, "title", "title", "")
        ],
        [],
        generateFloatingCanvasSetWeb)

main()

I realize that this may seem like a lot of work just to be able to include some pspScripts in a gimp plug-in, and to be able to single-step through the emulation. But we are talking about maybe 10K lines of scripts (and multiple scripts). However, if any of this helps anyone else with dialogues inside plug-ins, etc., so much the better.

Chrys G
  • 99
  • 10