Initial checkin of Pika from heckimp
This commit is contained in:
337
plug-ins/python/colorxhtml.py
Normal file
337
plug-ins/python/colorxhtml.py
Normal file
@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Pika-Python - allows the writing of Pika plugins in Python.
|
||||
# Copyright (C) 2003, 2005 Manish Singh <yosh@gimp.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import string
|
||||
import struct
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
|
||||
escape_table = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"'
|
||||
}
|
||||
|
||||
style_def = """body {
|
||||
width: 100%%;
|
||||
font-size: %dpx;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
"""
|
||||
|
||||
preamble = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CSS Color XHTML written by PIKA</title>
|
||||
%s
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
"""
|
||||
|
||||
postamble = """\n</pre>\n</body>\n</html>\n"""
|
||||
|
||||
fmt_from_bpp = {
|
||||
3: 'BBB',
|
||||
6: 'HHH',
|
||||
12: 'III'
|
||||
}
|
||||
|
||||
def save_colorxhtml(procedure, run_mode, image, n_layers, layers, file, args, data):
|
||||
source_file = args.index(0)
|
||||
characters = args.index(1)
|
||||
size = args.index(2)
|
||||
separate = args.index(3)
|
||||
|
||||
if file is None:
|
||||
error = 'No file given'
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
|
||||
if run_mode == Pika.RunMode.INTERACTIVE:
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
PikaUi.init ("file-colorxhtml-save")
|
||||
|
||||
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
|
||||
dialog = Gtk.Dialog(use_header_bar=use_header_bar,
|
||||
title=_("Save as colored HTML text..."))
|
||||
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
|
||||
|
||||
choose_file_dialog = Gtk.FileChooserDialog(use_header_bar=use_header_bar,
|
||||
title=_("Read characters from file..."),
|
||||
action=Gtk.FileChooserAction.OPEN)
|
||||
choose_file_dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
choose_file_dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
|
||||
|
||||
def choose_file(button, user_data=None):
|
||||
choose_file_dialog.show()
|
||||
if choose_file_dialog.run() == Gtk.ResponseType.OK:
|
||||
characters_entry.set_text(choose_file_dialog.get_filename())
|
||||
|
||||
choose_file_dialog.hide()
|
||||
|
||||
grid = Gtk.Grid()
|
||||
grid.set_column_homogeneous(False)
|
||||
grid.set_border_width(10)
|
||||
grid.set_column_spacing(10)
|
||||
grid.set_row_spacing(10)
|
||||
|
||||
row = 0
|
||||
label = Gtk.Label(label=_("Characters"))
|
||||
label.set_tooltip_text(_("Characters that will be used as colored pixels. "))
|
||||
|
||||
grid.attach(label, 0, row, 1 , 1)
|
||||
label.show()
|
||||
|
||||
characters_entry = Gtk.Entry()
|
||||
characters_entry.set_width_chars(20)
|
||||
characters_entry.set_max_width_chars(80)
|
||||
characters_entry.set_text(characters)
|
||||
characters_entry.set_placeholder_text(_("Characters or file location"))
|
||||
grid.attach(characters_entry, 1, row, 1, 1)
|
||||
characters_entry.show()
|
||||
|
||||
row += 1
|
||||
|
||||
characters_checkbox = Gtk.CheckButton(label=_("Read characters from file"))
|
||||
characters_checkbox.set_active(source_file)
|
||||
characters_checkbox.set_tooltip_text(
|
||||
_("If set, the Characters text entry will be used as a file name, "
|
||||
"from which the characters will be read. Otherwise, the characters "
|
||||
"in the text entry will be used to render the image."))
|
||||
grid.attach(characters_checkbox, 0, row, 1, 1)
|
||||
characters_checkbox.show()
|
||||
|
||||
choose_file_button = Gtk.Button(label=_("Choose file"))
|
||||
grid.attach(choose_file_button, 1, row, 1, 1)
|
||||
choose_file_button.connect("clicked", choose_file)
|
||||
choose_file_button.show()
|
||||
|
||||
row += 1
|
||||
|
||||
label = Gtk.Label(label=_("Font Size(px)"))
|
||||
grid.attach(label, 0, row, 1 , 1)
|
||||
label.show()
|
||||
|
||||
font_size_adj = Gtk.Adjustment.new(size, 0.0, 100.0, 1.0, 0.0, 0.0)
|
||||
font_size_spin = Gtk.SpinButton.new(font_size_adj, climb_rate=1.0, digits=0)
|
||||
font_size_spin.set_numeric(True)
|
||||
grid.attach(font_size_spin, 1, row, 1 , 1)
|
||||
font_size_spin.show()
|
||||
|
||||
row += 1
|
||||
|
||||
separate_checkbox = Gtk.CheckButton(label=_("Write separate CSS file"))
|
||||
separate_checkbox.set_active(separate)
|
||||
grid.attach(separate_checkbox, 0, row, 2, 1)
|
||||
separate_checkbox.show()
|
||||
|
||||
dialog.get_content_area().add(grid)
|
||||
grid.show()
|
||||
|
||||
dialog.show()
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
separate = separate_checkbox.get_active()
|
||||
size = font_size_spin.get_value_as_int()
|
||||
source_file = characters_checkbox.get_active()
|
||||
characters = characters_entry.get_text()
|
||||
else:
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL,
|
||||
GLib.Error())
|
||||
|
||||
#For now, work with a single layer
|
||||
layer = layers[0]
|
||||
|
||||
width = layer.get_width()
|
||||
height = layer.get_height()
|
||||
bpp = layer.get_bpp()
|
||||
|
||||
html = open(file.peek_path(), 'w')
|
||||
|
||||
if separate:
|
||||
dirname, cssfile = os.path.split(file.peek_path())
|
||||
cssfile = os.path.splitext(cssfile)[0] + '.css'
|
||||
cssname = os.path.join(dirname, cssfile)
|
||||
|
||||
css = open(cssname, 'w')
|
||||
|
||||
if source_file:
|
||||
characters_file = open(characters, 'r')
|
||||
chars = characters_file.read()
|
||||
characters_file.close()
|
||||
else:
|
||||
chars = characters
|
||||
|
||||
# Remove unprintable characters from "chars".
|
||||
# TODO: This only handles ascii files. It would be nice to handle unicode
|
||||
# files, so this could work for any language.
|
||||
goodchars = string.digits + string.ascii_letters + string.punctuation
|
||||
badchars = ''.join(chr(i) for i in range(256) if chr(i) not in goodchars)
|
||||
allchars = str.maketrans('', '', badchars)
|
||||
chars = chars.translate(allchars)
|
||||
|
||||
data = [escape_table.get(c, c) for c in chars]
|
||||
|
||||
if data:
|
||||
data.reverse()
|
||||
else:
|
||||
data = list('X' * 80)
|
||||
|
||||
Pika.progress_init(_("Saving as colored XHTML"))
|
||||
|
||||
style = style_def % size
|
||||
|
||||
if separate:
|
||||
ss = '<link rel="stylesheet" type="text/css" href="%s" />' % cssfile
|
||||
css.write(style)
|
||||
else:
|
||||
ss = '<style type="text/css">\n%s</style>' % style
|
||||
|
||||
html.write(preamble % ss)
|
||||
|
||||
colors = {}
|
||||
chars = []
|
||||
|
||||
# Constants used for formatting the pixel color. We can handle image
|
||||
# types where each color is 8 bits, 16 bits, or 32 bit integers.
|
||||
fmt = fmt_from_bpp[bpp]
|
||||
pixel_shift = 8 * (bpp//3 - 1)
|
||||
|
||||
for y in range(0, height):
|
||||
|
||||
# The characters in "chars" will be used to draw the next row.
|
||||
# Lets fill "chars" with data so it has at least enough characters.
|
||||
while len(chars) < width:
|
||||
chars[0:0] = data
|
||||
|
||||
for x in range(0, width):
|
||||
pixel_bytes = layer.get_pixel(x, y)
|
||||
pixel_tuple = struct.unpack(fmt, pixel_bytes)
|
||||
if bpp > 3:
|
||||
pixel_tuple=(
|
||||
pixel_tuple[0] >> pixel_shift,
|
||||
pixel_tuple[1] >> pixel_shift,
|
||||
pixel_tuple[2] >> pixel_shift,
|
||||
)
|
||||
color = '%02x%02x%02x' % pixel_tuple
|
||||
style = 'background-color:black; color:#%s;' % color
|
||||
char = chars.pop()
|
||||
|
||||
if separate:
|
||||
if color not in colors:
|
||||
css.write('span.N%s { %s }\n' % (color, style))
|
||||
colors[color] = 1
|
||||
|
||||
html.write('<span class="N%s">%s</span>' % (color, char))
|
||||
|
||||
else:
|
||||
html.write('<span style="%s">%s</span>' % (style, char))
|
||||
|
||||
html.write('\n')
|
||||
|
||||
Pika.progress_update(y / float(height))
|
||||
|
||||
html.write(postamble)
|
||||
|
||||
html.close()
|
||||
if separate:
|
||||
css.close()
|
||||
|
||||
return Pika.ValueArray.new_from_values([
|
||||
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS)
|
||||
])
|
||||
|
||||
|
||||
class ColorXhtml(Pika.PlugIn):
|
||||
## Parameters ##
|
||||
__gproperties__ = {
|
||||
"source-file":(bool,
|
||||
_("_Read characters from file, if true, or use text entry"),
|
||||
_("_Read characters from file, if true, or use text entry"),
|
||||
False,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"characters": (str,
|
||||
_("_File to read or characters to use"),
|
||||
_("_File to read or characters to use"),
|
||||
"foo",
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"font-size": (int,
|
||||
_("Fo_nt size in pixels"),
|
||||
_("Fo_nt size in pixels"),
|
||||
5, 100, 10,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"separate": (bool,
|
||||
_("_Write a separate CSS file"),
|
||||
_("_Write a separate CSS file"),
|
||||
False,
|
||||
GObject.ParamFlags.READWRITE)
|
||||
}
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ 'file-colorxhtml-save' ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = None
|
||||
if name == 'file-colorxhtml-save':
|
||||
procedure = Pika.SaveProcedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
save_colorxhtml, None)
|
||||
procedure.set_image_types("RGB")
|
||||
procedure.set_documentation (
|
||||
_("Save as colored HTML text"),
|
||||
"Saves the image as colored XHTML text (based on Perl version by Marc Lehmann)",
|
||||
name)
|
||||
procedure.set_menu_label(_("Colored HTML text"))
|
||||
procedure.set_attribution("Manish Singh and Carol Spears",
|
||||
"(c) GPL V3.0 or later",
|
||||
"2003")
|
||||
|
||||
procedure.set_extensions ("html,xhtml");
|
||||
|
||||
procedure.add_argument_from_property(self, "source-file")
|
||||
procedure.add_argument_from_property(self, "characters")
|
||||
procedure.add_argument_from_property(self, "font-size")
|
||||
procedure.add_argument_from_property(self, "separate")
|
||||
|
||||
return procedure
|
||||
|
||||
Pika.main(ColorXhtml.__gtype__, sys.argv)
|
498
plug-ins/python/file-openraster.py
Normal file
498
plug-ins/python/file-openraster.py
Normal file
@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# PIKA Plug-in for the OpenRaster file format
|
||||
# http://create.freedesktop.org/wiki/OpenRaster
|
||||
|
||||
# Copyright (C) 2009 by Jon Nordby <jononor@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Based on MyPaint source code by Martin Renold
|
||||
# http://gitorious.org/mypaint/mypaint/blobs/edd84bcc1e091d0d56aa6d26637aa8a925987b6a/lib/document.py
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('Gegl', '0.4')
|
||||
from gi.repository import Gegl
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
|
||||
import os, sys, tempfile, zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
NESTED_STACK_END = object()
|
||||
|
||||
layermodes_map = {
|
||||
"svg:src-over": Pika.LayerMode.NORMAL,
|
||||
"svg:multiply": Pika.LayerMode.MULTIPLY,
|
||||
"svg:screen": Pika.LayerMode.SCREEN,
|
||||
"svg:overlay": Pika.LayerMode.OVERLAY,
|
||||
"svg:darken": Pika.LayerMode.DARKEN_ONLY,
|
||||
"svg:lighten": Pika.LayerMode.LIGHTEN_ONLY,
|
||||
"svg:color-dodge": Pika.LayerMode.DODGE,
|
||||
"svg:color-burn": Pika.LayerMode.BURN,
|
||||
"svg:hard-light": Pika.LayerMode.HARDLIGHT,
|
||||
"svg:soft-light": Pika.LayerMode.SOFTLIGHT,
|
||||
"svg:difference": Pika.LayerMode.DIFFERENCE,
|
||||
"svg:color": Pika.LayerMode.HSL_COLOR,
|
||||
"svg:luminosity": Pika.LayerMode.HSV_VALUE,
|
||||
"svg:hue": Pika.LayerMode.HSV_HUE,
|
||||
"svg:saturation": Pika.LayerMode.HSV_SATURATION,
|
||||
"svg:plus": Pika.LayerMode.ADDITION,
|
||||
}
|
||||
|
||||
# There are less svg blending ops than we have PIKA blend modes.
|
||||
# We are going to map them as closely as possible.
|
||||
pika_layermodes_map = {
|
||||
Pika.LayerMode.NORMAL: "svg:src-over",
|
||||
Pika.LayerMode.NORMAL_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.MULTIPLY: "svg:multiply",
|
||||
Pika.LayerMode.MULTIPLY_LEGACY: "svg:multiply",
|
||||
Pika.LayerMode.SCREEN: "svg:screen",
|
||||
Pika.LayerMode.SCREEN_LEGACY: "svg:screen",
|
||||
Pika.LayerMode.OVERLAY: "svg:overlay",
|
||||
Pika.LayerMode.OVERLAY_LEGACY: "svg:overlay",
|
||||
Pika.LayerMode.DARKEN_ONLY: "svg:darken",
|
||||
Pika.LayerMode.DARKEN_ONLY_LEGACY: "svg:darken",
|
||||
Pika.LayerMode.LIGHTEN_ONLY: "svg:lighten",
|
||||
Pika.LayerMode.LIGHTEN_ONLY_LEGACY: "svg:lighten",
|
||||
Pika.LayerMode.DODGE: "svg:color-dodge",
|
||||
Pika.LayerMode.DODGE_LEGACY: "svg:color-dodge",
|
||||
Pika.LayerMode.BURN: "svg:color-burn",
|
||||
Pika.LayerMode.BURN_LEGACY: "svg:color-burn",
|
||||
Pika.LayerMode.HARDLIGHT: "svg:hard-light",
|
||||
Pika.LayerMode.HARDLIGHT_LEGACY: "svg:hard-light",
|
||||
Pika.LayerMode.SOFTLIGHT: "svg:soft-light",
|
||||
Pika.LayerMode.SOFTLIGHT_LEGACY: "svg:soft-light",
|
||||
Pika.LayerMode.DIFFERENCE: "svg:difference",
|
||||
Pika.LayerMode.DIFFERENCE_LEGACY: "svg:difference",
|
||||
Pika.LayerMode.HSL_COLOR: "svg:color",
|
||||
Pika.LayerMode.HSL_COLOR_LEGACY: "svg:color",
|
||||
Pika.LayerMode.HSV_VALUE: "svg:luminosity",
|
||||
Pika.LayerMode.HSV_VALUE_LEGACY: "svg:luminosity",
|
||||
Pika.LayerMode.HSV_HUE: "svg:hue",
|
||||
Pika.LayerMode.HSV_HUE_LEGACY: "svg:hue",
|
||||
Pika.LayerMode.HSV_SATURATION: "svg:saturation",
|
||||
Pika.LayerMode.HSV_SATURATION_LEGACY: "svg:saturation",
|
||||
Pika.LayerMode.ADDITION: "svg:plus",
|
||||
Pika.LayerMode.ADDITION_LEGACY: "svg:plus",
|
||||
|
||||
# FIXME Determine the closest available layer mode
|
||||
# Alternatively we could add additional modes
|
||||
# e.g. something like "pika:dissolve", this
|
||||
# is what Krita seems to do too
|
||||
Pika.LayerMode.DISSOLVE: "svg:src-over",
|
||||
Pika.LayerMode.DIVIDE: "svg:src-over",
|
||||
Pika.LayerMode.DIVIDE_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.BEHIND: "svg:src-over",
|
||||
Pika.LayerMode.BEHIND_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.GRAIN_EXTRACT: "svg:src-over",
|
||||
Pika.LayerMode.GRAIN_EXTRACT_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.GRAIN_MERGE: "svg:src-over",
|
||||
Pika.LayerMode.GRAIN_MERGE_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.COLOR_ERASE: "svg:src-over",
|
||||
Pika.LayerMode.COLOR_ERASE_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.LCH_HUE: "svg:src-over",
|
||||
Pika.LayerMode.LCH_CHROMA: "svg:src-over",
|
||||
Pika.LayerMode.LCH_COLOR: "svg:src-over",
|
||||
Pika.LayerMode.LCH_LIGHTNESS: "svg:src-over",
|
||||
Pika.LayerMode.SUBTRACT: "svg:src-over",
|
||||
Pika.LayerMode.SUBTRACT_LEGACY: "svg:src-over",
|
||||
Pika.LayerMode.VIVID_LIGHT: "svg:src-over",
|
||||
Pika.LayerMode.PIN_LIGHT: "svg:src-over",
|
||||
Pika.LayerMode.LINEAR_LIGHT: "svg:src-over",
|
||||
Pika.LayerMode.HARD_MIX: "svg:src-over",
|
||||
Pika.LayerMode.EXCLUSION: "svg:src-over",
|
||||
Pika.LayerMode.LINEAR_BURN: "svg:src-over",
|
||||
Pika.LayerMode.LUMA_DARKEN_ONLY: "svg:src-over",
|
||||
Pika.LayerMode.LUMA_LIGHTEN_ONLY: "svg:src-over",
|
||||
Pika.LayerMode.LUMINANCE: "svg:src-over",
|
||||
Pika.LayerMode.COLOR_ERASE: "svg:src-over",
|
||||
Pika.LayerMode.ERASE: "svg:src-over",
|
||||
Pika.LayerMode.MERGE: "svg:src-over",
|
||||
Pika.LayerMode.SPLIT: "svg:src-over",
|
||||
Pika.LayerMode.PASS_THROUGH: "svg:src-over",
|
||||
}
|
||||
|
||||
def reverse_map(mapping):
|
||||
return dict((v,k) for k, v in mapping.items())
|
||||
|
||||
def get_image_attributes(orafile):
|
||||
xml = orafile.read('stack.xml')
|
||||
image = ET.fromstring(xml)
|
||||
stack = image.find('stack')
|
||||
w = int(image.attrib.get('w', ''))
|
||||
h = int(image.attrib.get('h', ''))
|
||||
|
||||
return stack, w, h
|
||||
|
||||
def get_layer_attributes(layer):
|
||||
a = layer.attrib
|
||||
path = a.get('src', '')
|
||||
name = a.get('name', '')
|
||||
x = int(a.get('x', '0'))
|
||||
y = int(a.get('y', '0'))
|
||||
opac = float(a.get('opacity', '1.0'))
|
||||
visible = a.get('visibility', 'visible') != 'hidden'
|
||||
m = a.get('composite-op', 'svg:src-over')
|
||||
layer_mode = layermodes_map.get(m, Pika.LayerMode.NORMAL)
|
||||
|
||||
return path, name, x, y, opac, visible, layer_mode
|
||||
|
||||
def get_group_layer_attributes(layer):
|
||||
a = layer.attrib
|
||||
name = a.get('name', '')
|
||||
opac = float(a.get('opacity', '1.0'))
|
||||
visible = a.get('visibility', 'visible') != 'hidden'
|
||||
m = a.get('composite-op', 'svg:src-over')
|
||||
layer_mode = layermodes_map.get(m, Pika.LayerMode.NORMAL)
|
||||
|
||||
return name, 0, 0, opac, visible, layer_mode
|
||||
|
||||
def thumbnail_ora(procedure, file, thumb_size, args, data):
|
||||
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
|
||||
orafile = zipfile.ZipFile(file.peek_path())
|
||||
stack, w, h = get_image_attributes(orafile)
|
||||
|
||||
# create temp file
|
||||
tmp = os.path.join(tempdir, 'tmp.png')
|
||||
with open(tmp, 'wb') as fid:
|
||||
fid.write(orafile.read('Thumbnails/thumbnail.png'))
|
||||
|
||||
thumb_file = Gio.file_new_for_path(tmp)
|
||||
result = Pika.get_pdb().run_procedure('file-png-load', [
|
||||
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
|
||||
GObject.Value(Gio.File, thumb_file),
|
||||
])
|
||||
os.remove(tmp)
|
||||
os.rmdir(tempdir)
|
||||
|
||||
if (result.index(0) == Pika.PDBStatusType.SUCCESS):
|
||||
img = result.index(1)
|
||||
# TODO: scaling
|
||||
|
||||
return Pika.ValueArray.new_from_values([
|
||||
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS),
|
||||
GObject.Value(Pika.Image, img),
|
||||
GObject.Value(GObject.TYPE_INT, w),
|
||||
GObject.Value(GObject.TYPE_INT, h),
|
||||
GObject.Value(Pika.ImageType, Pika.ImageType.RGB_IMAGE),
|
||||
GObject.Value(GObject.TYPE_INT, 1)
|
||||
])
|
||||
else:
|
||||
return procedure.new_return_values(result.index(0), GLib.Error(result.index(1)))
|
||||
|
||||
# We would expect the n_drawables parameter to not be there with introspection but
|
||||
# currently that isn't working, see issue #5312. Until that is resolved we keep
|
||||
# this parameter here or else saving would fail.
|
||||
def save_ora(procedure, run_mode, image, n_drawables, drawables, file, args, data):
|
||||
def write_file_str(zfile, fname, data):
|
||||
# work around a permission bug in the zipfile library:
|
||||
# http://bugs.python.org/issue3394
|
||||
zi = zipfile.ZipInfo(fname)
|
||||
zi.external_attr = int("100644", 8) << 16
|
||||
zfile.writestr(zi, data)
|
||||
|
||||
Pika.progress_init("Exporting openraster image")
|
||||
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
|
||||
|
||||
# use .tmpsave extension, so we don't overwrite a valid file if
|
||||
# there is an exception
|
||||
orafile = zipfile.ZipFile(file.peek_path() + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
|
||||
|
||||
write_file_str(orafile, 'mimetype', 'image/openraster') # must be the first file written
|
||||
|
||||
# build image attributes
|
||||
xml_image = ET.Element('image')
|
||||
stack = ET.SubElement(xml_image, 'stack')
|
||||
a = xml_image.attrib
|
||||
a['w'] = str(image.get_width())
|
||||
a['h'] = str(image.get_height())
|
||||
|
||||
def store_layer(image, drawable, path):
|
||||
tmp = os.path.join(tempdir, 'tmp.png')
|
||||
interlace, compression = 0, 2
|
||||
|
||||
Pika.get_pdb().run_procedure('file-png-save', [
|
||||
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
|
||||
GObject.Value(Pika.Image, image),
|
||||
GObject.Value(GObject.TYPE_INT, 1),
|
||||
GObject.Value(Pika.ObjectArray, Pika.ObjectArray.new(Pika.Drawable, [drawable], False)),
|
||||
GObject.Value(Gio.File, Gio.File.new_for_path(tmp)),
|
||||
GObject.Value(GObject.TYPE_BOOLEAN, interlace),
|
||||
GObject.Value(GObject.TYPE_INT, compression),
|
||||
# write all PNG chunks except oFFs(ets)
|
||||
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save background color (bKGD chunk)
|
||||
GObject.Value(GObject.TYPE_BOOLEAN, False), # Save layer offset (oFFs chunk)
|
||||
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save resolution (pHYs chunk)
|
||||
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save creation time (tIME chunk)
|
||||
# Other settings
|
||||
GObject.Value(GObject.TYPE_BOOLEAN, True), # Save color values from transparent pixels
|
||||
])
|
||||
if (os.path.exists(tmp)):
|
||||
orafile.write(tmp, path)
|
||||
os.remove(tmp)
|
||||
else:
|
||||
print("Error removing ", tmp)
|
||||
|
||||
def add_layer(parent, x, y, opac, pika_layer, path, visible=True):
|
||||
store_layer(image, pika_layer, path)
|
||||
# create layer attributes
|
||||
layer = ET.Element('layer')
|
||||
parent.append(layer)
|
||||
a = layer.attrib
|
||||
a['src'] = path
|
||||
a['name'] = pika_layer.get_name()
|
||||
a['x'] = str(x)
|
||||
a['y'] = str(y)
|
||||
a['opacity'] = str(opac)
|
||||
a['visibility'] = 'visible' if visible else 'hidden'
|
||||
a['composite-op'] = pika_layermodes_map.get(pika_layer.get_mode(), 'svg:src-over')
|
||||
return layer
|
||||
|
||||
def add_group_layer(parent, opac, pika_layer, visible=True):
|
||||
# create layer attributes
|
||||
group_layer = ET.Element('stack')
|
||||
parent.append(group_layer)
|
||||
a = group_layer.attrib
|
||||
a['name'] = pika_layer.get_name()
|
||||
a['opacity'] = str(opac)
|
||||
a['visibility'] = 'visible' if visible else 'hidden'
|
||||
a['composite-op'] = pika_layermodes_map.get(pika_layer.get_mode(), 'svg:src-over')
|
||||
return group_layer
|
||||
|
||||
|
||||
def enumerate_layers(layers):
|
||||
for layer in layers:
|
||||
if not layer.is_group():
|
||||
yield layer
|
||||
else:
|
||||
yield layer
|
||||
for sublayer in enumerate_layers(layer.list_children()):
|
||||
yield sublayer
|
||||
yield NESTED_STACK_END
|
||||
|
||||
# save layers
|
||||
parent_groups = []
|
||||
i = 0
|
||||
|
||||
layer_stack = image.list_layers()
|
||||
# Number of top level layers for tracking progress
|
||||
lay_cnt = len(layer_stack)
|
||||
|
||||
for lay in enumerate_layers(layer_stack):
|
||||
prev_lay = i
|
||||
if lay is NESTED_STACK_END:
|
||||
parent_groups.pop()
|
||||
continue
|
||||
_, x, y = lay.get_offsets()
|
||||
opac = lay.get_opacity () / 100.0 # needs to be between 0.0 and 1.0
|
||||
|
||||
if not parent_groups:
|
||||
path_name = 'data/{:03d}.png'.format(i)
|
||||
i += 1
|
||||
else:
|
||||
path_name = 'data/{}-{:03d}.png'.format(
|
||||
parent_groups[-1][1], parent_groups[-1][2])
|
||||
parent_groups[-1][2] += 1
|
||||
|
||||
parent = stack if not parent_groups else parent_groups[-1][0]
|
||||
|
||||
if lay.is_group():
|
||||
group = add_group_layer(parent, opac, lay, lay.get_visible())
|
||||
group_path = ("{:03d}".format(i) if not parent_groups else
|
||||
parent_groups[-1][1] + "-{:03d}".format(parent_groups[-1][2]))
|
||||
parent_groups.append([group, group_path , 0])
|
||||
else:
|
||||
add_layer(parent, x, y, opac, lay, path_name, lay.get_visible())
|
||||
|
||||
if (i > prev_lay):
|
||||
Pika.progress_update(i/lay_cnt)
|
||||
|
||||
# save mergedimage
|
||||
thumb = image.duplicate()
|
||||
thumb_layer = thumb.merge_visible_layers (Pika.MergeType.CLIP_TO_IMAGE)
|
||||
store_layer (thumb, thumb_layer, 'mergedimage.png')
|
||||
|
||||
# save thumbnail
|
||||
w, h = image.get_width(), image.get_height()
|
||||
if max (w, h) > 256:
|
||||
# should be at most 256x256, without changing aspect ratio
|
||||
if w > h:
|
||||
w, h = 256, max(h*256/w, 1)
|
||||
else:
|
||||
w, h = max(w*256/h, 1), 256
|
||||
thumb_layer.scale(w, h, False)
|
||||
if thumb.get_precision() != Pika.Precision.U8_GAMMA:
|
||||
thumb.convert_precision (Pika.Precision.U8_GAMMA)
|
||||
store_layer(thumb, thumb_layer, 'Thumbnails/thumbnail.png')
|
||||
thumb.delete()
|
||||
|
||||
# write stack.xml
|
||||
xml = ET.tostring(xml_image, encoding='UTF-8')
|
||||
write_file_str(orafile, 'stack.xml', xml)
|
||||
|
||||
# finish up
|
||||
orafile.close()
|
||||
os.rmdir(tempdir)
|
||||
if os.path.exists(file.peek_path()):
|
||||
os.remove(file.peek_path()) # win32 needs that
|
||||
os.rename(file.peek_path() + '.tmpsave', file.peek_path())
|
||||
|
||||
Pika.progress_end()
|
||||
|
||||
return Pika.ValueArray.new_from_values([
|
||||
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS)
|
||||
])
|
||||
|
||||
def load_ora(procedure, run_mode, file, args, data):
|
||||
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
|
||||
orafile = zipfile.ZipFile(file.peek_path())
|
||||
stack, w, h = get_image_attributes(orafile)
|
||||
|
||||
Pika.progress_init("Loading openraster image")
|
||||
|
||||
img = Pika.Image.new(w, h, Pika.ImageBaseType.RGB)
|
||||
img.set_file (file)
|
||||
|
||||
def get_layers(root):
|
||||
"""iterates over layers and nested stacks"""
|
||||
for item in root:
|
||||
if item.tag == 'layer':
|
||||
yield item
|
||||
elif item.tag == 'stack':
|
||||
yield item
|
||||
for subitem in get_layers(item):
|
||||
yield subitem
|
||||
yield NESTED_STACK_END
|
||||
|
||||
parent_groups = []
|
||||
|
||||
# Number of top level layers for tracking progress
|
||||
lay_cnt = len(stack)
|
||||
|
||||
layer_no = 0
|
||||
for item in get_layers(stack):
|
||||
prev_lay = layer_no
|
||||
|
||||
if item is NESTED_STACK_END:
|
||||
parent_groups.pop()
|
||||
continue
|
||||
|
||||
if item.tag == 'stack':
|
||||
name, x, y, opac, visible, layer_mode = get_group_layer_attributes(item)
|
||||
pika_layer = Pika.Layer.group_new(img)
|
||||
|
||||
else:
|
||||
path, name, x, y, opac, visible, layer_mode = get_layer_attributes(item)
|
||||
|
||||
if not path.lower().endswith('.png'):
|
||||
continue
|
||||
if not name:
|
||||
# use the filename without extension as name
|
||||
n = os.path.basename(path)
|
||||
name = os.path.splitext(n)[0]
|
||||
|
||||
# create temp file. Needed because pika cannot load files from inside a zip file
|
||||
tmp = os.path.join(tempdir, 'tmp.png')
|
||||
with open(tmp, 'wb') as fid:
|
||||
try:
|
||||
data = orafile.read(path)
|
||||
except KeyError:
|
||||
# support for bad zip files (saved by old versions of this plugin)
|
||||
data = orafile.read(path.encode('utf-8'))
|
||||
print('WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(path))
|
||||
fid.write(data)
|
||||
|
||||
# import layer, set attributes and add to image
|
||||
result = pika_layer = Pika.get_pdb().run_procedure('pika-file-load-layer', [
|
||||
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
|
||||
GObject.Value(Pika.Image, img),
|
||||
GObject.Value(Gio.File, Gio.File.new_for_path(tmp)),
|
||||
])
|
||||
if (result.index(0) == Pika.PDBStatusType.SUCCESS):
|
||||
pika_layer = pika_layer.index(1)
|
||||
os.remove(tmp)
|
||||
else:
|
||||
print("Error loading layer from openraster image.")
|
||||
|
||||
pika_layer.set_name(name)
|
||||
pika_layer.set_mode(layer_mode)
|
||||
pika_layer.set_offsets(x, y) # move to correct position
|
||||
pika_layer.set_opacity(opac * 100) # a float between 0 and 100
|
||||
pika_layer.set_visible(visible)
|
||||
|
||||
img.insert_layer(pika_layer,
|
||||
parent_groups[-1][0] if parent_groups else None,
|
||||
parent_groups[-1][1] if parent_groups else layer_no)
|
||||
if parent_groups:
|
||||
parent_groups[-1][1] += 1
|
||||
else:
|
||||
layer_no += 1
|
||||
|
||||
if pika_layer.is_group():
|
||||
parent_groups.append([pika_layer, 0])
|
||||
|
||||
if (layer_no > prev_lay):
|
||||
Pika.progress_update(layer_no/lay_cnt)
|
||||
|
||||
Pika.progress_end()
|
||||
|
||||
os.rmdir(tempdir)
|
||||
|
||||
return Pika.ValueArray.new_from_values([
|
||||
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS),
|
||||
GObject.Value(Pika.Image, img),
|
||||
])
|
||||
|
||||
|
||||
class FileOpenRaster (Pika.PlugIn):
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ 'file-openraster-load-thumb',
|
||||
'file-openraster-load',
|
||||
'file-openraster-save' ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
if name == 'file-openraster-save':
|
||||
procedure = Pika.SaveProcedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
save_ora, None)
|
||||
procedure.set_image_types("*");
|
||||
procedure.set_documentation ('save an OpenRaster (.ora) file',
|
||||
'save an OpenRaster (.ora) file',
|
||||
name)
|
||||
procedure.set_menu_label('OpenRaster')
|
||||
procedure.set_extensions ("ora");
|
||||
elif name == 'file-openraster-load':
|
||||
procedure = Pika.LoadProcedure.new (self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
load_ora, None)
|
||||
procedure.set_menu_label('OpenRaster')
|
||||
procedure.set_documentation ('load an OpenRaster (.ora) file',
|
||||
'load an OpenRaster (.ora) file',
|
||||
name)
|
||||
procedure.set_mime_types ("image/openraster");
|
||||
procedure.set_extensions ("ora");
|
||||
procedure.set_thumbnail_loader ('file-openraster-load-thumb');
|
||||
else: # 'file-openraster-load-thumb'
|
||||
procedure = Pika.ThumbnailProcedure.new (self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
thumbnail_ora, None)
|
||||
procedure.set_documentation ('loads a thumbnail from an OpenRaster (.ora) file',
|
||||
'loads a thumbnail from an OpenRaster (.ora) file',
|
||||
name)
|
||||
procedure.set_attribution('Jon Nordby', #author
|
||||
'Jon Nordby', #copyright
|
||||
'2009') #year
|
||||
return procedure
|
||||
|
||||
Pika.main(FileOpenRaster.__gtype__, sys.argv)
|
156
plug-ins/python/foggify.py
Normal file
156
plug-ins/python/foggify.py
Normal file
@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 1997 James Henstridge <james@daa.com.au>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
import time
|
||||
import sys
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
|
||||
def foggify(procedure, run_mode, image, n_drawables, drawables, args, data):
|
||||
config = procedure.create_config()
|
||||
config.begin_run(image, run_mode, args)
|
||||
|
||||
if run_mode == Pika.RunMode.INTERACTIVE:
|
||||
PikaUi.init('python-fu-foggify')
|
||||
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
|
||||
dialog.fill(None)
|
||||
if not dialog.run():
|
||||
dialog.destroy()
|
||||
config.end_run(Pika.PDBStatusType.CANCEL)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
|
||||
else:
|
||||
dialog.destroy()
|
||||
|
||||
color = config.get_property('color')
|
||||
name = config.get_property('name')
|
||||
turbulence = config.get_property('turbulence')
|
||||
opacity = config.get_property('opacity')
|
||||
|
||||
Pika.context_push()
|
||||
image.undo_group_start()
|
||||
|
||||
if image.get_base_type() is Pika.ImageBaseType.RGB:
|
||||
type = Pika.ImageType.RGBA_IMAGE
|
||||
else:
|
||||
type = Pika.ImageType.GRAYA_IMAGE
|
||||
for drawable in drawables:
|
||||
fog = Pika.Layer.new(image, name,
|
||||
drawable.get_width(), drawable.get_height(),
|
||||
type, opacity,
|
||||
Pika.LayerMode.NORMAL)
|
||||
fog.fill(Pika.FillType.TRANSPARENT)
|
||||
image.insert_layer(fog, drawable.get_parent(),
|
||||
image.get_item_position(drawable))
|
||||
|
||||
Pika.context_set_background(color)
|
||||
fog.edit_fill(Pika.FillType.BACKGROUND)
|
||||
|
||||
# create a layer mask for the new layer
|
||||
mask = fog.create_mask(0)
|
||||
fog.add_mask(mask)
|
||||
|
||||
# add some clouds to the layer
|
||||
Pika.get_pdb().run_procedure('plug-in-plasma', [
|
||||
GObject.Value(Pika.RunMode, Pika.RunMode.NONINTERACTIVE),
|
||||
GObject.Value(Pika.Image, image),
|
||||
GObject.Value(Pika.Drawable, mask),
|
||||
GObject.Value(GObject.TYPE_INT, int(time.time())),
|
||||
GObject.Value(GObject.TYPE_DOUBLE, turbulence),
|
||||
])
|
||||
|
||||
# apply the clouds to the layer
|
||||
fog.remove_mask(Pika.MaskApplyMode.APPLY)
|
||||
fog.set_visible(True)
|
||||
|
||||
Pika.displays_flush()
|
||||
|
||||
image.undo_group_end()
|
||||
Pika.context_pop()
|
||||
|
||||
config.end_run(Pika.PDBStatusType.SUCCESS)
|
||||
|
||||
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
|
||||
_color = Pika.RGB()
|
||||
_color.set(240.0, 0, 0)
|
||||
|
||||
class Foggify (Pika.PlugIn):
|
||||
## Parameters ##
|
||||
__gproperties__ = {
|
||||
"name": (str,
|
||||
_("Layer _name"),
|
||||
_("Layer name"),
|
||||
_("Clouds"),
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"turbulence": (float,
|
||||
_("_Turbulence"),
|
||||
_("Turbulence"),
|
||||
0.0, 7.0, 1.0,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"opacity": (float,
|
||||
_("O_pacity"),
|
||||
_("Opacity"),
|
||||
0.0, 100.0, 100.0,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
}
|
||||
# I use a different syntax for this property because I think it is
|
||||
# supposed to allow setting a default, except it doesn't seem to
|
||||
# work. I still leave it this way for now until we figure this out
|
||||
# as it should be the better syntax.
|
||||
color = GObject.Property(type =Pika.RGB, default=_color,
|
||||
nick =_("_Fog color"),
|
||||
blurb=_("Fog color"))
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ 'python-fu-foggify' ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = Pika.ImageProcedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
foggify, None)
|
||||
procedure.set_image_types("RGB*, GRAY*");
|
||||
procedure.set_sensitivity_mask (Pika.ProcedureSensitivityMask.DRAWABLE |
|
||||
Pika.ProcedureSensitivityMask.DRAWABLES)
|
||||
procedure.set_documentation (_("Add a layer of fog"),
|
||||
_("Adds a layer of fog to the image."),
|
||||
name)
|
||||
procedure.set_menu_label(_("_Fog..."))
|
||||
procedure.set_attribution("James Henstridge",
|
||||
"James Henstridge",
|
||||
"1999,2007")
|
||||
procedure.add_menu_path ("<Image>/Filters/Decor")
|
||||
|
||||
procedure.add_argument_from_property(self, "name")
|
||||
procedure.add_argument_from_property(self, "color")
|
||||
procedure.add_argument_from_property(self, "turbulence")
|
||||
procedure.add_argument_from_property(self, "opacity")
|
||||
return procedure
|
||||
|
||||
Pika.main(Foggify.__gtype__, sys.argv)
|
239
plug-ins/python/gradients-save-as-css.py
Normal file
239
plug-ins/python/gradients-save-as-css.py
Normal file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Allows saving (TODO: and loading) CSS gradient files
|
||||
# Copyright (C) 2011 João S. O. Bueno <gwidion@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# Currently this exports all color segments as RGB linear centered segments.
|
||||
# TODO: Respect gradient alpha, off-center segments, different blending
|
||||
# functions and HSV colors
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
import time
|
||||
import sys
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
|
||||
w3c_template = """background-image: linear-gradient(top, %s);\n"""
|
||||
moz_template = """background-image: -moz-linear-gradient(center top, %s);\n"""
|
||||
webkit_template = """background-image: -webkit-gradient(linear, """ \
|
||||
"""left top, left bottom, %s);\n"""
|
||||
|
||||
color_to_html = lambda c: "rgb(%d,%d,%d)" % (c.r, c.g, c.b)
|
||||
|
||||
def format_text(text):
|
||||
counter = 0
|
||||
new_text = []
|
||||
for token in text.split(","):
|
||||
if counter + len(token) > 77:
|
||||
token = "\n " + token
|
||||
counter = 4
|
||||
new_text.append(token)
|
||||
if "\n" in token:
|
||||
counter = len(token.rsplit("\n")[-1]) + 1
|
||||
else:
|
||||
counter += len(token) + 1
|
||||
|
||||
return ",".join(new_text)
|
||||
|
||||
def gradient_css_save(procedure, args, data):
|
||||
if args.length() != 3:
|
||||
error = 'Wrong parameters given'
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
runmode = args.index(0)
|
||||
gradient = args.index(1)
|
||||
file = args.index(2)
|
||||
|
||||
config = procedure.create_config()
|
||||
config.begin_run(None, Pika.RunMode.INTERACTIVE, args)
|
||||
|
||||
if runmode == Pika.RunMode.INTERACTIVE:
|
||||
PikaUi.init('python-fu-gradient-save-as-css')
|
||||
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
|
||||
|
||||
# Add gradient button
|
||||
dialog.fill (["gradient"])
|
||||
|
||||
# UI for the file parameter
|
||||
# from histogram-export.py
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
|
||||
homogeneous=False, spacing=10)
|
||||
dialog.get_content_area().add(vbox)
|
||||
vbox.show()
|
||||
|
||||
grid = Gtk.Grid()
|
||||
grid.set_column_homogeneous(False)
|
||||
grid.set_border_width(10)
|
||||
grid.set_column_spacing(10)
|
||||
grid.set_row_spacing(10)
|
||||
vbox.add(grid)
|
||||
grid.show()
|
||||
|
||||
def choose_file(widget):
|
||||
if file_chooser_dialog.run() == Gtk.ResponseType.OK:
|
||||
if file_chooser_dialog.get_file() is not None:
|
||||
config.set_property("file", file_chooser_dialog.get_file())
|
||||
file_entry.set_text(file_chooser_dialog.get_file().get_path())
|
||||
file_chooser_dialog.hide()
|
||||
|
||||
file_chooser_button = Gtk.Button.new_with_mnemonic(label=_("_File..."))
|
||||
grid.attach(file_chooser_button, 0, 0, 1, 1)
|
||||
file_chooser_button.show()
|
||||
file_chooser_button.connect("clicked", choose_file)
|
||||
|
||||
file_entry = Gtk.Entry.new()
|
||||
grid.attach(file_entry, 1, 0, 1, 1)
|
||||
file_entry.set_width_chars(40)
|
||||
file_entry.set_placeholder_text(_("Choose CSS file..."))
|
||||
if config.get_property ("file") != None:
|
||||
file = config.get_property("file")
|
||||
if file is not None:
|
||||
file_entry.set_text(file.get_path())
|
||||
file_entry.show()
|
||||
|
||||
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
|
||||
file_chooser_dialog = Gtk.FileChooserDialog(use_header_bar=use_header_bar,
|
||||
title=_("Save as CSS file..."),
|
||||
action=Gtk.FileChooserAction.SAVE)
|
||||
file_chooser_dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
file_chooser_dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
|
||||
|
||||
#Connect config so reset works on custom widget
|
||||
def gradient_reset (*args):
|
||||
if len(args) >= 2:
|
||||
if config.get_property("file") is not None and config.get_property("file").get_path() != file_entry.get_text():
|
||||
file_entry.set_text(config.get_property("file").get_path())
|
||||
else:
|
||||
file_entry.set_text("")
|
||||
|
||||
config.connect("notify::gradient", gradient_reset)
|
||||
config.connect("notify::file", gradient_reset)
|
||||
|
||||
if not dialog.run():
|
||||
dialog.destroy()
|
||||
config.end_run(Pika.PDBStatusType.CANCEL)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
|
||||
else:
|
||||
gradient = config.get_property("gradient")
|
||||
file = Gio.file_new_for_path(file_entry.get_text())
|
||||
|
||||
#Save configs for non-connected UI element
|
||||
config.set_property ("file", file)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
if file is None:
|
||||
error = 'No file given'
|
||||
config.end_run(Pika.PDBStatusType.CALLING_ERROR)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
|
||||
stops = []
|
||||
wk_stops = []
|
||||
n_segments = gradient.get_number_of_segments()
|
||||
last_stop = None
|
||||
for index in range(n_segments):
|
||||
success, lcolor, lopacity = gradient.segment_get_left_color(index)
|
||||
success, rcolor, ropacity = gradient.segment_get_right_color(index)
|
||||
success, lpos = gradient.segment_get_left_pos(index)
|
||||
success, rpos = gradient.segment_get_right_pos(index)
|
||||
|
||||
lstop = color_to_html(lcolor) + " %d%%" % int(100 * lpos)
|
||||
wk_lstop = "color-stop(%.03f, %s)" %(lpos, color_to_html(lcolor))
|
||||
if lstop != last_stop:
|
||||
stops.append(lstop)
|
||||
wk_stops.append(wk_lstop)
|
||||
|
||||
rstop = color_to_html(rcolor) + " %d%%" % int(100 * rpos)
|
||||
wk_rstop = "color-stop(%.03f, %s)" %(rpos, color_to_html(rcolor))
|
||||
|
||||
stops.append(rstop)
|
||||
wk_stops.append(wk_rstop)
|
||||
last_stop = rstop
|
||||
|
||||
final_text = w3c_template % ", ".join(stops)
|
||||
final_text += moz_template % ",".join(stops)
|
||||
final_text += webkit_template % ",".join(wk_stops)
|
||||
|
||||
success, etag = file.replace_contents(bytes(format_text(final_text), encoding='utf-8'),
|
||||
etag=None,
|
||||
make_backup=False,
|
||||
flags=Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||||
cancellable=None)
|
||||
if success:
|
||||
config.end_run(Pika.PDBStatusType.SUCCESS)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
else:
|
||||
config.end_run(Pika.PDBStatusType.EXECUTION_ERROR)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
|
||||
GLib.Error('File saving failed: {}'.format(file.get_path())))
|
||||
|
||||
class GradientsSaveAsCSS (Pika.PlugIn):
|
||||
## Parameters ##
|
||||
__gproperties__ = {
|
||||
"run-mode": (Pika.RunMode,
|
||||
_("Run mode"),
|
||||
_("The run mode"),
|
||||
Pika.RunMode.NONINTERACTIVE,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"gradient": (Pika.Gradient,
|
||||
_("_Gradient to use"), "",
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"file": (Gio.File,
|
||||
_("_File"), None,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
}
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ 'gradient-save-as-css' ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = Pika.Procedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
gradient_css_save, None)
|
||||
if name == 'gradient-save-as-css':
|
||||
procedure.set_documentation (_("Creates a new palette from a given gradient"),
|
||||
_("Creates a new palette from a given gradient"),
|
||||
name)
|
||||
procedure.set_menu_label(_("Save as CSS..."))
|
||||
procedure.set_attribution("Joao S. O. Bueno",
|
||||
"(c) GPL V3.0 or later",
|
||||
"2011")
|
||||
procedure.add_menu_path('<Gradients>')
|
||||
|
||||
procedure.add_argument_from_property(self, "run-mode")
|
||||
procedure.add_argument_from_property(self, "gradient")
|
||||
procedure.add_argument_from_property(self, "file")
|
||||
return procedure
|
||||
|
||||
Pika.main(GradientsSaveAsCSS.__gtype__, sys.argv)
|
369
plug-ins/python/histogram-export.py
Normal file
369
plug-ins/python/histogram-export.py
Normal file
@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env python3
|
||||
#coding: utf-8
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Exports the image histogram to a text file,
|
||||
so that it can be used by other programs
|
||||
and loaded into spreadsheets.
|
||||
|
||||
The resulting file is a CSV file (Comma Separated
|
||||
Values), which can be imported
|
||||
directly in most spreadsheet programs.
|
||||
|
||||
The first two columns are the bucket boundaries,
|
||||
followed by the selected columns. The histogram
|
||||
refers to the selected image area, and
|
||||
can use either Sample Average data or data
|
||||
from the current drawable only.;
|
||||
|
||||
The output is in "weighted pixels" - meaning
|
||||
all fully transparent pixels are not counted.
|
||||
|
||||
Check the pika-histogram call
|
||||
"""
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
class StringEnum:
|
||||
"""
|
||||
Helper class for when you want to use strings as keys of an enum. The values would be
|
||||
user facing strings that might undergo translation.
|
||||
|
||||
The constructor accepts an even amount of arguments. Each pair of arguments
|
||||
is a key/value pair.
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
self.keys = []
|
||||
self.values = []
|
||||
|
||||
for i in range(len(args)//2):
|
||||
self.keys.append(args[i*2])
|
||||
self.values.append(args[i*2+1])
|
||||
|
||||
def get_tree_model(self):
|
||||
""" Get a tree model that can be used in GTK widgets. """
|
||||
tree_model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
|
||||
for i in range(len(self.keys)):
|
||||
tree_model.append([self.keys[i], self.values[i]])
|
||||
return tree_model
|
||||
|
||||
def __getattr__(self, name):
|
||||
""" Implements access to the key. For example, if you provided a key "red", then you could access it by
|
||||
referring to
|
||||
my_enum.red
|
||||
It may seem silly as "my_enum.red" is longer to write then just "red",
|
||||
but this provides verification that the key is indeed inside enum. """
|
||||
key = name.replace("_", " ")
|
||||
if key in self.keys:
|
||||
return key
|
||||
raise AttributeError("No such key string " + key)
|
||||
|
||||
|
||||
output_format_enum = StringEnum(
|
||||
"pixel count", _("Pixel count"),
|
||||
"normalized", _("Normalized"),
|
||||
"percent", _("Percent")
|
||||
)
|
||||
|
||||
|
||||
def histogram_export(procedure, img, layers, gio_file,
|
||||
bucket_size, sample_average, output_format,
|
||||
progress_bar):
|
||||
layers = img.get_selected_layers()
|
||||
layer = layers[0]
|
||||
if sample_average:
|
||||
new_img = img.duplicate()
|
||||
layer = new_img.merge_visible_layers(Pika.MergeType.CLIP_TO_IMAGE)
|
||||
|
||||
channels_txt = ["Value"]
|
||||
channels_pika = [Pika.HistogramChannel.VALUE]
|
||||
if layer.is_rgb():
|
||||
channels_txt += ["Red", "Green", "Blue", "Luminance"]
|
||||
channels_pika += [Pika.HistogramChannel.RED, Pika.HistogramChannel.GREEN, Pika.HistogramChannel.BLUE,
|
||||
Pika.HistogramChannel.LUMINANCE]
|
||||
if layer.has_alpha():
|
||||
channels_txt += ["Alpha"]
|
||||
channels_pika += [Pika.HistogramChannel.ALPHA]
|
||||
|
||||
try:
|
||||
with open(gio_file.get_path(), "wt") as hfile:
|
||||
writer = csv.writer(hfile)
|
||||
|
||||
# Write headers:
|
||||
writer.writerow(["Range start"] + channels_txt)
|
||||
|
||||
max_index = 1.0/bucket_size if bucket_size > 0 else 1
|
||||
i = 0
|
||||
progress_bar_int_percent = 0
|
||||
while True:
|
||||
start_range = i * bucket_size
|
||||
i += 1
|
||||
if start_range >= 1.0:
|
||||
break
|
||||
|
||||
row = [start_range]
|
||||
for channel in channels_pika:
|
||||
result = Pika.get_pdb().run_procedure('pika-drawable-histogram',
|
||||
[ GObject.Value(Pika.Drawable, layer),
|
||||
GObject.Value(Pika.HistogramChannel, channel),
|
||||
GObject.Value(GObject.TYPE_DOUBLE,
|
||||
float(start_range)),
|
||||
GObject.Value(GObject.TYPE_DOUBLE,
|
||||
float(min(start_range + bucket_size, 1.0))) ])
|
||||
|
||||
if output_format == output_format_enum.pixel_count:
|
||||
count = int(result.index(5))
|
||||
else:
|
||||
pixels = result.index(4)
|
||||
count = (result.index(5) / pixels) if pixels else 0
|
||||
if output_format == output_format_enum.percent:
|
||||
count = "%.2f%%" % (count * 100)
|
||||
row.append(str(count))
|
||||
writer.writerow(row)
|
||||
|
||||
# Update progress bar
|
||||
if progress_bar:
|
||||
fraction = i / max_index
|
||||
# Only update the progress bar if it changed at least 1% .
|
||||
new_percent = math.floor(fraction * 100)
|
||||
if new_percent != progress_bar_int_percent:
|
||||
progress_bar_int_percent = new_percent
|
||||
progress_bar.set_fraction(fraction)
|
||||
# Make sure the progress bar gets drawn on screen.
|
||||
while Gtk.events_pending():
|
||||
Gtk.main_iteration()
|
||||
except IsADirectoryError:
|
||||
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
|
||||
GLib.Error(_("File is either a directory or file name is empty.")))
|
||||
except FileNotFoundError:
|
||||
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
|
||||
GLib.Error(_("Directory not found.")))
|
||||
except PermissionError:
|
||||
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
|
||||
GLib.Error("You do not have permissions to write that file."))
|
||||
|
||||
if sample_average:
|
||||
new_img.delete()
|
||||
|
||||
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
|
||||
|
||||
def run(procedure, run_mode, image, n_layers, layers, args, data):
|
||||
gio_file = args.index(0)
|
||||
bucket_size = args.index(1)
|
||||
sample_average = args.index(2)
|
||||
output_format = args.index(3)
|
||||
|
||||
progress_bar = None
|
||||
config = None
|
||||
|
||||
if run_mode == Pika.RunMode.INTERACTIVE:
|
||||
|
||||
config = procedure.create_config()
|
||||
|
||||
# Set properties from arguments. These properties will be changed by the UI.
|
||||
#config.set_property("file", gio_file)
|
||||
#config.set_property("bucket_size", bucket_size)
|
||||
#config.set_property("sample_average", sample_average)
|
||||
#config.set_property("output_format", output_format)
|
||||
config.begin_run(image, run_mode, args)
|
||||
|
||||
PikaUi.init("histogram-export.py")
|
||||
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
|
||||
dialog = PikaUi.Dialog(use_header_bar=use_header_bar,
|
||||
title=_("Histogram Export..."))
|
||||
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
|
||||
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
|
||||
homogeneous=False, spacing=10)
|
||||
dialog.get_content_area().add(vbox)
|
||||
vbox.show()
|
||||
|
||||
# Create grid to set all the properties inside.
|
||||
grid = Gtk.Grid()
|
||||
grid.set_column_homogeneous(False)
|
||||
grid.set_border_width(10)
|
||||
grid.set_column_spacing(10)
|
||||
grid.set_row_spacing(10)
|
||||
vbox.add(grid)
|
||||
grid.show()
|
||||
|
||||
# UI for the file parameter
|
||||
|
||||
def choose_file(widget):
|
||||
if file_chooser_dialog.run() == Gtk.ResponseType.OK:
|
||||
if file_chooser_dialog.get_file() is not None:
|
||||
config.set_property("file", file_chooser_dialog.get_file())
|
||||
file_entry.set_text(file_chooser_dialog.get_file().get_path())
|
||||
file_chooser_dialog.hide()
|
||||
|
||||
file_chooser_button = Gtk.Button.new_with_mnemonic(label=_("_File..."))
|
||||
grid.attach(file_chooser_button, 0, 0, 1, 1)
|
||||
file_chooser_button.show()
|
||||
file_chooser_button.connect("clicked", choose_file)
|
||||
|
||||
file_entry = Gtk.Entry.new()
|
||||
grid.attach(file_entry, 1, 0, 1, 1)
|
||||
file_entry.set_width_chars(40)
|
||||
file_entry.set_placeholder_text(_("Choose export file..."))
|
||||
if gio_file is not None:
|
||||
file_entry.set_text(gio_file.get_path())
|
||||
file_entry.show()
|
||||
|
||||
file_chooser_dialog = Gtk.FileChooserDialog(use_header_bar=use_header_bar,
|
||||
title=_("Histogram Export file..."),
|
||||
action=Gtk.FileChooserAction.SAVE)
|
||||
file_chooser_dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
file_chooser_dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
|
||||
|
||||
# Bucket size parameter
|
||||
label = Gtk.Label.new_with_mnemonic(_("_Bucket Size"))
|
||||
grid.attach(label, 0, 1, 1, 1)
|
||||
label.show()
|
||||
spin = PikaUi.prop_spin_button_new(config, "bucket_size", step_increment=0.001, page_increment=0.1, digits=3)
|
||||
grid.attach(spin, 1, 1, 1, 1)
|
||||
spin.show()
|
||||
|
||||
# Sample average parameter
|
||||
spin = PikaUi.prop_check_button_new(config, "sample_average", _("Sample _Average"))
|
||||
spin.set_tooltip_text(_("If checked, the histogram is generated from merging all visible layers."
|
||||
" Otherwise, the histogram is only for the current layer."))
|
||||
grid.attach(spin, 1, 2, 1, 1)
|
||||
spin.show()
|
||||
|
||||
# Output format parameter
|
||||
label = Gtk.Label.new_with_mnemonic(_("_Output Format"))
|
||||
grid.attach(label, 0, 3, 1, 1)
|
||||
label.show()
|
||||
combo = PikaUi.prop_string_combo_box_new(config, "output_format", output_format_enum.get_tree_model(), 0, 1)
|
||||
grid.attach(combo, 1, 3, 1, 1)
|
||||
combo.show()
|
||||
|
||||
progress_bar = Gtk.ProgressBar()
|
||||
vbox.add(progress_bar)
|
||||
progress_bar.show()
|
||||
|
||||
dialog.show()
|
||||
if dialog.run() != Gtk.ResponseType.OK:
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL,
|
||||
GLib.Error())
|
||||
|
||||
# Extract values from UI
|
||||
gio_file = Gio.file_new_for_path(file_entry.get_text()) # config.get_property("file")
|
||||
bucket_size = config.get_property("bucket_size")
|
||||
sample_average = config.get_property("sample_average")
|
||||
output_format = config.get_property("output_format")
|
||||
|
||||
if gio_file is None:
|
||||
error = 'No file given'
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
|
||||
result = histogram_export(procedure, image, layers, gio_file,
|
||||
bucket_size, sample_average, output_format, progress_bar)
|
||||
|
||||
# If the execution was successful, save parameters so they will be restored next time we show dialog.
|
||||
if result.index(0) == Pika.PDBStatusType.SUCCESS and config is not None:
|
||||
config.end_run(Pika.PDBStatusType.SUCCESS)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class HistogramExport(Pika.PlugIn):
|
||||
|
||||
## Parameters ##
|
||||
__gproperties__ = {
|
||||
# "filename": (str,
|
||||
# # TODO: I wanted this property to be a path (and not just str) , so I could use
|
||||
# # prop_file_chooser_button_new to open a file dialog. However, it fails without an error message.
|
||||
# # Pika.ConfigPath,
|
||||
# _("Histogram _File"),
|
||||
# _("Histogram _File"),
|
||||
# "histogram_export.csv",
|
||||
# # Pika.ConfigPathType.FILE,
|
||||
# GObject.ParamFlags.READWRITE),
|
||||
"file": (Gio.File,
|
||||
_("Histogram _File"),
|
||||
"Histogram export file",
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"bucket_size": (float,
|
||||
_("_Bucket Size"),
|
||||
"Bucket Size",
|
||||
0.001, 1.0, 0.01,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"sample_average": (bool,
|
||||
_("Sample _Average"),
|
||||
"Sample Average",
|
||||
False,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"output_format": (str,
|
||||
_("Output format"),
|
||||
"Output format: 'pixel count', 'normalized', 'percent'",
|
||||
"pixel count",
|
||||
GObject.ParamFlags.READWRITE),
|
||||
}
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return ['histogram-export']
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = None
|
||||
if name == 'histogram-export':
|
||||
procedure = Pika.ImageProcedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
run, None)
|
||||
|
||||
procedure.set_image_types("*")
|
||||
procedure.set_documentation (
|
||||
_("Exports the image histogram to a text file (CSV)"),
|
||||
globals()["__doc__"], # This includes the docstring, on the top of the file
|
||||
name)
|
||||
procedure.set_menu_label(_("_Export histogram..."))
|
||||
procedure.set_attribution("João S. O. Bueno",
|
||||
"(c) GPL V3.0 or later",
|
||||
"2014")
|
||||
procedure.add_menu_path("<Image>/Colors/Info/")
|
||||
|
||||
procedure.add_argument_from_property(self, "file")
|
||||
procedure.add_argument_from_property(self, "bucket_size")
|
||||
procedure.add_argument_from_property(self, "sample_average")
|
||||
procedure.add_argument_from_property(self, "output_format")
|
||||
|
||||
return procedure
|
||||
|
||||
|
||||
Pika.main(HistogramExport.__gtype__, sys.argv)
|
53
plug-ins/python/meson.build
Normal file
53
plug-ins/python/meson.build
Normal file
@ -0,0 +1,53 @@
|
||||
if not have_python
|
||||
subdir_done()
|
||||
endif
|
||||
|
||||
plugins = [
|
||||
{ 'name': 'colorxhtml' },
|
||||
{ 'name': 'file-openraster' },
|
||||
{ 'name': 'foggify' },
|
||||
{ 'name': 'gradients-save-as-css' },
|
||||
{ 'name': 'histogram-export' },
|
||||
{ 'name': 'palette-offset' },
|
||||
{ 'name': 'palette-sort' },
|
||||
{ 'name': 'palette-to-gradient' },
|
||||
{ 'name': 'python-eval' },
|
||||
{ 'name': 'spyro-plus' },
|
||||
]
|
||||
|
||||
if not stable or not release
|
||||
plugins += [
|
||||
{ 'name': 'test-dialog' },
|
||||
]
|
||||
endif
|
||||
|
||||
subdir('python-console')
|
||||
|
||||
foreach plugin : plugins
|
||||
name = plugin.get('name')
|
||||
srcs = plugin.get('srcs', [name + '.py'])
|
||||
|
||||
install_data(srcs, install_dir: pikaplugindir / 'plug-ins' / name,
|
||||
install_mode: 'rwxr-xr-x')
|
||||
|
||||
foreach src : srcs
|
||||
# Ugly trick to copy Python plug-ins into subfolders so that we can run PIKA
|
||||
# from the build directory without installing it.
|
||||
run_command(python, meson.project_source_root() / '.gitlab/cp-plug-in-subfolder.py',
|
||||
meson.current_source_dir() / src, meson.current_build_dir() / name, check: true)
|
||||
endforeach
|
||||
endforeach
|
||||
|
||||
|
||||
if python.found()
|
||||
python_config = configuration_data()
|
||||
python_config.set('PYTHON_PATH', python.full_path())
|
||||
|
||||
configure_file(
|
||||
input : 'pypika.interp.in',
|
||||
output: 'pypika.interp',
|
||||
configuration: python_config,
|
||||
install: true,
|
||||
install_dir: pikaplugindir / 'interpreters',
|
||||
)
|
||||
endif
|
195
plug-ins/python/palette-offset.py
Normal file
195
plug-ins/python/palette-offset.py
Normal file
@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
import sys
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
help_doc = r"""
|
||||
Offset the colors in the palette.
|
||||
Offsets and returns the given palette when it is editable,
|
||||
otherwise copies the given palette and returns it.
|
||||
"""
|
||||
|
||||
class PaletteOffset (Pika.PlugIn):
|
||||
## Parameter: run-mode ##
|
||||
@GObject.Property(type=Pika.RunMode,
|
||||
default=Pika.RunMode.NONINTERACTIVE,
|
||||
nick="Run mode", blurb="The run mode")
|
||||
def run_mode(self):
|
||||
"""Read-write integer property."""
|
||||
return self._run_mode
|
||||
|
||||
@run_mode.setter
|
||||
def run_mode(self, run_mode):
|
||||
self._run_mode = run_mode
|
||||
|
||||
## Parameter: palette ##
|
||||
@GObject.Property(type=Pika.Palette,
|
||||
nick= _("Palette"),
|
||||
blurb= _("Palette"))
|
||||
def palette(self):
|
||||
return self._palette
|
||||
|
||||
@palette.setter
|
||||
def palette(self, palette):
|
||||
self._palette = palette
|
||||
|
||||
## Parameter: amount ##
|
||||
@GObject.Property(type=int,
|
||||
default=1,
|
||||
nick= _("Off_set"),
|
||||
blurb= _("Offset"))
|
||||
def amount(self):
|
||||
return self._amount
|
||||
|
||||
@amount.setter
|
||||
def amount(self, amount):
|
||||
self._amount = amount
|
||||
|
||||
## Return: new-palette ##
|
||||
@GObject.Property(type=Pika.Palette,
|
||||
nick=_("The edited palette"),
|
||||
blurb=_("The newly created palette when read-only, otherwise the input palette"))
|
||||
def new_palette(self):
|
||||
return self.new_palette
|
||||
|
||||
@new_palette.setter
|
||||
def new_palette(self, new_palette):
|
||||
self.new_palette = new_palette
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ "python-fu-palette-offset" ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = Pika.Procedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
self.run, None)
|
||||
if name == 'python-fu-palette-offset':
|
||||
procedure.set_menu_label(_("_Offset Palette..."))
|
||||
procedure.set_documentation(_("Offset the colors in a palette"),
|
||||
help_doc,
|
||||
"")
|
||||
procedure.set_attribution("Joao S. O. Bueno Calligaris, Carol Spears",
|
||||
"(c) Joao S. O. Bueno Calligaris",
|
||||
"2004, 2006")
|
||||
procedure.add_argument_from_property(self, "run-mode")
|
||||
procedure.add_argument_from_property(self, "palette")
|
||||
procedure.add_argument_from_property(self, "amount")
|
||||
procedure.add_return_value_from_property(self, "new-palette")
|
||||
procedure.add_menu_path ('<Palettes>')
|
||||
else:
|
||||
procedure = None
|
||||
|
||||
return procedure
|
||||
|
||||
def run(self, procedure, args, data):
|
||||
palette = None
|
||||
amount = 1
|
||||
|
||||
# Get the parameters
|
||||
if args.length() < 1:
|
||||
error = 'No parameters given'
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
runmode = args.index(0)
|
||||
|
||||
if args.length() > 1:
|
||||
palette = args.index(1)
|
||||
if palette is None:
|
||||
palette = Pika.context_get_palette()
|
||||
|
||||
if not palette.is_valid():
|
||||
error = f'Invalid palette ID: {palette.get_id()}'
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
|
||||
if args.length() > 2:
|
||||
amount = args.index(2)
|
||||
|
||||
if runmode == Pika.RunMode.INTERACTIVE:
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
PikaUi.init ("palette-offset.py")
|
||||
|
||||
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
|
||||
dialog = PikaUi.Dialog(use_header_bar=use_header_bar,
|
||||
title=_("Offset Palette..."))
|
||||
|
||||
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
dialog.add_button(_("_OK"), Gtk.ResponseType.OK)
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
|
||||
homogeneous=False, spacing=12)
|
||||
dialog.get_content_area().add(box)
|
||||
box.show()
|
||||
|
||||
label = Gtk.Label.new(_("Offset"))
|
||||
box.pack_start(label, False, False, 1)
|
||||
label.show()
|
||||
|
||||
amount = self.set_property("amount", amount)
|
||||
spin = PikaUi.prop_spin_button_new(self, "amount", 1.0, 5.0, 0)
|
||||
spin.set_activates_default(True)
|
||||
box.pack_end(spin, False, False, 1)
|
||||
spin.show()
|
||||
|
||||
dialog.show()
|
||||
if dialog.run() != Gtk.ResponseType.OK:
|
||||
print ("Canceled")
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL,
|
||||
GLib.Error("Canceled"))
|
||||
amount = self.get_property("amount")
|
||||
|
||||
#If palette is read only, work on a copy:
|
||||
editable = palette.is_editable()
|
||||
if not editable:
|
||||
palette = palette.duplicate()
|
||||
|
||||
num_colors = palette.get_color_count()
|
||||
tmp_entry_array = []
|
||||
for i in range (num_colors):
|
||||
tmp_entry_array.append ((palette.entry_get_name(i)[1],
|
||||
palette.entry_get_color(i)[1]))
|
||||
for i in range (num_colors):
|
||||
target_index = i + amount
|
||||
if target_index >= num_colors:
|
||||
target_index -= num_colors
|
||||
elif target_index < 0:
|
||||
target_index += num_colors
|
||||
palette.entry_set_name(target_index, tmp_entry_array[i][0])
|
||||
palette.entry_set_color(target_index, tmp_entry_array[i][1])
|
||||
|
||||
retval = procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
value = GObject.Value(Pika.Palette, palette)
|
||||
retval.remove(1)
|
||||
retval.insert(1, value)
|
||||
return retval
|
||||
|
||||
Pika.main(PaletteOffset.__gtype__, sys.argv)
|
524
plug-ins/python/palette-sort.py
Normal file
524
plug-ins/python/palette-sort.py
Normal file
@ -0,0 +1,524 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# For a detailed explanation of the parameters of this plugin, see :
|
||||
# https://gitlab.gnome.org/GNOME/pika/-/issues/4368#note_763460
|
||||
|
||||
# little known, colorsys is part of Python's stdlib
|
||||
from colorsys import rgb_to_yiq
|
||||
from textwrap import dedent
|
||||
from random import randint
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
import sys
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
|
||||
AVAILABLE_CHANNELS = (_("Red"), _("Green"), _("Blue"),
|
||||
_("Luma (Y)"),
|
||||
_("Hue"), _("Saturation"), _("Value"),
|
||||
_("Saturation (HSL)"), _("Lightness (HSL)"),
|
||||
_("Index"),
|
||||
_("Random"))
|
||||
|
||||
channel_getters = [
|
||||
(lambda v, i: v.r),
|
||||
(lambda v, i: v.g),
|
||||
(lambda v, i: v.r),
|
||||
|
||||
(lambda v, i: rgb_to_yiq(v.r, v.g, v.b)[0]),
|
||||
|
||||
(lambda v, i: v.to_hsv().h),
|
||||
(lambda v, i: v.to_hsv().s),
|
||||
(lambda v, i: v.to_hsv().v),
|
||||
|
||||
(lambda v, i: v.to_hsl().s),
|
||||
(lambda v, i: v.to_hsl().l),
|
||||
|
||||
(lambda v, i: i),
|
||||
(lambda v, i: randint(0, 0x7fffffff))
|
||||
]
|
||||
|
||||
|
||||
GRAIN_SCALE = (1.0, 1.0 , 1.0,
|
||||
1.0,
|
||||
360., 100., 100.,
|
||||
100., 100.,
|
||||
16384.,
|
||||
float(0x7ffffff),
|
||||
100., 256., 256.,
|
||||
256., 360.,)
|
||||
|
||||
SELECT_ALL = 0
|
||||
SELECT_SLICE = 1
|
||||
SELECT_AUTOSLICE = 2
|
||||
SELECT_PARTITIONED = 3
|
||||
SELECTIONS = (SELECT_ALL, SELECT_SLICE, SELECT_AUTOSLICE, SELECT_PARTITIONED)
|
||||
|
||||
try:
|
||||
from colormath.color_objects import RGBColor, LabColor, LCHabColor
|
||||
|
||||
def to_lab(v):
|
||||
return RGBColor(v.r, v.g, v.b).convert_to('LAB').get_value_tuple()
|
||||
|
||||
def to_lchab(v):
|
||||
return RGBColor(v.r, v.g, v.b).convert_to('LCHab').get_value_tuple()
|
||||
|
||||
AVAILABLE_CHANNELS = AVAILABLE_CHANNELS + (_("Lightness (LAB)"),
|
||||
_("A-color"), _("B-color"),
|
||||
_("Chroma (LCHab)"),
|
||||
_("Hue (LCHab)"))
|
||||
channel_getters.extend([
|
||||
(lambda v, i: to_lab(v)[0]),
|
||||
(lambda v, i: to_lab(v)[1]),
|
||||
(lambda v, i: to_lab(v)[2]),
|
||||
|
||||
(lambda v, i: to_lchab(v)[1]),
|
||||
(lambda v, i: to_lchab(v)[2])
|
||||
])
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
slice_expr_doc = """
|
||||
Format is 'start:nrows,length' . All items are optional.
|
||||
|
||||
The empty string selects all items, as does ':'
|
||||
':4,' makes a 4-row selection out of all colors (length auto-determined)
|
||||
':4' also.
|
||||
':1,4' selects the first 4 colors
|
||||
':,4' selects rows of 4 colors (nrows auto-determined)
|
||||
':3,4' selects 3 rows of 4 colors
|
||||
'4:' selects a single row of all colors after 4, inclusive.
|
||||
'3:,4' selects rows of 4 colors, starting at 3 (nrows auto-determined)
|
||||
'2:3,4' selects 3 rows of 4 colors (12 colors total), beginning at index 2.
|
||||
'4' is illegal (ambiguous)
|
||||
"""
|
||||
|
||||
|
||||
def parse_slice(s, numcolors):
|
||||
"""Parse a slice spec and return (start, nrows, length)
|
||||
All items are optional. Omitting them makes the largest possible selection that
|
||||
exactly fits the other items.
|
||||
|
||||
In general, slices are comparable to a numpy sub-array.
|
||||
'start at element START, with shape (NROWS, LENGTH)'
|
||||
|
||||
"""
|
||||
s = s.strip()
|
||||
|
||||
def notunderstood():
|
||||
raise ValueError('Slice %r not understood. Should be in format'
|
||||
' START?:NROWS?,ROWLENGTH? eg. "0:4,16".' % s)
|
||||
def _int(v):
|
||||
try:
|
||||
return int(v)
|
||||
except ValueError:
|
||||
notunderstood()
|
||||
if s in ('', ':', ':,'):
|
||||
return 0, 1, numcolors # entire palette, one row
|
||||
if s.count(':') != 1:
|
||||
notunderstood()
|
||||
rowpos = s.find(':')
|
||||
start = 0
|
||||
if rowpos > 0:
|
||||
start = _int(s[:rowpos])
|
||||
numcolors -= start
|
||||
nrows = 1
|
||||
if ',' in s:
|
||||
commapos = s.find(',')
|
||||
nrows = s[rowpos+1:commapos]
|
||||
length = s[commapos+1:]
|
||||
if not nrows:
|
||||
if not length:
|
||||
notunderstood()
|
||||
else:
|
||||
length = _int(length)
|
||||
if length == 0:
|
||||
notunderstood()
|
||||
nrows = numcolors // length
|
||||
if numcolors % length:
|
||||
nrows = -nrows
|
||||
elif not length:
|
||||
nrows = _int(nrows)
|
||||
if nrows == 0:
|
||||
notunderstood()
|
||||
length = numcolors // nrows
|
||||
if numcolors % nrows:
|
||||
length = -length
|
||||
else:
|
||||
nrows = _int(nrows)
|
||||
if nrows == 0:
|
||||
notunderstood()
|
||||
length = _int(length)
|
||||
if length == 0:
|
||||
notunderstood()
|
||||
else:
|
||||
nrows = _int(s[rowpos+1:])
|
||||
if nrows == 0:
|
||||
notunderstood()
|
||||
length = numcolors // nrows
|
||||
if numcolors % nrows:
|
||||
length = -length
|
||||
return start, nrows, length
|
||||
|
||||
|
||||
def quantization_grain(channel, g):
|
||||
"Given a channel and a quantization, return the size of a quantization grain"
|
||||
g = max(1.0, g)
|
||||
if g <= 1.0:
|
||||
g = 0.00001
|
||||
else:
|
||||
g = max(0.00001, GRAIN_SCALE[channel] / g)
|
||||
return g
|
||||
|
||||
|
||||
def palette_sort(palette, selection, slice_expr, channel1, ascending1,
|
||||
channel2, ascending2, quantize, pchannel, pquantize):
|
||||
|
||||
grain1 = quantization_grain(channel1, quantize)
|
||||
grain2 = quantization_grain(channel2, quantize)
|
||||
pgrain = quantization_grain(pchannel, pquantize)
|
||||
|
||||
# If palette is read only, work on a copy:
|
||||
editable = palette.is_editable()
|
||||
if not editable:
|
||||
palette = palette.duplicate()
|
||||
|
||||
num_colors = palette.get_color_count()
|
||||
|
||||
start, nrows, length = None, None, None
|
||||
if selection == SELECT_AUTOSLICE:
|
||||
def find_index(color, startindex=0):
|
||||
for i in range(startindex, num_colors):
|
||||
c = palette.entry_get_color(i)
|
||||
if c[1].r == color[1].r and c[1].g == color[1].g and c[1].b == color[1].b:
|
||||
return i
|
||||
return None
|
||||
def hexcolor(c):
|
||||
return "#%02x%02x%02x" % (int(255 * c[1].r), int(255 * c[1].b), int(255 * c[1].g))
|
||||
fg = Pika.context_get_foreground()
|
||||
bg = Pika.context_get_background()
|
||||
start = find_index(fg)
|
||||
end = find_index(bg)
|
||||
if start is None:
|
||||
raise ValueError("Couldn't find foreground color %s in palette" % hexcolor(fg))
|
||||
if end is None:
|
||||
raise ValueError("Couldn't find background color %s in palette" % hexcolor(bg))
|
||||
if find_index(fg, start + 1):
|
||||
raise ValueError('Autoslice cannot be used when more than one'
|
||||
' instance of an endpoint'
|
||||
' (%s) is present' % hexcolor(fg))
|
||||
if find_index(bg, end + 1):
|
||||
raise ValueError('Autoslice cannot be used when more than one'
|
||||
' instance of an endpoint'
|
||||
' (%s) is present' % hexcolor(bg))
|
||||
if start > end:
|
||||
end, start = start, end
|
||||
length = (end - start) + 1
|
||||
try:
|
||||
_, nrows, _ = parse_slice(slice_expr, length)
|
||||
nrows = abs(nrows)
|
||||
if length % nrows:
|
||||
raise ValueError('Total length %d not evenly divisible'
|
||||
' by number of rows %d' % (length, nrows))
|
||||
length //= nrows
|
||||
except ValueError:
|
||||
# bad expression is okay here, just assume one row
|
||||
nrows = 1
|
||||
# remaining behavior is implemented by SELECT_SLICE 'inheritance'.
|
||||
selection = SELECT_SLICE
|
||||
elif selection in (SELECT_SLICE, SELECT_PARTITIONED):
|
||||
start, nrows, length = parse_slice(slice_expr, num_colors)
|
||||
|
||||
channels_getter_1 = channel_getters[channel1]
|
||||
channels_getter_2 = channel_getters[channel2]
|
||||
|
||||
def get_colors(start, end):
|
||||
result = []
|
||||
for i in range(start, end):
|
||||
entry = (palette.entry_get_name(i)[1],
|
||||
palette.entry_get_color(i)[1])
|
||||
index1 = channels_getter_1(entry[1], i)
|
||||
index2 = channels_getter_2(entry[1], i)
|
||||
index = ((index1 - (index1 % grain1)) * (1 if ascending1 else -1),
|
||||
(index2 - (index2 % grain2)) * (1 if ascending2 else -1)
|
||||
)
|
||||
result.append((index, entry))
|
||||
return result
|
||||
|
||||
if selection == SELECT_ALL:
|
||||
entry_list = get_colors(0, num_colors)
|
||||
entry_list.sort(key=lambda v: v[0])
|
||||
for i in range(num_colors):
|
||||
palette.entry_set_name(i, entry_list[i][1][0])
|
||||
palette.entry_set_color(i, entry_list[i][1][1])
|
||||
|
||||
elif selection == SELECT_PARTITIONED:
|
||||
if num_colors < (start + length * nrows) - 1:
|
||||
raise ValueError('Not enough entries in palette to '
|
||||
'sort complete rows! Got %d, expected >=%d' %
|
||||
(num_colors, start + length * nrows))
|
||||
pchannels_getter = channel_getters[pchannel]
|
||||
for row in range(nrows):
|
||||
partition_spans = [1]
|
||||
rowstart = start + (row * length)
|
||||
old_color = palette.entry_get_color(rowstart)[1]
|
||||
old_partition = pchannels_getter(old_color, rowstart)
|
||||
old_partition = old_partition - (old_partition % pgrain)
|
||||
for i in range(rowstart + 1, rowstart + length):
|
||||
this_color = palette.entry_get_color(i)[1]
|
||||
this_partition = pchannels_getter(this_color, i)
|
||||
this_partition = this_partition - (this_partition % pgrain)
|
||||
if this_partition == old_partition:
|
||||
partition_spans[-1] += 1
|
||||
else:
|
||||
partition_spans.append(1)
|
||||
old_partition = this_partition
|
||||
base = rowstart
|
||||
for size in partition_spans:
|
||||
palette_sort(SELECT_SLICE, '%d:1,%d' % (base, size),
|
||||
channel1, ascending1,
|
||||
channel2, ascending2,
|
||||
quantize, 0, 1.0)
|
||||
base += size
|
||||
else:
|
||||
# SELECT_SLICE and SELECT_AUTOSLICE
|
||||
stride = length
|
||||
if num_colors < (start + stride * nrows) - 1:
|
||||
raise ValueError('Not enough entries in palette to sort '
|
||||
'complete rows! Got %d, expected >=%d' %
|
||||
(num_colors, start + stride * nrows))
|
||||
|
||||
for row_start in range(start, start + stride * nrows, stride):
|
||||
sublist = get_colors(row_start, row_start + stride)
|
||||
sublist.sort(key=lambda v: v[0])
|
||||
for i, entry in zip(range(row_start, row_start + stride), sublist):
|
||||
palette.entry_set_name(i, entry[1][0])
|
||||
palette.entry_set_color(i, entry[1][1])
|
||||
|
||||
return palette
|
||||
|
||||
# FIXME: Write humanly readable help -
|
||||
# See for reference: https://gitlab.gnome.org/GNOME/pika/-/issues/4368#note_763460
|
||||
|
||||
# Important to describe the general effect on palettes rather than details of the sort.
|
||||
help_doc = r"""
|
||||
Sorts a palette, or part of a palette.
|
||||
Sorts the given palette when it is editable, otherwise creates a new sorted palette.
|
||||
The default is a 1D sort, but you can also sort over two color channels
|
||||
or create a 2D sorted palette with sorted rows.
|
||||
You can optionally install colormath (https://pypi.python.org/pypi/colormath/1.0.8)
|
||||
to PIKA's Python to get even more channels to choose from.
|
||||
"""
|
||||
|
||||
selections_option = [ _("All"), _("Slice / Array"), _("Autoslice (fg->bg)"), _("Partitioned") ]
|
||||
class PaletteSort (Pika.PlugIn):
|
||||
## Parameters ##
|
||||
__gproperties__ = {
|
||||
"run-mode": (Pika.RunMode,
|
||||
_("Run mode"),
|
||||
"The run mode",
|
||||
Pika.RunMode.INTERACTIVE,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"palette": (Pika.Palette,
|
||||
_("_Palette"),
|
||||
_("Palette"),
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"selections": (int,
|
||||
_("Select_ions"),
|
||||
str(selections_option),
|
||||
0, 3, 0,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
# TODO: It would be much simpler to replace the slice expression with three
|
||||
# separate parameters: start-index, number-of-rows, row_length
|
||||
"slice_expr": (str,
|
||||
_("Slice _expression"),
|
||||
slice_expr_doc,
|
||||
"",
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"channel1": (int,
|
||||
_("Channel _to sort"),
|
||||
"Channel to sort: " + str(AVAILABLE_CHANNELS),
|
||||
0, len(AVAILABLE_CHANNELS), 3,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"ascending1": (bool,
|
||||
_("_Ascending"),
|
||||
_("Ascending"),
|
||||
True,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"channel2": (int,
|
||||
_("Secondary C_hannel to sort"),
|
||||
"Secondary Channel to sort: " + str(AVAILABLE_CHANNELS),
|
||||
0, len(AVAILABLE_CHANNELS), 5,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"ascending2": (bool,
|
||||
_("Ascen_ding"),
|
||||
_("Ascending"),
|
||||
True,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"quantize": (float,
|
||||
_("_Quantization"),
|
||||
_("Quantization"),
|
||||
0.0, 1.0, 0.0,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"pchannel": (int,
|
||||
_("_Partitioning channel"),
|
||||
"Partitioning channel: " + str(AVAILABLE_CHANNELS),
|
||||
0, len(AVAILABLE_CHANNELS), 3,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
"pquantize": (float,
|
||||
_("Partition q_uantization"),
|
||||
_("Partition quantization"),
|
||||
0.0, 1.0, 0.0,
|
||||
GObject.ParamFlags.READWRITE),
|
||||
|
||||
# Returned value
|
||||
"new_palette": (Pika.Palette,
|
||||
_("Palette"),
|
||||
_("Palette"),
|
||||
GObject.ParamFlags.READWRITE),
|
||||
}
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return ["python-fu-palette-sort"]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = None
|
||||
if name == "python-fu-palette-sort":
|
||||
procedure = Pika.Procedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
self.run, None)
|
||||
procedure.set_menu_label(_("_Sort Palette..."))
|
||||
procedure.set_documentation(
|
||||
_("Sort the colors in a palette"),
|
||||
help_doc,
|
||||
""
|
||||
)
|
||||
procedure.set_attribution("João S. O. Bueno, Carol Spears, David Gowers",
|
||||
"João S. O. Bueno, Carol Spears, David Gowers",
|
||||
"2006-2014")
|
||||
procedure.add_menu_path ('<Palettes>')
|
||||
|
||||
procedure.add_argument_from_property(self, "run-mode")
|
||||
procedure.add_argument_from_property(self, "palette")
|
||||
procedure.add_argument_from_property(self, "selections")
|
||||
procedure.add_argument_from_property(self, "slice_expr")
|
||||
procedure.add_argument_from_property(self, "channel1")
|
||||
procedure.add_argument_from_property(self, "ascending1")
|
||||
procedure.add_argument_from_property(self, "channel2")
|
||||
procedure.add_argument_from_property(self, "ascending2")
|
||||
procedure.add_argument_from_property(self, "quantize")
|
||||
procedure.add_argument_from_property(self, "pchannel")
|
||||
procedure.add_argument_from_property(self, "pquantize")
|
||||
procedure.add_return_value_from_property(self, "new_palette")
|
||||
|
||||
return procedure
|
||||
|
||||
def run(self, procedure, args, data):
|
||||
config = procedure.create_config()
|
||||
config.begin_run(None, Pika.RunMode.INTERACTIVE, args)
|
||||
|
||||
run_mode = args.index(0)
|
||||
palette = args.index(1)
|
||||
selection = args.index(2)
|
||||
slice_expr = args.index(3)
|
||||
channel1 = args.index(4)
|
||||
ascending1 = args.index(5)
|
||||
channel2 = args.index(6)
|
||||
ascending2 = args.index(7)
|
||||
quantize = args.index(8)
|
||||
pchannel = args.index(9)
|
||||
pquantize = args.index(10)
|
||||
|
||||
if palette is None or not palette.is_valid():
|
||||
if palette is not None:
|
||||
sys.stderr.write(f'Invalid palette id: {palette.get_id()}\n')
|
||||
sys.stderr.write('This should not happen. Please report to PIKA project.\n')
|
||||
sys.stderr.write('Falling back to context palette instead.\n')
|
||||
palette = Pika.context_get_palette()
|
||||
|
||||
if not palette.is_valid():
|
||||
palette_name = palette.get_id()
|
||||
error = f'Invalid palette id: {palette_name}'
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CALLING_ERROR,
|
||||
GLib.Error(error))
|
||||
|
||||
if run_mode == Pika.RunMode.INTERACTIVE:
|
||||
PikaUi.init('python-fu-palette-sort')
|
||||
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
|
||||
|
||||
dialog.fill (["palette"])
|
||||
dialog.get_int_combo("selections", PikaUi.IntStore.new (selections_option))
|
||||
dialog.fill (["selections","slice-expr"])
|
||||
dialog.get_int_combo("channel1", PikaUi.IntStore.new (AVAILABLE_CHANNELS))
|
||||
dialog.fill (["channel1", "ascending1"])
|
||||
dialog.get_int_combo("channel2", PikaUi.IntStore.new (AVAILABLE_CHANNELS))
|
||||
dialog.fill (["channel2","ascending2", "quantize"])
|
||||
dialog.get_int_combo("pchannel", PikaUi.IntStore.new (AVAILABLE_CHANNELS))
|
||||
dialog.fill (["pchannel","pquantize"])
|
||||
|
||||
if not dialog.run():
|
||||
dialog.destroy()
|
||||
config.end_run(Pika.PDBStatusType.CANCEL)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
|
||||
else:
|
||||
palette = config.get_property("palette")
|
||||
selection = config.get_property("selections")
|
||||
slice_expr = config.get_property ("slice_expr")
|
||||
channel1 = config.get_property("channel1")
|
||||
ascending1 = config.get_property ("ascending1")
|
||||
channel2 = config.get_property("channel2")
|
||||
ascending2 = config.get_property ("ascending2")
|
||||
quantize = config.get_property ("quantize")
|
||||
pchannel = config.get_property("pchannel")
|
||||
pquantize = config.get_property ("pquantize")
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
try:
|
||||
new_palette = palette_sort(palette, selection, slice_expr, channel1, ascending1,
|
||||
channel2, ascending2, quantize, pchannel, pquantize)
|
||||
except ValueError as err:
|
||||
return procedure.new_return_values(Pika.PDBStatusType.EXECUTION_ERROR,
|
||||
GLib.Error(str(err)))
|
||||
|
||||
config.end_run(Pika.PDBStatusType.SUCCESS)
|
||||
|
||||
return_val = procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
value = GObject.Value(Pika.Palette, new_palette)
|
||||
return_val.remove(1)
|
||||
return_val.insert(1, value)
|
||||
return return_val
|
||||
|
||||
|
||||
Pika.main(PaletteSort.__gtype__, sys.argv)
|
196
plug-ins/python/palette-to-gradient.py
Normal file
196
plug-ins/python/palette-to-gradient.py
Normal file
@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
import sys
|
||||
|
||||
def N_(message): return message
|
||||
def _(message): return GLib.dgettext(None, message)
|
||||
|
||||
|
||||
def make_gradient(palette, num_segments, num_colors):
|
||||
|
||||
# name the gradient same as the source palette
|
||||
# For now, the name of a resource is the same as the ID
|
||||
palette_name = palette.get_id()
|
||||
gradient = Pika.Gradient.new(palette_name)
|
||||
# assert gradient is valid but is has only one segment
|
||||
assert gradient.get_number_of_segments() == 1
|
||||
|
||||
# split one segment into desired count
|
||||
# index is zero-based
|
||||
gradient.segment_range_split_uniform( 0, 0, num_segments)
|
||||
|
||||
for color_number in range(0,num_segments):
|
||||
if color_number == num_colors - 1:
|
||||
color_number_next = 0
|
||||
else:
|
||||
color_number_next = color_number + 1
|
||||
_, color_left = palette.entry_get_color(color_number)
|
||||
_, color_right = palette.entry_get_color(color_number_next)
|
||||
gradient.segment_set_left_color( color_number, color_left, 100.0)
|
||||
gradient.segment_set_right_color(color_number, color_right, 100.0)
|
||||
|
||||
# Side effects on the context. Probably not what most would expect.
|
||||
Pika.context_set_gradient(gradient)
|
||||
return gradient
|
||||
|
||||
def run(procedure, args, data):
|
||||
config = procedure.create_config()
|
||||
config.begin_run(None, Pika.RunMode.INTERACTIVE, args)
|
||||
|
||||
# Get the parameters
|
||||
run_mode = args.index(0)
|
||||
palette = args.index(1)
|
||||
|
||||
if run_mode == Pika.RunMode.INTERACTIVE:
|
||||
PikaUi.init(procedure.get_name())
|
||||
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
|
||||
|
||||
# Add palette button
|
||||
dialog.fill (["palette"])
|
||||
|
||||
if not dialog.run():
|
||||
dialog.destroy()
|
||||
config.end_run(Pika.PDBStatusType.CANCEL)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
|
||||
else:
|
||||
palette = config.get_property("palette")
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
if palette is None or not palette.is_valid():
|
||||
if palette is not None:
|
||||
sys.stderr.write(f'Invalid palette id: {palette.get_id()}\n')
|
||||
sys.stderr.write('This should not happen. Please report to PIKA project.\n')
|
||||
sys.stderr.write('Falling back to context palette instead.\n')
|
||||
palette = Pika.context_get_palette()
|
||||
|
||||
num_colors = palette.get_color_count()
|
||||
|
||||
if procedure.get_name() == 'python-fu-palette-to-gradient':
|
||||
num_segments = num_colors - 1
|
||||
else: # 'python-fu-palette-to-gradient-repeating'
|
||||
num_segments = num_colors
|
||||
gradient = make_gradient(palette, num_segments, num_colors)
|
||||
|
||||
config.end_run(Pika.PDBStatusType.SUCCESS)
|
||||
# XXX: for the error parameter, we want to return None.
|
||||
# Unfortunately even though the argument is (nullable), pygobject
|
||||
# looks like it may have a bug. So workaround is to just set a
|
||||
# generic GLib.Error() since anyway the error won't be process with
|
||||
# PIKA_PDB_SUCCESS status.
|
||||
# See pygobject#351
|
||||
retval = procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
|
||||
# XXX: I don't try to get the GValue with retval.index(1) because it
|
||||
# actually return a string (cf. pygobject#353). Just create a new
|
||||
# GValue and replace the default one with this one.
|
||||
# This comment dates from when resources are strings:
|
||||
# value = GObject.Value(GObject.TYPE_STRING, gradient)
|
||||
|
||||
value = GObject.Value(Pika.Gradient, gradient)
|
||||
retval.remove(1)
|
||||
retval.insert(1, value)
|
||||
|
||||
return retval
|
||||
|
||||
class PaletteToGradient (Pika.PlugIn):
|
||||
## Parameter: run mode ##
|
||||
@GObject.Property(type=Pika.RunMode,
|
||||
default=Pika.RunMode.NONINTERACTIVE,
|
||||
nick="Run mode", blurb="The run mode")
|
||||
def run_mode(self):
|
||||
'''The run mode (unused)'''
|
||||
return self.runmode
|
||||
|
||||
@run_mode.setter
|
||||
def run_mode(self, runmode):
|
||||
self.runmode = runmode
|
||||
|
||||
## Parameter: palette ##
|
||||
@GObject.Property(type=Pika.Palette,
|
||||
default=None,
|
||||
nick= _("Palette"))
|
||||
def palette(self):
|
||||
'''Palette or None for the currently selected palette'''
|
||||
return self.palette
|
||||
|
||||
@palette.setter
|
||||
def palette(self, palette):
|
||||
self.palette = palette
|
||||
|
||||
## Properties: return values ##
|
||||
@GObject.Property(type=Pika.Gradient,
|
||||
default="",
|
||||
nick=_("The newly created gradient"),
|
||||
blurb=_("The newly created gradient"))
|
||||
def new_gradient(self):
|
||||
"""Read-write integer property."""
|
||||
return self.new_gradient
|
||||
|
||||
@new_gradient.setter
|
||||
def new_gradient(self, new_gradient):
|
||||
self.new_gradient = new_gradient
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return ['python-fu-palette-to-gradient',
|
||||
'python-fu-palette-to-gradient-repeating']
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = Pika.Procedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
run, None)
|
||||
if name == 'python-fu-palette-to-gradient':
|
||||
procedure.set_menu_label(_("Palette to _Gradient"))
|
||||
procedure.set_documentation(_("Create a gradient using colors from the palette"),
|
||||
_("Create a new gradient using colors from the palette."),
|
||||
"")
|
||||
elif name == 'python-fu-palette-to-gradient-repeating':
|
||||
procedure.set_menu_label(_("Palette to _Repeating Gradient"))
|
||||
procedure.set_documentation(_("Create a repeating gradient using colors from the palette"),
|
||||
_("Create a new repeating gradient using colors from the palette."),
|
||||
"")
|
||||
else:
|
||||
procedure = None
|
||||
|
||||
if procedure is not None:
|
||||
procedure.set_attribution("Carol Spears, reproduced from previous work by Adrian Likins and Jeff Trefftz",
|
||||
"Carol Spears", "2006")
|
||||
# We don't build a GParamSpec ourselves because passing it
|
||||
# around is apparently broken in Python. Hence this trick.
|
||||
# See pygobject#227
|
||||
procedure.add_argument_from_property(self, "run-mode")
|
||||
procedure.add_argument_from_property(self, "palette")
|
||||
procedure.add_return_value_from_property(self, "new-gradient")
|
||||
|
||||
procedure.add_menu_path ('<Palettes>')
|
||||
|
||||
return procedure
|
||||
|
||||
Pika.main(PaletteToGradient.__gtype__, sys.argv)
|
5
plug-ins/python/pypika.interp.in
Normal file
5
plug-ins/python/pypika.interp.in
Normal file
@ -0,0 +1,5 @@
|
||||
python=@PYTHON_PATH@
|
||||
python3=@PYTHON_PATH@
|
||||
/usr/bin/python=@PYTHON_PATH@
|
||||
/usr/bin/python3=@PYTHON_PATH@
|
||||
:Python:E::py::python3:
|
5
plug-ins/python/python-console/meson.build
Normal file
5
plug-ins/python/python-console/meson.build
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
plugins += {
|
||||
'name': 'python-console',
|
||||
'srcs': [ 'python-console/pyconsole.py', 'python-console/python-console.py' ],
|
||||
}
|
765
plug-ins/python/python-console/pyconsole.py
Normal file
765
plug-ins/python/python-console/pyconsole.py
Normal file
@ -0,0 +1,765 @@
|
||||
#
|
||||
# pyconsole.py
|
||||
#
|
||||
# Copyright (C) 2004-2006 by Yevgen Muntyan <muntyan@math.tamu.edu>
|
||||
# Portions of code by Geoffrey French.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public version 2.1 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# See COPYING.lib file that comes with this distribution for full text
|
||||
# of the license.
|
||||
#
|
||||
|
||||
# This module 'runs' python interpreter in a TextView widget.
|
||||
# The main class is Console, usage is:
|
||||
# Console(locals=None, banner=None, completer=None, use_rlcompleter=True, start_script='') -
|
||||
# it creates the widget and 'starts' interactive session; see the end
|
||||
# of this file. If start_script is not empty, it pastes it as it was
|
||||
# entered from keyboard.
|
||||
#
|
||||
# Console has "command" signal which is emitted when code is about to
|
||||
# be executed. You may connect to it using console.connect or
|
||||
# console.connect_after to get your callback ran before or after the
|
||||
# code is executed.
|
||||
#
|
||||
# To modify output appearance, set attributes of console.stdout_tag and
|
||||
# console.stderr_tag.
|
||||
#
|
||||
# Console may subclass a type other than gtk.TextView, to allow syntax
|
||||
# highlighting and stuff,
|
||||
# e.g.:
|
||||
# console_type = pyconsole.ConsoleType(moo.edit.TextView)
|
||||
# console = console_type(use_rlcompleter=False, start_script="import moo\nimport gtk\n")
|
||||
#
|
||||
# This widget is not a replacement for real terminal with python running
|
||||
# inside: GtkTextView is not a terminal.
|
||||
# The use case is: you have a python program, you create this widget,
|
||||
# and inspect your program interiors.
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Pango
|
||||
import code
|
||||
import sys
|
||||
import keyword
|
||||
import re
|
||||
|
||||
def pango_pixels(value):
|
||||
# The PANGO_PIXELS macro is not accessible through GObject
|
||||
# Introspection. Just reimplement it:
|
||||
# #define PANGO_PIXELS(d) (((int)(d) + 512) >> 10)
|
||||
return (value + 512) >> 10
|
||||
|
||||
# commonprefix() from posixpath
|
||||
def _commonprefix(m):
|
||||
"Given a list of pathnames, returns the longest common leading component"
|
||||
if not m: return ''
|
||||
prefix = m[0]
|
||||
for item in m:
|
||||
for i in range(len(prefix)):
|
||||
if prefix[:i+1] != item[:i+1]:
|
||||
prefix = prefix[:i]
|
||||
if i == 0:
|
||||
return ''
|
||||
break
|
||||
return prefix
|
||||
|
||||
class _ReadLine(object):
|
||||
|
||||
class Output(object):
|
||||
def __init__(self, console, tag_name):
|
||||
object.__init__(self)
|
||||
self.buffer = console.get_buffer()
|
||||
self.tag_name = tag_name
|
||||
def write(self, text):
|
||||
pos = self.buffer.get_iter_at_mark(self.buffer.get_insert())
|
||||
self.buffer.insert_with_tags_by_name(pos, text, self.tag_name)
|
||||
def flush(self):
|
||||
# The output is just a GtkTextBuffer inside a GtkTexView so I
|
||||
# believe it should be always in-sync without needing to be flushed.
|
||||
# Nevertheless someone might be just copy-pasting plug-in code to
|
||||
# test it, for instance. So let's just add a no-op flush() method to
|
||||
# get a similar interface as the real sys.stdout which we overrode.
|
||||
# It avoids useless and unexpected code failure.
|
||||
pass
|
||||
|
||||
class History(object):
|
||||
def __init__(self):
|
||||
object.__init__(self)
|
||||
self.items = ['']
|
||||
self.ptr = 0
|
||||
self.edited = {}
|
||||
|
||||
def commit(self, text):
|
||||
if text and self.items[-1] != text:
|
||||
self.items.append(text)
|
||||
self.ptr = 0
|
||||
self.edited = {}
|
||||
|
||||
def get(self, dir, text):
|
||||
if len(self.items) == 1:
|
||||
return None
|
||||
|
||||
if text != self.items[self.ptr]:
|
||||
self.edited[self.ptr] = text
|
||||
elif self.ptr in self.edited:
|
||||
del self.edited[self.ptr]
|
||||
|
||||
self.ptr = self.ptr + dir
|
||||
if self.ptr >= len(self.items):
|
||||
self.ptr = 0
|
||||
elif self.ptr < 0:
|
||||
self.ptr = len(self.items) - 1
|
||||
|
||||
try:
|
||||
return self.edited[self.ptr]
|
||||
except KeyError:
|
||||
return self.items[self.ptr]
|
||||
|
||||
def __init__(self, quit_func=None):
|
||||
object.__init__(self)
|
||||
|
||||
self.quit_func = quit_func
|
||||
|
||||
self.set_wrap_mode(Gtk.WrapMode.CHAR)
|
||||
self.modify_font(Pango.FontDescription("Monospace"))
|
||||
|
||||
self.buffer = self.get_buffer()
|
||||
self.buffer.connect("insert-text", self.on_buf_insert)
|
||||
self.buffer.connect("delete-range", self.on_buf_delete)
|
||||
self.buffer.connect("mark-set", self.on_buf_mark_set)
|
||||
self.do_insert = False
|
||||
self.do_delete = False
|
||||
|
||||
self.stdout_tag = self.buffer.create_tag("stdout", foreground="#006000")
|
||||
self.stderr_tag = self.buffer.create_tag("stderr", foreground="#B00000")
|
||||
self._stdout = _ReadLine.Output(self, "stdout")
|
||||
self._stderr = _ReadLine.Output(self, "stderr")
|
||||
|
||||
self.cursor = self.buffer.create_mark("cursor",
|
||||
self.buffer.get_start_iter(),
|
||||
False)
|
||||
insert = self.buffer.get_insert()
|
||||
self.cursor.set_visible(True)
|
||||
insert.set_visible(False)
|
||||
|
||||
self.ps = ''
|
||||
self.in_raw_input = False
|
||||
self.in_modal_raw_input = False
|
||||
self.run_on_raw_input = None
|
||||
self.tab_pressed = 0
|
||||
self.history = _ReadLine.History()
|
||||
self.nonword_re = re.compile("[^\w\._]")
|
||||
|
||||
def freeze_undo(self):
|
||||
try: self.begin_not_undoable_action()
|
||||
except: pass
|
||||
|
||||
def thaw_undo(self):
|
||||
try: self.end_not_undoable_action()
|
||||
except: pass
|
||||
|
||||
def raw_input(self, ps=None):
|
||||
'''Show prompt 'ps' and enter input mode until the current input
|
||||
is committed.'''
|
||||
|
||||
if ps:
|
||||
self.ps = ps
|
||||
else:
|
||||
self.ps = ''
|
||||
|
||||
iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
|
||||
|
||||
if ps:
|
||||
self.freeze_undo()
|
||||
self.buffer.insert(iter, self.ps)
|
||||
self.thaw_undo()
|
||||
|
||||
self.__move_cursor_to(iter)
|
||||
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
|
||||
|
||||
self.in_raw_input = True
|
||||
|
||||
if self.run_on_raw_input:
|
||||
run_now = self.run_on_raw_input
|
||||
self.run_on_raw_input = None
|
||||
self.buffer.insert_at_cursor(run_now + '\n')
|
||||
|
||||
def modal_raw_input(self, text):
|
||||
'''Starts raw input in modal mode. The event loop is spinned until
|
||||
the input is committed. Returns the text entered after the prompt.'''
|
||||
orig_ps = self.ps
|
||||
|
||||
self.raw_input(text)
|
||||
self.in_modal_raw_input = True
|
||||
|
||||
while self.in_modal_raw_input:
|
||||
Gtk.main_iteration()
|
||||
|
||||
self.ps = orig_ps
|
||||
self.in_modal_raw_input = False
|
||||
self.in_raw_input = False
|
||||
|
||||
return self.modal_raw_input_result
|
||||
|
||||
def modal_input(self, text):
|
||||
return eval(self.modal_raw_input(text))
|
||||
|
||||
# Each time the insert mark is modified, move the cursor to it.
|
||||
def on_buf_mark_set(self, buffer, iter, mark):
|
||||
if mark is not buffer.get_insert():
|
||||
return
|
||||
start = self.__get_start()
|
||||
end = self.__get_end()
|
||||
if iter.compare(self.__get_start()) >= 0 and \
|
||||
iter.compare(self.__get_end()) <= 0:
|
||||
buffer.move_mark_by_name("cursor", iter)
|
||||
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
|
||||
|
||||
def __insert(self, iter, text):
|
||||
self.do_insert = True
|
||||
self.buffer.insert(iter, text)
|
||||
self.do_insert = False
|
||||
|
||||
# Make sure that text insertions while in text input mode are properly
|
||||
# committed to the history.
|
||||
def on_buf_insert(self, buf, iter, text, len):
|
||||
# Bail out if not in input mode.
|
||||
if not self.in_raw_input or self.do_insert or not len:
|
||||
return
|
||||
|
||||
buf.stop_emission("insert-text")
|
||||
lines = text.splitlines()
|
||||
need_eol = False
|
||||
for l in lines:
|
||||
if need_eol:
|
||||
self._commit()
|
||||
iter = self.__get_cursor()
|
||||
else:
|
||||
cursor = self.__get_cursor()
|
||||
if iter.compare(self.__get_start()) < 0:
|
||||
iter = cursor
|
||||
elif iter.compare(self.__get_end()) > 0:
|
||||
iter = cursor
|
||||
else:
|
||||
self.__move_cursor_to(iter)
|
||||
need_eol = True
|
||||
self.__insert(iter, l)
|
||||
self.__move_cursor(0)
|
||||
|
||||
def __delete(self, start, end):
|
||||
self.do_delete = True
|
||||
self.buffer.delete(start, end)
|
||||
self.do_delete = False
|
||||
|
||||
def on_buf_delete(self, buf, start, end):
|
||||
if not self.in_raw_input or self.do_delete:
|
||||
return
|
||||
|
||||
buf.stop_emission("delete-range")
|
||||
|
||||
start.order(end)
|
||||
line_start = self.__get_start()
|
||||
line_end = self.__get_end()
|
||||
|
||||
if start.compare(line_end) > 0:
|
||||
return
|
||||
if end.compare(line_start) < 0:
|
||||
return
|
||||
|
||||
self.__move_cursor(0)
|
||||
|
||||
if start.compare(line_start) < 0:
|
||||
start = line_start
|
||||
if end.compare(line_end) > 0:
|
||||
end = line_end
|
||||
self.__delete(start, end)
|
||||
|
||||
# We overload the key press event handler to handle "special keys"
|
||||
# when in input mode to make history browsing, completions, etc. work.
|
||||
def do_key_press_event(self, event):
|
||||
if not self.in_raw_input:
|
||||
return Gtk.TextView.do_key_press_event(self, event)
|
||||
|
||||
tab_pressed = self.tab_pressed
|
||||
self.tab_pressed = 0
|
||||
handled = True
|
||||
|
||||
state = event.state & (Gdk.ModifierType.SHIFT_MASK |
|
||||
Gdk.ModifierType.CONTROL_MASK |
|
||||
Gdk.ModifierType.MOD1_MASK)
|
||||
keyval = event.keyval
|
||||
|
||||
if not state:
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
pass
|
||||
elif keyval == Gdk.KEY_Return:
|
||||
self._commit()
|
||||
elif keyval == Gdk.KEY_Up:
|
||||
self.__history(-1)
|
||||
elif keyval == Gdk.KEY_Down:
|
||||
self.__history(1)
|
||||
elif keyval == Gdk.KEY_Left:
|
||||
self.__move_cursor(-1)
|
||||
elif keyval == Gdk.KEY_Right:
|
||||
self.__move_cursor(1)
|
||||
elif keyval == Gdk.KEY_Home:
|
||||
self.__move_cursor(-10000)
|
||||
elif keyval == Gdk.KEY_End:
|
||||
self.__move_cursor(10000)
|
||||
elif keyval == Gdk.KEY_Tab:
|
||||
cursor = self.__get_cursor()
|
||||
if cursor.starts_line():
|
||||
handled = False
|
||||
else:
|
||||
cursor.backward_char()
|
||||
if cursor.get_char().isspace():
|
||||
handled = False
|
||||
else:
|
||||
self.tab_pressed = tab_pressed + 1
|
||||
self.__complete()
|
||||
else:
|
||||
handled = False
|
||||
elif state == Gdk.ModifierType.CONTROL_MASK:
|
||||
if keyval == Gdk.KEY_u:
|
||||
start = self.__get_start()
|
||||
end = self.__get_cursor()
|
||||
self.__delete(start, end)
|
||||
elif keyval == Gdk.KEY_d:
|
||||
if self.quit_func:
|
||||
self.quit_func()
|
||||
else:
|
||||
handled = False
|
||||
else:
|
||||
handled = False
|
||||
|
||||
# Handle ordinary keys
|
||||
if not handled:
|
||||
return Gtk.TextView.do_key_press_event(self, event)
|
||||
else:
|
||||
return True
|
||||
|
||||
def __history(self, dir):
|
||||
text = self._get_line()
|
||||
new_text = self.history.get(dir, text)
|
||||
if not new_text is None:
|
||||
self.__replace_line(new_text)
|
||||
self.__move_cursor(0)
|
||||
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
|
||||
|
||||
def __get_cursor(self):
|
||||
'''Returns an iterator at the current cursor position.'''
|
||||
return self.buffer.get_iter_at_mark(self.cursor)
|
||||
|
||||
def __get_start(self):
|
||||
'''Returns an iterator at the start of the input on the current
|
||||
cursor line.'''
|
||||
|
||||
iter = self.__get_cursor()
|
||||
iter.set_line(iter.get_line())
|
||||
iter.forward_chars(len(self.ps))
|
||||
return iter
|
||||
|
||||
def __get_end(self):
|
||||
'''Returns an iterator at the end of the cursor line.'''
|
||||
iter = self.__get_cursor()
|
||||
if not iter.ends_line():
|
||||
iter.forward_to_line_end()
|
||||
return iter
|
||||
|
||||
def __get_text(self, start, end):
|
||||
'''Get text between 'start' and 'end' markers.'''
|
||||
return self.buffer.get_text(start, end, False)
|
||||
|
||||
def __move_cursor_to(self, iter):
|
||||
self.buffer.place_cursor(iter)
|
||||
self.buffer.move_mark_by_name("cursor", iter)
|
||||
|
||||
def __move_cursor(self, howmany):
|
||||
iter = self.__get_cursor()
|
||||
end = self.__get_cursor()
|
||||
if not end.ends_line():
|
||||
end.forward_to_line_end()
|
||||
line_len = end.get_line_offset()
|
||||
move_to = iter.get_line_offset() + howmany
|
||||
move_to = min(max(move_to, len(self.ps)), line_len)
|
||||
iter.set_line_offset(move_to)
|
||||
self.__move_cursor_to(iter)
|
||||
|
||||
def __delete_at_cursor(self, howmany):
|
||||
iter = self.__get_cursor()
|
||||
end = self.__get_cursor()
|
||||
if not end.ends_line():
|
||||
end.forward_to_line_end()
|
||||
line_len = end.get_line_offset()
|
||||
erase_to = iter.get_line_offset() + howmany
|
||||
if erase_to > line_len:
|
||||
erase_to = line_len
|
||||
elif erase_to < len(self.ps):
|
||||
erase_to = len(self.ps)
|
||||
end.set_line_offset(erase_to)
|
||||
self.__delete(iter, end)
|
||||
|
||||
def __get_width(self):
|
||||
'''Estimate the number of characters that will fit in the area
|
||||
currently allocated to this widget.'''
|
||||
|
||||
if not self.get_realized():
|
||||
return 80
|
||||
|
||||
context = self.get_pango_context()
|
||||
metrics = context.get_metrics(context.get_font_description(),
|
||||
context.get_language())
|
||||
pix_width = metrics.get_approximate_char_width()
|
||||
allocation = Gtk.Widget.get_allocation(self)
|
||||
return allocation.width * Pango.SCALE / pix_width
|
||||
|
||||
def __print_completions(self, completions):
|
||||
line_start = self.__get_text(self.__get_start(), self.__get_cursor())
|
||||
line_end = self.__get_text(self.__get_cursor(), self.__get_end())
|
||||
iter = self.buffer.get_end_iter()
|
||||
self.__move_cursor_to(iter)
|
||||
self.__insert(iter, "\n")
|
||||
|
||||
width = max(self.__get_width(), 4)
|
||||
max_width = max(len(s) for s in completions)
|
||||
n_columns = max(int(width / (max_width + 1)), 1)
|
||||
col_width = int(width / n_columns)
|
||||
total = len(completions)
|
||||
col_length = total / n_columns
|
||||
if total % n_columns:
|
||||
col_length = col_length + 1
|
||||
col_length = max(col_length, 1)
|
||||
|
||||
if col_length == 1:
|
||||
n_columns = total
|
||||
col_width = width / total
|
||||
|
||||
for i in range(int(col_length)):
|
||||
for j in range(n_columns):
|
||||
ind = i + j*col_length
|
||||
if ind < total:
|
||||
if j == n_columns - 1:
|
||||
n_spaces = 0
|
||||
else:
|
||||
n_spaces = int(col_width - len(completions[int(ind)]))
|
||||
self.__insert(iter, completions[int(ind)] + " " * n_spaces)
|
||||
self.__insert(iter, "\n")
|
||||
|
||||
self.__insert(iter, "%s%s%s" % (self.ps, line_start, line_end))
|
||||
iter.set_line_offset(len(self.ps) + len(line_start))
|
||||
self.__move_cursor_to(iter)
|
||||
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
|
||||
|
||||
def __complete(self):
|
||||
text = self.__get_text(self.__get_start(), self.__get_cursor())
|
||||
start = ''
|
||||
word = text
|
||||
nonwords = self.nonword_re.findall(text)
|
||||
if nonwords:
|
||||
last = text.rfind(nonwords[-1]) + len(nonwords[-1])
|
||||
start = text[:last]
|
||||
word = text[last:]
|
||||
|
||||
completions = self.complete(word)
|
||||
|
||||
if completions:
|
||||
prefix = _commonprefix(completions)
|
||||
if prefix != word:
|
||||
start_iter = self.__get_start()
|
||||
start_iter.forward_chars(len(start))
|
||||
end_iter = start_iter.copy()
|
||||
end_iter.forward_chars(len(word))
|
||||
self.__delete(start_iter, end_iter)
|
||||
self.__insert(end_iter, prefix)
|
||||
elif self.tab_pressed > 1:
|
||||
self.freeze_undo()
|
||||
self.__print_completions(completions)
|
||||
self.thaw_undo()
|
||||
self.tab_pressed = 0
|
||||
|
||||
def complete(self, text):
|
||||
return None
|
||||
|
||||
def _get_line(self):
|
||||
'''Return the current input behind the prompt.'''
|
||||
start = self.__get_start()
|
||||
end = self.__get_end()
|
||||
return self.buffer.get_text(start, end, False)
|
||||
|
||||
def __replace_line(self, new_text):
|
||||
'''Replace the current input with 'new_text' '''
|
||||
start = self.__get_start()
|
||||
end = self.__get_end()
|
||||
self.__delete(start, end)
|
||||
self.__insert(end, new_text)
|
||||
|
||||
def _commit(self):
|
||||
'''Commit the input entered on the current line.'''
|
||||
|
||||
# Find iterator and end of cursor line.
|
||||
end = self.__get_cursor()
|
||||
if not end.ends_line():
|
||||
end.forward_to_line_end()
|
||||
|
||||
# Get text at current line.
|
||||
text = self._get_line()
|
||||
|
||||
# Move cursor to the end of the line, insert new line.
|
||||
self.__move_cursor_to(end)
|
||||
self.freeze_undo()
|
||||
self.__insert(end, "\n")
|
||||
|
||||
self.history.commit(text)
|
||||
if self.in_modal_raw_input:
|
||||
self.in_modal_raw_input = False
|
||||
self.modal_raw_input_result = text
|
||||
else:
|
||||
self.in_raw_input = False
|
||||
self.do_raw_input(text)
|
||||
|
||||
self.thaw_undo()
|
||||
|
||||
def do_raw_input(self, text):
|
||||
pass
|
||||
|
||||
|
||||
class _Console(_ReadLine, code.InteractiveInterpreter):
|
||||
def __init__(self, locals=None, banner=None,
|
||||
completer=None, use_rlcompleter=True,
|
||||
start_script=None, quit_func=None):
|
||||
_ReadLine.__init__(self, quit_func)
|
||||
|
||||
code.InteractiveInterpreter.__init__(self, locals)
|
||||
self.locals["__console__"] = self
|
||||
|
||||
# The builtin raw_input function reads from stdin, we don't want
|
||||
# this. Therefore, replace this function with our own modal raw
|
||||
# input function.
|
||||
exec ("import builtins", self.locals)
|
||||
#self.locals['builtins'].__dict__['raw_input'] = lambda text='': self.modal_raw_input(text)
|
||||
self.locals['builtins'].__dict__['input'] = lambda text='': self.modal_input(text)
|
||||
|
||||
self.start_script = start_script
|
||||
self.completer = completer
|
||||
self.banner = banner
|
||||
|
||||
if not self.completer and use_rlcompleter:
|
||||
try:
|
||||
import rlcompleter
|
||||
self.completer = rlcompleter.Completer()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
self.ps1 = ">>> "
|
||||
self.ps2 = "... "
|
||||
self.__start()
|
||||
self.run_on_raw_input = start_script
|
||||
self.raw_input(self.ps1)
|
||||
|
||||
def __start(self):
|
||||
self.cmd_buffer = ""
|
||||
|
||||
self.freeze_undo()
|
||||
self.thaw_undo()
|
||||
|
||||
self.do_delete = True
|
||||
self.buffer.set_text("")
|
||||
self.do_delete = False
|
||||
|
||||
if self.banner:
|
||||
iter = self.buffer.get_start_iter()
|
||||
self.buffer.insert_with_tags_by_name(iter, self.banner, "stdout")
|
||||
if not iter.starts_line():
|
||||
self.buffer.insert(iter, "\n")
|
||||
|
||||
def clear(self, start_script=None):
|
||||
if start_script is None:
|
||||
start_script = self.start_script
|
||||
else:
|
||||
self.start_script = start_script
|
||||
|
||||
self.__start()
|
||||
self.run_on_raw_input = start_script
|
||||
self.raw_input(self.ps1)
|
||||
|
||||
def do_raw_input(self, text):
|
||||
if self.cmd_buffer:
|
||||
cmd = self.cmd_buffer + "\n" + text
|
||||
else:
|
||||
cmd = text
|
||||
|
||||
saved_stdout, saved_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout, sys.stderr = self._stdout, self._stderr
|
||||
|
||||
if self.runsource(cmd):
|
||||
self.cmd_buffer = cmd
|
||||
ps = self.ps2
|
||||
else:
|
||||
self.cmd_buffer = ''
|
||||
ps = self.ps1
|
||||
|
||||
sys.stdout, sys.stderr = saved_stdout, saved_stderr
|
||||
self.raw_input(ps)
|
||||
|
||||
def do_command(self, code):
|
||||
try:
|
||||
eval(code, self.locals)
|
||||
except SystemExit:
|
||||
if self.quit_func:
|
||||
self.quit_func()
|
||||
else:
|
||||
raise
|
||||
except:
|
||||
self.showtraceback()
|
||||
|
||||
def runcode(self, code):
|
||||
#if gtk.pygtk_version[1] < 8:
|
||||
self.do_command(code)
|
||||
#else:
|
||||
#self.emit("command", code)
|
||||
|
||||
def complete_attr(self, start, end):
|
||||
try:
|
||||
obj = eval(start, self.locals)
|
||||
strings = dir(obj)
|
||||
|
||||
if end:
|
||||
completions = {}
|
||||
for s in strings:
|
||||
if s.startswith(end):
|
||||
completions[s] = None
|
||||
completions = completions.keys()
|
||||
else:
|
||||
completions = strings
|
||||
|
||||
completions.sort()
|
||||
return [start + "." + s for s in completions]
|
||||
except:
|
||||
return None
|
||||
|
||||
def complete(self, text):
|
||||
if self.completer:
|
||||
completions = []
|
||||
i = 0
|
||||
try:
|
||||
while 1:
|
||||
s = self.completer.complete(text, i)
|
||||
if s:
|
||||
completions.append(s)
|
||||
i += 1
|
||||
else:
|
||||
completions.sort()
|
||||
return completions
|
||||
except NameError:
|
||||
return None
|
||||
|
||||
dot = text.rfind(".")
|
||||
if dot >= 0:
|
||||
return self.complete_attr(text[:dot], text[dot+1:])
|
||||
|
||||
completions = {}
|
||||
strings = keyword.kwlist
|
||||
|
||||
if self.locals:
|
||||
strings.extend(self.locals.keys())
|
||||
|
||||
try: strings.extend(eval("globals()", self.locals).keys())
|
||||
except: pass
|
||||
|
||||
try:
|
||||
exec("import __builtin__", self.locals)
|
||||
strings.extend(eval("dir(__builtin__)", self.locals))
|
||||
except:
|
||||
pass
|
||||
|
||||
for s in strings:
|
||||
if s.startswith(text):
|
||||
completions[s] = None
|
||||
completions = completions.keys()
|
||||
completions.sort()
|
||||
return completions
|
||||
|
||||
|
||||
def ReadLineType(t=Gtk.TextView):
|
||||
class readline(t, _ReadLine):
|
||||
def __init__(self, *args, **kwargs):
|
||||
t.__init__(self)
|
||||
_ReadLine.__init__(self, *args, **kwargs)
|
||||
def do_key_press_event(self, event):
|
||||
return _ReadLine.do_key_press_event(self, event)
|
||||
GObject.type_register(readline)
|
||||
return readline
|
||||
|
||||
def ConsoleType(t=Gtk.TextView):
|
||||
class console_type(t, _Console):
|
||||
__gsignals__ = {
|
||||
'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (object,)),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
#if gtk.pygtk_version[1] < 8:
|
||||
GObject.GObject.__init__(self)
|
||||
#else:
|
||||
#t.__init__(self)
|
||||
_Console.__init__(self, *args, **kwargs)
|
||||
|
||||
def do_command(self, code):
|
||||
return _Console.do_command(self, code)
|
||||
|
||||
def do_key_press_event(self, event):
|
||||
return _Console.do_key_press_event(self, event)
|
||||
|
||||
def get_default_size(self):
|
||||
context = self.get_pango_context()
|
||||
metrics = context.get_metrics(context.get_font_description(),
|
||||
context.get_language())
|
||||
width = metrics.get_approximate_char_width()
|
||||
height = metrics.get_ascent() + metrics.get_descent()
|
||||
|
||||
# Default to a 80x40 console
|
||||
width = pango_pixels(int(width * 80 * 1.05))
|
||||
height = pango_pixels(height * 40)
|
||||
|
||||
return width, height
|
||||
|
||||
#if gtk.pygtk_version[1] < 8:
|
||||
GObject.type_register(console_type)
|
||||
|
||||
return console_type
|
||||
|
||||
ReadLine = ReadLineType()
|
||||
Console = ConsoleType()
|
||||
|
||||
def _make_window():
|
||||
window = Gtk.Window()
|
||||
window.set_title("pyconsole.py")
|
||||
swin = Gtk.ScrolledWindow()
|
||||
swin.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
|
||||
window.add(swin)
|
||||
console = Console(banner="Hello there!",
|
||||
use_rlcompleter=False,
|
||||
start_script="gi.require_version('Pika', '3.0')\nfrom gi.repository import Pika\n")
|
||||
swin.add(console)
|
||||
|
||||
width, height = console.get_default_size()
|
||||
sb_width, sb_height = swin.get_vscrollbar().size_request()
|
||||
|
||||
window.set_default_size(width + sb_width, height)
|
||||
window.show_all()
|
||||
|
||||
if not Gtk.main_level():
|
||||
window.connect("destroy", Gtk.main_quit)
|
||||
Gtk.main()
|
||||
|
||||
return console
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2 or sys.argv[1] != '-pika':
|
||||
_make_window()
|
370
plug-ins/python/python-console/python-console.py
Normal file
370
plug-ins/python/python-console/python-console.py
Normal file
@ -0,0 +1,370 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Pika-Python - allows the writing of Pika plugins in Python.
|
||||
# Copyright (C) 1997 James Henstridge <james@daa.com.au>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import Pika
|
||||
from gi.repository import PikaUi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
from gi.repository import GLib
|
||||
|
||||
import sys
|
||||
import pyconsole
|
||||
#import pikashelf, pikaui, pyconsole
|
||||
|
||||
import gettext
|
||||
textdomain = "pika30-python"
|
||||
gettext.bindtextdomain(textdomain, Pika.locale_directory())
|
||||
gettext.textdomain(textdomain)
|
||||
_ = gettext.gettext
|
||||
|
||||
PROC_NAME = 'python-fu-console'
|
||||
|
||||
RESPONSE_BROWSE, RESPONSE_CLEAR, RESPONSE_SAVE = range(3)
|
||||
|
||||
def run(procedure, args, data):
|
||||
PikaUi.init ("python-console.py")
|
||||
|
||||
namespace = {'__builtins__': __builtins__,
|
||||
'__name__': '__main__', '__doc__': None,
|
||||
'Babl': gi.repository.Babl,
|
||||
'cairo': gi.repository.cairo,
|
||||
'Gdk': gi.repository.Gdk,
|
||||
'Gegl': gi.repository.Gegl,
|
||||
'Pika': gi.repository.Pika,
|
||||
'Gio': gi.repository.Gio,
|
||||
'Gtk': gi.repository.Gtk,
|
||||
'GdkPixbuf': gi.repository.GdkPixbuf,
|
||||
'GLib': gi.repository.GLib,
|
||||
'GObject': gi.repository.GObject,
|
||||
'Pango': gi.repository.Pango }
|
||||
|
||||
class PikaConsole(pyconsole.Console):
|
||||
def __init__(self, quit_func=None):
|
||||
banner = ('PIKA %s Python Console\nPython %s\n' %
|
||||
(Pika.version(), sys.version))
|
||||
pyconsole.Console.__init__(self,
|
||||
locals=namespace, banner=banner,
|
||||
quit_func=quit_func)
|
||||
def _commit(self):
|
||||
pyconsole.Console._commit(self)
|
||||
Pika.displays_flush()
|
||||
|
||||
class ConsoleDialog(PikaUi.Dialog):
|
||||
def __init__(self):
|
||||
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
|
||||
PikaUi.Dialog.__init__(self, use_header_bar=use_header_bar)
|
||||
self.set_property("help-id", PROC_NAME)
|
||||
Gtk.Window.set_title(self, _("Python Console"))
|
||||
Gtk.Window.set_role(self, PROC_NAME)
|
||||
Gtk.Dialog.add_button(self, _("_Save"), Gtk.ResponseType.OK)
|
||||
Gtk.Dialog.add_button(self, _("Cl_ear"), RESPONSE_CLEAR)
|
||||
Gtk.Dialog.add_button(self, _("_Browse..."), RESPONSE_BROWSE)
|
||||
Gtk.Dialog.add_button(self, _("_Close"), Gtk.ResponseType.CLOSE)
|
||||
|
||||
Gtk.Widget.set_name (self, PROC_NAME)
|
||||
PikaUi.Dialog.set_alternative_button_order_from_array(self,
|
||||
[ Gtk.ResponseType.CLOSE,
|
||||
RESPONSE_BROWSE,
|
||||
RESPONSE_CLEAR,
|
||||
Gtk.ResponseType.OK ])
|
||||
|
||||
self.cons = PikaConsole(quit_func=lambda: Gtk.main_quit())
|
||||
|
||||
self.style_set (None, None)
|
||||
|
||||
self.connect('response', self.response)
|
||||
self.connect('style-set', self.style_set)
|
||||
|
||||
self.browse_dlg = None
|
||||
self.save_dlg = None
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=12)
|
||||
vbox.set_border_width(12)
|
||||
contents_area = Gtk.Dialog.get_content_area(self)
|
||||
contents_area.pack_start(vbox, True, True, 0)
|
||||
|
||||
scrl_win = Gtk.ScrolledWindow()
|
||||
scrl_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
|
||||
vbox.pack_start(scrl_win, True, True, 0)
|
||||
|
||||
scrl_win.add(self.cons)
|
||||
|
||||
width, height = self.cons.get_default_size()
|
||||
minreq, requisition = Gtk.Widget.get_preferred_size(scrl_win.get_vscrollbar())
|
||||
sb_width = requisition.width
|
||||
sb_height = requisition.height
|
||||
|
||||
# Account for scrollbar width and border width to ensure
|
||||
# the text view gets a width of 80 characters. We don't care
|
||||
# so much whether the height will be exactly 40 characters.
|
||||
Gtk.Window.set_default_size(self, width + sb_width + 2 * 12, height)
|
||||
|
||||
def style_set(self, old_style, user_data):
|
||||
pass
|
||||
#style = Gtk.Widget.get_style (self)
|
||||
#self.cons.stdout_tag.set_property ("foreground", style.text[Gtk.StateType.NORMAL])
|
||||
#self.cons.stderr_tag.set_property ("foreground", style.text[Gtk.StateType.INSENSITIVE])
|
||||
|
||||
def response(self, dialog, response_id):
|
||||
if response_id == RESPONSE_BROWSE:
|
||||
self.browse()
|
||||
elif response_id == RESPONSE_CLEAR:
|
||||
self.cons.banner = None
|
||||
self.cons.clear()
|
||||
elif response_id == Gtk.ResponseType.OK:
|
||||
self.save_dialog()
|
||||
else:
|
||||
Gtk.main_quit()
|
||||
|
||||
self.cons.grab_focus()
|
||||
|
||||
def command_for_procedure(self, proc_name):
|
||||
'''
|
||||
Assemble string of Python code that when eval'd
|
||||
will call proc_name with contrived arguments.
|
||||
|
||||
The purpose is to generate a template for a call
|
||||
to the PDB procedure the user has selected.
|
||||
The call MIGHT work as is.
|
||||
Otherwise, the names of the arguments might be enough
|
||||
that the user can figure out how to edit the template.
|
||||
|
||||
The code will run in the environment of the console/browser,
|
||||
which is not the PIKA v2 PikaFu environment
|
||||
but the PIKA v3 PyGObject introspected environment.
|
||||
|
||||
If ever PikaFu module is resurrected, and Python console imports it,
|
||||
then revert this code to its v2 form.
|
||||
'''
|
||||
proc = Pika.get_pdb().lookup_procedure(proc_name)
|
||||
if proc is None:
|
||||
return None
|
||||
|
||||
cmd = ''
|
||||
|
||||
return_values = proc.get_return_values()
|
||||
# assert is list of GParamSpec
|
||||
|
||||
'''
|
||||
Cat str of variable names to which unpack return values
|
||||
Variable names same as return value name, mangled
|
||||
Str like: 'retval_1, ret_val_2 = '
|
||||
'''
|
||||
if len(return_values) > 0:
|
||||
cmd += ', '.join(x.name.replace('-', '_') for x in return_values)
|
||||
cmd += ' = '
|
||||
# else is a void PDB procedure
|
||||
|
||||
'''
|
||||
Cat prefix of str for a call to procedure name
|
||||
Prefix like: Pika.get_pdb().run_procedure('<foo>',
|
||||
Note:
|
||||
- proc name is quoted, run_procedure wants a string.
|
||||
- proc name has hyphens. Not a Python name. Matches name in PDB.
|
||||
- trailing comma, another arg to follow:
|
||||
run_procedure takes two args: string name, and GValueArray of args
|
||||
'''
|
||||
cmd += f"Pika.get_pdb().run_procedure('{proc_name}', "
|
||||
|
||||
'''
|
||||
Assemble argument string.
|
||||
Using names of formal args, which might not match names already
|
||||
defined in the browsing environment (the REPL).
|
||||
|
||||
Args are passed to a PDB procedure in a GValueArray.
|
||||
Assemble a string like '[arg_1, arg_2]'.
|
||||
When eval'd, the Python binding will convert to a GValueArray.
|
||||
'''
|
||||
param_specs = proc.get_arguments()
|
||||
cmd += '[ '
|
||||
|
||||
'''
|
||||
Special handling for run mode.
|
||||
PIKA v2: PikaFu had different handling for run mode.
|
||||
Insure run mode interactive, i.e. called procedure may open a GUI.
|
||||
|
||||
This assumes that procedures use the same formal name for runmode arg.
|
||||
There might be rare other cases, especially for third party plugins?
|
||||
E.G. See formal signature of file-gex-load
|
||||
|
||||
There is no other way to distinguish the run mode formal argument,
|
||||
as its formal type is PikaParamEnum, a generic enum.
|
||||
'''
|
||||
if len(param_specs) > 0 and param_specs[0].name == 'run-mode':
|
||||
cmd += 'Pika.RunMode.INTERACTIVE, '
|
||||
param_specs = param_specs[1:]
|
||||
# else doesn't take a run mode arg
|
||||
|
||||
# Cat string of arg names to a call
|
||||
# Like: 'arg_1, arg_2' where formal names arg-1 and arg-2
|
||||
cmd += ', '.join(x.name.replace('-', '_') for x in param_specs)
|
||||
|
||||
# terminate the arg array, and close parens the call
|
||||
cmd += '])'
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
def browse_response(self, dlg, response_id):
|
||||
if response_id != Gtk.ResponseType.APPLY:
|
||||
Gtk.Widget.hide(dlg)
|
||||
return
|
||||
|
||||
proc_name = dlg.get_selected()
|
||||
|
||||
if not proc_name:
|
||||
# Apply button was enabled without a selection?
|
||||
return
|
||||
|
||||
cmd = self.command_for_procedure(proc_name)
|
||||
if cmd is None:
|
||||
# Should not happen. We browsed a name not in the PDB?
|
||||
return
|
||||
|
||||
buffer = self.cons.buffer
|
||||
|
||||
lines = buffer.get_line_count()
|
||||
iter = buffer.get_iter_at_line_offset(lines - 1, 4)
|
||||
buffer.delete(iter, buffer.get_end_iter())
|
||||
buffer.place_cursor(buffer.get_end_iter())
|
||||
buffer.insert_at_cursor(cmd)
|
||||
# not insert a newline, user can edit and then "enter" the command
|
||||
|
||||
def browse(self):
|
||||
if not self.browse_dlg:
|
||||
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
|
||||
dlg = PikaUi.ProcBrowserDialog(use_header_bar=use_header_bar)
|
||||
Gtk.Window.set_title(dlg, _("Python Procedure Browser"))
|
||||
Gtk.Window.set_role(dlg, PROC_NAME)
|
||||
Gtk.Dialog.add_button(dlg, _("_Apply"), Gtk.ResponseType.APPLY)
|
||||
Gtk.Dialog.add_button(dlg, _("_Close"), Gtk.ResponseType.CLOSE)
|
||||
|
||||
Gtk.Dialog.set_default_response(self, Gtk.ResponseType.OK)
|
||||
PikaUi.Dialog.set_alternative_button_order_from_array(dlg,
|
||||
[ Gtk.ResponseType.CLOSE,
|
||||
Gtk.ResponseType.APPLY ])
|
||||
|
||||
dlg.connect('response', self.browse_response)
|
||||
dlg.connect('row-activated',
|
||||
lambda dlg: dlg.response(Gtk.ResponseType.APPLY))
|
||||
|
||||
self.browse_dlg = dlg
|
||||
|
||||
Gtk.Window.present(self.browse_dlg)
|
||||
|
||||
def save_response(self, dlg, response_id):
|
||||
if response_id == Gtk.ResponseType.DELETE_EVENT:
|
||||
self.save_dlg = None
|
||||
return
|
||||
elif response_id == Gtk.ResponseType.OK:
|
||||
filename = dlg.get_filename()
|
||||
|
||||
try:
|
||||
logfile = open(filename, 'w')
|
||||
except IOError as e:
|
||||
Pika.message(_("Could not open '%s' for writing: %s") %
|
||||
(filename, e.strerror))
|
||||
return
|
||||
|
||||
buffer = self.cons.buffer
|
||||
|
||||
start = buffer.get_start_iter()
|
||||
end = buffer.get_end_iter()
|
||||
|
||||
log = buffer.get_text(start, end, False)
|
||||
|
||||
try:
|
||||
logfile.write(log)
|
||||
logfile.close()
|
||||
except IOError as e:
|
||||
Pika.message(_("Could not write to '%s': %s") %
|
||||
(filename, e.strerror))
|
||||
return
|
||||
|
||||
Gtk.Widget.hide(dlg)
|
||||
|
||||
def save_dialog(self):
|
||||
if not self.save_dlg:
|
||||
dlg = Gtk.FileChooserDialog()
|
||||
Gtk.Window.set_title(dlg, _("Save Python-Fu Console Output"))
|
||||
Gtk.Window.set_transient_for(dlg, self)
|
||||
Gtk.Dialog.add_button(dlg, _("_Cancel"), Gtk.ResponseType.CANCEL)
|
||||
Gtk.Dialog.add_button(dlg, _("_Save"), Gtk.ResponseType.OK)
|
||||
Gtk.Dialog.set_default_response(self, Gtk.ResponseType.OK)
|
||||
PikaUi.Dialog.set_alternative_button_order_from_array(dlg,
|
||||
[ Gtk.ResponseType.OK,
|
||||
Gtk.ResponseType.CANCEL ])
|
||||
|
||||
dlg.connect('response', self.save_response)
|
||||
|
||||
self.save_dlg = dlg
|
||||
|
||||
self.save_dlg.present()
|
||||
|
||||
def run(self):
|
||||
Gtk.Widget.show_all(self)
|
||||
Gtk.main()
|
||||
|
||||
ConsoleDialog().run()
|
||||
|
||||
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
|
||||
class PythonConsole (Pika.PlugIn):
|
||||
## Properties: parameters ##
|
||||
@GObject.Property(type=Pika.RunMode,
|
||||
default=Pika.RunMode.NONINTERACTIVE,
|
||||
nick=_("Run mode"), blurb=_("The run mode"))
|
||||
def run_mode(self):
|
||||
"""Read-write integer property."""
|
||||
return self.runmode
|
||||
|
||||
@run_mode.setter
|
||||
def run_mode(self, runmode):
|
||||
self.runmode = runmode
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, name):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ PROC_NAME ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
if name == PROC_NAME:
|
||||
procedure = Pika.Procedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
run, None)
|
||||
procedure.set_menu_label(_("Python _Console"))
|
||||
procedure.set_documentation(_("Interactive PIKA Python interpreter"),
|
||||
_("Type in commands and see results"),
|
||||
"")
|
||||
procedure.set_attribution("James Henstridge",
|
||||
"James Henstridge",
|
||||
"1997-1999")
|
||||
procedure.add_argument_from_property(self, "run-mode")
|
||||
procedure.add_menu_path ("<Image>/Filters/Development/Python-Fu")
|
||||
|
||||
return procedure
|
||||
return None
|
||||
|
||||
Pika.main(PythonConsole.__gtype__, sys.argv)
|
59
plug-ins/python/python-eval.py
Normal file
59
plug-ins/python/python-eval.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Pika-Python - allows the writing of Pika plugins in Python.
|
||||
# Copyright (C) 2006 Manish Singh <yosh@gimp.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def code_eval(procedure, run_mode, code, args, data):
|
||||
if code == '-':
|
||||
code = sys.stdin.read()
|
||||
exec(code, globals())
|
||||
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
|
||||
|
||||
class PythonEval (Pika.PlugIn):
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return ['python-fu-eval']
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = Pika.BatchProcedure.new(self, name, "Python 3",
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
code_eval, None)
|
||||
procedure.set_documentation ("Evaluate Python code",
|
||||
"Evaluate python code under the python interpreter (primarily for batch mode)",
|
||||
name)
|
||||
|
||||
procedure.set_attribution("Manish Singh",
|
||||
"Manish Singh",
|
||||
"2006")
|
||||
|
||||
return procedure
|
||||
|
||||
|
||||
Pika.main(PythonEval.__gtype__, sys.argv)
|
2373
plug-ins/python/spyro-plus.py
Normal file
2373
plug-ins/python/spyro-plus.py
Normal file
File diff suppressed because it is too large
Load Diff
182
plug-ins/python/test-dialog.py
Normal file
182
plug-ins/python/test-dialog.py
Normal file
@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import gi
|
||||
gi.require_version('Pika', '3.0')
|
||||
from gi.repository import Pika
|
||||
gi.require_version('PikaUi', '3.0')
|
||||
from gi.repository import PikaUi
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
import time
|
||||
import sys
|
||||
|
||||
|
||||
'''
|
||||
A Python plugin.
|
||||
Tests PikaProcedureDialog.
|
||||
|
||||
Temporarily, just testing widgets for subclasses of PikaResource.
|
||||
Temporarily, just testing Brush subclass of Resource.
|
||||
FUTURE: For all the parameter types.
|
||||
Formerly PF_ constants, now all parameters for which PikaParamSpecs exist.
|
||||
|
||||
Not localized, no i18n
|
||||
'''
|
||||
|
||||
|
||||
def process_args(brush, font, gradient, palette, pattern):
|
||||
'''
|
||||
Test the args are sane.
|
||||
'''
|
||||
assert brush is not None
|
||||
assert isinstance(brush, Pika.Brush)
|
||||
id = brush.get_id()
|
||||
name = brush.get_name()
|
||||
assert id is not None
|
||||
msg = "Brush: {} (ID: {})".format(name, id)
|
||||
print(msg)
|
||||
Pika.message(msg)
|
||||
|
||||
assert font is not None
|
||||
assert isinstance(font, Pika.Font)
|
||||
id = font.get_id()
|
||||
name = font.get_name()
|
||||
assert id is not None
|
||||
msg = "Font: {} (ID: {})".format(name, id)
|
||||
print(msg)
|
||||
Pika.message(msg)
|
||||
|
||||
assert gradient is not None
|
||||
assert isinstance(gradient, Pika.Gradient)
|
||||
id = gradient.get_id()
|
||||
name = gradient.get_name()
|
||||
assert id is not None
|
||||
msg = "Gradient: {} (ID: {})".format(name, id)
|
||||
print(msg)
|
||||
Pika.message(msg)
|
||||
|
||||
assert palette is not None
|
||||
assert isinstance(palette, Pika.Palette)
|
||||
id = palette.get_id()
|
||||
name = palette.get_name()
|
||||
assert id is not None
|
||||
msg = "Palette: {} (ID: {})".format(name, id)
|
||||
print(msg)
|
||||
Pika.message(msg)
|
||||
|
||||
assert pattern is not None
|
||||
assert isinstance(pattern, Pika.Pattern)
|
||||
id = pattern.get_id()
|
||||
name = pattern.get_name()
|
||||
assert id is not None
|
||||
msg = "Pattern: {} (ID: {})".format(name, id)
|
||||
print(msg)
|
||||
Pika.message(msg)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def test_dialog(procedure, run_mode, image, n_drawables, drawables, args, data):
|
||||
'''
|
||||
Just a standard shell for a plugin.
|
||||
'''
|
||||
config = procedure.create_config()
|
||||
config.begin_run(image, run_mode, args)
|
||||
|
||||
if run_mode == Pika.RunMode.INTERACTIVE:
|
||||
PikaUi.init('python-fu-test-dialog')
|
||||
dialog = PikaUi.ProcedureDialog(procedure=procedure, config=config)
|
||||
dialog.fill(None)
|
||||
if not dialog.run():
|
||||
dialog.destroy()
|
||||
config.end_run(Pika.PDBStatusType.CANCEL)
|
||||
return procedure.new_return_values(Pika.PDBStatusType.CANCEL, GLib.Error())
|
||||
else:
|
||||
dialog.destroy()
|
||||
|
||||
brush = config.get_property('brush')
|
||||
font = config.get_property('font')
|
||||
gradient = config.get_property('gradient')
|
||||
palette = config.get_property('palette')
|
||||
pattern = config.get_property('pattern')
|
||||
|
||||
Pika.context_push()
|
||||
|
||||
process_args(brush, font, gradient, palette, pattern)
|
||||
|
||||
Pika.displays_flush()
|
||||
|
||||
Pika.context_pop()
|
||||
|
||||
config.end_run(Pika.PDBStatusType.SUCCESS)
|
||||
|
||||
return procedure.new_return_values(Pika.PDBStatusType.SUCCESS, GLib.Error())
|
||||
|
||||
|
||||
class TestDialogPlugin (Pika.PlugIn):
|
||||
## Parameters ##
|
||||
# See comments about this in foggify.py, from which we borrowed
|
||||
|
||||
brush = GObject.Property(type = Pika.Brush,
|
||||
nick = "_Brush",
|
||||
blurb = "Brush")
|
||||
font = GObject.Property(type = Pika.Font,
|
||||
nick = "_Font",
|
||||
blurb = "Font")
|
||||
gradient = GObject.Property(type = Pika.Gradient,
|
||||
nick = "_Gradient",
|
||||
blurb = "Gradient")
|
||||
palette = GObject.Property(type = Pika.Palette,
|
||||
nick = "_Palette",
|
||||
blurb = "Palette")
|
||||
pattern = GObject.Property(type = Pika.Pattern,
|
||||
nick = "Pa_ttern",
|
||||
blurb = "Pattern")
|
||||
|
||||
# FUTURE all other Pika classes that have PikaParamSpecs
|
||||
|
||||
## PikaPlugIn virtual methods ##
|
||||
def do_set_i18n(self, procname):
|
||||
return True, 'pika30-python', None
|
||||
|
||||
def do_query_procedures(self):
|
||||
return [ 'python-fu-test-dialog' ]
|
||||
|
||||
def do_create_procedure(self, name):
|
||||
procedure = Pika.ImageProcedure.new(self, name,
|
||||
Pika.PDBProcType.PLUGIN,
|
||||
test_dialog, None)
|
||||
procedure.set_sensitivity_mask (Pika.ProcedureSensitivityMask.NO_IMAGE)
|
||||
procedure.set_documentation ("Test dialog",
|
||||
"Test dialog",
|
||||
name)
|
||||
procedure.set_menu_label("Test dialog...")
|
||||
procedure.set_attribution("Lloyd Konneker",
|
||||
"Lloyd Konneker",
|
||||
"2022")
|
||||
# Top level menu "Test"
|
||||
procedure.add_menu_path ("<Image>/Test")
|
||||
|
||||
procedure.add_argument_from_property(self, "brush")
|
||||
procedure.add_argument_from_property(self, "font")
|
||||
procedure.add_argument_from_property(self, "gradient")
|
||||
procedure.add_argument_from_property(self, "palette")
|
||||
procedure.add_argument_from_property(self, "pattern")
|
||||
|
||||
return procedure
|
||||
|
||||
Pika.main(TestDialogPlugin.__gtype__, sys.argv)
|
Reference in New Issue
Block a user