├── po ├── LINGUAS ├── POTFILES.skip ├── POTFILES ├── meson.build └── it.po ├── .gitignore ├── NEWS ├── data ├── application.css ├── com.example.Gtk.JSApplication.service.in ├── com.example.Gtk.JSApplication.busname ├── com.example.Gtk.JSApplication.desktop.in ├── com.example.Gtk.JSApplication.data.gresource.xml ├── com.example.Gtk.JSApplication.gschema.xml ├── app-menu.ui ├── com.example.Gtk.JSApplication.appdata.xml.in ├── meson.build └── main.ui ├── src ├── com.example.Gtk.JSApplication.in ├── com.example.Gtk.JSApplication.src.gresource.xml ├── meson.build ├── main.js ├── util.js └── window.js ├── tests ├── smoke_test.py └── testutil.py ├── meson └── meson_post_install.py ├── meson.build ├── com.example.Gtk.JSApplication.json ├── README.md ├── COPYING └── .eslintrc.yml /po/LINGUAS: -------------------------------------------------------------------------------- 1 | it 2 | -------------------------------------------------------------------------------- /po/POTFILES.skip: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .flatpak-builder/ 2 | 3 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 0.1 2 | === 3 | 4 | * Initial release 5 | -------------------------------------------------------------------------------- /data/application.css: -------------------------------------------------------------------------------- 1 | .big-label { 2 | font-size: 4em; 3 | font-weight: bold; 4 | } 5 | -------------------------------------------------------------------------------- /data/com.example.Gtk.JSApplication.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=@PACKAGE_NAME@ 3 | Exec=@pkgdatadir@/@PACKAGE_NAME@ --gapplication-service 4 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/app-menu.ui 2 | data/main.ui 3 | data/com.example.Gtk.JSApplication.desktop.in 4 | data/com.example.Gtk.JSApplication.appdata.xml.in 5 | data/com.example.Gtk.JSApplication.gschema.xml 6 | src/main.js 7 | src/window.js 8 | -------------------------------------------------------------------------------- /src/com.example.Gtk.JSApplication.in: -------------------------------------------------------------------------------- 1 | #!@GJS@ 2 | imports.package.init({ 3 | name: 'com.example.Gtk.JSApplication', 4 | version: '@PACKAGE_VERSION@', 5 | prefix: '@prefix@', 6 | libdir: '@libdir@', 7 | }); 8 | imports.package.run(imports.main); 9 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | langs = [ 2 | 'it' 3 | ] 4 | 5 | if langs.length() > 0 6 | intl.gettext(GETTEXT_PACKAGE, 7 | languages: langs, 8 | args: [ 9 | '--from-code=UTF-8', 10 | '--keyword=g_dngettext:2,3', 11 | '--add-comments', 12 | ], 13 | ) 14 | endif 15 | -------------------------------------------------------------------------------- /src/com.example.Gtk.JSApplication.src.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | main.js 5 | util.js 6 | window.js 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/com.example.Gtk.JSApplication.busname: -------------------------------------------------------------------------------- 1 | [Unit] 2 | # Optional 3 | Description=com.example.Gtk.JSApplication 4 | # Optional (obviously) 5 | Documentation=http://www.example.com/gtk-js-app/ 6 | 7 | [BusName] 8 | # Optional 9 | Name=com.example.Gtk.JSApplication 10 | # Optional 11 | Service=com.example.Gtk.JSApplication.service -------------------------------------------------------------------------------- /data/com.example.Gtk.JSApplication.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=JS Application 4 | Comment=Demo JS Application and template 5 | Icon=com.example.Gtk.JSApplication 6 | Exec=gapplication launch com.example.Gtk.JSApplication 7 | DBusActivatable=true 8 | StartupNotify=true 9 | Keywords=gtk;gjs;demo;development; 10 | -------------------------------------------------------------------------------- /data/com.example.Gtk.JSApplication.data.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | app-menu.ui 5 | main.ui 6 | application.css 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/com.example.Gtk.JSApplication.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Show exclamation mark 6 | 7 | Use the exclamation mark at the end of "Hello world". 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | app_resource = gnome.compile_resources(app_id + '.src', 2 | app_id + '.src.gresource.xml', 3 | source_dir: '.', 4 | gresource_bundle: true, 5 | install: true, 6 | install_dir : pkgdatadir) 7 | 8 | app_launcher = configure_file( 9 | output : app_id, 10 | input : app_id + '.in', 11 | configuration: app_configuration) 12 | install_data(app_launcher, 13 | install_dir: get_option('bindir'), 14 | install_mode: 'rwxr-xr-x' 15 | ) 16 | 17 | run_target('devel', command: [gjs, app_launcher], 18 | depends: [app_resource, data_resource, compile_local_schemas]) 19 | -------------------------------------------------------------------------------- /tests/smoke_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from testutil import * 4 | 5 | from gi.repository import Gio, GLib 6 | 7 | import os, sys 8 | import pyatspi 9 | from dogtail import tree 10 | from dogtail import utils 11 | from dogtail.procedural import * 12 | 13 | def active(widget): 14 | return widget.getState().contains(pyatspi.STATE_ARMED) 15 | def visible(widget): 16 | return widget.getState().contains(pyatspi.STATE_VISIBLE) 17 | 18 | init() 19 | try: 20 | app = start() 21 | print "app started" 22 | assert app is not None 23 | 24 | # test something 25 | finally: 26 | print "tearing down" 27 | fini() 28 | -------------------------------------------------------------------------------- /meson/meson_post_install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pathlib 5 | import subprocess 6 | 7 | prefix = pathlib.Path(os.environ.get('MESON_INSTALL_PREFIX', '/usr/local')) 8 | datadir = prefix / 'share' 9 | destdir = os.environ.get('DESTDIR', '') 10 | 11 | if not destdir: 12 | print('Compiling gsettings schemas...') 13 | subprocess.call(['glib-compile-schemas', str(datadir / 'glib-2.0' / 'schemas')]) 14 | 15 | print('Updating icon cache...') 16 | subprocess.call(['gtk-update-icon-cache', '-qtf', str(datadir / 'icons' / 'hicolor')]) 17 | 18 | print('Updating desktop database...') 19 | subprocess.call(['update-desktop-database', '-q', str(datadir / 'applications')]) 20 | -------------------------------------------------------------------------------- /data/app-menu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | _New 6 | win.new 7 | <Primary>n 8 | 9 |
10 |
11 | 12 | win.about 13 | About Example 14 | 15 | 16 | app.quit 17 | Quit 18 | <Primary>q 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('gtk-js-app', 'c', 2 | version: '1.0.0', 3 | meson_version: '>= 0.50.0', 4 | ) 5 | 6 | app_id = 'com.example.Gtk.JSApplication' 7 | 8 | gnome = import('gnome') 9 | intl = import('i18n') 10 | 11 | gjs = find_program('gjs') 12 | GETTEXT_PACKAGE = app_id 13 | 14 | app_configuration = configuration_data() 15 | 16 | app_configuration.set('GJS', gjs.path()) 17 | app_configuration.set('PACKAGE_NAME', app_id) 18 | app_configuration.set('PACKAGE_VERSION', meson.project_version()) 19 | app_configuration.set('prefix', get_option('prefix')) 20 | 21 | pkgdatadir = join_paths(get_option('datadir'), app_id) 22 | app_configuration.set('libdir', join_paths(get_option('prefix'), get_option('libdir'))) 23 | app_configuration.set('pkgdatadir', pkgdatadir) 24 | 25 | subdir('data') 26 | subdir('src') 27 | subdir('po') 28 | meson.add_install_script('meson/meson_post_install.py') 29 | -------------------------------------------------------------------------------- /po/it.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: my-js-app 0.1\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-02-22 03:56+0100\n" 12 | "PO-Revision-Date: 2013-02-22 03:58+0100\n" 13 | "Last-Translator: Giovanni Campagna \n" 14 | "Language-Team: Italian \n" 15 | "Language: it\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: ../data/my-js-app.desktop.in.h:1 21 | msgid "My JS Application" 22 | msgstr "La mia applicazione JS" 23 | 24 | #: ../data/my-js-app.desktop.in.h:2 25 | msgid "Application Demo" 26 | msgstr "Demo Applicazione" 27 | 28 | #: ../data/my-js-app.desktop.in.h:3 29 | msgid "js;demo;app" 30 | msgstr "js;demo;app" 31 | 32 | #: ../src/main.js:33 33 | msgid "My JS Application started" 34 | msgstr "La mia applicazione JS è avviata" 35 | -------------------------------------------------------------------------------- /com.example.Gtk.JSApplication.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id" : "com.example.Gtk.JSApplication", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "40", 5 | "branch" : "stable", 6 | "sdk" : "org.gnome.Sdk", 7 | "command" : "/app/bin/com.example.Gtk.JSApplication", 8 | "finish-args" : [ 9 | "--share=ipc", 10 | "--socket=fallback-x11", 11 | "--socket=wayland", 12 | "--share=network" 13 | ], 14 | "cleanup" : [ 15 | "/include", 16 | "/lib/pkgconfig", 17 | "/share/pkgconfig", 18 | "/share/aclocal", 19 | "/man", 20 | "/share/man", 21 | "/share/gtk-doc", 22 | "/share/vala", 23 | "*.la", 24 | "*.a" 25 | ], 26 | "modules" : [ 27 | { 28 | "name" : "gtk-js-app", 29 | "sources" : [ 30 | { 31 | "type" : "git", 32 | "url" : "https://github.com/gcampax/gtk-js-app" 33 | } 34 | ] 35 | } 36 | ], 37 | "build-options" : { 38 | "env" : { 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /data/com.example.Gtk.JSApplication.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.example.Gtk.JSApplication.desktop 4 | CC0 5 | 6 | <_p> 7 | A small but complete template to build a Gtk+ JS application, 8 | using all the GNOME technologies expected from a modern app, 9 | as well as gjs as the underlying language. 10 | 11 | <_p> 12 | It is meant to showcase working examples (that can be readily transfered into 13 | a real application) for the following GNOME and Freedesktop techologies: 14 | 15 |
    16 | <_li>The build system (Meson based) 17 | <_li>Configuring and using GSettings 18 | <_li>Using GResources for source and data 19 | <_li>Desktop file and appdata 20 |
21 |
22 | 23 | https://www.example.com/gtk-js-app/screenshots/large.png 24 | 25 | https://www.example.com/gtk-js-app 26 | gcampagna_at_src.gnome.org 27 |
28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About My JS Application 2 | ======================= 3 | 4 | My JS Application, as the name suggests, is nothing but an 5 | application written in JS. It installs, and it runs. 6 | It's meant as an example of the GNOME application platform, 7 | and in particular Gtk and the Gjs package system. 8 | 9 | It should also work as a template for developing a real 10 | application, ie. one that does something, without spending 11 | all the time doing build system configuration. 12 | 13 | Features 14 | ======== 15 | 16 | My JS Application most of the familiar UI you'd expect 17 | from a core application: it has a main window with a header 18 | bar, it has a search bar and it has multiple page stack. 19 | 20 | On the developer side, the most prominent feature is 21 | that My JS Application runs uninstalled with a special 22 | Meson target. You should be able to run 23 | ``` 24 | meson _build 25 | meson compile -C _build devel 26 | ``` 27 | and everything should just work, without having to run `ninja install`. 28 | 29 | Also, it features an util module, which deals with 30 | GtkBuilder and GtkCssProvider, again providing 31 | transparency between the installed and not installed cases. 32 | 33 | License 34 | ======= 35 | 36 | The package is under a 3-clause BSD license, to 37 | make it suitable for inclusion in any application. 38 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Giovanni Campagna 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * Neither the name of the GNOME Foundation nor the 11 | names of its contributors may be used to endorse or promote products 12 | derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | data_resource = gnome.compile_resources(app_id + '.data', 2 | app_id + '.data.gresource.xml', 3 | source_dir: '.', 4 | gresource_bundle: true, 5 | install: true, 6 | install_dir : pkgdatadir) 7 | 8 | appsdir = join_paths(get_option('datadir'), 'applications') 9 | desktop = intl.merge_file( 10 | input : app_id + '.desktop.in', 11 | output : app_id + '.desktop', 12 | po_dir : '../po', 13 | type : 'desktop', 14 | install: true, 15 | install_dir: appsdir 16 | ) 17 | 18 | gsettingsdir = join_paths(get_option('datadir'), 'glib-2.0', 'schemas') 19 | gsettings_schema = app_id + '.gschema.xml' 20 | install_data(gsettings_schema, install_dir : gsettingsdir) 21 | meson.add_install_script('../meson/meson_post_install.py') 22 | 23 | local_schemas = configure_file(copy: true, 24 | input: gsettings_schema, 25 | output: gsettings_schema) 26 | compile_local_schemas = custom_target('compile_local_schemas', 27 | input: local_schemas, 28 | output: 'gschemas.compiled', 29 | command: ['glib-compile-schemas', meson.current_build_dir()]) 30 | 31 | appdatadir = join_paths(get_option('datadir'), 'appdata') 32 | appdata = intl.merge_file( 33 | input : app_id + '.appdata.xml.in', 34 | output : app_id + '.appdata.xml', 35 | po_dir : '../po', 36 | type : 'xml', 37 | install: true, 38 | install_dir: appdatadir 39 | ) 40 | 41 | dbusservicedir = join_paths(get_option('datadir'), 'dbus-1', 'services') 42 | dbus_service = configure_file( 43 | configuration : app_configuration, 44 | input : app_id + '.service.in', 45 | output : app_id + '.service', 46 | install : true, 47 | install_dir : dbusservicedir 48 | ) 49 | -------------------------------------------------------------------------------- /tests/testutil.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | from gi.repository import GLib, Gio 4 | 5 | from dogtail.utils import isA11yEnabled, enableA11y 6 | if not isA11yEnabled(): 7 | enableA11y(True) 8 | 9 | from dogtail import tree 10 | from dogtail import utils 11 | from dogtail.predicate import * 12 | from dogtail.procedural import * 13 | 14 | import os, sys 15 | import subprocess 16 | 17 | APPLICATION_ID = "com.example.Gtk.JSApplication" 18 | 19 | _bus = None 20 | 21 | class IsTextEqual(Predicate): 22 | """Predicate subclass that looks for top-level windows""" 23 | def __init__(self, text): 24 | self.text = text 25 | 26 | def satisfiedByNode(self, node): 27 | try: 28 | textIface = node.queryText() 29 | #print textIface.getText(0, -1) 30 | return textIface.getText(0, -1) == self.text 31 | except NotImplementedError: 32 | return False 33 | 34 | def describeSearchResult(self): 35 | return '%s text node' % self.text 36 | 37 | def _do_bus_call(method, params): 38 | global _bus 39 | 40 | if _bus == None: 41 | _bus = Gio.bus_get_sync(Gio.BusType.SESSION) 42 | _bus.call_sync(APPLICATION_ID, '/' + APPLICATION_ID.replace('.', '/'), 43 | 'org.freedesktop.Application', 44 | method, params, None, 45 | Gio.DBusCallFlags.NONE, 46 | -1, None) 47 | 48 | def start(): 49 | builddir = os.environ.get('G_TEST_BUILDDIR', None) 50 | if builddir and not 'TESTUTIL_DONT_START' in os.environ: 51 | subprocess.Popen([os.path.join(builddir, '..', 'src', APPLICATION_ID)], 52 | cwd=os.path.join(builddir, '..')) 53 | else: 54 | _do_bus_call("Activate", GLib.Variant('(a{sv})', ([],))) 55 | utils.doDelay(3) 56 | 57 | app = tree.root.application(APPLICATION_ID) 58 | focus.application(APPLICATION_ID) 59 | 60 | return app 61 | 62 | def init(): 63 | pass 64 | def fini(): 65 | _do_bus_call("ActivateAction", GLib.Variant('(sava{sv})', ('quit', [], []))) 66 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*- 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | // SPDX-FileCopyrightText: 2013 Giovanni Campagna 4 | 5 | /* exported main */ 6 | 7 | pkg.initGettext(); 8 | pkg.require({ 9 | 'Gdk': '3.0', 10 | 'Gio': '2.0', 11 | 'GLib': '2.0', 12 | 'GObject': '2.0', 13 | 'Gtk': '3.0', 14 | }); 15 | 16 | const Gio = imports.gi.Gio; 17 | const GLib = imports.gi.GLib; 18 | const GObject = imports.gi.GObject; 19 | const Gtk = imports.gi.Gtk; 20 | 21 | const Util = imports.util; 22 | const Window = imports.window; 23 | 24 | function initEnvironment() { 25 | globalThis.getApp = function () { 26 | return Gio.Application.get_default(); 27 | }; 28 | } 29 | 30 | const MyApplication = GObject.registerClass(class MyApplication extends Gtk.Application { 31 | _init() { 32 | super._init({application_id: pkg.name}); 33 | 34 | GLib.set_application_name(_("My JS Application")); 35 | } 36 | 37 | _onQuit() { 38 | this.quit(); 39 | } 40 | 41 | _initAppMenu() { 42 | let builder = new Gtk.Builder(); 43 | builder.add_from_resource('/com/example/Gtk/JSApplication/app-menu.ui'); 44 | 45 | let menu = builder.get_object('app-menu'); 46 | this.set_app_menu(menu); 47 | } 48 | 49 | vfunc_startup() { 50 | super.vfunc_startup(); 51 | 52 | Util.loadStyleSheet('/com/example/Gtk/JSApplication/application.css'); 53 | 54 | Util.initActions(this, [{ 55 | name: 'quit', 56 | activate: this._onQuit, 57 | }]); 58 | this._initAppMenu(); 59 | 60 | log(_("My JS Application started")); 61 | } 62 | 63 | vfunc_activate() { 64 | new Window.MainWindow({application: this}).show(); 65 | } 66 | 67 | vfunc_shutdown() { 68 | log(_("My JS Application exiting")); 69 | 70 | super.vfunc_shutdown(); 71 | } 72 | }); 73 | 74 | function main(argv) { 75 | initEnvironment(); 76 | 77 | return new MyApplication().run(argv); 78 | } 79 | -------------------------------------------------------------------------------- /data/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 55 | 56 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*- 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | // SPDX-FileCopyrightText: 2013 Giovanni Campagna 4 | 5 | /* exported arrayEqual, getSettings, initActions, loadStyleSheet, loadIcon, 6 | loadUI */ 7 | 8 | const Gdk = imports.gi.Gdk; 9 | const Gio = imports.gi.Gio; 10 | const Gtk = imports.gi.Gtk; 11 | const System = imports.system; 12 | 13 | function loadUI(resourcePath, objects) { 14 | let ui = new Gtk.Builder(); 15 | 16 | if (objects) { 17 | for (let o in objects) 18 | ui.expose_object(o, objects[o]); 19 | } 20 | 21 | ui.add_from_resource(resourcePath); 22 | return ui; 23 | } 24 | 25 | function loadStyleSheet(resource) { 26 | let provider = new Gtk.CssProvider(); 27 | provider.load_from_file(Gio.File.new_for_uri(`resource://${resource}`)); 28 | Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), 29 | provider, 30 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 31 | } 32 | 33 | function initActions(actionMap, simpleActionEntries, defaultContext = actionMap) { 34 | simpleActionEntries.forEach(function (entry) { 35 | const { 36 | activate = null, 37 | change_state = null, 38 | context = defaultContext, 39 | ...params 40 | } = entry; 41 | const action = new Gio.SimpleAction(params); 42 | 43 | if (activate) 44 | action.connect('activate', activate.bind(context)); 45 | if (change_state) 46 | action.connect('change-state', change_state.bind(context)); 47 | 48 | actionMap.add_action(action); 49 | }); 50 | } 51 | 52 | function arrayEqual(one, two) { 53 | if (one.length !== two.length) 54 | return false; 55 | 56 | for (let i = 0; i < one.length; i++) { 57 | if (one[i] !== two[i]) 58 | return false; 59 | } 60 | 61 | return true; 62 | } 63 | 64 | function getSettings(schemaId, path) { 65 | const GioSSS = Gio.SettingsSchemaSource; 66 | let schemaSource; 67 | 68 | if (!pkg.moduledir.startsWith('resource://')) { 69 | // Running from the source tree 70 | schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir, 71 | GioSSS.get_default(), 72 | false); 73 | } else { 74 | schemaSource = GioSSS.get_default(); 75 | } 76 | 77 | let schemaObj = schemaSource.lookup(schemaId, true); 78 | if (!schemaObj) { 79 | log(`Missing GSettings schema ${schemaId}`); 80 | System.exit(1); 81 | } 82 | 83 | if (path === undefined) { 84 | return new Gio.Settings({settings_schema: schemaObj}); 85 | } else { 86 | return new Gio.Settings({ 87 | settings_schema: schemaObj, 88 | path, 89 | }); 90 | } 91 | } 92 | 93 | function loadIcon(iconName, size) { 94 | let theme = Gtk.IconTheme.get_default(); 95 | 96 | return theme.load_icon(iconName, 97 | size, 98 | Gtk.IconLookupFlags.GENERIC_FALLBACK); 99 | } 100 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | // -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*- 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | // SPDX-FileCopyrightText: 2013 Giovanni Campagna 4 | 5 | /* exported MainWindow */ 6 | 7 | const GLib = imports.gi.GLib; 8 | const GObject = imports.gi.GObject; 9 | const Gtk = imports.gi.Gtk; 10 | 11 | const Util = imports.util; 12 | 13 | var MainWindow = GObject.registerClass({ 14 | Template: 'resource:///com/example/Gtk/JSApplication/main.ui', 15 | Children: ['main-grid', 'main-search-bar', 'main-search-entry', 16 | 'search-active-button'], 17 | Properties: {'search-active': GObject.ParamSpec.boolean('search-active', '', '', GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, false)}, 18 | }, class MainWindow extends Gtk.ApplicationWindow { 19 | _init(params) { 20 | super._init({ 21 | title: GLib.get_application_name(), 22 | default_width: 640, 23 | default_height: 480, 24 | ...params, 25 | }); 26 | 27 | this._searchActive = false; 28 | 29 | Util.initActions(this, [ 30 | { 31 | name: 'new', 32 | activate: this._new, 33 | }, 34 | { 35 | name: 'about', 36 | activate: this._about, 37 | }, 38 | { 39 | name: 'search-active', 40 | activate: this._toggleSearch, 41 | parameter_type: new GLib.VariantType('b'), 42 | state: new GLib.Variant('b', false), 43 | }, 44 | ]); 45 | 46 | this.bind_property('search-active', this.search_active_button, 'active', 47 | GObject.BindingFlags.SYNC_CREATE | 48 | GObject.BindingFlags.BIDIRECTIONAL); 49 | this.bind_property('search-active', this.main_search_bar, 'search-mode-enabled', 50 | GObject.BindingFlags.SYNC_CREATE | 51 | GObject.BindingFlags.BIDIRECTIONAL); 52 | this.main_search_bar.connect_entry(this.main_search_entry); 53 | 54 | this._view = new MainView(); 55 | this._view.visible_child_name = Math.random() <= 0.5 ? 'one' : 'two'; 56 | this.main_grid.add(this._view); 57 | this.main_grid.show_all(); 58 | 59 | // Due to limitations of gobject-introspection wrt GdkEvent and GdkEventKey, 60 | // this needs to be a signal handler 61 | this.connect('key-press-event', this._handleKeyPress.bind(this)); 62 | } 63 | 64 | get search_active() { 65 | return this._searchActive; 66 | } 67 | 68 | set search_active(v) { 69 | if (this._searchActive === v) 70 | return; 71 | 72 | this._searchActive = v; 73 | // do something with v 74 | this.notify('search-active'); 75 | } 76 | 77 | _handleKeyPress(self, event) { 78 | return this.main_search_bar.handle_event(event); 79 | } 80 | 81 | _new() { 82 | log(_("New something")); 83 | } 84 | 85 | _about() { 86 | let aboutDialog = new Gtk.AboutDialog({ 87 | authors: ['Giovanni Campagna '], 88 | translator_credits: _("translator-credits"), 89 | program_name: _("JS Application"), 90 | comments: _("Demo JS Application and template"), 91 | copyright: 'Copyright 2013 The gjs developers', 92 | license_type: Gtk.License.GPL_2_0, 93 | logo_icon_name: 'com.example.Gtk.JSApplication', 94 | version: pkg.version, 95 | website: 'http://www.example.com/gtk-js-app/', 96 | wrap_license: true, 97 | modal: true, 98 | transient_for: this, 99 | }); 100 | 101 | aboutDialog.show(); 102 | aboutDialog.connect('response', function () { 103 | aboutDialog.destroy(); 104 | }); 105 | } 106 | }); 107 | 108 | const MainView = GObject.registerClass(class MainView extends Gtk.Stack { 109 | _init(params) { 110 | super._init({ 111 | hexpand: true, 112 | vexpand: true, 113 | ...params, 114 | }); 115 | 116 | this._settings = Util.getSettings(pkg.name); 117 | 118 | this._buttonOne = this._addPage('one', _("First page"), ''); 119 | this._buttonOne.connect('clicked', () => { 120 | this.visible_child_name = 'two'; 121 | }); 122 | this._syncLabel(); 123 | this._settings.connect('changed::show-exclamation-mark', this._syncLabel.bind(this)); 124 | 125 | let two = this._addPage('two', _("Second page"), _("What did you expect?")); 126 | two.connect('clicked', () => { 127 | this.visible_child_name = 'one'; 128 | }); 129 | } 130 | 131 | _addPage(name, label, button) { 132 | let labelWidget = new Gtk.Label({label}); 133 | labelWidget.get_style_context().add_class('big-label'); 134 | let buttonWidget = new Gtk.Button({label: button}); 135 | 136 | let grid = new Gtk.Grid({ 137 | orientation: Gtk.Orientation.VERTICAL, 138 | halign: Gtk.Align.CENTER, 139 | valign: Gtk.Align.CENTER, 140 | }); 141 | grid.add(labelWidget); 142 | grid.add(buttonWidget); 143 | 144 | this.add_named(grid, name); 145 | return buttonWidget; 146 | } 147 | 148 | _syncLabel() { 149 | if (this._settings.get_boolean('show-exclamation-mark')) 150 | this._buttonOne.label = _("Hello, world!"); 151 | else 152 | this._buttonOne.label = _("Hello world"); 153 | } 154 | }); 155 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # SPDX-License-Identifier: MIT OR LGPL-2.0-or-later 3 | # SPDX-FileCopyrightText: 2018 Claudio André 4 | env: 5 | es6: true 6 | es2020: true 7 | extends: 'eslint:recommended' 8 | rules: 9 | array-bracket-newline: 10 | - error 11 | - consistent 12 | array-bracket-spacing: 13 | - error 14 | - never 15 | array-callback-return: error 16 | arrow-parens: 17 | - error 18 | - as-needed 19 | arrow-spacing: error 20 | block-scoped-var: error 21 | block-spacing: error 22 | brace-style: error 23 | # Waiting for this to have matured a bit in eslint 24 | # camelcase: 25 | # - error 26 | # - properties: never 27 | # allow: [^vfunc_, ^on_, _instance_init] 28 | comma-dangle: 29 | - error 30 | - arrays: always-multiline 31 | objects: always-multiline 32 | functions: never 33 | comma-spacing: 34 | - error 35 | - before: false 36 | after: true 37 | comma-style: 38 | - error 39 | - last 40 | computed-property-spacing: error 41 | curly: 42 | - error 43 | - multi-or-nest 44 | - consistent 45 | dot-location: 46 | - error 47 | - property 48 | eol-last: error 49 | eqeqeq: error 50 | func-call-spacing: error 51 | func-name-matching: error 52 | func-style: 53 | - error 54 | - declaration 55 | - allowArrowFunctions: true 56 | indent: 57 | - error 58 | - 4 59 | - ignoredNodes: 60 | # Allow not indenting the body of GObject.registerClass, since in the 61 | # future it's intended to be a decorator 62 | - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' 63 | # Allow dedenting chained member expressions 64 | MemberExpression: 'off' 65 | key-spacing: 66 | - error 67 | - beforeColon: false 68 | afterColon: true 69 | keyword-spacing: 70 | - error 71 | - before: true 72 | after: true 73 | linebreak-style: 74 | - error 75 | - unix 76 | lines-between-class-members: error 77 | max-nested-callbacks: error 78 | max-statements-per-line: error 79 | new-parens: error 80 | no-array-constructor: error 81 | no-await-in-loop: error 82 | no-caller: error 83 | no-constant-condition: 84 | - error 85 | - checkLoops: false 86 | no-div-regex: error 87 | no-empty: 88 | - error 89 | - allowEmptyCatch: true 90 | no-extra-bind: error 91 | no-extra-parens: 92 | - error 93 | - all 94 | - conditionalAssign: false 95 | nestedBinaryExpressions: false 96 | returnAssign: false 97 | no-implicit-coercion: 98 | - error 99 | - allow: 100 | - '!!' 101 | no-invalid-this: error 102 | no-iterator: error 103 | no-label-var: error 104 | no-lonely-if: error 105 | no-loop-func: error 106 | no-nested-ternary: error 107 | no-new-object: error 108 | no-new-wrappers: error 109 | no-octal-escape: error 110 | no-proto: error 111 | no-prototype-builtins: 'off' 112 | no-restricted-globals: [error, window] 113 | no-restricted-properties: 114 | - error 115 | - object: imports 116 | property: format 117 | message: Use template strings 118 | - object: pkg 119 | property: initFormat 120 | message: Use template strings 121 | - object: Lang 122 | property: copyProperties 123 | message: Use Object.assign() 124 | - object: Lang 125 | property: bind 126 | message: Use arrow notation or Function.prototype.bind() 127 | - object: Lang 128 | property: Class 129 | message: Use ES6 classes 130 | no-restricted-syntax: 131 | - error 132 | - selector: >- 133 | MethodDefinition[key.name="_init"] > 134 | FunctionExpression[params.length=1] > 135 | BlockStatement[body.length=1] 136 | CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] > 137 | Identifier:first-child 138 | message: _init() that only calls super._init() is unnecessary 139 | - selector: >- 140 | MethodDefinition[key.name="_init"] > 141 | FunctionExpression[params.length=0] > 142 | BlockStatement[body.length=1] 143 | CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"] 144 | message: _init() that only calls super._init() is unnecessary 145 | - selector: BinaryExpression[operator="instanceof"][right.name="Array"] 146 | message: Use Array.isArray() 147 | no-return-assign: error 148 | no-return-await: error 149 | no-self-compare: error 150 | no-shadow: error 151 | no-shadow-restricted-names: error 152 | no-spaced-func: error 153 | no-tabs: error 154 | no-template-curly-in-string: error 155 | no-throw-literal: error 156 | no-trailing-spaces: error 157 | no-undef-init: error 158 | no-unneeded-ternary: error 159 | no-unused-expressions: error 160 | no-unused-vars: 161 | - error 162 | # Vars use a suffix _ instead of a prefix because of file-scope private vars 163 | - varsIgnorePattern: (^unused|_$) 164 | argsIgnorePattern: ^(unused|_) 165 | no-useless-call: error 166 | no-useless-computed-key: error 167 | no-useless-concat: error 168 | no-useless-constructor: error 169 | no-useless-rename: error 170 | no-useless-return: error 171 | no-whitespace-before-property: error 172 | no-with: error 173 | nonblock-statement-body-position: 174 | - error 175 | - below 176 | object-curly-newline: 177 | - error 178 | - consistent: true 179 | multiline: true 180 | object-curly-spacing: error 181 | object-shorthand: error 182 | operator-assignment: error 183 | operator-linebreak: error 184 | padded-blocks: 185 | - error 186 | - never 187 | # These may be a bit controversial, we can try them out and enable them later 188 | # prefer-const: error 189 | # prefer-destructuring: error 190 | prefer-numeric-literals: error 191 | prefer-promise-reject-errors: error 192 | prefer-rest-params: error 193 | prefer-spread: error 194 | prefer-template: error 195 | require-await: error 196 | rest-spread-spacing: error 197 | semi: 198 | - error 199 | - always 200 | semi-spacing: 201 | - error 202 | - before: false 203 | after: true 204 | semi-style: error 205 | space-before-blocks: error 206 | space-before-function-paren: 207 | - error 208 | - named: never 209 | # for `function ()` and `async () =>`, preserve space around keywords 210 | anonymous: always 211 | asyncArrow: always 212 | space-in-parens: error 213 | space-infix-ops: 214 | - error 215 | - int32Hint: false 216 | space-unary-ops: error 217 | spaced-comment: error 218 | switch-colon-spacing: error 219 | symbol-description: error 220 | template-curly-spacing: error 221 | template-tag-spacing: error 222 | unicode-bom: error 223 | wrap-iife: 224 | - error 225 | - inside 226 | yield-star-spacing: error 227 | yoda: error 228 | globals: 229 | _: readonly 230 | globalThis: readonly 231 | imports: readonly 232 | log: readonly 233 | logError: readonly 234 | pkg: readonly 235 | print: readonly 236 | printerr: readonly 237 | parserOptions: 238 | ecmaVersion: 2020 239 | --------------------------------------------------------------------------------