Commit 35357909 authored by Patrick Griffis's avatar Patrick Griffis

meson-templates: Various template improvements

This supports GNOME application templates (using Gtk Template, Gtk Application, etc)
for C, JavaScript, Python, and Vala.
parent 3b8fc650
......@@ -35,15 +35,18 @@ from gi.repository import (
_ = Ide.gettext
def get_module_data_path(name):
engine = Peas.Engine.get_default()
plugin = engine.get_plugin_info('meson_templates')
data_dir = plugin.get_data_dir()
return path.join(data_dir, name)
class LibraryTemplateProvider(GObject.Object, Ide.TemplateProvider):
def do_get_project_templates(self):
return [EmptyProjectTemplate()]
return [GnomeProjectTemplate(), LibraryProjectTemplate()]
class MesonTemplateLocator(Template.TemplateLocator):
license = None
......@@ -57,7 +60,7 @@ class MesonTemplateLocator(Template.TemplateLocator):
manager = GtkSource.LanguageManager.get_default()
language = manager.guess_language(filename, None)
if self.license == None or language == None:
if self.license is None or language is None:
return self.empty()
header = Ide.language_format_header(language, self.license)
......@@ -67,11 +70,6 @@ class MesonTemplateLocator(Template.TemplateLocator):
return super().do_locate(self, path)
# Map builder langs to meson ones
LANGUAGE_MAP = {
'c': 'c',
'c++': 'cpp',
}
class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
def __init__(self, id, name, icon_name, description, languages):
......@@ -110,7 +108,7 @@ class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
else:
self.language = 'c'
if self.language not in ('c', 'c++'):
if self.language not in ('c', 'javascript', 'python', 'vala'):
task.return_error(GLib.Error('Language %s not supported' %
self.language))
return
......@@ -129,35 +127,65 @@ class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
scope.get('template').assign_string(self.id)
name = params['name'].get_string().lower()
name_ = name.lower().replace('-','_')
name_ = name.lower().replace('-', '_')
scope.get('name').assign_string(name)
scope.get('name_').assign_string(name_)
scope.get('NAME').assign_string(name.upper().replace('-','_'))
# TODO: Support setting app id
appid = 'org.gnome.' + name.title()
appid_path = '/' + appid.replace('.', '/')
scope.get('appid').assign_string(appid)
scope.get('appid_path').assign_string(appid_path)
prefix = name if not name.endswith('-glib') else name[:-5]
PREFIX = prefix.upper().replace('-','_')
prefix_ = prefix.lower().replace('-','_')
PreFix = ''.join([word.capitalize() for word in prefix.lower().split('-')])
scope.get('prefix').assign_string(prefix)
scope.get('Prefix').assign_string(prefix.capitalize())
scope.get('PreFix').assign_string(PreFix)
scope.get('prefix_').assign_string(prefix_)
scope.get('PREFIX').assign_string(PREFIX)
enable_gnome = isinstance(self, GnomeProjectTemplate)
scope.get('project_version').assign_string('0.1.0')
scope.get('enable_i18n').assign_boolean(True)
scope.get('language').assign_string(LANGUAGE_MAP[self.language])
scope.get('enable_i18n').assign_boolean(enable_gnome)
scope.get('enable_gnome').assign_boolean(enable_gnome)
scope.get('language').assign_string(self.language)
scope.get('author').assign_string(author_name)
# Just avoiding dealing with template bugs
if self.language == 'c':
ui_file = prefix + '-window.ui'
else:
ui_file = 'window.ui'
scope.get('ui_file').assign_string(ui_file)
exec_name = appid if self.language == 'javascript' else name
scope.get('exec_name').assign_string(exec_name)
modes = {
'resources/src/hello.js.in': 0o750,
'resources/src/hello.py.in': 0o750,
'resources/meson_post_install.py': 0o750,
}
expands = {
'prefix': prefix,
'appid': appid,
'name_': name_,
'name': name,
}
files = {
# Build files
'resources/meson.build': 'meson.build',
'resources/src/meson.build': 'src/meson.build',
# Translations
'resources/po/LINGUAS': 'po/LINGUAS',
'resources/po/meson.build': 'po/meson.build',
'resources/po/POTFILES': 'po/POTFILES',
}
if self.language == 'c':
files['resources/src/main.c'] = 'src/main.c'
elif self.language == 'c++':
files['resources/src/main.c'] = 'src/main.cpp'
self.prepare_files(files)
if 'license_full' in params:
license_full_path = params['license_full'].get_string()
......@@ -177,7 +205,7 @@ class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
for src, dst in files.items():
destination = directory.get_child(dst % expands)
if src.startswith("resource://"):
if src.startswith('resource://'):
self.add_resource(src[11:], destination, scope, modes.get(src, 0))
else:
path = get_module_data_path(src)
......@@ -199,14 +227,82 @@ class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
task.return_error(GLib.Error(repr(exc)))
class EmptyProjectTemplate(MesonTemplate):
class GnomeProjectTemplate(MesonTemplate):
def __init__(self):
super().__init__(
'empty-meson',
_('Empty Meson Project'),
'gnome-app',
_('GNOME Application'),
'pattern-gnome',
_('Create a new GNOME application'),
['C', 'Python', 'JavaScript', 'Vala']
)
def prepare_files(self, files):
# Shared files
files['resources/flatpak.json'] = '%(appid)s.json'
files['resources/data/hello.desktop.in'] = 'data/%(appid)s.desktop.in'
files['resources/data/hello.appdata.xml.in'] = 'data/%(appid)s.appdata.xml.in'
files['resources/data/hello.gschema.xml'] = 'data/%(appid)s.gschema.xml'
files['resources/data/meson.build'] = 'data/meson.build'
files['resources/meson_post_install.py'] = 'meson_post_install.py'
files['resources/po/LINGUAS'] = 'po/LINGUAS'
files['resources/po/meson.build'] = 'po/meson.build'
files['resources/po/POTFILES'] = 'po/POTFILES'
window_ui_name = 'src/window.ui'
resource_name = 'src/%(prefix)s.gresource.xml'
meson_file = 'resources/src/meson-c-vala.build'
if self.language == 'c':
files['resources/src/main.c'] = 'src/main.c'
files['resources/src/window.c'] = 'src/%(prefix)s-window.c'
files['resources/src/window.h'] = 'src/%(prefix)s-window.h'
window_ui_name = 'src/%(prefix)s-window.ui'
elif self.language == 'vala':
files['resources/src/main.vala'] = 'src/main.vala'
files['resources/src/window.vala'] = 'src/window.vala'
elif self.language == 'javascript':
files['resources/src/main.js'] = 'src/main.js'
files['resources/src/hello.js.in'] = 'src/%(appid)s.in'
files['resources/src/window.js'] = 'src/window.js'
files['resources/src/hello.src.gresource.xml'] = 'src/%(appid)s.src.gresource.xml'
resource_name = 'src/%(appid)s.data.gresource.xml'
meson_file = 'resources/src/meson-js.build'
elif self.language == 'python':
files['resources/src/hello.py.in'] = 'src/%(name)s.in'
files['resources/src/gi_composites.py'] = 'src/gi_composites.py'
files['resources/src/__init__.py'] = 'src/__init__.py'
files['resources/src/window.py'] = 'src/window.py'
files['resources/src/main.py'] = 'src/main.py'
meson_file = 'resources/src/meson-py.build'
files['resources/src/hello.gresource.xml'] = resource_name
files['resources/src/window.ui'] = window_ui_name
files[meson_file] = 'src/meson.build'
class LibraryProjectTemplate(MesonTemplate):
def __init__(self):
super().__init__(
'shared-library',
_("Shared Library"),
'pattern-library',
_('Create a new empty meson project'),
['C', 'C++']
_("Create a new project with a shared library"),
['C']
)
def prepare_files(self, files):
if self.language == 'c':
files['resources/src/meson-clib.build'] = 'src/meson.build'
files['resources/src/hello.h'] = 'src/%(name)s.h'
files['resources/src/hello-version.h.in'] = 'src/%(prefix)s-version.h.in'
class EmptyProjectTemplate(MesonTemplate):
def __init__(self):
super().__init__(
'empty',
_('Empty Project'),
'pattern-library',
_('Create a new empty project'),
['C']
)
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>{{appid}}.desktop</id>
<licence>CC0</licence>
<description>
</description>
</component>
[Desktop Entry]
Name={{name}}
Exec={{exec_name}}
Terminal=false
Type=Application
Categories=GTK;
StartupNotify=true
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="{{name}}">
<schema id="{{appid}}" path="{{appid_path}}/">
</schema>
</schemalist>
desktop_file = i18n.merge_file(
input: '{{appid}}.desktop.in',
output: '{{appid}}.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'applications')
)
desktop_utils = find_program('desktop-file-validate', required: false)
if desktop_utils.found()
test('Validate desktop file', desktop_utils,
args: [desktop_file]
)
endif
appstream_file = i18n.merge_file(
input: '{{appid}}.appdata.xml.in',
output: '{{appid}}.appdata.xml',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'appdata')
)
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test('Validate appstream file', appstream_util,
args: ['validate', appstream_file]
)
endif
install_data('{{appid}}.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]
)
endif
{
"app-id": "{{appid}}",
"runtime": "org.gnome.Platform",
"runtime-version": "3.24",
"sdk": "org.gnome.Sdk",
"command": "{{exec_name}}",
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--filesystem=xdg-run/dconf",
"--filesystem=~/.config/dconf:ro",
"--talk-name=ca.desrt.dconf",
"--env=DCONF_USER_CONFIG_DIR=.config/dconf"
],
"build-options": {
"cflags": "-O2 -g",
"cxxflags": "-O2 -g",
"env": {
"V": "1"
}
},
"cleanup": [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
{{if language == "vala"}} "/share/vala",{{end}}
"*.la",
"*.a"
],
"modules": [
{
"name": "{{name}}",
"buildsystem": "meson",
"config-opts": [
"--libdir=lib"
],
"builddir": true,
"sources": [
{
"type": "git",
"url": "file://{{project_path}}"
}
]
}
]
}
project('{{name}}', '{{language}}',
project('{{name}}',{{if language == "c"}}'c',{{else if language == "vala"}}'c', 'vala',{{end}}
version: '{{project_version}}',
meson_version: '>= 0.36.0',
meson_version: '>= 0.40.0',
)
{{if enable_i18n}}
i18n = import('i18n'){{end}}
config_h = configuration_data()
config_h.set_quoted('GETTEXT_PACKAGE', '{{name}}')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir')))
{{if language == "c"}}config_h = configuration_data()
{{if enable_i18n}}config_h.set_quoted('GETTEXT_PACKAGE', '{{name}}')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))){{end}}
configure_file(
output: 'config.h',
output: '{{prefix}}-config.h',
configuration: config_h,
)
add_global_arguments([
'-DHAVE_CONFIG_H',
add_project_arguments([
'-I' + meson.build_root(),
], language: '{{language}}')
], language: 'c')
{{end}}
{{if enable_gnome}}
subdir('data'){{end}}
subdir('src')
{{if enable_i18n}}subdir('po'){{end}}
{{if enable_gnome}}
meson.add_install_script('meson_post_install.py'){{end}}
#!/usr/bin/env python3
from os import environ, path
from subprocess import call
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
datadir = path.join(prefix, 'share')
destdir = environ.get('DESTDIR', '')
# Package managers set this so we don't need to run
if not destdir:
print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
print('Compiling GSettings schemas...')
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
src/{{if language == "c"}}main.c{{else if language == "cpp"}}main.cpp{{end}}
data/{{appid}}.desktop.in
data/{{appid}}.appdata.xml.in
data/{{appid}}.gschema.xml
src/{{ui_file}}
{{if language == "c"}}src/main.c
src/{{prefix}}-window.c{{end}}
{{if language == "vala"}}src/main.vala
src/window.vala{{end}}
{{if language == "javascript"}}src/main.js
src/window.js{{end}}
{{if language == "python"}}src/main.py
src/window.py{{end}}
i18n = import('i18n')
langs = [
# TODO: Translate app
]
if langs.length() > 0
i18n.gettext('{{name}}',
languages: langs,
args: [
'--from-code=UTF-8',
'--keyword=g_dngettext:2,3',
'--add-comments',
],
)
endif
i18n.gettext('{{name}}', preset: 'glib')
#
# Copyright (C) 2015 Dustin Spicuzza <dustin@virtualroadside.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
# USA
from os.path import abspath, join
import inspect
import warnings
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gtk
__all__ = ['GtkTemplate']
class GtkTemplateWarning(UserWarning):
pass
def _connect_func(builder, obj, signal_name, handler_name,
connect_object, flags, cls):
'''Handles GtkBuilder signal connect events'''
if connect_object is None:
extra = ()
else:
extra = (connect_object,)
# The handler name refers to an attribute on the template instance,
# so ask GtkBuilder for the template instance
template_inst = builder.get_object(cls.__gtype_name__)
if template_inst is None: # This should never happen
errmsg = "Internal error: cannot find template instance! obj: %s; " \
"signal: %s; handler: %s; connect_obj: %s; class: %s" % \
(obj, signal_name, handler_name, connect_object, cls)
warnings.warn(errmsg, GtkTemplateWarning)
return
handler = getattr(template_inst, handler_name)
if flags == GObject.ConnectFlags.AFTER:
obj.connect_after(signal_name, handler, *extra)
else:
obj.connect(signal_name, handler, *extra)
template_inst.__connected_template_signals__.add(handler_name)
def _register_template(cls, template_bytes):
'''Registers the template for the widget and hooks init_template'''
# This implementation won't work if there are nested templates, but
# we can't do that anyways due to PyGObject limitations so it's ok
if not hasattr(cls, 'set_template'):
raise TypeError("Requires PyGObject 3.13.2 or greater")
cls.set_template(template_bytes)
bound_methods = set()
bound_widgets = set()
# Walk the class, find marked callbacks and child attributes
for name in dir(cls):
o = getattr(cls, name, None)
if inspect.ismethod(o):
if hasattr(o, '_gtk_callback'):
bound_methods.add(name)
# Don't need to call this, as connect_func always gets called
#cls.bind_template_callback_full(name, o)
elif isinstance(o, _Child):
cls.bind_template_child_full(name, True, 0)
bound_widgets.add(name)
# Have to setup a special connect function to connect at template init
# because the methods are not bound yet
cls.set_connect_func(_connect_func, cls)
cls.__gtemplate_methods__ = bound_methods
cls.__gtemplate_widgets__ = bound_widgets
base_init_template = cls.init_template
cls.init_template = lambda s: _init_template(s, cls, base_init_template)
def _init_template(self, cls, base_init_template):
'''This would be better as an override for Gtk.Widget'''
# TODO: could disallow using a metaclass.. but this is good enough
# .. if you disagree, feel free to fix it and issue a PR :)
if self.__class__ is not cls:
raise TypeError("Inheritance from classes with @GtkTemplate decorators "
"is not allowed at this time")
connected_signals = set()
self.__connected_template_signals__ = connected_signals
base_init_template(self)
for name in self.__gtemplate_widgets__:
widget = self.get_template_child(cls, name)
self.__dict__[name] = widget
if widget is None:
# Bug: if you bind a template child, and one of them was
# not present, then the whole template is broken (and
# it's not currently possible for us to know which
# one is broken either -- but the stderr should show
# something useful with a Gtk-CRITICAL message)
raise AttributeError("A missing child widget was set using "
"GtkTemplate.Child and the entire "
"template is now broken (widgets: %s)" %
', '.join(self.__gtemplate_widgets__))
for name in self.__gtemplate_methods__.difference(connected_signals):
errmsg = ("Signal '%s' was declared with @GtkTemplate.Callback " +
"but was not present in template") % name
warnings.warn(errmsg, GtkTemplateWarning)
# TODO: Make it easier for IDE to introspect this
class _Child(object):
'''
Assign this to an attribute in your class definition and it will
be replaced with a widget defined in the UI file when init_template
is called
'''
__slots__ = []
@staticmethod
def widgets(count):
'''
Allows declaring multiple widgets with less typing::
button \
label1 \
label2 = GtkTemplate.Child.widgets(3)
'''
return [_Child() for _ in range(count)]
class _GtkTemplate(object):
'''
Use this class decorator to signify that a class is a composite
widget which will receive widgets and connect to signals as
defined in a UI template. You must call init_template to
cause the widgets/signals to be initialized from the template::
@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):
def __init__(self):
super(Foo, self).__init__()
self.init_template()
The 'ui' parameter can either be a file path or a GResource resource
path::
@GtkTemplate(ui='/org/example/foo.ui')
class Foo(Gtk.Box):
pass
To connect a signal to a method on your instance, do::
@GtkTemplate.Callback
def on_thing_happened(self, widget):
pass
To create a child attribute that is retrieved from your template,
add this to your class definition::
@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):
widget = GtkTemplate.Child()
Note: This is implemented as a class decorator, but if it were
included with PyGI I suspect it might be better to do this
in the GObject metaclass (or similar) so that init_template
can be called automatically instead of forcing the user to do it.
.. note:: Due to limitations in PyGObject, you may not inherit from
python objects that use the GtkTemplate decorator.
'''
__ui_path__ = None
@staticmethod
def Callback(f):
'''
Decorator that designates a method to be attached to a signal from
the template
'''
f._gtk_callback = True
return f
Child = _Child
@staticmethod
def set_ui_path(*path):
'''
If using file paths instead of resources, call this *before*
loading anything that uses GtkTemplate, or it will fail to load
your template file
:param path: one or more path elements, will be joined together
to create the final path
TODO: Alternatively, could wait until first class instantiation
before registering templates? Would need a metaclass...
'''
_GtkTemplate.__ui_path__ = abspath(join(*path))
def __init__(self, ui):
self.ui = ui
def __call__(self, cls):
if not issubclass(cls, Gtk.Widget):
raise TypeError("Can only use @GtkTemplate on Widgets")
# Nested templates don't work
if hasattr(cls, '__gtemplate_methods__'):
raise TypeError("Cannot nest template classes")
# Load the template either from a resource path or a file
# - Prefer the resource path first
try:
template_bytes = Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE)