Compare commits

..

27 Commits
0.96.0 ... main

Author SHA1 Message Date
Juan Pablo Ugarte
53062b7d3c
Rolling 0.97.3 2025-06-22 16:41:07 -04:00
Juan Pablo Ugarte
9aaace82f7
Update Casilda dependency to 0.9.0
Remove merengue GSK_RENDERER var to enable GL client rendering.
2025-06-22 16:29:57 -04:00
Juan Pablo Ugarte
bbb2ce2bf9
cmb_init_dev add missing repository directory 2025-06-22 16:29:12 -04:00
Juan Pablo Ugarte
dd90c3fc2b
Bump to 0.97.2 2025-05-24 16:47:08 -04:00
Juan Pablo Ugarte
8742945f3f
CmbObjectEditor: allways pass info to data editors 2025-05-24 16:46:00 -04:00
Juan Pablo Ugarte
06c62349a7
CmbObjectDataEditor: rever changes made to fix data removal. 2025-05-24 16:45:31 -04:00
Juan Pablo Ugarte
bc6163b1ac
CmbObject: wrap remove_data() history commands 2025-05-24 16:44:44 -04:00
Juan Pablo Ugarte
ac74e23fde
CmbObjectData: group remove_data() history commands 2025-05-24 16:43:41 -04:00
Juan Pablo Ugarte
2e29c016f7
CmbNotification: log request error as info instead of warning 2025-05-24 16:43:01 -04:00
Juan Pablo Ugarte
b572a7eae3
Add blueprint support
- CmbProject: add blueprint load/save support
 - CmbWindow: show blueprint compiler errors on save
 - CmbUIEditor: add format dropdown
 - CmbView: show blueprint source code instead of XML
 - Add blueprint tests

Closes issue #80 "Support for blueprint file format"
2025-05-23 08:48:02 -04:00
Juan Pablo Ugarte
f96d19ab64
CmbDB: add private flags to control boolean and enum output 2025-05-21 19:08:34 -04:00
Juan Pablo Ugarte
680f9d3243
CmbFileButton: add mime_types, accept_label and unnamed_filename properties 2025-05-21 19:05:59 -04:00
Juan Pablo Ugarte
6fa7f22c9c
CmbTypeInfo: add use_nick param to enum_get_value_as_string() 2025-05-21 19:03:47 -04:00
Juan Pablo Ugarte
6ac16ca8c0
tests/test_import_export.py: Use parametrize 2025-05-21 19:01:53 -04:00
Juan Pablo Ugarte
e2dbb15a18
CmbSourceView: fix lang getter 2025-05-21 19:01:01 -04:00
Juan Pablo Ugarte
c57891a3c8
CmbUI: update display-name on filename or template-id change 2025-05-21 19:01:01 -04:00
Juan Pablo Ugarte
ccdf5d6ef0
CmbVersionNotificationView: do not show read more button if there is no link 2025-05-15 18:28:40 -04:00
Juan Pablo Ugarte
bb39873cd5
Rolling 0.97.1 2025-05-14 17:22:30 -04:00
Juan Pablo Ugarte
9529bfaf76
CmbObjectDataEditor: fix object data remove. 2025-05-14 17:17:53 -04:00
Juan Pablo Ugarte
13db1c20c2
CmbProject: fix GResource list model update 2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
963cee5eec
CmbResource: misc improvements
- Notify display-name when underlying props changes
 - Add private functions to update parent data needed to keep
   list model in sync
2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
08a73f9c28
CmbResourceEditor: use open for choosing a file 2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
02935555bf
CmbDB: add update_gresource_children_position() 2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
befb056d2c
CmbFileButton: add use_open property 2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
9cd6e05d5f
run-dev.py: Misc cleanups
- Make tests work without any special env set
 - Remove .local.env bash env
 - Simplify coverage script
2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
57a3f3832a
CmbNotification: generate UUID locally 2025-05-14 11:40:44 -04:00
Juan Pablo Ugarte
8c80fc5d3d
Bump to 0.97.0 - Development version 2025-04-21 07:25:10 -04:00
41 changed files with 849 additions and 386 deletions

View File

@ -1,13 +0,0 @@
#!/usr/bin/bash
SCRIPT=$(readlink -f $0)
DIRNAME=$(dirname $SCRIPT)
ARCH_TRIPLET=$(cc -dumpmachine)
export LIBDIR=$DIRNAME/.local/lib/$ARCH_TRIPLET
export LD_LIBRARY_PATH=$LIBDIR:$LIBDIR/cambalache:$LIBDIR/cmb_catalog_gen:$LD_LIBRARY_PATH
export GI_TYPELIB_PATH=$LIBDIR/girepository-1.0:$LIBDIR/cambalache:$LIBDIR/cmb_catalog_gen:$GI_TYPELIB_PATH
export PKG_CONFIG_PATH=$LIBDIR/pkgconfig:$PKG_CONFIG_PATH
export GSETTINGS_SCHEMA_DIR=$DIRNAME/.local/share/glib-2.0/schemas:$GSETTINGS_SCHEMA_DIR
export XDG_DATA_DIRS=$DIRNAME/.local/share:$XDG_DATA_DIRS
export PYTHONPATH=$DIRNAME/.local/lib/python3/dist-packages:$PYTHONPATH
export PATH=$DIRNAME/.local/bin:$PATH

View File

@ -152,7 +152,7 @@ cambalache under .local directoy and set up all environment variables needed to
run the app from the source directory. (Follow manual installation to ensure
you have everything needed)
`./run-dev.sh`
`./run-dev.py`
This is meant for Cambalache development only.

View File

@ -83,8 +83,8 @@
{
"type" : "git",
"url" : "https://gitlab.gnome.org/jpu/casilda.git",
"tag" : "0.2.0",
"commit" : "99a0173f21345b85713198c1fa1fbb388d00182f"
"tag" : "0.9.0",
"commit" : "4f7b1be321cf76832b12bda11fd91897257377e2"
}
]
},

View File

@ -43,9 +43,12 @@ from cambalache import (
notification_center,
config,
utils,
_
_,
N_
)
from cambalache.cmb_blueprint import CmbBlueprintError
logger = getLogger(__name__)
GObject.type_ensure(CmbGResourceEditor.__gtype__)
@ -64,6 +67,7 @@ class CmbWindow(Adw.ApplicationWindow):
gtk4_filter = Gtk.Template.Child()
gtk3_filter = Gtk.Template.Child()
gtk_builder_filter = Gtk.Template.Child()
blueprint_filter = Gtk.Template.Child()
glade_filter = Gtk.Template.Child()
css_filter = Gtk.Template.Child()
gresource_filter = Gtk.Template.Child()
@ -136,7 +140,13 @@ class CmbWindow(Adw.ApplicationWindow):
self.gtk4_import_filters = Gio.ListStore()
for filter in [self.gtk4_filter, self.gtk_builder_filter, self.css_filter, self.gresource_filter]:
for filter in [
self.gtk4_filter,
self.gtk_builder_filter,
self.blueprint_filter,
self.css_filter,
self.gresource_filter
]:
self.gtk4_import_filters.append(filter)
self.gtk3_import_filters = Gio.ListStore()
@ -1135,7 +1145,7 @@ class CmbWindow(Adw.ApplicationWindow):
print("IMPORT", path, content_type)
if content_type in ["application/x-gtk-builder", "application/x-glade"]:
if content_type in ["application/x-gtk-builder", "application/x-glade", "text/x-blueprint"]:
self.import_file(file.get_path())
elif content_type == "text/css":
self.project.add_css(path)
@ -1164,10 +1174,25 @@ class CmbWindow(Adw.ApplicationWindow):
self.project.set_selection([gresource])
def __save(self):
if self.project.save():
self.__last_saved_index = self.project.history_index
self.__update_action_save()
self.emit("project-saved", self.project)
retval = False
try :
retval = self.project.save()
except CmbBlueprintError as e:
self.present_message_to_user(
_("Error saving project"),
secondary_text=N_(
"blueprintcompiler encounter the following error:",
"blueprintcompiler encounter the following errors:",
len(e.errors)
),
details=[str(e)]
)
finally:
if retval:
self.__last_saved_index = self.project.history_index
self.__update_action_save()
self.emit("project-saved", self.project)
def save_project(self):
if self.project is None:

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<!-- Created with Cambalache 0.97.1 -->
<interface>
<!-- interface-name cmb_window.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
@ -836,6 +836,9 @@
<object class="GtkFileFilter" id="glade_filter">
<property name="mime-types">application/x-glade</property>
</object>
<object class="GtkFileFilter" id="blueprint_filter">
<property name="mime-types">text/x-blueprint</property>
</object>
<object class="GtkFileFilter" id="css_filter">
<property name="mime-types">text/css</property>
</object>
@ -844,6 +847,7 @@
</object>
<object class="GtkFileFilter" id="gtk4_filter">
<property name="mime-types">application/x-gtk-builder
text/x-blueprint
text/css</property>
<property name="name">All supported files</property>
<property name="suffixes">gresource.xml</property>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
<!-- Created with Cambalache 0.95.1 -->
<cambalache-project version="0.95.0" target_tk="gtk-4.0">
<!-- Created with Cambalache 0.97.1 -->
<cambalache-project version="0.96.0" target_tk="gtk-4.0">
<gresources filename="cambalache.gresource.xml" sha256="fdcf4cd517493f548aa4b4fe206ff7762cee9cdda7ec5a85a718b46eb1c4731b"/>
<gresources filename="app/app.gresource.xml" sha256="3684aa78fce08d8e81d0907317214aeb179c5aea091dd0df405476b43e286941"/>
<css filename="cambalache.css" priority="400" is_global="1"/>
@ -221,7 +221,10 @@
<ui template-class="CmbContextMenu" filename="cmb_context_menu.ui" sha256="81eba3adf715348a5c03ef4cbc151eebd5d9aa8b5a14c5968232f68a61ae573c"/>
<ui template-class="CmbDBInspector" filename="cmb_db_inspector.ui" sha256="4451cdb08d24bd4a802ea692c0ebb4ef46af13152984c0b435d29bf4eb7dab55"/>
<ui filename="app/cmb_shortcuts.ui" sha256="d7ac37fd2430788a9e210ed4bc84dcfeba5609bdcc801afb192bfd900c7a8883"/>
<ui template-class="CmbFileButton" filename="control/cmb_file_button.ui" sha256="f859b4f85d7c80c1fef69b68ebb9129423d9c72fdb38d304132784f7361cbbfd"/>
<ui template-class="CmbFileButton" filename="control/cmb_file_button.ui" sha256="f859b4f85d7c80c1fef69b68ebb9129423d9c72fdb38d304132784f7361cbbfd">
<property id="dialog-title" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="use-open" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
</ui>
<ui template-class="CmbNotificationListView" filename="cmb_notification_list_view.ui" sha256="13622645038ef2aaa154f74cd300f9c0fa0dccf69d45d6c9376f9034e6ee57fb"/>
<ui template-class="CmbVersionNotificationView" filename="cmb_version_notification_view.ui" sha256="9a3ced46b90eb7e425d1c345853c4e8e908870c61c75475f7e20ce3c9ee8cec6"/>
<ui template-class="CmbMessageNotificationView" filename="cmb_message_notification_view.ui" sha256="debeffd184e225d82ed29ac590654b8160363e8d5606366dc8acb3ff9840fee3"/>
@ -253,7 +256,7 @@
<signal id="placeholder-activated"/>
<signal id="placeholder-selected"/>
</ui>
<ui template-class="CmbGResourceEditor" filename="cmb_gresource_editor.ui" sha256="46969468ae070bdd315ca4091869d0b8a9bcb24c10cde82208bfd7d36f39fdd0">
<ui template-class="CmbGResourceEditor" filename="cmb_gresource_editor.ui" sha256="2050887ef1c45facb6ebff14500214bb035e6808ca61d2a7d661e696d79026ca">
<requires>CmbFileButton</requires>
<requires>CmbEntry</requires>
</ui>
@ -261,13 +264,13 @@
<requires>CmbFileButton</requires>
<requires>CmbSourceView</requires>
</ui>
<ui template-class="CmbUIEditor" filename="cmb_ui_editor.ui" sha256="0e4e205a3737fa207406ce74f4d8d3fbb4a477409b8a7b425b3d53c41309b306">
<ui template-class="CmbUIEditor" filename="cmb_ui_editor.ui" sha256="70e272e2c6c499a5424c6019154cd8338d9edde1fa111ad592a1c104019bb7ee">
<requires>CmbTextBuffer</requires>
<requires>CmbFileButton</requires>
<requires>CmbEntry</requires>
<requires>CmbToplevelChooser</requires>
</ui>
<ui template-class="CmbWindow" filename="app/cmb_window.ui" sha256="20a192093a13209e0add725f15922e4f09689dda08fbf0b3a3eedf5d9adf2efc">
<ui template-class="CmbWindow" filename="app/cmb_window.ui" sha256="df07e3e03b88f9b097ad7d65efa15923d339db83b9d4a7c015a312a71f8c9685">
<requires>CmbNotificationListView</requires>
<requires>CmbScrolledWindow</requires>
<requires>CmbObjectEditor</requires>

View File

@ -0,0 +1,86 @@
#
# Blueprint compiler integration functions
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# 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;
# version 2.1 of the License.
#
# 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import io
try:
import blueprintcompiler as bp
from blueprintcompiler import parser, tokenizer
from blueprintcompiler.decompiler import decompile_string
from blueprintcompiler.outputs import XmlOutput
except Exception:
bp = None
class CmbBlueprintError(Exception):
def __init__(self, message, errors=[]):
super().__init__(message)
self.errors = errors
class CmbBlueprintUnsupportedError(CmbBlueprintError):
pass
class CmbBlueprintMissingError(CmbBlueprintError):
def __init__(self):
super().__init__("blueprintcompiler is not available")
def cmb_blueprint_decompile(data: str) -> str:
if bp is None:
raise CmbBlueprintMissingError()
try:
retval = decompile_string(data)
except bp.decompiler.UnsupportedError as e:
raise CmbBlueprintUnsupportedError(str(e))
except Exception as e:
raise CmbBlueprintError(str(e))
return retval
def cmb_blueprint_compile(data: str) -> str:
if bp is None:
raise CmbBlueprintMissingError()
tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens)
if errors:
f = io.StringIO("")
errors.pretty_print("temp", data, f)
f.seek(0)
raise CmbBlueprintError(f.read(), errors=errors)
if ast is None:
raise CmbBlueprintError("AST is None")
# Ignore warnings
retval = XmlOutput().emit(ast)
return retval.encode()

View File

@ -82,6 +82,9 @@ class CmbDB(GObject.GObject):
self.__history_commands = {}
self.__table_column_mapping = {}
self._output_lowercase_boolean = False
self._output_use_enum_value = False
self.clipboard = []
self.clipboard_ids = []
@ -2023,7 +2026,7 @@ class CmbDB(GObject.GObject):
self.__unknown_tag(child, root, child.tag)
continue
prefix, = self.__node_get(child, "prefix")
prefix, = self.__node_get(child, ["prefix"])
resource_id = self.add_gresource("gresource", parent_id=gresource_id, gresource_prefix=prefix)
@ -2372,6 +2375,7 @@ class CmbDB(GObject.GObject):
value = None
value_node = None
pinfo = self.type_info.get(property_type_id, None)
is_inline_object = not disable_inline_object and self.target_tk == "gtk-4.0"
@ -2399,6 +2403,10 @@ class CmbDB(GObject.GObject):
value = obj_name
elif property_type_id == "GBytes":
value = etree.CDATA(val)
elif self._output_lowercase_boolean and property_type_id == "gboolean":
value = "true" if utils.bool_from_string(val) else "false"
elif self._output_use_enum_value and pinfo and pinfo.parent_id == "enum":
value = str(pinfo.enum_get_value_as_string(val, use_nick=False))
else:
value = val
@ -2445,16 +2453,16 @@ class CmbDB(GObject.GObject):
node = E.signal(name=name, handler=handler)
if data:
utils.xml_node_set(node, "object", data)
# if object is set, swap defaults to True
if not swap:
utils.xml_node_set(node, "swapped", "no")
utils.xml_node_set(node, "swapped", "False")
utils.xml_node_set(node, "object", data)
elif swap:
utils.xml_node_set(node, "swapped", "yes")
utils.xml_node_set(node, "swapped", "True")
if after:
utils.xml_node_set(node, "after", "yes")
utils.xml_node_set(node, "after", "True")
obj.append(node)
self.__node_add_comment(node, comment)
@ -2518,6 +2526,7 @@ class CmbDB(GObject.GObject):
owner_id,
) = row
pinfo = self.type_info.get(property_type_id, None)
value = None
# Ignore properties depending on metadata (Gtk4)
@ -2537,6 +2546,15 @@ class CmbDB(GObject.GObject):
if obj_name is None:
continue
value = obj_name
elif self._output_lowercase_boolean and property_type_id == "gboolean":
value = "true" if utils.bool_from_string(val) else "false"
elif self._output_lowercase_boolean and property_type_id == "CmbBooleanUndefined":
if val == "undefined":
value = "undefined"
else:
value = "true" if utils.bool_from_string(val) else "false"
elif self._output_use_enum_value and pinfo and pinfo.parent_id == "enum":
value = str(pinfo.enum_get_value_as_string(val, use_nick=False))
else:
value = val
@ -2746,7 +2764,7 @@ class CmbDB(GObject.GObject):
for child in root:
node.append(child)
def export_ui(self, ui_id, merengue=False, skip_version_comment=False):
def export_ui(self, ui_id, merengue=False):
c = self.conn.cursor()
c.execute("SELECT translation_domain, comment, template_id, custom_fragment FROM ui WHERE ui_id=?;", (ui_id,))
@ -2759,8 +2777,7 @@ class CmbDB(GObject.GObject):
node = E.interface()
if not skip_version_comment:
node.addprevious(etree.Comment(f" Created with Cambalache {config.VERSION} "))
node.addprevious(etree.Comment(f" Created with Cambalache {config.VERSION} "))
utils.xml_node_set(node, "domain", translation_domain)
@ -3034,6 +3051,20 @@ class CmbDB(GObject.GObject):
(ui_id, ) if parent_id is None else (ui_id, parent_id)
)
def update_gresource_children_position(self, gresource_id):
self.execute(
"""
UPDATE gresource SET position=new.position - 1
FROM (
SELECT row_number() OVER (PARTITION BY parent_id ORDER BY position) position, parent_id, gresource_id
FROM gresource
WHERE parent_id=?
) AS new
WHERE gresource.parent_id=new.parent_id AND gresource.gresource_id=new.gresource_id;
""",
(gresource_id, )
)
# Function used in SQLite

View File

@ -36,6 +36,8 @@ class CmbGResource(CmbBaseGResource, Gio.ListModel):
path_parent = GObject.Property(type=CmbPath, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
self._last_known = None
super().__init__(**kwargs)
self.connect("notify", self.__on_notify)
@ -47,6 +49,13 @@ class CmbGResource(CmbBaseGResource, Gio.ListModel):
return f"CmbGResource<{self.resource_type}> id={self.gresource_id}"
def __on_notify(self, obj, pspec):
resource_type = self.resource_type
if (resource_type == "gresources" and pspec.name == "gresources-filename") or \
(resource_type == "gresource" and pspec.name == "gresource-prefix") or \
(resource_type == "file" and pspec.name == "file-filename"):
obj.notify("display-name")
self.project._gresource_changed(self, pspec.name)
@GObject.Property(type=CmbBaseGResource)
@ -84,6 +93,34 @@ class CmbGResource(CmbBaseGResource, Gio.ListModel):
file_filename = self.file_filename
return file_filename if file_filename else _("Unnamed file {id}").format(id=self.gresource_id)
# GListModel helpers
def _save_last_known_parent_and_position(self):
self._last_known = (self.parent, self.position)
def _update_new_parent(self):
parent = self.parent
position = self.position
# Emit GListModel signal to update model
if parent:
parent.items_changed(position, 0, 1)
parent.notify("n-items")
self._last_known = None
def _remove_from_old_parent(self):
if self._last_known is None:
return
parent, position = self._last_known
# Emit GListModel signal to update model
if parent:
parent.items_changed(position, 1, 0)
parent.notify("n-items")
self._last_known = None
# GListModel iface
def do_get_item(self, position):
gresource_id = self.gresource_id

View File

@ -221,6 +221,7 @@
</child>
<child>
<object class="CmbFileButton" id="file_filename">
<property name="use-open">True</property>
<layout>
<property name="column">1</property>
<property name="column-span">1</property>

View File

@ -30,6 +30,7 @@ import http.client
import time
import platform
from uuid import uuid4
from urllib.parse import urlparse
from .config import VERSION
from gi.repository import GObject, GLib, Gio, Gdk, Gtk, Adw, HarfBuzz
@ -130,7 +131,7 @@ class CmbNotificationCenter(GObject.GObject):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.retry_interval = 1
self.retry_interval = 2
self.user_agent = self.__get_user_agent()
self.store = Gio.ListStore(item_type=CmbNotification)
self.settings = Gio.Settings(schema_id="ar.xjuan.Cambalache.notification")
@ -156,6 +157,10 @@ class CmbNotificationCenter(GObject.GObject):
logger.warning(f"{backend.scheme} is not supported, only HTTPS")
return
# Ensure we have a UUID
if not self.uuid:
self.uuid = str(uuid4())
logger.info(f"User Agent: {self.user_agent}")
logger.info(f"UUID: {self.uuid}")
@ -248,9 +253,6 @@ class CmbNotificationCenter(GObject.GObject):
def __get_notification_idle(self, data):
logger.debug(f"Got notification response {json.dumps(data, indent=2, sort_keys=True)}")
if "uuid" in data:
self.uuid = data["uuid"]
if "notification" in data:
notification = self.__notification_from_dict(data["notification"])
self.store.insert(0, notification)
@ -264,10 +266,10 @@ class CmbNotificationCenter(GObject.GObject):
return GLib.SOURCE_REMOVE
def __get_notification_thread(self):
headers = {"User-Agent": self.user_agent}
if self.uuid:
headers["x-cambalache-uuid"] = self.uuid
headers = {
"User-Agent": self.user_agent,
"x-cambalache-uuid": self.uuid,
}
try:
logger.info(f"GET /notification {headers=}")
@ -277,7 +279,7 @@ class CmbNotificationCenter(GObject.GObject):
assert response.status == 200
# Reset retry interval
self.retry_interval = 1
self.retry_interval = 8
data = response.read().decode()
@ -290,7 +292,7 @@ class CmbNotificationCenter(GObject.GObject):
self.retry_interval *= 2
self.retry_interval = min(self.retry_interval, 256)
logger.warning(f"Request error {e}, retrying in {self.retry_interval}s")
logger.info(f"Request error {e}, retrying in {self.retry_interval}s")
GLib.timeout_add_seconds(self.retry_interval, self._get_notification)
self.connection.close()
@ -318,19 +320,19 @@ class CmbNotificationCenter(GObject.GObject):
def __poll_vote_idle(self, data):
logger.debug(f"Got vote response {data}")
uuid = data["uuid"]
poll_uuid = data["uuid"]
results = data["results"]
for notification in self.store:
if isinstance(notification, CmbPollNotification) and notification.poll.id == uuid:
if isinstance(notification, CmbPollNotification) and notification.poll.id == poll_uuid:
notification.results = CmbPollResult(**results)
self.__save_notifications()
break
return GLib.SOURCE_REMOVE
def __poll_vote_exception_idle(self, uuid):
def __poll_vote_exception_idle(self, poll_uuid):
for notification in self.store:
if isinstance(notification, CmbPollNotification) and notification.poll.id == uuid:
if isinstance(notification, CmbPollNotification) and notification.poll.id == poll_uuid:
notification.my_votes = []
break
return GLib.SOURCE_REMOVE

View File

@ -458,11 +458,17 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
def remove_data(self, data):
try:
assert data.get_id_string() in self.data_dict
self.project.history_push(
_("Remove {key} from {name}").format(key=data.info.key, name=self.display_name_type)
)
self.project.db.execute(
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;",
(self.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
)
self.project.db.commit()
self.project.history_pop()
except Exception as e:
logger.warning(f"{self} Error removing data {data}: {e}")
return False

View File

@ -27,7 +27,7 @@ from gi.repository import GObject
from .cmb_objects_base import CmbBaseObjectData
from .cmb_type_info import CmbTypeDataInfo
from cambalache import getLogger
from cambalache import getLogger, _
logger = getLogger(__name__)
@ -194,11 +194,17 @@ class CmbObjectData(CmbBaseObjectData):
def remove_data(self, data):
try:
assert data in self.children
self.project.history_push(
_("Remove {key} from {name}").format(key=data.info.key, name=self.object.display_name_type)
)
self.project.db.execute(
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;",
(self.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
)
self.project.db.commit()
self.project.history_pop()
except Exception as e:
logger.warning(f"{self} Error removing data {data}: {e}")
return False

View File

@ -31,6 +31,12 @@ from .control import cmb_create_editor
from cambalache import _
# Everyone knows that debugging is twice as hard as writing a program in the first place.
# So if youre as clever as you can be when you write it, how will you ever debug it?
# -- Brian Kernighan, 1974
#
# TODO: rewrite this!
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_object_data_editor.ui")
class CmbObjectDataEditor(Gtk.Box):
__gtype_name__ = "CmbObjectDataEditor"
@ -69,7 +75,7 @@ class CmbObjectDataEditor(Gtk.Box):
def __on_remove_clicked(self, button):
if self.info:
self.object.remove_data(self.__data)
else:
elif self.__data:
self.__data.parent.remove_data(self.__data)
@GObject.Property(type=GObject.Object)
@ -138,8 +144,7 @@ class CmbObjectDataEditor(Gtk.Box):
self.__update_view()
def __on_data_removed(self, obj, data):
if self.object and self.info:
self.__remove_data_editor(data)
self.__remove_data_editor(data)
def __ensure_object_data(self, history_message):
if self.data:

View File

@ -260,7 +260,7 @@ It has to be exposed by your application with GtkBuilder expose_object method."
hexpand=True,
object=obj,
data=data,
info=None if data else info.data[data_key],
info=info.data[data_key],
)
grid.attach(editor, 0, i, 2, 1)

View File

@ -58,6 +58,9 @@ class CmbPath(CmbBase, Gio.ListModel):
return self.__path_items.get(directory, None)
def add_item(self, item):
if item in self.__items:
return
display_name = item.display_name
is_path = isinstance(item, CmbPath)
@ -85,9 +88,13 @@ class CmbPath(CmbBase, Gio.ListModel):
self.notify("display-name")
def remove_item(self, item):
if item not in self.__items:
return
if isinstance(item, CmbPath) and item.path in self.__path_items:
del self.__path_items[item.path]
item.path_parent = None
i = self.__items.index(item)
self.__items.pop(i)
self.items_changed(i, 1, 0)

View File

@ -27,9 +27,10 @@ import os
import json
import time
import sqlite3
import hashlib
from pathlib import Path
from gi.repository import GObject, Gio
from gi.repository import GObject, Gio, GLib
from graphlib import TopologicalSorter, CycleError
from lxml import etree
@ -49,6 +50,7 @@ from .cmb_layout_property import CmbLayoutProperty
from .cmb_library_info import CmbLibraryInfo
from .cmb_type_info import CmbTypeInfo
from .cmb_objects_base import CmbSignal
from .cmb_blueprint import cmb_blueprint_decompile, cmb_blueprint_compile
from .utils import FileHash
from . import constants, utils
from cambalache import config, getLogger, _, N_
@ -276,7 +278,23 @@ class CmbProject(GObject.Object, Gio.ListModel):
return root, relpath, hexdigest
return None, None
return None, None, None
def __parse_blp_file(self, filename):
fullpath, relpath = self.__get_abs_path(filename)
with open(fullpath, "rb") as fd:
blueprint_decompiled = fd.read()
m = hashlib.sha256()
m.update(blueprint_decompiled)
hexdigest = m.hexdigest()
blueprint_compiled = cmb_blueprint_compile(blueprint_decompiled.decode())
root = etree.fromstring(blueprint_compiled)
return root, relpath, hexdigest
return None, None, None
def __get_version_comment_from_root(self, root):
comment = root.getprevious()
@ -287,7 +305,10 @@ class CmbProject(GObject.Object, Gio.ListModel):
def __load_ui_from_node(self, node):
filename, sha256 = utils.xml_node_get(node, ["filename", "sha256"])
if filename:
root, relpath, hexdigest = self.__parse_xml_file(filename)
if filename.endswith(".blp"):
root, relpath, hexdigest = self.__parse_blp_file(filename)
else:
root, relpath, hexdigest = self.__parse_xml_file(filename)
if sha256 != hexdigest:
logger.warning(f"{filename} hash mismatch, file was modified")
@ -525,36 +546,49 @@ class CmbProject(GObject.Object, Gio.ListModel):
else:
fullpath = filename
dirty = True
interface = root.getroot()
hexdigest = None
blueprint_decompiled = None
use_blp = filename.endswith(".blp")
if use_blp:
str_exported = etree.tostring(interface, pretty_print=True, encoding="UTF-8").decode("UTF-8")
blueprint_decompiled = cmb_blueprint_decompile(str_exported)
# Ensure directory exists
os.makedirs(os.path.dirname(fullpath), exist_ok=True)
original_comment, original_hash = self.__file_state.get(filename, (None, None))
if original_comment is not None:
interface = root.getroot()
if use_blp:
m = hashlib.sha256()
m.update(blueprint_decompiled.encode())
hexdigest = m.hexdigest()
else:
comment = self.__get_version_comment_from_root(interface)
new_comment = comment.text
comment.text = original_comment.text
comment = self.__get_version_comment_from_root(interface)
new_comment = comment.text
comment.text = original_comment.text
# Calculate hash
hash_file = FileHash()
root.write(hash_file, pretty_print=True, xml_declaration=True, encoding="UTF-8")
hexdigest = hash_file.hexdigest()
hash_file.close()
comment.text = new_comment
dirty = original_hash != hexdigest
if dirty:
# Dump xml to file
with open(fullpath, "wb") as fd:
hash_file = FileHash(fd)
# Calculate hash
hash_file = FileHash()
root.write(hash_file, pretty_print=True, xml_declaration=True, encoding="UTF-8")
hexdigest = hash_file.hexdigest()
hash_file.close()
comment.text = new_comment
if original_hash is None or original_hash != hexdigest:
if use_blp:
with open(fullpath, "wb") as fd:
fd.write(blueprint_decompiled.encode())
else:
# Dump xml to file
with open(fullpath, "wb") as fd:
hash_file = FileHash(fd)
root.write(hash_file, pretty_print=True, xml_declaration=True, encoding="UTF-8")
hexdigest = hash_file.hexdigest()
hash_file.close()
# Store filename and hash in node
utils.xml_node_set(node, "filename", filename)
utils.xml_node_set(node, "sha256", hexdigest)
@ -631,8 +665,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
self.__save_xml_and_update_node(ui, root, filename)
else:
# Embed UI content in project as CDATA
root = self.db.export_ui(ui_id, skip_version_comment=True)
self.__save_xml_in_node(ui, root)
root = self.db.export_ui(ui_id)
self.__save_xml_in_node(ui, root.getroot())
return ui
@ -655,8 +689,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
self.__save_xml_and_update_node(gresources, root, filename)
else:
# Embed file contents in project as CDATA
root = self.db.export_gresource(gresource_id, skip_version_comment=True)
self.__save_xml_in_node(gresources, root)
root = self.db.export_gresource(gresource_id)
self.__save_xml_in_node(gresources, root.getroot())
return gresources
@ -774,7 +808,12 @@ class CmbProject(GObject.Object, Gio.ListModel):
# Import file
self.foreign_keys = False
root, relpath, hexdigest = self.__parse_xml_file(filename)
if filename.endswith(".blp"):
root, relpath, hexdigest = self.__parse_blp_file(filename)
else:
root, relpath, hexdigest = self.__parse_xml_file(filename)
ui_id = self.db.import_from_node(root, relpath)
self.foreign_keys = True
@ -999,33 +1038,41 @@ class CmbProject(GObject.Object, Gio.ListModel):
self.db.commit()
self.history_pop()
except Exception:
logger.warning("Tried to add GResource", exc_info=True)
return None
else:
return self.__add_gresource(True, gresource_id, resource_type)
finally:
gresource = self.__add_gresource(True, gresource_id, resource_type)
gresource._update_new_parent()
return gresource
def __remove_gresource(self, gresource):
if gresource is None:
logger.warning("Tried to remove a None GResource", exc_info=True)
return
print("__remove_gresource", gresource, self.__gresource_id.get(gresource.gresource_id, None))
self.__gresource_id.pop(gresource.gresource_id, None)
self.__selection_remove(gresource)
print("SELECTION", self.__selection)
self.__gresource_id.pop(gresource.gresource_id, None)
self.emit("gresource-removed", gresource)
def remove_gresource(self, gresource):
try:
print("remove_gresource", gresource)
parent_id = gresource.parent_id
gresource._save_last_known_parent_and_position()
self.history_push(_('Remove GResource "{name}"').format(name=gresource.display_name))
self.db.execute("DELETE FROM gresource WHERE gresource_id=?;", (gresource.gresource_id,))
# Update position
if parent_id:
self.db.update_gresource_children_position(parent_id)
self.history_pop()
self.db.commit()
self.__remove_gresource(gresource)
except Exception as e:
logger.warning(f"Error removing gresource {e}", exc_info=True)
finally:
self.__remove_gresource(gresource)
gresource._remove_from_old_parent()
def get_css_providers(self):
return list(self.__css_id.values())
@ -1117,7 +1164,7 @@ class CmbProject(GObject.Object, Gio.ListModel):
except Exception as e:
logger.warning(f"Error adding object {obj_name}: {e}")
return None
else:
finally:
obj = self.__add_object(True, ui_id, object_id, obj_type, name, parent_id, position=position)
obj._update_new_parent()
return obj
@ -1283,10 +1330,17 @@ class CmbProject(GObject.Object, Gio.ListModel):
obj._property_changed(p)
def __undo_redo_do(self, undo, update_objects=None):
def get_object_position(c, row):
ui_id, parent_id, position = row[0], row[4], row[8]
parent = self.get_object_by_id(ui_id, parent_id)
return parent, position
def get_object_position(table, row):
if table == "object":
ui_id, parent_id, position = row[0], row[4], row[8]
parent = self.get_object_by_id(ui_id, parent_id)
return parent, position
elif table == "gresource":
parent_id, position = row[2], row[3]
parent = self.get_gresource_by_id(parent_id)
return parent, position
return None, None
c = self.db.cursor()
@ -1303,8 +1357,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
# Undo or Redo command
if command == "INSERT":
if table == "object":
parent, position = get_object_position(c, new_values)
if table in ["object", "gresource"]:
parent, position = get_object_position(table, new_values)
if undo:
update_objects.append((parent, position, 1, 0))
@ -1318,8 +1372,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
self.__undo_redo_update_insert_delete(c, undo, command, table, columns, table_pk, old_values, new_values)
elif command == "DELETE":
if table == "object":
parent, position = get_object_position(c, old_values)
if table in ["object", "gresource"]:
parent, position = get_object_position(table, old_values)
if undo:
update_objects.append((parent, position, 0, 1))
@ -1335,8 +1389,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
elif command == "UPDATE":
# parent_id and position have to change together because their are part of a unique index
if update_objects is not None and table == "object" and "position" in columns and "parent_id" in columns:
old_parent, old_position = get_object_position(c, old_values)
new_parent, new_position = get_object_position(c, new_values)
old_parent, old_position = get_object_position(table, old_values)
new_parent, new_position = get_object_position(table, new_values)
if undo:
if old_position >= 0:
@ -1348,6 +1402,9 @@ class CmbProject(GObject.Object, Gio.ListModel):
update_objects.append((new_parent, new_position, 0, 1))
if old_position >= 0:
update_objects.append((old_parent, old_position, 1, 0))
elif table == "gresource":
# TODO
pass
if undo:
self.db.history_update(table, columns, table_pk, old_values)
@ -1476,12 +1533,27 @@ class CmbProject(GObject.Object, Gio.ListModel):
else:
obj._remove_data(data)
else:
parent = obj.data_dict.get(f"{row[2]}.{row[6]}", None)
owner_id, data_id, id, parent_id = row[2], row[3], row[4], row[6]
parent = obj.data_dict.get(f"{owner_id}.{parent_id}", None)
if parent:
parent._add_child(row[2], row[3], row[4])
parent._add_child(owner_id, data_id, id)
else:
obj._add_data(row[2], row[3], row[4])
info = self.type_info.get(owner_id)
taginfo = None
if info:
r = self.db.execute(
"SELECT key FROM type_data WHERE owner_id=? AND data_id=?;",
(owner_id, data_id)
).fetchone()
data_key = r[0] if r else None
if data_key:
taginfo = info.get_data_info(data_key)
obj._add_data(owner_id, data_id, id, info=taginfo)
elif table == "object_data_arg":
obj = self.get_object_by_id(pk[0], pk[1])
if obj:
@ -2208,6 +2280,7 @@ class CmbProject(GObject.Object, Gio.ListModel):
i += 1
item.path_parent = None
self.__items.insert(i, item)
self.items_changed(i, 0, 1)
@ -2263,23 +2336,45 @@ class CmbProject(GObject.Object, Gio.ListModel):
path_parent = item.path_parent
# Do not do anything if the path is the same
if path_parent and path_parent.path and path_parent.path == os.path.dirname(filename):
return
# Remove item
self.__remove_item(item)
# add it again
self.__add_item(item, filename)
if in_selection:
self.set_selection([item])
GLib.idle_add(self.__set_selection_idle, item)
# Clear unused paths
if path_parent.n_items == 0:
while path_parent is not None:
next_parent = path_parent.path_parent
if path_parent and path_parent.n_items == 0:
GLib.idle_add(self.__clear_unused_paths_idle, path_parent)
if path_parent.n_items <= 1:
logger.warning(path_parent)
def __set_selection_idle(self, item):
self.set_selection([item])
return GLib.SOURCE_REMOVE
path_parent = next_parent
def __clear_unused_paths_idle(self, path_parent):
if path_parent.n_items:
return
while path_parent is not None:
next_parent = path_parent.path_parent
if path_parent.n_items != 1:
break
path_parent = next_parent
if path_parent:
if path_parent.path_parent:
path_parent.path_parent.remove_item(path_parent)
else:
self.__remove_item(path_parent)
return GLib.SOURCE_REMOVE
def do_ui_added(self, ui):
self.__add_item(ui, ui.filename)

View File

@ -331,7 +331,7 @@ class CmbTypeInfo(CmbBaseTypeInfo):
return False
def enum_get_value_as_string(self, value):
def enum_get_value_as_string(self, value, use_nick=True):
if self.parent_id != "enum":
return None
@ -340,7 +340,7 @@ class CmbTypeInfo(CmbBaseTypeInfo):
# Always use nick as value
if value == enum_name or value == enum_nick or value == str(enum_value):
return enum_nick
return enum_nick if use_nick else enum_value
return None

View File

@ -65,6 +65,10 @@ class CmbUI(CmbBaseUI, Gio.ListModel):
def __on_notify(self, obj, pspec):
self.project._ui_changed(self, pspec.name)
# Update display name if one of the following properties changed
if pspec.name in ["filename", "template-id"]:
self.notify("display-name")
def list_libraries(self):
retval = {}

View File

@ -23,8 +23,11 @@
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
from gi.repository import GObject, Gtk
from cambalache import _
from .cmb_ui import CmbUI
@ -33,6 +36,7 @@ class CmbUIEditor(Gtk.Grid):
__gtype_name__ = "CmbUIEditor"
filename = Gtk.Template.Child()
format = Gtk.Template.Child()
template_id = Gtk.Template.Child()
description = Gtk.Template.Child()
copyright = Gtk.Template.Child()
@ -54,9 +58,6 @@ class CmbUIEditor(Gtk.Grid):
@object.setter
def _set_object(self, obj):
if obj == self._object:
return
for binding in self._bindings:
binding.unbind()
@ -79,9 +80,15 @@ class CmbUIEditor(Gtk.Grid):
self.template_id.object = obj
self.filename.dirname = obj.project.dirname
# Set some default name
self.filename.unnamed_filename = _("unnamed.ui")
if not obj.filename and obj.template_id:
template = obj.project.get_object_by_id(obj.ui_id, obj.template_id)
if template:
self.filename.unnamed_filename = f"{template.name}.ui".lower()
for field in self.fields:
binding = GObject.Object.bind_property(
obj,
binding = obj.bind_property(
field,
getattr(self, field),
"cmb-value",
@ -89,5 +96,42 @@ class CmbUIEditor(Gtk.Grid):
)
self._bindings.append(binding)
if obj.project.target_tk == "gtk-4.0":
self.filename.mime_types = "application/x-gtk-builder;text/x-blueprint"
# filename -> format
binding = obj.bind_property(
"filename",
self.format,
"selected",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
transform_to=self.__filename_to_format,
transform_from=self.__format_to_filename,
user_data=obj
)
self._bindings.append(binding)
self.format.show()
self.format.set_sensitive(bool(obj.filename))
else:
self.filename.mime_types = "application/x-gtk-builder;application/x-glade"
self.format.hide()
def __filename_to_format(self, binding, source_value, ui):
if not source_value:
self.format.props.sensitive = False
return 0
self.format.props.sensitive = True
return 1 if source_value.endswith(".blp") else 0
def __format_to_filename(self, binding, target_value, ui):
if not ui.filename:
self.format.props.sensitive = False
return None
self.format.props.sensitive = True
return os.path.splitext(ui.filename)[0] + (".blp" if target_value == 1 else ".ui")
Gtk.WidgetClass.set_css_name(CmbUIEditor, "CmbUIEditor")

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<!-- Created with Cambalache 0.97.1 -->
<interface>
<!-- interface-name cmb_ui_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
@ -27,7 +27,7 @@
<property name="label" translatable="yes">Description:</property>
<layout>
<property name="column">0</property>
<property name="row">2</property>
<property name="row">3</property>
</layout>
</object>
</child>
@ -37,7 +37,7 @@
<property name="label" translatable="yes">Copyright:</property>
<layout>
<property name="column">0</property>
<property name="row">3</property>
<property name="row">4</property>
</layout>
</object>
</child>
@ -47,7 +47,7 @@
<property name="label" translatable="yes">Authors:</property>
<layout>
<property name="column">0</property>
<property name="row">4</property>
<property name="row">5</property>
</layout>
</object>
</child>
@ -57,7 +57,7 @@
<property name="label" translatable="yes">Domain:</property>
<layout>
<property name="column">0</property>
<property name="row">5</property>
<property name="row">6</property>
</layout>
</object>
</child>
@ -78,7 +78,7 @@
<property name="visible">True</property>
<layout>
<property name="column">1</property>
<property name="row">5</property>
<property name="row">6</property>
</layout>
</object>
</child>
@ -95,7 +95,7 @@
<property name="min-content-height">96</property>
<layout>
<property name="column">1</property>
<property name="row">2</property>
<property name="row">3</property>
</layout>
</object>
</child>
@ -112,7 +112,7 @@
<property name="min-content-height">96</property>
<layout>
<property name="column">1</property>
<property name="row">4</property>
<property name="row">5</property>
</layout>
</object>
</child>
@ -124,7 +124,7 @@
<property name="visible">True</property>
<layout>
<property name="column">1</property>
<property name="row">1</property>
<property name="row">2</property>
</layout>
</object>
</child>
@ -134,7 +134,7 @@
<property name="label" translatable="yes">Template:</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
<property name="row">2</property>
</layout>
</object>
</child>
@ -151,7 +151,7 @@
<property name="min-content-height">96</property>
<layout>
<property name="column">1</property>
<property name="row">3</property>
<property name="row">4</property>
</layout>
</object>
</child>
@ -161,7 +161,7 @@
<property name="label" translatable="yes">Comment:</property>
<layout>
<property name="column">0</property>
<property name="row">6</property>
<property name="row">7</property>
</layout>
</object>
</child>
@ -178,7 +178,34 @@
<property name="min-content-height">96</property>
<layout>
<property name="column">1</property>
<property name="row">6</property>
<property name="row">7</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Format:</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkDropDown" id="format">
<property name="halign">start</property>
<property name="model">
<object class="GtkStringList">
<property name="strings">Gtk Builder
Blueprint</property>
</object>
</property>
<layout>
<property name="column">1</property>
<property name="column-span">1</property>
<property name="row">1</property>
<property name="row-span">1</property>
</layout>
</object>
</child>

View File

@ -49,4 +49,9 @@ class CmbVersionNotificationView(Gtk.Box):
notification = self.notification
self.version_label.props.label = _("<b>Version {version} is available</b>").format(version=notification.version)
self.release_notes_label.props.label = notification.release_notes
self.read_more_button.props.uri = notification.read_more_url
if notification.read_more_url:
self.read_more_button.props.uri = notification.read_more_url
self.read_more_button.show()
else:
self.read_more_button.hide()

View File

@ -39,8 +39,9 @@ from . import config
from .cmb_ui import CmbUI
from .cmb_object import CmbObject
from .cmb_context_menu import CmbContextMenu
from cambalache.cmb_blueprint import cmb_blueprint_decompile
from . import utils
from cambalache import getLogger, _
from cambalache import getLogger, _, N_
logger = getLogger(__name__)
@ -167,7 +168,6 @@ class CmbMerengueProcess(GObject.Object):
env = json.loads(os.environ.get("MERENGUE_DEV_ENV", "{}"))
env = env | {
"GDK_BACKEND": "wayland",
"GSK_RENDERER": "cairo",
"WAYLAND_DISPLAY": self.wayland_display,
}
@ -279,7 +279,7 @@ class CmbView(Gtk.Box):
def __init__(self, **kwargs):
self.__project = None
self.__ui_id = 0
self.__ui = None
self.__theme = None
self.menu = self.__create_context_menu()
@ -337,14 +337,34 @@ class CmbView(Gtk.Box):
return self.__project.db.tostring(ui_id, merengue=merengue)
def __update_view(self):
if self.__project and self.__ui_id > 0:
if self.__project and self.__ui:
if self.stack.props.visible_child_name == "ui_xml":
ui = self.__get_ui_xml(self.__ui_id)
self.text_view.buffer.set_text(ui)
ui_source = self.__get_ui_xml(self.__ui.ui_id)
if self.__ui.filename and self.__ui.filename.endswith(".blp"):
try:
ui_source = cmb_blueprint_decompile(ui_source)
self.text_view.lang = "blueprint"
except Exception as e:
ui_source = _("Error exporting project")
ui_source += "\n"
ui_source += N_(
"blueprintcompiler encounter the following error:",
"blueprintcompiler encounter the following errors:",
len(e.errors)
)
ui_source += "\n"
ui_source += str(e)
self.text_view.lang = ""
# TODO: forward error to parent to show to user
else:
self.text_view.lang = "xml"
self.text_view.buffer.set_text(ui_source)
return
self.text_view.buffer.set_text("")
self.__ui_id = 0
self.__ui = None
def __get_ui_dirname(self, ui_id):
dirname = GLib.get_home_dir()
@ -448,19 +468,23 @@ class CmbView(Gtk.Box):
if len(selection) > 0:
obj = selection[0]
if type(obj) not in [CmbUI, CmbObject]:
if isinstance(obj, CmbUI):
ui = obj
elif isinstance(obj, CmbObject):
ui = obj.ui
else:
return
ui_id = obj.ui_id
if self.__ui_id != ui_id:
self.__ui_id = ui_id
self.__merengue_update_ui(ui_id)
if self.__ui != ui:
self.__ui = ui
self.__merengue_update_ui(ui.ui_id)
objects = self.__get_selection_objects(selection, ui_id)
objects = self.__get_selection_objects(selection, ui.ui_id)
self.__merengue_command("selection_changed", args={"ui_id": ui_id, "selection": objects})
else:
self.__ui_id = 0
self.__ui = None
self.__merengue_update_ui(0)
self.__update_view()
@ -632,7 +656,7 @@ class CmbView(Gtk.Box):
self.__merengue_last_exit = None
return
self.__ui_id = 0
self.__ui = None
self.__merengue.start()
def __command_selection_changed(self, selection):
@ -691,7 +715,7 @@ class CmbView(Gtk.Box):
self.__load_css_providers()
self.__ui_id = 0
self.__ui = None
self.__on_project_selection_changed(self.__project)
elif command == "placeholder_selected":
self.emit(

View File

@ -35,36 +35,56 @@ class CmbFileButton(Gtk.Button):
dirname = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
dialog_title = GObject.Property(type=str, default=_("Select filename"), flags=GObject.ParamFlags.READWRITE)
accept_label = GObject.Property(type=str, default=_("Select"), flags=GObject.ParamFlags.READWRITE)
unnamed_filename = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
use_open = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
label = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.__filename = None
self.__filters = None
@Gtk.Template.Callback("on_button_clicked")
def __on_button_clicked(self, button):
dialog = Gtk.FileDialog(
modal=True,
title=self.dialog_title
filters=self.__filters,
title=self.dialog_title,
accept_label=self.accept_label
)
if self.dirname is not None:
if self.__filename is not None:
if self.__filename:
fullpath = os.path.join(self.dirname, self.__filename)
dialog.set_initial_file(Gio.File.new_for_path(fullpath))
file = Gio.File.new_for_path(fullpath)
dialog.set_initial_file(file)
# See which filter matches the file info and use it as default
if file.query_exists(None):
info = file.query_info(Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, Gio.FileQueryInfoFlags.NONE, None)
for filter in self.__filters:
if filter.match(info):
dialog.set_default_filter(filter)
break
else:
dialog.set_initial_folder(Gio.File.new_for_path(self.dirname))
# dialog.set_initial_name("unnamed.ui")
if self.unnamed_filename:
dialog.set_initial_name(self.unnamed_filename)
def dialog_callback(dialog, res):
try:
file = dialog.save_finish(res)
file = dialog.open_finish(res) if self.use_open else dialog.save_finish(res)
self.cmb_value = os.path.relpath(file.get_path(), start=self.dirname)
except Exception:
pass
dialog.save(self.get_root(), None, dialog_callback)
if self.use_open:
dialog.open(self.get_root(), None, dialog_callback)
else:
dialog.save(self.get_root(), None, dialog_callback)
@GObject.Property(type=str)
def cmb_value(self):
@ -77,3 +97,18 @@ class CmbFileButton(Gtk.Button):
self.__filename = value if value is not None else ""
self.label.set_label(self.__filename)
@GObject.Property(type=str)
def mime_types(self):
if self.__filters:
return ";".join([f.props.mime_types for f in self.__filters])
return ""
@mime_types.setter
def _set_mime_types(self, value):
if value:
self.__filters = Gio.ListStore()
for mime in value.split(';'):
self.__filters.append(Gtk.FileFilter(mime_types=[mime]))
else:
self.__filters = None

View File

@ -40,7 +40,8 @@ class CmbSourceView(GtkSource.View):
@GObject.Property(type=str)
def lang(self):
return self.buffer.get_language()
language = self.buffer.get_language()
return language.get_id() if language else ""
@lang.setter
def _set_lang(self, value):

View File

@ -26,6 +26,7 @@ configure_file(
install_data([
'cmb_accessible_editor.py',
'cmb_base.py',
'cmb_blueprint.py',
'cmb_context_menu.py',
'cmb_css.py',
'cmb_css_editor.py',

View File

@ -1,11 +1,7 @@
#!/bin/bash
source .local.env
SCRIPT=$(readlink -f $0)
DIRNAME=$(dirname $SCRIPT)
export GSETTINGS_BACKEND=memory
export HOME=$DIRNAME/.local/home
mkdir -p $HOME/Projects
export COVERAGE_PROCESS_START=$DIRNAME/pyproject.toml

View File

@ -1,6 +1,6 @@
project(
'cambalache', 'c',
version: '0.96.0',
version: '0.97.3',
meson_version: '>= 1.1.0',
default_options: [
'c_std=c11',
@ -29,7 +29,7 @@ privatecmb_catalog_gendir = join_paths(get_option('prefix'), get_option('libdir'
libxml2_dep = dependency('libxml-2.0', version: '>= 2.9.0')
pygobject_dep = dependency('pygobject-3.0', version: '>= 3.52.0')
gtk4_dep = dependency('gtk4', version: '>= 4.18.0')
casilda_dep = dependency('casilda-0.1', version: '>= 0.2.0', fallback: ['casilda', 'casilda_dep'])
casilda_dep = dependency('casilda-0.1', version: '>= 0.9.0', fallback: ['casilda', 'casilda_dep'])
adw_dep = dependency('libadwaita-1', version: '>= 1.7.0')
gtksource_dep = dependency('gtksourceview-5', version: '>= 5.16.0')

39
run-dev.py Executable file
View File

@ -0,0 +1,39 @@
#!/bin/python3
#
# run-dev - Script to run Cambalache from sources
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# 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; version 2 of the License.
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
import os
import sys
import locale
from tools.cmb_init_dev import cmb_init_dev
# Compile deps and install things in .local
cmb_init_dev()
basedir = os.path.join(os.path.split(os.path.dirname(__file__))[0])
locale.bindtextdomain("cambalache", os.path.join(basedir, ".local", "share", "locale"))
locale.textdomain("cambalache")
from cambalache.app import CmbApplication # noqa E402
CmbApplication().run(sys.argv)

View File

@ -1,16 +0,0 @@
#!/usr/bin/bash
source .local.env
if python3 $DIRNAME/tools/cmb_init_dev.py; then
python3 - $@ << EOF
import sys
import locale
locale.bindtextdomain("cambalache", "$DIRNAME/.local/share/locale")
locale.textdomain("cambalache")
from cambalache.app import CmbApplication
CmbApplication().run(sys.argv)
EOF
else
echo Could not initialize dev environment
fi

View File

@ -1,10 +0,0 @@
#!/usr/bin/bash
source .local.env
SCRIPT=$(readlink -f $0)
DIRNAME=$(dirname $SCRIPT)
export GSETTINGS_BACKEND=memory
export HOME=$DIRNAME/.local/home
mkdir -p $HOME/Projects
pytest $@

View File

@ -1,5 +1,5 @@
[wrap-git]
directory = casilda
url = https://gitlab.gnome.org/jpu/casilda.git
revision = 0.2.0
revision = 0.9.0
depth = 1

View File

@ -1,5 +1,15 @@
import os
import gi
basedir = os.path.join(os.path.split(os.path.dirname(__file__))[0])
# Ensure home directory
homedir = os.path.join(basedir, ".local", "home")
os.makedirs(os.path.join(homedir, "Projects"), exist_ok=True)
os.environ["GSETTINGS_BACKEND"] = "memory"
os.environ["HOME"] = homedir
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk # noqa E402
from tools.cmb_init_dev import cmb_init_dev # noqa E402

View File

@ -6,9 +6,9 @@
<signal name="activate-default" handler="on_activate_default"/>
<signal name="activate-default" handler="on_activate_default2"/>
<signal name="add" handler="on_add"/>
<signal name="focus" handler="on_focus" swapped="yes"/>
<signal name="focus-in-event" handler="on_focus_in_event" after="yes"/>
<signal name="focus-out-event" handler="on_focus_out_event" swapped="yes" after="yes"/>
<signal name="focus" handler="on_focus" swapped="True"/>
<signal name="focus-in-event" handler="on_focus_in_event" after="True"/>
<signal name="focus-out-event" handler="on_focus_out_event" swapped="True" after="True"/>
</object>
<object class="GtkDialog" id="dialog1"/>
</interface>

View File

@ -26,7 +26,7 @@
<child>
<object class="GtkLabel" id="label">
<accessibility>
<property name="help-text">help text</property>
<property name="description">help text</property>
<property name="label">a label</property>
<relation name="described-by">a11y3</relation>
<relation name="details">a11y2</relation>

View File

@ -9,11 +9,11 @@
<child>
<object class="GtkButton">
<signal name="activate" handler="on_button_activate"/>
<signal name="clicked" handler="on_button_clicked" swapped="yes"/>
<signal name="clicked" handler="on_button_clicked2" after="yes"/>
<signal name="clicked" handler="on_button_clicked3" swapped="yes" after="yes"/>
<signal name="clicked" handler="on_button_clicked" swapped="True"/>
<signal name="clicked" handler="on_button_clicked2" after="True"/>
<signal name="clicked" handler="on_button_clicked3" swapped="True" after="True"/>
<signal name="clicked" handler="on_button_clicked4" object="win1"/>
<signal name="clicked" handler="on_button_clicked5" object="win1" swapped="no"/>
<signal name="clicked" handler="on_button_clicked5" swapped="False" object="win1"/>
<signal name="notify::label" handler="on_notify"/>
</object>
</child>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<interface>
<!-- interface-name string_list.ui -->
<requires lib="gtk" version="4.8"/>
<requires lib="gtk" version="4.10"/>
<object class="GtkStringList" id="list1">
<property name="strings">a
b

109
tests/test_cmb_blueprint.py Normal file
View File

@ -0,0 +1,109 @@
#!/usr/bin/pytest
"""
import .ui files into cambalache and compare it to blueprint compiler output
"""
import os
import pytest
from lxml import etree
from cambalache import CmbProject
from cambalache.cmb_blueprint import cmb_blueprint_decompile, cmb_blueprint_compile, CmbBlueprintUnsupportedError
def tostring(ui):
db = ui.project.db
# Internal API to ensure Cambalache outputs the same as blueprint compiler
db._output_lowercase_boolean = True
db._output_use_enum_value = True
tree = db.export_ui(ui.ui_id)
if tree is None:
return None
root = tree.getroot()
# Remove all XML comments since they are not supported by blueprint
for node in root.xpath("//comment()"):
parent = node.getparent()
if parent is not None:
parent.remove(node)
for node in root.iterfind("requires"):
lib = node.get("lib", None)
if lib != "gtk":
root.remove(node)
return etree.tostring(root, pretty_print=True, encoding="UTF-8").decode("UTF-8")
def get_exported_and_blueprint(filename):
"""
import .ui file and compare it with the exported version
"""
path = os.path.join(os.path.dirname(__file__), "gtk-4.0", filename)
project = CmbProject(target_tk="gtk-4.0")
ui, msgs, detail_msg = project.import_file(path)
assert (ui)
assert (msgs is None)
assert (detail_msg is None)
# Export Cambalache UI
str_exported = tostring(ui)
# Decompile and recompile UI to Blueprint
blueprint_decompiled = cmb_blueprint_decompile(str_exported)
blueprint_compiled = cmb_blueprint_compile(blueprint_decompiled)
assert blueprint_compiled is not None
# Remove blueprint DO NOT EDIT comment
root = etree.fromstring(blueprint_compiled)
blueprint_compiled = etree.tostring(root, pretty_print=True, encoding="UTF-8").decode("UTF-8")
return str_exported, blueprint_compiled
@pytest.mark.parametrize("filename", [
"window.ui",
"children.ui",
"layout.ui",
"signals.ui",
"template.ui",
"inline_object.ui",
"stack_page.ui",
"comboboxtext.ui",
"style.ui",
"filefilter.ui",
"menu.ui",
# help-text is not an accessibility property
"accessibility.ui",
# bind-flags missing
# "bindings.ui",
# Translation comment missing
# "string_list.ui",
])
def test_assert_exported_and_compiled(filename):
"""
import .ui file and compare it with the exported version
"""
str_exported, blueprint_compiled = get_exported_and_blueprint(filename)
# Compare blueprint generated string with cambalache
assert blueprint_compiled.strip() == str_exported.strip()
@pytest.mark.parametrize("filename", [
"liststore.ui",
"treestore.ui",
"label.ui",
])
def test_assert_exported_and_compiled_unsupported(filename):
with pytest.raises(CmbBlueprintUnsupportedError):
str_exported, blueprint_compiled = get_exported_and_blueprint(filename)

View File

@ -13,7 +13,7 @@ from . import utils as test_utils
DAY = 3600 * 24
now = utils.utcnow()
CMB_UUID = str(uuid4())
CMB_UUID = notification_center.uuid
POLL_UUID = str(uuid4())
POLL_NOTIFICATION_BASE = {
@ -44,16 +44,14 @@ def wait_for_all_threads():
def test_cmb_notification_disabled():
assert not notification_center.uuid
assert notification_center.enabled is False
assert len(notification_center.store) == 0
notification_center.enabled = True
@pytest.mark.parametrize("response, headers, n", [
@pytest.mark.parametrize("response, n", [
(
{
"uuid": CMB_UUID,
"notification": {
"type": "version",
"start_date": now - DAY,
@ -63,14 +61,10 @@ def test_cmb_notification_disabled():
"read_more_url": "http://localhost"
}
},
{
'User-Agent': notification_center.user_agent
},
1
),
(
{
"uuid": CMB_UUID,
"notification": {
"type": "message",
"start_date": now - DAY,
@ -79,26 +73,16 @@ def test_cmb_notification_disabled():
"message": "This is a message notification"
}
},
{
'User-Agent': notification_center.user_agent,
'x-cambalache-uuid': CMB_UUID
},
2
),
(
{
"uuid": CMB_UUID,
"notification": POLL_NOTIFICATION_BASE
},
{
'User-Agent': notification_center.user_agent,
'x-cambalache-uuid': CMB_UUID
},
3
),
(
{
"uuid": CMB_UUID,
"notification": {
**POLL_NOTIFICATION_BASE,
"results": {
@ -107,14 +91,10 @@ def test_cmb_notification_disabled():
}
}
},
{
'User-Agent': notification_center.user_agent,
'x-cambalache-uuid': CMB_UUID
},
4
)
])
def test_cmb_notification_get(mocker, response, headers, n):
def test_cmb_notification_get(mocker, response, n):
wait_for_all_threads()
mocker.patch(
@ -137,7 +117,14 @@ def test_cmb_notification_get(mocker, response, headers, n):
test_utils.process_all_pending_gtk_events()
request_mock.assert_called_with("GET", "/notification", headers=headers)
request_mock.assert_called_with(
"GET",
"/notification",
headers={
'User-Agent': notification_center.user_agent,
'x-cambalache-uuid': CMB_UUID
}
)
assert notification_center.uuid == CMB_UUID
on_new_notification.assert_called()

View File

@ -4,11 +4,52 @@
import .ui files into cambalache and export to compare results
"""
import os
import pytest
from cambalache import CmbProject, config
def assert_original_and_exported(target_tk, filename):
@pytest.mark.parametrize("target_tk,filename", [
("gtk+-3.0", "window.ui"),
("gtk+-3.0", "children.ui"),
("gtk+-3.0", "packing.ui"),
("gtk+-3.0", "signals.ui"),
("gtk+-3.0", "template.ui"),
("gtk+-3.0", "comboboxtext.ui"),
("gtk+-3.0", "dialog.ui"),
("gtk+-3.0", "label.ui"),
("gtk+-3.0", "levelbar.ui"),
("gtk+-3.0", "liststore.ui"),
("gtk+-3.0", "scale.ui"),
("gtk+-3.0", "sizegroup.ui"),
("gtk+-3.0", "style.ui"),
("gtk+-3.0", "treestore.ui"),
("gtk+-3.0", "filefilter.ui"),
("gtk+-3.0", "custom_fragment.ui"),
("gtk+-3.0", "bindings.ui"),
("gtk+-3.0", "menu.ui"),
("gtk+-3.0", "accessibility.ui"),
("gtk-4.0", "window.ui"),
("gtk-4.0", "children.ui"),
("gtk-4.0", "layout.ui"),
("gtk-4.0", "signals.ui"),
("gtk-4.0", "template.ui"),
("gtk-4.0", "inline_object.ui"),
("gtk-4.0", "stack_page.ui"),
("gtk-4.0", "liststore.ui"),
("gtk-4.0", "treestore.ui"),
("gtk-4.0", "comboboxtext.ui"),
("gtk-4.0", "style.ui"),
("gtk-4.0", "label.ui"),
("gtk-4.0", "filefilter.ui"),
("gtk-4.0", "custom_fragment.ui"),
("gtk-4.0", "bindings.ui"),
("gtk-4.0", "menu.ui"),
("gtk-4.0", "string_list.ui"),
("gtk-4.0", "accessibility.ui"),
("gtk-4.0", "ui_comments.ui")
])
def test_(target_tk, filename):
"""
import .ui file and compare it with the exported version
"""
@ -29,160 +70,3 @@ def assert_original_and_exported(target_tk, filename):
assert str_exported == str_original
#
# Gtk+ 3.0 Tests
#
def test_gtk3_window():
assert_original_and_exported("gtk+-3.0", "window.ui")
def test_gtk3_children():
assert_original_and_exported("gtk+-3.0", "children.ui")
def test_gtk3_packing():
assert_original_and_exported("gtk+-3.0", "packing.ui")
def test_gtk3_signals():
assert_original_and_exported("gtk+-3.0", "signals.ui")
def test_gtk3_template():
assert_original_and_exported("gtk+-3.0", "template.ui")
def test_gtk3_comboboxtext():
assert_original_and_exported("gtk+-3.0", "comboboxtext.ui")
def test_gtk3_dialog():
assert_original_and_exported("gtk+-3.0", "dialog.ui")
def test_gtk3_label():
assert_original_and_exported("gtk+-3.0", "label.ui")
def test_gtk3_levelbar():
assert_original_and_exported("gtk+-3.0", "levelbar.ui")
def test_gtk3_liststore():
assert_original_and_exported("gtk+-3.0", "liststore.ui")
def test_gtk3_scale():
assert_original_and_exported("gtk+-3.0", "scale.ui")
def test_gtk3_sizegroup():
assert_original_and_exported("gtk+-3.0", "sizegroup.ui")
def test_gtk3_style():
assert_original_and_exported("gtk+-3.0", "style.ui")
def test_gtk3_treestore():
assert_original_and_exported("gtk+-3.0", "treestore.ui")
def test_gtk3_filefilter():
assert_original_and_exported("gtk+-3.0", "filefilter.ui")
def test_gtk3_custom_fragment():
assert_original_and_exported("gtk+-3.0", "custom_fragment.ui")
def test_gtk3_bindings():
assert_original_and_exported("gtk+-3.0", "bindings.ui")
def test_gtk3_menu():
assert_original_and_exported("gtk+-3.0", "menu.ui")
def test_gtk3_accessibility():
assert_original_and_exported("gtk+-3.0", "accessibility.ui")
#
# Gtk 4.0 Tests
#
def test_gtk4_window():
assert_original_and_exported("gtk-4.0", "window.ui")
def test_gtk4_children():
assert_original_and_exported("gtk-4.0", "children.ui")
def test_gtk4_layout():
assert_original_and_exported("gtk-4.0", "layout.ui")
def test_gtk4_signals():
assert_original_and_exported("gtk-4.0", "signals.ui")
def test_gtk4_template():
assert_original_and_exported("gtk-4.0", "template.ui")
def test_gtk4_inline_object():
assert_original_and_exported("gtk-4.0", "inline_object.ui")
def test_gtk4_stack_page():
assert_original_and_exported("gtk-4.0", "stack_page.ui")
def test_gtk4_liststore():
assert_original_and_exported("gtk-4.0", "liststore.ui")
def test_gtk4_treestore():
assert_original_and_exported("gtk-4.0", "treestore.ui")
def test_gtk4_comboboxtext():
assert_original_and_exported("gtk-4.0", "comboboxtext.ui")
def test_gtk4_style():
assert_original_and_exported("gtk-4.0", "style.ui")
def test_gtk4_label():
assert_original_and_exported("gtk-4.0", "label.ui")
def test_gtk4_filefilter():
assert_original_and_exported("gtk-4.0", "filefilter.ui")
def test_gtk4_custom_fragment():
assert_original_and_exported("gtk-4.0", "custom_fragment.ui")
def test_gtk4_bindings():
assert_original_and_exported("gtk-4.0", "bindings.ui")
def test_gtk4_menu():
assert_original_and_exported("gtk-4.0", "menu.ui")
def test_gtk4_string_list():
assert_original_and_exported("gtk-4.0", "string_list.ui")
def test_gtk4_accessibility():
assert_original_and_exported("gtk-4.0", "accessibility.ui")
def test_gtk4_ui_comments():
assert_original_and_exported("gtk-4.0", "ui_comments.ui")

View File

@ -2,7 +2,7 @@
#
# Cambalache UI Maker developer mode
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
# Copyright (C) 2021-2025 Juan Pablo Ugarte
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
@ -22,18 +22,46 @@
#
import os
import gi
import sys
import stat
import signal
import subprocess
basedir = os.path.join(os.path.split(os.path.dirname(__file__))[0])
sys.path.insert(1, basedir)
cambalachedir = os.path.join(basedir, "cambalache")
localdir = os.path.join(basedir, ".local")
locallibdir = os.path.join(localdir, "lib", sys.implementation._multiarch)
cambalachedir = os.path.join(basedir, "cambalache")
localpkgdatadir = os.path.join(localdir, "share", "cambalache")
catalogsdir = os.path.join(localpkgdatadir, "catalogs")
localbindir = os.path.join(localdir, "bin")
repository = gi.Repository.get_default()
LD_LIBRARY_PATH = [locallibdir, f"{locallibdir}/cambalache", f"{locallibdir}/cmb_catalog_gen"]
for path in LD_LIBRARY_PATH:
repository.prepend_library_path(path)
GI_TYPELIB_PATH = [f"{locallibdir}/girepository-1.0", f"{locallibdir}/cambalache", f"{locallibdir}/cmb_catalog_gen"]
for path in GI_TYPELIB_PATH:
repository.prepend_search_path(path)
for var, value in [
("LD_LIBRARY_PATH", ":".join(LD_LIBRARY_PATH)),
("GI_TYPELIB_PATH", ":".join(GI_TYPELIB_PATH)),
("PKG_CONFIG_PATH", os.path.join(locallibdir, "pkgconfig")),
("GSETTINGS_SCHEMA_DIR", os.path.join(localdir, "share", "glib-2.0", "schemas")),
("XDG_DATA_DIRS", os.path.join(localdir, "share")),
("PYTHONPATH", os.path.join(localdir, "lib", "python3", "dist-packages"))
]:
if var in os.environ:
old_value = os.environ[var]
os.environ[var] = f"{value}:{old_value}"
else:
os.environ[var] = value
sys.path.insert(1, basedir)
sys.path.insert(1, localbindir)
from gi.repository import GLib # noqa: E402