Compare commits

...

10 Commits
main ... 0.96.1

Author SHA1 Message Date
Juan Pablo Ugarte
ae11d6485a
Rolling 0.96.1 2025-05-16 08:33:13 -04:00
Juan Pablo Ugarte
a69df48d17
CmbVersionNotificationView: do not show read more button if there is no link 2025-05-15 18:29:26 -04:00
Juan Pablo Ugarte
090ef7f633
CmbObjectDataEditor: fix object data remove. 2025-05-14 17:19:13 -04:00
Juan Pablo Ugarte
4cce14ea3f
CmbProject: fix GResource list model update 2025-05-13 19:29:57 -04:00
Juan Pablo Ugarte
7ebfa1999c
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-13 19:29:57 -04:00
Juan Pablo Ugarte
d81f8ee7fd
CmbResourceEditor: use open for choosing a file 2025-05-13 19:29:57 -04:00
Juan Pablo Ugarte
aef72f38cc
CmbDB: add update_gresource_children_position() 2025-05-13 19:29:57 -04:00
Juan Pablo Ugarte
3527266b4a
CmbFileButton: add use_open property 2025-05-13 19:29:57 -04:00
Juan Pablo Ugarte
d8f59a9144
run-dev.py: Misc cleanups
- Make tests work without any special env set
 - Remove .local.env bash env
 - Simplify coverage script
2025-05-12 20:37:53 -04:00
Juan Pablo Ugarte
c8a3f36641
CmbNotification: generate UUID locally 2025-05-12 20:35:46 -04:00
21 changed files with 243 additions and 119 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

@ -9,6 +9,11 @@ Cambalache used even/odd minor numbers to differentiate between stable and
development releases. development releases.
## 0.96.1
- Fix/improve GResource list model update
- Fix removing custom class data like styles
## 0.96.0 ## 0.96.0
- Add GResource support - Add GResource support

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 run the app from the source directory. (Follow manual installation to ensure
you have everything needed) you have everything needed)
`./run-dev.sh` `./run-dev.py`
This is meant for Cambalache development only. This is meant for Cambalache development only.

View File

@ -221,7 +221,10 @@
<ui template-class="CmbContextMenu" filename="cmb_context_menu.ui" sha256="81eba3adf715348a5c03ef4cbc151eebd5d9aa8b5a14c5968232f68a61ae573c"/> <ui template-class="CmbContextMenu" filename="cmb_context_menu.ui" sha256="81eba3adf715348a5c03ef4cbc151eebd5d9aa8b5a14c5968232f68a61ae573c"/>
<ui template-class="CmbDBInspector" filename="cmb_db_inspector.ui" sha256="4451cdb08d24bd4a802ea692c0ebb4ef46af13152984c0b435d29bf4eb7dab55"/> <ui template-class="CmbDBInspector" filename="cmb_db_inspector.ui" sha256="4451cdb08d24bd4a802ea692c0ebb4ef46af13152984c0b435d29bf4eb7dab55"/>
<ui filename="app/cmb_shortcuts.ui" sha256="d7ac37fd2430788a9e210ed4bc84dcfeba5609bdcc801afb192bfd900c7a8883"/> <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="CmbNotificationListView" filename="cmb_notification_list_view.ui" sha256="13622645038ef2aaa154f74cd300f9c0fa0dccf69d45d6c9376f9034e6ee57fb"/>
<ui template-class="CmbVersionNotificationView" filename="cmb_version_notification_view.ui" sha256="9a3ced46b90eb7e425d1c345853c4e8e908870c61c75475f7e20ce3c9ee8cec6"/> <ui template-class="CmbVersionNotificationView" filename="cmb_version_notification_view.ui" sha256="9a3ced46b90eb7e425d1c345853c4e8e908870c61c75475f7e20ce3c9ee8cec6"/>
<ui template-class="CmbMessageNotificationView" filename="cmb_message_notification_view.ui" sha256="debeffd184e225d82ed29ac590654b8160363e8d5606366dc8acb3ff9840fee3"/> <ui template-class="CmbMessageNotificationView" filename="cmb_message_notification_view.ui" sha256="debeffd184e225d82ed29ac590654b8160363e8d5606366dc8acb3ff9840fee3"/>

View File

@ -2023,7 +2023,7 @@ class CmbDB(GObject.GObject):
self.__unknown_tag(child, root, child.tag) self.__unknown_tag(child, root, child.tag)
continue 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) resource_id = self.add_gresource("gresource", parent_id=gresource_id, gresource_prefix=prefix)
@ -3034,6 +3034,20 @@ class CmbDB(GObject.GObject):
(ui_id, ) if parent_id is None else (ui_id, parent_id) (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 # 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) path_parent = GObject.Property(type=CmbPath, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self._last_known = None
super().__init__(**kwargs) super().__init__(**kwargs)
self.connect("notify", self.__on_notify) 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}" return f"CmbGResource<{self.resource_type}> id={self.gresource_id}"
def __on_notify(self, obj, pspec): 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) self.project._gresource_changed(self, pspec.name)
@GObject.Property(type=CmbBaseGResource) @GObject.Property(type=CmbBaseGResource)
@ -84,6 +93,34 @@ class CmbGResource(CmbBaseGResource, Gio.ListModel):
file_filename = self.file_filename file_filename = self.file_filename
return file_filename if file_filename else _("Unnamed file {id}").format(id=self.gresource_id) 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 # GListModel iface
def do_get_item(self, position): def do_get_item(self, position):
gresource_id = self.gresource_id gresource_id = self.gresource_id

View File

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

View File

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

View File

@ -70,7 +70,10 @@ class CmbObjectDataEditor(Gtk.Box):
if self.info: if self.info:
self.object.remove_data(self.__data) self.object.remove_data(self.__data)
else: else:
if self.__data.parent:
self.__data.parent.remove_data(self.__data) self.__data.parent.remove_data(self.__data)
else:
self.__data.object.remove_data(self.__data)
@GObject.Property(type=GObject.Object) @GObject.Property(type=GObject.Object)
def object(self): def object(self):
@ -133,12 +136,10 @@ class CmbObjectDataEditor(Gtk.Box):
self.__update_arg(key) self.__update_arg(key)
def __on_data_added(self, obj, data): def __on_data_added(self, obj, data):
if self.info and self.data is None and self.info == data.info:
self.data = data self.data = data
self.__update_view() self.__update_view()
def __on_data_removed(self, obj, data): 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): def __ensure_object_data(self, history_message):

View File

@ -999,33 +999,41 @@ class CmbProject(GObject.Object, Gio.ListModel):
self.db.commit() self.db.commit()
self.history_pop() self.history_pop()
except Exception: except Exception:
logger.warning("Tried to add GResource", exc_info=True)
return None return None
else: finally:
return self.__add_gresource(True, gresource_id, resource_type) gresource = self.__add_gresource(True, gresource_id, resource_type)
gresource._update_new_parent()
return gresource
def __remove_gresource(self, gresource): def __remove_gresource(self, gresource):
if gresource is None: if gresource is None:
logger.warning("Tried to remove a None GResource", exc_info=True) logger.warning("Tried to remove a None GResource", exc_info=True)
return 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) self.__selection_remove(gresource)
print("SELECTION", self.__selection) self.__gresource_id.pop(gresource.gresource_id, None)
self.emit("gresource-removed", gresource) self.emit("gresource-removed", gresource)
def remove_gresource(self, gresource): def remove_gresource(self, gresource):
try: 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.history_push(_('Remove GResource "{name}"').format(name=gresource.display_name))
self.db.execute("DELETE FROM gresource WHERE gresource_id=?;", (gresource.gresource_id,)) 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.history_pop()
self.db.commit() self.db.commit()
self.__remove_gresource(gresource)
except Exception as e: except Exception as e:
logger.warning(f"Error removing gresource {e}", exc_info=True) 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): def get_css_providers(self):
return list(self.__css_id.values()) return list(self.__css_id.values())
@ -1117,7 +1125,7 @@ class CmbProject(GObject.Object, Gio.ListModel):
except Exception as e: except Exception as e:
logger.warning(f"Error adding object {obj_name}: {e}") logger.warning(f"Error adding object {obj_name}: {e}")
return None return None
else: finally:
obj = self.__add_object(True, ui_id, object_id, obj_type, name, parent_id, position=position) obj = self.__add_object(True, ui_id, object_id, obj_type, name, parent_id, position=position)
obj._update_new_parent() obj._update_new_parent()
return obj return obj
@ -1283,10 +1291,17 @@ class CmbProject(GObject.Object, Gio.ListModel):
obj._property_changed(p) obj._property_changed(p)
def __undo_redo_do(self, undo, update_objects=None): def __undo_redo_do(self, undo, update_objects=None):
def get_object_position(c, row): def get_object_position(table, row):
if table == "object":
ui_id, parent_id, position = row[0], row[4], row[8] ui_id, parent_id, position = row[0], row[4], row[8]
parent = self.get_object_by_id(ui_id, parent_id) parent = self.get_object_by_id(ui_id, parent_id)
return parent, position 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() c = self.db.cursor()
@ -1303,8 +1318,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
# Undo or Redo command # Undo or Redo command
if command == "INSERT": if command == "INSERT":
if table == "object": if table in ["object", "gresource"]:
parent, position = get_object_position(c, new_values) parent, position = get_object_position(table, new_values)
if undo: if undo:
update_objects.append((parent, position, 1, 0)) update_objects.append((parent, position, 1, 0))
@ -1318,8 +1333,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
self.__undo_redo_update_insert_delete(c, undo, command, table, columns, table_pk, old_values, new_values) self.__undo_redo_update_insert_delete(c, undo, command, table, columns, table_pk, old_values, new_values)
elif command == "DELETE": elif command == "DELETE":
if table == "object": if table in ["object", "gresource"]:
parent, position = get_object_position(c, old_values) parent, position = get_object_position(table, old_values)
if undo: if undo:
update_objects.append((parent, position, 0, 1)) update_objects.append((parent, position, 0, 1))
@ -1335,8 +1350,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
elif command == "UPDATE": elif command == "UPDATE":
# parent_id and position have to change together because their are part of a unique index # 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: 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) old_parent, old_position = get_object_position(table, old_values)
new_parent, new_position = get_object_position(c, new_values) new_parent, new_position = get_object_position(table, new_values)
if undo: if undo:
if old_position >= 0: if old_position >= 0:
@ -1348,6 +1363,9 @@ class CmbProject(GObject.Object, Gio.ListModel):
update_objects.append((new_parent, new_position, 0, 1)) update_objects.append((new_parent, new_position, 0, 1))
if old_position >= 0: if old_position >= 0:
update_objects.append((old_parent, old_position, 1, 0)) update_objects.append((old_parent, old_position, 1, 0))
elif table == "gresource":
# TODO
pass
if undo: if undo:
self.db.history_update(table, columns, table_pk, old_values) self.db.history_update(table, columns, table_pk, old_values)
@ -1476,12 +1494,27 @@ class CmbProject(GObject.Object, Gio.ListModel):
else: else:
obj._remove_data(data) obj._remove_data(data)
else: 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: if parent:
parent._add_child(row[2], row[3], row[4]) parent._add_child(owner_id, data_id, id)
else: 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": elif table == "object_data_arg":
obj = self.get_object_by_id(pk[0], pk[1]) obj = self.get_object_by_id(pk[0], pk[1])
if obj: if obj:

View File

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

@ -35,6 +35,7 @@ class CmbFileButton(Gtk.Button):
dirname = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE) dirname = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
dialog_title = GObject.Property(type=str, default=_("Select filename"), flags=GObject.ParamFlags.READWRITE) dialog_title = GObject.Property(type=str, default=_("Select filename"), flags=GObject.ParamFlags.READWRITE)
use_open = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
label = Gtk.Template.Child() label = Gtk.Template.Child()
@ -59,11 +60,14 @@ class CmbFileButton(Gtk.Button):
def dialog_callback(dialog, res): def dialog_callback(dialog, res):
try: 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) self.cmb_value = os.path.relpath(file.get_path(), start=self.dirname)
except Exception: except Exception:
pass pass
if self.use_open:
dialog.open(self.get_root(), None, dialog_callback)
else:
dialog.save(self.get_root(), None, dialog_callback) dialog.save(self.get_root(), None, dialog_callback)
@GObject.Property(type=str) @GObject.Property(type=str)

View File

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

View File

@ -14,6 +14,15 @@
</p> </p>
</description> </description>
<releases> <releases>
<release date="2025-05-16" version="0.96.1">
<description>
<p>GResource First Bugfix Release!</p>
<ul>
<li>Fix/improve GResource list model update</li>
<li>Fix removing custom class data like styles</li>
</ul>
</description>
</release>
<release date="2025-04-20" version="0.96.0"> <release date="2025-04-20" version="0.96.0">
<description> <description>
<p>GResource Release!</p> <p>GResource Release!</p>

View File

@ -1,6 +1,6 @@
project( project(
'cambalache', 'c', 'cambalache', 'c',
version: '0.96.0', version: '0.96.1',
meson_version: '>= 1.1.0', meson_version: '>= 1.1.0',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

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,15 @@
import os
import gi 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") gi.require_version("Gtk", "4.0")
from gi.repository import Gtk # noqa E402 from gi.repository import Gtk # noqa E402
from tools.cmb_init_dev import cmb_init_dev # noqa E402 from tools.cmb_init_dev import cmb_init_dev # noqa E402

View File

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

View File

@ -28,12 +28,29 @@ import signal
import subprocess import subprocess
basedir = os.path.join(os.path.split(os.path.dirname(__file__))[0]) 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") 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") localpkgdatadir = os.path.join(localdir, "share", "cambalache")
catalogsdir = os.path.join(localpkgdatadir, "catalogs") catalogsdir = os.path.join(localpkgdatadir, "catalogs")
localbindir = os.path.join(localdir, "bin")
for var, value in [
("LD_LIBRARY_PATH", f"{locallibdir}:{locallibdir}/cambalache:{locallibdir}/cmb_catalog_gen"),
("GI_TYPELIB_PATH", f"{locallibdir}/girepository-1.0:{locallibdir}/cambalache:{locallibdir}/cmb_catalog_gen"),
("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 from gi.repository import GLib # noqa: E402