├── README ├── doc ├── NEWS ├── TODO ├── UserManual.html ├── UserManual.txt └── posting ├── gimpscripter ├── __init__.py ├── constantmaps.py ├── generate.py ├── gui │ ├── __init__.py │ ├── gimpscripter.glade │ ├── main_gui.py │ ├── param_dialog.py │ └── param_widgets.py ├── macros.py ├── mockmenu │ ├── __init__.py │ ├── db_treemodel.py │ ├── map_procedures.py │ ├── path_treemodel.py │ └── plugindb.py ├── parameters.py ├── parse_params.py ├── runtime.py ├── specification.py └── template.py └── plugin-gimpscripter.py /README: -------------------------------------------------------------------------------- 1 | About Gimpscripter 2 | ================== 3 | 4 | Gimpscripter is a new version of the "Make Shortcut" plugin, now called "GimpScripter". It lets you point-and-click create a Gimp plugin that calls a sequence of plugins, PDB procedures, or macros. It is a plugin authoring tool. 5 | 6 | Gimpscripter is a Gimp plugin written in Python. It generates Python code for a Gimp plugin. 7 | 8 | The source is at github.com/bootchk/gimpscripter. Installation instruction are in the README file. The source includes many readable documents such as NEWS, TODO, and a usermanual. 9 | 10 | Gimpscripter is still in development. It works usually, but is incomplete and could be improved. 11 | 12 | Take a look if you are interested in scripting Gimp, as a user or as a programmer. 13 | 14 | Gimpscripter lets you visually (graphically, point-and-click) implement a sequential recipe, for example "Choose this, set that parameter, choose that, ..". It doesn't have any control flow statements. 15 | 16 | It uses a stack model of computation: it hides a prefix of parameters and references them to active objects. 17 | 18 | It includes a macro facility and macros for common sequences of operations, and to wrap certain PDB procedures with higher-level parameter type, e.g. PF_BRUSH instead of PF_STRING for a brush. 19 | 20 | Some people suggest using a recorder/playback tool to automate Gimp. Scripts from such tools break when the Gimp GUI changes, and the scripts are not easily distributable. Gimpscripter is an alternative. 21 | 22 | Gimpscripter does have many weaknesses, some of which can be attributed to lack of support from the PDB. So it could help guide improvements to the PDB (but it might not raise any issues not already known, such as not storing defaults.) 23 | 24 | I would welcome comments or contributions. 25 | 26 | Installation 27 | ============ 28 | 29 | Instructions for typical installation on Linux, with Gimp version 2.6: 30 | 31 | cp plugin-gimpscripter.py ~/.gimp-2.6/plug-ins (user's local directory) 32 | chmod +x ~/.gimp-2.6/plug-ins/plugin-gimpscripter.py (make it executable) 33 | cp -r gimpscripter ~/.gimp-2.6/plug-ins (copy gimpscripter directory comprising .glade and .py files) 34 | -------------------------------------------------------------------------------- /doc/NEWS: -------------------------------------------------------------------------------- 1 | NEWS 2 | ==== 3 | 4 | This is the new version of the "Make Shortcut" plugin for Gimp. It is now called "Gimpscripter." It lets you point-and-click to create a plugin that calls a sequence of other plugins or PDB procedures or macros (common sequences of calls.) 5 | 6 | The differences from the prior version: 7 | 8 | - a sequence of calls instead of just one call 9 | - call any PDB procedure, not just plugins 10 | - call macros that more closely match Gimp menu semantics than the PDB does 11 | (e.g. Channel/New, which also attaches the channel) 12 | - call macros for common sequences (e.g. Display/New, also flushes) 13 | - call macros that better define parameter types (e.g. Context/Set/Choose brush) 14 | - lets you enter parameters for the called procedures 15 | - OR lets you defer parameters for called procedures until runtime 16 | 17 | See the TODO file for deficiencies. 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /doc/TODO: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | Here are some of the deficiencies: 5 | 6 | - defaults for settings are lame (because the PDB does not keep them) 7 | - range checking for settings is non-existent (because the PDB does not keep them) 8 | - mock menu items are missing (file map_procedures.py is incomplete) 9 | - macros are missing (file macros.py is incomplete) 10 | - the GUI could be more user friendly 11 | - the look and feel should match the Gimp style and might need to use the user's theme 12 | - should allow user to insert and remove commands from a sequence 13 | - should allow user to enter the blurb for a wrapper plugin 14 | - lightly tested. Testing should be automated. 15 | - let user choose a menu for the wrapper, not just "Shortcut>" 16 | - check the return value of wrapped procedures and raise, if error returns can be determined 17 | - substitute our own blurb for mockmenu items that have new semantics via the GS stack model 18 | 19 | These are some Gimp quirks that macros address, and that might go away: 20 | - the floating selection 21 | - creating layers that are not attached to an image 22 | - inconvenient (for users) parameter units, such as pixels instead of percent for scaling 23 | - declared type of parameters for many procedures is too generic, e.g. string for brush 24 | -------------------------------------------------------------------------------- /doc/UserManual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GimpScripter User's Manual 8 | 305 | 306 | 307 |
308 |

GimpScripter User's Manual

309 | 310 |

Copyright 2010, 2011 Lloyd Konneker. 311 | This is part of the GimpScripter User's Manual. 312 | It is licensed under the GNU Free Documentation License, Version 1.3, 3 November 2008. 313 | See the file COPYING for copying conditions.

314 |
315 |

About GimpScripter

316 |

GimpScripter is an authoring tool for Gimp plugins. It is point-and-click. It lets non-programmers author plugins. You DO need to understand Gimp and the Gimp Programmer's Data Base (PDB.) Gimpscripter itself is a Gimp plugin.

317 |

It succeeds the earlier plugin called "Make Shortcut". It lets a shortcut call a sequence of procedures, instead of just one.

318 |

GimpScripter is open source software written in the Python language. It generates a plugin in the Python language. But you don't need to understand Python to use GimpScripter.

319 |

GimpScripter is a prototype or beta version. It works, but has rough edges.

320 |
321 |
322 |

Quick Start

323 |

Start Gimp and choose "Filters/Gimpscripter...". A new window opens, having three panes.

324 |

The left pane is a mockup of a cascading menu. Manipulate the menu until you see a terminal menu item. Click on the menu item. That puts the menu item, representing a command, into the middle pane. The middle pane is a list (sequence) of the commands you have chosen.

325 |

The right pane shows you the parameters or settings of the command. For now, don't change the settings.

326 |

Enter a name, for example "Foo", in the textbox labeled "Shortcut name".

327 |

Choose the OK button. The window closes and an informational dialog opens to show a summary of the plugin you have created. Choose the OK button.

328 |

Now close Gimp and restart it. You will see a new menu called "Shortcuts" with item "Foo." That is the plugin you just created.

329 |

Open an image and choose "Shortcuts/Foo". You won't see any dialog since in the settings pane you left all parameters as constants to your plugin. But your plugin "Foo" should perform the command you chose earlier. You might not be able to see the results, depending on the command you chose.

330 |

In general, you would choose a sequence of many commands, and change settings for commands, before choosing OK to create a shortcut.

331 |
332 |
333 |

Why GimpScripter?

334 |

It lets you:

335 | 341 |
342 |
343 |

Terminology

344 |

Wrapper: the plugin you author using GimpScripter.

345 |

Commands: procedures in a sequence. They can be:

346 | 351 |

Plugins and internal procedures are defined in the PDB. Macros are defined within GimpScripter. Macros are sequences of calls to the PDB.

352 |

You: the author, a user of GimpScripter

353 |

The user: someone using the wrapper you create with GimpScripter

354 |

Mock menu: the menu on the "Menu" pane. It resembles the Gimp menus.

355 |

Runtime: when the user choses your wrapper from Gimp.

356 |

PDB: the Gimp Programmers Data Base.

357 |
358 |
359 |

Choosing a Sequence

360 |

You click on a cascading menu on the left. When you click on a command, it is appended to the sequence shown in the middle pane and its parameters are shown in the right pane. You can enter the parameters when you first choose a command, or later.

361 |

(For now, there is no "Remove", you just start over. There is also no "Insert": a new command is always appended at the end.)

362 |

Mouseover or hover (tooltips) on a menu item shows you the 'blurb' or description of the command.

363 |
364 |
365 |

Using the Settings Pane

366 |

The settings shown in the right pane are for one command, the command that is selected in either the left pane (the mock menu) or in the middle pane (the command sequence.)

367 |

When you first choose a command, you see its settings in the right pane. You can change the settings then, or revisit them later.

368 |

(Future: if the command is a plugin, you can choose to ignore the settings and run the plugin with its most recent values at runtime.)

369 |
370 |

Revisiting Settings

371 |

To revisit settings for a command, select the command in the middle pane. The command's settings will appear in the right pane, with any values you entered earlier. You don't need to "Save" any settings you change. Whenever you revisit, you will see any changes you made earlier.

372 |
373 |
374 |

Validity Checking

375 |

Gimpscripter understands the valid type (for example, integer) for settings, and checks them as you enter them. But Gimpscripter does NOT understand the valid ranges for settings (for example, the smallest and largest acceptable values). Unfortunately, those valid ranges are not readily accessible from the PDB.

376 |
377 |
378 |

Initial Values For Settings

379 |

When you first see settings for a command, the initial values are arbitrary values. The arbitrary initial values are valid for the type of each setting. But they are NOT what you might know as the default values for the settings. Unfortunately, those default values are not readily accessible from the PDB.

380 |
381 |
382 |

Constant Settings

383 |

All settings start as "constants." The value you enter will be the value used at runtime and the user won't have a choice.

384 |
385 |
386 |

Deferred Settings

387 |

When you uncheck the "Constant" checkbox, you 'defer' a setting: the user will be able to change the setting at runtime, in a dialog as the wrapper starts. The value you enter will be what the user sees as the default in the dialog. All settings you defer will appear together in a single dialog at runtime, for the user to change.

388 |
389 |
390 |

Hidden Settings

391 |

Most commands have implied operands: the active image, layer, channel, or path. See "The Stack." These operands (parameters) are defined in the PDB but GimpScript hides them from you, the GimpScripter author, and hides them from a user.

392 |

Note that hidden settings are images, layers, or channels, but not all settings that are images, layers, or channels are hidden: any parameter of a PDB procedure that is of type image, layer, or channel and that is the second parameter of that type in the procedure, is NOT hidden.

393 |
394 |
395 |
396 |

The Stack

397 |

GimpScripter uses a stack model of computation. Each command operates on active objects, that is, the top of a set of stacks. A command that creates an object makes it active (pushes it onto a stack.) A command that removes an object makes a new object active, the object that was previously active. In other words, it pops a stack. When you choose a sequence of commands, you must consider how they will work together on a set of stacks.

398 |

There are separate stacks for images, drawable, layers, and channels. The top of the stack of drawables is either a layer or a channel, whatever was most recently created.

399 |

GimpScripter hides the parameters (settings) of commands that read or affect the stacks. That is, you won't see those parameters on the "Settings" pane.

400 |

When a wrapper starts, the active image and drawable in Gimp are on the GimpScripter stacks. (For now, you can't create a wrapper that doesn't require an open image.)

401 |

When a wrapper ends, it may have changed the active image and drawable that Gimp considers active. The top of the GimpScripter stacks will be active. You might need to include delete or remove commands in your wrapper sequence.

402 |
403 |
404 |

Macros

405 |

Some commands that you can choose from the mock menus are macros implemented in GimpScripter. A macro is a sequence of PDB procedure calls. These macros simplify common but confusing sequences of PDB calls. For example, "Channel/New" calls a macro that creates a channel and adds it to the image (which is almost always what you want.) Just be aware that some items in the mock menu do not directly map to a PDB procedure you might be familiar with (but likely to map to a Gimp menu item you might be familiar with.) The tooltips will alert you to which are macros. For more information, see the comments in the file gimpscripter/macros.py.

406 |

You can write your own macros. See the macros.py file for details. Any macros you write get expanded into wrappers you create, so you don't need to distribute your macro definitions. (More macros are needed for future GimpScripter distributions.)

407 |
408 |
409 |

The Context

410 |

The Gimp context (the active color, brush, pattern, etc.) is NOT part of the GimpScripter stacks. If you change the context, those are not stacked by GimpScripter. You can manage the context by putting commands "Context/Save" and "Context/Restore" in your wrapper sequence.

411 |
412 |
413 |

Practical Sequencing Using Stacks

414 |

You can use named buffers to reshuffle the stack, that is, to get a different object on top of a stack without losing anything on the stack. See the "Edit" commands.

415 |
416 |
417 |

About this Document

418 |

The original of this document is in the restructured text format (ReST). The HTML format is generated by invoking "rst2html UserManual.txt UserManual.html".

419 |
420 |
421 | 422 | 423 | -------------------------------------------------------------------------------- /doc/UserManual.txt: -------------------------------------------------------------------------------- 1 | GimpScripter User's Manual 2 | ========================== 3 | 4 | Copyright 2010, 2011 Lloyd Konneker. 5 | This is part of the GimpScripter User's Manual. 6 | It is licensed under the GNU Free Documentation License, Version 1.3, 3 November 2008. 7 | See the file COPYING for copying conditions. 8 | 9 | About GimpScripter 10 | ------------------ 11 | 12 | GimpScripter is an authoring tool for Gimp plugins. It is point-and-click. It lets non-programmers author plugins. You DO need to understand Gimp and the Gimp Programmer's Data Base (PDB.) Gimpscripter itself is a Gimp plugin. 13 | 14 | It succeeds the earlier plugin called "Make Shortcut". It lets a shortcut call a sequence of procedures, instead of just one. 15 | 16 | GimpScripter is open source software written in the Python language. It generates a plugin in the Python language. But you don't need to understand Python to use GimpScripter. 17 | 18 | GimpScripter is a prototype or beta version. It works, but has rough edges. 19 | 20 | 21 | Quick Start 22 | ----------- 23 | 24 | Start Gimp and choose "Filters/Gimpscripter...". A new window opens, having three panes. 25 | 26 | The left pane is a mockup of a cascading menu. Manipulate the menu until you see a terminal menu item. Click on the menu item. That puts the menu item, representing a command, into the middle pane. The middle pane is a list (sequence) of the commands you have chosen. 27 | 28 | The right pane shows you the parameters or settings of the command. For now, don't change the settings. 29 | 30 | Enter a name, for example "Foo", in the textbox labeled "Shortcut name". 31 | 32 | Choose the OK button. The window closes and an informational dialog opens to show a summary of the plugin you have created. Choose the OK button. 33 | 34 | Now close Gimp and restart it. You will see a new menu called "Shortcuts" with item "Foo." That is the plugin you just created. 35 | 36 | Open an image and choose "Shortcuts/Foo". You won't see any dialog since in the settings pane you left all parameters as constants to your plugin. But your plugin "Foo" should perform the command you chose earlier. You might not be able to see the results, depending on the command you chose. 37 | 38 | In general, you would choose a sequence of many commands, and change settings for commands, before choosing OK to create a shortcut. 39 | 40 | Why GimpScripter? 41 | ----------------- 42 | 43 | It lets you: 44 | 45 | - automate common sequences of Gimp operations 46 | - package a sequence so you can call it from a batch processor 47 | - simplify plugins by making some of their parameters constants 48 | - alias plugins, giving them a more memorable name 49 | 50 | Terminology 51 | ----------- 52 | 53 | Wrapper: the plugin you author using GimpScripter. 54 | 55 | Commands: procedures in a sequence. They can be: 56 | 57 | - plugins 58 | - Gimp internal procedures 59 | - macros. 60 | 61 | Plugins and internal procedures are defined in the PDB. Macros are defined within GimpScripter. Macros are sequences of calls to the PDB. 62 | 63 | You: the author, a user of GimpScripter 64 | 65 | The user: someone using the wrapper you create with GimpScripter 66 | 67 | Mock menu: the menu on the "Menu" pane. It resembles the Gimp menus. 68 | 69 | Runtime: when the user choses your wrapper from Gimp. 70 | 71 | PDB: the Gimp Programmers Data Base. 72 | 73 | Choosing a Sequence 74 | ------------------- 75 | 76 | You click on a cascading menu on the left. When you click on a command, it is appended to the sequence shown in the middle pane and its parameters are shown in the right pane. You can enter the parameters when you first choose a command, or later. 77 | 78 | (For now, there is no "Remove", you just start over. There is also no "Insert": a new command is always appended at the end.) 79 | 80 | Mouseover or hover (tooltips) on a menu item shows you the 'blurb' or description of the command. 81 | 82 | Using the Settings Pane 83 | ----------------------- 84 | 85 | The settings shown in the right pane are for one command, the command that is selected in either the left pane (the mock menu) or in the middle pane (the command sequence.) 86 | 87 | When you first choose a command, you see its settings in the right pane. You can change the settings then, or revisit them later. 88 | 89 | (Future: if the command is a plugin, you can choose to ignore the settings and run the plugin with its most recent values at runtime.) 90 | 91 | Revisiting Settings 92 | +++++++++++++++++++ 93 | 94 | To revisit settings for a command, select the command in the middle pane. The command's settings will appear in the right pane, with any values you entered earlier. You don't need to "Save" any settings you change. Whenever you revisit, you will see any changes you made earlier. 95 | 96 | Validity Checking 97 | +++++++++++++++++ 98 | 99 | Gimpscripter understands the valid type (for example, integer) for settings, and checks them as you enter them. But Gimpscripter does NOT understand the valid ranges for settings (for example, the smallest and largest acceptable values). Unfortunately, those valid ranges are not readily accessible from the PDB. 100 | 101 | Initial Values For Settings 102 | +++++++++++++++++++++++++++ 103 | 104 | When you first see settings for a command, the initial values are arbitrary values. The arbitrary initial values are valid for the type of each setting. But they are NOT what you might know as the default values for the settings. Unfortunately, those default values are not readily accessible from the PDB. 105 | 106 | Constant Settings 107 | +++++++++++++++++ 108 | 109 | All settings start as "constants." The value you enter will be the value used at runtime and the user won't have a choice. 110 | 111 | Deferred Settings 112 | +++++++++++++++++ 113 | 114 | When you uncheck the "Constant" checkbox, you 'defer' a setting: the user will be able to change the setting at runtime, in a dialog as the wrapper starts. The value you enter will be what the user sees as the default in the dialog. All settings you defer will appear together in a single dialog at runtime, for the user to change. 115 | 116 | Hidden Settings 117 | +++++++++++++++ 118 | 119 | Most commands have implied operands: the active image, layer, channel, or path. See "The Stack." These operands (parameters) are defined in the PDB but GimpScript hides them from you, the GimpScripter author, and hides them from a user. 120 | 121 | Note that hidden settings are images, layers, or channels, but not all settings that are images, layers, or channels are hidden: any parameter of a PDB procedure that is of type image, layer, or channel and that is the second parameter of that type in the procedure, is NOT hidden. 122 | 123 | The Stack 124 | --------- 125 | 126 | GimpScripter uses a stack model of computation. Each command operates on active objects, that is, the top of a set of stacks. A command that creates an object makes it active (pushes it onto a stack.) A command that removes an object makes a new object active, the object that was previously active. In other words, it pops a stack. When you choose a sequence of commands, you must consider how they will work together on a set of stacks. 127 | 128 | There are separate stacks for images, drawable, layers, and channels. The top of the stack of drawables is either a layer or a channel, whatever was most recently created. 129 | 130 | GimpScripter hides the parameters (settings) of commands that read or affect the stacks. That is, you won't see those parameters on the "Settings" pane. 131 | 132 | When a wrapper starts, the active image and drawable in Gimp are on the GimpScripter stacks. (For now, you can't create a wrapper that doesn't require an open image.) 133 | 134 | When a wrapper ends, it may have changed the active image and drawable that Gimp considers active. The top of the GimpScripter stacks will be active. You might need to include delete or remove commands in your wrapper sequence. 135 | 136 | 137 | Macros 138 | ------ 139 | 140 | Some commands that you can choose from the mock menus are macros implemented in GimpScripter. A macro is a sequence of PDB procedure calls. These macros simplify common but confusing sequences of PDB calls. For example, "Channel/New" calls a macro that creates a channel and adds it to the image (which is almost always what you want.) Just be aware that some items in the mock menu do not directly map to a PDB procedure you might be familiar with (but likely to map to a Gimp menu item you might be familiar with.) The tooltips will alert you to which are macros. For more information, see the comments in the file gimpscripter/macros.py. 141 | 142 | You can write your own macros. See the macros.py file for details. Any macros you write get expanded into wrappers you create, so you don't need to distribute your macro definitions. (More macros are needed for future GimpScripter distributions.) 143 | 144 | The Context 145 | ----------- 146 | 147 | The Gimp context (the active color, brush, pattern, etc.) is NOT part of the GimpScripter stacks. If you change the context, those are not stacked by GimpScripter. You can manage the context by putting commands "Context/Save" and "Context/Restore" in your wrapper sequence. 148 | 149 | Practical Sequencing Using Stacks 150 | --------------------------------- 151 | 152 | You can use named buffers to reshuffle the stack, that is, to get a different object on top of a stack without losing anything on the stack. See the "Edit" commands. 153 | 154 | About this Document 155 | ------------------- 156 | 157 | The original of this document is in the restructured text format (ReST). The HTML format is generated by invoking "rst2html UserManual.txt UserManual.html". 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /doc/posting: -------------------------------------------------------------------------------- 1 | Announcing a new version of the "Make Shortcut" plugin, now called "GimpScripter". It lets you point-and-click create a plugin that calls a sequence of plugins, PDB procedures, or macros. It is a plugin authoring tool. 2 | 3 | Gimpscripter is a Gimp plugin written in Python. It generates Python code for a plugin. 4 | 5 | The source is at github.com/bootchk/gimpscripter. Installation instruction are in the README file. The source includes many readable documents such as NEWS, TODO, and a user's manual. 6 | 7 | Gimpscripter is still in development. It usually works, but is incomplete and could be improved. 8 | 9 | Take a look if you are interested in scripting Gimp, as a user or as a programmer. 10 | 11 | Gimpscripter lets you visually (graphically, point-and-click) implement a sequential recipe, for example "Choose this, set that parameter, choose that, ..". It doesn't have any control flow statements. 12 | 13 | It uses a stack model: it hides a prefix of parameters and references them to active objects. 14 | 15 | It includes a macro facility and macros for common sequences of operations, and to wrap certain PDB procedures with higher-level parameter type, e.g. PF_BRUSH instead of PF_STRING for a brush. 16 | 17 | Some people suggest using a recorder/playback tool to automate Gimp. Scripts from such tools break when the Gimp GUI changes, and the scripts are not easily distributable. Gimpscripter is an alternative. 18 | 19 | Gimpscripter does have many weaknesses, some of which can be attributed to lack of support from the PDB. So it could help guide improvements to the PDB (but it might not raise any issues not already known, such as not storing defaults.) 20 | 21 | I welcome comments or contributions. 22 | 23 | Here is an example use, to make a plugin "Stroke selection with selection": 24 | 25 | Choose "Filter/Gimpscripter" to start Gimpscripter. 26 | From the menu pane choose "Edit/Copy". 27 | Choose "Edit/Paste as/New Brush". 28 | Choose "Select/To path". 29 | Choose "Edit/Stroke/Path". 30 | Enter a name for the plugin, for example "Stroke selection with selection". 31 | Choose the OK button. 32 | Read the summary and choose the OK button. 33 | Restart Gimp. 34 | Open an image and make a selection. 35 | Choose "Shortcuts/Stroke selection with selection". 36 | You should see a mobius like effect. 37 | 38 | 39 | -------------------------------------------------------------------------------- /gimpscripter/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gimpscripter/constantmaps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Mappings of constants. 5 | Derived from gimpfu.py. 6 | 7 | Copyright 2010 Lloyd Konneker 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | ''' 23 | from gimpfu import * 24 | 25 | ''' 26 | Here I started an elegant way to create the mapping, but gave up on it. 27 | How do you instantiate a type without knowing what parameters __call__ takes?? 28 | _instance_mapping = { } 29 | for (typeconstant, type) in gimpfu._obj_mapping.items(): 30 | _instance_mapping[typeconstant] = type.__call__() 31 | 32 | ''' 33 | 34 | ''' 35 | Map parameter type to don't care instance of that parameter type. 36 | Used for actual values to a plugin call, might be type checked 37 | (not by Python but possibly by Pygimp?) 38 | but ignored because it is RUN_LAST_VAL. 39 | !!! Use None for ephemeral types. 40 | ''' 41 | _instance_map = { 42 | PF_INT8 : 0, 43 | PF_INT16 : 0, 44 | PF_INT32 : 0, 45 | PF_FLOAT : 0.0, 46 | PF_STRING : "foo", # At shortcut time: a don't care string (can't be empty.) 47 | PF_COLOR : (0,0,0), # a don't care color 48 | PF_DISPLAY : None, 49 | PF_IMAGE : None, 50 | PF_LAYER : None, 51 | PF_CHANNEL : None, 52 | PF_DRAWABLE : None, 53 | PF_VECTORS : None, 54 | PF_BOOL : True, 55 | 56 | PF_BRUSH : "foo", # TODO the rest of the upconverted types 57 | } 58 | 59 | ''' 60 | Map parameter type to default instance of that parameter type. 61 | There is some logic here, 1 is better than 0 as a default for most plugins parameters? 62 | Note this might go away if we can determine the actual defaults for wrapped plugins. 63 | !!! Use common names for ephemeral types, since we are defaulting a string widget. 64 | ''' 65 | _default_map = { 66 | PF_INT8 : 1, 67 | PF_INT16 : 1, 68 | PF_INT32 : 1, 69 | PF_FLOAT : 5.0, 70 | PF_STRING : "bar", # At shortcut time: a don't care string (can't be empty.) 71 | PF_COLOR : (37,37,37), # a don't care color 72 | PF_DISPLAY : None, # ??? 73 | PF_IMAGE : "Untitled", 74 | PF_LAYER : "Clipboard", 75 | PF_CHANNEL : "Alpha", 76 | PF_DRAWABLE : "Clipboard", 77 | PF_VECTORS : "Path", 78 | PF_BOOL : "True", 79 | 80 | PF_BRUSH : "Circle (05)" 81 | } 82 | 83 | 84 | ''' 85 | For hidden parameters, map parameter type to an actual parameter. 86 | Actual parameters are references to stacks of ephemera. 87 | Used to generate a call string. 88 | ''' 89 | _hidden_actual_map = { 90 | PF_IMAGE : "ephemera.top(PF_IMAGE)", 91 | PF_DRAWABLE : "ephemera.top(PF_DRAWABLE)", 92 | PF_LAYER : "ephemera.top(PF_LAYER)", 93 | PF_CHANNEL : "ephemera.top(PF_CHANNEL)", 94 | PF_VECTORS : "ephemera.top(PF_VECTORS)", 95 | } 96 | 97 | ''' 98 | Map numeric parameter type PDB to name in Python. 99 | 100 | Note, we have lost information: the PDB doesn't know the original types, 101 | that also described the widget for entering it, such as PF_SLIDER for PF_FLOAT. 102 | ''' 103 | _type_to_string_map = { 104 | PF_INT8 : "PF_INT8", 105 | PF_INT16 : "PF_INT16", 106 | PF_INT32 : "PF_INT32", 107 | PF_FLOAT : "PF_FLOAT", 108 | PF_STRING : "PF_STRING", 109 | PF_COLOR : "PF_COLOR", 110 | PF_DISPLAY : "PF_DISPLAY", 111 | PF_IMAGE : "PF_IMAGE", 112 | PF_LAYER : "PF_LAYER", 113 | PF_CHANNEL : "PF_CHANNEL", 114 | PF_DRAWABLE : "PF_DRAWABLE", 115 | PF_VECTORS : "PF_VECTORS", 116 | PF_BOOL : "PF_BOOL", 117 | 118 | PF_BRUSH : "PF_BRUSH", 119 | } 120 | -------------------------------------------------------------------------------- /gimpscripter/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Crux of generating a wrapping plugin. 5 | Generates another plugin (in Python language) that is a wrapping plugin to a target plugin. 6 | Shortcut means: 7 | 1) alias or link to target plugin 8 | 2) possibly different settings dialog at wrapping plugin runtime: 9 | a) no dialog, use standard or current settings for target plugin. 10 | b) no dialog, use constant settings for target plugin (chosen at creation time) 11 | c) dialog of deferred parameters (deferred and defaulted at creation time.) 12 | 13 | Copyright 2010 Lloyd Konneker 14 | 15 | This program is free software; you can redistribute it and/or modify 16 | it under the terms of the GNU General Public License as published by 17 | the Free Software Foundation; either version 2 of the License, or 18 | (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU General Public License for more details. 24 | 25 | You should have received a copy of the GNU General Public License 26 | along with this program; if not, write to the Free Software 27 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 28 | ''' 29 | 30 | ''' 31 | Terminology: 32 | 33 | Wrapped versus wrapping, target versus wrapping plugin: 34 | This plugin: what you are reading now, that generates code. 35 | Wrapping plugin: plugin code generated by this plugin. 36 | Wrapped plugin: plugin that wrapping plugin calls, iow the target. 37 | 38 | Formal versus actual parameters: 39 | Formal parameters are in a definition 40 | Actual parameters are in a call 41 | 42 | Script-declared versus PDB-declared parameters: 43 | Script-declared: in "parameters" argument of a register() call 44 | PDB-declared: in paramdefs held by PDB 45 | and in formal parameters of a plugin_main() 46 | Hidden parameters: the difference, i.e. prefix of PDB-declared parameters 47 | e.g. "run-mode, image, drawable" 48 | that Pygimp gimpfu module hides from script authors. 49 | 50 | Users: 51 | wrapping-user: a user of a plugin generated by GimpScripter 52 | author-user: a user of GimpScripter to create a plugin 53 | 54 | Definitions: 55 | 56 | Settings versus options versus preferences: all essentially the same thing. 57 | Gimp seems to use "settings" for plugins and "options" for tools. 58 | In most GUI terminology, "options" or "preferences" is used. 59 | 60 | Preset: define initial value of, but still allow changes. Same as default. 61 | Constant: not changeable by wrapping-user. Changeable when you create a wrapping plugin, constant thereafter. 62 | ''' 63 | 64 | ''' 65 | I tried to simply register an existing plugin under a new menu item. 66 | It doesn't work, disallowed by GIMP, can only register menu at certain times in life of plugin, init etc. 67 | gimp.pdb.gimp_plugin_menu_register("python_fu_impscriptor", "/Filters/Map/Foo"); 68 | ''' 69 | 70 | from gimpfu import * 71 | 72 | import os 73 | import stat 74 | # import operator # for or_ 75 | 76 | # our own submodules 77 | from gimpscripter import parameters 78 | from gimpscripter import constantmaps 79 | from gimpscripter import parse_params 80 | from gimpscripter import template 81 | from gimpscripter import macros 82 | from gimpscripter.mockmenu import plugindb 83 | # Refers to a pdb dictionary of useable procedures ( a facsimile subset of the Gimp PDB.) 84 | 85 | 86 | 87 | 88 | # Constants, at least in this version. 89 | GIMPSCRIPTER_WRAP = "wrapper-" 90 | GIMP_STD_FILENAME_PREFIX = "plugin-" 91 | GIMP_STD_PROCEDURENAME_PREFIX = "python-fu-" 92 | # Next version might let author-user chose menu path for wrapping plugin 93 | # !!! Not have a trailing / 94 | WRAPPING_MENU_PATH_PREFIX_REGISTER = "/Shortcuts" # For registration, need 95 | WRAPPING_MENU_PATH_PREFIX = "Shortcuts" 96 | 97 | 98 | # Name of function to lookup object for an ephemeral by name. 99 | # Parameters will be appended. 100 | # Use "runtime.ephemera.lookup" if we are not putting runtime directly in wrapping plugin 101 | EPHEMERAL_LOOKUP = "ephemera.lookup" 102 | 103 | # Lines of code to insert 104 | # indent 2 spaces and trail newline !!! 105 | # TODO unify names 106 | INTER_COMMAND_RUNTIME = " ephemera.update()\n" 107 | GIMPSCRIPTER_PRELUDE = " ephemera = GimpEphemera(image, drawable)" # WAS image_stack = GimpStack(image)\n" 108 | GIMPSCRIPTER_POSTLUDE = "" 109 | # !!! Note call to image_stack.top() is in constantmaps.py 110 | 111 | # prefix to use before pdb procedure names. 112 | # gimp module creates a dictionary named pdb. 113 | # gimpfu module aliases gimp.pdb to pdb. 114 | PDB_INVOKE_PREFIX = "pdb." 115 | 116 | # TODO make wrapping plugin return stack top 117 | 118 | # Constants for type of wrapping plugin. 119 | # In future, maybe other types eg sequences of plugins 120 | WRAPPING_TYPE_CONSTANT = 1 # all settings constant i.e. hardcoded 121 | WRAPPING_TYPE_DEFERRED = 2 # some constants, some settings deferred to wrapping plugin runtime 122 | WRAPPING_TYPE_LAST = 3 # using last (current at runtime) settings of wrapped 123 | 124 | # Other constants 125 | PARAM_SEP = ", " 126 | 127 | # Globals 128 | substitutions = {} 129 | 130 | 131 | def comma_separate(strings): 132 | ''' 133 | Strategy for parameter separators: add to any parameter sequence that is not empty. 134 | A trailing comma in a parameter list doesn't hurt. 135 | Join separate parameter sequences without a comma, since they all have trailing commas. 136 | ''' 137 | if strings: 138 | return PARAM_SEP.join(strings) + PARAM_SEP # !!! Trailing 139 | else: 140 | return "" 141 | 142 | 143 | def summarize(): 144 | ''' 145 | Return string describing wrapping plugin we generated. 146 | !!! Assert substitutions created. 147 | ''' 148 | return template.summarytemplate.substitute(substitutions) 149 | 150 | 151 | def generate(plugin_spec): 152 | ''' 153 | Generate file of python code for a Gimp plugin. 154 | Specified by plugin_spec. 155 | ''' 156 | 157 | # !!! uniquify before generation 158 | plugin_spec.commands.param_list.uniquify_names() 159 | 160 | global substitutions 161 | substitutions = make_substitution_map(plugin_spec) 162 | 163 | # Substitute substitutions into python template, creating a Python script. 164 | substitutedtemplate = template.wrappingtemplate.substitute(substitutions) 165 | 166 | # Write completed script to file. 167 | # TODO warn of overwrite 168 | #if os.path.isfile(filepath): 169 | filepath = substitutions["filepath"] 170 | with open(filepath, "w") as f: 171 | f.write(substitutedtemplate) 172 | 173 | # Make wrapping plugin file executable. (Linux, Mac OSX, not needed for Windows?) 174 | os.chmod(filepath, stat.S_IRWXU) 175 | 176 | 177 | 178 | def make_substitution_map(plugin_spec): 179 | ''' 180 | Create map for template substitution (actual parameters to template.) 181 | !!! Note substitution map is used for more than one template. 182 | ''' 183 | 184 | ''' 185 | Make common substitution strings. 186 | ''' 187 | # User's chosen name. Author-user should choose a unique one, else there will be clashes. 188 | # Might not be clashes in menus, but could be clashes in filenames and PDB procedure names. 189 | # !!! Translate spaces and other punctuation: delete them 190 | trans_menuitem = plugin_spec.wrapping.menuname.translate(None, ' .,?!:;()') 191 | 192 | # Formerly tried to include a name from wrapped or its blurb, but its not easy 193 | # Gimp std + our wrapping std + author-user's wrapping name 194 | # Example PDB name: python-fu-foo-wraps-blur 195 | # Example filename: plugin-foo-wraps-blur 196 | # !!! include author-user's chosen name so author-user can create many wrapping plugins to same wrapped. 197 | wrappingname = GIMPSCRIPTER_WRAP + trans_menuitem # EG -wrapper-foo 198 | # WAS item + "-" + GIMPSCRIPTER_WRAP + plugin_spec.wrapping.name 199 | wrappingprocedurename = GIMP_STD_PROCEDURENAME_PREFIX + wrappingname 200 | wrappingfilename = GIMP_STD_FILENAME_PREFIX + wrappingname 201 | 202 | substitutions = {} 203 | 204 | ''' 205 | Make substitution strings for plugin_main() of wrapping plugin. 206 | ''' 207 | commands = plugin_spec.commands 208 | 209 | substitutions["wrappingmainformalparams"] = make_wrapping_main_formal_params(commands) 210 | substitutions["wrappingmainbody"] = make_wrapping_main_body(commands) 211 | 212 | ''' 213 | Make substitution strings for registering wrapping plugin. 214 | ''' 215 | # Image type for wrapping plugin that is same image type as wrapped plugin. 216 | substitutions["wrappingimagetype"] = what_in_image_type(commands) 217 | # make standard name for wrapping plugin procedure. 218 | # !!! Note this is a string, don't need to substitute underbars for dash 219 | substitutions["wrappingprocedurename"] = wrappingprocedurename 220 | # TODO let author-user enter a blurb but default it to a reasonable guess 221 | substitutions["wrappingblurb"] = make_wrapping_blurb(commands) 222 | substitutions["wrappingparameterdefs"] = make_wrapping_paramdefs(commands) 223 | ''' 224 | label and menu parameters work together. 225 | !!! A side effect of menu parameter is to omit image and drawable parameters to a plugin. 226 | !!! Obscure wierdness in gimpfu. 227 | Set them so that image and drawable parameters are passed to wrapping plugin, 228 | if wrapped plugin needs them. 229 | Otherwise gimpfu might open a dialog for image parameter in some circumstances. 230 | ''' 231 | substitutions["wrappingmenupath"] = WRAPPING_MENU_PATH_PREFIX + "/" + plugin_spec.wrapping.menuname 232 | """ 233 | if commands.is_take_image(): 234 | # label comprises menu path cat menu item 235 | # menu keyword arg omitted 236 | # !!! gimpfu will pass image and drawable to wrapping plugin 237 | substitutions["wrappinglabel"] = substitutions["wrappingmenupath"] 238 | substitutions["wrappingmenuarg"] = "" 239 | else: 240 | """ 241 | # label is just menu item 242 | # menu keyword is menupath 243 | # !!! gimpfu will NOT pass image and drawable to wrapping plugin 244 | substitutions["wrappinglabel"] = plugin_spec.wrapping.menuname 245 | substitutions["wrappingmenuarg"] = 'menu="' + WRAPPING_MENU_PATH_PREFIX_REGISTER + '"' 246 | 247 | if is_need_runtime(commands): 248 | filepath = gimp.directory + "/plug-ins/gimpscripter/runtime.py" 249 | with open(filepath, "r") as f: 250 | substitutions["wrappingruntimelibrary"] = f.read() 251 | substitutions["prelude"] = GIMPSCRIPTER_PRELUDE 252 | substitutions["postlude"] = GIMPSCRIPTER_POSTLUDE 253 | else: # omit runtime 254 | substitutions["wrappingruntimelibrary"] = "" 255 | substitutions["prelude"] = "" 256 | substitutions["postlude"] = "" 257 | 258 | # Make file path to local plugins. 259 | # Pygimp knows parent directory + standard directory name + filename + standard extension 260 | substitutions["filepath"] = gimp.directory + "/plug-ins/" + wrappingfilename + ".py" 261 | 262 | return substitutions 263 | 264 | 265 | def what_in_image_type(commands): 266 | ''' 267 | What is IN image type of a seq of commands? 268 | !!! first command defines it for all subsequent commands. 269 | A command may reduce image type (for example gimp-mode-greyscale.) 270 | Do we want to check these semantics? 271 | ''' 272 | return plugindb.plugindb[commands.command_list[0].name].imagetype 273 | 274 | 275 | def what_wrapping_type(command, wrappedparms): 276 | ''' 277 | Return type of wrapping plugin being generated. 278 | Depends on author-user choices in parameter_dialog: whether LAST_VALS, and whether any parameters are deferred. 279 | !!! Note this is for a single command. 280 | ''' 281 | if command.is_use_last: 282 | return WRAPPING_TYPE_LAST 283 | else: 284 | if parameters.any_parms_deferred(wrappedparms): # if any were deferred 285 | # A mix of author-user entered values, and names for deferred parameters. 286 | # !!! wrapping plugin will be INTERACTIVE, wrapped target will be NONINTERACTIVE 287 | return WRAPPING_TYPE_DEFERRED 288 | else: 289 | # All parameters constant. Both wrapping plugin and wrapped target will be NONINTERACTIVE, just "go" 290 | return WRAPPING_TYPE_CONSTANT 291 | 292 | 293 | ''' 294 | These are decision routines. 295 | ''' 296 | def is_need_runtime(commands): 297 | ''' 298 | A runtime library is needed if: 299 | TODO update this comment 300 | - if wrapping has hidden params 301 | - if author-user did not defer (did enter name strings) for ephemeral parameters. 302 | Put library code into wrapping plugin, rather than import a module of library code, 303 | so publishing a wrapping plugin is simpler (not dependent on other modules.) 304 | Alternatively, template should include "from gimpscripter import runtime" 305 | and EPHEMERAL_CALL should be "runtime.ephemeral.lookup" 306 | ''' 307 | # TODO if all ephemeral are OUT params of wrapped plugins, no need for this 308 | ## WAS if commands.has_ephemeral_params() : 309 | 310 | # Temporarily, until we can figure out whether macros use ephemera, 311 | # return True 312 | return commands.has_ephemeral_params() 313 | 314 | 315 | def is_need_wrapper_main_formal_param_image(commands): 316 | ''' Does wrapper need formal parameters "image, drawable" 317 | If any command refers to "image" or "drawable" in leading params (what we call IN or hidden params) 318 | OR if needs runtime (since the runtime depends on image, drawable to initialize stacks. 319 | ''' 320 | # WAS is_take_image() 321 | return commands.has_in_params() or is_need_runtime(commands) 322 | 323 | 324 | ''' 325 | These make_foo routines put together strings for substitution in a template of Python code 326 | from various pieces of slices of Param for wrapped plugin. 327 | ''' 328 | 329 | 330 | def make_wrapping_main_formal_params(commands): 331 | ''' 332 | Return string for formal parameters of wrapping plugin main() procedure. 333 | Depends on whether wrapped commands take same parameters (which are passed through). 334 | Note names of hidden parameters (e.g. "image") must match between: 335 | wrapping main formal parameters 336 | call to wrapped commands 337 | Any deferred wrapped parameters must also be parameters of wrapping. 338 | ''' 339 | 340 | if is_need_wrapper_main_formal_param_image(commands): 341 | hidden_params = "image, drawable, " # !!! Trailing comma 342 | else: 343 | # Also, imagetypes param to register() should be empty, 344 | # meaning wrapped plugin is enabled always (whether an image is open or not.) 345 | hidden_params = "" 346 | return hidden_params + make_wrapping_formal_params_for_wrapped(commands) 347 | 348 | 349 | 350 | def make_paramdef_default(parm): 351 | ''' 352 | Returns a string (for use in generated code) that is a default value for a paramdef. 353 | 354 | For most types of parameters, author-user enters a value of same type e.g. integer. 355 | But for ephemeral types, author-user enters a value of a different type e.g. string. 356 | For ephemerals, make a None value, which is a value of all types. 357 | That forces wrapping-user to make another choice at wrapping plugin runtime i.e. it is intial value, 358 | but not really a default in the sense that it is acceptable. 359 | ''' 360 | if parameters.is_ephemeral_type(parm.type): 361 | return "None" # TODO should it be -1 since PyGimp translates -1 to None? 362 | else: 363 | return parm.get_evaluable_value() # repr() so a string type is quoted 364 | 365 | 366 | def make_standard_paramdefs(commands): 367 | if is_need_wrapper_main_formal_param_image(commands): 368 | return '(PF_IMAGE, "image", "Input image", None), (PF_DRAWABLE, "drawable", "Input drawable", None)' 369 | else: 370 | return "" 371 | 372 | 373 | def make_wrapping_paramdefs(commands): 374 | ''' 375 | Return string of parameter definitions for wrapping plugin registration. 376 | These comprise: 377 | standard "image, drawable" hidden paramdefs if needed 378 | deferred parameters. 379 | They are made from wrapped param defs for deferred parameters. 380 | 381 | Note names must match between: 382 | wrapping main formal parameters 383 | wrapping paramdefs 384 | 385 | These will cause wrapping plugin to show a dialog at its runtime 386 | so wrapping-user can enter deferred parameters to be passed to wrapped plugins. 387 | 388 | Note that default values are missing from paramdefs (since PDB doesn't hold them.) 389 | Thus for default (remember, we are generating Python plugin code, which takes a default 390 | as 4th argument for a paramdef) 391 | For default value, use value author-user entered when deferring parameter. 392 | 393 | # TBD another options (range) parameter for certain types? SLIDERS 394 | ''' 395 | wrappedparms = commands.param_list 396 | 397 | # Comma separated cat param defs, turned into strings 398 | paramdefstrings = [] 399 | 400 | standard_paramdefs = make_standard_paramdefs(commands) 401 | if standard_paramdefs: 402 | paramdefstrings.append(standard_paramdefs) 403 | 404 | for parm in parameters.get_parms_deferred(wrappedparms): 405 | 406 | paramdefparts = [constantmaps._type_to_string_map[parm.type], # type: numeric => name of a constant 407 | # name and desc strings, need quotation, ie a string literal 408 | '"' + parm.unique_name + '"', # !!! unique_name 409 | '"' + parm.desc + '"', 410 | make_paramdef_default(parm) ] # The default. Is a string but not quoted unless it is string literal 411 | paramdefstring = "[" + PARAM_SEP.join(paramdefparts) + "]" # Each paramdef is a list or tuple 412 | paramdefstrings.append(paramdefstring) 413 | 414 | return PARAM_SEP.join(paramdefstrings) 415 | 416 | 417 | 418 | def make_wrapping_formal_params_for_wrapped(commands): 419 | ''' 420 | Return comma separated string of names of formal parameters for wrapping plugin 421 | that must be passed so they can be put in call to wrapped. 422 | e.g. "tweak, tweak1, color" 423 | These : 424 | -are nonhidden formal parameters of wrapped plugin 425 | -AND are deferred parameters 426 | -AND are unique within the string (and within the wrapper.) 427 | nondefered parameters don't need to pass through wrapping main, 428 | they are just constants in a call to wrapped. 429 | ''' 430 | return comma_separate(commands.deferred_unique_names()) 431 | 432 | 433 | def make_wrapping_blurb(commands): 434 | ''' 435 | Return blurb string for wrapping plugin. 436 | One of three types. 437 | ''' 438 | suffix = "" 439 | return "blurb" # TODO 440 | command = commands[0] # TODO aggregate 441 | wrapping_type = what_wrapping_type(command) # TODO this takes wrappingparms 442 | if wrapping_type == WRAPPING_TYPE_DEFERRED: 443 | suffix = " with a settings dialog at runtime." 444 | elif wrapping_type == WRAPPING_TYPE_CONSTANT: 445 | suffix = " with all settings constant." 446 | elif wrapping_type == WRAPPING_TYPE_LAST: 447 | suffix = " using it's latest settings." 448 | 449 | # If this is a simple shortcut wrapping ONE other, 450 | # a good blurb is "Wrapping menupathmap[command.name] 451 | return "A wrapping plugin" + suffix 452 | 453 | 454 | def make_imagetype_string(procname): 455 | ''' 456 | Return imagetype string eg. "RGB" for a plugin. 457 | ''' 458 | return make_procname_to_imagetype_map()[procname] 459 | 460 | 461 | 462 | def make_LHS_name(paramtype): 463 | if paramtype == PF_IMAGE: 464 | return "fooimage" 465 | else: 466 | return "fooobject" 467 | 468 | def make_LHS_string(command): 469 | ''' 470 | Make LHS (left hand side) for an assignment of a call to a procedure. 471 | !!! Note that few plugins don't have return values, but some do e.g. Offset Palette 472 | !!! Note LHS contains '=' (usual meaning of LHS does not) 473 | ''' 474 | names = [] 475 | for parm in parameters.get_return_parms(command.name): 476 | names.append(make_LHS_name(parm.type)) 477 | if names: 478 | if len(names) > 1: 479 | return "(" + ",".join(names) + ") =" # a tuple is returned from plugins unless a single value 480 | else: 481 | return names[0] + "=" 482 | else: 483 | return "" 484 | 485 | 486 | def make_wrapping_main_body(commands): 487 | ''' Generate seq of command invocations for body of wrapping plugin main. ''' 488 | script = "" 489 | for position in range(0, len(commands)): 490 | script += make_invocation(commands, position) 491 | return script 492 | 493 | 494 | def make_invocation(commands, position): 495 | ''' 496 | Return Python code for an invocation (call) of a command. 497 | It may include lines of code before and after call. 498 | ''' 499 | script = "" 500 | if commands.has_ephemeral_params() : 501 | # Each command must be preceded by because any prior command may have created ephemera 502 | # TODO we might be able to forego this, if we knew which commands created ephemera 503 | script += INTER_COMMAND_RUNTIME 504 | 505 | # Generate a comment that indicates what menu item was chosen by author-user 506 | script += " # " + commands.get_command_for(position).pathstring + '\n' 507 | 508 | # Generate call 509 | script += " " # Indent to standard depth: two spaces. Must match template. 510 | # Since two commands may have same name, get parms for them by position 511 | script += make_call_string(commands, position) # , commands.get_parms_for(position)) 512 | script += '\n' # trail newline. Note this is platform independent, Python accepts all terminators. 513 | return script 514 | 515 | 516 | def make_call_string(commands, position): 517 | ''' 518 | Make Python code for a call to named procedure or macro, with parameters. 519 | !!! For internal procedure, plugin, or macro. But there are subtle differences. 520 | 521 | Note that for a plugin, in the PDB, the first three parameters are always run-mode, image, and drawable. 522 | But Pygimp requires run-mode as a keyword arg 523 | ''' 524 | command = commands.get_command_for(position) 525 | parms = commands.get_parms_for(position) 526 | 527 | # transliterate _ => - in name 528 | # Because in Python, - is subtraction operator, can't be used in names. 529 | underbar_name = command.name.replace("-", "_") 530 | 531 | wrapping_type = what_wrapping_type(command, parms) 532 | 533 | if parse_params.has_runmode(parms): 534 | # PDB procedure of type Plugin except for those whose name begins with 'file-' 535 | if wrapping_type == WRAPPING_TYPE_LAST: 536 | run_mode_string = "run_mode=RUN_WITH_LAST_VALS" 537 | else: 538 | run_mode_string = "run_mode=RUN_NONINTERACTIVE" 539 | else: # PDB procedure of type Internal Procedure or a Plugin whose name begins with 'file-' 540 | run_mode_string = "" 541 | 542 | # TODO condense this and refactor into a subroutine 543 | if wrapping_type == WRAPPING_TYPE_DEFERRED: 544 | # A mix of author-user entered values, and names for deferred parameters. 545 | # !!! wrapping plugin will be INTERACTIVE, wrapped target will be NONINTERACTIVE 546 | paramstring = make_actual_nonhidden_params(parms) # WAS make_deferred_params(parms) 547 | elif wrapping_type == WRAPPING_TYPE_CONSTANT: 548 | # All settings passed as constants. 549 | # author-user saw, and optionally changed, settings when creating wrapping plugin. 550 | # Both wrapping plugin and wrapped target will be NONINTERACTIVE, just "go" 551 | paramstring = make_actual_nonhidden_params(parms) # WAS make_constant_actual_params(parms) 552 | elif wrapping_type == WRAPPING_TYPE_LAST: 553 | # Not Care parameters but tell wrapped to use last values. 554 | paramstring = make_NC_params(parms) 555 | else : 556 | raise RuntimeError, "Unknown wrapping type" 557 | 558 | if macros.is_macro(command.name): 559 | return generate_macro_call(command.name, parms) 560 | else: 561 | ''' 562 | Generic form: = gimp.pdb. ( , , run_mode= ) 563 | ''' 564 | return make_LHS_string(command) \ 565 | + PDB_INVOKE_PREFIX + underbar_name \ 566 | + "( " + make_hidden_params(parms) \ 567 | + paramstring \ 568 | + run_mode_string \ 569 | + ")" 570 | 571 | 572 | def make_hidden_params(wrappedparms): 573 | ''' 574 | Make a string of actual parameters for hidden parameters of a wrapped plugin. 575 | !!! Exclude run-mode 576 | ''' 577 | # For each hidden param (excluding first, which is run_mode), map its type to string for canonical name. 578 | # tbd more to it, this is a trick, but not adequate? 579 | # tbd in future, it might be a reference to an earlier name returned from earlier step, ie stacked 580 | if parse_params.has_runmode(wrappedparms): 581 | start = 1 582 | else: 583 | start = 0 584 | return comma_separate([str(constantmaps._hidden_actual_map[parm.type]) for parm in parameters.get_parms_hidden(wrappedparms)[start:]]) 585 | 586 | 587 | def make_NC_params(wrappedparms): 588 | ''' For call to wrapped plugin, make a string of actual parameters with don't care values.''' 589 | params = [] 590 | 591 | # !!!Note pygimp provides pdb[name].params attribute, a list of parameter definitions. 592 | # !!!Don't need to call gimp.pdb.gimp_procedural_db_proc_info(name) 593 | # 594 | # Omit first standard params: run-mode 595 | # image, drawable are in map: 596 | # They are mapped not to an instance, but to an actual parameter 597 | # that will be passed through from wrapping plugin to wrapped plugin. 598 | # TODO this is broke 599 | for paramdef in wrappedparms.nonhiddenparamdefs: 600 | paramtype = paramdef[0] 601 | # map param type to instance of type 602 | # !!! Note want str() not repr() so an "image" parameter is not quoted. TBD revisit this 603 | params.append(str(constantmaps._instance_map[paramtype])) 604 | return comma_separate(params) 605 | 606 | 607 | def make_lookup(paramtype, name): 608 | ''' 609 | Generate call to a wrapping plugin runtime routine to lookup an ephemeral object by name. 610 | E.g. "runtime.ephemera.lookup(PF_IMAGE, "foo") 611 | ''' 612 | return EPHEMERAL_LOOKUP + "(" \ 613 | + constantmaps._type_to_string_map[paramtype] \ 614 | + "," \ 615 | + name \ 616 | + ")" 617 | 618 | 619 | def list_actual_nonhidden_params(parms): 620 | ''' 621 | Return a list of strings for actual non-hidden params. 622 | Such a list can subsequently be substituted in macros, or comma separated. 623 | ''' 624 | ''' 625 | For call to wrapped plugin, make a string of actual parameter values. 626 | Where author-user, at creation time: 627 | - entered values, so constant at runtime (no dialog to wrapping-user.) 628 | - chose "Use current" for ephemeral types, so actual parameter is a reference to stack 629 | - did NOT chose "Use current" for ephemeral types, generate a runtime lookup of a constant name 630 | ''' 631 | actual_params = [] 632 | for parm in parameters.get_parms_nonhidden(parms): 633 | if parm.is_deferred: 634 | # deferred. Could be ephemeral 635 | # wrapping will have this name as formal parameter. 636 | # wrapping will have a dialog to enter actual parameter. 637 | # wrapping will assign to this name at runtime. 638 | # wrapped will refer to this name in its actual parameters. 639 | actual = parm.unique_name # !!!! unique 640 | elif parameters.is_ephemeral_type(parm.type): 641 | # not deferred but emphemeral 642 | actual = make_lookup(parm.type, parm.get_evaluable_value() ) 643 | else: 644 | # not deferred and not ephemeral, a constant 645 | actual = parm.get_evaluable_value() # convert object to evaluable 646 | assert actual is not None, str(parm) 647 | actual_params.append(actual) 648 | return actual_params 649 | 650 | 651 | # Was make_constant_actual_params 652 | def make_actual_nonhidden_params(wrappedparms): 653 | ''' 654 | Return a comma separated string of actual, nonhidden params. 655 | Such a string is part of actual parameters in a call string. 656 | ''' 657 | return comma_separate(list_actual_nonhidden_params(wrappedparms)) 658 | 659 | 660 | def generate_macro_call(name, parms): 661 | ''' Generate a string that is Python code from our macros ''' 662 | 663 | # Template for substitutions is text of macro declaration. 664 | template = macros.template_for(name) 665 | 666 | # Create substitutions for parameters of the macro. 667 | substitutions = {} 668 | # If author-user deferred parameter, substitute a unique object name (which will get a value at run-time) 669 | # else if author-user entered a constant value, substitute that 670 | # substitutions["channelName"] = '"foo"' # Test 671 | actual_params = list_actual_nonhidden_params(parms) 672 | i = 0 673 | for item in actual_params: 674 | # Note that actual params have uniquified names that no longer match placeholder name 675 | substitutions[parms[i].name] = str(item) 676 | i += 1 677 | 678 | # Return result string of substitutions 679 | try: 680 | result = template.substitute(substitutions) 681 | except ValueError as details: 682 | raise RuntimeError("Placeholders in GimpScripter macros must be valid Python identifiers" + str(details) ) 683 | except KeyError as details: 684 | raise RuntimeError("Missing substitute for a placeholder in a GimpScripter macro" + str(details) ) 685 | print "Macro result is", result 686 | return result 687 | 688 | 689 | """ 690 | def make_deferred_params(wrappedparms): 691 | ''' 692 | For a call to a procedure, make string of actual parameters (args) which is a mix of: 693 | -actual values the author-user entered (constants or strings that name ephemerals) 694 | -names of deferred parameters 695 | ''' 696 | params = [] 697 | for i in range(0,len(wrappedparms.userentered)): 698 | if wrappedparms.defers[i]: 699 | # Argument is name of deferred parameter, which matches 700 | # name of a formal parameter to wrapping plugin. 701 | # Wrapping-user will see dialog to enter it at wrapping plugin runtime. 702 | params.append(wrappedparms.nonhiddennames[i]) 703 | else: 704 | params.append(wrappedparms.userentered[i]) 705 | return comma_separate(params) 706 | """ 707 | 708 | ''' 709 | These are three test instances: python, scheme, C. 710 | To prove that LAST_VALS ignores parameters, etc. 711 | !!! Note, must pass all parameters, even if LAST_VALS means ignore them 712 | # pdb.plug_in_resynthesizer2(timg, tdrawable, 0,0, 1, tdrawable, -1, -1, 0.0, 0.117, 16, 500, run_mode=RUN_WITH_LAST_VALS) 713 | # pdb.script_fu_add_bevel(timg, tdrawable, 5.0, 0, 0, run_mode=RUN_WITH_LAST_VALS) 714 | # pdb.plug_in_nova(timg, tdrawable, 0,0,(10,10,10),0, 0, 0, run_mode=RUN_WITH_LAST_VALS) 715 | ''' 716 | 717 | 718 | 719 | 720 | -------------------------------------------------------------------------------- /gimpscripter/gui/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gimpscripter/gui/gimpscripter.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 5 19 | Gimpscripter 20 | 800 21 | 600 22 | normal 23 | False 24 | 25 | 26 | 27 | True 28 | 2 29 | 30 | 31 | True 32 | True 33 | 34 | 35 | True 36 | True 37 | 38 | 39 | 100 40 | True 41 | True 42 | automatic 43 | automatic 44 | 45 | 46 | True 47 | True 48 | treestore1 49 | 50 | 51 | Menu 52 | 53 | 54 | 55 | 0 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | True 65 | True 66 | 67 | 68 | 69 | 70 | True 71 | True 72 | 73 | 74 | True 75 | True 76 | automatic 77 | automatic 78 | 79 | 80 | True 81 | True 82 | liststore1 83 | 84 | 85 | Commands 86 | 87 | 88 | 89 | 0 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | True 99 | True 100 | 101 | 102 | 103 | 104 | 150 105 | True 106 | True 107 | automatic 108 | automatic 109 | 110 | 111 | True 112 | queue 113 | 114 | 115 | True 116 | 117 | 118 | True 119 | 120 | 121 | True 122 | 0.49000000953674316 123 | Settings 124 | 125 | 126 | False 127 | 2 128 | 0 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | True 146 | True 147 | 148 | 149 | 150 | 151 | True 152 | True 153 | 154 | 155 | 156 | 157 | False 158 | True 159 | 160 | 161 | 162 | 163 | True 164 | 165 | 166 | True 167 | Shortcut name 168 | 169 | 170 | False 171 | 2 172 | 0 173 | 174 | 175 | 176 | 177 | True 178 | True 179 | 180 | 181 | 182 | 183 | False 184 | 2 185 | 1 186 | 187 | 188 | 189 | 190 | False 191 | True 192 | 193 | 194 | 195 | 196 | 1 197 | 198 | 199 | 200 | 201 | True 202 | end 203 | 204 | 205 | gtk-cancel 206 | True 207 | True 208 | True 209 | True 210 | 211 | 212 | 213 | False 214 | False 215 | 0 216 | 217 | 218 | 219 | 220 | gtk-ok 221 | True 222 | True 223 | True 224 | True 225 | 226 | 227 | 228 | False 229 | False 230 | 1 231 | 232 | 233 | 234 | 235 | False 236 | end 237 | 0 238 | 239 | 240 | 241 | 242 | 243 | button2 244 | button1 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /gimpscripter/gui/main_gui.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | ''' 3 | Gimpscripter main GUI. 4 | 5 | Copyright 2010 Lloyd Konneker 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program; if not, write to the Free Software 19 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 | ''' 21 | 22 | import pygtk 23 | pygtk.require("2.0") 24 | import gtk 25 | 26 | # Our own sub modules, installed in same directory as this file. 27 | # These are independent of db 28 | from gimpscripter.mockmenu import db_treemodel 29 | from gimpscripter.mockmenu import path_treemodel 30 | from gimpscripter import generate 31 | from gimpscripter import specification # bundle of data drives generation 32 | from gimpscripter.gui import param_dialog 33 | 34 | 35 | 36 | # Make fully qualified path so pygtk finds glade file. 37 | # Get glade file from same directory as this file (must be together) 38 | import os.path 39 | UI_FILENAME = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'gimpscripter.glade') 40 | 41 | 42 | 43 | class gimpscripterApp(object): 44 | 45 | def __init__(self, dictofviews): 46 | 47 | # Note: use self for all variables accessed across class methods, 48 | # but not passed into a method, eg in a callback. 49 | 50 | self.dictofviews = dictofviews # dictionary of views that drives this app 51 | self.currentviewname = dictofviews.keys()[0] # arbitrarily the first 52 | 53 | self.selected_command_index = None 54 | self.is_settings_valid = True # Initially, nonexistant settings are valid. 55 | 56 | ''' 57 | One treemodel, a menu tree of plugins. 58 | Data driven construction of a set of treemodels as specified by a dictofviews. 59 | Also populates and sorts treemodels. 60 | Note: ignore any treestore model from glade, don't: model = builder.get_object("treestore1") 61 | ''' 62 | self.models = db_treemodel.TreeModelDictionary(self.dictofviews) 63 | 64 | self.spec = specification.GimpScripterSpec() 65 | 66 | builder = gtk.Builder() 67 | if not builder.add_from_file(UI_FILENAME): 68 | raise RuntimeError, "Failed to open glade file." 69 | 70 | # Get references to widgets for use in callbacks 71 | self.mainwidget = self.safe_build(builder, "dialog1") 72 | self.OKbutton = self.safe_build(builder, "button1") 73 | self.mockmenu = self.safe_build(builder, "treeview1") 74 | self.command_seq_listview = self.safe_build(builder, "treeview2") 75 | self.name_textentry = self.safe_build(builder, "entry1") 76 | 77 | # parent of parameter_widgets 78 | self.parameter_box = builder.get_object("vbox1") 79 | self.parameter_widgets = [] # built dynamically 80 | 81 | # Initial internal GUI state. 82 | self.OKbutton.set_sensitive(False) 83 | 84 | ''' 85 | Selection handling: connect selection event to callback function. 86 | Note a treeselection object is always part of a treeview: no need to create in glade 87 | I don't think this is done by connect_signals() above, since selection objects are not in glade? 88 | ''' 89 | # Mock menu tree 90 | self.mockmenu.get_selection().connect("changed", self.on_mockmenu_selection_changed) 91 | # Connect pre-selection event to callback function that determines what can be selected 92 | self.mockmenu.get_selection().set_select_function(self.filter_select_menu_item) 93 | 94 | # Commands list 95 | self.command_seq_listview.get_selection().connect("changed", self.on_commands_selection_changed) 96 | self.command_seq_listview.get_selection().set_select_function(self.filter_select_command) # Filtered: any row can be selected except 97 | 98 | # Set model of treeview to the treemodel of the current viewspec 99 | self.mockmenu.set_model(self.models[self.currentviewname].treemodel) 100 | 101 | # Set tooltips to be blurb column 102 | self.mockmenu.set_tooltip_column(2) 103 | 104 | builder.connect_signals(self) 105 | 106 | 107 | def safe_build(self, builder, name): 108 | ''' 109 | Call GTKBuilder to find a GTK widget in the glade file. 110 | Fail if not found. 111 | Return reference to the built widget. 112 | ''' 113 | gtk_object = builder.get_object(name) 114 | if gtk_object is None: 115 | raise RuntimeError("GTKBuilder error: glade file mismatches code") 116 | return gtk_object 117 | 118 | 119 | def main(self): 120 | self.mainwidget.show_all() # April 2011 WAS show() 121 | gtk.main() # event loop 122 | 123 | 124 | ''' 125 | Callbacks. 126 | 127 | !!! Note that here assistant is NOT a widget but an enclosing class. 128 | In callbacks, widget is the assistant.mainwidget. 129 | ''' 130 | 131 | def on_buttonOK_clicked(self, widget): 132 | ''' Save and present summary dialog''' 133 | print "OK button" 134 | self.apply() 135 | 136 | def on_buttonCancel_clicked(self, widget): 137 | ''' TODO confirm''' 138 | gtk.main_quit() 139 | 140 | def on_dialog1_close(self, widget): 141 | ''' TODO confirm''' 142 | gtk.main_quit() 143 | 144 | def message_dialog(self, message): 145 | ''' Display a modal summary dialog with no parent and no buttons''' 146 | dialog = gtk.MessageDialog(flags = gtk.DIALOG_MODAL, 147 | buttons=gtk.BUTTONS_OK, 148 | type=gtk.MESSAGE_INFO, 149 | message_format=message) 150 | dialog.run() # run, not show, to wait for user response 151 | dialog.destroy() 152 | 153 | 154 | def apply(self): 155 | ''' 156 | Do the main action: 157 | create a plugin. 158 | 159 | GimpScripter should be atomic: don't do anything permanent unless it all can be done. 160 | That is, no cleanup on cancel. 161 | ''' 162 | self.validate_and_capture_parameters(self.selected_command_index) # Capture parameters for selected command 163 | self.spec.wrapping.set_menu_name(self.name_textentry.get_text()) # Put final name in spec 164 | print self.spec.commands.param_list 165 | generate.generate(self.spec) 166 | self.mainwidget.destroy() 167 | self.message_dialog(generate.summarize()) # Wrapper is generated, but summarize 168 | 169 | 170 | """ 171 | def create_labels(self): 172 | for i in range(0, 20): 173 | # pack a separator in the vbox 174 | print "Put label in vbox" 175 | label = gtk.Label("foo") 176 | #label.set_use_underline(True) 177 | label.set_alignment(0.1, 0.5) 178 | label.show() 179 | self.parameter_box.pack_start(label, expand=False) 180 | self.parameter_widgets.append(label) 181 | """ 182 | 183 | def destroy_old_parameter_widgets(self): 184 | ''' Destroy old widgets, really only one, a GtkTable ''' 185 | if self.parameter_widgets: 186 | for widget in self.parameter_widgets: 187 | print "Destroy", widget 188 | widget.destroy() 189 | self.parameter_widgets.remove(widget) 190 | 191 | def create_parameter_widget(self, pdefs, defaults, toggles): 192 | ''' 193 | Create one parameter widget and pack front into a GtkBox. 194 | The created widget is usually a GtkTable, one row per parameter. 195 | This returns a wrapper of the GtkTable. 196 | ''' 197 | table = param_dialog.GimpParamWidget( 198 | self.parameter_box, # pack parameter widgets in this box 199 | self.mainwidget, # parent window, for error dialogs 200 | pdefs, 201 | defaults, 202 | self.set_sensitive_settings_valid, # Callback when user touches a widget 203 | is_use_toggles = True, 204 | toggle_label="Constant", 205 | toggle_initial_values= toggles) 206 | return table 207 | 208 | 209 | def prepare_parameter_page(self, commands, index, is_first_time=False): 210 | ''' 211 | Prepare dynamic widgets for parameter page. 212 | Dynamic: depends on count and type of parameters. 213 | Its parent and window is the parameter page (a scrolling window child of assistant) 214 | Index is the index of the newly selected command. 215 | is_first_time: whether this is a new command, use defaults instead of user-entered values 216 | ''' 217 | assert commands # should not get here with empty commands 218 | 219 | if self.selected_command_index is not None: # If parameters for some command are displayed 220 | self.validate_and_capture_parameters(self.selected_command_index) # capture displayed parameters 221 | self.destroy_old_parameter_widgets() 222 | 223 | self.selected_command_index = index 224 | 225 | """ 226 | # Create a set of parameter widgets, one for each command, packed into a vbox. 227 | for i in range(0, len(commands)): 228 | command = commands.get_command_for(i) 229 | print "Param widget for", command.name 230 | 231 | # Then: commands.param_list.get_nonhidden_pdefs_for(i) 232 | """ 233 | 234 | pdefs = commands.param_list.get_nonhidden_pdefs_for(index) 235 | if pdefs: 236 | if is_first_time: # if first time displaying parameters for this command 237 | values = commands.param_list.get_nonhidden_defaults_for(index) # Display defaults 238 | toggles = [False for i in range(len(values))] # Toggles all initially False, not deferred 239 | else: 240 | # Restore widget to previous appearance when user last viewed it 241 | values = commands.param_list.get_nonhidden_values_for(index) # Display values user entered previously 242 | toggles = commands.param_list.get_defers_for(index) # Display previous toggle values 243 | print "Len toggles", len(toggles) 244 | # Put nonhidden parameters of indexth command into new widget 245 | self.parameter_widgets.append(self.create_parameter_widget(pdefs, values, toggles)) 246 | # else no parameters to show 247 | 248 | # Must capture parameters before generating in case this is last command and user never touches it 249 | 250 | 251 | 252 | def validate_and_capture_parameters(self, index): 253 | ''' 254 | Validate user entered parameters and capture to the spec. 255 | For the one command whose parameters are displayed. 256 | !!! But the command might not have any parameters. 257 | ''' 258 | print "Validating parameters for command ordinal", index 259 | if self.parameter_widgets: 260 | self.spec.commands.param_list.preset(*self.parameter_widgets[0].validate(), command_index=index) 261 | 262 | 263 | ''' 264 | Routines to sensitize buttons 265 | ''' 266 | def set_sensitive_settings_valid(self, direction): 267 | ''' 268 | A callback from parameter widget when touched and loses focus. 269 | Direction is False if invalid entry. 270 | ''' 271 | self.is_settings_valid = direction 272 | 273 | 274 | def set_sensitive_completion(self): 275 | ''' Set sensitive buttons if complete ''' 276 | # If: 277 | # - user entered at least one command 278 | # - and named the wrapper 279 | # - and settings are valid 280 | is_sensitive = len(self.name_textentry.get_text()) > 0 \ 281 | and len(self.spec.commands) > 0 \ 282 | and self.is_settings_valid 283 | self.OKbutton.set_sensitive(is_sensitive) 284 | 285 | 286 | 287 | ''' 288 | Callbacks for child or grandchild widgets (widgets in pages of the assistant.) 289 | ''' 290 | """ 291 | def on_radiobutton_toggled(self, button): 292 | ''' 293 | Both radiobuttons signal to this handler. 294 | Grouping handled by gtk/glade. 295 | Since there are only two buttons, ignore which is calling back. 296 | ''' 297 | # If user wants to preset parameters instead of use last values 298 | is_preset = self.radiobutton_usepreset.get_active() 299 | # Sensitize the parameter widgets / tables 300 | for widget in self.parameter_widgets: 301 | widget.set_sensitive(is_preset) 302 | self.spec.command.set_is_use_last(not is_preset) # the model 303 | """ 304 | 305 | 306 | def on_entry1_changed(self, widget): 307 | ''' Signal when user types or deletes a character or pastes, etc. In name TextEntry. ''' 308 | self.set_sensitive_completion() 309 | 310 | 311 | def on_mockmenu_selection_changed(self, theSelection): 312 | ''' 313 | Callback for mockmenu to choose a target command. 314 | Treeview widget, signal=selectionChanged on theSelection which is a gtk.TreeSelection 315 | Action: keep a GUI state variable showing user has made selection. 316 | Single selection is enforced by default. 317 | ''' 318 | 319 | if not self.is_settings_valid: 320 | return # TODO disallow the click beforehand 321 | 322 | global is_new_proc_selection 323 | is_new_proc_selection = True 324 | 325 | model, path = theSelection.get_selected() 326 | # path is a GTK_tree_iter 327 | if path: # if selection was made 328 | # Alternative: column 1 (hidden?) drives selection (ie is the model value) 329 | name = model.get_value(path, 1) # column 1 is procname 330 | menupath = path_treemodel.get_path_string(model, path) 331 | else: 332 | name = None 333 | menupath = None 334 | 335 | if name: 336 | try: 337 | # Every click, add a command 338 | # For now, append 339 | # TODO insert in position selected in the command list widget 340 | command = specification.CommandSpec(name, menupath) 341 | self.spec.commands.append(command) 342 | # Feed it back 343 | self.command_seq_listview.get_model().append([menupath]) # tuple of column values 344 | 345 | # It is focused in the mock menu. Unselect anything in the command list. OR select the newly appended command. 346 | self.command_seq_listview.get_selection().unselect_all() 347 | 348 | # Show parameters, for the first time 349 | self.prepare_parameter_page(self.spec.commands, len(self.spec.commands)-1, is_first_time = True) 350 | except: 351 | ''' 352 | Certain plugins raise KeyError on parameters e.g. image/colors/map/rearrange on INT8ARRAY 353 | Or for any other exception, disallow proceeding. 354 | User must choose another plugin, or Cancel. 355 | ''' 356 | param_dialog.warning_dialog(self.mainwidget, "GimpScripter exception: the item you chose can't be used.") 357 | raise # Exception to stderr (a property of gtk is to not crash on exceptions.) 358 | else: # try succeeded 359 | self.set_sensitive_completion() # Possibly allow user to complete 360 | 361 | 362 | def on_commands_selection_changed(self, theSelection): 363 | ''' 364 | Callback for commands list to select a target command. 365 | Treeview widget, signal=selectionChanged on theSelection which is a gtk.TreeSelection 366 | Action: show the parameters for this command 367 | Remember this is a hint, there might not be a selection. 368 | ''' 369 | model, path = theSelection.get_selected() 370 | # path is a GTK_tree_iter 371 | if path: # if selection was made 372 | # Alternative: column 1 (hidden?) drives selection (ie is the model value) 373 | menupath = model.get_value(path, 0) # column 0 is command menupath, a string 374 | intpath = model.get_path(path) # get [int,int] form of path 375 | index = intpath[0] # index of theSelection 376 | else: 377 | pass 378 | 379 | # unselect in the mock menu 380 | self.mockmenu.get_selection().unselect_all() 381 | 382 | self.prepare_parameter_page(self.spec.commands, index ) 383 | 384 | 385 | ''' Callbacks when user clicks in a treeview. Return whether to allow selection. ''' 386 | 387 | def filter_select_menu_item(self, path): 388 | ''' 389 | Callback: return boolean indicating whether selection is allowed. 390 | Let user select only leaves. 391 | Filters user's clicks in a mockmenu, and ignores those clicks that are not in leaves. 392 | !!! Also disallow clicks if settings are invalid. 393 | ''' 394 | ''' 395 | Note: tree levels without children are a complication. 396 | The definition of a leaf here is: row with non-empty second, hidden column. 397 | If search filtering takes out all leaf rows under a tree level row, 398 | that tree level row has no children. 399 | If we choose to display tree level rows without children, 400 | then definition of leaf is not: has no children. 401 | ''' 402 | if not self.is_settings_valid: 403 | self.message_dialog("You can't select another command while settings are invalid for the current command.") 404 | model = self.mockmenu.get_model() # !!! Get treeview's model, which varies in this app. 405 | return model.get_value(model.get_iter(path), 1) != "" and self.is_settings_valid 406 | # WAS return not model.iter_has_child(model.get_iter(path)) # no child means leaf 407 | 408 | 409 | def filter_select_command(self, path): 410 | ''' Disallow selection in command list if settings for current command are invalid ''' 411 | if not self.is_settings_valid: 412 | self.message_dialog("You can't select another command while settings are invalid for the current command.") 413 | return self.is_settings_valid 414 | 415 | -------------------------------------------------------------------------------- /gimpscripter/gui/param_dialog.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | ''' 4 | GUI to ask user for parameters of a plugin. 5 | 6 | Differs from the normal dialog (done by gimpfu): 7 | -doesn't run the plugin on OK button 8 | -adds a toggle button to each parameter, meaning defer entry till later 9 | -not necessarily in a dialog 10 | 11 | Returns: 12 | -tuple of actual parameters that the user entered (or the initial values.) 13 | -tuple of toggle states (boolean) for each parameter 14 | 15 | If the user declines to enter a value for a parameter, 16 | the initial value is returned. 17 | 18 | Note the initial values are not the "default" values of the plugin, 19 | or the last values used, 20 | since for now, is hard to get for all but Python plugins. 21 | 22 | (I suppose this is general enough that the toggle could mean something else.) 23 | 24 | Largely derived from gimpfu.py. 25 | They should be unified. 26 | 27 | Copyright 2010 Lloyd Konneker 28 | 29 | This program is free software; you can redistribute it and/or modify 30 | it under the terms of the GNU General Public License as published by 31 | the Free Software Foundation; either version 2 of the License, or 32 | (at your option) any later version. 33 | 34 | This program is distributed in the hope that it will be useful, 35 | but WITHOUT ANY WARRANTY; without even the implied warranty of 36 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 37 | GNU General Public License for more details. 38 | 39 | You should have received a copy of the GNU General Public License 40 | along with this program; if not, write to the Free Software 41 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 42 | 43 | ''' 44 | 45 | # This program can NOT be called standalone: depends on GIMP environment. 46 | 47 | from gimp import locale_directory 48 | from gimpenums import * 49 | # certain param types: their widgets require special treatment 50 | from gimpfu import * 51 | 52 | import pygtk 53 | pygtk.require('2.0') 54 | 55 | import gimpui 56 | import gtk 57 | import operator # for not_ 58 | 59 | from gimpscripter.gui import param_widgets # Future refactoring: should be shared with gimpfu, verbatim. 60 | 61 | 62 | 63 | import gettext 64 | t = gettext.translation('gimp20-python', locale_directory, fallback=True) 65 | _ = t.ugettext 66 | 67 | 68 | 69 | # From gimpfu but not needed? class error(RuntimeError): pass 70 | 71 | # Raised by dialog 72 | class CancelError(RuntimeError): pass 73 | 74 | 75 | 76 | ''' 77 | def get_defaults(proc_name): 78 | 79 | import gimpshelf 80 | (blurb, help, author, copyright, date, 81 | label, imagetypes, plugin_type, 82 | params, results, function, menu, domain, 83 | on_query, on_run) = _registered_plugins_[proc_name] 84 | 85 | key = "python-fu-save--" + proc_name 86 | 87 | if gimpshelf.shelf.has_key(key): 88 | return gimpshelf.shelf[key] 89 | else: 90 | # return the default values 91 | return [x[3] for x in params] 92 | ''' 93 | ''' 94 | I think that _registered_plugins_ only contains the set of plugins 95 | registered during this execution of the plugin.py file. 96 | gimpshelf can contain the last parameters for any prior invocation of the plugin, 97 | since the life of the shelf. 98 | When is the shelf renewed, and what if a procedure's parameter signature has changed? 99 | ''' 100 | 101 | def get_defaults(proc_name, paramdefs): 102 | ''' 103 | What it is: 104 | the 4th column of the registration (only available in the original, Python plugin.) 105 | 106 | What it should be: 107 | More generally, for any plugin, not just Python plugins. 108 | Gets the persisted parameter values changed by user on previous interaction, 109 | else the standard parameter values declared at plugin creation. 110 | ''' 111 | return [x[3] for x in paramdefs] 112 | 113 | 114 | 115 | def interact(proc_name, paramdefs): 116 | ''' 117 | This is a test harness. 118 | 119 | paramdefs are the paramdefs from the PDB, not from Pygimp. 120 | !!! Thus, they don't have the default (initial) values. 121 | ''' 122 | 123 | def run_script(run_params, toggles): 124 | print "Result", run_params, toggles 125 | 126 | # short circuit for no parameters ... 127 | if len(paramdefs) == 0: 128 | print "Don't call if no params" 129 | return 130 | 131 | ''' 132 | Here, defaults is the len of non-hidden params. 133 | Gimpfu.py does something different. 134 | ''' 135 | defaults = get_defaults(proc_name, paramdefs) 136 | 137 | # Build the dialog 138 | dialog = parameter_dialog(proc_name, 139 | run_script, 140 | paramdefs, 141 | defaults, 142 | blurb = "blurb", 143 | is_use_toggles=True, 144 | is_use_progress=False) 145 | 146 | gtk.main() 147 | 148 | if hasattr(dialog, 'res'): 149 | res = dialog.res 150 | dialog.destroy() 151 | return res 152 | else: 153 | dialog.destroy() 154 | raise CancelError 155 | 156 | 157 | ''' 158 | Verbatim from gimpfu.interact(). 159 | Would import them, but they are hidden in a nested scope. 160 | ''' 161 | def warning_dialog(parent, primary, secondary=None): 162 | # MODAL means parent can't be canceled to kill both 163 | # DESTROY_WITH_PARENT means parent CAN be canceled to kill both 164 | # ??? would the warning reappear and prevent user from exiting? 165 | dlg = gtk.MessageDialog(parent, gtk.DIALOG_MODAL, # gtk.DIALOG_DESTROY_WITH_PARENT, 166 | gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 167 | primary) 168 | 169 | def response(widget, id): 170 | widget.destroy() 171 | 172 | dlg.connect("response", response) 173 | dlg.show() 174 | # gimpfu.py used: dlg.run(), dlg.destroy() 175 | 176 | 177 | 178 | def error_dialog(parent, proc_name): 179 | import sys, traceback 180 | 181 | exc_str = exc_only_str = _('Missing exception information') 182 | 183 | try: 184 | etype, value, tb = sys.exc_info() 185 | exc_str = ''.join(traceback.format_exception(etype, value, tb)) 186 | exc_only_str = ''.join(traceback.format_exception_only(etype, value)) 187 | finally: 188 | etype = value = tb = None 189 | 190 | title = _("An error occured running %s") % proc_name 191 | dlg = gtk.MessageDialog(parent, gtk.DIALOG_DESTROY_WITH_PARENT, 192 | gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 193 | title) 194 | dlg.format_secondary_text(exc_only_str) 195 | 196 | alignment = gtk.Alignment(0.0, 0.0, 1.0, 1.0) 197 | alignment.set_padding(0, 0, 12, 12) 198 | dlg.vbox.pack_start(alignment) 199 | alignment.show() 200 | 201 | expander = gtk.Expander(_("_More Information")); 202 | expander.set_use_underline(True) 203 | expander.set_spacing(6) 204 | alignment.add(expander) 205 | expander.show() 206 | 207 | scrolled = gtk.ScrolledWindow() 208 | scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 209 | scrolled.set_size_request(-1, 200) 210 | expander.add(scrolled) 211 | scrolled.show() 212 | 213 | 214 | label = gtk.Label(exc_str) 215 | label.set_alignment(0.0, 0.0) 216 | label.set_padding(6, 6) 217 | label.set_selectable(True) 218 | scrolled.add_with_viewport(label) 219 | label.show() 220 | 221 | def response(widget, id): 222 | widget.destroy() 223 | 224 | dlg.connect("response", response) 225 | dlg.set_resizable(True) 226 | dlg.show() 227 | ''' 228 | End of verbatim from gimpfu.interact() 229 | ''' 230 | 231 | 232 | class GimpParamWidget(object): 233 | ''' 234 | Hides a widget that lets user set and defer actual parameters to a Gimp plugin. 235 | This widget holds many parameters (this widget a GtkTable.) 236 | ''' 237 | 238 | def __init__(self, 239 | packable_box, # a packable widget i.e. vbox or hbox 240 | parentwindow, # a window to parent error dialog, parent of packable_box 241 | paramdefs, 242 | defaults, # initial values of widgets 243 | advance_callback, # callback to caller to enable user advances in the GUI state machine 244 | is_use_toggles=False, 245 | toggle_label="Use", 246 | toggle_initial_values=[] # initial value of toggles 247 | ): 248 | 249 | ''' 250 | Build a Table widget of Gimp parameter editing widgets. 251 | Pack the table into packable_box, nested in parentwindow. 252 | Takes a callback to be called with Boolean whenever the validity of the table changes. 253 | Used to set sensitive widgets in the parent. 254 | ''' 255 | self.advance_callback = advance_callback 256 | self.is_use_toggles = is_use_toggles 257 | 258 | # Validation differs from gimpfu 259 | # Note we validate again later. 260 | def _validate_entry(wid): 261 | try: 262 | wid.get_value() 263 | self.advance_callback(True) # not quite right, might still exist invalid values 264 | return True 265 | except param_widgets.EntryValueError: 266 | warning_dialog(parentwindow, _("Invalid input for '%s'") % wid.desc) 267 | self.advance_callback(False) # to inhibit advance in GUI flow 268 | return False 269 | 270 | def focus_out_validate_entry(wid, id): 271 | ''' 272 | Callback when focus leaves parameter editing widget. 273 | We don't rip the focus back if invalid. 274 | ''' 275 | _validate_entry(wid) 276 | 277 | def changed_validate_entry(wid): 278 | ''' Callback on a change to value (keystroke, cut, etc.) ''' 279 | _validate_entry(wid) 280 | 281 | 282 | ''' 283 | def toggle_callback(toggle): 284 | # !!! Against Gnome GUI Guidelines to flip the label 285 | pass 286 | ''' 287 | 288 | if is_use_toggles: 289 | columncount = 3 290 | else : 291 | columncount = 2 292 | 293 | # a GtkTable widget for each parameter, with several columns 294 | self.table_wid = gtk.Table(len(paramdefs), columncount, False) 295 | self.table_wid.set_row_spacings(6) 296 | self.table_wid.set_col_spacings(6) 297 | # Pack the table widget into parent box 298 | packable_box.pack_start(self.table_wid, expand=False) 299 | self.table_wid.show() 300 | 301 | self.edit_wids = [] 302 | self.toggle_wids = [] 303 | for i in range(len(paramdefs)): 304 | pf_type = paramdefs[i][0] 305 | name = paramdefs[i][1] 306 | desc = paramdefs[i][2] 307 | # lkk Truncate the desc since it is not wrapping 308 | if len(desc) > 60 : 309 | desc = desc[0:60] 310 | def_val = defaults[i] 311 | 312 | label = gtk.Label(desc) 313 | label.set_use_underline(True) 314 | label.set_alignment(0.0, 0.5) 315 | self.table_wid.attach(label, 1, 2, i, i+1, xoptions=gtk.FILL) 316 | label.show() 317 | 318 | # certain types have additional field in paramdef, a range 319 | if pf_type in (PF_SPINNER, PF_SLIDER, PF_RADIO, PF_OPTION): 320 | wid = param_widgets._edit_mapping[pf_type](def_val, paramdefs[i][4]) 321 | else: 322 | wid = param_widgets._edit_mapping[pf_type](def_val) 323 | 324 | # !!! connect after so default handler gets signal first 325 | # certain types are validated each keystroke 326 | if pf_type not in (PF_COLOR, PF_LAYER, PF_CHANNEL, PF_FONT , 327 | PF_FILE, PF_FILENAME, PF_DIRNAME, PF_BRUSH, PF_PATTERN, PF_GRADIENT, PF_PALETTE): 328 | wid.connect_after("changed", changed_validate_entry) # validate each keystroke 329 | else: 330 | # these widgets have no changed signal 331 | wid.connect_after("focus-out-event", focus_out_validate_entry) # validate on focus change 332 | 333 | label.set_mnemonic_widget(wid) 334 | 335 | self.table_wid.attach(wid, 2,3, i,i+1, yoptions=0) 336 | 337 | if pf_type != PF_TEXT: 338 | wid.set_tooltip_text(desc) 339 | else: 340 | #Attach tip to TextView, not to ScrolledWindow 341 | wid.viewset_tooltip_text(desc) 342 | wid.show() 343 | 344 | wid.desc = desc 345 | self.edit_wids.append(wid) 346 | 347 | if is_use_toggles: 348 | toggle = gtk.CheckButton(label=toggle_label, use_underline=False) 349 | self.table_wid.attach(toggle, 3, 4, i, i+1, xoptions=gtk.FILL) 350 | # toggle.connect("toggled", toggle_callback) 351 | toggle.show() 352 | toggle.set_active(not toggle_initial_values[i]) # Initial toggle setting !!! Invert 353 | self.toggle_wids.append(toggle) 354 | 355 | print "Built widgets: ", len(self.edit_wids) 356 | self.table_wid.show() 357 | 358 | 359 | def validate(self): 360 | ''' 361 | Validate what the user entered, massage and return the data. 362 | Raise exception if any invalid values for their types. 363 | ''' 364 | params = [] 365 | toggles = [] 366 | 367 | i=0 368 | for wid in self.edit_wids: 369 | params.append(wid.get_value()) 370 | if self.is_use_toggles: 371 | toggles.append(self.toggle_wids[i].get_active()) 372 | i += 1 373 | # Any widget can raise param_widgets.EntryValueError: 374 | 375 | ''' 376 | !!! Invert sense of toggles. 377 | The widgets are labeled "Constants" but the rest of this treats them as "Defers" 378 | which is inverted, i.e. "Not constant" 379 | ''' 380 | toggles = map(operator.not_, toggles) 381 | return params, toggles 382 | 383 | ''' 384 | Implement parts of the widget API 385 | ''' 386 | def destroy(self): 387 | self.table_wid.destroy() 388 | 389 | def set_sensitive(self, truth): 390 | self.table_wid.set_sensitive(truth) 391 | 392 | 393 | 394 | 395 | def parameter_dialog( 396 | proc_name, 397 | run_func, # on OK button clicked, call with actual params 398 | paramdefs, # tuple, definitions of formal parameters 399 | defaults, # tuple of initial values: last used (persistent) or standard values 400 | # defaults is a misnomer, but commonly used. 401 | blurb = None, # displays blurb of plugin at top of dialog 402 | is_use_toggles=False, 403 | is_use_progress=True 404 | ): 405 | ''' 406 | Builds dialog to ask user for parameters. 407 | Runs inside a gtk event loop. 408 | 409 | Largely copied from gimpfu.interact() . 410 | Should be unified. 411 | 412 | Changes: 413 | made it a function with parameters 414 | added optional toggle button in each control (row of table) 415 | made the action func a parameter 416 | changed certain names: dialog => dlg (same widget, different names) 417 | changed certain names: params => paramdefs (more descriptive, formal parameter defs) 418 | made the progress bar optional 419 | Busted out table into separate class, taking tooltips with it. 420 | ''' 421 | 422 | ''' 423 | This is a call to libgimpui, via _gimpui.so (the Python binding), not gimpui.py. 424 | Uses libgimpui so that the dialog follows the Gimp theme, help policy, progress policy, etc. 425 | See libgimp/gimpui.c etc. 426 | ''' 427 | dialog = gimpui.Dialog(proc_name, 'python-fu', None, 0, None, proc_name, 428 | (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 429 | gtk.STOCK_OK, gtk.RESPONSE_OK)) 430 | 431 | dialog.set_alternative_button_order((gtk.RESPONSE_OK, gtk.RESPONSE_CANCEL)) 432 | 433 | dialog.set_transient() 434 | 435 | vbox = gtk.VBox(False, 12) 436 | vbox.set_border_width(12) 437 | dialog.vbox.pack_start(vbox) 438 | vbox.show() 439 | 440 | # part 1: blurb 441 | if blurb: 442 | # see gimpfu.py for excised domain i8n code 443 | box = gimpui.HintBox(blurb) 444 | vbox.pack_start(box, expand=False) 445 | box.show() 446 | 447 | # part 2: table of parameters 448 | 449 | # added from gimpfu 450 | def enable_OK(direction): 451 | dialog.set_response_sensitive(gtk.RESPONSE_OK, direction) 452 | 453 | table = GimpParamWidget(vbox, dialog, paramdefs, defaults, is_use_toggles, enable_OK) 454 | 455 | # part 3: progress 456 | if is_use_progress: 457 | progress_vbox = gtk.VBox(False, 6) 458 | vbox.pack_end(progress_vbox, expand=False) 459 | progress_vbox.show() 460 | 461 | progress = gimpui.ProgressBar() 462 | progress_vbox.pack_start(progress) 463 | progress.show() 464 | 465 | 466 | def response(dlg, id): 467 | if id == gtk.RESPONSE_OK: 468 | dlg.set_response_sensitive(gtk.RESPONSE_OK, False) 469 | dlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False) 470 | 471 | try: 472 | dialog.res = run_func(table.validate()) 473 | except param_widgets.EntryValueError: 474 | warning_dialog(dlg, _("Invalid input")) # WAS wid.desc here 475 | # dialog continues with OK and CANCEL insensitive? 476 | except Exception: 477 | dlg.set_response_sensitive(gtk.RESPONSE_CANCEL, True) 478 | error_dialog(dlg, proc_name) 479 | raise 480 | 481 | dlg.hide() 482 | 483 | dialog.connect("response", response) 484 | dialog.show() 485 | return dialog 486 | 487 | 488 | -------------------------------------------------------------------------------- /gimpscripter/gui/param_widgets.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Derived from gimpfu.py. 3 | Here I broke out the widgets for entering parameters to Gimp plugins. 4 | They were hidden inside interact(). 5 | More are in gimpui.py, see the mapping below. 6 | 7 | Copyright 2010 Lloyd Konneker 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | ''' 23 | 24 | from gimp import locale_directory 25 | 26 | import pygtk 27 | pygtk.require('2.0') 28 | 29 | import gimpui 30 | import gtk 31 | 32 | 33 | 34 | import gettext 35 | t = gettext.translation('gimp20-python', locale_directory, fallback=True) 36 | _ = t.ugettext 37 | 38 | 39 | ''' 40 | These copied verbatim from gimpfu.py. 41 | Too laborius to import gimpfu and use eg gimpfu.PF_INT8 . 42 | !!! Keep them the same if gimpfu changes. 43 | ''' 44 | from gimpfu import * 45 | 46 | 47 | # Raised by widgets 48 | class EntryValueError(Exception): 49 | pass 50 | 51 | 52 | # widgets for entering params for Gimp plugins 53 | 54 | class StringEntry(gtk.Entry): 55 | def __init__(self, default=''): 56 | gtk.Entry.__init__(self) 57 | self.set_text(str(default)) 58 | 59 | def get_value(self): 60 | return self.get_text() 61 | 62 | class TextEntry(gtk.ScrolledWindow): 63 | def __init__ (self, default=''): 64 | gtk.ScrolledWindow.__init__(self) 65 | self.set_shadow_type(gtk.SHADOW_IN) 66 | 67 | self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 68 | self.set_size_request(100, -1) 69 | 70 | self.view = gtk.TextView() 71 | self.add(self.view) 72 | self.view.show() 73 | 74 | self.buffer = self.view.get_buffer() 75 | 76 | self.set_value(str(default)) 77 | 78 | def set_value(self, text): 79 | self.buffer.set_text(text) 80 | 81 | def get_value(self): 82 | return self.buffer.get_text(self.buffer.get_start_iter(), 83 | self.buffer.get_end_iter()) 84 | 85 | class IntEntry(StringEntry): 86 | def get_value(self): 87 | try: 88 | return int(self.get_text()) 89 | except ValueError, e: 90 | raise EntryValueError, e.args 91 | 92 | class FloatEntry(StringEntry): 93 | def get_value(self): 94 | try: 95 | return float(self.get_text()) 96 | except ValueError, e: 97 | raise EntryValueError, e.args 98 | 99 | # class ArrayEntry(StringEntry): 100 | # def get_value(self): 101 | # return eval(self.get_text(), {}, {}) 102 | 103 | 104 | def precision(step): 105 | # calculate a reasonable precision from a given step size 106 | if math.fabs(step) >= 1.0 or step == 0.0: 107 | digits = 0 108 | else: 109 | digits = abs(math.floor(math.log10(math.fabs(step)))); 110 | if digits > 20: 111 | digits = 20 112 | return int(digits) 113 | 114 | class SliderEntry(gtk.HScale): 115 | # bounds is (upper, lower, step) 116 | def __init__(self, default=0, bounds=(0, 100, 5)): 117 | step = bounds[2] 118 | self.adj = gtk.Adjustment(default, bounds[0], bounds[1], 119 | step, 10 * step, 0) 120 | gtk.HScale.__init__(self, self.adj) 121 | self.set_digits(precision(step)) 122 | 123 | def get_value(self): 124 | return self.adj.value 125 | 126 | class SpinnerEntry(gtk.SpinButton): 127 | # bounds is (upper, lower, step) 128 | def __init__(self, default=0, bounds=(0, 100, 5)): 129 | step = bounds[2] 130 | self.adj = gtk.Adjustment(default, bounds[0], bounds[1], 131 | step, 10 * step, 0) 132 | gtk.SpinButton.__init__(self, self.adj, step, precision(step)) 133 | 134 | class ToggleEntry(gtk.ToggleButton): 135 | def __init__(self, default=0): 136 | gtk.ToggleButton.__init__(self) 137 | 138 | self.label = gtk.Label(_("No")) 139 | self.add(self.label) 140 | self.label.show() 141 | 142 | self.connect("toggled", self.changed) 143 | 144 | self.set_active(default) 145 | 146 | def changed(self, tog): 147 | if tog.get_active(): 148 | self.label.set_text(_("Yes")) 149 | else: 150 | self.label.set_text(_("No")) 151 | 152 | def get_value(self): 153 | return self.get_active() 154 | 155 | class RadioEntry(gtk.VBox): 156 | def __init__(self, default=0, items=((_("Yes"), 1), (_("No"), 0))): 157 | gtk.VBox.__init__(self, homogeneous=False, spacing=2) 158 | 159 | button = None 160 | 161 | for (label, value) in items: 162 | button = gtk.RadioButton(button, label) 163 | self.pack_start(button) 164 | button.show() 165 | 166 | button.connect("toggled", self.changed, value) 167 | 168 | if value == default: 169 | button.set_active(True) 170 | self.active_value = value 171 | 172 | def changed(self, radio, value): 173 | if radio.get_active(): 174 | self.active_value = value 175 | 176 | def get_value(self): 177 | return self.active_value 178 | 179 | class ComboEntry(gtk.ComboBox): 180 | def __init__(self, default=0, items=()): 181 | store = gtk.ListStore(str) 182 | for item in items: 183 | store.append([item]) 184 | 185 | gtk.ComboBox.__init__(self, model=store) 186 | 187 | cell = gtk.CellRendererText() 188 | self.pack_start(cell) 189 | self.set_attributes(cell, text=0) 190 | 191 | self.set_active(default) 192 | 193 | def get_value(self): 194 | return self.get_active() 195 | 196 | def FileSelector(default=''): 197 | if default and default.endswith('/'): 198 | selector = DirnameSelector 199 | if default == '/': default = '' 200 | else: 201 | selector = FilenameSelector 202 | return selector(default) 203 | 204 | class FilenameSelector(gtk.FileChooserButton): 205 | def __init__(self, default='', save_mode=False): 206 | gtk.FileChooserButton.__init__(self, 207 | _("Python-Fu File Selection")) 208 | self.set_action(gtk.FILE_CHOOSER_ACTION_OPEN) 209 | if default: 210 | self.set_filename(default) 211 | 212 | def get_value(self): 213 | return self.get_filename() 214 | 215 | class DirnameSelector(gtk.FileChooserButton): 216 | def __init__(self, default=''): 217 | gtk.FileChooserButton.__init__(self, 218 | _("Python-Fu Folder Selection")) 219 | self.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) 220 | if default: 221 | self.set_filename(default) 222 | 223 | def get_value(self): 224 | return self.get_filename() 225 | 226 | # Define a mapping of param types to edit objects ... 227 | # Used to build a table of widgets in the dialog for a plugin 228 | ''' 229 | !!! This has been altered for Gimpscripter: 230 | At generate time, for ephemera, we let user enter a string. 231 | We DO NOT let user choose from a list of ephemera existing at generate time. 232 | At runtime, if the parameter is deferred, we DO let user choose... 233 | ''' 234 | 235 | _edit_mapping = { 236 | PF_INT8 : IntEntry, 237 | PF_INT16 : IntEntry, 238 | PF_INT32 : IntEntry, 239 | PF_FLOAT : FloatEntry, 240 | PF_STRING : StringEntry, 241 | #PF_INT8ARRAY : ArrayEntry, 242 | #PF_INT16ARRAY : ArrayEntry, 243 | #PF_INT32ARRAY : ArrayEntry, 244 | #PF_FLOATARRAY : ArrayEntry, 245 | #PF_STRINGARRAY : ArrayEntry, 246 | PF_COLOR : gimpui.ColorSelector, 247 | # These are ephemerals, doesn't make sense to show a chooser at generation time 248 | PF_IMAGE : StringEntry, # at runtime is: gimpui.ImageSelector, 249 | PF_LAYER : StringEntry, # at runtime is: gimpui.LayerSelector, 250 | PF_CHANNEL : StringEntry, # at runtime is: gimpui.ChannelSelector, 251 | PF_DRAWABLE : StringEntry, # at runtime is: gimpui.DrawableSelector, 252 | PF_VECTORS : StringEntry, # at runtime is: gimpui.VectorsSelector, 253 | 254 | PF_TOGGLE : ToggleEntry, 255 | PF_SLIDER : SliderEntry, 256 | PF_SPINNER : SpinnerEntry, 257 | PF_RADIO : RadioEntry, 258 | PF_OPTION : ComboEntry, 259 | 260 | PF_FONT : gimpui.FontSelector, 261 | # Next three also ephemeral? 262 | PF_FILE : FileSelector, 263 | PF_FILENAME : FilenameSelector, 264 | PF_DIRNAME : DirnameSelector, 265 | PF_BRUSH : gimpui.BrushSelector, 266 | PF_PATTERN : gimpui.PatternSelector, 267 | PF_GRADIENT : gimpui.GradientSelector, 268 | PF_PALETTE : gimpui.PaletteSelector, 269 | PF_TEXT : TextEntry 270 | } 271 | 272 | 273 | -------------------------------------------------------------------------------- /gimpscripter/macros.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Macros: generally, text templates with substitutions. 5 | Here specifically, Python code that is a seq of or a nesting of calls to Gimp PDB procedures. 6 | 7 | This includes : 8 | - support for macros 9 | - macro definitions themselves 10 | 11 | A author-user COULD write their own macros by altering this file. 12 | 13 | Notes for writing macros: 14 | Use double quotes outside, single quotes inside a macro. 15 | Use "ephemera.top(PF_FOO)" to refer to the currently active object of type PF_FOO 16 | Use $foo, $bar, etc. for placeholders for parameters foo, bar, ... Note the placeholder names 17 | should be the same as the parameter names in the parameter def (ParamDef). 18 | 19 | Copyright 2010 Lloyd Konneker 20 | 21 | This program is free software; you can redistribute it and/or modify 22 | it under the terms of the GNU General Public License as published by 23 | the Free Software Foundation; either version 2 of the License, or 24 | (at your option) any later version. 25 | 26 | This program is distributed in the hope that it will be useful, 27 | but WITHOUT ANY WARRANTY; without even the implied warranty of 28 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 29 | GNU General Public License for more details. 30 | 31 | You should have received a copy of the GNU General Public License 32 | along with this program; if not, write to the Free Software 33 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 34 | ''' 35 | 36 | from gimpfu import * # for PF types 37 | 38 | ''' A macro definition comprises: 39 | "macro-name" : # use dash or underbar, usually name matches an entry in map_procedures.py 40 | ("text", # quoted Python code text with $placeholders (macro parameters) 41 | ( # a tuple of one or more... 42 | (type, parameterName, desc), ... # ParamDef 43 | ), 44 | "blurb" # quoted blurb 45 | ) 46 | 47 | !!! Note the comma after the first ParamDef is always needed else it is not a tuple!!! 48 | !!! Note placeholders must start with a $ and then a valid Python identifier e.g. starting with a letter 49 | !!! A placeholder name can appear in many places in the body. 50 | !!! and the placeholders should match the parameterName 51 | !!! Any sequence should have proper indentation (2 spaces) between statements (after a newline) NOT inside a raw string. 52 | !!! Any procedure names should use underbar, not dash 53 | !!! Any procedure names should be prefixed with "pdb." 54 | 55 | ''' 56 | 57 | from string import Template 58 | 59 | # Create a new channel named bar from red channel 60 | macros = { \ 61 | "macro-channel-new" : ("pdb.gimp_image_add_channel(ephemera.top(PF_IMAGE), pdb.gimp_channel_new_from_component(ephemera.top(PF_IMAGE), 0, $channelName ), 1)", # macro text 62 | ((PF_STRING, 'channelName', 'The name to give to the channel'), ), # macro pdef tuple 63 | "Create new channel and add it to image."), # macro blurb 64 | "macro-layer-copy" : ("pdb.gimp_layer_copy(ephemera.top(PF_LAYER), $addAlpha)\n pdb.gimp_displays_flush()", ((PF_INT32, 'addAlpha', 'Add an alpha channel?'), ), 65 | "Copy layer and add it to image BROKEN?"), 66 | # Add layer from visible then nested add to image. User must name the layer 67 | "macro-layer-new-visible" : ("pdb.gimp_image_add_layer(ephemera.top(PF_IMAGE), pdb.gimp_layer_new_from_visible(ephemera.top(PF_IMAGE), ephemera.top(PF_IMAGE), $layerName), 0)", 68 | ((PF_STRING, 'layerName', 'Layer name'), ), 69 | "Create layer from visible and add it to image on top."), 70 | # I also tried to lookup the layer later, but it is not in ephemera unless it is attached to image. 71 | # Add layer blank then nested add to image. User must name the layer. 72 | # Note the mode comes from the active layer, not image? Opacity 100, combination mode 0 for normal 73 | "macro-layer-new-blank-attached" : ("pdb.gimp_image_add_layer(ephemera.top(PF_IMAGE), pdb.gimp_layer_new(ephemera.top(PF_IMAGE), ephemera.top(PF_IMAGE).width, ephemera.top(PF_IMAGE).height, ephemera.top(PF_LAYER).mode, $layerName, 100, 0), 0)", 74 | ((PF_STRING, 'layerName', 'Layer name'), ), 75 | "Create blank layer like the image and add it to image on top."), 76 | # New display then flush. Justification: rarely a reason to create a display without flushing it. 77 | "macro-display-new" : ("pdb.gimp_display_new(ephemera.top(PF_IMAGE))\n pdb.gimp_displays_flush()", 78 | (), # Empty pdefs 79 | "Create new display from current image and flush it so user can see it."), 80 | # Context set brush with chooser. 81 | # !!! This upconverts the parameter type: 82 | # at generation time and runtime, author-user or user sees a brushChooser widget instead of a stringEntry 83 | "macro-context-choose-brush" : ("pdb.gimp_context_set_brush($brush)", 84 | ((PF_BRUSH, 'brush', 'Brush'), ), 85 | "Create layer from visible and add it to image on top."), 86 | } 87 | 88 | """ 89 | Work in progress 90 | # Paste as new layer (not available in PDB?) 91 | # Internal buffer to named buffer 92 | # New layer size of internal buffer 93 | # Add new layer to image 94 | # Paste named buffer into layer 95 | # Anchor 96 | "macro-paste-as-new-layer" : ("pdb.gimp_edit_named_paste('temp')\n pdb.gimp_image_add_layer(ephemera.top(PF_IMAGE), pdb.gimp_layer_new(ephemera.top(PF_IMAGE), pdb.gimp_buffer_get_width('temp'), pdb.gimp_buffer_get_height('temp'), pdb.gimp_buffer_get_height('temp'), pdb.gimp_buffer_get_image_type('temp'), $layerName, 100, 0), 0)\n pdb.gimp_edit_paste()\n pdb.gimp_floating_sel_anchor()", 97 | ((PF_STRING, 'layerName', 'Layer name'), ), 98 | "Paste into a new anchored layer and add it to image on top."), 99 | """ 100 | 101 | # pdb.gimp_image_add_layer(ephemera.top(PF_IMAGE), ephemera.top(PF_LAYER), 1) 102 | 103 | def is_macro(name): 104 | return macros.has_key(name) 105 | 106 | 107 | # Getters 108 | # TODO class for macros 109 | 110 | def get_pdefs_for(name): 111 | result = macros[name][1] # second field is list of pdefs 112 | if not isinstance(result, tuple): 113 | raise RuntimeError("The pdefs for a GimpScripter macro must be a tuple") 114 | return result 115 | 116 | def get_blurb(name): 117 | return macros[name][2] 118 | 119 | def template_for(name): 120 | ''' Return the template for macro name ''' 121 | return Template(macros[name][0]) # 0 is the text 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /gimpscripter/mockmenu/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gimpscripter/mockmenu/db_treemodel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | This understands the db, viewspecs, gtk.treemodel. 5 | IE how to load a gtk.treemodel from the db according to the viewspec for an inspector app. 6 | 7 | Similar to classical views on a database: different ways of organizing, viewing the same data. 8 | 9 | The viewspec tells which attributes of objects in the db are paths, types, categories (sets of types). 10 | 11 | Copyright 2010 Lloyd Konneker 12 | 13 | This program is free software; you can redistribute it and/or modify 14 | it under the terms of the GNU General Public License as published by 15 | the Free Software Foundation; either version 2 of the License, or 16 | (at your option) any later version. 17 | 18 | This program is distributed in the hope that it will be useful, 19 | but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | GNU General Public License for more details. 22 | 23 | You should have received a copy of the GNU General Public License 24 | along with this program; if not, write to the Free Software 25 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 26 | ''' 27 | 28 | # Does not need: from gimpfu import * 29 | # Hide that the db was loaded from gimp or any other specific source. 30 | 31 | import pygtk 32 | pygtk.require("2.0") 33 | import gtk 34 | 35 | import types 36 | import string # for maketrans 37 | 38 | # our own sub module, must be installed alongside 39 | from gimpscripter.mockmenu import path_treemodel # load tree by set of paths 40 | 41 | 42 | 43 | # Constants defining the types of views 44 | VIEW_TYPE_LIST = "List" # list view on name (no attribute from the dict value) 45 | VIEW_TYPE_TYPE = "Type" # hierarchal view on attribute that is a type having elementary values 46 | VIEW_TYPE_SLASHPATH = "SlashPath" # hierarchal view on attribute having values that are slash delimited paths 47 | VIEW_TYPE_CATEGORY = "Category" # hierarchal view on attribute having values that name sets of types 48 | # TBD use these 49 | 50 | 51 | class ViewSpec(): 52 | ''' 53 | A specification of a GUI treeview of a db. 54 | Many treeviews per db. 55 | ''' 56 | def __init__(self, viewname, attrname, attrtype, typedict, db, filterdict): 57 | # TBD sanity checking, types are StringType and DictType 58 | self.viewname = viewname # Displayed name of the view 59 | self.attrname = attrname # Attribute of objects in db. Attribute values populate treemodel of treeview. 60 | self.type = attrtype # Type of the attribute (and thus of the treeview) 61 | self.typedict = typedict # Dictionary of unique values in the attribute, maps to a string for display in treeview. 62 | self.db = db # Dictionary of objects to be browsed/inspected 63 | self.filterdict = filterdict # db and its filter dict one-to-one 64 | 65 | 66 | class MyModel(): 67 | ''' 68 | Wrapper for treemodel with other attributes: 69 | a spec for a view 70 | a dict for filtering (in the viewspec) 71 | ''' 72 | def __init__(self, name, viewspec): 73 | # all treemodels have the same structure: three columns of type string 74 | self.treemodel = gtk.TreeStore(str, str, str) 75 | # all sorted same way 76 | self.treemodel.set_sort_column_id(0, gtk.SORT_ASCENDING) 77 | self.viewspec = viewspec 78 | 79 | def rebuild(self, pattern): 80 | ''' 81 | Search string changed. 82 | Rebuild filterdict and repopulate model. 83 | 84 | Performance Note: No special measures (disconnecting treeview from treemodel, or setting sort funct to None) 85 | since this seems fast enough. 86 | I tried treeview fixed_height_mode yes on row height, it didn't work. 87 | ''' 88 | self.viewspec.filterdict.filter(self.viewspec.db, pattern) 89 | _populateModel(self) 90 | 91 | def len(self): 92 | ''' The filtered length: count leaf rows: what user can select ''' 93 | return self.viewspec.filterdict.values().count(True) 94 | 95 | 96 | 97 | class TreeModelDictionary(dict): 98 | ''' 99 | A read only dictionary of gtk.treemodels 100 | Initializes itself with data passed in a dictofviews and you can't setitem. 101 | ''' 102 | 103 | def __init__(self, dictofviews): 104 | 105 | # Base class init 106 | dict.__init__(self) 107 | 108 | ''' 109 | Fill self with data 110 | Build (populate) and initialize sorting of several treestore models 111 | (all of same signature for one view). 112 | These are the base models of the filtered models. 113 | Models are specified by dictofviews. 114 | ''' 115 | for key, viewspec in dictofviews.iteritems(): 116 | model = MyModel(key, viewspec) 117 | _populateModel(model) 118 | # put model in a dictionary by name of model 119 | dict.__setitem__(self, key, model) 120 | 121 | 122 | 123 | def __setitem__(self): 124 | raise RuntimeError, "TreeModelDictionary is read-only" 125 | 126 | 127 | def _populateModel(model): 128 | ''' 129 | Populate according to the type. 130 | These type names are hardcoded, used in the viewspec. 131 | This understands which types use which building method. 132 | Future: some types might need parsing into slashed paths during building. 133 | ''' 134 | # Model is empty, put in a single row telling empty. 135 | if not model.len(): 136 | model.treemodel.clear() 137 | db = model.viewspec.db 138 | model.treemodel.append(None, ["", ""]) # second, hidden column empty so not clickable 139 | return 140 | 141 | if model.viewspec.type == "List": 142 | _build_list_tree(model) 143 | elif model.viewspec.type == "Type": 144 | _build_type_tree(model) 145 | elif model.viewspec.type == "SlashPath": 146 | _build_path_tree(model) 147 | elif model.viewspec.type == "Category": 148 | _build_type_tree(model) 149 | else: 150 | raise RuntimeError, "Unknown model type: " + model.viewspec.type 151 | 152 | 153 | def _build_list_tree(model): 154 | ''' 155 | Build a gtk.treemodel from a dictionary of objects. 156 | The treemodel is a list (a tree with only one level). 157 | Each row in the treemodel is a key from the dictionary. 158 | ''' 159 | model.treemodel.clear() 160 | db = model.viewspec.db 161 | for name in db.keys(): 162 | if model.viewspec.filterdict[name]: # is filtered out by search string? 163 | # append to treemodel in order, no parents 164 | piter = model.treemodel.append(None, [name, name]) # second, hidden column non-empty so clickable 165 | 166 | 167 | 168 | def _build_type_tree(model): 169 | ''' 170 | Build a gtk.treemodel from a dictionary of objects. 171 | Branch rows in the treemodel will be the set of types 172 | taken from the dictionary's object's attribute that is conceptually (to the user) a type 173 | (having string values from a small set). 174 | Leaf rows in the treemodel will be keys from the dictionary 175 | (which usually are the same as the ID or name attr of objects in the dictionary.) 176 | 177 | Also, the object's attribute can be a list of types (categories). 178 | That is, we build the same tree structure for viewtype: Type and viewtype: Category. 179 | For viewtype Category, a thing can appear in more than one row of the treeview. 180 | ''' 181 | model.treemodel.clear() 182 | 183 | # tree structure: one level: type names, second level: leaves, names of objects having that type 184 | # for example, a tree whose leaves are PDB procedure names and whose branches are procedure types. 185 | # EG, as an image of the displayed treeview, where ^ is an icon that collapses the branch 186 | # treeview: typedict: [db[].thing.name, .ztype]: viewspec: 187 | # ^Integer (1: Integer) foo, 1 ("Type and Name", "ztype", "Type", typedict) 188 | # foo (2: Char) bar, 1 189 | # bar zed, 2 190 | # ^Char 191 | # zed 192 | # 193 | # ^Integer (1: Integer) foo, Integer,Char ("Type and Name", "ztype", "Category", typedict) 194 | # foo (2: Char) bar, Integer 195 | # bar zed, Char 196 | # ^Char 197 | # foo *appears in two types 198 | # zed 199 | 200 | db = model.viewspec.db 201 | 202 | type_to_row = {} 203 | SEP = string.maketrans(',', ' ') 204 | 205 | # add parent rows to treemodel, remembering the tree path. 206 | # treerowreferences are persistent, treeiters and treepaths are not, so convert back and forth 207 | # do parents first, then children, so can raise exceptions for missing parents. 208 | for parent in model.viewspec.typedict.keys(): # for each unique value of the type 209 | # Parent means parent row in the treemodel. 210 | # Translate to friendly displayed string, different from type strings in the db 211 | displayedtype = model.viewspec.typedict[parent] 212 | piter = model.treemodel.append(None, [displayedtype, ""]) # second, hidden column empty 213 | row = gtk.TreeRowReference(model.treemodel, model.treemodel.get_path(piter)) 214 | type_to_row[parent] = row 215 | # add child rows to treemodel, looking up parent tree path 216 | for name, thing in db.iteritems(): # EG key is name, value is an object with an attribute that is a type. 217 | if not model.viewspec.filterdict[name]: # is filtered out by search string? 218 | continue 219 | # Get the value of the thing's attribute. The value is 'of the type'. 220 | # The name of the attribute is given in the viewspec for the model. 221 | value = eval("thing." + model.viewspec.attrname) 222 | # !!! Value can be a list of type names (a category) 223 | if model.viewspec.type == "Category": 224 | # For now, categories cannot be encoded, must be str 225 | # list words in commma OR whitespace delimited string 226 | # First translate commas to whitespace, then split on any whitespace 227 | valuelist = value.translate(SEP).split() 228 | if len(valuelist) < 1: 229 | valuelist = ["NA"] # the type of an empty category 230 | # Note that it is better to insure the db has no empty category values, substitue "NA" at load time 231 | for avalue in valuelist: 232 | try: 233 | parentrow = type_to_row[avalue] 234 | piter = model.treemodel.get_iter(parentrow.get_path()) 235 | model.treemodel.append(piter, [name, name]) # second use is as ID of procedure 236 | except KeyError: 237 | print "Key error: type not found in viewspec.typedict: ", avalue 238 | else: # viewtype is Type. 239 | try: 240 | parentrow = type_to_row[value] 241 | piter = model.treemodel.get_iter(parentrow.get_path()) 242 | model.treemodel.append(piter, [name, name]) # second use is as ID of procedure 243 | except KeyError: 244 | print "Key error: type not found in viewspec.typedict" 245 | 246 | 247 | 248 | ''' OLD 249 | # outer loop on unique values of the type, inner loop on the entire db 250 | for parent in model.viewspec.typedict.keys(): # for each unique value of the type 251 | # Parent means parent row in the treemodel. 252 | # Translate to friendly displayed string, different from type strings in the db 253 | displayedtype = model.viewspec.typedict[parent] 254 | piter = model.treemodel.append(None, [displayedtype, ""]) # second, hidden column empty 255 | # for each thing in the db by name 256 | print "Type: ", parent 257 | for name, thing in db.iteritems(): # EG key is name, value is an object with an attribute that is a type. 258 | if not model.viewspec.filterdict[name]: # is filtered out by search string? 259 | continue 260 | # Get the value of the thing's attribute. The value is 'of the type'. 261 | # The name of the attribute is given in the viewspec for the model. 262 | value = eval("thing." + model.viewspec.attrname) 263 | # !!! Value can be a list of type names (a category) 264 | if model.viewspec.type == "Category": 265 | valuelist = value.split(',') # list words in commma delimited string 266 | # For now, categories cannot be encoded, must be str 267 | # !!! TBD No exception if some or all of the types in the category are not in the typedict. 268 | if parent in valuelist: 269 | # Put the name of the thing in the view, second hidden column is as an ID 270 | model.treemodel.append(piter, [name, name]) 271 | else: # viewtype is Type. 272 | # If there is a mapping of types to other (friendly?) strings 273 | #if model.viewspec.typedict : 274 | # Decode numeric (or otherwise) values to strings 275 | # KeyError exception raised if not in typedict 276 | # value = model.viewspec.typedict[value] 277 | # If it matches the type of the outer loop (branch of the treemodel) 278 | print "Value: ", value 279 | if value == parent: 280 | print "Adding row" 281 | model.treemodel.append(piter, [name, name]) # second use is as ID of procedure 282 | ''' 283 | 284 | def _build_path_tree(model): 285 | ''' 286 | Understands: 287 | tree is path tree 288 | tree is given as a db of things having paths 289 | thing names installed at the path tree leaves (alternative). 290 | ''' 291 | print "Building path tree model" 292 | model.treemodel.clear() 293 | db = model.viewspec.db 294 | count = 0 295 | 296 | # For each (name, thing) in the db 297 | # Load tree from db[name].attrname.menupath 298 | for name, thing in db.iteritems(): 299 | if model.viewspec.filterdict[name]: # is filtered in by search string? 300 | if model.viewspec.attrname: # names are unique and attribute gives a path 301 | try: 302 | pathvalue = eval("thing." + model.viewspec.attrname) 303 | except: 304 | # Likely source of configuration errors, print more info. 305 | print "Inspect db must contain objects having repr method and attribute holding a path" 306 | raise 307 | else: 308 | pathvalue = name # the name itself is a path 309 | assert pathvalue != "" # !!! Each must have a path, even if just 310 | 311 | # Add thing to the treemodel 312 | # Formerly,we just adding the name of the thing. 313 | # Now we pass the thing along, and extract thing.name and more attributes, later 314 | if not path_treemodel.add_path(model.treemodel, thing, pathvalue): 315 | print "Duplicate path:", pathvalue, "to ID:", name 316 | # raise RuntimeError 317 | else: 318 | count += 1 319 | print "Count path tree model: ", count 320 | 321 | -------------------------------------------------------------------------------- /gimpscripter/mockmenu/map_procedures.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Map a menu item to a command. 3 | 4 | This helps populate a tree model in the GUI. 5 | Plugins are put into the tree model separately. 6 | Many commands here are PDB procedures. 7 | Some are macros (see macro.py.) 8 | 9 | Note that some PDB procedures map to Gimp menu items 10 | but this is NOT a documentation of that map, 11 | which involves a menubar and various pop-up context menus. 12 | 13 | We also change the semantics of some PDB procedure name WORDS: 14 | "New" here means new AND added to the image, whereas in some PDB procedures 15 | "New" means new and NOT attached to an image. 16 | 17 | This is hand-coded and might not be complete. 18 | 19 | Annotation: 20 | 21 | - "mismatch" there is a significant difference between the name and the menu path 22 | - "fabricated" there is no menu path in Gimp app itself 23 | - "diff" we changed the menu path from Gimp app itself to GimpScripter mock menus 24 | - "dupe" the command appears in more than one place in our mock menu 25 | - "already included" the command is a plugin and already included in the tree model 26 | 27 | Copyright 2010 Lloyd Konneker 28 | 29 | This program is free software; you can redistribute it and/or modify 30 | it under the terms of the GNU General Public License as published by 31 | the Free Software Foundation; either version 2 of the License, or 32 | (at your option) any later version. 33 | 34 | This program is distributed in the hope that it will be useful, 35 | but WITHOUT ANY WARRANTY; without even the implied warranty of 36 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 37 | GNU General Public License for more details. 38 | 39 | You should have received a copy of the GNU General Public License 40 | along with this program; if not, write to the Free Software 41 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 42 | ''' 43 | 44 | 45 | menu_to_procname = { \ 46 | "Layer/Transform/Offset" : "gimp-drawable-offset", 47 | # 48 | "Edit/Clear" : "gimp-edit-clear", 49 | "Edit/Copy" : "gimp-edit-copy", 50 | "Edit/Copy Visible" : "gimp-edit-copy-visible", 51 | "Edit/Cut" : "gimp-edit-cut", 52 | "Edit/Paste" : "gimp-edit-paste", 53 | "Edit/Paste as/New Image" : "gimp-edit-paste-as-new", #mismatch, 54 | # TODO "Edit/Paste as/New Layer" : "macro-paste-as-new-layer", # missing 55 | # Edit/Paste as/ are named to match the similar plugins below 56 | # "Edit/Paste as New/Brush" : "script-fu-paste-as-brush", # already included 57 | # "Edit/Paste as New/Pattern" : "script-fu-paste-as-pattern", # already included 58 | "Edit/Fill" : "gimp-edit-fill", 59 | "Edit/Buffer/Copy Named" : "gimp-edit-named-copy", 60 | "Edit/Buffer/Copy Visible Named" : "gimp-edit-named-copy-visible", 61 | "Edit/Buffer/Cut Named" : "gimp-edit-named-cut", 62 | "Edit/Buffer/Paste Named" : "gimp-edit-named-paste", 63 | "Edit/Stroke/Path" : "gimp-edit-stroke-vectors", 64 | # 65 | # TODO the next one is bogus: same as Edit/Paste as/New Image 66 | "File/Create/Acquire/From Clipboard" : "gimp-edit-paste-as-new", 67 | # Because File/Save is already a path, can't name this just File/Save, 68 | # must add 'By Extension' 69 | "File/Save/By extension" : "gimp-file-save", 70 | # 71 | "Image/Mode/Grayscale" : "gimp-image-convert-grayscale", 72 | "Image/Mode/Indexed" : "gimp-image-convert-indexed", 73 | "Image/Mode/RGB" : "gimp-image-convert-rgb", 74 | "Image/Crop/Crop to Selection" : "gimp-image-crop", 75 | "Image/New/Blank" : "gimp-image-new", # diff File/New 76 | "Image/New/Duplicate" : "gimp-image-duplicate", 77 | "Image/Display" : "gimp-display-new", # fabricated 78 | "Image/Delete" : "gimp-image-delete", # fabricated, requires OS file delete? 79 | "Image/Flatten" : "gimp-image-flatten", 80 | "Image/Resize/Canvas Size" : "gimp-image-resize", 81 | "Image/Resize/Fit Canvas to Layers" : "gimp-image-resize-to-layers", 82 | "Image/Scale" : "gimp-image-scale", # diff Image/Scale Image 83 | "Image/Transform/Flip" : "gimp-image-flip", # mismatch 84 | "Image/Transform/Rotate" : "gimp-image-rotate", # mismatch 85 | "Image/Add Layer" : "gimp-image-add-layer", # fabricated 86 | # Add Layer is not much use if we always add new layers to image 87 | # Image/Transform/Guillotine is a plugin 88 | # 89 | "Colors/Threshold" : "gimp-threshold", 90 | "Colors/Threshold Alpha" : "plug-in-threshold-alpha", # diff 91 | "Colors/Levels" : "gimp-levels", 92 | "Colors/Levels Stretch" : "gimp-levels-stretch", 93 | # 94 | # For now, if a menupath is a prefix of another, it isn't clickable as a command 95 | # So we add "Blank" here 96 | "Layer/New/Blank" : "gimp-layer-new", # diff 97 | "Layer/New/Blank Attached" : "macro-layer-new-blank-attached", 98 | "Layer/New/From Visible" : "macro-layer-new-visible", # diff 99 | # Omit since it doesn't add the layer to the image: "gimp-layer-new-from-visible" 100 | # !!! gimp-FOO-delete is only useful for a FOO NOT added to an image 101 | # gimp-layer-delete deprecated for gimp-item-delete 102 | "Layer/Delete" : "gimp-image-remove-layer", # mismatch also, is Layer/Structure/Delete Layer 103 | "Layer/Resize/To Boundary Size" : "gimp-layer-resize", # diff 104 | "Layer/Resize/To Image Size" : "gimp-layer-resize-to-image-size", # diff 105 | "Layer/Scale" : "gimp-layer-scale", # mismatch 106 | "Layer/Set Mode" : "gimp-layer-set-mode", 107 | "Layer/Translate" : "gimp-layer-translate", 108 | "Layer/Alpha/Add" : "gimp-layer-add-alpha", 109 | "Layer/Alpha/Remove" : "gimp-layer-flatten", # mismatch 110 | "Layer/Copy" : "macro-layer-copy", # WAS "gimp-layer-copy", 111 | "Layer/Copy active layer" : "gimp-layer-new-from-drawable", # mismatch 112 | 113 | "Layer/Active/Set" : "gimp-image-set-active-layer", # fabricated 114 | "Layer/Active/Get" : "gimp-image-get-active-layer", # fabricated 115 | "Layer/Anchor" : "gimp-floating-sel-anchor", # mismatch 116 | # 117 | "Select/All" : "gimp-selection-all", 118 | "Select/Float" : "gimp-selection-float", 119 | "Select/Invert" : "gimp-selection-invert", 120 | "Select/None" : "gimp-selection-none", 121 | "Select/By Color" : "gimp-by-color-select", # mismatch 122 | "Select/Modify/Border" : "gimp-selection-border", 123 | "Select/Modify/Feather" : "gimp-selection-feather", 124 | "Select/Modify/Grow" : "gimp-selection-grow", 125 | "Select/Modify/Sharpen" : "gimp-selection-sharpen", 126 | "Select/Modify/Shrink" : "gimp-selection-shrink", 127 | "Select/Save to Channel" : "gimp-selection-save", 128 | "Select/From path" : "gimp-vectors-to-selection", 129 | "Select/To path" : "plug-in-sel2path", 130 | "Select/Add/Channel" : "gimp-selection-load", #dupe 131 | # Select/To path is a plugin, not internal procedure but the plugin query doesn't get it ??? 132 | # 133 | "Drawable/Transform/Flip" : "gimp-drawable-transform-flip-simple", # mismatch 134 | "Drawable/Transform/Rotate" : "gimp-drawable-transform-rotate-simple", # mismatch 135 | "Drawable/Fill" : "gimp-drawable-fill","Select/Float" : "gimp-selection-float", 136 | 137 | # These have no presence in Gimp menus so we fabricate a menu item 138 | "Context/Pop" : "gimp-context-pop", # fabricated 139 | "Context/Push" : "gimp-context-push", 140 | "Context/Set/FG" : "gimp-context-set-foreground", 141 | "Context/Set/Brush" : "gimp-context-set-brush", # string parameter 142 | "Context/Set/Choose brush" : "macro-context-choose-brush", # PF_BRUSH parameter 143 | # "Context/Swap FG & BG colors" : "gimp-context-set-brush", 144 | "Channel/New" : "macro-channel-new", # WAS "gimp-channel-new", # fabricated or a context menu? 145 | # TODO we need a macro here pdb.gimp_image_add_channel(image, pdb.gimp_channel_new(FOO), 1) 146 | "Channel/Delete" : "gimp-image-remove_channel", # mismatch ????? 147 | "Channel/Copy" : "gimp-channel-copy", 148 | "Channel/Add to Selection" : "gimp-selection-load", # mismatch, dupe 149 | "Channel/Activate" : "gimp-image-set-active-channel", # fabricated 150 | # 151 | # We can't do this one until we exclude its parameter from the parameters page 152 | # or make param_widgets have a widget for a Display 153 | #"Display/Delete" : "gimp-display-delete", # fabricated: created by Image/Display 154 | "Display/New Hidden" : "gimp-display-new", # mismatch 155 | "Display/Flush" : "gimp-displays-flush", # mismatch, extra s 156 | "Display/New" : "macro-display-new", 157 | # 158 | "Path/Remove Named" : "gimp-path-delete", # mismatch, string param 159 | "Path/Remove Active" : "gimp-image-remove-vectors", # mismatch, hidden PF_VECTORS param 160 | "Path/Copy" : "gimp-vectors-copy", 161 | "Path/Import" : "gimp-vectors-import-from-file", 162 | "Path/Export" : "gimp-vectors-export-to-file", 163 | } 164 | 165 | -------------------------------------------------------------------------------- /gimpscripter/mockmenu/path_treemodel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | TBD should be renamed: load_treemodel_from_paths 5 | TBD an outer procedure that understand the db contains the paths? 6 | 7 | Load tree into gtk.treestore, 8 | where tree given as set of paths for leaves. 9 | In a path, items delimited by slash. 10 | Typically path is menupath or filepath. 11 | 12 | This is independent of the application, ie generic. 13 | Augments treemodel with an API for adding paths. 14 | 15 | Here, using two or more columns. 16 | All rows except for leaves have column 0 path item, empty column 1, etc. 17 | Leaf rows have empty column 0, column 1 is the internal ID of the object the path leads to. 18 | That is, the path is user strings, but the ID is internal. 19 | If your app doesn't make a distinction, just redundantly use the last item in path for leaf value. 20 | This does NOT allow multiple rows with the same path, and different leaf values. 21 | Column two might well be hidden from user view. 22 | 23 | Copyright 2010 Lloyd Konneker 24 | 25 | This program is free software; you can redistribute it and/or modify 26 | it under the terms of the GNU General Public License as published by 27 | the Free Software Foundation; either version 2 of the License, or 28 | (at your option) any later version. 29 | 30 | This program is distributed in the hope that it will be useful, 31 | but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | GNU General Public License for more details. 34 | 35 | You should have received a copy of the GNU General Public License 36 | along with this program; if not, write to the Free Software 37 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 38 | ''' 39 | 40 | ''' 41 | Programming notes: 42 | Another way to do this might use gtk.treerows, something like this search: 43 | (where iterchildren() is different from iter_children since it is a real iterator.) 44 | 45 | for row in treemodel: # top level 46 | if match_row(...) 47 | 48 | def match_row(rows, value): 49 | if not rows: return None 50 | for row in rows: 51 | if row[0] == value: return row 52 | result = match_row(row.iterchildren(), value) 53 | if result: return result 54 | return None 55 | ''' 56 | 57 | ''' 58 | Programming notes: 59 | !!! Surprise, the root is NOT always there, can be None. 60 | !!! get_iter_root is the same as get_iter_first, which is better named. 61 | ''' 62 | 63 | import warnings 64 | 65 | # This string constant should be used in the db 66 | # for any paths that are not known 67 | UNKNOWN_PATH_STRING = "" 68 | 69 | def add_path(model, leaf, path): 70 | ''' 71 | Add path to treestore, if not already there. 72 | Here, the path is a slash delimited string. 73 | Leaf is an object to added. 74 | Leaf must have a name attribute. 75 | Returns True if path was added, False if already exists. 76 | ''' 77 | # print leaf, path 78 | items = path.split('/') # parse path into items 79 | if len(items) < 1: 80 | warnings.warn("Empty path for leaf " + str(leaf)) 81 | return False 82 | # Sanity checking for "//" in path 83 | for item in items: 84 | if not item: 85 | warnings.warn("Empty submenu in path: %s" % path) 86 | # In beginning, don't have a parent. 87 | return __add_path_from_node(model, None, items, leaf) 88 | 89 | 90 | def get_path_string(model, iter): 91 | ''' Get a slash delimited string for a path given by iter, a treeIter. ''' 92 | iter_string = model.get_string_from_iter(iter) 93 | node_strings = iter_string.split(':') 94 | slashpath = "" 95 | pathstring = "" 96 | for node in node_strings: 97 | pathstring += node 98 | path = model.get_iter_from_string(pathstring) 99 | node_label = model.get_value(path, 0) 100 | slashpath += node_label 101 | pathstring += ':' 102 | slashpath += '/' 103 | slashpath = slashpath[0:len(slashpath)-1] # elide last slash 104 | return slashpath 105 | 106 | """ 107 | def __model_append(node, values): 108 | ''' Put a row in the model. This hides the number and type of columns we are adding''' 109 | in progress refactoring 110 | """ 111 | 112 | def __add_path_from_node(model, parent, pathitems, leaf): 113 | ''' 114 | Add path below parent, if not already there. 115 | This is private, recursive. 116 | Parent can be None 117 | Path is a list of items, not a string. 118 | Leaf is object to add as leaf. 119 | ''' 120 | if not parent: # first call in recursion 121 | iternode = model.get_iter_first() 122 | else: 123 | iternode = model.iter_children(parent) 124 | while iternode: 125 | if pathitems[0] == model.get_value(iternode, column=0): # if match 126 | if len(pathitems) <= 1: 127 | # Found the complete path 128 | if pathitems[0] == UNKNOWN_PATH_STRING : # if lacking a meaningful path 129 | # New child here (under at top level.) 130 | # Note this makes the treeview leaves appear to user as two kinds: 131 | # 1) real leaf path items, 2) and names of things without path 132 | model.append(iternode, [leaf.name, leaf.name, leaf.blurb]) 133 | return True 134 | elif model.get_value(iternode, 1) != leaf.name: # column 2 135 | # This is a collision, two different things want the same path 136 | # TBD print a warning only if the viewspec specifies single occupancy 137 | print "Differing leaf names for same path:", leaf.name, ":", model.get_value(iternode, 1) 138 | return False 139 | else: 140 | # This is a duplicate, the same named thing wants a path item twice. 141 | # Could be a different version? 142 | # Whether this is unexpected depends on the conceptual model of things, 143 | # and on enforcement earlier. 144 | print "Same leaf value requested path twice", str(leaf) 145 | return False 146 | # Else, match but more path to match 147 | del pathitems[0] # advance in suffix 148 | # recurse on suffix of path, matching child 149 | return __add_path_from_node(model, iternode, pathitems, leaf) 150 | break 151 | else: # no match, next sibling 152 | iternode = model.iter_next(iternode) 153 | else: # no break (not matched) and iternode is None (no more siblings) 154 | # Add new path at this level, below parent, with any sibling 155 | # !!! If tree is empty, parent is None 156 | __add_path_suffix(model, parent, pathitems, leaf) 157 | return True 158 | 159 | 160 | def __add_path_suffix(model, parent, pathsuffix, leaf): 161 | ''' 162 | Add new path suffix to treestore below parent. 163 | Parent can be None for case: at a row at top level. 164 | Path suffix is list of items, not a path string. 165 | ''' 166 | assert pathsuffix # Pathsuffix CANNOT be empty. 167 | # Add rows in model for the path 168 | for item in pathsuffix: 169 | # parent becomes the new row for next iteration. 170 | # See gtk doc: append(parent,..) means below parent or at toplevel if parent None. 171 | # First column the path item, second column empty. 172 | parent = model.append(parent, [item, "", ""]) 173 | # Change row in model for leaf. 174 | # Note we just added it above, but with null values 175 | # Second (hidden?) column holds leaf value. 176 | model.set_value(parent, 1, leaf.name) # column 2 177 | # !!! This is where we make col 3 (which is tooltip) a cat of name and blurb 178 | # TODO this should be decided elsewhere? 179 | model.set_value(parent, 2, leaf.name + ": " + leaf.blurb) # column 3 180 | 181 | 182 | if __name__ == "__main__": 183 | # test 184 | import pygtk 185 | pygtk.require("2.0") 186 | import gtk 187 | treemodel = gtk.TreeStore(str, str) # note use Python type, not GTK constant 188 | add_path(treemodel, "foo/bar", "foo") 189 | add_path(treemodel, "foo", "foo") 190 | add_path(treemodel, "bar", "foo") 191 | print treemodel 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /gimpscripter/mockmenu/plugindb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Glue between Makeshortcut plugin and pygimp pdb. 5 | 6 | Derived from inspector.inspectpdb.py (see it for more notes.) 7 | 8 | Reimplement pdb from pygimp: 9 | Make it iterable (a full dictionary) 10 | Add __repr__ method that documents a procedure 11 | Add extra attributes. 12 | Limit it to plugins. 13 | 14 | Also define views on the db, for use by the app. 15 | 16 | The general API for glue objects between a treeview and its data: 17 | dictofviews object a dictionary of viewspecs on a db object, 18 | dictionary of objects with attributes and repr method (referred to as the db.) 19 | a filter dictionary: boolean valued with same keys as the db 20 | Many viewspecs can all refer to the same dictionary of objects 21 | 22 | Copyright 2010 Lloyd Konneker 23 | 24 | This program is free software; you can redistribute it and/or modify 25 | it under the terms of the GNU General Public License as published by 26 | the Free Software Foundation; either version 2 of the License, or 27 | (at your option) any later version. 28 | 29 | This program is distributed in the hope that it will be useful, 30 | but WITHOUT ANY WARRANTY; without even the implied warranty of 31 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 | GNU General Public License for more details. 33 | 34 | You should have received a copy of the GNU General Public License 35 | along with this program; if not, write to the Free Software 36 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 37 | ''' 38 | 39 | 40 | import gimpfu 41 | import gimp 42 | import gimpenums # for proctypes 43 | import types 44 | import time 45 | 46 | # our own submodules 47 | from gimpscripter.mockmenu import db_treemodel 48 | from gimpscripter.mockmenu import map_procedures 49 | from gimpscripter import macros 50 | 51 | 52 | # Dictionaries of types in the conceptual model 53 | 54 | ''' 55 | This used to map integer types to strings on loading from the PDB. 56 | Programming surprise: some docs say constants are like PROC_PLUG_IN, others say GIMP_PLUG_IN 57 | Constants defined by gimpenums are same as in libgimp but without prefix GIMP_ e.g. GIMP_EXTENSION -> EXTENSION 58 | ''' 59 | proctypedict = { 60 | gimpenums.PLUGIN : "Plugin", 61 | gimpenums.EXTENSION : "Extension", 62 | gimpenums.TEMPORARY : "Temporary", 63 | gimpenums.INTERNAL : "Internal", 64 | -1 : "Unknown" # Hope this doesn't conflict or change 65 | } 66 | 67 | 68 | 69 | 70 | class Procedure: 71 | ''' 72 | Procedures in the Gimp PDB. 73 | Mimics the attributes exposed by the PDB. 74 | Unifies ALL the exposed attributes of any type of procedure. 75 | Even attributes that aren't readily available from Gimp. 76 | Implements repr for str() 77 | ''' 78 | # TBD catch ValueError on decode ? 79 | 80 | # Note it is important to properly default those attributes that we build views on 81 | def __init__(self, name, accel, loc, time, menupath = "", imagetype="", 82 | blurb="missing", help="", author="", copyright="", date="", proctype=-1 ): 83 | 84 | # global proctypedict 85 | 86 | # attributes returned by gimp_plugin_query 87 | self.name = name 88 | self.menupath = menupath 89 | self.accel = accel 90 | self.loc = loc 91 | self.imagetype = imagetype 92 | self.time = time 93 | # attributes returned by gimp_procedural_db_proc_info 94 | self.blurb = blurb 95 | self.help = help 96 | self.author = author 97 | self.copyright = copyright 98 | self.date = date 99 | self.type = proctypedict[proctype] 100 | # other attributes that can be discerned, eg by inference or parsing source files 101 | self.filename = "Unknown" 102 | self.language = "Unknown" 103 | 104 | 105 | def __repr__(self): 106 | ''' 107 | Return text describing procedure. 108 | 109 | Future: different formats for different types 110 | Future: formatted 111 | Future: highlight the search hits 112 | ''' 113 | # print self.__dict__ 114 | text = "" 115 | for attrname, attrvalue in self.__dict__.iteritems(): 116 | if attrvalue is None: # Don't know why pygimp didn't do this earlier? 117 | attrvalue = "" # Must be a string 118 | if not isinstance(attrvalue, types.StringType) : 119 | print "Non string value for attr:", attrname, "of:", self.name 120 | attrvalue = "***Non string error***" 121 | 122 | text = text + attrname + ': ' + attrvalue + "\n" 123 | return text 124 | 125 | 126 | def update(self, blurb, help, author, copyright, date, thetype): 127 | ''' 128 | TBD generalize 129 | ''' 130 | self.blurb = blurb 131 | self.help = help 132 | self.author = author 133 | self.copyright = copyright 134 | self.date = date 135 | self.type = proctypedict[thetype] 136 | 137 | 138 | def standardize_menu_path(path): 139 | # Delete <> from the menupath 140 | result = path.translate(None, '<>') 141 | # Strip leading "Image/" 142 | # Since for our use, that is implied 143 | if result.startswith("Image/"): 144 | result = result.replace("Image/", "", 1) 145 | return result 146 | 147 | 148 | class Pdb(dict): 149 | ''' 150 | A read only dictionary of objects of type Procedure mimicing the Gimp PDB. 151 | For now, it initializes itself with data. 152 | You can also add items. 153 | ''' 154 | 155 | def __init__(self): 156 | 157 | # Base class init 158 | dict.__init__(self) 159 | 160 | # Fill self with data from Gimp PDB 161 | 162 | # Query the plugins, which have different attributes exposed. 163 | # !!! Here we want the menupath. 164 | # Empty search string means get all. Returns count, list, ... 165 | c1, menupath, c2, accel, c3, loc, c4, imagetype, c5, times, c6, name = gimp.pdb.gimp_plugins_query("") 166 | 167 | for i in range(0,len(name)): 168 | 169 | # Create new procedure object 170 | procedure = Procedure(name[i], accel[i], loc[i], 171 | time.strftime("%c", time.localtime(times[i])), # format time. TBD convert to UTF8 172 | standardize_menu_path(menupath[i]), 173 | imagetype[i], 174 | blurb = gimpfu.pdb[name[i]].proc_blurb ) # Additional fields directly from gimpfu.pdb 175 | # Note the attr in the gimpfu.pdb are named proc_foo. 176 | 177 | # Note about future development: 178 | # pygimp wraps pdb.gimp_procedural_db_get_data as gimp.pygimp_get_data(name[i]) 179 | # data will be the default parameters for plugins written in C. 180 | 181 | dict.__setitem__(self, name[i], procedure) 182 | 183 | def __setitem__(self, key, value): 184 | # This allows the Pdb to be supplemented 185 | dict.__setitem__(self, key, value) 186 | # raise RuntimeError, "Pdb does not allow adding procedures" 187 | 188 | # iterator methods, and all other special methods, inherited from base 189 | # No overriding is necessary. 190 | 191 | 192 | 193 | def append_gimp_internal_procedures(plugindb): 194 | ''' 195 | Supplement given dictionary with a subset of gimp internal procedures. 196 | Subset is: only those most useful to plugin creators. 197 | 198 | Cases for whether internal procedure have menupath presence in Gimp menus: 199 | 200 | - No : we fabricate a menu item. 201 | - Yes: no programmatic way to discern, we hand coded corresponding Gimp menu item. 202 | 203 | Minimal procedure descriptors: having at least: 204 | 205 | - the attribute declared in viewspec: menupath 206 | - imagetype 207 | 208 | Pygimp pdb does not expose the imagetype as attribute of a PDB function. 209 | gimp-procedural-db-query also does not return the imagetype. 210 | Imagetype blank or "*" means available for all image types. 211 | TODO decide whether some internal procedures have imagetype contraints. 212 | gimp-flatten does not apply when there IS no alpha, and throws an exception? 213 | ''' 214 | for menupath, procname in map_procedures.menu_to_procname.items(): 215 | if procname in plugindb : 216 | print "Supplemental menu ", menupath, " is duplicate path to plugin ", procname 217 | # But go ahead and add it 218 | 219 | if macros.is_macro(procname): 220 | plugindb[procname] = Procedure(procname, 221 | "bar", "bar", "bar", # accel, loc, time all unknown 222 | menupath, # <= from the map 223 | imagetype="", # unknown 224 | blurb = macros.get_blurb(procname) # lookup 225 | ) 226 | else: # Gimp internal procedure 227 | # Many fields unknown for PDB procedures that are not plugins 228 | plugindb[procname] = Procedure(procname, 229 | "bar", "bar", "bar", # accel, loc, time all unknown 230 | menupath, # <= from the map 231 | imagetype="", # unknown 232 | blurb = gimpfu.pdb[procname].proc_blurb # lookup 233 | ) 234 | 235 | 236 | 237 | ''' 238 | This is the meat of this glue module: 239 | define views on an augmented PDB. 240 | ''' 241 | # make a dictionary of plugin descriptors, keyed by name 242 | plugindb = Pdb() # db of plugins, exported, a main product 243 | append_gimp_internal_procedures(plugindb) 244 | 245 | # TODO the rest of this should be in another module 246 | 247 | # A map that defines what rows appear in the gtk.treeview 248 | # TBD make it show only plugins that are not shortcuts ! 249 | # ie no need for a shortcut to a shortcut. 250 | pluginfilterdict = {} 251 | for name, value in plugindb.iteritems(): 252 | pluginfilterdict[name] = True # show all 253 | 254 | dictofviews = {} # Exported, a main product 255 | 256 | 257 | dictofviews["Procedures by menu path"] = db_treemodel.ViewSpec("Procedures by menu path", "menupath", "SlashPath", None, plugindb, pluginfilterdict) 258 | 259 | 260 | if __name__ == "__main__": 261 | # test 262 | # Currently, this doesn't work: abort at wire to gimp 263 | # That is, gimp must be running. 264 | import sys 265 | 266 | # gimp module is .so in /usr/lib/gimp/2.0/python 267 | # gimpfu.py in /usr/lib/gimp/2.0/plug-ins 268 | sys.path.append('/usr/lib/gimp/2.0/python') 269 | import gimp 270 | 271 | # test 272 | mypdb = Pdb() 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /gimpscripter/parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Parameters of a plugin, both formal and actual. 5 | 6 | Understands various views and slices: 7 | hidden and nonhidden parameters 8 | deferred parameters 9 | formal (paramdefs) versus actual (entered) parameters 10 | This class hides knowledge about how parameter sets are sliced. 11 | 12 | Typical state transition for this object: 13 | new(), init_for_procname(), preset(), [one or more repeats of init or preset], access slices 14 | 15 | Formal parameters are available as soon as this created. 16 | Actual parameters are available after user has used a dialog 17 | for entering actual and deferring parameters. 18 | The deferring action slices both formal and actual parameters. 19 | 20 | Hidden: 21 | Two meanings: 22 | - pygimp gimpfu module hides them for beginning programmers. The PDB does NOT hide them. 23 | - hidden from the user by GimpScripter, i.e. never presented in a dialog. 24 | 25 | userentered: actual parameter values returned by dialog with user, but only non-hidden parameters 26 | The user may not have actually touched them, but accepted the initial (default) values. 27 | 28 | defers: sequence of booleans telling which params user deferred, from the dialog 29 | deferred is shorter than non-hidden is shorter than all. 30 | defers is same length as userentered. 31 | 32 | Example: 33 | Formal params, as from the PDB: run-mode, image, drawable, tweak, color 34 | Hidden formal: run-mode, image, drawable 35 | 36 | Python paramdefs: 37 | [ (PF_INT, tweak, "Speed of fitering", 37), 38 | (PF_COLOR, color, "Color to filter", (125,10,20) ) 39 | ] 40 | (type, name, desc, default, optional) 41 | 42 | User entered: [int instance, color instance] 43 | Deferred: [True, False] (boolean, same length as user entered.) 44 | 45 | !!! Note that from the PDB, paramdefs are only (type, name, desc) 46 | 47 | Unique names: 48 | Over a set of sets of paramdefs, names are not unique. 49 | We show the names from the paramdefs to the user: they can cope with duplicate names. 50 | When generating code, we uniquify names to distinguish references. 51 | Unique names are used: 52 | in formal params to wrapper 53 | in registration of wrapper 54 | in wrapper's calls to wrapped 55 | 56 | Copyright 2010 Lloyd Konneker 57 | 58 | This program is free software; you can redistribute it and/or modify 59 | it under the terms of the GNU General Public License as published by 60 | the Free Software Foundation; either version 2 of the License, or 61 | (at your option) any later version. 62 | 63 | This program is distributed in the hope that it will be useful, 64 | but WITHOUT ANY WARRANTY; without even the implied warranty of 65 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 66 | GNU General Public License for more details. 67 | 68 | You should have received a copy of the GNU General Public License 69 | along with this program; if not, write to the Free Software 70 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 71 | ''' 72 | 73 | from gimpfu import * 74 | import gimpcolor 75 | 76 | from gimpscripter import constantmaps 77 | from gimpscripter import parse_params 78 | 79 | 80 | 81 | 82 | class Param(object): 83 | ''' 84 | A parameter definition and declaration, i.e. formal and actual. 85 | More attributes than a Gimp ParamDef, but encloses same named attributes (name, type, desc) 86 | ''' 87 | 88 | def __init__(self, gimp_pdef, hidden): 89 | ''' 90 | Initialize from a Gimp ParamDef 91 | 92 | hiddenness depends on order in a list, can't be computed just from the gimp_pdef 93 | ''' 94 | # Formal attributes 95 | self.type = gimp_pdef[0] 96 | self.name = gimp_pdef[1] 97 | self.desc = gimp_pdef[2] 98 | # !!! A desc can be very long. It is a problem for our GUI, but not here. 99 | self.pdef = gimp_pdef # Keep it for convenience 100 | self._hidden = hidden 101 | # does user see it. Not computed just from type, but from position. 102 | 103 | ''' 104 | Defaults are not available from the PDB. 105 | Each plugin language (C, Scheme, Python) has its own way of storing defaults (initial values) 106 | and last values (recently used values). 107 | This is lowest common denominator: a "typical" value for the type, 108 | not the same as the initial value (called a default) given in plugin definitions. 109 | Future: recover the recently used values. 110 | ''' 111 | if not hidden: 112 | # map parameter type to a typical(canonical) default of the type 113 | # !!! There are some plugins that raise KeyError eg Image/Colors/Map/Rearrange for INT8ARRAY 114 | self.default = constantmaps._default_map[gimp_pdef[0]] 115 | else: 116 | self.default = None # Hidden params don't have defaults 117 | 118 | # Attributes of actual. Initially unknown until user interaction 119 | self.is_deferred = False 120 | self.value = None 121 | 122 | # Attributes computed over an entire list of paramdefs (from more than one procedure) 123 | # The op to compute these names must be done after all commands are in the sequence 124 | # and before unique_name is accessed. 125 | self.unique_name = None 126 | 127 | 128 | def __str__(self): 129 | return str(self.type) + " " + self.name + " " + self.desc + " " + str(self.value) 130 | 131 | # Getters 132 | 133 | def is_ephemeral(self): 134 | ''' Computed from type. ''' 135 | return is_ephemeral_type(self.type) 136 | 137 | def is_hidden(self): 138 | return self._hidden 139 | 140 | def get_evaluable_value(self): 141 | ''' 142 | In this app (code generation) need evaluable strings for the userentered params, not objects. 143 | Hence we repr(value). Value is an object. repr(value) is evaluable. 144 | This is obvious to a Pythonista: thats the definition of repr(), but I want to emphasize. 145 | Notes: 146 | values of type string: repr() adds pair of quotes, which is what we want. 147 | values of type gimpcolor.RGB, the repr is "gimpcolor.RGB(0,0,0,1)", which IS evaluable 148 | ''' 149 | return repr(self.value) 150 | 151 | 152 | class ParamList(list): 153 | ''' 154 | A container of Paramdefs. 155 | 156 | Slices are sequences of parameters for a named procedure. 157 | Slices can be inserted and removed by position or name. 158 | 159 | Has a state: whether user entered actual values. 160 | ''' 161 | 162 | def __init__(self): 163 | 164 | self.procedure_start_index = [0] 165 | # List of indexes of first parm of procedures 166 | # !!! Also used to find the end parm of a procedures 167 | # so it is post-incremented 168 | 169 | 170 | def delete_params_of(self, procname): 171 | pass #TODO 172 | 173 | def insert_params_of(self, command, position): 174 | ''' 175 | Insert slice of formal parameters of PDB procedure. 176 | Mainly, copy and convert from Gimp ParamDef to our Param. 177 | ''' 178 | inparamdefs = command.get_paramdefs() 179 | # TODO We don't need the return values since we are inferring creation of objects. 180 | # Scheme scripts don't have return values, and most returned objects are inferrable. 181 | # returnparamdefs = pdb[procname].return_vals 182 | 183 | # Assert start of params for this command position is already captured in procedure_start_index 184 | # TODO more general, if we insert anywhere but at the end. 185 | start = self.procedure_start_index[-1] 186 | 187 | for x in inparamdefs: 188 | param = Param(x, hidden=False) # temporarily hidden is False 189 | self.append(param) 190 | 191 | # Remember end of params for this command position 192 | self.procedure_start_index.append(len(self)) 193 | end = self.procedure_start_index[-1] 194 | 195 | # Determine which params are hidden, a leading prefix. 196 | # Gimpfu does something similar to hide image, drawable parameters. 197 | print start, end 198 | count = parse_params.count_hidden_params(self[start:end]) 199 | for param in self[start: start+count]: 200 | param._hidden = True # TODO setter 201 | 202 | print "Total count params", len(self) 203 | # After insert, must uniquify names again 204 | 205 | 206 | def _get_range_for_position(self, position): 207 | ''' Get the range for parameters of command at position. ''' 208 | start, end = self.procedure_start_index[position], self.procedure_start_index[position+1] 209 | print "Range of parameters for position", position, " is ", start, end 210 | return start, end 211 | 212 | def get_parms_for(self, position): 213 | ''' Return slice of parms for command at position ''' 214 | # TODO for ease of use, link parm values to earlier parms ??? 215 | start_parm, end_parm = self._get_range_for_position(position) 216 | return self[start_parm:end_parm] 217 | 218 | 219 | def preset(self, userentered, defers, command_index=None): 220 | ''' 221 | Preset with actual parameters and deferments from a dialog with user. 222 | Userentered means the user at least reviewed the initial values and possibly entered them. 223 | Index is the command whose parameters the user changed. 224 | ''' 225 | print "Validating", userentered, defers 226 | # Userentered and defers must be the length of nonhidden. 227 | assert len(userentered) == len(defers) 228 | 229 | # In the range of parameters for command_index 230 | start_parm, end_parm = self._get_range_for_position(command_index) 231 | 232 | # Shuffle values from userentered, defers into self 233 | source_index = 0 234 | for item in self[start_parm:end_parm]: 235 | if not item.is_hidden(): 236 | item.is_deferred = defers[source_index] 237 | item.value = userentered[source_index] # Note referring to an object, possibly not a string 238 | source_index += 1 239 | 240 | 241 | def is_take_image(self): 242 | ''' 243 | Return whether any procedure takes an image as hidden parameter. 244 | i.e. if its second parameter has type PF_IMAGE (Also, it has the name "image"). 245 | Every procedure takes a run-mode for first parameter. 246 | If it has a second parameter AND it is PF_IMAGE, then the third parameter is PF_DRAWABLE? 247 | Note that parametes of type image CAN also exist beyond the second parameter, 248 | TODO there is more to this, it depends on the type of the procedure. 249 | ''' 250 | # Iterate by procedure 251 | for index in self.procedure_start_index: 252 | second_parameter_index = index + 1 253 | if second_parameter_index < len(self): 254 | # TODO and less than the end of the parameters for this procedure 255 | if self[second_parameter_index].type == PF_IMAGE: 256 | return True 257 | return False 258 | 259 | 260 | def has_user_enterable_params(self): 261 | ''' 262 | Does any procedure have nonhidden parameters defined? 263 | Not whether user entered them yet. 264 | ''' 265 | # Map takes a function as first parameter, here a getter 266 | result = not all(map(Param.is_hidden, self)) 267 | # print "Has user params returns ", result 268 | return result 269 | 270 | def has_ephemeral_params(self): 271 | ''' Does any procedure have ephemeral parameters defined? ''' 272 | return any(map(Param.is_ephemeral, self)) 273 | 274 | def get_nonhidden_pdefs_for(self, position): 275 | ''' Return slice of nonhidden pdefs for command at position ''' 276 | start_parm, end_parm = self._get_range_for_position(position) 277 | return [x.pdef for x in self[start_parm:end_parm] if not x.is_hidden()] 278 | 279 | def get_nonhidden_defaults_for(self, position): 280 | ''' Return slice of nonhidden defaults for command at position ''' 281 | start_parm, end_parm = self._get_range_for_position(position) 282 | return [x.default for x in self[start_parm:end_parm] if not x.is_hidden()] 283 | 284 | def get_nonhidden_values_for(self, position): 285 | ''' Return slice of nonhidden userentered values for command at position ''' 286 | start_parm, end_parm = self._get_range_for_position(position) 287 | return [x.value for x in self[start_parm:end_parm] if not x.is_hidden()] 288 | 289 | def get_defers_for(self, position): 290 | ''' Return is_deferred for slice of nonhidden parameters. ''' 291 | start_parm, end_parm = self._get_range_for_position(position) 292 | return [x.is_deferred for x in self[start_parm:end_parm] if not x.is_hidden()] 293 | 294 | def nonhiddenpdefs(self): 295 | ''' Return slice of pdefs of nonhidden Params 296 | Note these are PyGimp pdef structures or fascimiles. 297 | ''' 298 | return [x.pdef for x in self if not x.is_hidden()] 299 | 300 | def nonhiddendefaults(self): 301 | ''' Return slice of nonhidden defaults ''' 302 | return [x.default for x in self if not x.is_hidden()] 303 | 304 | def deferred_unique_names(self): 305 | ''' Return unique names of slice of deferred ''' 306 | # Note deferred implies not hidden 307 | return [x.unique_name for x in self if x.is_deferred] 308 | 309 | 310 | def uniquify_names(self): 311 | ''' 312 | For a sequence of commands, uniquify names across all paramdefs. 313 | They might be used for parameters to plug_in main, where they must be unique. 314 | !!! Also insure names are Pythonic: no dash. 315 | Note this computes an attribute and must be called before the attribute is accessed. 316 | 317 | TODO doctest this 318 | n, n, n, n_1 => n, n_1, n_2, n_1_1 319 | n_1, n, n_1 => n_1, n, n_1_1 320 | ''' 321 | names = {} # name => count of uses 322 | 323 | for param in self: 324 | original_name = param.pdef[1] 325 | 326 | # Since these names will be used in Python code, transliterate dash to underbar 327 | original_name = original_name.replace("-", "_") 328 | 329 | if original_name in names: 330 | count = names[original_name] + 1 331 | names[original_name] = count 332 | # generate unique name, use _ 333 | # Note generated name may clash with name yet to be seen, 334 | # but the yet to be seen name will get a new name. 335 | unique_name = original_name + "_" + str(count) 336 | # !!! Put the unique name in names also 337 | names[unique_name] = 1 # first use of unique name 338 | else: # first use 339 | names[original_name] = 1 340 | unique_name = original_name 341 | 342 | param.unique_name = unique_name # store computed attribute 343 | 344 | 345 | 346 | def any_parms_deferred(parms): 347 | ''' 348 | Are any parms in the given list deferred? 349 | This is called when the parm list for a command is already in hand. 350 | ''' 351 | # reduce(operator.or_, parms.is_deferred, False): 352 | for item in parms: 353 | if item.is_deferred: 354 | return True 355 | return False 356 | 357 | def get_parms_deferred(parms): 358 | ''' 359 | Return deferred parms in the given list? 360 | This is called when the parm list for a command is already in hand. 361 | ''' 362 | return [x for x in parms if x.is_deferred] 363 | 364 | def get_parms_nonhidden(parms): 365 | ''' 366 | Return nonhidden parms in the given list? 367 | This is called when the parm list for a command is already in hand. 368 | ''' 369 | return [x for x in parms if not x.is_hidden()] 370 | 371 | def get_parms_hidden(parms): 372 | ''' Return the hidden parms, including run-mode. ''' 373 | result = [x for x in parms if x.is_hidden()] 374 | for parm in result: 375 | print "Hidden parms", parm 376 | return result 377 | 378 | 379 | def is_ephemeral_type(paramtype): 380 | ''' 381 | Return whether paramtype is that of ephemeral Gimp objects. 382 | Ephemeral means object set existing at wrapping plugin creation time can differ from 383 | set existing at wrapping plugin run time. 384 | In other words, use the name of the object as a pseudo UID (universal ID) spanning sessions with Gimp. 385 | ''' 386 | return paramtype in (PF_DISPLAY, PF_IMAGE, PF_LAYER, PF_CHANNEL, PF_DRAWABLE, PF_VECTORS) 387 | 388 | def get_return_parms(procname): 389 | #TODO 390 | return [] 391 | 392 | 393 | 394 | 395 | 396 | -------------------------------------------------------------------------------- /gimpscripter/parse_params.py: -------------------------------------------------------------------------------- 1 | 2 | ''' 3 | Determine hidden parameters from a list of parameter definitions. 4 | 5 | Hidden parameters are always a leading prefix. 6 | Here we determine them by simple parsing. 7 | 8 | BNF: 9 | 10 | Version 1: 11 | This version is inadequate: it hides second drawable parameters that shouldn't be hidden. 12 | For example, in Color/Map/Sample Colorize. 13 | 14 | ::= ["run-mode"] [*] 15 | ::= image-type | drawable-type | layer-type 16 | 17 | In other words, the optional run-mode parameter is identified by name, 18 | and the other hidden parameters are identified by type (not name). 19 | 20 | Version 2: 21 | 22 | ::= ["run-mode"] [] [] [] [] [] 23 | 24 | [] means 0 or 1. IOW the types are in a certain order. 25 | When a type is seen that is not in the order, 26 | or when a second parameter of the same type is seen, 27 | counting stops. 28 | 29 | Examples: 30 | Image returns 1 31 | Drawable returns 1 32 | Image, Image returns 1 33 | Image, Drawable returns 2 34 | Image, Layer returns 2 35 | Image, Channel returns 2 36 | Image, Drawable, Drawable returns 2 37 | Image, Drawable, Layer returns 3 38 | Image, Drawable, Layer, Channel returns 4 39 | 40 | Copyright 2010 Lloyd Konneker 41 | 42 | This program is free software; you can redistribute it and/or modify 43 | it under the terms of the GNU General Public License as published by 44 | the Free Software Foundation; either version 2 of the License, or 45 | (at your option) any later version. 46 | 47 | This program is distributed in the hope that it will be useful, 48 | but WITHOUT ANY WARRANTY; without even the implied warranty of 49 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 50 | GNU General Public License for more details. 51 | 52 | You should have received a copy of the GNU General Public License 53 | along with this program; if not, write to the Free Software 54 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 55 | ''' 56 | 57 | 58 | from gimpfu import * 59 | 60 | class paramlistScanner(object): 61 | ''' 62 | Simple lexical scanner of param list by type attribute. 63 | Safe to scan with scannee empty: count() returns 0 64 | ''' 65 | def __init__(self, paramlist): 66 | self.token = 0 67 | self.count = 0 68 | self.scannee = paramlist 69 | 70 | def get_next_type(self): 71 | if self.token >= len(self.scannee): 72 | return None # !!! None will not match anything 73 | else: 74 | return self.scannee[self.token].type 75 | 76 | def advance(self): 77 | self.token += 1 78 | self.count += 1 79 | 80 | def skip(self): 81 | self.token += 1 # !!! But don't increment count 82 | 83 | def get_count(self): 84 | return self.count 85 | 86 | 87 | 88 | 89 | def count_hidden_params(paramlist): 90 | ''' 91 | Parse and count hidden params from a list of Params. 92 | ''' 93 | scanner = paramlistScanner(paramlist) 94 | 95 | if len(paramlist) < 1 : 96 | return 0 97 | 98 | # Zero or one pdefs with name equal run-mode 99 | if paramlist[0].name == 'run-mode': 100 | scanner.advance() 101 | if scanner.get_next_type() == PF_IMAGE: 102 | scanner.advance() 103 | if scanner.get_next_type() == PF_DRAWABLE: 104 | scanner.advance() 105 | if scanner.get_next_type() == PF_LAYER: 106 | scanner.advance() 107 | if scanner.get_next_type() == PF_CHANNEL: 108 | scanner.advance() 109 | if scanner.get_next_type() == PF_VECTORS: 110 | scanner.advance() 111 | print "Count hidden params is ", scanner.get_count() 112 | return scanner.get_count() 113 | 114 | 115 | def count_nonrunmode_hidden_params(paramlist): 116 | ''' Parse and count hidden params that are NOT run-mode from a list of Params .''' 117 | if len(paramlist) < 1 : 118 | return 0 119 | 120 | scanner = paramlistScanner(paramlist) 121 | 122 | # Zero or one params with name equal run-mode 123 | if paramlist[0].name == 'run-mode': 124 | scanner.skip() # !!! don't count 125 | if scanner.get_next_type() == PF_IMAGE: 126 | scanner.advance() 127 | if scanner.get_next_type() == PF_DRAWABLE: 128 | scanner.advance() 129 | if scanner.get_next_type() == PF_LAYER: 130 | scanner.advance() 131 | if scanner.get_next_type() == PF_CHANNEL: 132 | scanner.advance() 133 | if scanner.get_next_type() == PF_VECTORS: 134 | scanner.advance() 135 | print "Count hidden params is ", scanner.get_count() 136 | return scanner.get_count() 137 | 138 | 139 | def has_runmode(params): 140 | ''' Does this ParamList start with a Param of type runmode?''' 141 | return len(params) > 0 and params[0].name == 'run-mode' 142 | -------------------------------------------------------------------------------- /gimpscripter/runtime.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Runtime "library" for a Gimp wrapper plugin. 3 | Runtime means can be called by the wrapper plugin when it runs. 4 | Not needed or present in all wrapper plugins. 5 | 6 | You are probably reading this in a wrapper plugin generated by GimpScripter. 7 | The source for this file is gimpscripter/runtime.py 8 | ''' 9 | from gimpfu import * 10 | 11 | ''' 12 | Classes for shadowing Gimp objects so that we can infer their creation and deletion 13 | by commands in the wrapper plugin 14 | and maintain a stack whose top can be referred to by commands. 15 | ''' 16 | 17 | class GimpStack(list): 18 | ''' GimpScripter stack of active Gimp objects.''' 19 | 20 | def __init__(self, type_on_stack, an_object=None): 21 | self.type_on_stack = type_on_stack 22 | # Readable type, not the encoding PF_foo 23 | 24 | # For init, an_object MUST be a formal parameter of wrapper plugin. 25 | # Otherwise, what would an_object refer to? 26 | if an_object: 27 | self.append(an_object) 28 | self.stack_top = 0 29 | else: 30 | # At initialization time, there is no object of this type known to be active. 31 | self.stack_top = -1 32 | 33 | def __str__(self): 34 | return "GimpStack type " + self.type_on_stack 35 | 36 | def push(self, an_object): 37 | self.stack_top += 1 # preincrement 38 | self.append(an_object) 39 | print "Pushed ", an_object.name, " to stack type", self.type_on_stack, " position ", self.stack_top 40 | 41 | def pop(self): 42 | ''' 43 | assert the top object has been deleted from Gimp 44 | Ours may be the last reference to it. 45 | Can we still get its name? 46 | If so, for robustness we should check we are deleting the name we inferred is gone. 47 | ''' 48 | del(self[self.stack_top]) # NOT remove, we don't need the object 49 | self.stack_top -= 1 50 | print "Popped. New top is ", self[self.stack_top].name, " at position ", self.stack_top 51 | 52 | def top(self): 53 | try: 54 | return self[self.stack_top] 55 | except IndexError: 56 | # Probably the stack is empty. Rather than return None, abort. 57 | pdb.gimp_message("This wrapper plugin can't find active object of type: %s." % self.type_on_stack) 58 | raise RuntimeError("Plugin failed to find an active image, drawable, layer, or channel.") 59 | 60 | 61 | 62 | 63 | class GimpEphemera(object): 64 | ''' GimpScripter's shadow of Gimp objects ''' 65 | 66 | def __init__(self, a_image, a_drawable): 67 | 68 | ''' 69 | Must be initialized to the ephemera existing when Ephemera instance created 70 | which is in the first line of plugin_main. 71 | Ephemera existing then are captured in ephemera but not in stacks. 72 | Only the image and drawable passed to a wrapper are then stacked. 73 | Only objects created by a wrapper are subsequently stacked. 74 | Thus a wrapper can not remove, but reference to stack top, any object 75 | the wrapper did not create, except for the passed image and drawable. 76 | FUTURE: revisit this, possibly call gimp_image_get_foo to initialize stacks, 77 | if Gimp also is reliably using a stack model, with an active instance for each type. 78 | ''' 79 | self.ephemera = {} 80 | self._update_ephemera() 81 | self.prior_ephemera = self._copy_ephemera_keys() 82 | # prior_ephemera is keys only, the values are not ephemeral Gimp objects 83 | # since those may be deleted by commands in wrapper plugin 84 | 85 | # Dictionary of stacks, one for each type of ephemeral 86 | self.stacks = {} 87 | self.stacks[PF_VECTORS] = GimpStack("Path") # Path stack always empty 88 | # The other stacks are initialized by passed image and drawable, or empty. 89 | self.stacks[PF_IMAGE] = GimpStack("Image", a_image) 90 | self.stacks[PF_DRAWABLE] = GimpStack("Drawable", a_drawable) 91 | # We put a drawable on two stacks 92 | if pdb.gimp_drawable_is_layer(a_drawable): 93 | # a_drawable.type == PDB_LAYER: # Doesn't work?? 94 | self.stacks[PF_LAYER] = GimpStack("Layer", a_drawable) 95 | # If a layer was passed in, channel stack is empty 96 | self.stacks[PF_CHANNEL] = GimpStack("Channel") 97 | elif pdb.gimp_drawable_is_channel(a_drawable): 98 | # a_drawable.type == PF_CHANNEL: 99 | self.stacks[PF_CHANNEL] = GimpStack("Channel", a_drawable) 100 | self.stacks[PF_LAYER] = GimpStack("Layer") 101 | else: 102 | print "Unknown drawable type", a_drawable.type 103 | raise RuntimeError("Unknown drawable type") 104 | 105 | # TODO other ephemerals??, etc. 106 | 107 | def _update_ephemera(self): 108 | ''' Update our dictionary of ephemera. ''' 109 | self.ephemera = {} # clear, start anew 110 | images = gimp.image_list() # what exists now 111 | for image in images: 112 | # !!! Note layers, channels, vectors belong to images 113 | # but we chunk them all into ephemera together, 114 | # not distinguishing two names of the same type on different images. 115 | # TODO could be a problem for some wrapper use cases 116 | print image.name 117 | # !!! Note name is often "Untitled" and image.filename is None 118 | # At one time, I used filename but why?? 119 | self.ephemera[(image.name, PF_IMAGE)] = image 120 | for layer in image.layers: 121 | print "Layer ", layer.name 122 | # !!!! Two entries. Each will go on their own stack. 123 | self.ephemera[(layer.name, PF_LAYER)] = layer 124 | self.ephemera[(layer.name, PF_DRAWABLE)] = layer 125 | for channel in image.channels: 126 | print "Channel ", channel.name 127 | self.ephemera[(channel.name, PF_CHANNEL)] = channel 128 | self.ephemera[(channel.name, PF_DRAWABLE)] = channel # !!!! Two entries 129 | # vector aka path 130 | for vector in image.vectors: 131 | print "Path ", vector.name 132 | self.ephemera[(vector.name, PF_VECTORS)] = vector # !!! VECTORS with an S 133 | 134 | def _update_stack(self, a_type): 135 | ''' Update our stack of the given type''' 136 | 137 | # make dictionaries by name for objects of type, from ephemera now and then 138 | now = self._dict_by_name(self.ephemera, a_type) 139 | then = self._dict_by_name(self.prior_ephemera, a_type) 140 | 141 | # Diff to find a recently created object 142 | diff_name = set(now)-set(then) # difference dictionary keys using sets 143 | if len(diff_name) == 1: # TODO filename for images??? 144 | for name in diff_name: # Only one, but can't index sets 145 | self.stacks[a_type].push(now[name]) # push recently created object 146 | elif len(diff_name) > 1: 147 | print diff_name 148 | raise RuntimeError("Many ephemera of same type created by one command") 149 | # else zero, pass 150 | 151 | # Diff to find recently deleted object 152 | diff_name = set(then)-set(now) # difference dictionary keys using sets 153 | if len(diff_name) == 1: 154 | for name in diff_name: # Only one, but can't index sets 155 | self.stacks[a_type].pop() # pop recently deleted object then[name] 156 | elif len(diff_name) > 1: 157 | print diff_name 158 | raise RuntimeError("Many ephemera deleted by one command") 159 | 160 | def _update_stacks(self): 161 | ''' Update our stacks of ephemera. ''' 162 | # !!! this list must match the one in parameters.py 163 | for a_type in (PF_IMAGE, PF_DRAWABLE, PF_LAYER, PF_CHANNEL, PF_VECTORS): 164 | self._update_stack(a_type) 165 | 166 | def _copy_ephemera_keys(self): 167 | ''' Return a dictionary whose keys are copy of ephemera. Values are not needed. ''' 168 | result = {} 169 | for (keyname, keytype) in self.ephemera.iterkeys(): 170 | result[(keyname, keytype)] = True 171 | return result 172 | 173 | def update(self): 174 | ''' Refresh ephemera by querying Gimp ''' 175 | self._update_ephemera() 176 | self._update_stacks() # diff ephemera and prior_ephemera 177 | # !!! Remember keys of prior_ephemera for the next update 178 | self.prior_ephemera = self._copy_ephemera_keys() 179 | 180 | def _dict_by_name(self, a_ephemera, a_type): 181 | ''' Return dict keyed by name of ephemera of type ''' 182 | result = {} 183 | for (keyname, keytype) in a_ephemera.iterkeys(): 184 | if keytype == a_type: 185 | result[keyname] = a_ephemera[(keyname, keytype)] 186 | return result 187 | 188 | 189 | def top(self, a_type): 190 | ''' Return ephemeral object for stack of active object of given type. ''' 191 | result = self.stacks[a_type].top() 192 | print "Top ", str(self.stacks[a_type]), result.name 193 | return result 194 | 195 | def lookup(self, a_type, a_name): 196 | ''' 197 | Return ephemeral object having given name and type. 198 | 199 | Drawable is the super class for layers and channels 200 | If looking up a drawable, 201 | return any layer or a channel or drawable having given name. 202 | If looking up more specifically a layer (or channel) 203 | return only a layer or channel of given name 204 | ''' 205 | for (keyname, keytype) in self.ephemera.iterkeys(): 206 | if a_name == keyname: 207 | if a_type == keytype or ( 208 | a_type == PF_DRAWABLE and ( 209 | keytype == PF_LAYER or 210 | keytype == PF_CHANNEL 211 | )): 212 | return self.ephemera[(keyname, keytype)] 213 | ''' 214 | !!! Note we omit REGION and DISPLAY, no need for them in wrapper plugins? 215 | !!! Note that PF_REGION is deprecated since gimp-2.7 ?? 216 | ''' 217 | if a_type in (PF_DISPLAY, ): # !!! Note the comma to make this a tuple 218 | raise RuntimeError("Wrapper plugins do not support lookup for this type.") 219 | 220 | ''' 221 | If we get here, failed to lookup ephemeral instance by name. 222 | Display error in status line. Don't raise exception that will imply the plugin crashed. 223 | This is USUALLY wrapping-user error, not establishing ephemera that are preconditions to wrapper plugin. 224 | However, it could that author-user constructed a bad wrapper plugin, using wrong names for ephemera. 225 | Lastly, it could be a bug in GimpScripter. 226 | ''' 227 | print "Lookup failed on %s" % a_name 228 | pdb.gimp_message("This wrapper plugin can't run without object named: %s." % a_name) 229 | # TODO the user can miss this. Make it a dialog, but how? raise RuntimeError also is silent 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /gimpscripter/specification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Specifications. What a user specifies or enters. 5 | Specifies the wrapper plugin. 6 | 7 | Copyright 2010 Lloyd Konneker 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | ''' 23 | from gimpfu import * 24 | 25 | from gimpscripter import parameters 26 | from gimpscripter import parse_params 27 | from gimpscripter import macros 28 | 29 | class GimpScripterSpec(object): 30 | ''' 31 | Everything the user specified. 32 | The data passed from the GUI to the generator. 33 | 34 | Comprises: 35 | attributes of wrapping plugin 36 | attributes of commands use chose 37 | attributes of parameters user entered 38 | ''' 39 | 40 | def __init__(self): 41 | self.wrapping = WrappingPluginSpec() 42 | self.commands = Commands() 43 | # TODO excise this, unused 44 | self. parameter_results = [None, None] # results of parameter dialog: actual parameters and defers 45 | 46 | def set_parameter_results(self, actual, toggles): 47 | self.parameter_results = [actual, toggles] 48 | 49 | 50 | 51 | class WrappingPluginSpec(object): 52 | ''' 53 | A specification for user chosen attributes of a Gimp wrapping plugin. 54 | 55 | wrappingmenuitem: of wrapping plugin, user chose. 56 | wrappingmenupath: TODO 57 | ''' 58 | def __init__(self): 59 | self.menuname = "" 60 | self.name = "bar" # TODO User given name 61 | self.blurb = "zed" # TODO User given blurb 62 | 63 | def set_menu_name(self, name): 64 | self.menuname = name 65 | 66 | 67 | 68 | class Commands(object): 69 | ''' 70 | A sequence of commands. 71 | 72 | The main thing this does is allow access to the aggregate parameters. 73 | ''' 74 | def __init__(self): 75 | self.command_list = [] 76 | # list of commands in this seq 77 | self.param_list = parameters.ParamList() 78 | # list of all params for all commands in this seq 79 | 80 | def __len__(self): 81 | return len(self.command_list) 82 | 83 | def append(self, command): 84 | ''' Append command to command list ''' 85 | self.param_list.insert_params_of(command, len(self)) 86 | command.position = len(self) 87 | self.command_list.append(command) 88 | 89 | 90 | # TODO insert, remove 91 | 92 | def get_parms_for(self, position): 93 | ''' Return list of parms for command at position''' 94 | return self.param_list.get_parms_for(position) 95 | 96 | def get_command_for(self, position): 97 | ''' Return list of parms for command at position''' 98 | return self.command_list[position] 99 | 100 | def has_macros(self): 101 | ''' Any macros in this command sequence? ''' 102 | return any(map(CommandSpec.is_macro, self.command_list)) 103 | 104 | def has_in_params(self): 105 | ''' 106 | Does any command have hidden params that are not run-mode? 107 | This must be command by command, it can't be just over the aggregate parameter list. 108 | ''' 109 | for command in self.command_list: 110 | # Assume all macros access hidden params 111 | # TODO take all this special case code out, just ask the command and have subclasses. 112 | if macros.is_macro(command.name): 113 | return True 114 | else: 115 | # Not a macro, look at the params for this command 116 | # Alternatively we could call the pdb for the pdefs 117 | if parse_params.count_nonrunmode_hidden_params(self.param_list.get_parms_for(command.position)): 118 | return True 119 | return False 120 | 121 | 122 | def is_take_image(self): 123 | ''' 124 | Does any command have image as first parameter? 125 | If so, the wrapping plugin must take an image. 126 | ''' 127 | return self.param_list.is_take_image() 128 | 129 | 130 | def deferred_unique_names(self): 131 | ''' 132 | Return the names of all deferred parameters. 133 | !!! Uniquified names among separate commands in list. 134 | ''' 135 | return self.param_list.deferred_unique_names() 136 | 137 | 138 | def has_ephemeral_params(self): 139 | ''' 140 | Does any command in sequence have ephemeral parameters? 141 | Or use ephemeral parameters internally to a macro? 142 | For now, assume all macros use ephemeral parameters internally. 143 | TODO relax this assumption 144 | ''' 145 | return self.param_list.has_ephemeral_params() or self.has_macros() 146 | 147 | 148 | def has_user_enterable_params(self): 149 | ''' Does any command have user enterable parameters?''' 150 | result = self.param_list.has_user_enterable_params() 151 | # print "Has params returns ", result, len(self.param_list) 152 | return result 153 | 154 | 155 | 156 | class CommandSpec(object): 157 | ''' 158 | Specifies a single command and user entered attributes of its use. 159 | 160 | Commands are of 3 types: 161 | - plugin 162 | - procedure 163 | - macro of the above 164 | First two are both defined in the PDB. Macros are defined here. 165 | 166 | name: command name for which user chose menu item 167 | ''' 168 | def __init__(self, name, pathstring): 169 | self.name = name 170 | self.is_use_last = False 171 | self.position = None 172 | # position of this command in Commands, set when appended 173 | self.pathstring = pathstring 174 | # menupath in our mock menu of commands 175 | 176 | 177 | def set_is_use_last(self, truth): 178 | ''' 179 | Some commands can be executed to use the most recent parameters from prior use, 180 | even prio use outside this wrapping plugin. 181 | This is a user choice to use the command in that fashion. 182 | ''' 183 | self.is_use_last = truth 184 | 185 | """ We can't do this without a reference to the parent list of this command 186 | def get_params(self): 187 | '''Return the Params of this command.''' 188 | """ 189 | 190 | 191 | def get_paramdefs(self): 192 | ''' Get the paramdefs of this command. ''' 193 | if macros.macros.has_key(self.name): 194 | print "A Macro" 195 | return macros.get_pdefs_for(self.name) 196 | else: 197 | return pdb[self.name].params 198 | 199 | def is_macro(self): 200 | # TODO refactor using classes 201 | return macros.is_macro(self.name) 202 | 203 | 204 | 205 | """ 206 | class PluginCommandSpec(CommandSpec): 207 | ''' 208 | A plugin command has: 209 | 210 | - a leading run-mode parameter (and can be run with last values.) 211 | - hidden image and drawable parameters 212 | ''' 213 | pass 214 | """ 215 | -------------------------------------------------------------------------------- /gimpscripter/template.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Template for the wrapper plugin. 3 | See standard Python documentation for the Template module. Briefly: $placeholder 4 | No copyright or license for the wrapper plugin. 5 | 6 | !!! Note the indentation in the template must meet Python requirements. 7 | Some indentation is generated. 8 | 9 | Copyright 2010 Lloyd Konneker 10 | 11 | This program is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 2 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program; if not, write to the Free Software 23 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | ''' 25 | 26 | # TODO in the summary 27 | # TODO The shortcut requires ephemeral zed when it runs. 28 | # TODO The shortcut replaced an existing shortcut. 29 | # TODO The wrapper plugin is for images of modes: $wrappingimagetype. 30 | 31 | 32 | from string import Template 33 | 34 | wrappingtemplate = Template( 35 | r'''#!/usr/bin/env python 36 | 37 | # This plugin was created by the GIMP plugin "GimpScripter..." i.e. plugin-gimpscripter.py 38 | # This *wrapper* plugin calls one or more *wrapped* or *target* plugins or PDB procedures. 39 | # Below, "# <=" indicates lines that had substitutions by GimpScripter 40 | 41 | 42 | $wrappingruntimelibrary 43 | 44 | def plugin_main($wrappingmainformalparams): # <= formal parameters 45 | # Call the wrapped procedures. 46 | # If the wrapped procedure requires (image, drawable), they are passed through. 47 | # Any other non-constant arguments have names which match formal parameters to plugin_main above 48 | # and paramdefs in register() below: they are deferred and a Gimp dialog will ask user for values. 49 | $prelude # <= prelude 50 | # 51 | $wrappingmainbody # <= body 52 | # 53 | $postlude # <= postlude 54 | 55 | 56 | if __name__ == "__main__": # invoked at top level, from GIMP 57 | 58 | from gimpfu import * 59 | 60 | gettext.install("gimp20-python", gimp.locale_directory, unicode=True) 61 | 62 | register( 63 | "$wrappingprocedurename", # <= procedure name 64 | "$wrappingblurb", # <= blurb 65 | "This plugin was created using 'GimpScripter...'", 66 | "Anonymous", 67 | "Uncopyrighted", 68 | "No copyright date", 69 | "$wrappinglabel", # <= menu item 70 | "$wrappingimagetype", # <= image type 71 | [$wrappingparameterdefs], # <= hidden and deferred parameters 72 | [], 73 | plugin_main, 74 | $wrappingmenuarg, # <= menu path 75 | domain=("gimp20-python", gimp.locale_directory)) 76 | 77 | 78 | main() 79 | ''' 80 | ) 81 | 82 | 83 | ''' 84 | Template for the summary. 85 | ''' 86 | summarytemplate = Template( 87 | r''' 88 | Created wrapper plugin: $wrappingmenupath. 89 | 90 | Its description is: $wrappingblurb 91 | 92 | It requires an open image of mode: any. 93 | 94 | To change the wrapper plugin later, create a wrapper plugin with the same name. 95 | 96 | To remove the wrapper plugin, delete the file: $filepath. To distribute the wrapper plugin, distribute the same file. 97 | 98 | The wrapper plugin will appear in Gimp menus after you restart Gimp. 99 | ''' 100 | ) 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /plugin-gimpscripter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | A GIMP plugin. 5 | 6 | Generates another plugin (in Python language). 7 | Generated plugin is sequence of commands. 8 | A command is a call to: 9 | - a target plugin 10 | - or a PDB procedure 11 | - or a macro builtin to this app. 12 | 13 | Generated wrapper plugin can be: 14 | 15 | - a shortcut (simple renaming and simplification of parameters to one command) 16 | - a sequence (the combined function of a sequence.) 17 | 18 | The purpose of wrapping is: 19 | 1) alias or link to target command 20 | 2) simplify settings dialog (standard, current, or preset options for the target commands.) 21 | 3) automate a sequence 22 | 4) hide certain complexities of PDB programming, i.e. change the model slightly 23 | (e.g. always add a new object to the image, unlike in the PDB.) 24 | 25 | 26 | Help text is in /doc and the .glade file. 27 | 28 | Future: 29 | Note that the parameters you choose not to defer for the wrapper DO NOT become the last values 30 | for the target plugin (it is run non-interactive.) 31 | You can't change the not deferred parameters later without editing the wrapper. 32 | The parameters you choose to defer DO become the last values for the wrapper plugin 33 | (they will appear as the initial values the next time you run the wrapper.) 34 | 35 | Versions: 36 | 37 | beta 0.1 : simple shortcut to one plugin using last values 38 | beta 0.2 2010 : presets: let user enter parameters for target plugin. 39 | beta 0.3 April 2011 : renamed Gimpscripter. Sequences. Macros. Calls to PDB procedures. 40 | 41 | 42 | To Do: 43 | 44 | More functionality: 45 | defaults for parameters taken from plugins themselves or modify PDB to support 46 | 47 | User friendliness: 48 | gui non-modal: Apply/Quit 49 | let user root wrappers elsewhere in menu tree 50 | domain i8n 51 | check if menu item already used? 52 | Since the menu item is not the same as the filename? 53 | check if the filename already used 54 | tell the user we created it but GIMP restart required 55 | and don't show that message more than once. 56 | Disallow making wrapper to wrapper? 57 | Disallow making wrapper to Load/Save? 58 | Return key in name text entry Apply 59 | 60 | Copyright 2010 Lloyd Konneker 61 | 62 | License: 63 | This program is free software; you can redistribute it and/or modify 64 | it under the terms of the GNU General Public License as published by 65 | the Free Software Foundation; either version 2 of the License, or 66 | (at your option) any later version. 67 | ''' 68 | 69 | 70 | from gimpfu import * 71 | 72 | gettext.install("gimp20-python", gimp.locale_directory, unicode=True) 73 | 74 | def plugin_main(): 75 | from gimpscripter.gui import main_gui 76 | 77 | # Build data that drives the app: dictionary of views on dbs 78 | # For each kind of db, import the glue module to the db 79 | # and get the dictofviews from the glue module. 80 | 81 | # Here, there is only one view, a treeview on GIMP plugins. 82 | from gimpscripter.mockmenu import plugindb # glue to the Gimp PDB 83 | dictofviews = plugindb.dictofviews.copy() 84 | 85 | app = main_gui.gimpscripterApp(dictofviews) # create instance of gtkBuilder app 86 | app.main() # event loop for app 87 | 88 | 89 | 90 | if __name__ == "__main__": 91 | # if invoked from Gimp app as a plugin 92 | 93 | register( 94 | "python_fu_gimpscripter", 95 | "Point-and-click create a plugin.", 96 | "This plugin creates another menu item, a wrapper plugin that calls a sequence of PDB procedures or plugins. The settings of the created plugin may simplify the settings of the sequence.", 97 | "Lloyd Konneker", 98 | "Copyright 2010 Lloyd Konneker", 99 | "2010", 100 | N_("Gimpscripter..."), # menu item 101 | "", # image types: blank means don't care but no image param 102 | [], # No parameters 103 | [], # No return value 104 | plugin_main, 105 | menu=N_("/Filters"), # menupath 106 | domain=("gimp20-python", gimp.locale_directory)) 107 | 108 | print "Starting Gimpscripter" 109 | main() 110 | 111 | 112 | 113 | 114 | --------------------------------------------------------------------------------