Compare commits

..

No commits in common. "main" and "0.90.2" have entirely different histories.
main ... 0.90.2

256 changed files with 17912 additions and 35430 deletions

3
.gitignore vendored
View File

@ -3,7 +3,7 @@
__pycache__ __pycache__
.flatpak-builder .flatpak-builder
.catalogs .catalogs
.local .lib
.lc_messages .lc_messages
.vscode .vscode
.env.local .env.local
@ -17,7 +17,6 @@ cambalache/app.gresource
cambalache/config.py cambalache/config.py
cambalache/merengue/config.py cambalache/merengue/config.py
cambalache/merengue/merengue cambalache/merengue/merengue
subprojects/casilda
tools/CmbUtils-3.0.gir tools/CmbUtils-3.0.gir
tools/CmbUtils-3.0.typelib tools/CmbUtils-3.0.typelib
tools/CmbUtils-4.0.gir tools/CmbUtils-4.0.gir

View File

@ -1,323 +0,0 @@
# Changelog
This documents user relevant changes which are also included in
data/ar.xjuan.Cambalache.metainfo.xml.in, closed issues from
(Gitlab)[https://gitlab.gnome.org/jpu/cambalache/-/issues/] and
packaging changes like new dependencies or build system changes.
Cambalache used even/odd minor numbers to differentiate between stable and
development releases.
## 0.96.0
- Add GResource support
- Add internal children support
- New project format
- Save directly to .ui files
- Show directory structure in navigation
- Unified import dialog for all file types
- Add Finnish translation. Erwinjitsu
- Use AdwAboutDialog lo-vely
- Add action child type to GtkDialog
### Packaging changes
- pygobject-3.0 dependency bumped to 3.52 which depends on the new gi repository from GLib
- libcambalacheprivate-[3|4] and its typelib are now installed under libdir/cambalache
- libcmbcatalogutils-[3|4] and its typelib are now installed under libdir/cmb_catalog_gen
- Gtk 3, Handy, webkit2gtk and webkitgtk are now optional dependencies
### Issues
- #253 "Error updating UI 1: gtk-builder-error-quark: .:8:1 Invalid object type 'AdwApplicationWindow' (6)"
- #145 "Consider Cambalache to manage resource description file for building the resource bundle"
- #54 "Add support for internal children"
- #255 "Unable to open files via the UI in a KDE Plasma session"
- #260 "Wrong default for Swap setting in signals"
- #259 "Install private shared libraries in sub directories of the main library path"
- #263 "Translatable setting resets when label field is empty"
- #264 "Error undoing removal of parent GtkGrid"
- #266 "Error "Unknown internal child: entry (6)" with particular GTK 3 UI file"
- #265 "GtkButtonBox shows too many buttons"
- #267 "Make drag'n'drop of top-level more intuitive"
- #269 "Failed to display some element of a validated ui file"
- #272 "Background of compositor does not change colors, when adwaita colors are changed"
- #273 "GtkComboBoxText items gets their translatable property removed"
## 0.94.0
2024-11-25 - Accessibility release
- Gtk 4 and Gtk 3 accessibility support
- Support property subclass override defaults
- AdwDialog placeholder support
- Improved object description in hierarchy
- Lots of bug fixes and minor UI improvements
### Issues
- #252 "Workspace process error / "Error updating UI 1: gtk-builder-error-quark: .:185:38 Object with ID reset not found (13)" with specific UI file"
- #251 "GTK 3 message dialog from specific .ui file rendered incorrectly"
- #250 "Error trying to import specific (LibreOffice) GTK 3 .ui file: "'NoneType has no attribute 'type_id'""
- #240 "Do not show cryptic paths for imported ui files (flatpak)"
- #202 "cambalache crashes when using"
- #203 "AdwActionRow : wrong default for activatable property"
- #241 "Handle adding widgets in empty workspace"
- #234 "Hold <alt> to create object in place is not clear"
- #242 "Support quit via Ctrl + Q"
- #239 "Preview feature is not clear"
- #235 "Remember last saved / open location"
- #236 "`Import` menu operation is not clear"
- #233 "Widget tree is confusing"
- #137 "Add accessibility support"
- #232 "Crashes when restarting workspace"
## 0.92.0
2024-09-27 - Adwaita + Casilda release
- Support 3rd party libraries
- Improved Drag&Drop support
- Streamline headerbar
- Replaced widget hierarchy treeview with column view
- New custom wayland compositor for workspace
- Improve workspace performance
- Fix window ordering
- Enable workspace animations
- Basic port to Adwaita
- Support new desktop dark style
- Many thanks to emersion, kennylevinsen, vyivel and the wlroots community for their support and awesome project
### Packaging changes
- New dependency on [casilda 0.2.0](https://gitlab.gnome.org/jpu/casilda)
Used for workspace compositor, depends on wlroots 0.18
- New python tool cmb-catalog-gen
- New shared library cmbcatalogutils-[3|4] used by cmb-catalog-gen
This library is built twice once linked with Gtk 3 and one with Gtk 4
- Depends on Gtk 4.16 and Adwaita 1.6
### Issues
- #231 "Workspace will crash with inserting Some Adw objects"
- #230 "Exporting byte data messes encoding (libxml)"
- #227 "Add casilda as meson subproject" (sid)
- #220 "BUG: Typing cursor for style classes always in the front of style entries."
- #222 "cannot create instance of abstract (non-instantiatable) type 'GtkWidget'"
- #223 "Cannot add widgets to GtkSizeGroup"
- #225 "Cambalache crashes"
- #219 "Move existing widgets / hierarchy sections into property fields"
- #224 "GtkPicture:file property does not work out of the box"
- #11 "Support 3rd party libraries"
- #216 "Cambalache 0.90.2 Segment faults"
- #213 "Cannot open .ui file created using Gnome Builder"
- #215 "Port UI to LibAdwaita"
## 0.90.4
2024-03-29 - Gtk 4 port
- Migrate main application to Gtk 4
- Update widget catalogs to SDK 46
- Add support for child custom fragments
- Add add parent context menu action
- Mark AdwSplitButton.dropdown-tooltip translatable. (Danial Behzadi)
- Bumped version to 0.90 to better indicate we are close to version 1.0
- Add WebKitWebContext class
- Add brand colors
### Issues
- #184 "Headerbar save button not enabled when "translatable" checkbox's state is changed"
- #207 "Adding or changing data to signal doesn't activate 'Save' button"
- #212 "[Feature] add parent"
- #199 "Copy and pasting messes references between widgets"
- #196 "postinstall.py is trying to modify files in prefix."
- #201 "AdwToolbarView needs special child types"
- #220 "BUG: Typing cursor for style classes always in the front of style entries."
## 0.16.0
2023-09-24: GNOME 45 Release!
- Bump SDK dependency to SDK 45
- Add support for types and properties added in SDK 45
- Marked various missing translatable properties
### Issues
- #190 "Missing translatable property for Gtk.ColumnViewColumn.title"
- #190 "Unassigned local variable"
## 0.14.0
2023-09-07: GMenu release!
- Add GMenu support
- Add UI requirements edit support
- Add Swedish translation. Anders Jonsson
- Updated Italian translation. Lorenzo Capalbo
- Show deprecated and not available warnings for Objects, properties and signals
- Output minimum required library version instead of latest one
- Fix output for templates with inline object properties
- Various optimizations and bug fixes
- Bump test coverage to 66%
### Issues
- #185 "Unable to import certain files converted from GTK3 to GTK4""
- #177 "Panel is not derivable"
- #173 "Cambalache 0.12.0 can't open 0.10.3 project"
## 0.12.0
2023-06-16: New Features release!
- User Templates: use your templates anywhere in your project
- Workspace CSS support: see your CSS changes live
- GtkBuildable Custom Tags: support for styles, items, etc
- Property Bindings: bind your property to any source property
- XML Fragments: add any xml to any object or UI as a fallback
- Preview mode: hide placeholders in workspace
- WebKit support: new widget catalog available
- External objects references support
- Add support for GdkPixbuf, GListModel and GListStore types
- Add missing child type attributes to Gtk4 GtkActionBar (B. Teeuwen)
- Added French Translation (rene-coty)
### Issues
- #121 "Adding handy fails silently without libhandy installed"
- #113 "Add button/toggle to disable the placeholders and make the window look like it would look as an app"
- #123 "Export should be more user-friendly"
- #130 "GtkAboutDialog missing properties"
- #135 "List of string properties that should be translatable in Adw"
- #136 "Can't build via Flatpak"
- #138 "libadwaita widgets aren't categorized"
- #122 "Handy widgets not correctly categorized."
- #96 "Window resize itself when cut content of notebook tab and go to first tab"
- #101 "Right clicking after deselcting button, brokes mouse input"
- #120 "Box doesn't remove empty space"
- #147 "The "Close" button doesn't close the "About" dialog."
- #146 "Scrolling a properties pane conflicts with mousewheel handling of property widgets"
- #143 "Support for nested files"
- #148 "bug: preview display"
- #156 "GDK_BACKEND leaks to workspace process"
- #154 "GtkPaned: for properties to be set consistently, need to use start-child and end-child instead of child
- #160 "Faster prototyping"
- #166 "Allow external Widget or/and from another ui template"
- #163 "Add named object to Gtk.Stack"
- #170 "Support for actions (GtkActionable, menu models)"
- #169 "[main] GtkOrientable is missing in GtkBox properties (maybe in others too)"
- #167 "Gtk*Selection models are missing the model property"
- #168 "Is there a way to add string items to a GtkStringList?"
- #171 "Extended support for inline objects"
- #172 "Certain Adw widgets are not availabe (AdwEntryRow)"
## 0.10.0
2022-06-15: 3rd party libs release!
- Add Adwaita and Handy library support
- Add inline object properties support (only Gtk 4)
- Add special child type support (GtkWindow title widget)
- Improve clipboard functionality
- Add support for reordering children position
- Add/Improve worspace support for GtkMenu, GtkNotebook, GtkPopover, GtkStack, GtkAssistant, GtkListBox, GtkMenuItem and GtkCenterBox
- New property editors for icon name and color properties
- Add support for GdkPixbuf, Pango, Gio, Gdk and Gsk flags/enums types
- Add Ukrainian translation (Volodymyr M. Lisivka)
- Add Italian translation (capaz)
- Add Dutch translation (Gert)
### Issues
- #47 "Proper ui file(which compile properly), fails to open in cambalache and show error"
- #79 "Change column/row count of GtkBox and GtkGrid"
- #81 "No way to add rows to GtkListBox"
- #68 "Trouble with GtkHeaderBar"
- #82 "Can't change x and y values of widgets in Gtk4 when using GtkFixed"
- #62 "Many widget-specific properties appear to be missing"
- #83 "Gettext domain is not initialized properly"
- #66 "Allow adding new items directly in tree view instead of (only) through preview view"
- #86 "Automatically restart merengue when merengue crashes"
- #89 "Error `AttributeError: 'NoneType' object has no attribute 'info'` when deleting UI file"
- #90 "Cambalache fails to import valid glade/ui files"
- #75 "How to use GtkStack"
- #78 "How to use GTKAssistant"
- #63 "Allow automatically exporting on save (or make it easier to do so)"
- #91 "Unable to export"
- #85 "Provide icon selection for Button / Image"
- #92 "'Debug Project Data' does nothing"
- #9 "Support for libadwaita and libhandy"
- #59 "Reordering children in a parent"
- #100 "Signals get broken"
- #105 "Child layout properties not available when parent is a subclass (AdwHeaderBar)"
- #102 "Popovers are not visible"
- #104 "Error when trying to add children to buttonbox"
- #98 "No way to add tab in Notebook"
- #108 "Popovers stay on scene after deleting file which contains them"
- #109 "Cambalache adds to container GtkRecentChooserMenu even if prints that this won't happen"
- #110 "Screen flashing when creating GBinding"
- #116 "Error when trying to click at Notebook content"
- #117 "Error `'NoneType' object has no attribute 'props'` when changing notebook tab"
- #115 "Cannot copy/paste widget"
- #69 "Undo and redo operations don't always match up"
## 0.8.0
2021-12-09: UX improvements Release!
- New Type chooser bar
- Workspace placeholder support
- Translatable properties support (Philipp Unger)
- Clipboard actions support (Copy, Paste, Cut)
- Better unsupported features report
- New Matrix channel #cambalache:gnome.org
- You can now also support Cambalache on Liberapay
### Issues
- #22: Gtk.AboutDialog: license bug
- #10: Export widgets layout data packed in GtkGrid
- #23: Better appdata summary
- #25: Error about target version mismatch
- #29: Error opening project
- #27: Needs a better icon
- #31: Newest ver (git) doesn't display loaded UI
- #34: Translations aren't working in the interactive tour
- #35: Interactive tour isn't working anymore
- #30: Gtk types listed in Cambalache
- #36: Can't build Flatpak after the update of the german translation
- #38: Add translatable metadata to CmbPropertyInfo
- #37: Add support for translatable properties
- #39: Save window state (Philipp Unger)
- #41: Add clipboard support
- #33: No context menu on left pane, the "project view"
## 0.7.0
2021-08-08: New translations release!
- Add Czech translation. Vojtěch Perník
- Add German translation. PhilProg
- Add x-cambalache mimetype with icon
## 0.6.0
2021-07-21: First public release!
- Suport for both Gtk 3 and 4 versions
- Import and export multiple UI at once
- Support plain (no custom tags) GtkBuilder features
- Undo / Redo stack
- LGPL version 2.1

View File

@ -1,28 +1,23 @@
FROM debian:sid-slim FROM debian:sid-slim
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
desktop-file-utils \ python3-gi \
gettext \
gir1.2-adw-1 \
gir1.2-gtk-3.0 \ gir1.2-gtk-3.0 \
gir1.2-gtk-4.0 \ gir1.2-gtk-4.0 \
gir1.2-gtksource-5 \ gir1.2-gtksource-4 \
gir1.2-handy-1 \ gir1.2-handy-1 \
gir1.2-adw-1 \
gir1.2-webkit2-4.1 \ gir1.2-webkit2-4.1 \
gir1.2-webkit-6.0 \ gir1.2-webkit-6.0 \
git \ python3-lxml \
libadwaita-1-dev \ meson \
libgirepository-1.0-dev \ ninja-build \
libgtk-3-dev \ libgtk-3-dev \
libgtk-4-dev \ libgtk-4-dev \
libhandy-1-dev \ libhandy-1-dev \
libwlroots-dev \ libadwaita-1-dev \
meson \ gettext \
ninja-build \ desktop-file-utils
python3-gi \
python3-lxml \
python-gi-dev
RUN useradd -ms /bin/bash discepolo RUN useradd -ms /bin/bash discepolo
ENV DISPLAY :0 ENV DISPLAY :0
@ -30,31 +25,13 @@ ENV DISPLAY :0
RUN mkdir -p /src/build RUN mkdir -p /src/build
COPY . /src/ COPY . /src/
WORKDIR /src
RUN git clone -b 0.18 https://gitlab.freedesktop.org/wlroots/wlroots.git && \
cd wlroots && \
meson setup build/ && \
ninja -C build/ && \
ninja -C build/ install
WORKDIR /src/build WORKDIR /src/build
RUN meson --prefix=/usr && ninja && ninja install RUN meson --prefix=/usr
RUN ninja
RUN ninja install
RUN rm -rf /src RUN rm -rf /src
RUN apt-get remove -y \
git \
libadwaita-1-dev \
libgirepository-1.0-dev \
libgtk-3-dev \
libgtk-4-dev \
libhandy-1-dev \
libwlroots-dev \
meson \
ninja-build \
python-gi-dev
USER discepolo USER discepolo
ENTRYPOINT ["/bin/sh", "-c", "$0 \"$@\"", "cambalache"] ENTRYPOINT ["/bin/sh", "-c", "$0 \"$@\"", "cambalache"]

View File

@ -1,7 +1,4 @@
repo: ar.xjuan.Cambalache.json .git/objects repo: ar.xjuan.Cambalache.json .git/objects
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install --noninteractive --user flathub org.gnome.Sdk//48
flatpak install --noninteractive --user flathub org.gnome.Platform//48
flatpak-builder --force-clean --repo=repo build ar.xjuan.Cambalache.json flatpak-builder --force-clean --repo=repo build ar.xjuan.Cambalache.json
cambalache.flatpak: repo cambalache.flatpak: repo

105
README.md
View File

@ -1,12 +1,12 @@
![Cambalache](cambalache/app/images/logo-horizontal.svg) ![Cambalache](cambalache/app/images/logo-horizontal.svg)
Cambalache is a RAD tool for Gtk 4 and 3 with a clear MVC design and data model first philosophy. Cambalache is a new RAD tool for Gtk 4 and 3 with a clear MVC design and data model first philosophy.
This translates to a wide feature coverage with minimal/none developer intervention for basic support. This translates to a wide feature coverage with minimal/none developer intervention for basic support.
![Data Model Diagram](datamodel.svg) ![Data Model Diagram](datamodel.svg)
To support multiple Gtk versions it renders the workspace out of process using To support multiple Gtk versions it renders the workspace out of process using
a custom wayland compositor widget based on wlroots. the Gdk broadway backend.
![Merengue Diagram](merengue.svg) ![Merengue Diagram](merengue.svg)
@ -28,39 +28,26 @@ Source code lives on GNOME gitlab [here](https://gitlab.gnome.org/jpu/cambalache
* Python 3 - Cambalache is written in Python * Python 3 - Cambalache is written in Python
* [Meson](http://mesonbuild.com) build system * [Meson](http://mesonbuild.com) build system
* [GTK](http://www.gtk.org) 3 and 4 * [GTK](http://www.gtk.org) 3 and 4 with broadway backend enabled
* python-gi - Python GTK bindings * python-gi - Python GTK bindings
* python3-lxml - Python libxml2 bindings * python3-lxml - Python libxml2 bindings
* [casilda](https://gitlab.gnome.org/jpu/casilda) - Workspace custom compositor * WebkitGTK - Webview for workspace
## Flathub ## Running from sources
Flathub is the place to get and distribute apps for all of desktop Linux. To run it without installing use run-dev.py script, it will automatically compile
It is powered by Flatpak, allowing Flathub apps to run on almost any Linux resources and create extra files needed to run.
distribution.
Instructions on how to install flatpak can be found [here](https://flatpak.org/setup/). `./run-dev.py`
You can get the official build [here](https://flathub.org/apps/details/ar.xjuan.Cambalache) The minimum requirements are Gtk 3 and lxml, Gtk 4 is only needed to have a functional Gtk 4 workspace.
Use the following to install:
```
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user flathub ar.xjuan.Cambalache
```
## Flatpak ## Flatpak
Use the following commands to install build dependencies: The preferred way to run Cambalache is using flatpak.
Instructions on how to install flatpak can be found [here](https://flatpak.org/setup/).
```
flatpak remote-add --user --if-not-exists gnome-nightly https://nightly.gnome.org/gnome-nightly.flatpakrepo
flatpak install --user org.gnome.Sdk//master
flatpak install --user org.gnome.Platform//master
```
Build your bundle with the following commands Build your bundle with the following commands
``` ```
flatpak-builder --force-clean --repo=repo build ar.xjuan.Cambalache.json flatpak-builder --force-clean --repo=repo build ar.xjuan.Cambalache.json
flatpak build-bundle repo cambalache.flatpak ar.xjuan.Cambalache flatpak build-bundle repo cambalache.flatpak ar.xjuan.Cambalache
@ -74,9 +61,14 @@ make install
Will create the flatpak repository, then the bundle and install it Will create the flatpak repository, then the bundle and install it
Run as: ## Flathub
You can get Cambalache prebuilt bundles [here](https://flathub.org/apps/details/ar.xjuan.Cambalache)
Use the following to install:
``` ```
flatpak run --user ar.xjuan.Cambalache//master flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user flathub ar.xjuan.Cambalache
``` ```
## Manual installation ## Manual installation
@ -84,20 +76,20 @@ flatpak run --user ar.xjuan.Cambalache//master
This is a regular meson package and can be installed the usual way. This is a regular meson package and can be installed the usual way.
``` ```
# Configure project in _build directory # Create build directory and configure project
meson setup --wipe --prefix=~/.local _build . mkdir _build && cd _build
meson --prefix=~/.local
# Build and install in ~/.local # Build and install
ninja -C _build install ninja
ninja install
``` ```
To run it from .local/ you might need to setup PYTHONPATH and GI_TYPELIB_PATH env variable depending
To run it from .local/ you might need to setup a few env variable depending on your distribution on your distribution defaults
``` ```
export PYTHONPATH=~/.local/lib/python3/dist-packages/ export PYTHONPATH=~/.local/lib/python3/dist-packages/
export LD_LIBRARY_PATH=~/.local/lib/x86_64-linux-gnu/
export GI_TYPELIB_PATH=~/.local/lib/x86_64-linux-gnu/girepository-1.0/ export GI_TYPELIB_PATH=~/.local/lib/x86_64-linux-gnu/girepository-1.0/
cambalache
``` ```
## Docker ## Docker
@ -110,52 +102,20 @@ Build the image with:
docker build -t cambalache . docker build -t cambalache .
``` ```
On linux you can run it on wayland with: On linux, enable localhost connections to your X server and run with:
```
docker run \
-e XDG_RUNTIME_DIR=/tmp \
-e WAYLAND_DISPLAY=$WAYLAND_DISPLAY \
-v $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:/tmp/$WAYLAND_DISPLAY \
--user=$(id -u):$(id -g) \
cambalache
```
or on X server with:
``` ```
xhost +local: xhost +local:
docker run -v /tmp/.X11-unix:/tmp/.X11-unix cambalache docker run -v /tmp/.X11-unix:/tmp/.X11-unix cambalache
``` ```
NOTE: There is no official support for Docker, please use Flatpak if possible.
## MS Windows ## MS Windows
Instructions to run in MS Windows are [here](README.win.md) Instructions to run in MS Windows are [here](README.win.md)
NOTE: There is no official support for Windows yet, these instruction should be
taken with a grain of salt as they might not work on all Windows versions or
be obsolete.
## MacOS ## MacOS
Instructions to run in MacOS are [here](README.mac.md) Instructions to run in MacOS are [here](README.mac.md)
NOTE: There is no official support for MacOS yet, these instruction should be
taken with a grain of salt as they might not work on all MacOS versions or
be obsolete.
## Running from sources
To run it without installing use run-dev.sh script, it will automatically compile
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.py`
This is meant for Cambalache development only.
## Contributing ## Contributing
If you are interested in contributing you can open an issue [here](https://gitlab.gnome.org/jpu/cambalache/-/issues) If you are interested in contributing you can open an issue [here](https://gitlab.gnome.org/jpu/cambalache/-/issues)
@ -186,11 +146,10 @@ like all these [people](./SUPPORTERS.md) did.
- ~8% commission fee - ~8% commission fee
- ~8% payment processing fee - ~8% payment processing fee
## cmb-catalog-gen ## Tools
This tool is used to generate Cambalache catalogs from Gir files. - cambalache-db:
Generate Data Model from Gir files
A catalog is a XML file with all the necessary data for Cambalache to produce - db-codegen:
UI files with widgets from a particular library, this includes the different Generate GObject classes from DB tables
GTypes, with their properties, signals and everything else except
the actual object implementations.

View File

@ -5,18 +5,16 @@ Many thanks to all the people that support the project
- Stephan McCormick - Stephan McCormick
- Willo Vincent - Willo Vincent
- Javier Jardón - Javier Jardón
- Franz Gratzer
- David
- Sonny Piers - Sonny Piers
- David
- Patrick Griffis - Patrick Griffis
- Aemilia Scott
- Jonathan K.
- Luis Barron - Luis Barron
- Mitch 4J - Aemilia Scott
- Michel Fodje
- JustRyan - JustRyan
- Platon workaccount - Platon workaccount
- Jonathan K.
- ~1826340 - ~1826340
- Mula Gabriel
- Felipe Borges - Felipe Borges
- Johannes Deutsch - Johannes Deutsch
- Patrick - Patrick

15
TODO.md Normal file
View File

@ -0,0 +1,15 @@
## Project:
- GResource
## GtkBuilder missing features:
- Internal children
- <child internal-child="name">
- GtkWidget
- <action-widgets>
<action-widget response="">value</action-widget>
</action-widgets>

View File

@ -1,9 +1,9 @@
{ {
"app-id" : "ar.xjuan.Cambalache", "app-id" : "ar.xjuan.Cambalache",
"runtime" : "org.gnome.Platform", "runtime" : "org.gnome.Platform",
"runtime-version" : "48", "runtime-version" : "46",
"sdk" : "org.gnome.Sdk", "sdk" : "org.gnome.Sdk",
"separate-locales" : false, "separate-locales": false,
"command" : "cambalache", "command" : "cambalache",
"finish-args" : [ "finish-args" : [
"--share=ipc", "--share=ipc",
@ -26,65 +26,16 @@
], ],
"modules" : [ "modules" : [
{ {
"name" : "python3-lxml", "name": "python3-lxml",
"buildsystem" : "simple", "buildsystem": "simple",
"build-commands" : [ "build-commands": [
"pip3 install --exists-action=i --ignore-installed --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"lxml\" --no-build-isolation" "pip3 install --exists-action=i --ignore-installed --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"lxml\" --no-build-isolation"
], ],
"sources" : [ "sources": [
{ {
"type" : "file", "type": "file",
"url" : "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz", "url": "https://files.pythonhosted.org/packages/30/39/7305428d1c4f28282a4f5bdbef24e0f905d351f34cf351ceb131f5cddf78/lxml-4.9.3.tar.gz",
"sha256" : "773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1" "sha256": "48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"
}
]
},
{
"name" : "libseat",
"buildsystem" : "meson",
"config-opts" : [
"-Dserver=disabled",
"-Dman-pages=disabled"
],
"sources" : [
{
"type" : "archive",
"url" : "https://git.sr.ht/~kennylevinsen/seatd/archive/0.8.0.tar.gz",
"sha256" : "a562a44ee33ccb20954a1c1ec9a90ecb2db7a07ad6b18d0ac904328efbcf65a0",
"x-checker-data" : {
"type" : "anitya",
"project-id" : 234932,
"stable-only" : true,
"url-template" : "https://git.sr.ht/~kennylevinsen/seatd/archive/$version.tar.gz"
}
}
]
},
{
"name" : "wlroots",
"builddir" : true,
"buildsystem" : "meson",
"config-opts" : [],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.freedesktop.org/wlroots/wlroots.git",
"tag" : "0.18.1",
"commit" : "5bc39071d173301eb8b2cd652c711075526dfbd9"
}
]
},
{
"name" : "casilda",
"builddir" : true,
"buildsystem" : "meson",
"config-opts" : [],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/jpu/casilda.git",
"tag" : "0.9.0",
"commit" : "4f7b1be321cf76832b12bda11fd91897257377e2"
} }
] ]
}, },
@ -96,15 +47,9 @@
{ {
"type" : "git", "type" : "git",
"path" : ".", "path" : ".",
"branch" : "HEAD" "branch": "HEAD"
} }
],
"config-opts" : [
"--libdir=lib"
] ]
} }
], ]
"build-options" : {
"env" : { }
}
} }

View File

@ -19,8 +19,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
import gi import gi
@ -30,12 +28,10 @@ import builtins
from . import config from . import config
gi.require_version("GIRepository", "3.0")
gi.require_version("Gdk", "4.0") gi.require_version("Gdk", "4.0")
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("GtkSource", "5") gi.require_version("GtkSource", "5")
gi.require_version("WebKit", "6.0") gi.require_version("WebKit", "6.0")
gi.require_version('Adw', '1')
# Ensure _() builtin # Ensure _() builtin
if "_" not in builtins.__dict__: if "_" not in builtins.__dict__:
@ -57,7 +53,7 @@ resource._register()
provider = Gtk.CssProvider() provider = Gtk.CssProvider()
provider.load_from_resource("/ar/xjuan/Cambalache/cambalache.css") provider.load_from_resource("/ar/xjuan/Cambalache/cambalache.css")
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - 1) Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
# FIXME: this is needed in flatpak for icons to work # FIXME: this is needed in flatpak for icons to work
Gtk.IconTheme.get_for_display(display).add_search_path("/app/share/icons") Gtk.IconTheme.get_for_display(display).add_search_path("/app/share/icons")
@ -70,17 +66,15 @@ def getLogger(name):
ch.setFormatter(formatter) ch.setFormatter(formatter)
logger = logging.getLogger(name) logger = logging.getLogger(name)
logger.setLevel(os.environ.get("CAMBALACHE_LOGLEVEL", "WARNING").upper()) logger.setLevel(os.environ.get("MERENGUE_LOGLEVEL", "WARNING").upper())
logger.addHandler(ch) logger.addHandler(ch)
return logger return logger
from .cmb_objects_base import CmbBaseObject
from .cmb_css import CmbCSS from .cmb_css import CmbCSS
from .cmb_ui import CmbUI from .cmb_ui import CmbUI
from .cmb_object import CmbObject from .cmb_object import CmbObject
from .cmb_gresource import CmbGResource
# from .cmb_object_data import CmbObjectData # from .cmb_object_data import CmbObjectData
from .cmb_property import CmbProperty from .cmb_property import CmbProperty
@ -89,19 +83,14 @@ from .cmb_layout_property import CmbLayoutProperty
from .cmb_type_info import CmbTypeInfo from .cmb_type_info import CmbTypeInfo
from .cmb_project import CmbProject from .cmb_project import CmbProject
from .cmb_db_inspector import CmbDBInspector
from .cmb_view import CmbView from .cmb_view import CmbView
from .cmb_list_view import CmbListView from .cmb_tree_view import CmbTreeView
from .cmb_notification import notification_center, CmbNotification, CmbNotificationCenter
from .cmb_notification_list_view import CmbNotificationListView
from .cmb_object_editor import CmbObjectEditor from .cmb_object_editor import CmbObjectEditor
from .cmb_signal_editor import CmbSignalEditor from .cmb_signal_editor import CmbSignalEditor
from .cmb_ui_editor import CmbUIEditor from .cmb_ui_editor import CmbUIEditor
from .cmb_ui_requires_editor import CmbUIRequiresEditor from .cmb_ui_requires_editor import CmbUIRequiresEditor
from .cmb_css_editor import CmbCSSEditor from .cmb_css_editor import CmbCSSEditor
from .cmb_gresource_editor import CmbGResourceEditor
from .cmb_fragment_editor import CmbFragmentEditor from .cmb_fragment_editor import CmbFragmentEditor
from .cmb_accessible_editor import CmbAccessibleEditor
from .cmb_type_chooser import CmbTypeChooser from .cmb_type_chooser import CmbTypeChooser
from .cmb_type_chooser_widget import CmbTypeChooserWidget from .cmb_type_chooser_widget import CmbTypeChooserWidget
from .cmb_type_chooser_popover import CmbTypeChooserPopover from .cmb_type_chooser_popover import CmbTypeChooserPopover

View File

@ -19,8 +19,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os

View File

@ -1,8 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Cambalache 0.95.0 -->
<gresources> <gresources>
<gresource prefix="/ar/xjuan/Cambalache/app"> <gresource prefix="/ar/xjuan/Cambalache/app">
<file>metainfo.xml</file>
<file>SUPPORTERS.md</file> <file>SUPPORTERS.md</file>
<file>cmb_window.ui</file> <file>cmb_window.ui</file>
<file>cmb_shortcuts.ui</file> <file>cmb_shortcuts.ui</file>

View File

@ -38,25 +38,7 @@ window.cmb-window label.message {
background-color: rgba(0, 0, 0, .6); background-color: rgba(0, 0, 0, .6);
} }
window.cmb-window list.notifications { window.cmb-window.dark label.message {
margin: 1em 2em;
border-radius: 1em;
border: 1px solid var(--secondary-sidebar-border-color);
}
window.cmb-window list.notifications > row {
padding: 1ex;
}
window.cmb-window list.notifications > row:last-child {
border-width: 0;
}
window.cmb-window list.notifications row > revealer > box > box:last-child {
padding: 0 1em;
}
window.cmb-window.dark .message {
color: black; color: black;
background-color: rgba(255, 255, 255, .6); background-color: rgba(255, 255, 255, .6);
} }
@ -86,9 +68,7 @@ popover.cmb-tutor image {
button.cmb-tutor-highlight, button.cmb-tutor-highlight,
modelbutton.cmb-tutor-highlight, modelbutton.cmb-tutor-highlight,
buttonbox.cmb-tutor-highlight > button, buttonbox.cmb-tutor-highlight > button,
menubutton.cmb-tutor-highlight > button,
stackswitcher.cmb-tutor-highlight > button, stackswitcher.cmb-tutor-highlight > button,
stack.cmb-tutor-highlight,
entry.cmb-tutor-highlight, entry.cmb-tutor-highlight,
treeview.cmb-tutor-highlight, treeview.cmb-tutor-highlight,
box.cmb-tutor-highlight, box.cmb-tutor-highlight,
@ -107,14 +87,11 @@ CmbTypeChooser {
CmbUIEditor, CmbUIEditor,
CmbFragmentEditor, CmbFragmentEditor,
CmbObjectEditor, CmbObjectEditor {
CmbAccessibleEditor {
padding: 0 4px 4px 4px; padding: 0 4px 4px 4px;
} }
CmbCSSEditor, CmbCSSEditor,
CmbGResourceEditor,
CmbGResourceFileEditor,
stackswitcher.property-pane { stackswitcher.property-pane {
padding: 4px; padding: 4px;
} }
@ -134,7 +111,3 @@ image.icon-size-32 {
image.icon-size-64 { image.icon-size-64 {
-gtk-icon-size: 64px; -gtk-icon-size: 64px;
} }
windowtitle.changed {
font-style: italic;
}

View File

@ -21,8 +21,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
import sys import sys

View File

@ -20,59 +20,91 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
import sys import sys
import gi
gi.require_version('Adw', '1') from gi.repository import GLib, Gdk, Gtk, Gio
from gi.repository import GLib, Gdk, Gtk, Gio, Adw
from .cmb_window import CmbWindow from .cmb_window import CmbWindow
from cambalache import CmbProject, utils, config, _ from cambalache import CmbProject, config, _
basedir = os.path.dirname(__file__) or "." basedir = os.path.dirname(__file__) or "."
class CmbApplication(Adw.Application): class CmbApplication(Gtk.Application):
def __init__(self): def __init__(self):
super().__init__(application_id="ar.xjuan.Cambalache", flags=Gio.ApplicationFlags.HANDLES_OPEN) super().__init__(application_id="ar.xjuan.Cambalache", flags=Gio.ApplicationFlags.HANDLES_OPEN)
self.add_main_option("version", b"v", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Print version"), None) self.add_main_option("version", b"v", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Print version"), None)
self.add_main_option( self.add_main_option("export-all", b"E", GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME, _("Export project"), None)
"export-all", b"E", GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME, _("Deprecated: Export project"), None
)
def add_new_window(self): def __add_window(self):
window = CmbWindow(application=self) window = CmbWindow(application=self)
window.connect("open-project", self.__on_open_project)
window.connect("close-request", self.__on_window_close_request) window.connect("close-request", self.__on_window_close_request)
window.connect("project-closed", self.__on_window_project_closed)
self.add_window(window) self.add_window(window)
return window return window
def open_project(self, path, target_tk=None): def open_project(self, path, target_tk=None, uiname=None):
window = None window = None
for win in self.get_windows(): for win in self.get_windows():
if win.project and win.project.filename == path: if win.project is not None and win.project.filename == path:
window = win window = win
if window is None: if window is None:
window = self.add_new_window() window = self.__add_window()
if path is not None: if path is not None:
window.open_project(path, target_tk=target_tk) window.open_project(path, target_tk=target_tk, uiname=uiname)
window.present() window.present()
def import_file(self, path): def import_file(self, path):
window = self.add_new_window() if self.props.active_window is None else self.props.active_window window = self.__add_window() if self.props.active_window is None else self.props.active_window
window.import_file(path) window.import_file(path)
window.present() window.present()
def check_can_quit(self, window=None): def do_open(self, files, nfiles, hint):
for file in files:
path = file.get_path()
content_type, uncertain = Gio.content_type_guess(path, None)
if uncertain:
with open(path, "rb") as fd:
data = fd.read(1024)
content_type, uncertain = Gio.content_type_guess(path, data)
if content_type == "application/x-cambalache-project":
self.open_project(path)
elif content_type in ["application/x-gtk-builder", "application/x-glade"]:
self.import_file(path)
def do_startup(self):
Gtk.Application.do_startup(self)
for action in ["quit"]:
gaction = Gio.SimpleAction.new(action, None)
gaction.connect("activate", getattr(self, f"_on_{action}_activate"))
self.add_action(gaction)
provider = Gtk.CssProvider()
provider.load_from_resource("/ar/xjuan/Cambalache/app/cambalache.css")
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
def do_activate(self):
if self.props.active_window is None:
self.open_project(None)
def __on_open_project(self, window, filename, target_tk, uiname):
if window.project is None:
window.open_project(filename, target_tk, uiname)
else:
self.open_project(filename, target_tk, uiname)
def __check_can_quit(self, window=None):
windows = self.__get_windows() if window is None else [window] windows = self.__get_windows() if window is None else [window]
unsaved_windows = [] unsaved_windows = []
windows2save = [] windows2save = []
@ -98,8 +130,26 @@ class CmbApplication(Adw.Application):
return return
# Create Dialog # Create Dialog
window = windows[0] text = _("Save changes before closing?")
dialog = window._close_project_dialog_new() dialog = Gtk.MessageDialog(
transient_for=windows[0],
message_type=Gtk.MessageType.QUESTION,
text=f"<b><big>{text}</big></b>",
use_markup=True,
modal=True,
)
# Add buttons
dialog.add_buttons(
_("Close without Saving"),
Gtk.ResponseType.CLOSE,
_("Cancel"),
Gtk.ResponseType.CANCEL,
_("Save"),
Gtk.ResponseType.ACCEPT,
)
dialog.set_default_response(Gtk.ResponseType.ACCEPT)
if unsaved_windows_len > 1 or unsaved_windows[0].project.filename is None: if unsaved_windows_len > 1 or unsaved_windows[0].project.filename is None:
# Add checkbox for each unsaved project # Add checkbox for each unsaved project
@ -164,96 +214,28 @@ class CmbApplication(Adw.Application):
return retval return retval
def __on_window_close_request(self, window): def __on_window_close_request(self, window):
self.check_can_quit(window) self.__check_can_quit(window)
return True return True
def __on_window_project_closed(self, window):
windows = self.__get_windows()
if len(windows) > 1:
self.remove_window(window)
window.destroy()
# Action handlers
def _on_quit_activate(self, action, data):
self.check_can_quit()
def _on_open_activate(self, action, data):
filename, target_tk = data.unpack()
# FIXME: use nullable parameter
target_tk = target_tk if target_tk else None
filename = filename if filename else None
window = self.props.active_window
if window and window.project is None:
window.open_project(filename, target_tk)
else:
self.open_project(filename, target_tk)
def _on_new_activate(self, action, data):
target_tk, filename, uipath = data.unpack()
# FIXME: use nullable parameter
target_tk = target_tk if target_tk else None
filename = filename if filename else None
uipath = uipath if uipath else None
window = self.props.active_window
if window is None or window.project is not None:
window = self.add_new_window()
window.create_project(target_tk, filename, uipath)
window.present()
# GApplication interface
def do_open(self, files, nfiles, hint):
for file in files:
path = file.get_path()
content_type = utils.content_type_guess(path)
if content_type == "application/x-cambalache-project":
self.open_project(path)
elif content_type in ["application/x-gtk-builder", "application/x-glade"]:
self.import_file(path)
def do_startup(self):
Adw.Application.do_startup(self)
for action, accelerators, parameter_type in [
("quit", ["<Primary>q"], None),
("open", None, "(ss)"),
("new", None, "(sss)"),
]:
gaction = Gio.SimpleAction.new(action, GLib.VariantType.new(parameter_type) if parameter_type else None)
gaction.connect("activate", getattr(self, f"_on_{action}_activate"))
self.add_action(gaction)
if accelerators:
self.set_accels_for_action(f"app.{action}", accelerators)
provider = Gtk.CssProvider()
provider.load_from_resource("/ar/xjuan/Cambalache/app/cambalache.css")
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
def do_activate(self):
if self.props.active_window is None:
self.open_project(None)
def do_window_removed(self, window): def do_window_removed(self, window):
windows = self.__get_windows() windows = self.__get_windows()
if len(windows) == 0: if len(windows) == 0:
self.activate_action("quit") self.activate_action("quit")
def _on_quit_activate(self, action, data):
self.__check_can_quit()
def do_handle_local_options(self, options): def do_handle_local_options(self, options):
if options.contains("version"): if options.contains("version"):
print(config.VERSION) print(config.VERSION)
return 0 return 0
if options.contains("export-all"): if options.contains("export-all"):
print("Export has been deprecated and does nothing. Every UI file is updated on project save.") filename = options.lookup_value("export-all")
filename = "".join([chr(c) for c in filename.unpack()])
project = CmbProject(filename=filename)
project.export()
return 0 return 0
return -1 return -1

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import Gtk from gi.repository import Gtk

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_shortcuts.ui --> <!-- interface-name cmb_shortcuts.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<object class="GtkShortcutsWindow" id="shortcuts"> <object class="GtkShortcutsWindow" id="shortcuts">
<property name="section-name">shortcuts</property> <property name="section-name">shortcuts</property>
@ -15,56 +14,32 @@
<property name="view">shortcuts</property> <property name="view">shortcuts</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;n</property> <property name="accelerator">&lt;Control&gt;n</property>
<property name="title" translatable="yes">Create new project</property> <property name="title" translatable="yes">Create new project</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;o</property> <property name="accelerator">&lt;Control&gt;o</property>
<property name="title" translatable="yes">Open a project</property> <property name="title" translatable="yes">Open a project</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;i</property> <property name="accelerator">&lt;Control&gt;w</property>
<property name="title" translatable="yes">Import file</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;s</property>
<property name="title" translatable="yes">Save project</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;&lt;shift&gt;s</property>
<property name="title" translatable="yes">Save project as</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;w</property>
<property name="title" translatable="yes">Close the project</property> <property name="title" translatable="yes">Close the project</property>
</object> </object>
</child> </child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">General</property>
<property name="view">general</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;question</property> <property name="accelerator">&lt;Control&gt;s</property>
<property name="title" translatable="yes">Keyboard Shortcuts</property> <property name="title" translatable="yes">Save the project</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;q</property> <property name="accelerator">&lt;Control&gt;e</property>
<property name="title" translatable="yes">Quit application</property> <property name="title" translatable="yes">Save and Export</property>
</object> </object>
</child> </child>
</object> </object>
@ -75,31 +50,25 @@
<property name="view">shortcuts</property> <property name="view">shortcuts</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">Delete</property> <property name="accelerator">&lt;Control&gt;Insert</property>
<property name="title" translatable="yes">Delete object</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;Insert</property>
<property name="title" translatable="yes">Add slot/column</property> <property name="title" translatable="yes">Add slot/column</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;Delete</property> <property name="accelerator">&lt;Control&gt;Delete</property>
<property name="title" translatable="yes">Remove slot/column</property> <property name="title" translatable="yes">Remove slot/column</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;&lt;shift&gt;Insert</property> <property name="accelerator">&lt;Control&gt;&lt;shift&gt;Insert</property>
<property name="title" translatable="yes">Add row</property> <property name="title" translatable="yes">Add row</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;&lt;shift&gt;Delete</property> <property name="accelerator">&lt;Control&gt;&lt;shift&gt;Delete</property>
<property name="title" translatable="yes">Remove row</property> <property name="title" translatable="yes">Remove row</property>
</object> </object>
</child> </child>

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
# Based on glade-intro.c (C) 2017-2018 Juan Pablo Ugarte # Based on glade-intro.c (C) 2017-2018 Juan Pablo Ugarte
# #

View File

@ -20,33 +20,25 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from .cmb_tutor import CmbTutorPosition from .cmb_tutor import CmbTutorPosition
from cambalache import _ from cambalache import _
intro = [ intro = [
(_("Hi, I will show you around Cambalache"), "intro_button", 5), (_("Hi, I will show you around Cambalache"), "intro_button", 5),
(_("You can open a project and find recently used"), "open_button", 5), (_("You can open a project"), "open_button", 3),
(
_("find recently used"),
"recent_button",
2,
),
(_("or create a new one"), "new_button", 4),
(_("Common actions like Undo"), "undo_button", 4), (_("Common actions like Undo"), "undo_button", 4),
(_("Redo"), "redo_button", 2), (_("Redo"), "redo_button", 2),
(_("and Add new UI are directly accessible in the headerbar"), "add_button", 3), (_("Add new UI file"), "add_button", 3),
(_("together with the main menu"), "menu_button", 3), (_("and Save are directly accessible in the headerbar"), "cmb_save_button", 6),
( (_("just like Save As"), "save_as_button", 2),
_("Where you can create a new project"), (_("and the main menu"), "menu_button", 3, "menu_button"),
_("New Project"),
5,
None,
CmbTutorPosition.LEFT,
),
(
_("Import UI files"),
_("Import"),
3,
None,
CmbTutorPosition.LEFT,
),
(_("Create a project to continue"), "intro_button", 2, "add-project"), (_("Create a project to continue"), "intro_button", 2, "add-project"),
(_("Great!"), "intro_button", 2), (_("Great!"), "intro_button", 2),
( (
@ -72,6 +64,13 @@ intro = [
(_("Try adding a grid"), "intro_button", 3, "add-grid"), (_("Try adding a grid"), "intro_button", 3, "add-grid"),
(_("and a button"), "intro_button", 3, "add-button"), (_("and a button"), "intro_button", 3, "add-button"),
(_("Quite easy! Isn't it?"), "intro_button", 3), (_("Quite easy! Isn't it?"), "intro_button", 3),
(
_("Once you finish, you can export all UI files to xml here"),
_("Export all"),
5,
"main-menu",
CmbTutorPosition.LEFT,
),
( (
_("If you have any question, contact us on Matrix!"), _("If you have any question, contact us on Matrix!"),
_("Contact"), _("Contact"),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ gnome.compile_resources('app',
conf = configuration_data() conf = configuration_data()
conf.set('VERSION', meson.project_version()) conf.set('VERSION', meson.project_version())
conf.set('PYTHON', python_bin.full_path()) conf.set('PYTHON', python_bin.path())
conf.set('localedir', localedir) conf.set('localedir', localedir)
conf.set('pkgdatadir', pkgdatadir) conf.set('pkgdatadir', pkgdatadir)

View File

@ -1 +0,0 @@
../../data/ar.xjuan.Cambalache.metainfo.xml.in

View File

@ -1,290 +0,0 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
<!-- 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"/>
<css filename="app/cambalache.css" is_global="0"/>
<ui template-class="CmbChildTypeComboBox">
<content><![CDATA[<interface>
<!-- interface-name CmbChildTypeComboBox -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbChildTypeComboBox" parent="GtkComboBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbColorEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbColorEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbColorEntry" parent="GtkBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbEnumComboBox">
<content><![CDATA[<interface>
<!-- interface-name CmbEnumComboBox -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbEnumComboBox" parent="GtkComboBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbFileEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbFileEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbFileEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbFlagsEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbFlagsEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbFlagsEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbIconNameEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbIconNameEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbIconNameEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbObjectChooser">
<content><![CDATA[<interface>
<!-- interface-name CmbObjectChooser -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbObjectChooser" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbSourceView">
<property id="lang" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbSourceView -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbSourceView" parent="GtkTextView"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbSpinButton">
<content><![CDATA[<interface>
<!-- interface-name CmbSpinButton -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbSpinButton" parent="GtkSpinButton"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbSwitch">
<content><![CDATA[<interface>
<!-- interface-name CmbSwitch -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbSwitch" parent="GtkSwitch"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTextBuffer">
<content><![CDATA[<interface>
<!-- interface-name CmbTextBuffer -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTextBuffer" parent="GtkTextBuffer"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTextView">
<content><![CDATA[<interface>
<!-- interface-name CmbTextView -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTextView" parent="GtkScrolledWindow"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbToplevelChooser">
<property id="derivable-only" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbToplevelChooser -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbToplevelChooser" parent="GtkComboBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTranslatablePopover">
<content><![CDATA[<interface>
<!-- interface-name CmbTranslatablePopover -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTranslatablePopover" parent="GtkPopover"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTranslatableWidget" filename="control/cmb_translatable_widget.ui" sha256="b3178157210f93308b92d99f933cb8d04f2de0278ad75680c7460e2c520b1684"/>
<ui template-class="CasildaCompositor">
<signal id="context-menu"/>
<content><![CDATA[<interface>
<!-- interface-name CmbCompositor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CasildaCompositor" parent="GtkDrawingArea"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTypeChooserPopover">
<property id="category" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="derived-type-id" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="parent-type-id" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="show-categories" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<property id="uncategorized-only" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbTypeChooserPopover -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTypeChooserPopover" parent="GtkPopover"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbAccessibleEditor">
<content><![CDATA[<interface>
<!-- interface-name CmbAccessibleEditor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbAccessibleEditor" parent="GtkGrid"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbUIRequiresEditor">
<content><![CDATA[<interface>
<!-- interface-name CmbUIRequiresEditor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbUIRequiresEditor" parent="GtkGrid"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbScrolledWindow">
<content><![CDATA[<interface>
<!-- interface-name CmbScrolledWindow -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbScrolledWindow" parent="GtkScrolledWindow"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbObjectEditor">
<property id="lang" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="layout" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbObjectEditor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbObjectEditor" parent="GtkBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbListView">
<content><![CDATA[<interface>
<!-- interface-name CmbListView -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbListView" parent="GtkListView"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTypeChooserWidget" filename="cmb_type_chooser_widget.ui" sha256="15dedacc452656abdb9d245ec876370705c610422201213d944c1f6329c5c8f6"/>
<ui template-class="CmbSignalEditor" filename="cmb_signal_editor.ui" sha256="df7ca002e39a3a8e16a7334e48d8b1a7847972b0a3f5213f71ba154a70194a1d"/>
<ui template-class="CmbObjectDataEditor" filename="cmb_object_data_editor.ui" sha256="4a590fc58d66e781f731134214a9fdeefabd8b8c11edcc50f6530cac81f796d1"/>
<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">
<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"/>
<ui template-class="CmbPollNotificationView" filename="cmb_poll_notification_view.ui" sha256="8f47a1e503b85eb5ac3ac54962a40fc2237588e1216afba859a3016a1dcfc121"/>
<ui template-class="CmbPollOptionCheck" filename="cmb_poll_option_check.ui" sha256="aa433f201dc1863f3727e1baa2c4cc239192a1ae4c53553de69d529ba2cc6fed"/>
<ui template-class="CmbNotificationListRow" filename="cmb_notification_list_row.ui" sha256="5ef66fcc24e10d40a91ff0eada84f6aa8a595e368961364d98fde1800755edfc"/>
<ui template-class="CmbPixbufEntry">
<requires>CmbFileEntry</requires>
<content><![CDATA[<interface>
<!-- interface-name CmbPixbufEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<template class="CmbPixbufEntry" parent="CmbFileEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbFragmentEditor" filename="cmb_fragment_editor.ui" sha256="0c2de873c689cd60558a835a39edaf4e1d5e68ef3afeb157628e13fd57282057">
<requires>CmbSourceView</requires>
</ui>
<ui template-class="CmbTypeChooser" filename="cmb_type_chooser.ui" sha256="1b4be880ede6d6d6e88e452418c435640d91f5f61cad97b3f82f44429247e5e6">
<requires>CmbTypeChooserPopover</requires>
<signal id="chooser-popdown"/>
<signal id="chooser-popup"/>
<signal id="type-selected"/>
</ui>
<ui template-class="CmbView" filename="cmb_view.ui" sha256="6b9251d30d6e4751b8b04b2af923eec9a8f00d590a322d96bf080fb200b58424">
<requires>CasildaCompositor</requires>
<requires>CmbSourceView</requires>
<requires>CmbDBInspector</requires>
<signal id="placeholder-activated"/>
<signal id="placeholder-selected"/>
</ui>
<ui template-class="CmbGResourceEditor" filename="cmb_gresource_editor.ui" sha256="2050887ef1c45facb6ebff14500214bb035e6808ca61d2a7d661e696d79026ca">
<requires>CmbFileButton</requires>
<requires>CmbEntry</requires>
</ui>
<ui template-class="CmbCSSEditor" filename="cmb_css_editor.ui" sha256="6f39c9cb2f112dac9da415322792a717b8c0a80845dcbb2edacd7695388cba63">
<requires>CmbFileButton</requires>
<requires>CmbSourceView</requires>
</ui>
<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="df07e3e03b88f9b097ad7d65efa15923d339db83b9d4a7c015a312a71f8c9685">
<requires>CmbNotificationListView</requires>
<requires>CmbScrolledWindow</requires>
<requires>CmbObjectEditor</requires>
<requires>CmbSignalEditor</requires>
<requires>CmbAccessibleEditor</requires>
<requires>CmbFragmentEditor</requires>
<requires>CmbUIEditor</requires>
<requires>CmbUIRequiresEditor</requires>
<requires>CmbCSSEditor</requires>
<requires>CmbGResourceEditor</requires>
<requires>CmbTypeChooser</requires>
<requires>CmbView</requires>
<requires>CmbSourceView</requires>
<requires>CmbListView</requires>
<css-provider>app/cambalache.css</css-provider>
</ui>
</cambalache-project>

View File

@ -40,16 +40,6 @@ CmbPropertyLabel.hidden:hover > box > image {
opacity: 1; opacity: 1;
} }
popover.cmb-binding-popover button.close,
list.notifications button.close {
padding: unset;
margin: unset;
border: unset;
border-radius: 50%;
min-width: 20px;
min-height: 20px;
}
CmbPropertyLabel { CmbPropertyLabel {
min-width:unset; min-width:unset;
min-height: unset; min-height: unset;
@ -61,22 +51,6 @@ CmbPropertyLabel {
outline: unset; outline: unset;
} }
CmbPropertyLabel > box > label {
padding: 2px 4px;
}
CmbPropertyLabel:focus > box > label {
/*
FIXME: use focus_border_color
$focus_border_color: if($variant == 'light', transparentize($selected_bg_color, 0.5), transparentize($selected_bg_color, 0.3));
*/
outline-color: color-mix(in srgb, var(--accent-bg-color) 60%, transparent);
outline-offset: -2px;
outline-width: 2px;
outline-style: solid;
border-radius: 6px;
}
CmbPropertyLabel.modified > box > label { CmbPropertyLabel.modified > box > label {
font-style: italic; font-style: italic;
} }
@ -84,48 +58,3 @@ CmbPropertyLabel.modified > box > label {
CmbPropertyLabel.warning > box > label { CmbPropertyLabel.warning > box > label {
text-decoration: underline wavy @warning_color; text-decoration: underline wavy @warning_color;
} }
listview.cmb-list-view {
background-color: @theme_bg_color;
}
listview.cmb-list-view > row {
padding: 2px 8px;
min-height: 30px;
}
listview.cmb-list-view > row:drop(active):not(.drop-after):not(.drop-before) {
outline: 2px solid color-mix(in srgb, @theme_bg_color 80%, black);
outline-offset: -4px;
}
listview.cmb-list-view > row.drop-before:drop(active) {
border: 0;
border-radius: 0;
border-top: 2px solid color-mix(in srgb, @theme_bg_color 80%, black);
margin-top: -2px;
}
listview.cmb-list-view > row.drop-after:drop(active) {
border: 0;
border-radius: 0;
border-bottom: 2px solid color-mix(in srgb, @theme_bg_color 80%, black);
margin-bottom: 0;
}
listview.cmb-list-view > row > treeexpander.cmb-path > expander {
-gtk-icon-source: -gtk-icontheme("folder-symbolic");
}
listview.cmb-list-view > row > treeexpander.cmb-path > expander:checked {
-gtk-icon-source: -gtk-icontheme("folder-open-symbolic");
}
listview.cmb-list-view > row > treeexpander.cmb-unsaved-path > expander {
-gtk-icon-source: -gtk-icontheme("view-list-symbolic");
}
button.compact {
padding: 2px 4px;
font-weight: normal;
}

View File

@ -1,31 +1,21 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Cambalache 0.95.0 -->
<gresources> <gresources>
<gresource prefix="/ar/xjuan/Cambalache"> <gresource prefix="/ar/xjuan/Cambalache">
<file>cambalache.css</file> <file>control/cmb_translatable_widget.ui</file>
<file>db/cmb_base.sql</file>
<file>db/cmb_project.sql</file>
<file>db/cmb_history.sql</file>
<file>cmb_view.ui</file>
<file>cmb_context_menu.ui</file> <file>cmb_context_menu.ui</file>
<file>cmb_css_editor.ui</file>
<file>cmb_db_inspector.ui</file>
<file>cmb_fragment_editor.ui</file>
<file>cmb_gresource_editor.ui</file>
<file>cmb_notification_list_view.ui</file>
<file>cmb_version_notification_view.ui</file>
<file>cmb_message_notification_view.ui</file>
<file>cmb_poll_option_check.ui</file>
<file>cmb_poll_notification_view.ui</file>
<file>cmb_object_data_editor.ui</file>
<file>cmb_signal_editor.ui</file>
<file>cmb_type_chooser.ui</file> <file>cmb_type_chooser.ui</file>
<file>cmb_type_chooser_widget.ui</file> <file>cmb_type_chooser_widget.ui</file>
<file>cmb_ui_editor.ui</file> <file>cmb_ui_editor.ui</file>
<file>cmb_view.ui</file> <file>cmb_css_editor.ui</file>
<file>control/cmb_file_button.ui</file> <file>cmb_fragment_editor.ui</file>
<file>control/cmb_translatable_widget.ui</file> <file>cmb_object_data_editor.ui</file>
<file>db/cmb_base.sql</file> <file>cmb_signal_editor.ui</file>
<file>db/cmb_history.sql</file> <file>cambalache.css</file>
<file>db/cmb_project.sql</file>
<file>icons/scalable/actions/binded-symbolic.svg</file>
<file>icons/scalable/actions/bind-symbolic.svg</file> <file>icons/scalable/actions/bind-symbolic.svg</file>
<file>cmb_notification_list_row.ui</file> <file>icons/scalable/actions/binded-symbolic.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@ -1,236 +0,0 @@
#
# CmbAccessibleEditor - Cambalache Accessible Editor
#
# Copyright (C) 2024 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
#
from gi.repository import GObject, Gtk
from .cmb_object import CmbObject
from .control import cmb_create_editor, CmbEnumComboBox
from .cmb_property_label import CmbPropertyLabel
from . import utils, _
class CmbAccessibleEditor(Gtk.Box):
__gtype_name__ = "CmbAccessibleEditor"
def __init__(self, **kwargs):
self.__object = None
self.__bindings = []
self.__accessibility_metadata = None
self.__role_filter_model = None
super().__init__(**kwargs)
self.props.orientation = Gtk.Orientation.VERTICAL
self.__role_box = Gtk.Box(spacing=6)
self.__role_box.append(Gtk.Label(label="accessible-role"))
self.__role_combobox = CmbEnumComboBox()
self.__role_box.append(self.__role_combobox)
self.append(self.__role_box)
self.__box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.append(self.__box)
def bind_property(self, *args):
binding = GObject.Object.bind_property(*args)
self.__bindings.append(binding)
return binding
def __on_expander_expanded(self, expander, pspec, revealer):
expanded = expander.props.expanded
if expanded:
revealer.props.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN
else:
revealer.props.transition_type = Gtk.RevealerTransitionType.SLIDE_UP
revealer.props.reveal_child = expanded
def __update_view(self):
for child in utils.widget_get_children(self.__box):
self.__box.remove(child)
if self.__object is None or not self.__object.info.is_a("GtkWidget"):
return
obj = self.__object
if obj.project.target_tk == "gtk-4.0":
if not self.__object.info.is_a("GtkWidget"):
self.__role_box.hide()
return
self.__role_box.show()
prop = self.__object.properties_dict["accessible-role"]
if prop.value in ["none", "presentation"]:
# No need to set properties if role none or presentation
return
role_data = self.__object.project.db.accessibility_metadata.get(prop.value, None)
if role_data:
role_properties = role_data["properties"]
role_states = role_data["states"]
else:
role_properties = None
role_states = None
a11y_data = [
("CmbAccessibleProperty", _("Properties"), len("cmb-a11y-properties"), role_properties),
("CmbAccessibleState", _("States"), len("cmb-a11y-states"), role_states),
("CmbAccessibleRelation", _("Relations"), None, None),
]
else:
type_actions = self.__object.project.db.accessibility_metadata.get(obj.type_id, [])
a11y_data = [
("CmbAccessibleAction", _("Actions"), len("cmb-a11y-actions"), type_actions),
("CmbAccessibleProperty", _("Properties"), None, None),
("CmbAccessibleRelation", _("Relations"), None, None),
]
self.__role_box.hide()
properties = obj.properties_dict
for owner_id, title, prefix_len, allowed_ids in a11y_data:
info = obj.project.type_info.get(owner_id, None)
if info is None:
continue
# Editor count
i = 0
# Grid for all editors
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4)
# Accessible iface properties
for property_id in info.properties:
# Ignore properties or status not for this role
if prefix_len and allowed_ids is not None and property_id[prefix_len:] not in allowed_ids:
continue
prop = properties.get(property_id, None)
if prop is None or prop.info is None:
continue
editor = cmb_create_editor(prop.project, prop.info.type_id, prop=prop)
if editor is None:
return None, None
self.bind_property(
prop,
"value",
editor,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
label = CmbPropertyLabel(prop=prop, bindable=False)
grid.attach(label, 0, i, 1, 1)
grid.attach(editor, 1, i, 1, 1)
i += 1
if i == 0:
continue
# Create expander/revealer to pack editor grid
expander = Gtk.Expander(label=f"<b>{title}</b>", use_markup=True, expanded=True)
revealer = Gtk.Revealer(reveal_child=True)
expander.connect("notify::expanded", self.__on_expander_expanded, revealer)
revealer.set_child(grid)
self.__box.append(expander)
self.__box.append(revealer)
self.show()
def __on_object_property_changed_notify(self, obj, prop):
if prop.property_id == "accessible-role":
self.__update_view()
def __visible_func(self, model, iter, data):
if self.__accessibility_metadata is None:
return False
name, nick, value = model[iter]
role_data = self.__accessibility_metadata.get(nick, None)
if role_data:
# Ignore abstract roles
if nick != "none" and role_data.get("is_abstract", False):
return False
return True
@GObject.Property(type=CmbObject)
def object(self):
return self.__object
@object.setter
def _set_object(self, obj):
if obj == self.__object:
return
if self.__object and self.__object.info.is_a("GtkWidget"):
self.__object.disconnect_by_func(self.__on_object_property_changed_notify)
self.__role_combobox.props.model = None
self.__accessibility_metadata = None
for binding in self.__bindings:
binding.unbind()
self.__bindings = []
self.__object = obj
if self.__object and self.__object.info.is_a("GtkWidget"):
self.__object.connect("property-changed", self.__on_object_property_changed_notify)
self.__accessibility_metadata = self.__object.project.db.accessibility_metadata
a11y_info = self.__object.project.type_info.get("GtkAccessibleRole", None)
if a11y_info:
a11y_info = self.__object.project.type_info.get("GtkAccessibleRole", None)
self.__role_filter_model = Gtk.TreeModelFilter(child_model=a11y_info.enum)
self.__role_filter_model.set_visible_func(self.__visible_func)
self.__role_combobox.info = a11y_info
self.__role_combobox.props.model = self.__role_filter_model
prop = self.__object.properties_dict.get("accessible-role", None)
self.bind_property(
prop,
"value",
self.__role_combobox,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self.__update_view()
Gtk.WidgetClass.set_css_name(CmbAccessibleEditor, "CmbAccessibleEditor")

View File

@ -20,15 +20,12 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject from gi.repository import GObject
class CmbBase(GObject.GObject): class CmbBase(GObject.GObject):
project = GObject.Property(type=GObject.GObject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) project = GObject.Property(type=GObject.GObject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
display_name = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@ -1,86 +0,0 @@
#
# 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

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
@ -71,10 +69,6 @@ class CmbContextMenu(Gtk.PopoverMenu):
"GtkAligment", "GtkAligment",
"GtkEventBox" "GtkEventBox"
] ]
else:
types += [
"GtkGraphicsOffload",
]
self.add_submenu.remove_all() self.add_submenu.remove_all()

View File

@ -1,10 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_context_menu.ui --> <!-- interface-name cmb_context_menu.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gio" version="2.0"/>
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<requires lib="gio" version="2.0"/>
<menu id="menu_model"> <menu id="menu_model">
<section> <section>
<item> <item>

View File

@ -20,14 +20,9 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
from gi.repository import GObject, Gio from gi.repository import GObject, Gio
from .cmb_path import CmbPath
from .cmb_objects_base import CmbBaseCSS from .cmb_objects_base import CmbBaseCSS
from cambalache import _ from cambalache import _
@ -37,7 +32,6 @@ class CmbCSS(CmbBaseCSS):
"file-changed": (GObject.SignalFlags.RUN_FIRST, None, ()), "file-changed": (GObject.SignalFlags.RUN_FIRST, None, ()),
} }
path_parent = GObject.Property(type=CmbPath, flags=GObject.ParamFlags.READWRITE)
css = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE) css = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -57,13 +51,8 @@ class CmbCSS(CmbBaseCSS):
if pspec.name == "filename": if pspec.name == "filename":
self.load_css() self.load_css()
@classmethod def get_display_name(self):
def get_display_name(cls, css_id, filename): return self.filename if self.filename else _("Unnamed CSS {css_id}").format(css_id=self.css_id)
return os.path.basename(filename) if filename else _("Unnamed CSS {css_id}").format(css_id=css_id)
@GObject.Property(type=str)
def display_name(self):
return CmbCSS.get_display_name(self.css_id, self.filename)
@GObject.Property(type=int) @GObject.Property(type=int)
def priority(self): def priority(self):

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from cambalache import utils, _ from cambalache import utils, _
@ -81,7 +79,6 @@ class CmbCSSEditor(Gtk.Grid):
self.set_sensitive(False) self.set_sensitive(False)
return return
self.filename.dirname = obj.project.dirname
self.set_sensitive(True) self.set_sensitive(True)
for field, target in self.fields: for field, target in self.fields:
@ -108,6 +105,10 @@ class CmbCSSEditor(Gtk.Grid):
self.__update_provider_for() self.__update_provider_for()
self.__update_ui_button_label() self.__update_ui_button_label()
@Gtk.Template.Callback("on_remove_button_clicked")
def __on_remove_button_clicked(self, button):
self.emit("remove-css")
@Gtk.Template.Callback("on_save_button_clicked") @Gtk.Template.Callback("on_save_button_clicked")
def __on_save_button_clicked(self, button): def __on_save_button_clicked(self, button):
self._object.save_css() self._object.save_css()
@ -135,7 +136,7 @@ class CmbCSSEditor(Gtk.Grid):
# Generate a check button for each UI # Generate a check button for each UI
for ui in ui_list: for ui in ui_list:
check = Gtk.CheckButton( check = Gtk.CheckButton(
label=ui.display_name, active=ui.ui_id in provider_for, halign=Gtk.Align.START, visible=True label=ui.get_display_name(), active=ui.ui_id in provider_for, halign=Gtk.Align.START, visible=True
) )
check.connect("toggled", self.__on_check_button_toggled, ui) check.connect("toggled", self.__on_check_button_toggled, ui)
self.ui_box.append(check) self.ui_box.append(check)

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_css_editor.ui --> <!-- interface-name cmb_css_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<template class="CmbCSSEditor" parent="GtkGrid"> <template class="CmbCSSEditor" parent="GtkGrid">
<property name="column-spacing">3</property> <property name="column-spacing">3</property>
@ -39,8 +38,11 @@
</object> </object>
</child> </child>
<child> <child>
<object class="CmbFileButton" id="filename"> <object class="CmbEntry" id="filename">
<property name="can-focus">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
<property name="placeholder-text" translatable="yes">&lt;file name relative to project&gt;</property>
<property name="visible">True</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">0</property> <property name="row">0</property>
@ -108,6 +110,20 @@
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="spacing">4</property> <property name="spacing">4</property>
<child>
<object class="GtkButton" id="remove_button">
<property name="focusable">1</property>
<!-- <property name="tooltip-text" translatable="1">Remove CSS file from project</property> -->
<property name="halign">start</property>
<property name="valign">end</property>
<signal name="clicked" handler="on_remove_button_clicked"/>
<child>
<object class="GtkImage">
<property name="icon-name">app-remove-symbolic</property>
</object>
</child>
</object>
</child>
<child> <child>
<object class="GtkLabel"> <object class="GtkLabel">
<property name="halign">center</property> <property name="halign">center</property>
@ -173,6 +189,7 @@
</child> </child>
</object> </object>
</child> </child>
<!-- Custom object fragments -->
</object> </object>
</child> </child>
<child> <child>

File diff suppressed because it is too large Load Diff

View File

@ -1,294 +0,0 @@
#
# CmbDBInspector
#
# Copyright (C) 2024 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
#
from gi.repository import GLib, GObject, Gio, Gtk
from cambalache import CmbProject
class CmbDBTable(GObject.Object):
def __init__(self, **kwargs):
self.__properties = {}
super().__init__(**kwargs)
def do_get_property(self, prop):
# TODO: read from DB directly
if not prop.name.startswith("cmb-int-") and prop.name not in self.__properties_set__:
raise AttributeError('unknown property %s' % prop.name)
return self.__properties[prop.name]
def do_set_property(self, prop, value):
# TODO: only store PK values when using DB
if not prop.name.startswith("cmb-int-") and prop.name not in self.__properties_set__:
raise AttributeError('unknown property %s' % prop.name)
self.__properties[prop.name] = value
self.notify(prop.name)
class CmbDBStore(GObject.GObject, Gio.ListModel):
__gtype_name__ = 'CmbDBStore'
project = GObject.Property(type=CmbProject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, ItemClass, **kwargs):
super().__init__(**kwargs)
self.__item_class = ItemClass
self.__history_index = None
self._objects = []
def do_get_item(self, position):
self.__check_refresh()
return self._objects[position] if position < len(self._objects) else None
def do_get_item_type(self):
return self.__item_class
def do_get_n_items(self):
self.__check_refresh()
return len(self._objects)
def __check_refresh(self):
history_index = self.project.history_index
# Nothing to update if history did not changed
if history_index == self.__history_index:
return
ItemClass = self.__item_class
properties = ItemClass.__properties__
int_properties = ItemClass.__int_properties__
table = ItemClass.__table__
needs_update = False
# Basic optimization, only update if something changed in this table
# TODO: this could be optimized more by check command to know exactly which row changed
if self.__history_index is None or table.startswith("history") or table in ["global", "__profile__"]:
needs_update = True
else:
change_table = table[7:] if table.startswith("history_") else table
# TODO: detect command compression
for row in self.project.db.execute(
"SELECT table_name FROM history WHERE history_id >= ? ORDER BY history_id;", (self.__history_index, )
):
table_name, = row
if table_name == change_table:
needs_update = True
break
self.__history_index = history_index
if not needs_update:
return
# Emit signal to clear model
n_items = len(self._objects)
if n_items:
self._objects = []
self.items_changed(0, n_items, 0)
if len(ItemClass.__pk__):
pk_columns = ",".join(ItemClass.__pk__)
else:
pk_columns = "rowid"
for row in self.project.db.execute(f"SELECT * FROM {table} ORDER BY {pk_columns};"):
item = ItemClass()
for i, val in enumerate(row):
property_id = properties[i]
if property_id in int_properties:
item.set_property(f"cmb-int-{property_id}", val if val is not None else 0)
item.set_property(property_id, val)
self._objects.append(item)
# Emit signal to populate model
self.items_changed(0, 0, len(self._objects))
class TableView(Gtk.ColumnView):
project = GObject.Property(type=CmbProject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, ItemClass, **kwargs):
super().__init__(**kwargs)
self.props.show_row_separators = True
self.props.show_column_separators = True
self.props.reorderable = False
self.__model = None
self.__filter_model = None
self.__item_class = ItemClass
for property_id in ItemClass.__properties__:
factory = Gtk.SignalListItemFactory()
factory.connect("setup", self._on_factory_setup)
factory.connect("bind", self._on_factory_bind, property_id)
factory.connect("unbind", self._on_factory_unbind)
col = Gtk.ColumnViewColumn(title=property_id, factory=factory)
if property_id in ItemClass.__int_properties__:
property_expression = Gtk.PropertyExpression.new(ItemClass, None, f"cmb-int-{property_id}")
sorter = Gtk.NumericSorter()
else:
property_expression = Gtk.PropertyExpression.new(ItemClass, None, property_id)
sorter = Gtk.StringSorter()
col.props.resizable = True
col.props.expand = True
sorter.set_expression(property_expression)
col.set_sorter(sorter)
self.append_column(col)
# TODO: keep track of project changes only while we are showing this model
self.connect("map", self.__on_map)
self.project.connect("changed", self.__on_project_changed)
def __update_label(self, item, label, property_id):
val = str(item.get_property(property_id))
label.set_text(val if val else "")
def __on_item_notify(self, item, pspec, label):
self.__update_label(item, label, pspec.name)
def _on_factory_setup(self, factory, list_item):
label = Gtk.Inscription()
list_item.set_child(label)
def _on_factory_bind(self, factory, list_item, property_id):
label = list_item.get_child()
item = list_item.get_item()
self.__update_label(item, label, property_id)
item.connect(f"notify::{property_id}", self.__on_item_notify, label)
def _on_factory_unbind(self, factory, list_item):
item = list_item.get_item()
item.disconnect_by_func(self.__on_item_notify)
def __on_map(self, w):
# Trigger check refresh
if self.__model is not None:
self.__model.get_n_items()
return
# Load model when widget is shown
self.__model = CmbDBStore(self.__item_class, project=self.project)
self.__filter_model = Gtk.SortListModel(model=self.__model, sorter=self.get_sorter())
self.set_model(Gtk.NoSelection(model=self.__filter_model))
def __on_project_changed(self, project):
# Trigger check refresh
if self.__model is not None and self.is_visible():
self.__model.get_n_items()
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_db_inspector.ui")
class CmbDBInspector(Gtk.Box):
__gtype_name__ = "CmbDBInspector"
stack = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__project = None
self.__table_classes = None
super().__init__(**kwargs)
self.connect("map", self.__on_map)
@GObject.Property(type=CmbProject)
def project(self):
return self.__project
@project.setter
def _set_project(self, project):
self.__project = project
def __init_tables(self):
db = self.project.db
self.__table_classes = {}
for row in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"):
table, = row
if table.startswith("sqlite_"):
continue
klass = self.__class_from_table(table)
self.__table_classes[table] = klass
def _metadata_from_table(self, table):
db = self.project.db
properties = []
gproperties = {}
int_properties = set()
pk_list = []
for row in db.execute(f"PRAGMA table_info({table});"):
col = row[1]
col_type = row[2]
pk = row[5]
name = col.replace("_", "-")
properties.append(name)
if col_type == "INTEGER":
int_properties.add(name)
gproperties[f"cmb-int-{name}"] = (int, "", "", GLib.MININT, GLib.MAXINT, 0, GObject.ParamFlags.READWRITE)
gproperties[name] = (str, "", "", None, GObject.ParamFlags.READWRITE)
if pk:
pk_list.append(col)
return properties, gproperties, int_properties, pk_list
def __class_from_table(self, table):
class_name = f"CmbDBTable_{table}"
properties, gproperties, int_properties, pk = self._metadata_from_table(table)
klass = type(class_name, (CmbDBTable,), dict(
__table__=table,
__gproperties__=gproperties,
__properties__=properties,
__properties_set__=set(properties),
__int_properties__=int_properties,
__pk__=pk)
)
return klass
def __populate_stack(self):
for table, klass in self.__table_classes.items():
sw = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
propagate_natural_width=True,
propagate_natural_height=True)
view = TableView(klass, project=self.__project)
sw.set_child(view)
self.stack.add_titled(sw, table, table)
def __on_map(self, w):
if self.__table_classes is None and self.__project is not None:
self.__init_tables()
self.__populate_stack()

View File

@ -1,21 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_db_inspector.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbDBInspector" parent="GtkBox">
<child>
<object class="GtkStackSidebar">
<property name="stack">stack</property>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="halign">start</property>
<property name="transition-type">crossfade</property>
<property name="valign">start</property>
</object>
</child>
</template>
</interface>

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
def ensure_columns_for_0_7_5(table, data): def ensure_columns_for_0_7_5(table, data):
@ -39,24 +37,24 @@ def migrate_table_data_to_0_7_5(c, table, data):
if table == "object": if table == "object":
c.execute( c.execute(
""" """
UPDATE temp.object SET position=new.position - 1 UPDATE object SET position=new.position - 1
FROM ( FROM (
SELECT row_number() OVER (PARTITION BY parent_id ORDER BY object_id) position, ui_id, object_id SELECT row_number() OVER (PARTITION BY parent_id ORDER BY object_id) position, ui_id, object_id
FROM temp.object FROM object
WHERE parent_id IS NOT NULL WHERE parent_id IS NOT NULL
) AS new ) AS new
WHERE temp.object.ui_id=new.ui_id AND temp.object.object_id=new.object_id; WHERE object.ui_id=new.ui_id AND object.object_id=new.object_id;
""" """
) )
c.execute( c.execute(
""" """
UPDATE temp.object SET position=new.position - 1 UPDATE object SET position=new.position - 1
FROM ( FROM (
SELECT row_number() OVER (PARTITION BY ui_id ORDER BY object_id) position, ui_id, object_id SELECT row_number() OVER (PARTITION BY ui_id ORDER BY object_id) position, ui_id, object_id
FROM temp.object FROM object
WHERE parent_id IS NULL WHERE parent_id IS NULL
) AS new ) AS new
WHERE temp.object.ui_id=new.ui_id AND temp.object.object_id=new.object_id; WHERE object.ui_id=new.ui_id AND object.object_id=new.object_id;
""" """
) )
@ -74,9 +72,9 @@ def migrate_table_data_to_0_9_0(c, table, data):
# Remove all object properties with a 0 as value # Remove all object properties with a 0 as value
c.execute( c.execute(
""" """
DELETE FROM temp.object_property AS op DELETE FROM object_property AS op
WHERE value = 0 AND WHERE value = 0 AND
(SELECT property_id FROM temp.property WHERE owner_id=op.owner_id AND property_id=op.property_id AND is_object) (SELECT property_id FROM property WHERE owner_id=op.owner_id AND property_id=op.property_id AND is_object)
IS NOT NULL; IS NOT NULL;
""" """
) )
@ -118,9 +116,9 @@ def migrate_table_data_to_0_17_3(c, table, data):
if table in ["object_property", "object_layout_property", "object_data"]: if table in ["object_property", "object_layout_property", "object_data"]:
c.executescript( c.executescript(
f""" f"""
UPDATE temp.{table} SET translatable=1 UPDATE {table} SET translatable=1
WHERE translatable IS NOT NULL AND lower(translatable) IN (1, 'y', 'yes', 't', 'true'); WHERE translatable IS NOT NULL AND lower(translatable) IN (1, 'y', 'yes', 't', 'true');
UPDATE temp.{table} SET translatable=NULL UPDATE {table} SET translatable=NULL
WHERE translatable IS NOT NULL AND translatable != 1; WHERE translatable IS NOT NULL AND translatable != 1;
""" """
) )
@ -129,35 +127,8 @@ def migrate_table_data_to_0_17_3(c, table, data):
for prop in ["swap", "after"]: for prop in ["swap", "after"]:
c.executescript( c.executescript(
f""" f"""
UPDATE temp.object_signal SET {prop}=1 UPDATE object_signal SET {prop}=1
WHERE {prop} IS NOT NULL AND lower({prop}) IN (1, 'y', 'yes', 't', 'true'); WHERE {prop} IS NOT NULL AND lower({prop}) IN (1, 'y', 'yes', 't', 'true');
UPDATE temp.object_signal SET {prop}=NULL WHERE {prop} IS NOT NULL AND after != 1; UPDATE object_signal SET {prop}=NULL WHERE {prop} IS NOT NULL AND after != 1;
""" """
) )
def migrate_table_data_to_0_91_3(c, table, data):
# Ensure every object has a position
if table == "object":
c.execute(
"""
UPDATE temp.object SET position=new.position - 1
FROM (
SELECT row_number() OVER (PARTITION BY ui_id, parent_id ORDER BY position, object_id) position, ui_id, object_id
FROM temp.object
WHERE parent_id IS NOT NULL
) AS new
WHERE temp.object.ui_id=new.ui_id AND temp.object.object_id=new.object_id;
"""
)
c.execute(
"""
UPDATE temp.object SET position=new.position - 1
FROM (
SELECT row_number() OVER (PARTITION BY ui_id ORDER BY object_id) position, ui_id, object_id
FROM temp.object
WHERE parent_id IS NULL
) AS new
WHERE temp.object.ui_id=new.ui_id AND temp.object.object_id=new.object_id;
"""
)

View File

@ -1,112 +0,0 @@
import os
import time
import inspect
import sqlite3
class CmbProfileConnection(sqlite3.Connection):
def __init__(self, path, **kwargs):
super().__init__(path, **kwargs)
self.executescript(
"""
CREATE TABLE IF NOT EXISTS __profile__ (
id INTEGER PRIMARY KEY AUTOINCREMENT,
query TEXT NOT NULL,
plan TEXT,
executions INTEGER NOT NULL DEFAULT 1,
total_time INTEGER NOT NULL DEFAULT 0,
average_time INTEGER NOT NULL DEFAULT 0,
min_time INTEGER NOT NULL DEFAULT 0,
max_time INTEGER NOT NULL DEFAULT 0,
callers JSONB
);
"""
)
# Striped querys PK dictionary
self._querys = {}
# Populate querys
for row in super().execute("SELECT id, query FROM __profile__;"):
id, query = row
self._querys[query] = id
def cursor(self):
return super(CmbProfileConnection, self).cursor(CmbProfileCursor)
def execute(self, *args):
start = time.monotonic_ns()
retval = super().execute(*args)
self.log_query(time.monotonic_ns() - start, *args)
return retval
def log_query(self, exec_time, *args):
query = args[0].strip()
if query.startswith("CREATE") or query.startswith("PRAGMA"):
return
caller = inspect.getframeinfo(inspect.stack()[2][0])
file = os.path.basename(caller.filename).removesuffix('.py')
function = caller.function
# Use a different dot to avoid json syntax error
caller_id = f"{file}{function}:{caller.lineno}"
if file == "cmb_db" and function == "execute":
caller = inspect.getframeinfo(inspect.stack()[3][0])
file = os.path.basename(caller.filename).removesuffix('.py')
caller_id = f"{file}{caller.function}:{caller.lineno} {caller_id}"
# Convert from nano seconds to micro seconds
exec_time = int(exec_time / 1000)
pk_id = self._querys.get(query, None)
if pk_id is None:
# Get query plan
if len(args) > 1:
c = super().execute(f"EXPLAIN QUERY PLAN {query}", args[1])
else:
c = super().execute(f"EXPLAIN QUERY PLAN {query}")
# Convert plan to a string
plan = []
for row in c:
plan.append(" ".join(str(row)))
plan = "\n".join(plan)
# Create new query entry in profile table
c = super().execute(
"""
INSERT INTO __profile__(query, plan, total_time, average_time, min_time, max_time, callers)
VALUES(?, ?, ?, ?, ?, ?, json(?))
RETURNING id;
""",
(query, plan, exec_time, exec_time, exec_time, exec_time, f"""{{"{caller_id}": 1}}""")
)
pk_id = c.fetchone()[0]
self._querys[query] = pk_id
else:
# Increment number of executions of this query
super().execute(
f"""
UPDATE __profile__
SET
executions=executions+1,
total_time=total_time+?,
average_time=total_time/executions,
min_time=min(min_time, ?),
max_time=max(max_time, ?),
callers=json_set(callers, '$.{caller_id}', callers->'$.{caller_id}' + 1)
WHERE id=?;
""",
(exec_time, exec_time, exec_time, pk_id)
)
class CmbProfileCursor(sqlite3.Cursor):
def execute(self, *args):
start = time.monotonic_ns()
retval = super().execute(*args)
self.connection.log_query(time.monotonic_ns() - start, *args)
return retval

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from .cmb_object import CmbObject from .cmb_object import CmbObject
@ -70,7 +68,7 @@ class CmbFragmentEditor(Gtk.Box):
self.__bindings.append(binding) self.__bindings.append(binding)
# Only objects have child fragments # Only objects have child fragments
if type(obj) is CmbObject and obj.parent: if type(obj) is CmbObject and obj.parent is not None:
binding = GObject.Object.bind_property( binding = GObject.Object.bind_property(
obj, obj,
"custom-child-fragment", "custom-child-fragment",

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_fragment_editor.ui --> <!-- interface-name cmb_fragment_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<template class="CmbFragmentEditor" parent="GtkBox"> <template class="CmbFragmentEditor" parent="GtkBox">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>

View File

@ -1,151 +0,0 @@
#
# Cambalache GResource wrapper
#
# Copyright (C) 2024 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
#
from gi.repository import GObject, Gio
from .cmb_path import CmbPath
from .cmb_objects_base import CmbBaseGResource
from .cmb_list_error import CmbListError
from cambalache import _
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)
def __bool__(self):
return True
def __str__(self):
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)
def parent(self):
if self.resource_type in ["gresource", "file"]:
return self.project.get_gresource_by_id(self.parent_id)
return None
@GObject.Property(type=CmbBaseGResource)
def gresources_bundle(self):
resource_type = self.resource_type
if resource_type == "gresource":
return self.parent
elif resource_type == "file":
return self.parent.parent
return self
@GObject.Property(type=str)
def display_name(self):
resource_type = self.resource_type
if resource_type == "gresources":
gresources_filename = self.gresources_filename
if gresources_filename:
basename, relpath = self.project._get_basename_relpath(self.gresources_filename)
return basename
return _("Unnamed GResource {id}").format(id=self.gresource_id)
elif resource_type == "gresource":
gresource_prefix = self.gresource_prefix
return gresource_prefix if gresource_prefix else _("Unprefixed resource {id}").format(id=self.gresource_id)
elif resource_type == "file":
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
key = self.db_get(
"SELECT gresource_id FROM gresource WHERE parent_id=? AND position=?;",
(gresource_id, position)
)
if key is not None:
return self.project.get_gresource_by_id(key)
# This should not happen
return CmbListError()
def do_get_item_type(self):
return CmbBaseGResource
@GObject.Property(type=int)
def n_items(self):
if self.resource_type in ["gresources", "gresource"]:
retval = self.db_get("SELECT COUNT(gresource_id) FROM gresource WHERE parent_id=?;", (self.gresource_id, ))
return retval if retval is not None else 0
else:
return 0
def do_get_n_items(self):
return self.n_items

View File

@ -1,117 +0,0 @@
#
# CmbGResourceEditor - Cambalache GResource Editor
#
# Copyright (C) 2024 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
#
from gi.repository import GObject, Gtk
from .cmb_gresource import CmbGResource
from cambalache import getLogger
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_gresource_editor.ui")
class CmbGResourceEditor(Gtk.Box):
__gtype_name__ = "CmbGResourceEditor"
stack = Gtk.Template.Child()
gresources_filename = Gtk.Template.Child()
gresource_prefix = Gtk.Template.Child()
file_filename = Gtk.Template.Child()
file_compressed = Gtk.Template.Child()
file_preprocess = Gtk.Template.Child()
file_alias = Gtk.Template.Child()
fields = [
("gresources", "gresources_filename", "cmb-value"),
("gresource", "gresource_prefix", "cmb-value"),
("file", "file_filename", "cmb-value"),
("file", "file_compressed", "active"),
("file", "file_preprocess", "cmb-value"),
("file", "file_alias", "cmb-value"),
]
def __init__(self, **kwargs):
self._object = None
self._bindings = []
super().__init__(**kwargs)
@GObject.Property(type=CmbGResource)
def object(self):
return self._object
@object.setter
def _set_object(self, obj):
if obj == self._object:
return
for binding in self._bindings:
binding.unbind()
self._bindings = []
self._object = obj
if obj is None:
self.set_sensitive(False)
for for_type, field, target in self.fields:
widget = getattr(self, field)
target_prop = getattr(widget, target)
if isinstance(target_prop, int):
setattr(widget, target, 0)
else:
setattr(widget, target, None)
return
resource_type = obj.resource_type
self.set_sensitive(True)
self.stack.set_visible_child_name(resource_type)
self.gresources_filename.dirname = obj.project.dirname
self.file_filename.dirname = obj.project.dirname
for for_type, field, target in self.fields:
if resource_type != for_type:
continue
binding = GObject.Object.bind_property(
obj,
field,
getattr(self, field),
target,
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self._bindings.append(binding)
@Gtk.Template.Callback("on_add_gresource_button_clicked")
def __on_add_gresource_button_clicked(self, button):
self._object.project.add_gresource("gresource", parent_id=self._object.gresource_id)
@Gtk.Template.Callback("on_add_file_button_clicked")
def __on_add_file_button_clicked(self, button):
self._object.project.add_gresource("file", parent_id=self._object.gresource_id)
Gtk.WidgetClass.set_css_name(CmbGResourceEditor, "CmbGResourceEditor")

View File

@ -1,241 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_gresource_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.12"/>
<template class="CmbGResourceEditor" parent="GtkBox">
<child>
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkGrid">
<property name="column-spacing">4</property>
<property name="row-spacing">4</property>
<child>
<object class="GtkLabel">
<property name="halign">end</property>
<property name="label" translatable="yes">Filename</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="CmbFileButton" id="gresources_filename">
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">end</property>
<property name="label" translatable="yes">Add</property>
<layout>
<property name="column">0</property>
<property name="column-span">1</property>
<property name="row">1</property>
<property name="row-span">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">* This resource file need to be compiled and loaded at runtime</property>
<property name="valign">end</property>
<property name="vexpand">True</property>
<property name="wrap">True</property>
<property name="xalign">0.0</property>
<layout>
<property name="column">0</property>
<property name="column-span">2</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="add_gresource_button">
<property name="halign">start</property>
<property name="label">GResource</property>
<signal name="clicked" handler="on_add_gresource_button_clicked"/>
<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>
</object>
</property>
<property name="name">gresources</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkGrid">
<property name="column-spacing">4</property>
<property name="row-spacing">4</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Prefix</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="CmbEntry" id="gresource_prefix">
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">* Files defined inside this will be available at gresource://prefix</property>
<property name="valign">end</property>
<property name="vexpand">True</property>
<property name="wrap">True</property>
<property name="xalign">0.0</property>
<layout>
<property name="column">0</property>
<property name="column-span">2</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Add</property>
<layout>
<property name="column">0</property>
<property name="column-span">1</property>
<property name="row">1</property>
<property name="row-span">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="add_file_button">
<property name="halign">start</property>
<property name="label" translatable="yes">file</property>
<signal name="clicked" handler="on_add_file_button_clicked"/>
<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>
</object>
</property>
<property name="name">gresource</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkGrid">
<property name="column-spacing">4</property>
<property name="row-spacing">4</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Filename</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Compressed</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Preprocess</property>
<layout>
<property name="column">0</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Alias</property>
<layout>
<property name="column">0</property>
<property name="row">3</property>
</layout>
</object>
</child>
<child>
<object class="GtkSwitch" id="file_compressed">
<property name="halign">start</property>
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="CmbEntry" id="file_preprocess">
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="CmbEntry" id="file_alias">
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">3</property>
</layout>
</object>
</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>
<property name="row">0</property>
<property name="row-span">1</property>
</layout>
</object>
</child>
</object>
</property>
<property name="name">file</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -20,13 +20,10 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject from gi.repository import GObject
from .cmb_objects_base import CmbBaseLayoutProperty from .cmb_objects_base import CmbBaseLayoutProperty, CmbPropertyInfo
from .cmb_property_info import CmbPropertyInfo
from . import utils from . import utils
@ -35,7 +32,9 @@ class CmbLayoutProperty(CmbBaseLayoutProperty):
info = GObject.Property(type=CmbPropertyInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) info = GObject.Property(type=CmbPropertyInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__on_init = True
super().__init__(**kwargs) super().__init__(**kwargs)
self.__on_init = False
self.version_warning = None self.version_warning = None
owner_info = self.project.type_info.get(self.info.owner_id, None) owner_info = self.project.type_info.get(self.info.owner_id, None)
@ -106,6 +105,13 @@ class CmbLayoutProperty(CmbBaseLayoutProperty):
(self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id, value), (self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id, value),
) )
# Update object position if this is a position property
if self.info.is_position:
self.object.position = int(value) if value else 0
if not self.__on_init:
self.object._layout_property_changed(self)
c.close() c.close()
def _update_version_warning(self): def _update_version_warning(self):

View File

@ -20,16 +20,11 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject
from .cmb_objects_base import CmbBaseLibraryInfo from .cmb_objects_base import CmbBaseLibraryInfo
class CmbLibraryInfo(CmbBaseLibraryInfo): class CmbLibraryInfo(CmbBaseLibraryInfo):
third_party = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@ -1,7 +1,7 @@
# #
# Cambalache GListModel error item # CmbListStore - Cambalache List Store
# #
# Copyright (C) 2024 Juan Pablo Ugarte # Copyright (C) 2021 Juan Pablo Ugarte
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
@ -20,23 +20,27 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject from gi.repository import GObject, Gtk
from .cmb_base import CmbBase
# This class is used by GListModel implementations when they do not know which item to return class CmbListStore(Gtk.ListStore):
class CmbListError(CmbBase): __gtype_name__ = "CmbListStore"
table = GObject.Property(type=str)
query = GObject.Property(type=str)
project = GObject.Property(type=GObject.GObject)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@GObject.Property(type=str) data = self.project._get_table_data(self.table)
def display_name(self): self.set_column_types(data["types"])
return "list error" self.__populate()
@GObject.Property(type=int) def __populate(self):
def n_items(self): c = self.project.db.cursor()
return 0 for row in c.execute(self.query):
self.append(row)
c.close()

View File

@ -1,260 +0,0 @@
#
# CmbColumnView
#
# Copyright (C) 2024 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
#
from gi.repository import GLib, GObject, Gdk, Gtk
from .cmb_ui import CmbUI
from .cmb_object import CmbObject
from .cmb_gresource import CmbGResource
from .cmb_context_menu import CmbContextMenu
from .cmb_path import CmbPath
from .cmb_project import CmbProject
from .cmb_tree_expander import CmbTreeExpander
class CmbListView(Gtk.ListView):
__gtype_name__ = "CmbListView"
def __init__(self, **kwargs):
self.__project = None
self.__tree_model = None
self.__in_selection_change = False
self.single_selection = Gtk.SingleSelection()
super().__init__(**kwargs)
self.props.has_tooltip = True
self.props.hexpand = True
factory = Gtk.SignalListItemFactory()
factory.connect("setup", self._on_factory_setup)
factory.connect("bind", self._on_factory_bind)
factory.connect("unbind", self._on_factory_unbind)
self.props.factory = factory
self.single_selection.connect("notify::selected-item", self.__on_selected_item_notify)
self.set_model(self.single_selection)
gesture = Gtk.GestureClick(button=Gdk.BUTTON_SECONDARY)
gesture.connect("pressed", self.__on_button_press)
self.add_controller(gesture)
self.connect("activate", self.__on_activate)
self.add_css_class("navigation-sidebar")
self.add_css_class("cmb-list-view")
def __on_button_press(self, gesture, npress, x, y):
expander = self.__get_tree_expander(x, y)
if expander is None or npress != 1:
return False
# Select row at x,y
list_row = expander.get_list_row()
self.single_selection.set_selected(list_row.get_position())
menu = CmbContextMenu()
if self.__project:
menu.target_tk = self.__project.target_tk
menu.set_parent(self)
menu.popup_at(x, y)
return True
def __get_path_parent(self, obj):
if isinstance(obj, CmbObject):
parent = obj.parent
return parent if parent else obj.ui
elif isinstance(obj, CmbGResource):
return obj.path_parent if obj.resource_type == "gresources" else obj.parent
elif isinstance(obj, CmbProject):
return None
return obj.path_parent
def __get_object_ancestors(self, obj):
ancestors = set()
parent = self.__get_path_parent(obj)
while parent:
ancestors.add(parent)
parent = self.__get_path_parent(parent)
return ancestors
def __object_ancestor_expand(self, obj):
ancestors = self.__get_object_ancestors(obj)
i = 0
# Iterate over tree model
# NOTE: only visible/expanded rows are returned
list_row = self.__tree_model.get_row(i)
while list_row:
item = list_row.get_item()
# Return position if we reached the object row
if item == obj:
return i
elif item in ancestors:
# Expand row if its part of the hierarchy
list_row.set_expanded(True)
i += 1
list_row = self.__tree_model.get_row(i)
return None
def __on_project_selection_changed(self, p):
list_row = self.single_selection.get_selected_item()
current_selection = [list_row.get_item()] if list_row else []
selection = self.__project.get_selection()
if selection == current_selection:
return
self.__in_selection_change = True
if len(selection) > 0:
position = self.__object_ancestor_expand(selection[0])
if position is not None:
self.single_selection.select_item(position, True)
else:
self.single_selection.unselect_all()
else:
self.single_selection.unselect_all()
self.__in_selection_change = False
@GObject.Property(type=CmbProject)
def project(self):
return self.__project
@project.setter
def _set_project(self, project):
if self.__project:
self.__project.disconnect_by_func(self.__on_project_selection_changed)
self.__project = project
if project:
self.__tree_model = Gtk.TreeListModel.new(
project,
False,
False,
self.__tree_model_create_func,
None
)
self.single_selection.props.model = self.__tree_model
self.__project.connect("selection-changed", self.__on_project_selection_changed)
else:
self.single_selection.props.model = None
def __tree_model_create_func(self, item, data):
if isinstance(item, CmbObject):
return item
elif isinstance(item, CmbUI):
return item
elif isinstance(item, CmbGResource):
return item
elif isinstance(item, CmbPath):
return item
return None
def __on_selected_item_notify(self, single_selection, pspec):
if self.__in_selection_change or self.__project is None:
return
list_item = single_selection.get_selected_item()
position = single_selection.get_selected()
if list_item is None:
self.__project.set_selection([])
return
item = list_item.get_item()
self.activate_action("list.activate-item", GLib.Variant("u", position))
if item and not isinstance(item, CmbPath):
item = list_item.get_item()
self.__project.set_selection([item])
else:
self.__project.set_selection([])
def _on_factory_setup(self, factory, list_item):
expander = CmbTreeExpander()
list_item.set_child(expander)
def _on_factory_bind(self, factory, list_item):
row = list_item.get_item()
expander = list_item.get_child()
expander.set_list_row(row)
expander.update_bind()
def _on_factory_unbind(self, factory, list_item):
expander = list_item.get_child()
expander.clear_bind()
def __get_tree_expander(self, x, y):
pick = self.pick(x, y, Gtk.PickFlags.DEFAULT)
if pick is None:
return None
if isinstance(pick, Gtk.TreeExpander):
return pick
child = pick.get_first_child()
if child and isinstance(child, Gtk.TreeExpander):
return child
parent = pick.props.parent
if parent and isinstance(parent, Gtk.TreeExpander):
return parent
return None
def __on_activate(self, column_view, position):
item = self.__tree_model.get_item(position)
item.set_expanded(not item.get_expanded())
def do_query_tooltip(self, x, y, keyboard_mode, tooltip):
expander = self.__get_tree_expander(x, y)
if expander is None:
return False
obj = expander.get_item()
if isinstance(obj, CmbObject):
msg = obj.version_warning
if msg:
tooltip.set_text(msg)
return True
return False

View File

@ -1,50 +0,0 @@
#
# CmbMessageNotificationView
#
# 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
#
from cambalache import getLogger
from gi.repository import GObject, Gtk
from .cmb_notification import CmbMessageNotification
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_message_notification_view.ui")
class CmbMessageNotificationView(Gtk.Box):
__gtype_name__ = "CmbMessageNotificationView"
notification = GObject.Property(
type=CmbMessageNotification, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
# Message
title_label = Gtk.Template.Child()
message_label = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
notification = self.notification
self.title_label.props.label = f"<b>{notification.title}</b>"
self.message_label.props.label = notification.message

View File

@ -1,22 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_message_notification_view.ui -->
<requires lib="gtk" version="4.0"/>
<template class="CmbMessageNotificationView" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="halign">start</property>
<property name="use-markup">True</property>
</object>
</child>
<child>
<object class="GtkLabel" id="message_label">
<property name="halign">start</property>
<property name="use-markup">True</property>
</object>
</child>
</template>
</interface>

View File

@ -1,378 +0,0 @@
#
# Cambalache notification system
#
# 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 os
import json
import threading
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
from cambalache import getLogger
from . import utils
logger = getLogger(__name__)
class CmbBaseData(GObject.GObject):
def __init__(self, **kwargs):
for prop in self.list_properties():
name = prop.name.replace("-", "_")
if name not in kwargs:
continue
value = kwargs[name]
if isinstance(value, dict) and prop.value_type in GTYPE_PTYHON:
Klass = GTYPE_PTYHON[prop.value_type]
kwargs[name] = Klass(**value)
super().__init__(**kwargs)
def dict(self):
retval = {}
for prop in self.list_properties():
name = prop.name.replace("-", "_")
value = self.get_property(prop.name)
if prop.value_type in GTYPE_PTYHON:
retval[name] = value.dict() if value else None
elif not isinstance(value, GObject.Object):
retval[name] = value
return retval
class CmbPollData(CmbBaseData):
id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
title = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
description = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
options = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
allowed_votes = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE)
start_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
end_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
class CmbPollResult(CmbBaseData):
votes = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
total = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
GTYPE_PTYHON = {CmbPollData.__gtype__: CmbPollData, CmbPollResult.__gtype__: CmbPollResult}
class CmbNotification(CmbBaseData):
center = GObject.Property(type=GObject.Object, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
type = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
start_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
end_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
class CmbVersionNotification(CmbNotification):
version = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
release_notes = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
read_more_url = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
class CmbMessageNotification(CmbNotification):
title = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
message = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
class CmbPollNotification(CmbNotification):
poll = GObject.Property(type=CmbPollData, flags=GObject.ParamFlags.READWRITE)
results = GObject.Property(type=CmbPollResult, flags=GObject.ParamFlags.READWRITE)
my_votes = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
class CmbNotificationCenter(GObject.GObject):
__gsignals__ = {
"new-notification": (GObject.SignalFlags.RUN_FIRST, None, (CmbNotification,)),
}
# Settings
enabled = GObject.Property(type=bool, default=True, flags=GObject.ParamFlags.READWRITE)
uuid = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
next_request = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
notifications = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
store = GObject.Property(type=Gio.ListStore, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
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")
for prop in ["enabled", "uuid", "next-request", "notifications"]:
self.settings.bind(prop, self, prop.replace("-", "_"), Gio.SettingsBindFlags.DEFAULT)
# Disable notifications if settings backend is ephemeral
if GObject.type_name(Gio.SettingsBackend.get_default()) == "GMemorySettingsBackend":
logger.info("Disabling notifications")
self.enabled = False
self.__load_notifications()
backend = urlparse(os.environ.get("CMB_NOTIFICATION_URL", "https://xjuan.ar:1934"))
self.REQUEST_INTERVAL = 4 if backend.hostname == "localhost" else 24 * 60 * 60
if backend.scheme == "https":
logger.info(f"Backend: {backend.scheme}://{backend.hostname}:{backend.port}")
self.connection = http.client.HTTPSConnection(backend.hostname, backend.port, timeout=8)
else:
self.connection = None
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}")
self._get_notification()
def __get_container(self):
if "FLATPAK_ID" in os.environ:
return "flatpak"
elif "APPIMAGE" in os.environ:
return "appimage"
elif "SNAP" in os.environ:
return "snap"
return None
def __get_user_agent(self):
u = platform.uname()
platform_strings = []
table = str.maketrans({",": "\\,"})
if u.system == "Linux":
release = platform.freedesktop_os_release()
system = f"Linux {release['ID']}"
if "VERSION_ID" in release:
system += f" {release['VERSION_ID']}"
if "VERSION_CODENAME" in release:
system += f" {release['VERSION_CODENAME']}"
else:
system = u.system
display_type = GObject.type_name(Gdk.Display.get_default())
backend = display_type.removeprefix("Gdk").removesuffix("Display")
lang = HarfBuzz.language_to_string(HarfBuzz.language_get_default())
extra = []
# Container type
container = self.__get_container()
if container:
extra.append(f"container {container}")
# GSettings backend
settings_backend = Gio.SettingsBackend.get_default()
gsettings_backend = GObject.type_name(settings_backend).removesuffix("SettingsBackend")
for name, lib in [("GLib", GLib), ("Gtk", Gtk), ("Adw", Adw)]:
extra.append(f"{name} {lib.MAJOR_VERSION}.{lib.MINOR_VERSION}.{lib.MICRO_VERSION}")
# Ignore node name as that is private and irrelevant information
for string in [system, u.release, u.version, u.machine, backend, gsettings_backend]:
if not string:
continue
platform_strings.append(string.translate(table))
return f"Cambalache/{VERSION} ({', '.join(platform_strings)}; {'; '.join(extra)}; {lang})"
def __load_notifications(self):
self.store.remove_all()
if not self.notifications:
return
notifications = json.loads(self.notifications)
now = utils.utcnow()
for data in notifications:
if "end_date" in data and now > data["end_date"]:
continue
self.store.append(self.__notification_from_dict(data))
def __save_notifications(self):
notifications = []
for notification in self.store:
notifications.append(notification.dict())
# Store in GSettings
self.notifications = json.dumps(notifications, indent=2, sort_keys=True)
def __notification_from_dict(self, data):
ntype = data.get("type", None)
if ntype == "version":
return CmbVersionNotification(center=self, **data)
elif ntype == "message":
return CmbMessageNotification(center=self, **data)
elif ntype == "poll":
return CmbPollNotification(center=self, **data)
def __get_notification_idle(self, data):
logger.debug(f"Got notification response {json.dumps(data, indent=2, sort_keys=True)}")
if "notification" in data:
notification = self.__notification_from_dict(data["notification"])
self.store.insert(0, notification)
self.__save_notifications()
self.emit("new-notification", notification)
now = int(time.time())
self.next_request = now + self.REQUEST_INTERVAL
self._get_notification()
return GLib.SOURCE_REMOVE
def __get_notification_thread(self):
headers = {
"User-Agent": self.user_agent,
"x-cambalache-uuid": self.uuid,
}
try:
logger.info(f"GET /notification {headers=}")
self.connection.request("GET", "/notification", headers=headers)
response = self.connection.getresponse()
assert response.status == 200
# Reset retry interval
self.retry_interval = 8
data = response.read().decode()
logger.info(f"response={data}")
if data:
GLib.idle_add(self.__get_notification_idle, json.loads(data))
except Exception as e:
# If it fails we just wait a bit before retrying
self.retry_interval *= 2
self.retry_interval = min(self.retry_interval, 256)
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()
def __run_in_thread(self, function, *args, **kwargs):
if not self.connection:
logger.warning("No connection defined")
return
if not self.enabled:
logger.info("Notifications disabled")
return
thread = threading.Thread(target=function, args=args, kwargs=kwargs, daemon=True)
thread.start()
def _get_notification(self):
now = int(time.time())
if now >= self.next_request:
self.__run_in_thread(self.__get_notification_thread)
else:
GLib.timeout_add_seconds(self.next_request - now, self._get_notification)
def __poll_vote_idle(self, data):
logger.debug(f"Got vote response {data}")
poll_uuid = data["uuid"]
results = data["results"]
for notification in self.store:
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, poll_uuid):
for notification in self.store:
if isinstance(notification, CmbPollNotification) and notification.poll.id == poll_uuid:
notification.my_votes = []
break
return GLib.SOURCE_REMOVE
def __poll_vote_thread(self, method, poll_uuid, votes=None):
headers = {"User-Agent": self.user_agent, "x-cambalache-uuid": self.uuid, "Content-type": "application/json"}
try:
payload = json.dumps({"votes": votes}) if method == "POST" else None
self.connection.request(method, f"/poll/{poll_uuid}", payload, headers)
response = self.connection.getresponse()
assert response.status == 200
data = response.read().decode()
GLib.idle_add(self.__poll_vote_idle, json.loads(data))
except Exception as e:
logger.warning(f"Error voting {e}")
GLib.idle_add(self.__poll_vote_exception_idle, poll_uuid)
self.connection.close()
def poll_vote(self, notification: CmbPollNotification, votes: list[int]):
if self.uuid is None:
return
notification.my_votes = votes
self.__run_in_thread(self.__poll_vote_thread, "POST", notification.poll.id, votes)
def poll_refresh_results(self, notification: CmbPollNotification):
if self.uuid is None:
return
self.__run_in_thread(self.__poll_vote_thread, "GET", notification.poll.id)
def remove(self, notification: CmbNotification):
valid, position = self.store.find(notification)
if valid:
self.store.remove(position)
self.__save_notifications()
notification_center = CmbNotificationCenter()

View File

@ -1,60 +0,0 @@
#
# CmbNotificationListRow
#
# 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 datetime
from cambalache import getLogger
from gi.repository import GObject, Gtk
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_notification_list_row.ui")
class CmbNotificationListRow(Gtk.ListBoxRow):
__gtype_name__ = "CmbNotificationListRow"
view = GObject.Property(type=Gtk.Widget, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
box = Gtk.Template.Child()
date_label = Gtk.Template.Child()
close_button = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.props.activatable = False
notification = self.view.notification
start_date = datetime.datetime.utcfromtimestamp(notification.start_date).strftime("%x")
self.date_label.set_label(f"<small>{start_date}</small>")
self.box.append(self.view)
@Gtk.Template.Callback("on_map")
def __on_map(self, w):
self.props.child.props.reveal_child = True
@Gtk.Template.Callback("on_close_button_clicked")
def __on_close_button_clicked(self, button):
notification = self.view.notification
notification.center.remove(notification)

View File

@ -1,42 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_notification_list_row.ui -->
<requires lib="gtk" version="4.12"/>
<template class="CmbNotificationListRow" parent="GtkListBoxRow">
<signal name="map" handler="on_map"/>
<child>
<object class="GtkRevealer">
<child>
<object class="GtkBox" id="box">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<property name="vexpand-set">True</property>
<child>
<object class="GtkLabel" id="date_label">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="use-markup">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="close_button">
<property name="icon-name">window-close-symbolic</property>
<signal name="clicked" handler="on_close_button_clicked"/>
<style>
<class name="flat"/>
<class name="compact"/>
<class name="close"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -1,67 +0,0 @@
#
# CmbNotificationListView
#
# 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
#
from cambalache import getLogger
from gi.repository import GObject, Gtk
from .cmb_version_notification_view import CmbVersionNotificationView
from .cmb_message_notification_view import CmbMessageNotificationView
from .cmb_poll_notification_view import CmbPollNotificationView
from .cmb_notification_list_row import CmbNotificationListRow
from .cmb_notification import CmbNotificationCenter, CmbVersionNotification, CmbMessageNotification, CmbPollNotification
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_notification_list_view.ui")
class CmbNotificationListView(Gtk.Box):
__gtype_name__ = "CmbNotificationListView"
notification_center = GObject.Property(type=CmbNotificationCenter, flags=GObject.ParamFlags.READWRITE)
list_box = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect("notify::notification-center", self.__on_notification_center_notify)
def __on_notification_center_notify(self, obj, pspec):
self.list_box.bind_model(self.notification_center.store, self.__create_widget_func)
GObject.Object.bind_property(
self.notification_center.store,
"n-items",
self.list_box,
"visible",
GObject.BindingFlags.SYNC_CREATE,
)
def __create_widget_func(self, item):
if isinstance(item, CmbVersionNotification):
view = CmbVersionNotificationView(notification=item)
elif isinstance(item, CmbMessageNotification):
view = CmbMessageNotificationView(notification=item)
elif isinstance(item, CmbPollNotification):
view = CmbPollNotificationView(notification=item)
return CmbNotificationListRow(view=view)

View File

@ -1,26 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_notification_list_view.ui -->
<requires lib="gtk" version="4.6"/>
<template class="CmbNotificationListView" parent="GtkBox">
<property name="hexpand">True</property>
<child>
<object class="GtkScrolledWindow">
<property name="hexpand">True</property>
<property name="propagate-natural-height">True</property>
<property name="propagate-natural-width">True</property>
<child>
<object class="GtkListBox" id="list_box">
<property name="activate-on-single-click">False</property>
<property name="selection-mode">none</property>
<property name="show-separators">True</property>
<style>
<class name="notifications"/>
</style>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -20,26 +20,22 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gio from gi.repository import GObject
from .cmb_list_error import CmbListError
from .cmb_objects_base import CmbBaseObject, CmbSignal from .cmb_objects_base import CmbBaseObject, CmbSignal
from .cmb_property import CmbProperty from .cmb_property import CmbProperty
from .cmb_layout_property import CmbLayoutProperty from .cmb_layout_property import CmbLayoutProperty
from .cmb_object_data import CmbObjectData from .cmb_object_data import CmbObjectData
from .cmb_type_info import CmbTypeInfo from .cmb_type_info import CmbTypeInfo
from .cmb_ui import CmbUI from .cmb_ui import CmbUI
from .constants import GMENU_SECTION_TYPE, GMENU_SUBMENU_TYPE, GMENU_ITEM_TYPE
from . import utils from . import utils
from cambalache import getLogger, _ from cambalache import getLogger, _
logger = getLogger(__name__) logger = getLogger(__name__)
class CmbObject(CmbBaseObject, Gio.ListModel): class CmbObject(CmbBaseObject):
info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
__gsignals__ = { __gsignals__ = {
@ -50,23 +46,20 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
"signal-changed": (GObject.SignalFlags.RUN_FIRST, None, (CmbSignal,)), "signal-changed": (GObject.SignalFlags.RUN_FIRST, None, (CmbSignal,)),
"data-added": (GObject.SignalFlags.RUN_FIRST, None, (CmbObjectData,)), "data-added": (GObject.SignalFlags.RUN_FIRST, None, (CmbObjectData,)),
"data-removed": (GObject.SignalFlags.RUN_FIRST, None, (CmbObjectData,)), "data-removed": (GObject.SignalFlags.RUN_FIRST, None, (CmbObjectData,)),
"child-reordered": (GObject.SignalFlags.RUN_FIRST, None, (CmbBaseObject, int, int)),
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__properties = None self.properties = []
self.__properties_dict = None self.properties_dict = {}
self.__layout = None self.layout = []
self.__layout_dict = None self.layout_dict = {}
self.__signals = None self.signals = []
self.__signals_dict = None self.signals_dict = {}
self.__data = None self.data = []
self.__data_dict = None self.data_dict = {}
self.position_layout_property = None
self.inline_property_id = None self.inline_property_id = None
self.version_warning = None self.version_warning = None
self.__is_template = False
self._last_known = None
super().__init__(**kwargs) super().__init__(**kwargs)
@ -75,79 +68,26 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
if self.project is None: if self.project is None:
return return
self.__update_inline_property_id() # Append object to project automatically
self.project._append_object(self)
self.__populate_properties()
self.__populate_layout_properties()
self.__populate_signals()
self.__populate_data()
self.__update_version_warning() self.__update_version_warning()
self.ui.connect("notify", self._on_ui_notify)
self.ui.connect("library-changed", self._on_ui_library_changed) self.ui.connect("library-changed", self._on_ui_library_changed)
def __bool__(self):
# Override Truth Value Testing to ensure that CmbObject objects evaluates to True even if it does not have children
return True
def __str__(self): def __str__(self):
return f"CmbObject<{self.display_name_type}> {self.ui_id}:{self.object_id}" return f"CmbObject<{self.type_id}> {self.ui_id}:{self.object_id}"
@property
def properties(self):
self.__populate_properties()
return self.__properties
@property
def properties_dict(self):
self.__populate_properties()
return self.__properties_dict
@property
def layout(self):
self.__populate_layout()
return self.__layout
@property
def layout_dict(self):
self.__populate_layout()
return self.__layout_dict
@property
def signals(self):
self.__populate_signals()
return self.__signals
@property
def signals_dict(self):
self.__populate_signals()
return self.__signals_dict
@property
def data(self):
self.__populate_data()
return self.__data
@property
def data_dict(self):
self.__populate_data()
return self.__data_dict
def __update_inline_property_id(self):
ui_id = self.ui_id
object_id = self.object_id
parent_id = self.parent_id
if parent_id:
# Set which parent property makes a reference to this inline object
row = self.project.db.execute(
"SELECT property_id FROM object_property WHERE ui_id=? AND inline_object_id=?;", (ui_id, object_id)
).fetchone()
self.inline_property_id = row[0] if row else None
def __populate_type_properties(self, name): def __populate_type_properties(self, name):
property_info = self.project.get_type_properties(name) property_info = self.project.get_type_properties(name)
if property_info is None: if property_info is None:
return return
for property_name, info in property_info.items(): for property_name in property_info:
# Check if this property was already installed by a derived class info = property_info[property_name]
if property_name in self.__properties_dict:
continue
prop = CmbProperty( prop = CmbProperty(
object=self, object=self,
@ -160,31 +100,16 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
) )
# List of property # List of property
self.__properties.append(prop) self.properties.append(prop)
# Dictionary of properties # Dictionary of properties
self.__properties_dict[property_name] = prop self.properties_dict[property_name] = prop
def __populate_properties(self): def __populate_properties(self):
if self.__properties is not None:
return
self.__properties = []
self.__properties_dict = {}
self.__populate_type_properties(self.type_id) self.__populate_type_properties(self.type_id)
for parent_id in self.info.hierarchy: for parent_id in self.info.hierarchy:
self.__populate_type_properties(parent_id) self.__populate_type_properties(parent_id)
# Add accessible properties for GtkWidgets
if parent_id == "GtkWidget":
for accessible_id in [
"CmbAccessibleProperty",
"CmbAccessibleRelation",
"CmbAccessibleState",
"CmbAccessibleAction"
]:
self.__populate_type_properties(accessible_id)
def __populate_layout_properties_from_type(self, name): def __populate_layout_properties_from_type(self, name):
property_info = self.project.get_type_properties(name) property_info = self.project.get_type_properties(name)
if property_info is None: if property_info is None:
@ -206,10 +131,14 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
info=info, info=info,
) )
self.__layout.append(prop) # Keep a reference to the position layout property
if info.is_position:
self.position_layout_property = prop
self.layout.append(prop)
# Dictionary of properties # Dictionary of properties
self.__layout_dict[property_name] = prop self.layout_dict[property_name] = prop
def _property_changed(self, prop): def _property_changed(self, prop):
self.emit("property-changed", prop) self.emit("property-changed", prop)
@ -221,9 +150,8 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
self.project._object_layout_property_changed(parent, self, prop) self.project._object_layout_property_changed(parent, self, prop)
def __add_signal_object(self, signal): def __add_signal_object(self, signal):
self.__populate_signals() self.signals.append(signal)
self.__signals.append(signal) self.signals_dict[signal.signal_pk] = signal
self.__signals_dict[signal.signal_pk] = signal
self.emit("signal-added", signal) self.emit("signal-added", signal)
self.project._object_signal_added(self, signal) self.project._object_signal_added(self, signal)
@ -234,11 +162,11 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
self.project._object_signal_changed(self, signal) self.project._object_signal_changed(self, signal)
def __add_data_object(self, data): def __add_data_object(self, data):
if data.get_id_string() in self.data_dict: if data in self.data:
return return
self.__data.append(data) self.data.append(data)
self.__data_dict[data.get_id_string()] = data self.data_dict[data.get_id_string()] = data
self.emit("data-added", data) self.emit("data-added", data)
self.project._object_data_added(self, data) self.project._object_data_added(self, data)
@ -249,11 +177,6 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
self.project._object_changed(self, pspec.name) self.project._object_changed(self, pspec.name)
def __populate_signals(self): def __populate_signals(self):
if self.__signals is not None:
return
self.__signals = []
self.__signals_dict = {}
c = self.project.db.cursor() c = self.project.db.cursor()
# Populate signals # Populate signals
@ -261,11 +184,6 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
self.__add_signal_object(CmbSignal.from_row(self.project, *row)) self.__add_signal_object(CmbSignal.from_row(self.project, *row))
def __populate_data(self): def __populate_data(self):
if self.__data is not None:
return
self.__data = []
self.__data_dict = {}
c = self.project.db.cursor() c = self.project.db.cursor()
# Populate data # Populate data
@ -279,17 +197,13 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
parent_id = self.parent_id parent_id = self.parent_id
# FIXME: delete is anything is set? # FIXME: delete is anything is set?
self.__layout = [] self.layout = []
self.__layout_dict = {} self.layout_dict = {}
if parent_id > 0: if parent_id > 0:
# FIXME: what about parent layout properties?
parent = self.project.get_object_by_id(self.ui_id, parent_id) parent = self.project.get_object_by_id(self.ui_id, parent_id)
for owner_id in [parent.type_id] + parent.info.hierarchy: self.__populate_layout_properties_from_type(f"{parent.type_id}LayoutChild")
self.__populate_layout_properties_from_type(f"{owner_id}LayoutChild")
def __populate_layout(self):
if self.__layout is None:
self.__populate_layout_properties()
@GObject.Property(type=int) @GObject.Property(type=int)
def parent_id(self): def parent_id(self):
@ -304,42 +218,15 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
@parent_id.setter @parent_id.setter
def _set_parent_id(self, value): def _set_parent_id(self, value):
new_parent_id = value if value != 0 else None self.db_set(
old_parent_id = self.parent_id if self.parent_id != 0 else None "UPDATE object SET parent_id=? WHERE (ui_id, object_id) IS (?, ?);",
(
if old_parent_id == new_parent_id: self.ui_id,
return self.object_id,
),
# Save old parent and position value if value != 0 else None,
self._save_last_known_parent_and_position()
project = self.project
ui_id = self.ui_id
object_id = self.object_id
if new_parent_id is None:
new_position = self.db_get(
"SELECT MAX(position)+1 FROM object WHERE ui_id=? AND parent_id IS NULL",
(ui_id, )
)
else:
new_position = self.db_get(
"SELECT MAX(position)+1 FROM object WHERE ui_id=? AND parent_id=?",
(ui_id, new_parent_id)
)
project.db.execute(
"UPDATE object SET parent_id=?, position=? WHERE ui_id=? AND object_id=?;",
(new_parent_id, new_position or 0, ui_id, object_id)
) )
# Update children positions in old parent
project.db.update_children_position(ui_id, old_parent_id)
# Update GListModel
self._remove_from_old_parent()
self._update_new_parent()
self.__populate_layout_properties() self.__populate_layout_properties()
@GObject.Property(type=CmbUI) @GObject.Property(type=CmbUI)
@ -400,8 +287,8 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
) )
def _remove_signal(self, signal): def _remove_signal(self, signal):
self.__signals.remove(signal) self.signals.remove(signal)
del self.__signals_dict[signal.signal_pk] del self.signals_dict[signal.signal_pk]
self.emit("signal-removed", signal) self.emit("signal-removed", signal)
self.project._object_signal_removed(self, signal) self.project._object_signal_removed(self, signal)
@ -446,29 +333,23 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
return self._add_data(owner_id, data_id, id, info=taginfo) return self._add_data(owner_id, data_id, id, info=taginfo)
def _remove_data(self, data): def _remove_data(self, data):
if data.get_id_string() not in self.data_dict: if data not in self.data:
return return
self.__data.remove(data) self.data.remove(data)
del self.__data_dict[data.get_id_string()] del self.data_dict[data.get_id_string()]
self.emit("data-removed", data) self.emit("data-removed", data)
self.project._object_data_removed(self, data) self.project._object_data_removed(self, data)
def remove_data(self, data): def remove_data(self, data):
try: try:
assert data.get_id_string() in self.data_dict assert data in self.data
self.project.history_push(
_("Remove {key} from {name}").format(key=data.info.key, name=self.display_name_type)
)
self.project.db.execute( self.project.db.execute(
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;", "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.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
) )
self.project.db.commit() self.project.db.commit()
self.project.history_pop()
except Exception as e: except Exception as e:
logger.warning(f"{self} Error removing data {data}: {e}") logger.warning(f"{self} Error removing data {data}: {e}")
return False return False
@ -485,76 +366,45 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
logger.warning(f"{child} is not children of {self}") logger.warning(f"{child} is not children of {self}")
return return
old_position = child.position
old_list_position = child.list_position
if old_position == position:
return
name = child.name if child.name is not None else child.type_id name = child.name if child.name is not None else child.type_id
self.project.history_push( self.project.history_push(
_("Reorder object {name} from position {old} to {new}").format(name=name, old=old_position, new=position) _("Reorder object {name} from position {old} to {new}").format(name=name, old=child.position, new=position)
) )
db = self.project.db children = []
# Consider this children # Get children in order
# c = self.project.db.cursor()
# label 0 for row in c.execute(
# button 1 """
# entry 2 SELECT object_id, position
# switch 3 FROM object
# toggle 4 WHERE ui_id=? AND parent_id=? AND internal IS NULL AND object_id!=? AND object_id NOT IN
(SELECT inline_object_id FROM object_property WHERE inline_object_id IS NOT NULL AND ui_id=? AND object_id=?)
ORDER BY position;
""",
(self.ui_id, self.object_id, child.object_id, self.ui_id, self.object_id),
):
child_id, child_position = row
# Disable check so we can set position temporally to -1 obj = self.project.get_object_by_id(self.ui_id, child_id)
db.ignore_check_constraints = True if obj:
db.execute("UPDATE object SET position=-1 WHERE ui_id=? AND object_id=?;", (self.ui_id, child.object_id)) children.append(obj)
# Make room for new position # Insert child in new position
for select_stmt, update_stmt in [ children.insert(position, child)
(
"""
SELECT ui_id, object_id
FROM object
WHERE ui_id=? AND parent_id=? AND position <= ? AND position > ?
ORDER BY position ASC
""",
"UPDATE object SET position=position - 1 WHERE ui_id=? AND object_id=?;"
),
(
"""
SELECT ui_id, object_id
FROM object
WHERE ui_id=? AND parent_id=? AND position >= ? AND position < ?
ORDER BY position DESC
""",
"UPDATE object SET position=position + 1 WHERE ui_id=? AND object_id=?;"
),
]:
for row in db.execute(select_stmt, (self.ui_id, self.object_id, position, old_position)):
db.execute(update_stmt, tuple(row))
# Set new position # Update all positions
db.execute("UPDATE object SET position=? WHERE ui_id=? AND object_id=?;", (position, self.ui_id, child.object_id)) for pos, obj in enumerate(children):
# Sync layout property
db.ignore_check_constraints = False if obj.position_layout_property:
obj.position_layout_property.value = pos
list_position = child.list_position else:
# Or object position
self.project._ignore_selection = True obj.position = pos
# Emit GListModel signals
if position < old_position:
self.items_changed(list_position, 0, 1)
self.items_changed(old_list_position+1, 1, 0)
else:
if old_list_position != list_position:
self.items_changed(old_list_position, 1, 0)
self.items_changed(list_position, 0, 1)
self.project._ignore_selection = False
c.close()
self.project.history_pop() self.project.history_pop()
self.emit("child-reordered", child, old_position, position)
self.project._object_child_reordered(self, child, old_position, position)
def clear_properties(self): def clear_properties(self):
c = self.project.db.cursor() c = self.project.db.cursor()
@ -576,171 +426,22 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
c.close() c.close()
for prop_id in properties: for prop_id in properties:
prop = self.__properties_dict[prop_id] prop = self.properties_dict[prop_id]
prop.notify("value") prop.notify("value")
self._property_changed(prop) self._property_changed(prop)
@GObject.Property(type=str) def get_display_name(self):
def display_name_type(self): return self.name if self.name is not None else self.type_id
return f"{self.type_id} {self.name}" if self.name else self.type_id
@GObject.Property(type=str)
def display_name(self):
name = self.name or ""
type_id = self.type_id
parent_id = self.parent_id
if type_id in [GMENU_SECTION_TYPE, GMENU_SUBMENU_TYPE, GMENU_ITEM_TYPE]:
prop = self.properties_dict["label"]
label = prop.value or ""
display_name = f"{type_id} <i>{label}</i>"
elif not parent_id and self.ui.template_id == self.object_id:
# Translators: This is used for Template classes in the object tree
display_name = _("{name} (template)").format(name=name)
else:
inline_prop = self.inline_property_id
internal = self.internal
if inline_prop:
display_name = f"{type_id} <b>{inline_prop}</b> <i>{name}</i>"
elif internal:
display_name = f"{type_id} <b>{internal}</b> <i>{name}</i>"
else:
display_name = f"{type_id} <i>{name}</i>"
if self.version_warning:
return f'<span underline="error">{display_name}</span>'
else:
return display_name
def __update_version_warning(self): def __update_version_warning(self):
target = self.ui.get_target(self.info.library_id) target = self.ui.get_target(self.info.library_id)
self.version_warning = utils.get_version_warning(target, self.info.version, self.info.deprecated_version, self.type_id) self.version_warning = utils.get_version_warning(target, self.info.version, self.info.deprecated_version, self.type_id)
def _on_ui_notify(self, obj, pspec):
property_id = pspec.name
if property_id == "template-id":
was_template = self.__is_template
self.__is_template = obj.template_id == self.object_id
if was_template or self.__is_template:
self.notify("display-name")
self.notify("display-name-type")
def _on_ui_library_changed(self, ui, library_id): def _on_ui_library_changed(self, ui, library_id):
self.__update_version_warning() self.__update_version_warning()
self.__populate_properties()
self.__populate_layout()
# Update properties directly, to avoid having to connect too many times to this signal # Update properties directly, to avoid having to connect too many times to this signal
for props in [self.__properties, self.__layout]: for props in [self.properties, self.layout]:
for prop in props: for prop in props:
if prop.library_id == library_id: if prop.library_id == library_id:
prop._update_version_warning() prop._update_version_warning()
# GListModel helpers
def _save_last_known_parent_and_position(self):
self._last_known = (self.parent, self.list_position)
def _update_new_parent(self):
parent = self.parent
position = self.list_position
# Emit GListModel signal to update model
if parent:
parent.items_changed(position, 0, 1)
parent.notify("n-items")
else:
ui = self.ui
ui.items_changed(position, 0, 1)
ui.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")
else:
ui = self.ui
ui.items_changed(position, 1, 0)
ui.notify("n-items")
self._last_known = None
@GObject.Property(type=int)
def list_position(self):
ui_id = self.ui_id
if self.parent_id:
retval = self.db_get(
"""
SELECT rownum-1
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY position ASC) rownum, object_id
FROM object
WHERE ui_id=? AND parent_id=?
)
WHERE object_id=?;
""",
(ui_id, self.parent_id, self.object_id)
)
else:
retval = self.db_get(
"""
SELECT rownum-1
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY position ASC) rownum, object_id
FROM object
WHERE ui_id=? AND parent_id IS NULL
)
WHERE object_id=?;
""",
(ui_id, self.object_id)
)
return retval
# GListModel iface
def do_get_item(self, position):
ui_id = self.ui_id
# This query should use index object_ui_id_parent_id_position_idx
retval = self.db_get(
"""
SELECT object_id
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY position ASC) rownum, object_id
FROM object
WHERE ui_id=? AND parent_id=?
)
WHERE rownum=?;
""",
(ui_id, self.object_id, position+1)
)
if retval is not None:
return self.project.get_object_by_id(ui_id, retval)
# This should not happen
return CmbListError()
def do_get_item_type(self):
return CmbBaseObject
@GObject.Property(type=int)
def n_items(self):
if self.project is None:
return 0
retval = self.db_get("SELECT COUNT(object_id) FROM object WHERE ui_id=? AND parent_id=?;", (self.ui_id, self.object_id))
return retval if retval is not None else 0
def do_get_n_items(self):
return self.n_items

View File

@ -20,14 +20,12 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject from gi.repository import GObject
from .cmb_objects_base import CmbBaseObjectData from .cmb_objects_base import CmbBaseObjectData
from .cmb_type_info import CmbTypeDataInfo from .cmb_type_info import CmbTypeDataInfo
from cambalache import getLogger, _ from cambalache import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@ -61,6 +59,9 @@ class CmbObjectData(CmbBaseObjectData):
if self.object is None: if self.object is None:
self.object = self.project.get_object_by_id(self.ui_id, self.object_id) self.object = self.project.get_object_by_id(self.ui_id, self.object_id)
if self.parent_id is not None and self.parent is None:
self.parent = self.object.data_dict.get(f"{self.owner_id}.{self.parent_id}", None)
self.__populate_children() self.__populate_children()
self.connect("notify", self._on_notify) self.connect("notify", self._on_notify)
@ -175,7 +176,6 @@ class CmbObjectData(CmbBaseObjectData):
(self.ui_id, self.object_id, self.owner_id, self.id), (self.ui_id, self.object_id, self.owner_id, self.id),
): ):
obj = CmbObjectData.from_row(self.project, *row) obj = CmbObjectData.from_row(self.project, *row)
obj.parent = self
self.__add_child(obj) self.__add_child(obj)
def add_data(self, data_key, value=None, comment=None): def add_data(self, data_key, value=None, comment=None):
@ -194,17 +194,11 @@ class CmbObjectData(CmbBaseObjectData):
def remove_data(self, data): def remove_data(self, data):
try: try:
assert data in self.children 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( self.project.db.execute(
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;", "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.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
) )
self.project.db.commit() self.project.db.commit()
self.project.history_pop()
except Exception as e: except Exception as e:
logger.warning(f"{self} Error removing data {data}: {e}") logger.warning(f"{self} Error removing data {data}: {e}")
return False return False

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
@ -31,12 +29,6 @@ from .control import cmb_create_editor
from cambalache import _ 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") @Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_object_data_editor.ui")
class CmbObjectDataEditor(Gtk.Box): class CmbObjectDataEditor(Gtk.Box):
__gtype_name__ = "CmbObjectDataEditor" __gtype_name__ = "CmbObjectDataEditor"
@ -75,7 +67,7 @@ class CmbObjectDataEditor(Gtk.Box):
def __on_remove_clicked(self, button): def __on_remove_clicked(self, button):
if self.info: if self.info:
self.object.remove_data(self.__data) self.object.remove_data(self.__data)
elif self.__data: else:
self.__data.parent.remove_data(self.__data) self.__data.parent.remove_data(self.__data)
@GObject.Property(type=GObject.Object) @GObject.Property(type=GObject.Object)
@ -123,11 +115,7 @@ class CmbObjectDataEditor(Gtk.Box):
editor = self.__arg_editors.get(key, None) editor = self.__arg_editors.get(key, None)
if editor: if editor:
val = self.data.get_arg(key) editor.cmb_value = self.data.get_arg(key)
# Only update if there is a change
if val != editor.cmb_value:
editor.cmb_value = val
def __on_data_data_added(self, parent, data): def __on_data_data_added(self, parent, data):
self.__add_data_editor(data) self.__add_data_editor(data)
@ -144,7 +132,8 @@ class CmbObjectDataEditor(Gtk.Box):
self.__update_view() self.__update_view()
def __on_data_removed(self, obj, data): def __on_data_removed(self, obj, data):
self.__remove_data_editor(data) if self.object and self.info:
self.__remove_data_editor(data)
def __ensure_object_data(self, history_message): def __ensure_object_data(self, history_message):
if self.data: if self.data:
@ -250,7 +239,7 @@ class CmbObjectDataEditor(Gtk.Box):
# Value # Value
if info.type_id: if info.type_id:
editor = cmb_create_editor(project, info.type_id, data=self.data, parent=self.object) editor = cmb_create_editor(project, info.type_id, data=self.data)
self.__value_editor = editor self.__value_editor = editor
if self.data: if self.data:
@ -270,7 +259,7 @@ class CmbObjectDataEditor(Gtk.Box):
for arg in info.args: for arg in info.args:
arg_info = info.args[arg] arg_info = info.args[arg]
editor = cmb_create_editor(project, arg_info.type_id, parent=self.object) editor = cmb_create_editor(project, arg_info.type_id)
self.__arg_editors[arg_info.key] = editor self.__arg_editors[arg_info.key] = editor
# Initialize value # Initialize value

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_object_data_editor.ui --> <!-- interface-name cmb_object_data_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<template class="CmbObjectDataEditor" parent="GtkBox"> <template class="CmbObjectDataEditor" parent="GtkBox">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
@ -70,6 +69,7 @@
<object class="GtkGrid" id="grid"> <object class="GtkGrid" id="grid">
<property name="column-spacing">4</property> <property name="column-spacing">4</property>
<property name="row-spacing">4</property> <property name="row-spacing">4</property>
<property name="vexpand">1</property>
</object> </object>
</child> </child>
</template> </template>

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
@ -43,17 +41,11 @@ class CmbObjectEditor(Gtk.Box):
self.__object = None self.__object = None
self.__id_label = None self.__id_label = None
self.__template_switch = None self.__template_switch = None
self.__bindings = []
super().__init__(**kwargs) super().__init__(**kwargs)
self.props.orientation = Gtk.Orientation.VERTICAL self.props.orientation = Gtk.Orientation.VERTICAL
def bind_property(self, *args):
binding = GObject.Object.bind_property(*args)
self.__bindings.append(binding)
return binding
def __create_id_editor(self): def __create_id_editor(self):
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4) grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4)
@ -62,7 +54,7 @@ class CmbObjectEditor(Gtk.Box):
# Id/Class entry # Id/Class entry
entry = CmbEntry() entry = CmbEntry()
self.bind_property( GObject.Object.bind_property(
self.__object, self.__object,
"name", "name",
entry, entry,
@ -74,7 +66,7 @@ class CmbObjectEditor(Gtk.Box):
grid.attach(entry, 1, 0, 1, 1) grid.attach(entry, 1, 0, 1, 1)
# Template check # Template check
if self.__object and self.__object.parent_id == 0: if self.__object and not self.__object.parent_id:
is_template = self.__object.object_id == self.__object.ui.template_id is_template = self.__object.object_id == self.__object.ui.template_id
tooltip_text = _("Switch between object and template") tooltip_text = _("Switch between object and template")
derivable = self.__object.info.derivable derivable = self.__object.info.derivable
@ -137,7 +129,7 @@ class CmbObjectEditor(Gtk.Box):
combo = CmbChildTypeComboBox(object=self.__object) combo = CmbChildTypeComboBox(object=self.__object)
self.bind_property( GObject.Object.bind_property(
self.__object, self.__object,
"type", "type",
combo, combo,
@ -215,16 +207,12 @@ It has to be exposed by your application with GtkBuilder expose_object method."
if prop is None or prop.info is None: if prop is None or prop.info is None:
continue continue
# Only show properties in the class they where originally defined
if prop.info.original_owner_id is not None and owner_id != prop.info.original_owner_id:
continue
editor = cmb_create_editor(prop.project, prop.info.type_id, prop=prop) editor = cmb_create_editor(prop.project, prop.info.type_id, prop=prop)
if editor is None: if editor is None:
continue continue
self.bind_property( GObject.Object.bind_property(
prop, prop,
"value", "value",
editor, editor,
@ -237,10 +225,7 @@ It has to be exposed by your application with GtkBuilder expose_object method."
else: else:
label = CmbPropertyLabel(prop=prop, bindable=not is_builtin) label = CmbPropertyLabel(prop=prop, bindable=not is_builtin)
if prop.info.disabled: # Keep a dict of labels
for w in [editor, label]:
w.set_tooltip_text(_("This property is disabled for {type_id}").format(type_id=obj.type_id))
w.props.sensitive = False
grid.attach(label, 0, i, 1, 1) grid.attach(label, 0, i, 1, 1)
grid.attach(editor, 1, i, 1, 1) grid.attach(editor, 1, i, 1, 1)
@ -260,7 +245,7 @@ It has to be exposed by your application with GtkBuilder expose_object method."
hexpand=True, hexpand=True,
object=obj, object=obj,
data=data, data=data,
info=info.data[data_key], info=None if data else info.data[data_key],
) )
grid.attach(editor, 0, i, 2, 1) grid.attach(editor, 0, i, 2, 1)
@ -301,16 +286,11 @@ It has to be exposed by your application with GtkBuilder expose_object method."
self.__object.disconnect_by_func(self.__on_object_notify) self.__object.disconnect_by_func(self.__on_object_notify)
self.__object.ui.disconnect_by_func(self.__on_object_ui_notify) self.__object.ui.disconnect_by_func(self.__on_object_ui_notify)
for binding in self.__bindings:
binding.unbind()
self.__bindings = []
self.__object = obj self.__object = obj
if obj: if obj:
obj.connect("notify", self.__on_object_notify) self.__object.connect("notify", self.__on_object_notify)
obj.ui.connect("notify", self.__on_object_ui_notify) self.__object.ui.connect("notify", self.__on_object_ui_notify)
self.__update_view() self.__update_view()

View File

@ -3,7 +3,7 @@
# #
# Cambalache Base Object wrappers # Cambalache Base Object wrappers
# #
# Copyright (C) 2021-2024 Juan Pablo Ugarte # Copyright (C) 2021-2022 Juan Pablo Ugarte
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
@ -22,8 +22,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject from gi.repository import GObject
from .cmb_base import CmbBase from .cmb_base import CmbBase
@ -57,8 +55,8 @@ class CmbBaseLibraryInfo(CmbBase):
) )
class CmbBasePropertyInfo(CmbBase): class CmbPropertyInfo(CmbBase):
__gtype_name__ = "CmbBasePropertyInfo" __gtype_name__ = "CmbPropertyInfo"
owner_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) owner_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
property_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) property_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
@ -83,15 +81,13 @@ class CmbBasePropertyInfo(CmbBase):
disable_inline_object = GObject.Property( disable_inline_object = GObject.Property(
type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False
) )
deprecated = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) is_position = GObject.Property(
type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False
)
required = GObject.Property( required = GObject.Property(
type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False
) )
workspace_default = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) workspace_default = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
original_owner_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
disabled = GObject.Property(
type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -113,11 +109,9 @@ class CmbBasePropertyInfo(CmbBase):
deprecated_version, deprecated_version,
translatable, translatable,
disable_inline_object, disable_inline_object,
deprecated, is_position,
required, required,
workspace_default, workspace_default,
original_owner_id,
disabled,
): ):
return cls( return cls(
project=project, project=project,
@ -134,11 +128,9 @@ class CmbBasePropertyInfo(CmbBase):
deprecated_version=deprecated_version, deprecated_version=deprecated_version,
translatable=translatable, translatable=translatable,
disable_inline_object=disable_inline_object, disable_inline_object=disable_inline_object,
deprecated=deprecated, is_position=is_position,
required=required, required=required,
workspace_default=workspace_default, workspace_default=workspace_default,
original_owner_id=original_owner_id,
disabled=disabled,
) )
@ -285,30 +277,6 @@ class CmbTypeChildInfo(CmbBase):
) )
class CmbBaseTypeInternalChildInfo(CmbBase):
__gtype_name__ = "CmbBaseTypeInternalChildInfo"
type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
internal_child_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
internal_parent_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
internal_type = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
creation_property_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@classmethod
def from_row(cls, project, type_id, internal_child_id, internal_parent_id, internal_type, creation_property_id):
return cls(
project=project,
type_id=type_id,
internal_child_id=internal_child_id,
internal_parent_id=internal_parent_id,
internal_type=internal_type,
creation_property_id=creation_property_id,
)
class CmbBaseUI(CmbBase): class CmbBaseUI(CmbBase):
__gtype_name__ = "CmbBaseUI" __gtype_name__ = "CmbBaseUI"
@ -453,97 +421,6 @@ class CmbBaseCSS(CmbBase):
self.db_set("UPDATE css SET is_global=? WHERE (css_id) IS (?);", (self.css_id,), value) self.db_set("UPDATE css SET is_global=? WHERE (css_id) IS (?);", (self.css_id,), value)
class CmbBaseGResource(CmbBase):
__gtype_name__ = "CmbBaseGResource"
gresource_id = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
resource_type = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@classmethod
def from_row(
cls,
project,
gresource_id,
resource_type,
parent_id,
position,
gresources_filename,
gresource_prefix,
file_filename,
file_compressed,
file_preprocess,
file_alias,
):
return cls(project=project, gresource_id=gresource_id)
@GObject.Property(type=int)
def parent_id(self):
return self.db_get("SELECT parent_id FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@parent_id.setter
def _set_parent_id(self, value):
self.db_set("UPDATE gresource SET parent_id=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=int)
def position(self):
return self.db_get("SELECT position FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@position.setter
def _set_position(self, value):
self.db_set("UPDATE gresource SET position=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=str)
def gresources_filename(self):
return self.db_get("SELECT gresources_filename FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@gresources_filename.setter
def _set_gresources_filename(self, value):
self.db_set("UPDATE gresource SET gresources_filename=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=str)
def gresource_prefix(self):
return self.db_get("SELECT gresource_prefix FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@gresource_prefix.setter
def _set_gresource_prefix(self, value):
self.db_set("UPDATE gresource SET gresource_prefix=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=str)
def file_filename(self):
return self.db_get("SELECT file_filename FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@file_filename.setter
def _set_file_filename(self, value):
self.db_set("UPDATE gresource SET file_filename=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=bool, default=False)
def file_compressed(self):
return self.db_get("SELECT file_compressed FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@file_compressed.setter
def _set_file_compressed(self, value):
self.db_set("UPDATE gresource SET file_compressed=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=str)
def file_preprocess(self):
return self.db_get("SELECT file_preprocess FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@file_preprocess.setter
def _set_file_preprocess(self, value):
self.db_set("UPDATE gresource SET file_preprocess=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
@GObject.Property(type=str)
def file_alias(self):
return self.db_get("SELECT file_alias FROM gresource WHERE (gresource_id) IS (?);", (self.gresource_id,))
@file_alias.setter
def _set_file_alias(self, value):
self.db_set("UPDATE gresource SET file_alias=? WHERE (gresource_id) IS (?);", (self.gresource_id,), value)
class CmbBaseProperty(CmbBase): class CmbBaseProperty(CmbBase):
__gtype_name__ = "CmbBaseProperty" __gtype_name__ = "CmbBaseProperty"

View File

@ -1,118 +0,0 @@
#
# CmbPath
#
# Copyright (C) 2024 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
#
from gi.repository import GObject, Gio
from .cmb_base import CmbBase
from cambalache import _, getLogger
logger = getLogger(__name__)
class CmbPath(CmbBase, Gio.ListModel):
__gtype_name__ = "CmbPath"
path_parent = GObject.Property(type=CmbBase, flags=GObject.ParamFlags.READWRITE)
path = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# GListModel
self.__items = []
self.__path_items = {}
def __bool__(self):
return True
def __str__(self):
return f"CmbPath<{self.path}>"
@GObject.Property(type=str)
def display_name(self):
return self.path or _("{n} unsaved files").format(n=self.n_items)
def get_path_item(self, directory):
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)
if is_path:
self.__path_items[item.path] = item
i = 0
for list_item in self.__items:
if is_path:
if not isinstance(list_item, CmbPath):
break
if display_name < list_item.display_name:
break
elif not isinstance(list_item, CmbPath) and display_name < list_item.display_name:
break
i += 1
item.path_parent = self
self.__items.insert(i, item)
self.items_changed(i, 0, 1)
if not self.path:
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)
if not self.path:
self.notify("display-name")
# GListModel iface
@GObject.Property(type=int)
def n_items(self):
return len(self.__items)
def do_get_item(self, position):
return self.__items[position] if position < len(self.__items) else None
def do_get_item_type(self):
return CmbBase
def do_get_n_items(self):
return self.n_items

View File

@ -1,142 +0,0 @@
#
# CmbPollNotificationView
#
# 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 datetime
from cambalache import _, getLogger
from gi.repository import GObject, Gtk
from . import utils
from .cmb_notification import CmbPollNotification
from .cmb_poll_option_check import CmbPollOptionCheck
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_poll_notification_view.ui")
class CmbPollNotificationView(Gtk.Box):
__gtype_name__ = "CmbPollNotificationView"
notification = GObject.Property(
type=CmbPollNotification, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
# Poll
title_label = Gtk.Template.Child()
description_label = Gtk.Template.Child()
option_box = Gtk.Template.Child()
total_label = Gtk.Template.Child()
end_date_label = Gtk.Template.Child()
refresh_button = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__option_checks = []
self.__updating = False
super().__init__(**kwargs)
notification = self.notification
poll = notification.poll
active = utils.utcnow() < poll.end_date
self.title_label.props.label = f"<b>{poll.title}</b>"
self.description_label.props.label = poll.description
close_msg = _("<small>• Closes on {date}</small>") if active else _("<small>• Closed on {date}</small>")
end_date = datetime.datetime.fromtimestamp(poll.end_date)
self.end_date_label.props.label = close_msg.format(date=end_date.strftime("%x"))
self.end_date_label.props.tooltip_text = end_date.strftime("%c")
first_check = None
n_option = 0
for option in poll.options:
button = CmbPollOptionCheck(option=option, sensitive=active)
if poll.allowed_votes == 1:
if first_check is None:
first_check = button
else:
button.set_group(first_check)
button.connect("toggled", self.__on_check_button_toggled, n_option)
self.__option_checks.append(button)
self.option_box.append(button)
n_option += 1
self.__update_results()
notification.connect("notify", self.__on_poll_notify)
def __on_check_button_toggled(self, button, n_option):
allowed_votes = self.notification.poll.allowed_votes
if self.__updating or (allowed_votes == 1 and not button.props.active):
return
votes = []
for i, check in enumerate(self.__option_checks):
if check.props.active:
votes.append(i)
if allowed_votes > 1:
not_done = len(votes) < allowed_votes
for i, check in enumerate(self.__option_checks):
if not_done:
check.set_sensitive(True)
elif not check.props.active:
check.set_sensitive(False)
self.notification.center.poll_vote(self.notification, votes)
def __on_poll_notify(self, notification, pspec):
if pspec.name in ["my-votes", "results"]:
self.__update_results()
def __update_results(self):
notification = self.notification
results = notification.results
my_votes = notification.my_votes
if not results or not my_votes:
self.total_label.props.label = ""
for check in self.__option_checks:
check.fraction = -1
return
self.__updating = True
votes = results.votes
total = results.total
for i, check in enumerate(self.__option_checks):
check.set_active(i in my_votes)
check.fraction = votes[i] / total if total else 0
self.total_label.props.label = _("<small>• {total} vote</small>").format(total=results.total)
self.__updating = False
@Gtk.Template.Callback("on_refresh_button_clicked")
def __on_refresh_button_clicked(self, button):
self.notification.center.poll_refresh_results(self.notification)

View File

@ -1,64 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_poll_notification_view.ui -->
<requires lib="gtk" version="4.0"/>
<template class="CmbPollNotificationView" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="halign">start</property>
<property name="use-markup">True</property>
</object>
</child>
<child>
<object class="GtkLabel" id="description_label">
<property name="halign">start</property>
<property name="use-markup">True</property>
</object>
</child>
<child>
<object class="GtkBox" id="option_box">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<property name="vexpand-set">True</property>
<child>
<object class="GtkButton" id="refresh_button">
<property name="child">
<object class="GtkLabel">
<property name="label">&lt;small&gt;Refresh&lt;/small&gt;</property>
<property name="use-markup">True</property>
</object>
</property>
<signal name="clicked" handler="on_refresh_button_clicked"/>
<style>
<class name="flat"/>
<class name="compact"/>
<class name="link"/>
<class name="text-button"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="total_label">
<property name="use-markup">True</property>
<style>
<class name="link"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="end_date_label">
<property name="use-markup">True</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -1,76 +0,0 @@
#
# CmbPollOptionCheck
#
# 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
#
from cambalache import getLogger
from gi.repository import GLib, GObject, Gtk
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_poll_option_check.ui")
class CmbPollOptionCheck(Gtk.CheckButton):
__gtype_name__ = "CmbPollOptionCheck"
label = Gtk.Template.Child()
bar = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__fraction = None
self.__tick_id = None
super().__init__(**kwargs)
@GObject.Property(type=str)
def option(self):
return self.label.props.label
@option.setter
def _set_option(self, option):
self.label.props.label = option
@GObject.Property(type=float)
def fraction(self):
return self.__fraction
@fraction.setter
def _set_fraction(self, fraction):
self.__fraction = fraction
if fraction < 0:
self.bar.props.visible = False
else:
self.bar.props.visible = True
if self.__tick_id is None:
self.__tick_id = self.add_tick_callback(self.__update_fraction)
def __update_fraction(self, widget, frame_clock):
if self.bar.props.fraction < self.__fraction:
self.bar.props.fraction = min(self.__fraction, self.bar.props.fraction + 0.08)
elif self.bar.props.fraction > self.__fraction:
self.bar.props.fraction = max(self.__fraction, self.bar.props.fraction - 0.08)
else:
self.__tick_id = None
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE

View File

@ -1,23 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name r.ui -->
<requires lib="gtk" version="4.0"/>
<template class="CmbPollOptionCheck" parent="GtkCheckButton">
<child>
<object class="GtkBox">
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel" id="label">
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkProgressBar" id="bar"/>
</child>
</object>
</child>
</template>
</interface>

File diff suppressed because it is too large Load Diff

View File

@ -20,17 +20,11 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject from gi.repository import GObject
from .cmb_objects_base import CmbBaseProperty from .cmb_objects_base import CmbBaseProperty, CmbPropertyInfo
from .cmb_property_info import CmbPropertyInfo
from . import utils from . import utils
from cambalache import _, getLogger
logger = getLogger(__name__)
class CmbProperty(CmbBaseProperty): class CmbProperty(CmbBaseProperty):
@ -66,96 +60,9 @@ class CmbProperty(CmbBaseProperty):
@value.setter @value.setter
def _set_value(self, value): def _set_value(self, value):
self.__update_values(value, self.translatable, self.translation_context, self.translation_comments, self.bind_property) self.__update_values(value, self.bind_property)
@GObject.Property(type=bool, default=False) def __update_values(self, value, bind_property):
def translatable(self):
return super().translatable
@translatable.setter
def _set_translatable(self, value):
self.__update_values(self.value, value, self.translation_context, self.translation_comments, self.bind_property)
@GObject.Property(type=str)
def translation_context(self):
return self.db_get(
"SELECT translation_context FROM object_property WHERE (ui_id, object_id, owner_id, property_id) IS (?, ?, ?, ?);",
(
self.ui_id,
self.object_id,
self.owner_id,
self.property_id,
),
)
@translation_context.setter
def _set_translation_context(self, value):
self.__update_values(self.value, self.translatable, value, self.translation_comments, self.bind_property)
@GObject.Property(type=str)
def translation_comments(self):
return self.db_get(
"SELECT translation_comments FROM object_property WHERE (ui_id, object_id, owner_id, property_id) IS (?, ?, ?, ?);",
(
self.ui_id,
self.object_id,
self.owner_id,
self.property_id,
),
)
@translation_comments.setter
def _set_translation_comments(self, value):
self.__update_values(self.value, self.translatable, self.translation_context, value, self.bind_property)
def reset(self):
if self.info.internal_child:
self.project.history_push(_("Unset {obj} {prop} {prop_type}").format(**self.__get_msgs()))
self.project.db.execute(
"DELETE FROM object_property WHERE ui_id=? AND object_id=? AND owner_id=? AND property_id=?;",
(self.ui_id, self.object_id, self.owner_id, self.property_id),
)
self.__update_internal_child()
if self.info.internal_child:
self.project.history_pop()
def __update_internal_child(self):
internal_info = self.info.internal_child
if internal_info and internal_info.internal_parent_id:
logger.warning("Adding an internal child within an internal child automatically is not implemented")
return
elif internal_info is None:
return
value = self.value
child_id = self.db_get(
"SELECT object_id FROM object WHERE ui_id=? AND parent_id=? AND internal=?",
(self.ui_id, self.object_id, internal_info.internal_child_id)
)
if value and not child_id:
self.project.add_object(
self.ui_id,
internal_info.internal_type,
parent_id=self.object_id,
internal=internal_info.internal_child_id
)
elif child_id:
internal_child = self.project.get_object_by_id(self.ui_id, child_id)
if internal_child:
self.project.remove_object(internal_child, allow_internal_removal=True)
def __get_msgs(self, value=None):
return {
"obj": self.object.display_name_type,
"prop": self.property_id,
"prop_type": _("property"),
"value": str(value)
}
def __update_values(self, value, translatable, translation_context, translation_comments, bind_property):
c = self.project.db.cursor() c = self.project.db.cursor()
bind_source_id, bind_owner_id, bind_property_id = (None, None, None) bind_source_id, bind_owner_id, bind_property_id = (None, None, None)
@ -165,22 +72,18 @@ class CmbProperty(CmbBaseProperty):
bind_property_id = bind_property.property_id bind_property_id = bind_property.property_id
if ( if (
(value is None or value == self.info.default_value or (self.info.is_object and value == 0)) and value is None or value == self.info.default_value or (self.info.is_object and value == 0)
bind_property is None and ) and bind_property is None:
not translatable and c.execute(
translation_context is None and "DELETE FROM object_property WHERE ui_id=? AND object_id=? AND owner_id=? AND property_id=?;",
translation_comments is None (self.ui_id, self.object_id, self.owner_id, self.property_id),
): )
self.reset()
else: else:
if ( if (
value == self.value and value is None
translatable == self.translatable and and bind_source_id == self.bind_source_id
translation_context == self.translation_context and and bind_owner_id == self.bind_owner_id
translation_comments == self.translation_comments and and bind_property_id == self.bind_property_id
bind_source_id == self.bind_source_id and
bind_owner_id == self.bind_owner_id and
bind_property_id == self.bind_property_id
): ):
return return
@ -191,23 +94,14 @@ class CmbProperty(CmbBaseProperty):
) )
if count: if count:
if self.info.internal_child:
self.project.history_push(_("Update {obj} {prop} {prop_type} to {value}").format(**self.__get_msgs(value)))
c.execute( c.execute(
""" """
UPDATE object_property UPDATE object_property
SET SET value=?, bind_source_id=?, bind_owner_id=?, bind_property_id=?
value=?,
translatable=?, translation_context=?, translation_comments=?,
bind_source_id=?, bind_owner_id=?, bind_property_id=?
WHERE ui_id=? AND object_id=? AND owner_id=? AND property_id=?; WHERE ui_id=? AND object_id=? AND owner_id=? AND property_id=?;
""", """,
( (
value, value,
translatable,
translation_context,
translation_comments,
bind_source_id, bind_source_id,
bind_owner_id, bind_owner_id,
bind_property_id, bind_property_id,
@ -218,19 +112,11 @@ class CmbProperty(CmbBaseProperty):
), ),
) )
else: else:
if self.info.internal_child:
self.project.history_push(_("Set {obj} {prop} {prop_type} to {value}").format(**self.__get_msgs(value)))
c.execute( c.execute(
""" """
INSERT INTO object_property INSERT INTO object_property
( (ui_id, object_id, owner_id, property_id, value, bind_source_id, bind_owner_id, bind_property_id)
ui_id, object_id, owner_id, property_id, VALUES (?, ?, ?, ?, ?, ?, ?, ?);
value,
translatable, translation_context, translation_comments,
bind_source_id, bind_owner_id, bind_property_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""", """,
( (
self.ui_id, self.ui_id,
@ -238,20 +124,12 @@ class CmbProperty(CmbBaseProperty):
self.owner_id, self.owner_id,
self.property_id, self.property_id,
value, value,
translatable,
translation_context,
translation_comments,
bind_source_id, bind_source_id,
bind_owner_id, bind_owner_id,
bind_property_id, bind_property_id,
), ),
) )
self.__update_internal_child()
if self.info.internal_child:
self.project.history_pop()
if self._init is False: if self._init is False:
self.object._property_changed(self) self.object._property_changed(self)
@ -278,22 +156,11 @@ class CmbProperty(CmbBaseProperty):
@bind_property.setter @bind_property.setter
def _set_bind_property(self, bind_property): def _set_bind_property(self, bind_property):
self.__update_values(self.value, self.translatable, self.translation_context, self.translation_comments, bind_property) self.__update_values(self.value, bind_property)
self.project._object_property_binding_changed(self.object, self) self.project._object_property_binding_changed(self.object, self)
def _update_version_warning(self): def _update_version_warning(self):
target = self.object.ui.get_target(self.library_id) target = self.object.ui.get_target(self.library_id)
warning = utils.get_version_warning( self.version_warning = utils.get_version_warning(
target, self.info.version, self.info.deprecated_version, self.property_id target, self.info.version, self.info.deprecated_version, self.property_id
) or "" )
if self.project.target_tk == "gtk-4.0" and self.info.type_id == "GFile":
target = self.object.ui.get_target("gtk")
if target is not None:
version = utils.parse_version(target)
if version is None or utils.version_cmp(version, (4, 16, 0)) < 0:
if len(warning):
warning += "\n"
warning += _("Warning: GFile uri needs to be absolute for Gtk < 4.16")
self.version_warning = warning if len(warning) else None

View File

@ -1,66 +0,0 @@
#
# Cambalache Property Type Info wrapper
#
# Copyright (C) 2024 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
#
from gi.repository import GObject
from .cmb_objects_base import CmbBasePropertyInfo
from cambalache import getLogger
logger = getLogger(__name__)
class CmbPropertyInfo(CmbBasePropertyInfo):
internal_child = GObject.Property(type=GObject.GObject, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.is_a11y = CmbPropertyInfo.type_is_accessible(self.owner_id)
self.a11y_property_id = CmbPropertyInfo.accessible_property_remove_prefix(self.owner_id, self.property_id)
@classmethod
def type_is_accessible(cls, owner_id):
return owner_id in [
"CmbAccessibleProperty",
"CmbAccessibleRelation",
"CmbAccessibleState",
"CmbAccessibleAction"
]
@classmethod
def accessible_property_remove_prefix(cls, owner_id, property_id):
prefix = {
"CmbAccessibleProperty": "cmb-a11y-property-",
"CmbAccessibleRelation": "cmb-a11y-relation-",
"CmbAccessibleState": "cmb-a11y-state-",
"CmbAccessibleAction": "cmb-a11y-action-"
}.get(owner_id, None)
if prefix is None:
return None
# A11y property name without prefix
return property_id.removeprefix(prefix)

View File

@ -20,17 +20,14 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from .cmb_object import CmbObject from .cmb_object import CmbObject
from .cmb_property import CmbProperty from .cmb_property import CmbProperty
from .cmb_layout_property import CmbLayoutProperty from .cmb_layout_property import CmbLayoutProperty
from .cmb_property_info import CmbPropertyInfo from .cmb_objects_base import CmbPropertyInfo
from .control import CmbObjectChooser, CmbFlagsEntry from .control import CmbObjectChooser, CmbFlagsEntry
from cambalache import _
class CmbPropertyLabel(Gtk.Button): class CmbPropertyLabel(Gtk.Button):
@ -49,17 +46,15 @@ class CmbPropertyLabel(Gtk.Button):
raise Exception("CmbPropertyLabel requires prop or layout_prop to be set") raise Exception("CmbPropertyLabel requires prop or layout_prop to be set")
return return
self.props.focus_on_click = False self.label = Gtk.Label(xalign=0, visible=True)
self.label = Gtk.Label(halign=Gtk.Align.START, valign=Gtk.Align.CENTER) box = Gtk.Box(visible=True)
box = Gtk.Box()
# Update label status # Update label status
if self.prop: if self.prop:
self.bind_icon = Gtk.Image(icon_size=Gtk.IconSize.NORMAL) self.bind_icon = Gtk.Image(icon_size=Gtk.IconSize.NORMAL, visible=True)
box.append(self.bind_icon) box.append(self.bind_icon)
# A11y properties are prefixed to avoid clashes, do not show prefix here self.label.props.label = self.prop.property_id
self.label.props.label = self.prop.info.a11y_property_id if self.prop.info.is_a11y else self.prop.property_id
self.__update_property_label() self.__update_property_label()
self.prop.connect("notify::value", lambda o, p: self.__update_property_label()) self.prop.connect("notify::value", lambda o, p: self.__update_property_label())
@ -128,23 +123,13 @@ class CmbPropertyLabel(Gtk.Button):
self.__update_property_label() self.__update_property_label()
popover.popdown() popover.popdown()
def __on_close_clicked(self, button, popover):
popover.popdown()
def __on_bind_button_clicked(self, button): def __on_bind_button_clicked(self, button):
popover = Gtk.Popover(position=Gtk.PositionType.LEFT, css_classes=["cmb-binding-popover"]) popover = Gtk.Popover(position=Gtk.PositionType.LEFT)
popover.set_parent(self) popover.set_parent(self)
label = Gtk.Label(label="<b>Property Binding</b>", use_markup=True, halign=Gtk.Align.START, hexpand=True) grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4, visible=True)
close = Gtk.Button(icon_name="window-close-symbolic", halign=Gtk.Align.END, css_classes=["close"])
close.connect("clicked", self.__on_close_clicked, popover)
box = Gtk.Box(hexpand=True) grid.attach(Gtk.Label(label="<b>Property Binding</b>", use_markup=True, visible=True, xalign=0), 0, 0, 2, 1)
box.append(label)
box.append(close)
grid = Gtk.Grid(hexpand=True, row_spacing=6, column_spacing=6)
grid.attach(box, 0, 0, 2, 1)
# Get bind property to initialize inputs # Get bind property to initialize inputs
bind_source, bind_property = self.__find_bind_source_property(self.prop.bind_source_id, self.prop.bind_property_id) bind_source, bind_property = self.__find_bind_source_property(self.prop.bind_source_id, self.prop.bind_property_id)
@ -174,16 +159,18 @@ class CmbPropertyLabel(Gtk.Button):
i = 1 i = 1
for prop_label, editor in [("source", object_editor), ("property", property_editor), ("flags", flags_editor)]: for prop_label, editor in [("source", object_editor), ("property", property_editor), ("flags", flags_editor)]:
label = Gtk.Label(label=prop_label, xalign=0) editor.props.visible = True
label = Gtk.Label(label=prop_label, xalign=0, visible=True)
grid.attach(label, 0, i, 1, 1) grid.attach(label, 0, i, 1, 1)
grid.attach(editor, 1, i, 1, 1) grid.attach(editor, 1, i, 1, 1)
i += 1 i += 1
clear = Gtk.Button(label=_("Clear"), halign=Gtk.Align.END) clear = Gtk.Button(label="Clear", visible=True, halign=Gtk.Align.END)
clear.connect("clicked", self.__on_clear_clicked, popover) clear.connect("clicked", self.__on_clear_clicked, popover)
grid.attach(clear, 0, i, 2, 1)
grid.attach(clear, 0, i, 2, 1)
object_editor.grab_focus() object_editor.grab_focus()
popover.set_child(grid) popover.set_child(grid)
@ -223,9 +210,6 @@ class CmbPropertyChooser(Gtk.ComboBoxText):
for prop in sorted(self.object.properties, key=lambda p: p.property_id): for prop in sorted(self.object.properties, key=lambda p: p.property_id):
info = prop.info info = prop.info
if info.is_a11y:
continue
# Ignore construct only properties # Ignore construct only properties
if info.construct_only: if info.construct_only:
continue continue

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk, Pango from gi.repository import GObject, Gtk, Pango
@ -78,7 +76,7 @@ class CmbSignalEditor(Gtk.Box):
@object.setter @object.setter
def _set_object(self, obj): def _set_object(self, obj):
if self._object: if self._object is not None:
self.treestore.clear() self.treestore.clear()
self._object.disconnect_by_func(self.__on_signal_added) self._object.disconnect_by_func(self.__on_signal_added)
self._object.disconnect_by_func(self.__on_signal_removed) self._object.disconnect_by_func(self.__on_signal_removed)
@ -86,7 +84,7 @@ class CmbSignalEditor(Gtk.Box):
self._object = obj self._object = obj
if obj: if obj is not None:
self.__populate_treestore() self.__populate_treestore()
self._object.connect("signal-added", self.__on_signal_added) self._object.connect("signal-added", self.__on_signal_added)
self._object.connect("signal-removed", self.__on_signal_removed) self._object.connect("signal-removed", self.__on_signal_removed)
@ -138,17 +136,13 @@ class CmbSignalEditor(Gtk.Box):
if data_obj: if data_obj:
signal.user_data = data_obj.object_id signal.user_data = data_obj.object_id
name = data_obj.name name = data_obj.name
signal.swap = True
else: else:
signal.user_data = 0 signal.user_data = 0
signal.swap = False
self.treestore[iter_][Col.SWAP.value] = signal.swap
name = "" name = ""
self.treestore[iter_][Col.USER_DATA.value] = name self.treestore[iter_][Col.USER_DATA.value] = name
else: else:
signal.user_data = 0 signal.user_data = 0
signal.swap = False
self.treestore[iter_][Col.USER_DATA.value] = "" self.treestore[iter_][Col.USER_DATA.value] = ""
@Gtk.Template.Callback("on_swap_toggled") @Gtk.Template.Callback("on_swap_toggled")
@ -246,7 +240,7 @@ class CmbSignalEditor(Gtk.Box):
signal_id = tree_model[iter_][Col.SIGNAL_ID.value] signal_id = tree_model[iter_][Col.SIGNAL_ID.value]
warning = tree_model[iter_][Col.WARNING_MESSAGE.value] warning = tree_model[iter_][Col.WARNING_MESSAGE.value]
if info and info.detailed: if info is not None and info.detailed:
detail = tree_model[iter_][Col.DETAIL.value] detail = tree_model[iter_][Col.DETAIL.value]
signal = tree_model[iter_][Col.SIGNAL.value] signal = tree_model[iter_][Col.SIGNAL.value]

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_signal_editor.ui --> <!-- interface-name cmb_signal_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<object class="GtkEntryCompletion" id="handler_entrycompletion"> <object class="GtkEntryCompletion" id="handler_entrycompletion">
<child> <child>
@ -37,6 +36,9 @@
<property name="focusable">1</property> <property name="focusable">1</property>
<property name="model">treestore</property> <property name="model">treestore</property>
<property name="tooltip-column">9</property> <property name="tooltip-column">9</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child> <child>
<object class="GtkTreeViewColumn" id="signal_id_column"> <object class="GtkTreeViewColumn" id="signal_id_column">
<property name="min-width">64</property> <property name="min-width">64</property>

View File

@ -1,266 +0,0 @@
#
# CmbTreeExpander
#
# Copyright (C) 2024 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
#
from gi.repository import GObject, Gdk, Gtk, Graphene
from .cmb_ui import CmbUI
from .cmb_object import CmbObject
from .cmb_css import CmbCSS
from .cmb_path import CmbPath
from cambalache import _
class CmbTreeExpander(Gtk.TreeExpander):
__gtype_name__ = "CmbTreeExpander"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.__project = None
self.__drop_before = None
self.label = Gtk.Inscription(hexpand=True)
self.set_child(self.label)
self.__parent = None
self.__drag_source = None
self.__drop_target = None
def update_bind(self):
item = self.props.item
self.__parent = self.props.parent
# Drag source
self.__drag_source = Gtk.DragSource()
self.__drag_source.connect("prepare", self.__on_drag_prepare)
self.__drag_source.connect("drag-begin", self.__on_drag_begin)
self.__parent.add_controller(self.__drag_source)
# Drop target
self.__drop_target = Gtk.DropTarget.new(type=GObject.TYPE_NONE, actions=Gdk.DragAction.COPY)
self.__drop_target.set_gtypes([CmbObject, CmbUI])
self.__drop_target.connect("accept", self.__on_drop_accept)
self.__drop_target.connect("motion", self.__on_drop_motion)
self.__drop_target.connect("drop", self.__on_drop_drop)
self.__parent.add_controller(self.__drop_target)
# Handle label
self.__update_label(item)
item.connect("notify::display-name", self.__on_item_display_name_notify)
self.__project = item.project
if isinstance(item, CmbCSS):
self.props.hide_expander = True
return
elif isinstance(item, CmbPath):
self.props.hide_expander = False
self.add_css_class("cmb-path" if item.path else "cmb-unsaved-path")
return
self.props.hide_expander = item.props.n_items == 0
item.connect("notify::n-items", self.__on_item_n_items_notify)
def clear_bind(self):
item = self.props.item
if self.__parent:
self.__parent.remove_controller(self.__drag_source)
self.__parent.remove_controller(self.__drop_target)
self.__parent = None
self.__drag_source = None
self.__drop_target = None
self.remove_css_class("cmb-path")
self.remove_css_class("cmb-unsaved-path")
item.disconnect_by_func(self.__on_item_display_name_notify)
if isinstance(item, CmbCSS) or isinstance(item, CmbPath):
return
item.disconnect_by_func(self.__on_item_n_items_notify)
def __on_item_n_items_notify(self, item, pspec):
self.props.hide_expander = item.props.n_items == 0
def __on_item_display_name_notify(self, item, pspec):
self.__update_label(item)
def __update_label(self, item):
self.label.set_markup(item.props.display_name or "")
# Drop target callbacks
def __get_drop_before(self, widget, x, y):
h = widget.get_height()
if y < h/4:
return True
if y > h * 0.8:
return False
return None
def __on_object_drop_accept(self, drop, item):
origin_item = drop.get_drag()._item
if origin_item == item:
return None
if not isinstance(item, CmbObject):
return None
# Ignore if its the same parent
if origin_item.parent_id == item.object_id:
return None
return origin_item
def __on_drop_accept(self, target, drop):
item = self.props.item
origin_item = drop.get_drag()._item
if isinstance(item, CmbObject):
origin_item = self.__on_object_drop_accept(drop, item)
self.__drop_before = None
if origin_item is None or item.parent is None:
return False
return self.__project._check_can_add(origin_item.type_id, item.parent.type_id)
elif isinstance(item, CmbUI):
if origin_item == item:
return False
# Ignore if its the same UI and item is already a toplevel
if origin_item.ui_id == item.ui_id and origin_item.parent_id is None:
return False
return True
def __on_drop_motion(self, target, x, y):
item = self.props.item
if isinstance(item, CmbObject):
row_widget = target.get_widget()
drop_before = self.__get_drop_before(row_widget, x, y)
if self.__drop_before == drop_before:
return Gdk.DragAction.COPY
row_widget.remove_css_class("drop-before")
row_widget.remove_css_class("drop-after")
self.__drop_before = drop_before
if drop_before is not None:
row_widget.add_css_class("drop-before" if drop_before else "drop-after")
return Gdk.DragAction.COPY
def __on_drop_drop(self, target, origin_item, x, y):
row_widget = target.get_widget()
item = self.props.item
if isinstance(item, CmbObject):
drop_before = self.__get_drop_before(row_widget, x, y)
if drop_before is None:
# TODO: handle dragging from one UI to another
if origin_item.ui_id != item.ui_id:
return
if origin_item.parent_id != item.object_id:
self.__project.history_push(
_("Move {name} to {target}").format(name=origin_item.display_name, target=item.display_name)
)
origin_item.parent_id = item.object_id
self.__project.history_pop()
return
if origin_item.parent_id != item.parent_id:
if drop_before:
msg = _("Move {name} before {target}").format(name=origin_item.display_name, target=item.display_name)
else:
msg = _("Move {name} after {target}").format(name=origin_item.display_name, target=item.display_name)
self.__project.history_push(msg)
origin_item.parent_id = item.parent_id
else:
msg = None
parent = item.parent
origin_position = origin_item.position
target_position = item.position
if origin_position > target_position:
if drop_before:
position = item.position
else:
position = item.position + 1
else:
if drop_before:
position = item.position - 1
else:
position = item.position
parent.reorder_child(origin_item, position)
if msg:
self.__project.history_pop()
self.__project.set_selection([origin_item])
elif isinstance(item, CmbUI):
if origin_item.ui_id == item.ui_id:
self.__project.history_push(_("Move {name} as toplevel").format(name=origin_item.display_name))
origin_item.parent_id = 0
self.__project.history_pop()
else:
# TODO: Use copy/paste to move across UI files
pass
# Drag Source callbacks
def __on_drag_prepare(self, drag_source, x, y):
item = self.props.item
# Only CmbObject start a drag
if not isinstance(item, CmbObject):
return None
self.__drag_point = Graphene.Point()
self.__drag_point.x = x
self.__drag_point.y = y
return Gdk.ContentProvider.new_for_value(self.props.item)
def __on_drag_begin(self, drag_source, drag):
drag._item = self.props.item
valid, p = self.__parent.compute_point(self.label, self.__drag_point)
if valid:
drag_source.set_icon(Gtk.WidgetPaintable.new(self.label), p.x, p.y)

163
cambalache/cmb_tree_view.py Normal file
View File

@ -0,0 +1,163 @@
#
# CmbTreeView - Cambalache Tree View
#
# Copyright (C) 2021-2024 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>
#
from gi.repository import Gtk, Pango
from .cmb_object import CmbObject
from .cmb_context_menu import CmbContextMenu
from cambalache import _
class CmbTreeView(Gtk.TreeView):
__gtype_name__ = "CmbTreeView"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.props.has_tooltip = True
self._project = None
self._selection = self.get_selection()
self.__in_selection_change = False
self._selection.connect("changed", self.__on_selection_changed)
self.set_headers_visible(False)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Object(Type)", renderer)
column.set_cell_data_func(renderer, self.__name_cell_data_func, None)
self.append_column(column)
self.connect("notify::model", self.__on_model_notify)
self.connect("row-activated", self.__on_row_activated)
gesture = Gtk.GestureClick(button=3)
gesture.connect("pressed", self.__on_button_press)
self.add_controller(gesture)
self.set_reorderable(True)
def __on_button_press(self, widget, npress, x, y):
retval = self.get_path_at_pos(x, y)
if retval is None:
return False
path, col, xx, yy = retval
self.get_selection().select_path(path)
menu = CmbContextMenu()
if self._project is not None:
menu.target_tk = self._project.target_tk
# Use parent instead of self to avoid warning and focus not working properly
# (run-dev.py:188589): Gtk-CRITICAL **: 16:45:12.790: gtk_css_node_insert_after: assertion 'previous_sibling == NULL ||
# previous_sibling->parent == parent' failed
menu.set_parent(self.props.parent)
menu.popup_at(x, y)
return True
def __name_cell_data_func(self, column, cell, model, iter_, data):
obj = model.get_value(iter_, 0)
msg = None
if type(obj) is CmbObject:
inline_prop = obj.inline_property_id
inline_prop = f"<b>{inline_prop}</b> " if inline_prop else ""
name = f"{obj.name} " if obj.name else ""
extra = _("(template)") if not obj.parent_id and obj.ui.template_id == obj.object_id else obj.type_id
msg = obj.version_warning
text = f"{inline_prop}{name}<i>{extra}</i>"
else:
text = f"<b>{obj.get_display_name()}</b>"
cell.set_property("markup", text)
cell.set_property("underline", Pango.Underline.ERROR if msg else Pango.Underline.NONE)
def __on_project_ui_library_changed(self, project, ui, library_id):
self.queue_draw()
def __on_model_notify(self, treeview, pspec):
if self._project is not None:
self._project.disconnect_by_func(self.__on_project_selection_changed)
self._project.disconnect_by_func(self.__on_project_ui_library_changed)
self._project = self.props.model
if self._project:
self._project.connect("selection-changed", self.__on_project_selection_changed)
self._project.connect("ui-library-changed", self.__on_project_ui_library_changed)
def __on_row_activated(self, view, path, column):
if self.row_expanded(path):
self.collapse_row(path)
else:
self.expand_row(path, True)
def __on_project_selection_changed(self, p):
project, _iter = self._selection.get_selected()
current = [project.get_value(_iter, 0)] if _iter is not None else []
selection = project.get_selection()
if selection == current:
return
self.__in_selection_change = True
if len(selection) > 0:
obj = selection[0]
_iter = project.get_iter_from_object(obj)
path = project.get_path(_iter)
self.expand_to_path(path)
self._selection.select_iter(_iter)
else:
self._selection.unselect_all()
self.__in_selection_change = False
def __on_selection_changed(self, selection):
if self.__in_selection_change:
return
project, _iter = selection.get_selected()
if _iter is not None:
obj = project.get_value(_iter, 0)
project.set_selection([obj])
def do_query_tooltip(self, x, y, keyboard_mode, tooltip):
retval, model, path, iter_ = self.get_tooltip_context(x, y, keyboard_mode)
if not retval:
return False
obj = model.get_value(iter_, 0)
if type(obj) is CmbObject:
msg = obj.version_warning
if msg:
tooltip.set_text(msg)
return True
return False

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_type_chooser.ui --> <!-- interface-name cmb_type_chooser.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<object class="CmbTypeChooserPopover" id="all"> <object class="CmbTypeChooserPopover" id="all">
<property name="show-categories">True</property> <property name="show-categories">True</property>

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk

View File

@ -20,10 +20,8 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GLib, GObject, Gio, Gtk from gi.repository import GObject, Gtk
from .cmb_project import CmbProject from .cmb_project import CmbProject
from .cmb_type_info import CmbTypeInfo from .cmb_type_info import CmbTypeInfo
@ -48,12 +46,13 @@ class CmbTypeChooserWidget(Gtk.Box):
derived_type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE) derived_type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
scrolledwindow = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child()
listview = Gtk.Template.Child() treeview = Gtk.Template.Child()
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__project = None self.__project = None
self._search_text = ""
self.__model = None self.__model = None
self._search_text = ""
self._filter = None
super().__init__(**kwargs) super().__init__(**kwargs)
@ -93,6 +92,10 @@ class CmbTypeChooserWidget(Gtk.Box):
return retval return retval
def __store_append_info(self, store, info):
if store:
store.append([info.type_id, info.type_id.lower(), info, True])
def __model_from_project(self, project): def __model_from_project(self, project):
if project is None: if project is None:
return None return None
@ -108,12 +111,7 @@ class CmbTypeChooserWidget(Gtk.Box):
order = {"toplevel": 0, "layout": 1, "control": 2, "display": 3, "model": 4} order = {"toplevel": 0, "layout": 1, "control": 2, "display": 3, "model": 4}
# type_id, type_id.lower(), CmbTypeInfo, sensitive # type_id, type_id.lower(), CmbTypeInfo, sensitive
store = Gio.ListStore() store = Gtk.ListStore(str, str, CmbTypeInfo, bool)
custom_filter = Gtk.CustomFilter()
custom_filter.set_filter_func(self.__custom_filter_func, None)
filter_model = Gtk.FilterListModel(model=store, filter=custom_filter)
infos = [] infos = []
for key in project.type_info: for key in project.type_info:
@ -135,15 +133,15 @@ class CmbTypeChooserWidget(Gtk.Box):
if show_categories and last_category != i.category: if show_categories and last_category != i.category:
last_category = i.category last_category = i.category
category = categories.get(i.category, _("Others")) category = categories.get(i.category, _("Others"))
store.append(CmbTypeInfo(type_id=f"<i><b>▾ {category}</b></i>")) store.append([f"<i>▾ {category}</i>", "", None, False])
store.append(i) self.__store_append_info(store, i)
# Special case External object type # Special case External object type
if show_categories or self.uncategorized_only: if show_categories or self.uncategorized_only:
store.append(project.type_info[constants.EXTERNAL_TYPE]) self.__store_append_info(store, project.type_info[constants.EXTERNAL_TYPE])
return filter_model return store
@GObject.Property(type=CmbProject) @GObject.Property(type=CmbProject)
def project(self): def project(self):
@ -151,18 +149,24 @@ class CmbTypeChooserWidget(Gtk.Box):
@project.setter @project.setter
def _set_project(self, project): def _set_project(self, project):
if self.__project: if self.__project is not None:
self.__project.disconnect_by_func(self.__on_type_info_added) self.__project.disconnect_by_func(self.__on_type_info_added)
self.__project.disconnect_by_func(self.__on_type_info_removed) self.__project.disconnect_by_func(self.__on_type_info_removed)
self.__project.disconnect_by_func(self.__on_type_info_changed)
self.__project = project self.__project = project
self.__model = self.__model_from_project(project) self.__model = self.__model_from_project(project)
self.listview.set_model(Gtk.NoSelection(model=self.__model)) self._filter = Gtk.TreeModelFilter(child_model=self.__model) if self.__model else None
if self._filter:
self._filter.set_visible_func(self.__visible_func)
if project: self.treeview.props.model = self._filter
if project is not None:
project.connect("type-info-added", self.__on_type_info_added) project.connect("type-info-added", self.__on_type_info_added)
project.connect("type-info-removed", self.__on_type_info_removed) project.connect("type-info-removed", self.__on_type_info_removed)
project.connect("type-info-changed", self.__on_type_info_changed)
@Gtk.Template.Callback("on_searchentry_activate") @Gtk.Template.Callback("on_searchentry_activate")
def __on_searchentry_activate(self, entry): def __on_searchentry_activate(self, entry):
@ -175,34 +179,61 @@ class CmbTypeChooserWidget(Gtk.Box):
@Gtk.Template.Callback("on_searchentry_search_changed") @Gtk.Template.Callback("on_searchentry_search_changed")
def __on_searchentry_search_changed(self, entry): def __on_searchentry_search_changed(self, entry):
self._search_text = entry.props.text.lower() self._search_text = entry.props.text.lower()
self.__model.props.filter.changed(Gtk.FilterChange.DIFFERENT) self._filter.refilter()
@Gtk.Template.Callback("on_listview_activate") @Gtk.Template.Callback("on_treeview_row_activated")
def __on_listview_activate(self, listview, position): def __on_treeview_row_activated(self, treeview, path, column):
info = self.__model.get_item(position) model = treeview.props.model
info = model[model.get_iter(path)][2]
if info is not None and info.project: if info is not None:
self.emit("type-selected", info) self.emit("type-selected", info)
def __custom_filter_func(self, info, data): def __visible_func(self, model, iter, data):
return info.type_id.lower().find(self._search_text) >= 0 type_id, type_id_lower, info, sensitive = model[iter]
# Always show categories if we are not searching
if self._search_text == "" and info is None:
return True
return type_id_lower.find(self._search_text) >= 0
def __on_type_info_added(self, project, info): def __on_type_info_added(self, project, info):
if self.__model is None: if self.__model is None:
return return
# Append new type info # Append new type info
# TODO: insert in order
if self.__type_info_should_append(info): if self.__type_info_should_append(info):
self.__model.props.model.insert_sorted(info, lambda a, b, d: GLib.strcmp0(a.type_id, b.type_id), None) self.__store_append_info(self.__model, info)
def __on_type_info_removed(self, project, info): def __on_type_info_removed(self, project, info):
if self.__model is None: if self.__model is None:
return return
# Find info and remove it from model # Find info and remove it from model
found, position = self.__model.props.model.find(info) for row in self.__model:
if found: if info == row[2]:
self.__model.props.model.remove(position) self.__model.remove(row.iter)
def __on_type_info_changed(self, project, info):
if self.__model is None:
return
info_row = None
# Find info and update it from model
for row in self.__model:
if info == row[2]:
info_row = row
break
if info_row is None:
return
# Update Type Name
info_row[0] = info.type_id
info_row[1] = info.type_id.lower()
Gtk.WidgetClass.set_css_name(CmbTypeChooserWidget, "CmbTypeChooserWidget") Gtk.WidgetClass.set_css_name(CmbTypeChooserWidget, "CmbTypeChooserWidget")

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_type_chooser_widget.ui --> <!-- interface-name cmb_type_chooser_widget.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<template class="CmbTypeChooserWidget" parent="GtkBox"> <template class="CmbTypeChooserWidget" parent="GtkBox">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
@ -15,37 +14,34 @@
</child> </child>
<child> <child>
<object class="GtkScrolledWindow" id="scrolledwindow"> <object class="GtkScrolledWindow" id="scrolledwindow">
<property name="hexpand">True</property> <property name="child">
<property name="propagate-natural-height">True</property> <object class="GtkTreeView" id="treeview">
<property name="propagate-natural-width">True</property> <property name="activate-on-single-click">1</property>
<property name="vexpand">True</property> <property name="enable-search">0</property>
<child> <property name="headers-visible">0</property>
<object class="GtkListView" id="listview"> <signal name="row-activated" handler="on_treeview_row_activated"/>
<property name="factory"> <child internal-child="selection">
<object class="GtkBuilderListItemFactory"> <object class="GtkTreeSelection" id="treeview-selection"/>
<property name="bytes"><![CDATA[<?xml version='1.0' encoding='UTF-8'?> </child>
<interface> <child>
<template class="GtkListItem" parent="GObject"> <object class="GtkTreeViewColumn" id="column_adaptor">
<property name="child"> <child>
<object class="GtkInscription"> <object class="GtkCellRendererText" id="adaptor_cell"/>
<binding name="markup"> <!-- Custom child fragments -->
<lookup name="type_id" type="CmbTypeInfo"> <attributes>
<lookup name="item">GtkListItem</lookup> <attribute name="markup">0</attribute>
</lookup> <attribute name="sensitive">3</attribute>
</binding> </attributes>
</object> </child>
</property>
</template>
</interface>]]></property>
</object> </object>
</property> </child>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="single-click-activate">True</property>
<property name="vexpand">True</property>
<signal name="activate" handler="on_listview_activate"/>
</object> </object>
</child> </property>
<property name="hscrollbar-policy">never</property>
<property name="min-content-height">256</property>
<property name="propagate-natural-height">1</property>
<property name="propagate-natural-width">1</property>
<property name="window-placement">bottom-left</property>
</object> </object>
</child> </child>
</template> </template>

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
@ -29,18 +27,13 @@ from .cmb_objects_base import (
CmbBaseTypeInfo, CmbBaseTypeInfo,
CmbBaseTypeDataInfo, CmbBaseTypeDataInfo,
CmbBaseTypeDataArgInfo, CmbBaseTypeDataArgInfo,
CmbBaseTypeInternalChildInfo,
CmbTypeChildInfo, CmbTypeChildInfo,
CmbPropertyInfo,
CmbSignalInfo, CmbSignalInfo,
) )
from .cmb_property_info import CmbPropertyInfo
from .constants import EXTERNAL_TYPE, GMENU_TYPE, GMENU_SECTION_TYPE, GMENU_SUBMENU_TYPE, GMENU_ITEM_TYPE from .constants import EXTERNAL_TYPE, GMENU_TYPE, GMENU_SECTION_TYPE, GMENU_SUBMENU_TYPE, GMENU_ITEM_TYPE
from cambalache import getLogger
logger = getLogger(__name__)
class CmbTypeDataArgInfo(CmbBaseTypeDataArgInfo): class CmbTypeDataArgInfo(CmbBaseTypeDataArgInfo):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -60,33 +53,19 @@ class CmbTypeDataInfo(CmbBaseTypeDataInfo):
return f"CmbTypeDataArgInfo<{self.owner_id}>::{self.key}" return f"CmbTypeDataArgInfo<{self.owner_id}>::{self.key}"
class CmbTypeInternalChildInfo(CmbBaseTypeInternalChildInfo):
def __init__(self, **kwargs):
self.children = {}
super().__init__(**kwargs)
def __str__(self):
return f"CmbTypeInternalChildInfo<{self.type_id}>::{self.internal_child_id}"
class CmbTypeInfo(CmbBaseTypeInfo): class CmbTypeInfo(CmbBaseTypeInfo):
__gtype_name__ = "CmbTypeInfo"
type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT) type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT)
parent_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT) parent_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT)
parent = GObject.Property(type=GObject.Object, flags=GObject.ParamFlags.READWRITE) parent = GObject.Property(type=GObject.Object, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
if self.project is None:
return
self.hierarchy = self.__init_hierarchy() self.hierarchy = self.__init_hierarchy()
self.interfaces = self.__init_interfaces() self.interfaces = self.__init_interfaces()
self.properties = self.__init_properties_signals(CmbPropertyInfo, "property") self.properties = self.__init_properties_signals(CmbPropertyInfo, "property")
self.signals = self.__init_properties_signals(CmbSignalInfo, "signal") self.signals = self.__init_properties_signals(CmbSignalInfo, "signal")
self.data = self.__init_data() self.data = self.__init_data()
self.internal_children = self.__init_internal_children()
self.child_constraint, self.child_type_shortcuts = self.__init_child_constraint() self.child_constraint, self.child_type_shortcuts = self.__init_child_constraint()
if self.parent_id == "enum": if self.parent_id == "enum":
@ -155,7 +134,7 @@ class CmbTypeInfo(CmbBaseTypeInfo):
c = self.project.db.cursor() c = self.project.db.cursor()
for row in c.execute(f"SELECT * FROM {table} WHERE owner_id=? ORDER BY {table}_id;", (self.type_id,)): for row in c.execute(f"SELECT * FROM {table} WHERE owner_id=? ORDER BY {table}_id;", (self.type_id,)):
retval[row[1]] = Klass.from_row(self.project, *row) retval[row[1]] = Klass.from_row(self, *row)
c.close() c.close()
return retval return retval
@ -164,14 +143,14 @@ class CmbTypeInfo(CmbBaseTypeInfo):
args = {} args = {}
children = {} children = {}
parent_id = parent_id if parent_id is not None else 0 parent_id = parent_id if parent_id is not None else 0
retval = CmbTypeDataInfo.from_row(self.project, owner_id, data_id, parent_id, key, type_id, translatable) retval = CmbTypeDataInfo.from_row(self, owner_id, data_id, parent_id, key, type_id, translatable)
c = self.project.db.cursor() c = self.project.db.cursor()
# Collect Arguments # Collect Arguments
for row in c.execute("SELECT * FROM type_data_arg WHERE owner_id=? AND data_id=?;", (owner_id, data_id)): for row in c.execute("SELECT * FROM type_data_arg WHERE owner_id=? AND data_id=?;", (owner_id, data_id)):
_key = row[2] _key = row[2]
args[_key] = CmbTypeDataArgInfo.from_row(self.project, *row) args[_key] = CmbTypeDataArgInfo.from_row(self, *row)
# Recurse children # Recurse children
for row in c.execute("SELECT * FROM type_data WHERE owner_id=? AND parent_id=?;", (owner_id, data_id)): for row in c.execute("SELECT * FROM type_data WHERE owner_id=? AND parent_id=?;", (owner_id, data_id)):
@ -198,52 +177,6 @@ class CmbTypeInfo(CmbBaseTypeInfo):
c.close() c.close()
return retval return retval
def __type_get_internal_child(self, type_id, internal_child_id, internal_parent_id, internal_type, creation_property_id):
retval = CmbTypeInternalChildInfo.from_row(
self.project,
type_id,
internal_child_id,
internal_parent_id,
internal_type,
creation_property_id
)
children = {}
c = self.project.db.cursor()
# Recurse children
for row in c.execute(
"SELECT * FROM type_internal_child WHERE type_id=? AND internal_parent_id=?;",
(type_id, internal_child_id)
):
key = row[1]
children[key] = self.__type_get_internal_child(*row)
c.close()
retval.children = children
# Internal child back reference in property
if creation_property_id:
if creation_property_id in self.properties:
self.properties[creation_property_id].internal_child = retval
return retval
def __init_internal_children(self):
retval = {}
c = self.project.db.cursor()
for row in c.execute(
"SELECT * FROM type_internal_child WHERE type_id=? AND internal_parent_id IS NULL ORDER BY internal_child_id;",
(self.type_id,)
):
key = row[1]
retval[key] = self.__type_get_internal_child(*row)
c.close()
return retval
def __init_child_constraint(self): def __init_child_constraint(self):
retval = {} retval = {}
shortcuts = [] shortcuts = []
@ -281,7 +214,7 @@ class CmbTypeInfo(CmbBaseTypeInfo):
retval = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_INT) retval = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_INT)
c = self.project.db.cursor() c = self.project.db.cursor()
for row in c.execute(f"SELECT name, nick, value FROM type_{name} WHERE type_id=? ORDER BY nick;", (self.type_id,)): for row in c.execute(f"SELECT name, nick, value FROM type_{name} WHERE type_id=?", (self.type_id,)):
retval.append(row) retval.append(row)
c.close() c.close()
@ -330,48 +263,3 @@ class CmbTypeInfo(CmbBaseTypeInfo):
parent = parent.parent parent = parent.parent
return False return False
def enum_get_value_as_string(self, value, use_nick=True):
if self.parent_id != "enum":
return None
for row in self.enum:
enum_name, enum_nick, enum_value = row
# Always use nick as value
if value == enum_name or value == enum_nick or value == str(enum_value):
return enum_nick if use_nick else enum_value
return None
def flags_get_value_as_string(self, value):
if self.parent_id != "flags":
return None
value_type = type(value)
tokens = None
if value_type == str:
if value.isnumeric():
value = int(value)
value_type = int
else:
tokens = [t.strip() for t in value.split("|")]
elif value_type != int:
logger.warning(f"Unhandled value type {value_type} {value}")
return None
flags = []
for row in self.flags:
flag_name, flag_nick, flag_value = row
if value_type == str:
# Always use nick as value
if flag_name in tokens or flag_nick in tokens:
flags.append(flag_nick)
else:
if flag_value & value:
flags.append(flag_nick)
return "|".join(flags)

View File

@ -20,39 +20,25 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os from gi.repository import GObject
from gi.repository import GObject, Gio
from .cmb_path import CmbPath from .cmb_objects_base import CmbBaseUI
from .cmb_list_error import CmbListError
from .cmb_objects_base import CmbBaseUI, CmbBaseObject
from cambalache import getLogger, _ from cambalache import getLogger, _
logger = getLogger(__name__) logger = getLogger(__name__)
class CmbUI(CmbBaseUI, Gio.ListModel): class CmbUI(CmbBaseUI):
__gsignals__ = { __gsignals__ = {
"library-changed": (GObject.SignalFlags.RUN_FIRST, None, (str,)), "library-changed": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
} }
path_parent = GObject.Property(type=CmbPath, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.connect("notify", self.__on_notify) self.connect("notify", self.__on_notify)
def __bool__(self):
# Override Truth Value Testing to ensure that CmbUI objects evaluates to True even if it does not have children objects
return True
def __str__(self):
return f"CmbUI<{self.display_name}>"
@GObject.Property(type=int) @GObject.Property(type=int)
def template_id(self): def template_id(self):
retval = self.db_get("SELECT template_id FROM ui WHERE (ui_id) IS (?);", (self.ui_id,)) retval = self.db_get("SELECT template_id FROM ui WHERE (ui_id) IS (?);", (self.ui_id,))
@ -65,10 +51,6 @@ class CmbUI(CmbBaseUI, Gio.ListModel):
def __on_notify(self, obj, pspec): def __on_notify(self, obj, pspec):
self.project._ui_changed(self, pspec.name) 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): def list_libraries(self):
retval = {} retval = {}
@ -132,21 +114,18 @@ class CmbUI(CmbBaseUI, Gio.ListModel):
c.close() c.close()
@classmethod def get_display_name(self):
def get_display_name(cls, ui_id, filename): if self.filename:
return os.path.basename(filename) if filename else _("Unnamed {ui_id}").format(ui_id=ui_id) return self.filename
@GObject.Property(type=str)
def display_name(self):
filename = self.filename
template_id = self.template_id template_id = self.template_id
if filename is None and template_id: if template_id:
template = self.project.get_object_by_id(self.ui_id, template_id) template = self.project.get_object_by_id(self.ui_id, template_id)
if template: if template is not None:
return template.name return template.name
return CmbUI.get_display_name(self.ui_id, filename) return _("Unnamed {ui_id}").format(ui_id=self.ui_id)
def __get_infered_target(self, library_id): def __get_infered_target(self, library_id):
ui_id = self.ui_id ui_id = self.ui_id
@ -186,37 +165,3 @@ class CmbUI(CmbBaseUI, Gio.ListModel):
return info.min_version return info.min_version
return target return target
# GListModel iface
def do_get_item(self, position):
ui_id = self.ui_id
# This query should use auto index from UNIQUE constraint
retval = self.db_get(
"""
SELECT object_id
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY position ASC) rownum, object_id
FROM object
WHERE ui_id=? AND parent_id IS NULL
)
WHERE rownum=?;
""",
(ui_id, position+1)
)
if retval is not None:
return self.project.get_object_by_id(ui_id, retval)
# This should not happen
return CmbListError()
def do_get_item_type(self):
return CmbBaseObject
@GObject.Property(type=int)
def n_items(self):
retval = self.db_get("SELECT COUNT(object_id) FROM object WHERE ui_id=? AND parent_id IS NULL;", (self.ui_id,))
return retval if retval is not None else 0
def do_get_n_items(self):
return self.n_items

View File

@ -20,14 +20,9 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from cambalache import _
from .cmb_ui import CmbUI from .cmb_ui import CmbUI
@ -36,7 +31,6 @@ class CmbUIEditor(Gtk.Grid):
__gtype_name__ = "CmbUIEditor" __gtype_name__ = "CmbUIEditor"
filename = Gtk.Template.Child() filename = Gtk.Template.Child()
format = Gtk.Template.Child()
template_id = Gtk.Template.Child() template_id = Gtk.Template.Child()
description = Gtk.Template.Child() description = Gtk.Template.Child()
copyright = Gtk.Template.Child() copyright = Gtk.Template.Child()
@ -58,6 +52,9 @@ class CmbUIEditor(Gtk.Grid):
@object.setter @object.setter
def _set_object(self, obj): def _set_object(self, obj):
if obj == self._object:
return
for binding in self._bindings: for binding in self._bindings:
binding.unbind() binding.unbind()
@ -78,17 +75,10 @@ class CmbUIEditor(Gtk.Grid):
self.set_sensitive(True) self.set_sensitive(True)
self.template_id.object = obj 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: for field in self.fields:
binding = obj.bind_property( binding = GObject.Object.bind_property(
obj,
field, field,
getattr(self, field), getattr(self, field),
"cmb-value", "cmb-value",
@ -96,42 +86,37 @@ class CmbUIEditor(Gtk.Grid):
) )
self._bindings.append(binding) self._bindings.append(binding)
if obj.project.target_tk == "gtk-4.0": @Gtk.Template.Callback("on_remove_button_clicked")
self.filename.mime_types = "application/x-gtk-builder;text/x-blueprint" def __on_remove_button_clicked(self, button):
self.emit("remove-ui")
# filename -> format @Gtk.Template.Callback("on_export_button_clicked")
binding = obj.bind_property( def __on_export_button_clicked(self, button):
"filename", self.emit("export-ui")
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() @GObject.Signal(
self.format.set_sensitive(bool(obj.filename)) flags=GObject.SignalFlags.RUN_LAST,
else: return_type=bool,
self.filename.mime_types = "application/x-gtk-builder;application/x-glade" arg_types=(),
self.format.hide() accumulator=GObject.signal_accumulator_true_handled,
)
def export_ui(self):
if self.object:
self.object.project.export_ui(self.object)
def __filename_to_format(self, binding, source_value, ui): return True
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 @GObject.Signal(
flags=GObject.SignalFlags.RUN_LAST,
return_type=bool,
arg_types=(),
accumulator=GObject.signal_accumulator_true_handled,
)
def remove_ui(self):
if self.object:
self.object.project.remove_ui(self.object)
def __format_to_filename(self, binding, target_value, ui): return True
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") Gtk.WidgetClass.set_css_name(CmbUIEditor, "CmbUIEditor")

View File

@ -1,8 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.97.1 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_ui_editor.ui --> <!-- interface-name cmb_ui_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<object class="CmbTextBuffer" id="authors"/> <object class="CmbTextBuffer" id="authors"/>
<object class="CmbTextBuffer" id="comment"/> <object class="CmbTextBuffer" id="comment"/>
@ -27,7 +26,7 @@
<property name="label" translatable="yes">Description:</property> <property name="label" translatable="yes">Description:</property>
<layout> <layout>
<property name="column">0</property> <property name="column">0</property>
<property name="row">3</property> <property name="row">2</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -37,7 +36,7 @@
<property name="label" translatable="yes">Copyright:</property> <property name="label" translatable="yes">Copyright:</property>
<layout> <layout>
<property name="column">0</property> <property name="column">0</property>
<property name="row">4</property> <property name="row">3</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -47,7 +46,7 @@
<property name="label" translatable="yes">Authors:</property> <property name="label" translatable="yes">Authors:</property>
<layout> <layout>
<property name="column">0</property> <property name="column">0</property>
<property name="row">5</property> <property name="row">4</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -57,13 +56,16 @@
<property name="label" translatable="yes">Domain:</property> <property name="label" translatable="yes">Domain:</property>
<layout> <layout>
<property name="column">0</property> <property name="column">0</property>
<property name="row">6</property> <property name="row">5</property>
</layout> </layout>
</object> </object>
</child> </child>
<child> <child>
<object class="CmbFileButton" id="filename"> <object class="CmbEntry" id="filename">
<property name="can-focus">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
<property name="placeholder-text" translatable="yes">&lt;file name relative to project&gt;</property>
<property name="visible">True</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">0</property> <property name="row">0</property>
@ -78,7 +80,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">6</property> <property name="row">5</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -95,7 +97,7 @@
<property name="min-content-height">96</property> <property name="min-content-height">96</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">3</property> <property name="row">2</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -112,7 +114,7 @@
<property name="min-content-height">96</property> <property name="min-content-height">96</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">5</property> <property name="row">4</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -124,7 +126,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">2</property> <property name="row">1</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -134,7 +136,45 @@
<property name="label" translatable="yes">Template:</property> <property name="label" translatable="yes">Template:</property>
<layout> <layout>
<property name="column">0</property> <property name="column">0</property>
<property name="row">2</property> <property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<property name="valign">end</property>
<property name="vexpand">1</property>
<child>
<object class="GtkButton" id="remove_button">
<property name="focusable">1</property>
<property name="receives-default">1</property>
<signal name="clicked" handler="on_remove_button_clicked"/>
<child>
<object class="GtkImage">
<property name="icon-name">app-remove-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="export_button">
<property name="focusable">1</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Export</property>
<property name="receives-default">1</property>
<property name="tooltip-text" translatable="yes">Export</property>
<signal name="clicked" handler="on_export_button_clicked"/>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
<layout>
<property name="column">0</property>
<property name="column-span">2</property>
<property name="row">7</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -151,7 +191,7 @@
<property name="min-content-height">96</property> <property name="min-content-height">96</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">4</property> <property name="row">3</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -161,7 +201,7 @@
<property name="label" translatable="yes">Comment:</property> <property name="label" translatable="yes">Comment:</property>
<layout> <layout>
<property name="column">0</property> <property name="column">0</property>
<property name="row">7</property> <property name="row">6</property>
</layout> </layout>
</object> </object>
</child> </child>
@ -178,34 +218,7 @@
<property name="min-content-height">96</property> <property name="min-content-height">96</property>
<layout> <layout>
<property name="column">1</property> <property name="column">1</property>
<property name="row">7</property> <property name="row">6</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> </layout>
</object> </object>
</child> </child>

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk

View File

@ -1,57 +0,0 @@
#
# CmbVersionNotificationView
#
# 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
#
from cambalache import _, getLogger
from gi.repository import GObject, Gtk
from .cmb_notification import CmbVersionNotification
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_version_notification_view.ui")
class CmbVersionNotificationView(Gtk.Box):
__gtype_name__ = "CmbVersionNotificationView"
notification = GObject.Property(
type=CmbVersionNotification, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
# Version
version_label = Gtk.Template.Child()
release_notes_label = Gtk.Template.Child()
read_more_button = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
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
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

@ -1,50 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_version_notification_view.ui -->
<requires lib="gtk" version="4.12"/>
<template class="CmbVersionNotificationView" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="version_label">
<property name="halign">start</property>
<property name="use-markup">True</property>
</object>
</child>
<child>
<object class="GtkLabel" id="release_notes_label">
<property name="halign">start</property>
<property name="lines">16</property>
<property name="max-width-chars">32</property>
<property name="use-markup">True</property>
<property name="valign">start</property>
<property name="vexpand">True</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<property name="valign">end</property>
<child>
<object class="GtkLinkButton" id="read_more_button">
<property name="label" translatable="yes">Read more...</property>
<property name="uri">https://blogs.gnome.org/xjuan/</property>
<style>
<class name="compact"/>
</style>
</object>
</child>
<child>
<object class="GtkLinkButton" id="download_button">
<property name="label" translatable="yes">Get it on Flathub</property>
<property name="uri">https://flathub.org/apps/ar.xjuan.Cambalache</property>
<style>
<class name="compact"/>
</style>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -20,157 +20,66 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import gi
import os import os
import json import json
import socket
import time import time
import fcntl
import stat
import atexit
import shutil
gi.require_version('Casilda', '0.1') from gi.repository import GObject, GLib, Gtk, WebKit
from gi.repository import GObject, GLib, Gio, Gdk, Gtk, Casilda
from . import config from . import config
from .cmb_ui import CmbUI from .cmb_ui import CmbUI
from .cmb_object import CmbObject from .cmb_object import CmbObject
from .cmb_context_menu import CmbContextMenu from .cmb_context_menu import CmbContextMenu
from cambalache.cmb_blueprint import cmb_blueprint_decompile
from . import utils from . import utils
from cambalache import getLogger, _, N_ from cambalache import getLogger, _
logger = getLogger(__name__) logger = getLogger(__name__)
basedir = os.path.dirname(__file__) or "." basedir = os.path.dirname(__file__) or "."
GObject.type_ensure(Casilda.Compositor.__gtype__) GObject.type_ensure(WebKit.Settings.__gtype__)
GObject.type_ensure(WebKit.WebView.__gtype__)
class CmbMerengueProcess(GObject.Object): class CmbProcess(GObject.Object):
__gsignals__ = { __gsignals__ = {
"handle-command": (GObject.SignalFlags.RUN_LAST, None, (str,)), "stdout": (GObject.SignalFlags.RUN_LAST, bool, (GLib.IOCondition,)),
"exit": (GObject.SignalFlags.RUN_LAST, None, ()), "exit": (GObject.SignalFlags.RUN_LAST, None, ()),
} }
gtk_version = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE) file = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
merengue_started = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__command_queue = []
self.__file = os.path.join(config.merenguedir, "merengue", "merengue")
self.__command_in = None
self.__on_command_in_source = None
self.__connection = None
self.__pid = 0
self.__wayland_display = None
self.__command_socket = None
self.__service = None
super().__init__(**kwargs) super().__init__(**kwargs)
@GObject.Property(type=str) self.pid = 0
def wayland_display(self): self.stdin = None
return self.__wayland_display self.stdout = None
@wayland_display.setter def stop(self):
def _set_wayland_display(self, wayland_display): if self.stdin:
self.cleanup() self.stdin.shutdown(False)
self.stdin = None
self.__wayland_display = wayland_display if self.stdout:
self.stdout.shutdown(False)
self.stdout = None
if wayland_display is None: if self.pid:
try:
GLib.spawn_close_pid(self.pid)
os.kill(self.pid, 9)
except Exception as e:
logger.warning(f"Error stopping {self.file} {e}")
self.pid = 0
def run(self, args, env={}):
if self.file is None or self.pid > 0:
return return
# Create socket address object
dirname = os.path.dirname(wayland_display)
self.__command_socket = os.path.join(dirname, "merengue.sock")
socket_addr = Gio.UnixSocketAddress.new(self.__command_socket)
# Lock Socket
GLib.mkdir_with_parents(dirname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
lockfd = os.open(f"{self.__command_socket}.lock",
os.O_CREAT | os.O_CLOEXEC | os.O_RDWR,
stat.S_IRUSR | stat.S_IWUSR)
if lockfd < 0:
logger.warning(f"Can not open lockfile for {self.__command_socket}, check permissions")
return
try:
fcntl.flock(lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except Exception as e:
logger.warning(f"Can not lock lockfile for {self.__command_socket}, is it used by another compositor? {e}")
return
# Create socket listener and add address
self.__service = Gio.SocketService()
self.__service.add_address(socket_addr,
Gio.SocketType.STREAM,
Gio.SocketProtocol.DEFAULT,
None)
self.__service.connect("incoming", self.__on_service_incoming)
self.__service.start()
try:
os.lstat(self.__command_socket)
except Exception as e:
logger.warning(f"Can not stat file {self.__command_socket} {e}")
socket_addr = None
@GObject.Property(type=int)
def pid(self):
return self.__pid
def cleanup(self):
self.stop()
if self.__command_socket:
os.unlink(self.__command_socket)
os.unlink(f"{self.__command_socket}.lock")
if self.__service:
self.__service.start()
self.__service = None
def __on_command_in(self, channel, condition):
if condition == GLib.IOCondition.HUP or self.__command_in is None:
self.stop()
return GLib.SOURCE_REMOVE
payload = self.__command_in.readline()
if payload is not None and payload != "":
self.emit("handle-command", payload)
return GLib.SOURCE_CONTINUE
def __on_service_incoming(self, service, connection, source_object):
self.__connection = connection
self.__command_in = GLib.IOChannel.unix_new(self.__connection.props.input_stream.get_fd())
id = GLib.io_add_watch(self.__command_in,
GLib.PRIORITY_DEFAULT_IDLE,
GLib.IOCondition.IN | GLib.IOCondition.HUP,
self.__on_command_in)
self.__on_command_in_source = id
# Consume pending command queue
for cmd, payload in self.__command_queue:
self.__socket_write_command(cmd, payload)
self.__command_queue = []
def start(self):
if self.__file is None or self.__pid > 0:
return
env = json.loads(os.environ.get("MERENGUE_DEV_ENV", "{}"))
env = env | {
"GDK_BACKEND": "wayland",
"WAYLAND_DISPLAY": self.wayland_display,
}
envp = [f"{var}={val}" for var, val in os.environ.items() if var not in env] envp = [f"{var}={val}" for var, val in os.environ.items() if var not in env]
# Append extra vars # Append extra vars
@ -178,83 +87,28 @@ class CmbMerengueProcess(GObject.Object):
envp.append(f"{var}={env[var]}") envp.append(f"{var}={env[var]}")
pid, stdin, stdout, stderr = GLib.spawn_async( pid, stdin, stdout, stderr = GLib.spawn_async(
[self.__file, self.gtk_version, self.__command_socket], [self.file] + args,
envp=envp, envp=envp,
flags=GLib.SpawnFlags.DO_NOT_REAP_CHILD, flags=GLib.SpawnFlags.DO_NOT_REAP_CHILD,
standard_input=True,
standard_output=True,
) )
self.pid = pid
self.stdin = GLib.IOChannel.unix_new(stdin)
self.stdout = GLib.IOChannel.unix_new(stdout)
GLib.io_add_watch(self.stdout, GLib.PRIORITY_DEFAULT_IDLE, GLib.IOCondition.IN | GLib.IOCondition.HUP, self.__on_stdout)
self.__pid = pid
GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, self.__on_exit, None) GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, self.__on_exit, None)
def __cleanup(self):
self.merengue_started = False
if self.__on_command_in_source:
GLib.source_remove(self.__on_command_in_source)
self.__on_command_in_source = None
if self.__command_in:
self.__command_in = None
if self.__connection:
self.__connection.close()
self.__connection = None
def stop(self):
self.__cleanup()
if self.__pid:
try:
GLib.spawn_close_pid(self.__pid)
os.kill(self.__pid, 9)
except Exception as e:
logger.warning(f"Error stopping {self.__file} {e}")
finally:
self.__pid = 0
def write_command(self, command, payload=None, args=None):
cmd = {"command": command}
if payload is not None:
# Encode to binary first, before calculating lenght
payload = payload.encode()
cmd["payload_length"] = len(payload)
logger.debug(f"write_command {command} {len(payload)}")
if args is not None:
cmd["args"] = args
# Queue command while we are not connected
if self.__connection is None:
self.__command_queue.append((cmd, payload))
return
self.__socket_write_command(cmd, payload)
def __socket_write_command(self, cmd, payload=None):
# Send command in one line as json
output_stream = self.__connection.props.output_stream
def write_data(data):
total_bytes = len(data)
total_sent = 0
while total_sent < total_bytes:
total_sent += output_stream.write(data[total_sent:])
write_data(json.dumps(cmd).encode())
write_data(b"\n")
if payload is not None:
write_data(payload)
# Flush
output_stream.flush()
def __on_exit(self, pid, status, data): def __on_exit(self, pid, status, data):
self.__cleanup() self.stop()
self.__pid = 0
self.emit("exit") self.emit("exit")
def __on_stdout(self, channel, condition):
return self.emit("stdout", condition)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_view.ui") @Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_view.ui")
class CmbView(Gtk.Box): class CmbView(Gtk.Box):
@ -265,106 +119,112 @@ class CmbView(Gtk.Box):
"placeholder-activated": (GObject.SignalFlags.RUN_LAST, None, (int, int, object, int, str)), "placeholder-activated": (GObject.SignalFlags.RUN_LAST, None, (int, int, object, int, str)),
} }
show_merengue = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
preview = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE) preview = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
stack = Gtk.Template.Child() stack = Gtk.Template.Child()
compositor = Gtk.Template.Child() webview = Gtk.Template.Child()
compositor_offload = Gtk.Template.Child()
compositor_box = Gtk.Template.Child()
error_box = Gtk.Template.Child()
error_message = Gtk.Template.Child()
text_view = Gtk.Template.Child() text_view = Gtk.Template.Child()
db_inspector = Gtk.Template.Child()
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__project = None self.__project = None
self.__ui = None self.__restart_project = None
self.__ui_id = 0
self.__theme = None self.__theme = None
self.__dark = False
self.menu = self.__create_context_menu() self.menu = self.__create_context_menu()
super().__init__(**kwargs) super().__init__(**kwargs)
self.__click_gesture = Gtk.GestureClick( self.__merengue_bin = os.path.join(config.merenguedir, "merengue", "merengue")
propagation_phase=Gtk.PropagationPhase.CAPTURE, self.__broadwayd_bin = GLib.find_program_in_path("broadwayd")
button=3 self.__gtk4_broadwayd_bin = GLib.find_program_in_path("gtk4-broadwayd")
)
self.__click_gesture.connect("pressed", self.__on_click_gesture_pressed)
self.compositor_box.add_controller(self.__click_gesture)
self.__merengue = CmbMerengueProcess(wayland_display=self.compositor.props.socket) self.webview.connect("load-changed", self.__on_load_changed)
self.__merengue.connect("exit", self.__on_process_exit)
self.__merengue = None
self.__broadwayd = None
self.__port = None
self.__merengue_last_exit = None self.__merengue_last_exit = None
if self.__broadwayd_bin is None:
logger.warning("broadwayd not found, Gtk 3 workspace wont work.")
if self.__gtk4_broadwayd_bin is None:
logger.warning("gtk4-broadwayd not found, Gtk 4 workspace wont work.")
self.connect("notify::preview", self.__on_preview_notify) self.connect("notify::preview", self.__on_preview_notify)
# Ensure we delete all socket files when exiting def do_destroy(self):
atexit.register(self.__atexit) if self.__merengue:
self.__merengue_command("quit")
@Gtk.Template.Callback("on_restart_button_clicked") if self.__broadwayd:
def __on_restart_button_clicked(self, button): self.__broadwayd.stop()
self.restart_workspace()
def __atexit(self): def __evaluate_js(self, script):
dirname = os.path.dirname(self.compositor.props.socket) self.webview.evaluate_javascript(script, -1, None, None, None, None, None, None)
self.__merengue_command("quit")
self.__merengue.cleanup()
if os.path.exists(dirname):
shutil.rmtree(dirname)
def __set_dark_mode(self, dark):
valid, bg_color = self.get_style_context().lookup_color('theme_bg_color')
if valid:
self.compositor.props.bg_color = bg_color
return GLib.SOURCE_REMOVE
def _set_dark_mode(self, dark): def _set_dark_mode(self, dark):
# This needs to be called in an idle because theme_bg_color has not changed at this point self.__dark = dark
GLib.idle_add(self.__set_dark_mode, dark) self.__evaluate_js(f"document.body.style.background = '{'#222' if dark else 'inherit'}';")
def __on_load_changed(self, webview, event):
if event != WebKit.LoadEvent.FINISHED:
return
self._set_dark_mode(self.__dark)
# Disable alert() function used when broadwayd get disconnected
# Monkey pat ch setupDocument() to avoid disabling document.oncontextmenu
self.__evaluate_js(
"""
window.alert = function (message) {
console.log (message);
}
window.merengueSetupDocument = setupDocument;
window.setupDocument = function (document) {
var cb = oncontextmenu
merengueSetupDocument(document);
document.oncontextmenu = cb;
}
"""
)
def __merengue_command(self, command, payload=None, args=None): def __merengue_command(self, command, payload=None, args=None):
if self.__merengue.merengue_started: if self.__merengue is None or self.__merengue.stdin is None:
self.__merengue.write_command(command, payload, args) return
cmd = {"command": command, "payload": payload is not None}
if args is not None:
cmd["args"] = args
# Send command in one line as json
self.__merengue.stdin.write(json.dumps(cmd))
self.__merengue.stdin.write("\n")
if payload is not None:
self.__merengue.stdin.write(GLib.strescape(payload))
self.__merengue.stdin.write("\n")
# Flush
self.__merengue.stdin.flush()
def __get_ui_xml(self, ui_id, merengue=False): def __get_ui_xml(self, ui_id, merengue=False):
if self.show_merengue:
merengue = True
return self.__project.db.tostring(ui_id, merengue=merengue) return self.__project.db.tostring(ui_id, merengue=merengue)
def __update_view(self): def __update_view(self):
if self.__project and self.__ui: if self.__project is not None and self.__ui_id > 0:
if self.stack.props.visible_child_name == "ui_xml": if self.stack.props.visible_child_name == "ui_xml":
ui_source = self.__get_ui_xml(self.__ui.ui_id) ui = self.__get_ui_xml(self.__ui_id)
self.text_view.buffer.set_text(ui)
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 return
self.text_view.buffer.set_text("") self.text_view.buffer.set_text("")
self.__ui = None self.__ui_id = 0
def __get_ui_dirname(self, ui_id): def __get_ui_dirname(self, ui_id):
dirname = GLib.get_home_dir() dirname = GLib.get_home_dir()
@ -383,7 +243,7 @@ class CmbView(Gtk.Box):
return dirname return dirname
def __merengue_update_ui(self, ui_id): def __merengue_update_ui(self, ui_id):
ui = self.__get_ui_xml(ui_id, merengue=True) if ui_id else None ui = self.__get_ui_xml(ui_id, merengue=True)
toplevels = self.__project.db.get_toplevels(ui_id) toplevels = self.__project.db.get_toplevels(ui_id)
selection = self.__project.get_selection() selection = self.__project.get_selection()
objects = self.__get_selection_objects(selection, ui_id) objects = self.__get_selection_objects(selection, ui_id)
@ -417,13 +277,7 @@ class CmbView(Gtk.Box):
self.__merengue_update_ui(obj.ui_id) self.__merengue_update_ui(obj.ui_id)
def __on_object_property_changed(self, project, obj, prop): def __on_object_property_changed(self, project, obj, prop):
info = prop.info if obj.info.workspace_type is None and prop.info.construct_only:
# FIXME: implement new merengue command for updating a11y props
if info.is_a11y:
return
if obj.info.workspace_type is None and info.construct_only:
self.__merengue_update_ui(obj.ui_id) self.__merengue_update_ui(obj.ui_id)
return return
@ -468,27 +322,21 @@ class CmbView(Gtk.Box):
if len(selection) > 0: if len(selection) > 0:
obj = selection[0] obj = selection[0]
if isinstance(obj, CmbUI): if type(obj) not in [CmbUI, CmbObject]:
ui = obj
elif isinstance(obj, CmbObject):
ui = obj.ui
else:
return return
ui_id = obj.ui_id ui_id = obj.ui_id
if self.__ui != ui: if self.__ui_id != ui_id:
self.__ui = ui self.__ui_id = ui_id
self.__merengue_update_ui(ui.ui_id) self.__merengue_update_ui(ui_id)
objects = self.__get_selection_objects(selection, ui.ui_id) objects = self.__get_selection_objects(selection, ui_id)
self.__merengue_command("selection_changed", args={"ui_id": ui_id, "selection": objects}) self.__merengue_command("selection_changed", args={"ui_id": ui_id, "selection": objects})
else: else:
self.__ui = None self.__ui_id = 0
self.__merengue_update_ui(0) self.__merengue_update_ui(0)
self.__update_view()
def __on_css_added(self, project, obj): def __on_css_added(self, project, obj):
if self.project.filename and obj.filename: if self.project.filename and obj.filename:
dirname = os.path.dirname(self.project.filename) dirname = os.path.dirname(self.project.filename)
@ -531,26 +379,13 @@ class CmbView(Gtk.Box):
def __on_object_data_arg_changed(self, project, data, value): def __on_object_data_arg_changed(self, project, data, value):
self.__merengue_update_ui(data.ui_id) self.__merengue_update_ui(data.ui_id)
def __on_object_child_reordered(self, project, obj, child, old_position, new_position):
self.__merengue_update_ui(obj.ui_id)
def __set_error_message(self, message):
if message:
self.error_message.props.label = message
self.compositor_offload.set_visible(False)
self.error_box.set_visible(True)
else:
self.error_message.props.label = ""
self.compositor_offload.set_visible(True)
self.error_box.set_visible(False)
@GObject.Property(type=GObject.GObject) @GObject.Property(type=GObject.GObject)
def project(self): def project(self):
return self.__project return self.__project
@project.setter @project.setter
def _set_project(self, project): def _set_project(self, project):
if self.__project: if self.__project is not None:
self.__project.disconnect_by_func(self.__on_changed) self.__project.disconnect_by_func(self.__on_changed)
self.__project.disconnect_by_func(self.__on_ui_changed) self.__project.disconnect_by_func(self.__on_ui_changed)
self.__project.disconnect_by_func(self.__on_object_added) self.__project.disconnect_by_func(self.__on_object_added)
@ -563,20 +398,19 @@ class CmbView(Gtk.Box):
self.__project.disconnect_by_func(self.__on_object_data_removed) self.__project.disconnect_by_func(self.__on_object_data_removed)
self.__project.disconnect_by_func(self.__on_object_data_data_removed) self.__project.disconnect_by_func(self.__on_object_data_data_removed)
self.__project.disconnect_by_func(self.__on_object_data_arg_changed) self.__project.disconnect_by_func(self.__on_object_data_arg_changed)
self.__project.disconnect_by_func(self.__on_object_child_reordered)
self.__project.disconnect_by_func(self.__on_project_selection_changed) self.__project.disconnect_by_func(self.__on_project_selection_changed)
self.__merengue.disconnect_by_func(self.__on_merengue_stdout)
self.__project.disconnect_by_func(self.__on_css_added) self.__project.disconnect_by_func(self.__on_css_added)
self.__project.disconnect_by_func(self.__on_css_removed) self.__project.disconnect_by_func(self.__on_css_removed)
self.__project.disconnect_by_func(self.__on_css_changed) self.__project.disconnect_by_func(self.__on_css_changed)
self.__merengue.disconnect_by_func(self.__on_merengue_handle_command)
self.__merengue.stop() self.__merengue.stop()
self.__broadwayd.stop()
self.__project = project self.__project = project
self.db_inspector.project = project
self.__update_view() self.__update_view()
if project: if project is not None:
project.connect("changed", self.__on_changed) project.connect("changed", self.__on_changed)
project.connect("ui-changed", self.__on_ui_changed) project.connect("ui-changed", self.__on_ui_changed)
project.connect("object-added", self.__on_object_added) project.connect("object-added", self.__on_object_added)
@ -589,25 +423,28 @@ class CmbView(Gtk.Box):
project.connect("object-data-removed", self.__on_object_data_removed) project.connect("object-data-removed", self.__on_object_data_removed)
project.connect("object-data-data-removed", self.__on_object_data_data_removed) project.connect("object-data-data-removed", self.__on_object_data_data_removed)
project.connect("object-data-arg-changed", self.__on_object_data_arg_changed) project.connect("object-data-arg-changed", self.__on_object_data_arg_changed)
project.connect("object-child-reordered", self.__on_object_child_reordered)
project.connect("selection-changed", self.__on_project_selection_changed) project.connect("selection-changed", self.__on_project_selection_changed)
project.connect("css-added", self.__on_css_added) project.connect("css-added", self.__on_css_added)
project.connect("css-removed", self.__on_css_removed) project.connect("css-removed", self.__on_css_removed)
project.connect("css-changed", self.__on_css_changed) project.connect("css-changed", self.__on_css_changed)
self.__merengue.connect("handle-command", self.__on_merengue_handle_command)
# Run view process self.__merengue = CmbProcess(file=self.__merengue_bin)
if project.target_tk == "gtk+-3.0": self.__merengue.connect("stdout", self.__on_merengue_stdout)
self.__merengue.gtk_version = "3.0" self.__merengue.connect("exit", self.__on_process_exit)
elif project.target_tk == "gtk-4.0":
self.__merengue.gtk_version = "4.0"
# Clear any error self.__broadwayd_check(self.__project.target_tk)
self.__set_error_message(None)
self.__merengue.start() broadwayd = self.__gtk4_broadwayd_bin if self.__project.target_tk == "gtk-4.0" else self.__broadwayd_bin
self.__broadwayd = CmbProcess(file=broadwayd)
self.__broadwayd.connect("stdout", self.__on_broadwayd_stdout)
self.__broadwayd.connect("exit", self.__on_process_exit)
self.__port = self.__find_free_port()
display = self.__port - 8080
self.__broadwayd.run([f":{display}"])
# Update css themes # Update css themes
self.menu.target_tk = project.target_tk self.menu.target_tk = self.__project.target_tk
@GObject.Property(type=str) @GObject.Property(type=str)
def gtk_theme(self): def gtk_theme(self):
@ -618,24 +455,40 @@ class CmbView(Gtk.Box):
self.__theme = theme self.__theme = theme
self.__merengue_command("gtk_settings_set", args={"property": "gtk-theme-name", "value": theme}) self.__merengue_command("gtk_settings_set", args={"property": "gtk-theme-name", "value": theme})
def __on_click_gesture_pressed(self, gesture, n_press, x, y): @Gtk.Template.Callback("on_context_menu")
if gesture.get_current_button() == 3: def __on_context_menu(self, webview, menu, hit_test_result):
self.menu.popup_at(x, y) self.menu.popup_at(*utils.get_pointer(self))
return True
def __webview_set_msg(self, msg):
self.webview.load_html(
f"""
<html>
<body>
<h3 style="white-space: pre; text-align: center; margin-top: 45vh; opacity: 50%">{msg}</h3>
</body>
</html>
"""
)
def __broadwayd_check(self, target_tk):
bin = None
if target_tk == "gtk-4.0" and self.__gtk4_broadwayd_bin is None:
bin = "gtk4-broadwayd"
if target_tk == "gtk+-3.0" and self.__broadwayd_bin is None:
bin = "broadwayd"
if bin is not None:
self.__webview_set_msg(_("Workspace not available\n{bin} executable not found").format(bin=bin))
def inspect(self): def inspect(self):
self.stack.props.visible_child_name = "ui_xml" self.stack.props.visible_child_name = "ui_xml"
self.__update_view() self.__update_view()
def restart_workspace(self): def restart_workspace(self):
# Clear last exit timestamp self.__restart_project = self.__project
self.__merengue_last_exit = None self.project = None
if self.__merengue.pid:
# Let __on_process_exit() restart Merengue
self.__merengue.stop()
else:
self.__set_error_message(None)
self.__merengue.start()
def __create_context_menu(self): def __create_context_menu(self):
retval = CmbContextMenu(enable_theme=True) retval = CmbContextMenu(enable_theme=True)
@ -647,17 +500,22 @@ class CmbView(Gtk.Box):
return retval return retval
def __on_process_exit(self, process): def __on_process_exit(self, process):
if self.__merengue_last_exit is None: if process == self.__merengue:
self.__merengue_last_exit = time.monotonic() if self.__merengue_last_exit is None:
else: self.__merengue_last_exit = time.monotonic()
# Stop auto restart if Merengue exited less than 2 seconds ago else:
if (time.monotonic() - self.__merengue_last_exit) < 2: if (time.monotonic() - self.__merengue_last_exit) < 1:
self.__set_error_message(_("Workspace process error\nStopping auto restart")) self.__webview_set_msg(_("Workspace process error\nStopping auto restart"))
self.__merengue_last_exit = None self.__merengue_last_exit = None
return return
self.__ui = None if self.__broadwayd.pid == 0 and self.__merengue.pid == 0:
self.__merengue.start() self.project = self.__restart_project
self.__restart_project = None
self.__ui_id = 0
else:
self.__restart_project = self.__project
self.project = None
def __command_selection_changed(self, selection): def __command_selection_changed(self, selection):
objects = [] objects = []
@ -672,11 +530,8 @@ class CmbView(Gtk.Box):
if self.project is None: if self.project is None:
return return
for id, info in self.project.library_info.items(): for id in self.project.library_info:
# Only load 3rd party libraries, Gtk ones are already loaded info = self.project.library_info[id]
if not info.third_party:
continue
self.__merengue_command( self.__merengue_command(
"load_namespace", "load_namespace",
args={ args={
@ -695,50 +550,109 @@ class CmbView(Gtk.Box):
for css in providers: for css in providers:
self.__on_css_added(self.project, css) self.__on_css_added(self.project, css)
def __on_merengue_handle_command(self, merengue, payload): def __on_merengue_stdout(self, process, condition):
if condition == GLib.IOCondition.HUP:
self.__merengue.stop()
return GLib.SOURCE_REMOVE
if self.__merengue.stdout is None:
return GLib.SOURCE_REMOVE
retval = self.__merengue.stdout.readline()
cmd = None
try: try:
cmd = json.loads(payload) cmd = json.loads(retval)
command = cmd.get("command", None) command = cmd.get("command", None)
args = cmd.get("args", {}) args = cmd.get("args", {})
if command == "selection_changed":
self.__command_selection_changed(**args)
elif command == "started":
self.__merengue_command("gtk_settings_get", args={"property": "gtk-theme-name"})
self.__load_namespaces()
self.__load_css_providers()
self.__on_project_selection_changed(self.__project)
elif command == "placeholder_selected":
self.emit(
"placeholder-selected",
args["ui_id"],
args["object_id"],
args["layout"],
args["position"],
args["child_type"],
)
elif command == "placeholder_activated":
self.emit(
"placeholder-activated",
args["ui_id"],
args["object_id"],
args["layout"],
args["position"],
args["child_type"],
)
elif command == "gtk_settings_get":
if args["property"] == "gtk-theme-name":
self.__theme = args["value"]
self.notify("gtk_theme")
except Exception as e: except Exception as e:
logger.warning(f"Merengue command error: {e}") logger.warning(f"Merenge output error: {e}")
self.__merengue.stop() self.__merengue.stop()
return return GLib.SOURCE_REMOVE
if command == "selection_changed": return GLib.SOURCE_CONTINUE
self.__command_selection_changed(**args)
elif command == "started":
self.__merengue.merengue_started = True
self.__merengue_command("gtk_settings_get", args={"property": "gtk-theme-name"})
self.__load_namespaces() def __on_broadwayd_stdout(self, process, condition):
if condition == GLib.IOCondition.HUP:
self.__broadwayd.stop()
return GLib.SOURCE_REMOVE
self.__load_css_providers() if self.__broadwayd.stdout is None:
return GLib.SOURCE_REMOVE
self.__ui = None status, retval, length, terminator = self.__broadwayd.stdout.read_line()
self.__on_project_selection_changed(self.__project) # path = retval.replace("Listening on ", "").strip()
elif command == "placeholder_selected":
self.emit( # Run view process
"placeholder-selected", if self.__project.target_tk == "gtk+-3.0":
args["ui_id"], version = "3.0"
args["object_id"], elif self.__project.target_tk == "gtk-4.0":
args["layout"], version = "4.0"
args["position"],
args["child_type"], display = self.__port - 8080
)
elif command == "placeholder_activated": env = json.loads(os.environ.get("MERENGUE_DEV_ENV", "{}"))
self.emit( self.__merengue.run(
"placeholder-activated", [version],
args["ui_id"], env
args["object_id"], | {
args["layout"], "GDK_BACKEND": "broadway",
args["position"], # 'GTK_DEBUG': 'interactive',
args["child_type"], "BROADWAY_DISPLAY": f":{display}",
) },
elif command == "gtk_settings_get": )
if args["property"] == "gtk-theme-name":
self.__theme = args["value"] # Load broadway desktop
self.notify("gtk_theme") self.webview.load_uri(f"http://127.0.0.1:{self.__port}")
self.__broadwayd.stdout.shutdown(False)
self.__broadwayd.stdout = None
return GLib.SOURCE_REMOVE
def __find_free_port(self):
for port in range(8080, 8180):
s = socket.socket()
retval = s.connect_ex(("127.0.0.1", port))
s.close()
if retval != 0:
return port
return 0
def __add_remove_placeholder(self, command, modifier): def __add_remove_placeholder(self, command, modifier):
if self.project is None: if self.project is None:
@ -759,4 +673,3 @@ class CmbView(Gtk.Box):
Gtk.WidgetClass.set_css_name(CmbView, "CmbView") Gtk.WidgetClass.set_css_name(CmbView, "CmbView")

View File

@ -1,9 +1,18 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 --> <!-- Created with Cambalache 0.17.3 -->
<interface> <interface>
<!-- interface-name cmb_view.ui --> <!-- interface-name cmb_view.ui -->
<!-- interface-copyright Juan Pablo Ugarte --> <requires lib="gtk" version="4.0"/>
<requires lib="gtk" version="4.14"/> <requires lib="webkitgtk" version="6.0"/>
<object class="WebKitSettings" id="settings">
<property name="enable-fullscreen">False</property>
<property name="enable-html5-database">False</property>
<property name="enable-html5-local-storage">False</property>
<property name="enable-media">False</property>
<property name="enable-webaudio">False</property>
<property name="media-playback-allows-inline">False</property>
<property name="user-agent">Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15</property>
</object>
<template class="CmbView" parent="GtkBox"> <template class="CmbView" parent="GtkBox">
<child> <child>
<object class="GtkStack" id="stack"> <object class="GtkStack" id="stack">
@ -13,45 +22,11 @@
<child> <child>
<object class="GtkStackPage"> <object class="GtkStackPage">
<property name="child"> <property name="child">
<object class="GtkBox" id="compositor_box"> <object class="WebKitWebView" id="webview">
<child> <property name="can-focus">True</property>
<object class="GtkGraphicsOffload" id="compositor_offload"> <property name="settings">settings</property>
<property name="child"> <property name="visible">True</property>
<object class="CasildaCompositor" id="compositor"> <signal name="context-menu" handler="on_context_menu"/>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
</child>
<child>
<object class="GtkBox" id="error_box">
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<property name="vexpand">True</property>
<property name="visible">False</property>
<child>
<object class="GtkLabel" id="error_message">
<property name="hexpand">True</property>
<property name="justify">center</property>
<property name="vexpand">True</property>
<property name="yalign">0.7</property>
</object>
</child>
<child>
<object class="GtkButton" id="restart_button">
<property name="halign">center</property>
<property name="label">Restart worspace</property>
<property name="valign">start</property>
<property name="vexpand">True</property>
<signal name="clicked" handler="on_restart_button_clicked"/>
</object>
</child>
</object>
</child>
</object> </object>
</property> </property>
<property name="name">ui_view</property> <property name="name">ui_view</property>
@ -73,6 +48,7 @@
<property name="cursor-visible">False</property> <property name="cursor-visible">False</property>
<property name="editable">False</property> <property name="editable">False</property>
<property name="lang">xml</property> <property name="lang">xml</property>
<property name="visible">True</property>
</object> </object>
</child> </child>
</object> </object>
@ -91,30 +67,6 @@
<property name="title" translatable="yes">UI Definition</property> <property name="title" translatable="yes">UI Definition</property>
</object> </object>
</child> </child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="CmbDBInspector" id="db_inspector">
<property name="vexpand">True</property>
</object>
</child>
<child>
<object class="GtkStackSwitcher">
<property name="halign">center</property>
<property name="margin-bottom">4</property>
<property name="margin-top">4</property>
<property name="stack">stack</property>
</object>
</child>
</object>
</property>
<property name="name">data_model</property>
<property name="title" translatable="yes">Data Model</property>
</object>
</child>
</object> </object>
</child> </child>
</template> </template>

View File

@ -1,5 +1,5 @@
VERSION = '@VERSION@' VERSION = '@VERSION@'
FILE_FORMAT_VERSION = '@fileformatversion@' FILE_FORMAT_VERSION = '@fileformatversion@'
pkgdatadir = '@pkgdatadir@' pkgdatadir = '@pkgdatadir@'
merenguedir = '@merenguedir@'
catalogsdir = '@catalogsdir@' catalogsdir = '@catalogsdir@'
merenguedir = '@merenguedir@'

View File

@ -19,8 +19,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
# This is the name used for external objects references. See gtk_builder_expose_object() # This is the name used for external objects references. See gtk_builder_expose_object()
# It is not a valid GType name on purpose since it will never be exported. # It is not a valid GType name on purpose since it will never be exported.

View File

@ -19,13 +19,9 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from .cmb_boolean_undefined import CmbBooleanUndefined
from .cmb_child_type_combo_box import CmbChildTypeComboBox from .cmb_child_type_combo_box import CmbChildTypeComboBox
from .cmb_entry import CmbEntry from .cmb_entry import CmbEntry
from .cmb_file_button import CmbFileButton
from .cmb_file_entry import CmbFileEntry from .cmb_file_entry import CmbFileEntry
from .cmb_icon_name_entry import CmbIconNameEntry from .cmb_icon_name_entry import CmbIconNameEntry
from .cmb_pixbuf_entry import CmbPixbufEntry from .cmb_pixbuf_entry import CmbPixbufEntry
@ -36,7 +32,6 @@ from .cmb_color_entry import CmbColorEntry
from .cmb_enum_combo_box import CmbEnumComboBox from .cmb_enum_combo_box import CmbEnumComboBox
from .cmb_flags_entry import CmbFlagsEntry from .cmb_flags_entry import CmbFlagsEntry
from .cmb_object_chooser import CmbObjectChooser from .cmb_object_chooser import CmbObjectChooser
from .cmb_object_list_editor import CmbObjectListEditor
from .cmb_source_view import CmbSourceView from .cmb_source_view import CmbSourceView
from .cmb_switch import CmbSwitch from .cmb_switch import CmbSwitch
from .cmb_text_view import CmbTextView from .cmb_text_view import CmbTextView

View File

@ -1,78 +0,0 @@
#
# CmbBooleanUndefined
#
# Copyright (C) 2024 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
#
from gi.repository import GObject, Gtk
from cambalache import _
class CmbBooleanUndefined(Gtk.Box):
__gtype_name__ = "CmbBooleanUndefined"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.__undefined = Gtk.CheckButton(label=_("undefined"))
self.__true = Gtk.CheckButton(label=_("true"), group=self.__undefined)
self.__false = Gtk.CheckButton(label=_("false"), group=self.__undefined)
for button in [self.__undefined, self.__true, self.__false]:
self.append(button)
button.connect("notify::active", self.__on_notify)
def __on_notify(self, obj, pspec):
self.notify("cmb-value")
@GObject.Property(type=str)
def cmb_value(self):
if self.__undefined.props.active:
return "undefined"
elif self.__true.props.active:
return "True"
return "False"
@cmb_value.setter
def _set_cmb_value(self, value):
if value is not None:
if type(value) is str:
val = value.lower()
if val == "undefined":
self.__undefined.props.active = True
return
if val in {"1", "t", "y", "true", "yes"}:
active = True
else:
active = False
else:
active = bool(value)
if active:
self.__true.props.active = True
else:
self.__false.props.active = True
else:
self.__undefined.active = True

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import Gdk, GObject, Gtk, Pango from gi.repository import Gdk, GObject, Gtk, Pango

View File

@ -19,14 +19,11 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
import math import math
from gi.repository import GLib, Gtk from gi.repository import GLib, Gtk
from .cmb_boolean_undefined import CmbBooleanUndefined
from .cmb_entry import CmbEntry from .cmb_entry import CmbEntry
from .cmb_file_entry import CmbFileEntry from .cmb_file_entry import CmbFileEntry
from .cmb_icon_name_entry import CmbIconNameEntry from .cmb_icon_name_entry import CmbIconNameEntry
@ -36,12 +33,11 @@ from .cmb_color_entry import CmbColorEntry
from .cmb_enum_combo_box import CmbEnumComboBox from .cmb_enum_combo_box import CmbEnumComboBox
from .cmb_flags_entry import CmbFlagsEntry from .cmb_flags_entry import CmbFlagsEntry
from .cmb_object_chooser import CmbObjectChooser from .cmb_object_chooser import CmbObjectChooser
from .cmb_object_list_editor import CmbObjectListEditor
from .cmb_switch import CmbSwitch from .cmb_switch import CmbSwitch
from .cmb_text_view import CmbTextView from .cmb_text_view import CmbTextView
def cmb_create_editor(project, type_id, prop=None, data=None, parent=None): def cmb_create_editor(project, type_id, prop=None, data=None):
def get_min_max_for_type(type_id): def get_min_max_for_type(type_id):
if type_id == "gchar": if type_id == "gchar":
return (GLib.MININT8, GLib.MAXINT8) return (GLib.MININT8, GLib.MAXINT8)
@ -118,8 +114,6 @@ def cmb_create_editor(project, type_id, prop=None, data=None, parent=None):
adjustment = Gtk.Adjustment(lower=minimum, upper=maximum, step_increment=step_increment, page_increment=10) adjustment = Gtk.Adjustment(lower=minimum, upper=maximum, step_increment=step_increment, page_increment=10)
editor = CmbSpinButton(digits=digits, adjustment=adjustment) editor = CmbSpinButton(digits=digits, adjustment=adjustment)
elif type_id == "GBytes":
editor = CmbTextView(hexpand=True)
elif type_id == "GStrv": elif type_id == "GStrv":
editor = CmbTextView(hexpand=True) editor = CmbTextView(hexpand=True)
elif type_id == "GdkRGBA": elif type_id == "GdkRGBA":
@ -134,31 +128,11 @@ def cmb_create_editor(project, type_id, prop=None, data=None, parent=None):
editor = CmbIconNameEntry(hexpand=True, placeholder_text="<Icon Name>") editor = CmbIconNameEntry(hexpand=True, placeholder_text="<Icon Name>")
elif type_id in ["GtkShortcutTrigger", "GtkShortcutAction"]: elif type_id in ["GtkShortcutTrigger", "GtkShortcutAction"]:
editor = CmbEntry(hexpand=True, placeholder_text=f"<{type_id}>") editor = CmbEntry(hexpand=True, placeholder_text=f"<{type_id}>")
elif type_id == "CmbBooleanUndefined":
editor = CmbBooleanUndefined()
elif type_id == "CmbAccessibleList":
editor = CmbObjectListEditor(
parent=prop.object if prop else parent,
type_id="GtkAccessible",
)
elif info: elif info:
if info.is_object or info.parent_id == "interface": if info.is_object or info.parent_id == "interface":
if prop is None: # TODO: replace prop with project and is_inline
editor = CmbObjectChooser( editor = CmbObjectChooser(parent=prop.object, prop=prop)
project=project, if info.parent_id == "enum":
parent=parent,
type_id=type_id,
)
else:
editor = CmbObjectChooser(
project=project,
parent=prop.object,
is_inline=project.target_tk == "gtk-4.0" and not prop.info.disable_inline_object,
inline_object_id=prop.inline_object_id,
inline_property_id=prop.property_id,
type_id=type_id,
)
elif info.parent_id == "enum":
editor = CmbEnumComboBox(info=info) editor = CmbEnumComboBox(info=info)
elif info.parent_id == "flags": elif info.parent_id == "flags":
editor = CmbFlagsEntry(info=info) editor = CmbFlagsEntry(info=info)

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from .cmb_translatable_popover import CmbTranslatablePopover from .cmb_translatable_popover import CmbTranslatablePopover
@ -54,8 +52,4 @@ class CmbEntry(Gtk.Entry):
@cmb_value.setter @cmb_value.setter
def _set_cmb_value(self, value): def _set_cmb_value(self, value):
# We do not want to emit a change if there is none
if value == self.props.text:
return
self.props.text = value if value is not None else "" self.props.text = value if value is not None else ""

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from ..cmb_type_info import CmbTypeInfo from ..cmb_type_info import CmbTypeInfo
@ -30,7 +28,7 @@ from ..cmb_type_info import CmbTypeInfo
class CmbEnumComboBox(Gtk.ComboBox): class CmbEnumComboBox(Gtk.ComboBox):
__gtype_name__ = "CmbEnumComboBox" __gtype_name__ = "CmbEnumComboBox"
info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE) info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
text_column = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) text_column = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -42,9 +40,7 @@ class CmbEnumComboBox(Gtk.ComboBox):
self.add_attribute(renderer_text, "text", self.text_column) self.add_attribute(renderer_text, "text", self.text_column)
self.props.id_column = self.text_column self.props.id_column = self.text_column
self.props.model = self.info.enum
if self.info:
self.props.model = self.info.enum
def __on_changed(self, obj): def __on_changed(self, obj):
self.notify("cmb-value") self.notify("cmb-value")
@ -55,13 +51,12 @@ class CmbEnumComboBox(Gtk.ComboBox):
@cmb_value.setter @cmb_value.setter
def _set_cmb_value(self, value): def _set_cmb_value(self, value):
if self.info is None:
return
self.props.active_id = None self.props.active_id = None
active_id = self.info.enum_get_value_as_string(value)
if active_id == self.props.active_id: for row in self.info.enum:
return enum_name = row[0]
enum_nick = row[1]
self.props.active_id = active_id # Always use nick as value
if value == enum_name or value == enum_nick:
self.props.active_id = enum_nick

View File

@ -1,114 +0,0 @@
#
# CmbFileButton
#
# Copyright (C) 2021-2023 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 os
from cambalache import _
from gi.repository import GObject, Gio, Gtk
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/control/cmb_file_button.ui")
class CmbFileButton(Gtk.Button):
__gtype_name__ = "CmbFileButton"
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,
filters=self.__filters,
title=self.dialog_title,
accept_label=self.accept_label
)
if self.dirname is not None:
if self.__filename:
fullpath = os.path.join(self.dirname, self.__filename)
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))
if self.unnamed_filename:
dialog.set_initial_name(self.unnamed_filename)
def dialog_callback(dialog, res):
try:
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
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):
return self.__filename
@cmb_value.setter
def _set_cmb_value(self, value):
if value == self.__filename:
return
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

@ -1,34 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_file_button.ui -->
<requires lib="gtk" version="4.12"/>
<template class="CmbFileButton" parent="GtkButton">
<signal name="clicked" handler="on_button_clicked"/>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<child>
<object class="GtkImage" id="file_icon">
<property name="visible">False</property>
</object>
</child>
<child>
<object class="GtkLabel" id="label">
<property name="ellipsize">start</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label">(None)</property>
<property name="width-chars">8</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkImage">
<property name="icon-name">folder-open-symbolic</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -20,13 +20,11 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
import os import os
from cambalache import _ from cambalache import _
from gi.repository import GObject, Gio, Gtk from gi.repository import GObject, Gtk
class CmbFileEntry(Gtk.Entry): class CmbFileEntry(Gtk.Entry):
@ -37,7 +35,7 @@ class CmbFileEntry(Gtk.Entry):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.title = _("Select File") self.title = (_("Select File"),)
self.filter = None self.filter = None
self.props.placeholder_text = "<GFile>" self.props.placeholder_text = "<GFile>"
self.props.secondary_icon_name = "document-open-symbolic" self.props.secondary_icon_name = "document-open-symbolic"
@ -45,24 +43,19 @@ class CmbFileEntry(Gtk.Entry):
self.connect("notify::text", self.__on_text_notify) self.connect("notify::text", self.__on_text_notify)
self.connect("icon-press", self.__on_icon_pressed) self.connect("icon-press", self.__on_icon_pressed)
def __on_icon_pressed(self, widget, icon_pos): def __on_icon_pressed(self, widget, icon_pos, event):
dialog = Gtk.FileDialog( # Create Open Dialog
modal=True, dialog = Gtk.FileChooserNative(
title=self.title, title=self.title, transient_for=self.get_toplevel(), action=Gtk.FileChooserAction.OPEN, filter=self.filter
default_filter=self.filter,
) )
if self.dirname is not None: if self.dirname is not None:
dialog.set_initial_folder(Gio.File.new_for_path(self.dirname)) dialog.set_current_folder(self.dirname)
def dialog_callback(dialog, res): if dialog.run() == Gtk.ResponseType.ACCEPT:
try: self.props.text = os.path.relpath(dialog.get_filename(), start=self.dirname)
file = dialog.open_finish(res)
self.props.text = os.path.relpath(file.get_path(), start=self.dirname)
except Exception:
pass
dialog.open(self.get_root(), None, dialog_callback) dialog.destroy()
def __on_text_notify(self, obj, pspec): def __on_text_notify(self, obj, pspec):
self.notify("cmb-value") self.notify("cmb-value")
@ -73,6 +66,4 @@ class CmbFileEntry(Gtk.Entry):
@cmb_value.setter @cmb_value.setter
def _set_cmb_value(self, value): def _set_cmb_value(self, value):
if value == self.props.text:
return
self.props.text = value if value is not None else "" self.props.text = value if value is not None else ""

View File

@ -20,8 +20,6 @@
# Authors: # Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com> # Juan Pablo Ugarte <juanpablougarte@gmail.com>
# #
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk from gi.repository import GObject, Gtk
from ..cmb_type_info import CmbTypeInfo from ..cmb_type_info import CmbTypeInfo
@ -34,7 +32,6 @@ class CmbFlagsEntry(Gtk.Entry):
id_column = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) id_column = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
text_column = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) text_column = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
value_column = GObject.Property(type=int, default=2, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY) value_column = GObject.Property(type=int, default=2, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
separator = GObject.Property(type=str, default="|", flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.flags = {} self.flags = {}
@ -85,7 +82,7 @@ class CmbFlagsEntry(Gtk.Entry):
for row in self.info.flags: for row in self.info.flags:
flag_id = row[self.id_column] flag_id = row[self.id_column]
if self.flags.get(flag_id, False): if self.flags.get(flag_id, False):
retval = flag_id if retval is None else f"{retval} {self.separator} {flag_id}" retval = flag_id if retval is None else f"{retval} | {flag_id}"
return retval if retval is not None else "" return retval if retval is not None else ""
@ -95,9 +92,6 @@ class CmbFlagsEntry(Gtk.Entry):
@cmb_value.setter @cmb_value.setter
def _set_cmb_value(self, value): def _set_cmb_value(self, value):
if value == self.props.text:
return
self.props.text = value if value is not None else "" self.props.text = value if value is not None else ""
self.flags = {} self.flags = {}
@ -105,7 +99,7 @@ class CmbFlagsEntry(Gtk.Entry):
self._checks[check].props.active = False self._checks[check].props.active = False
if value: if value:
tokens = [t.strip() for t in value.split(self.separator)] tokens = [t.strip() for t in value.split("|")]
for row in self.info.flags: for row in self.info.flags:
flag_id = row[self.id_column] flag_id = row[self.id_column]

Some files were not shown because too many files have changed in this diff Show More