1#   Gimp-Python - allows the writing of GIMP plug-ins in Python.
2#   Copyright (C) 1997  James Henstridge <james@daa.com.au>
3#
4#   This program is free software: you can redistribute it and/or modify
5#   it under the terms of the GNU General Public License as published by
6#   the Free Software Foundation; either version 3 of the License, or
7#   (at your option) any later version.
8#
9#   This program is distributed in the hope that it will be useful,
10#   but WITHOUT ANY WARRANTY; without even the implied warranty of
11#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#   GNU General Public License for more details.
13#
14#   You should have received a copy of the GNU General Public License
15#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17"""Simple interface for writing GIMP plug-ins in Python.
18
19Instead of worrying about all the user interaction, saving last used
20values and everything, the gimpfu module can take care of it for you.
21It provides a simple register() function that will register your
22plug-in if needed, and cause your plug-in function to be called when
23needed.
24
25Gimpfu will also handle showing a user interface for editing plug-in
26parameters if the plug-in is called interactively, and will also save
27the last used parameters, so the RUN_WITH_LAST_VALUES run_type will
28work correctly.  It will also make sure that the displays are flushed
29on completion if the plug-in was run interactively.
30
31When registering the plug-in, you do not need to worry about
32specifying the run_type parameter.
33
34A typical gimpfu plug-in would look like this:
35  from gimpfu import *
36
37  def plugin_func(image, drawable, args):
38              # do what plugins do best
39  register(
40              "plugin_func",
41              "blurb",
42              "help message",
43              "author",
44              "copyright",
45              "year",
46              "My plug-in",
47              "*",
48              [
49                  (PF_IMAGE, "image", "Input image", None),
50                  (PF_DRAWABLE, "drawable", "Input drawable", None),
51                  (PF_STRING, "arg", "The argument", "default-value")
52              ],
53              [],
54              plugin_func, menu="<Image>/Somewhere")
55  main()
56
57The call to "from gimpfu import *" will import all the gimp constants
58into the plug-in namespace, and also import the symbols gimp, pdb,
59register and main.  This should be just about all any plug-in needs.
60
61You can use any of the PF_* constants below as parameter types, and an
62appropriate user interface element will be displayed when the plug-in
63is run in interactive mode.  Note that the the PF_SPINNER and
64PF_SLIDER types expect a fifth element in their description tuple -- a
653-tuple of the form (lower,upper,step), which defines the limits for
66the slider or spinner.
67
68If want to localize your plug-in, add an optional domain parameter to
69the register call. It can be the name of the translation domain or a
70tuple that consists of the translation domain and the directory where
71the translations are installed.
72"""
73
74import string as _string
75import math
76import gimp
77import gimpcolor
78from gimpenums import *
79pdb = gimp.pdb
80
81import gettext
82t = gettext.translation("gimp20-python", gimp.locale_directory, fallback=True)
83_ = t.ugettext
84
85class error(RuntimeError): pass
86class CancelError(RuntimeError): pass
87
88PF_INT8        = PDB_INT8
89PF_INT16       = PDB_INT16
90PF_INT32       = PDB_INT32
91PF_INT         = PF_INT32
92PF_FLOAT       = PDB_FLOAT
93PF_STRING      = PDB_STRING
94PF_VALUE       = PF_STRING
95#PF_INT8ARRAY   = PDB_INT8ARRAY
96#PF_INT16ARRAY  = PDB_INT16ARRAY
97#PF_INT32ARRAY  = PDB_INT32ARRAY
98#PF_INTARRAY    = PF_INT32ARRAY
99#PF_FLOATARRAY  = PDB_FLOATARRAY
100#PF_STRINGARRAY = PDB_STRINGARRAY
101PF_COLOR       = PDB_COLOR
102PF_COLOUR      = PF_COLOR
103PF_ITEM        = PDB_ITEM
104PF_DISPLAY     = PDB_DISPLAY
105PF_IMAGE       = PDB_IMAGE
106PF_LAYER       = PDB_LAYER
107PF_CHANNEL     = PDB_CHANNEL
108PF_DRAWABLE    = PDB_DRAWABLE
109PF_VECTORS     = PDB_VECTORS
110#PF_SELECTION   = PDB_SELECTION
111#PF_BOUNDARY    = PDB_BOUNDARY
112#PF_PATH        = PDB_PATH
113#PF_STATUS      = PDB_STATUS
114
115PF_TOGGLE      = 1000
116PF_BOOL        = PF_TOGGLE
117PF_SLIDER      = 1001
118PF_SPINNER     = 1002
119PF_ADJUSTMENT  = PF_SPINNER
120
121PF_FONT        = 1003
122PF_FILE        = 1004
123PF_BRUSH       = 1005
124PF_PATTERN     = 1006
125PF_GRADIENT    = 1007
126PF_RADIO       = 1008
127PF_TEXT        = 1009
128PF_PALETTE     = 1010
129PF_FILENAME    = 1011
130PF_DIRNAME     = 1012
131PF_OPTION      = 1013
132
133_type_mapping = {
134    PF_INT8        : PDB_INT8,
135    PF_INT16       : PDB_INT16,
136    PF_INT32       : PDB_INT32,
137    PF_FLOAT       : PDB_FLOAT,
138    PF_STRING      : PDB_STRING,
139    #PF_INT8ARRAY   : PDB_INT8ARRAY,
140    #PF_INT16ARRAY  : PDB_INT16ARRAY,
141    #PF_INT32ARRAY  : PDB_INT32ARRAY,
142    #PF_FLOATARRAY  : PDB_FLOATARRAY,
143    #PF_STRINGARRAY : PDB_STRINGARRAY,
144    PF_COLOR       : PDB_COLOR,
145    PF_ITEM        : PDB_ITEM,
146    PF_DISPLAY     : PDB_DISPLAY,
147    PF_IMAGE       : PDB_IMAGE,
148    PF_LAYER       : PDB_LAYER,
149    PF_CHANNEL     : PDB_CHANNEL,
150    PF_DRAWABLE    : PDB_DRAWABLE,
151    PF_VECTORS     : PDB_VECTORS,
152
153    PF_TOGGLE      : PDB_INT32,
154    PF_SLIDER      : PDB_FLOAT,
155    PF_SPINNER     : PDB_INT32,
156
157    PF_FONT        : PDB_STRING,
158    PF_FILE        : PDB_STRING,
159    PF_BRUSH       : PDB_STRING,
160    PF_PATTERN     : PDB_STRING,
161    PF_GRADIENT    : PDB_STRING,
162    PF_RADIO       : PDB_STRING,
163    PF_TEXT        : PDB_STRING,
164    PF_PALETTE     : PDB_STRING,
165    PF_FILENAME    : PDB_STRING,
166    PF_DIRNAME     : PDB_STRING,
167    PF_OPTION      : PDB_INT32,
168}
169
170_obj_mapping = {
171    PF_INT8        : int,
172    PF_INT16       : int,
173    PF_INT32       : int,
174    PF_FLOAT       : float,
175    PF_STRING      : str,
176    #PF_INT8ARRAY   : list,
177    #PF_INT16ARRAY  : list,
178    #PF_INT32ARRAY  : list,
179    #PF_FLOATARRAY  : list,
180    #PF_STRINGARRAY : list,
181    PF_COLOR       : gimpcolor.RGB,
182    PF_ITEM        : int,
183    PF_DISPLAY     : gimp.Display,
184    PF_IMAGE       : gimp.Image,
185    PF_LAYER       : gimp.Layer,
186    PF_CHANNEL     : gimp.Channel,
187    PF_DRAWABLE    : gimp.Drawable,
188    PF_VECTORS     : gimp.Vectors,
189
190    PF_TOGGLE      : bool,
191    PF_SLIDER      : float,
192    PF_SPINNER     : int,
193
194    PF_FONT        : str,
195    PF_FILE        : str,
196    PF_BRUSH       : str,
197    PF_PATTERN     : str,
198    PF_GRADIENT    : str,
199    PF_RADIO       : str,
200    PF_TEXT        : str,
201    PF_PALETTE     : str,
202    PF_FILENAME    : str,
203    PF_DIRNAME     : str,
204    PF_OPTION      : int,
205}
206
207_registered_plugins_ = {}
208
209def register(proc_name, blurb, help, author, copyright, date, label,
210             imagetypes, params, results, function,
211             menu=None, domain=None, on_query=None, on_run=None):
212    """This is called to register a new plug-in."""
213
214    # First perform some sanity checks on the data
215    def letterCheck(str):
216        allowed = _string.letters + _string.digits + "_" + "-"
217        for ch in str:
218            if not ch in allowed:
219                return 0
220        else:
221            return 1
222
223    if not letterCheck(proc_name):
224        raise error, "procedure name contains illegal characters"
225
226    for ent in params:
227        if len(ent) < 4:
228            raise error, ("parameter definition must contain at least 4 "
229                          "elements (%s given: %s)" % (len(ent), ent))
230
231        if type(ent[0]) != int:
232            raise error, "parameter types must be integers"
233
234        if not letterCheck(ent[1]):
235            raise error, "parameter name contains illegal characters"
236
237    for ent in results:
238        if len(ent) < 3:
239            raise error, ("result definition must contain at least 3 elements "
240                          "(%s given: %s)" % (len(ent), ent))
241
242        if type(ent[0]) != type(42):
243            raise error, "result types must be integers"
244
245        if not letterCheck(ent[1]):
246            raise error, "result name contains illegal characters"
247
248    plugin_type = PLUGIN
249
250    if (not proc_name.startswith("python-") and
251        not proc_name.startswith("python_") and
252        not proc_name.startswith("extension-") and
253        not proc_name.startswith("extension_") and
254        not proc_name.startswith("plug-in-") and
255        not proc_name.startswith("plug_in_") and
256        not proc_name.startswith("file-") and
257        not proc_name.startswith("file_")):
258           proc_name = "python-fu-" + proc_name
259
260    # if menu is not given, derive it from label
261    need_compat_params = False
262    if menu is None and label:
263        fields = label.split("/")
264        if fields:
265            label = fields.pop()
266            menu = "/".join(fields)
267            need_compat_params = True
268
269            import warnings
270            message = ("%s: passing the full menu path for the menu label is "
271                       "deprecated, use the 'menu' parameter instead"
272                       % (proc_name))
273            warnings.warn(message, DeprecationWarning, 3)
274
275        if need_compat_params and plugin_type == PLUGIN:
276            file_params = [(PDB_STRING, "filename", "The name of the file", ""),
277                           (PDB_STRING, "raw-filename", "The name of the file", "")]
278
279            if menu is None:
280                pass
281            elif menu.startswith("<Load>"):
282                params[0:0] = file_params
283            elif menu.startswith("<Image>") or menu.startswith("<Save>"):
284                params.insert(0, (PDB_IMAGE, "image", "Input image", None))
285                params.insert(1, (PDB_DRAWABLE, "drawable", "Input drawable", None))
286                if menu.startswith("<Save>"):
287                    params[2:2] = file_params
288
289    _registered_plugins_[proc_name] = (blurb, help, author, copyright,
290                                       date, label, imagetypes,
291                                       plugin_type, params, results,
292                                       function, menu, domain,
293                                       on_query, on_run)
294
295def _query():
296    for plugin in _registered_plugins_.keys():
297        (blurb, help, author, copyright, date,
298         label, imagetypes, plugin_type,
299         params, results, function, menu, domain,
300         on_query, on_run) = _registered_plugins_[plugin]
301
302        def make_params(params):
303            return [(_type_mapping[x[0]],
304                     x[1],
305                     _string.replace(x[2], "_", "")) for x in params]
306
307        params = make_params(params)
308        # add the run mode argument ...
309        params.insert(0, (PDB_INT32, "run-mode",
310                                     "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }"))
311
312        results = make_params(results)
313
314        if domain:
315            try:
316                (domain, locale_dir) = domain
317                gimp.domain_register(domain, locale_dir)
318            except ValueError:
319                gimp.domain_register(domain)
320
321        gimp.install_procedure(plugin, blurb, help, author, copyright,
322                               date, label, imagetypes, plugin_type,
323                               params, results)
324
325        if menu:
326            gimp.menu_register(plugin, menu)
327        if on_query:
328            on_query()
329
330def _get_defaults(proc_name):
331    import gimpshelf
332
333    (blurb, help, author, copyright, date,
334     label, imagetypes, plugin_type,
335     params, results, function, menu, domain,
336     on_query, on_run) = _registered_plugins_[proc_name]
337
338    key = "python-fu-save--" + proc_name
339
340    if gimpshelf.shelf.has_key(key):
341        return gimpshelf.shelf[key]
342    else:
343        # return the default values
344        return [x[3] for x in params]
345
346def _set_defaults(proc_name, defaults):
347    import gimpshelf
348
349    key = "python-fu-save--" + proc_name
350    gimpshelf.shelf[key] = defaults
351
352def _interact(proc_name, start_params):
353    (blurb, help, author, copyright, date,
354     label, imagetypes, plugin_type,
355     params, results, function, menu, domain,
356     on_query, on_run) = _registered_plugins_[proc_name]
357
358    def run_script(run_params):
359        params = start_params + tuple(run_params)
360        _set_defaults(proc_name, params)
361        return apply(function, params)
362
363    params = params[len(start_params):]
364
365    # short circuit for no parameters ...
366    if len(params) == 0:
367         return run_script([])
368
369    import pygtk
370    pygtk.require('2.0')
371
372    import gimpui
373    import gtk
374#    import pango
375    gimpui.gimp_ui_init ()
376
377    defaults = _get_defaults(proc_name)
378    defaults = defaults[len(start_params):]
379
380    class EntryValueError(Exception):
381        pass
382
383    def warning_dialog(parent, primary, secondary=None):
384        dlg = gtk.MessageDialog(parent, gtk.DIALOG_DESTROY_WITH_PARENT,
385                                        gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE,
386                                        primary)
387        if secondary:
388            dlg.format_secondary_text(secondary)
389        dlg.run()
390        dlg.destroy()
391
392    def error_dialog(parent, proc_name):
393        import sys, traceback
394
395        exc_str = exc_only_str = _("Missing exception information")
396
397        try:
398            etype, value, tb = sys.exc_info()
399            exc_str = "".join(traceback.format_exception(etype, value, tb))
400            exc_only_str = "".join(traceback.format_exception_only(etype, value))
401        finally:
402            etype = value = tb = None
403
404        title = _("An error occurred running %s") % proc_name
405        dlg = gtk.MessageDialog(parent, gtk.DIALOG_DESTROY_WITH_PARENT,
406                                        gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
407                                        title)
408        dlg.format_secondary_text(exc_only_str)
409
410        alignment = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
411        alignment.set_padding(0, 0, 12, 12)
412        dlg.vbox.pack_start(alignment)
413        alignment.show()
414
415        expander = gtk.Expander(_("_More Information"));
416        expander.set_use_underline(True)
417        expander.set_spacing(6)
418        alignment.add(expander)
419        expander.show()
420
421        scrolled = gtk.ScrolledWindow()
422        scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
423        scrolled.set_size_request(-1, 200)
424        expander.add(scrolled)
425        scrolled.show()
426
427
428        label = gtk.Label(exc_str)
429        label.set_alignment(0.0, 0.0)
430        label.set_padding(6, 6)
431        label.set_selectable(True)
432        scrolled.add_with_viewport(label)
433        label.show()
434
435        def response(widget, id):
436            widget.destroy()
437
438        dlg.connect("response", response)
439        dlg.set_resizable(True)
440        dlg.show()
441
442    # define a mapping of param types to edit objects ...
443    class StringEntry(gtk.Entry):
444        def __init__(self, default=""):
445            gtk.Entry.__init__(self)
446            self.set_text(str(default))
447            self.set_activates_default(True)
448
449        def get_value(self):
450            return self.get_text()
451
452    class TextEntry(gtk.ScrolledWindow):
453        def __init__ (self, default=""):
454            gtk.ScrolledWindow.__init__(self)
455            self.set_shadow_type(gtk.SHADOW_IN)
456
457            self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
458            self.set_size_request(100, -1)
459
460            self.view = gtk.TextView()
461            self.add(self.view)
462            self.view.show()
463
464            self.buffer = self.view.get_buffer()
465
466            self.set_value(str(default))
467
468        def set_value(self, text):
469            self.buffer.set_text(text)
470
471        def get_value(self):
472            return self.buffer.get_text(self.buffer.get_start_iter(),
473                                        self.buffer.get_end_iter())
474
475    class IntEntry(StringEntry):
476        def get_value(self):
477            try:
478                return int(self.get_text())
479            except ValueError, e:
480                raise EntryValueError, e.args
481
482    class FloatEntry(StringEntry):
483            def get_value(self):
484                try:
485                    return float(self.get_text())
486                except ValueError, e:
487                    raise EntryValueError, e.args
488
489#    class ArrayEntry(StringEntry):
490#            def get_value(self):
491#                return eval(self.get_text(), {}, {})
492
493
494    def precision(step):
495        # calculate a reasonable precision from a given step size
496        if math.fabs(step) >= 1.0 or step == 0.0:
497            digits = 0
498        else:
499            digits = abs(math.floor(math.log10(math.fabs(step))));
500        if digits > 20:
501            digits = 20
502        return int(digits)
503
504    class SliderEntry(gtk.HScale):
505        # bounds is (upper, lower, step)
506        def __init__(self, default=0, bounds=(0, 100, 5)):
507            step = bounds[2]
508            self.adj = gtk.Adjustment(default, bounds[0], bounds[1],
509                                      step, 10 * step, 0)
510            gtk.HScale.__init__(self, self.adj)
511            self.set_digits(precision(step))
512
513        def get_value(self):
514            return self.adj.value
515
516    class SpinnerEntry(gtk.SpinButton):
517        # bounds is (upper, lower, step)
518        def __init__(self, default=0, bounds=(0, 100, 5)):
519            step = bounds[2]
520            self.adj = gtk.Adjustment(default, bounds[0], bounds[1],
521                                      step, 10 * step, 0)
522            gtk.SpinButton.__init__(self, self.adj, step, precision(step))
523
524    class ToggleEntry(gtk.ToggleButton):
525        def __init__(self, default=0):
526            gtk.ToggleButton.__init__(self)
527
528            self.label = gtk.Label(_("No"))
529            self.add(self.label)
530            self.label.show()
531
532            self.connect("toggled", self.changed)
533
534            self.set_active(default)
535
536        def changed(self, tog):
537            if tog.get_active():
538                self.label.set_text(_("Yes"))
539            else:
540                self.label.set_text(_("No"))
541
542        def get_value(self):
543            return self.get_active()
544
545    class RadioEntry(gtk.VBox):
546        def __init__(self, default=0, items=((_("Yes"), 1), (_("No"), 0))):
547            gtk.VBox.__init__(self, homogeneous=False, spacing=2)
548
549            button = None
550
551            for (label, value) in items:
552                button = gtk.RadioButton(button, label)
553                self.pack_start(button)
554                button.show()
555
556                button.connect("toggled", self.changed, value)
557
558                if value == default:
559                    button.set_active(True)
560                    self.active_value = value
561
562        def changed(self, radio, value):
563            if radio.get_active():
564                self.active_value = value
565
566        def get_value(self):
567            return self.active_value
568
569    class ComboEntry(gtk.ComboBox):
570        def __init__(self, default=0, items=()):
571            store = gtk.ListStore(str)
572            for item in items:
573                store.append([item])
574
575            gtk.ComboBox.__init__(self, model=store)
576
577            cell = gtk.CellRendererText()
578            self.pack_start(cell)
579            self.set_attributes(cell, text=0)
580
581            self.set_active(default)
582
583        def get_value(self):
584            return self.get_active()
585
586    def FileSelector(default="", title=None):
587        # FIXME: should this be os.path.separator?  If not, perhaps explain why?
588        if default and default.endswith("/"):
589            if default == "/": default = ""
590            return DirnameSelector(default)
591        else:
592            return FilenameSelector(default, title=title, save_mode=False)
593
594    class FilenameSelector(gtk.HBox):
595        #gimpfu.FileChooserButton
596        def __init__(self, default, save_mode=True, title=None):
597            super(FilenameSelector, self).__init__()
598            if not title:
599                self.title = _("Python-Fu File Selection")
600            else:
601                self.title = title
602            self.save_mode = save_mode
603            box = self
604            self.entry = gtk.Entry()
605            image = gtk.Image()
606            image.set_from_stock(gtk.STOCK_FILE, gtk.ICON_SIZE_BUTTON)
607            self.button = gtk.Button()
608            self.button.set_image(image)
609            box.pack_start(self.entry)
610            box.pack_start(self.button, expand=False)
611            self.button.connect("clicked", self.pick_file)
612            if default:
613                self.entry.set_text(default)
614
615        def show(self):
616            super(FilenameSelector, self).show()
617            self.button.show()
618            self.entry.show()
619
620        def pick_file(self, widget):
621            entry = self.entry
622            dialog = gtk.FileChooserDialog(
623                         title=self.title,
624                         action=(gtk.FILE_CHOOSER_ACTION_SAVE
625                                     if self.save_mode else
626                                 gtk.FILE_CHOOSER_ACTION_OPEN),
627                         buttons=(gtk.STOCK_CANCEL,
628                                gtk.RESPONSE_CANCEL,
629                                gtk.STOCK_SAVE
630                                     if self.save_mode else
631                                gtk.STOCK_OPEN,
632                                gtk.RESPONSE_OK)
633                        )
634            dialog.set_alternative_button_order ((gtk.RESPONSE_OK, gtk.RESPONSE_CANCEL))
635            dialog.show_all()
636            response = dialog.run()
637            if response == gtk.RESPONSE_OK:
638                entry.set_text(dialog.get_filename())
639            dialog.destroy()
640
641        def get_value(self):
642            return self.entry.get_text()
643
644
645    class DirnameSelector(gtk.FileChooserButton):
646        def __init__(self, default=""):
647            gtk.FileChooserButton.__init__(self,
648                                           _("Python-Fu Folder Selection"))
649            self.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
650            if default:
651                self.set_filename(default)
652
653        def get_value(self):
654            return self.get_filename()
655
656    _edit_mapping = {
657            PF_INT8        : IntEntry,
658            PF_INT16       : IntEntry,
659            PF_INT32       : IntEntry,
660            PF_FLOAT       : FloatEntry,
661            PF_STRING      : StringEntry,
662            #PF_INT8ARRAY   : ArrayEntry,
663            #PF_INT16ARRAY  : ArrayEntry,
664            #PF_INT32ARRAY  : ArrayEntry,
665            #PF_FLOATARRAY  : ArrayEntry,
666            #PF_STRINGARRAY : ArrayEntry,
667            PF_COLOR       : gimpui.ColorSelector,
668            PF_ITEM        : IntEntry,  # should handle differently ...
669            PF_IMAGE       : gimpui.ImageSelector,
670            PF_LAYER       : gimpui.LayerSelector,
671            PF_CHANNEL     : gimpui.ChannelSelector,
672            PF_DRAWABLE    : gimpui.DrawableSelector,
673            PF_VECTORS     : gimpui.VectorsSelector,
674
675            PF_TOGGLE      : ToggleEntry,
676            PF_SLIDER      : SliderEntry,
677            PF_SPINNER     : SpinnerEntry,
678            PF_RADIO       : RadioEntry,
679            PF_OPTION      : ComboEntry,
680
681            PF_FONT        : gimpui.FontSelector,
682            PF_FILE        : FileSelector,
683            PF_FILENAME    : FilenameSelector,
684            PF_DIRNAME     : DirnameSelector,
685            PF_BRUSH       : gimpui.BrushSelector,
686            PF_PATTERN     : gimpui.PatternSelector,
687            PF_GRADIENT    : gimpui.GradientSelector,
688            PF_PALETTE     : gimpui.PaletteSelector,
689            PF_TEXT        : TextEntry
690    }
691
692    if on_run:
693        on_run()
694
695    dialog = gimpui.Dialog(proc_name, "python-fu", None, 0, None, proc_name,
696                           (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
697                            gtk.STOCK_OK, gtk.RESPONSE_OK))
698
699    dialog.set_alternative_button_order((gtk.RESPONSE_OK, gtk.RESPONSE_CANCEL))
700
701    dialog.set_transient()
702
703    vbox = gtk.VBox(False, 12)
704    vbox.set_border_width(12)
705    dialog.vbox.pack_start(vbox)
706    vbox.show()
707
708    if blurb:
709        if domain:
710            try:
711                (domain, locale_dir) = domain
712                trans = gettext.translation(domain, locale_dir, fallback=True)
713            except ValueError:
714                trans = gettext.translation(domain, fallback=True)
715            blurb = trans.ugettext(blurb)
716        box = gimpui.HintBox(blurb)
717        vbox.pack_start(box, expand=False)
718        box.show()
719
720    table = gtk.Table(len(params), 2, False)
721    table.set_row_spacings(6)
722    table.set_col_spacings(6)
723    vbox.pack_start(table, expand=False)
724    table.show()
725
726    def response(dlg, id):
727        if id == gtk.RESPONSE_OK:
728            dlg.set_response_sensitive(gtk.RESPONSE_OK, False)
729            dlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
730
731            params = []
732
733            try:
734                for wid in edit_wids:
735                    params.append(wid.get_value())
736            except EntryValueError:
737                warning_dialog(dialog, _("Invalid input for '%s'") % wid.desc)
738            else:
739                try:
740                    dialog.res = run_script(params)
741                except CancelError:
742                    pass
743                except Exception:
744                    dlg.set_response_sensitive(gtk.RESPONSE_CANCEL, True)
745                    error_dialog(dialog, proc_name)
746                    raise
747
748        gtk.main_quit()
749
750    dialog.connect("response", response)
751
752    edit_wids = []
753    for i in range(len(params)):
754        pf_type = params[i][0]
755        name = params[i][1]
756        desc = params[i][2]
757        def_val = defaults[i]
758
759        label = gtk.Label(desc)
760        label.set_use_underline(True)
761        label.set_alignment(0.0, 0.5)
762        table.attach(label, 1, 2, i, i+1, xoptions=gtk.FILL)
763        label.show()
764
765        # Remove accelerator markers from tooltips
766        tooltip_text = desc.replace("_", "")
767
768        if pf_type in (PF_SPINNER, PF_SLIDER, PF_RADIO, PF_OPTION):
769            wid = _edit_mapping[pf_type](def_val, params[i][4])
770        elif pf_type in (PF_FILE, PF_FILENAME):
771            wid = _edit_mapping[pf_type](def_val, title= "%s - %s" %
772                                          (proc_name, tooltip_text))
773        else:
774            wid = _edit_mapping[pf_type](def_val)
775
776
777        label.set_mnemonic_widget(wid)
778
779        table.attach(wid, 2,3, i,i+1, yoptions=0)
780
781        if pf_type != PF_TEXT:
782            wid.set_tooltip_text(tooltip_text)
783        else:
784            # Attach tip to TextView, not to ScrolledWindow
785            wid.view.set_tooltip_text(tooltip_text)
786        wid.show()
787
788        wid.desc = desc
789        edit_wids.append(wid)
790
791    progress_vbox = gtk.VBox(False, 6)
792    vbox.pack_end(progress_vbox, expand=False)
793    progress_vbox.show()
794
795    progress = gimpui.ProgressBar()
796    progress_vbox.pack_start(progress)
797    progress.show()
798
799#    progress_label = gtk.Label()
800#    progress_label.set_alignment(0.0, 0.5)
801#    progress_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
802
803#    attrs = pango.AttrList()
804#    attrs.insert(pango.AttrStyle(pango.STYLE_ITALIC, 0, -1))
805#    progress_label.set_attributes(attrs)
806
807#    progress_vbox.pack_start(progress_label)
808#    progress_label.show()
809
810    dialog.show()
811
812    gtk.main()
813
814    if hasattr(dialog, "res"):
815        res = dialog.res
816        dialog.destroy()
817        return res
818    else:
819        dialog.destroy()
820        raise CancelError
821
822def _run(proc_name, params):
823    run_mode = params[0]
824    func = _registered_plugins_[proc_name][10]
825
826    if run_mode == RUN_NONINTERACTIVE:
827        return apply(func, params[1:])
828
829    script_params = _registered_plugins_[proc_name][8]
830
831    min_args = 0
832    if len(params) > 1:
833        for i in range(1, len(params)):
834            param_type = _obj_mapping[script_params[i - 1][0]]
835            if not isinstance(params[i], param_type):
836                break
837
838        min_args = i
839
840    if len(script_params) > min_args:
841        start_params = params[:min_args + 1]
842
843        if run_mode == RUN_WITH_LAST_VALS:
844            default_params = _get_defaults(proc_name)
845            params = start_params + default_params[min_args:]
846        else:
847            params = start_params
848    else:
849       run_mode = RUN_NONINTERACTIVE
850
851    if run_mode == RUN_INTERACTIVE:
852        try:
853            res = _interact(proc_name, params[1:])
854        except CancelError:
855            return
856    else:
857        res = apply(func, params[1:])
858
859    gimp.displays_flush()
860
861    return res
862
863def main():
864    """This should be called after registering the plug-in."""
865    gimp.main(None, None, _query, _run)
866
867def fail(msg):
868    """Display an error message and quit"""
869    gimp.message(msg)
870    raise error, msg
871
872def N_(message):
873    return message
874