Codebase list libmypaint / HEAD generate.py
HEAD

Tree @HEAD (Download .tar.gz)

generate.py @HEADraw · history · blame

#!/usr/bin/env python
# libmypaint - The MyPaint Brush Library
# Copyright (C) 2007-2012 Martin Renold <martinxyz@gmx.ch>
# Copyright (C) 2012-2016 by the MyPaint Development Team.
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


"Code generator, part of the build process."

from __future__ import absolute_import, division, print_function

import os
import sys
from os.path import basename
import json
from collections import namedtuple

PY3 = sys.version_info >= (3,)

# A basic translator comment is generated for each string,
# noting whether it is an input or a setting, and for tooltips
# stating which input/setting it belongs to.
#
# In addition to that, more descriptive addendums can be added
# for individual strings using the tcomment_x attributes, where
# x is either 'name' or 'tooltip'.

_SETTINGS = []  # brushsettings.settings
_SETTING_ORDER = [
    "internal_name",  # cname
    "tcomment_name",  # comment for translators (optional)
    "displayed_name",   # name
    "constant",
    "minimum",  # min
    "default",
    "maximum",  # max
    "tcomment_tooltip",  # comment for translators (optional)
    "tooltip",
]
_INPUTS = []  # brushsettings.inputs
_INPUT_ORDER = [
    "id",   # name
    "hard_minimum",   # hard_min
    "soft_minimum",   # soft_min
    "normal",
    "soft_maximum",   # soft_max
    "hard_maximum",     # hard_max
    "tcomment_name",  # comment for translators (optional)
    "displayed_name",  # dname
    "tcomment_tooltip",  # comment for translators (optional)
    "tooltip",
]
_STATES = []   # brushsettings.states


class _BrushSetting (namedtuple("_BrushSetting", _SETTING_ORDER)):

    def validate(self):
        msg = "Failed to validate %s: %r" % (self.internal_name, self)
        if self.minimum and self.maximum:
            assert (self.minimum <= self.default), msg
            assert (self.maximum >= self.default), msg
            assert (self.minimum < self.maximum), msg
        assert self.default is not None


class _BrushInput (namedtuple("_BrushInput", _INPUT_ORDER)):

    def validate(self):
        msg = "Failed to validate %s: %r" % (self.id, self)
        if self.hard_maximum is not None:
            assert (self.hard_maximum >= self.soft_maximum), msg
            assert (self.hard_maximum >= self.normal), msg
        if self.hard_minimum is not None:
            assert (self.hard_minimum <= self.soft_minimum), msg
            assert (self.hard_minimum <= self.normal), msg
        if None not in (self.hard_maximum, self.hard_minimum):
            assert (self.hard_minimum < self.hard_maximum), msg
        assert self.normal is not None
        assert (self.soft_minimum < self.soft_maximum), msg
        assert (self.soft_minimum <= self.normal), msg
        assert (self.soft_maximum >= self.normal), msg


def _init_globals_from_json(filename):
    """Populate global variables above from the canonical JSON definition."""

    def with_comments(d):
        d.setdefault('tcomment_name', None)
        d.setdefault('tcomment_tooltip', None)
        return d

    flag = "r" if PY3 else "rb"
    with open(filename, flag) as fp:
        defs = json.load(fp)
    for input_def in defs["inputs"]:
        input = _BrushInput(**with_comments(input_def))
        input.validate()
        _INPUTS.append(input)
    for setting_def in defs["settings"]:
        setting = _BrushSetting(**with_comments(setting_def))
        setting.validate()
        _SETTINGS.append(setting)
    for state_name in defs["states"]:
        _STATES.append(state_name)


def writefile(filename, s):
    """Write generated code if changed."""
    bn = basename(sys.argv[0])
    s = '// DO NOT EDIT - autogenerated by ' + bn + '\n\n' + s
    if os.path.exists(filename) and open(filename).read() == s:
        print('Checked {}: up to date, not rewritten'.format(filename))
    else:
        print('Writing {}'.format(filename))
        open(filename, 'w').write(s)


def generate_enum(enum_name, enum_prefix, count_name, items):

    indent = " " * 4
    begin = "typedef enum {\n"
    end = "} %s;\n" % enum_name

    entries = []
    for idx, name in items:
        entries.append(indent + enum_prefix + name)
    entries.append(indent + count_name)

    return begin + ",\n".join(entries) + "\n" + end


def generate_static_struct_array(struct_type, instance_name, entries_list):

    indent = " " * 4
    begin = "static %s %s[] = {\n" % (struct_type, instance_name)
    end = "};\n"

    entries = []
    for entry in entries_list:
        entries.append(indent + "{%s}" % (", ".join(entry)))
    entries.append("\n")

    return begin + ", \n".join(entries) + end


def stringify(value):
    value = value.replace("\n", "\\n")
    value = value.replace('"', '\\"')
    return "\"%s\"" % value


def floatify(value, positive_inf=True):
    if value is None:
        return "FLT_MAX" if positive_inf else "-FLT_MAX"
    return str(value)


def gettextify(value, comment=None):
    result = "N_(%s)" % stringify(value)
    if comment:
        assert isinstance(comment, str) or isinstance(comment, unicode)
        result = "/* %s */ %s" % (comment, result)
    return result


def boolify(value):
    return str("TRUE") if value else str("FALSE")


def tcomment(base_comment, addendum=None):
    comment = base_comment
    if addendum:
        comment = "{c} - {a}".format(c=comment, a=addendum)
    return comment


def tooltip_comment(name, name_type, addendum=None):
    comment = 'Tooltip for the "{n}" brush {t}'.format(
        n=name, t=name_type)
    return tcomment(comment, addendum)


def input_info_struct(i):
    name_comment = tcomment("Brush input", i.tcomment_name)
    _tooltip_comment = tooltip_comment(
        i.displayed_name, "input", i.tcomment_tooltip)
    return (
        stringify(i.id),
        floatify(i.hard_minimum, positive_inf=False),
        floatify(i.soft_minimum, positive_inf=False),
        floatify(i.normal),
        floatify(i.soft_maximum),
        floatify(i.hard_maximum),
        gettextify(i.displayed_name, name_comment),
        gettextify(i.tooltip, _tooltip_comment),
    )


def settings_info_struct(s):
    name_comment = tcomment("Brush setting", s.tcomment_name)
    _tooltip_comment = tooltip_comment(
        s.displayed_name, "setting", s.tcomment_tooltip)
    return (
        stringify(s.internal_name),
        gettextify(s.displayed_name, name_comment),
        boolify(s.constant),
        floatify(s.minimum, positive_inf=False),
        floatify(s.default),
        floatify(s.maximum),
        gettextify(s.tooltip, _tooltip_comment),
    )


def header_guard_name(file_name):
    alfa_num = "".join(map(lambda c: c if c.isalnum() else '_', file_name))
    return alfa_num.upper()


def header_guarded(file_name, header_content):
    guard_name = header_guard_name(file_name)
    guard = '#ifndef {guard}\n#define {guard}\n{content}\n#endif\n'
    return guard.format(guard=guard_name, content=header_content)


def generate_internal_settings_code():
    content = ''
    content += generate_static_struct_array(
        "MyPaintBrushSettingInfo",
        "settings_info_array",
        [settings_info_struct(i) for i in _SETTINGS],
    )
    content += "\n"
    content += generate_static_struct_array(
        "MyPaintBrushInputInfo",
        "inputs_info_array",
        [input_info_struct(i) for i in _INPUTS],
    )
    return content


def generate_public_settings_code():
    content = ''
    content += generate_enum(
        "MyPaintBrushInput",
        "MYPAINT_BRUSH_INPUT_",
        "MYPAINT_BRUSH_INPUTS_COUNT",
        enumerate([i.id.upper() for i in _INPUTS]),
    )
    content += '\n'
    content += generate_enum(
        "MyPaintBrushSetting",
        "MYPAINT_BRUSH_SETTING_",
        "MYPAINT_BRUSH_SETTINGS_COUNT",
        enumerate([i.internal_name.upper() for i in _SETTINGS]),
    )
    content += '\n'
    content += generate_enum(
        "MyPaintBrushState",
        "MYPAINT_BRUSH_STATE_",
        "MYPAINT_BRUSH_STATES_COUNT",
        enumerate([i.upper() for i in _STATES]),
    )
    content += '\n'
    return content


if __name__ == '__main__':
    _init_globals_from_json("brushsettings.json")
    script = sys.argv[0]
    try:
        public_header_file, internal_header_file = sys.argv[1:]
    except Exception:
        msg = "usage: {} PUBLICdotH INTERNALdotH".format(script)
        print(msg, file=sys.stderr)
        sys.exit(2)
    phf = public_header_file
    writefile(phf, header_guarded(phf, generate_public_settings_code()))
    ihf = internal_header_file
    writefile(ihf, header_guarded(ihf, generate_internal_settings_code()))