mirror of
https://gitlab.gnome.org/jpu/cambalache.git
synced 2025-06-25 00:02:51 -04:00
Compare commits
381 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
53062b7d3c | ||
|
9aaace82f7 | ||
|
bbb2ce2bf9 | ||
|
dd90c3fc2b | ||
|
8742945f3f | ||
|
06c62349a7 | ||
|
bc6163b1ac | ||
|
ac74e23fde | ||
|
2e29c016f7 | ||
|
b572a7eae3 | ||
|
f96d19ab64 | ||
|
680f9d3243 | ||
|
6fa7f22c9c | ||
|
6ac16ca8c0 | ||
|
e2dbb15a18 | ||
|
c57891a3c8 | ||
|
ccdf5d6ef0 | ||
|
bb39873cd5 | ||
|
9529bfaf76 | ||
|
13db1c20c2 | ||
|
963cee5eec | ||
|
08a73f9c28 | ||
|
02935555bf | ||
|
befb056d2c | ||
|
9cd6e05d5f | ||
|
57a3f3832a | ||
|
8c80fc5d3d | ||
|
7baf191c0b | ||
|
8d170bab39 | ||
|
b592367640 | ||
|
d41c08b68a | ||
|
7de36972dd | ||
|
6b1ae2ad75 | ||
|
296d7767f9 | ||
|
e6e55da821 | ||
|
530c211262 | ||
|
175d1d1b61 | ||
|
1401c0c480 | ||
|
acadc33f02 | ||
|
9d8794cfb0 | ||
|
011c6dc082 | ||
|
258f04825b | ||
|
61893998dd | ||
|
7baa2cfbbe | ||
|
60f3e49672 | ||
|
7213b0351f | ||
|
0709c8487b | ||
|
b2d95c576d | ||
|
211d13b14a | ||
|
5e16c964ee | ||
|
bda30ef76e | ||
|
8858257f6d | ||
|
14d0b3dd39 | ||
|
05bdb5d98e | ||
|
af6258fe8a | ||
|
5de6214da2 | ||
|
4c8cbda88d | ||
|
04ec6758f0 | ||
|
ecf21c544d | ||
|
a9c05ef202 | ||
|
df1dd90146 | ||
|
477c45a32c | ||
|
99d58df208 | ||
|
5f69f152a1 | ||
|
45f6cf920b | ||
|
425aed1872 | ||
|
0d93f151bf | ||
|
a2945f2de2 | ||
|
34bdc5e530 | ||
|
dcc4b0a199 | ||
|
3f23b4a9e7 | ||
|
4db249bf28 | ||
|
8b02d13e01 | ||
|
31b93cef82 | ||
|
c7885f5ab6 | ||
|
9a0db6c2f3 | ||
|
cb1aed17b8 | ||
|
09d118dbc0 | ||
|
829dc50bae | ||
|
94cce882aa | ||
|
f0c1543982 | ||
|
fb930b36da | ||
|
48c004a1e6 | ||
|
f3662b8095 | ||
|
986e3705b4 | ||
|
e30b93506b | ||
|
907c4938a0 | ||
|
874a506f47 | ||
|
2623583f3a | ||
|
1f709586fb | ||
|
6e1935d9fb | ||
|
bc74332ecf | ||
|
7aaabca392 | ||
|
53854f6615 | ||
|
edee67cc72 | ||
|
937211565c | ||
|
c6c278444f | ||
|
56b8af7f97 | ||
|
526b880187 | ||
|
e8cf3eb578 | ||
|
0e4d55eaa2 | ||
|
627c7a4fa9 | ||
|
7f6cd3c005 | ||
|
ffdeb0c6c0 | ||
|
d3e52c3b3b | ||
|
bfe13f87d4 | ||
|
e4a6ae3e36 | ||
|
e242742a29 | ||
|
458e93abce | ||
|
1c0d1a686a | ||
|
0ce1286acc | ||
|
f6ffc64988 | ||
|
5ca4aa2377 | ||
|
2468a22178 | ||
|
3b6b8023af | ||
|
afd241b7ea | ||
|
23c6576514 | ||
|
4c219cd58a | ||
|
39b47123dd | ||
|
f941661e49 | ||
|
572bf8f1ac | ||
|
097ccce2f7 | ||
|
530ed75c79 | ||
|
be19adfba0 | ||
|
cf7a91ceb4 | ||
|
c88712db76 | ||
|
9ea01bdec7 | ||
|
6b323e4475 | ||
|
ee660f396e | ||
|
92cc81894a | ||
|
db70b1c2b8 | ||
|
9a91a2a085 | ||
|
f4c662263d | ||
|
f5886a1727 | ||
|
38bbf4e15f | ||
|
654e8739e9 | ||
|
bbb5c1b6ed | ||
|
15f08c470c | ||
|
8a253fcf67 | ||
|
1477b33c7b | ||
|
69875d1fdb | ||
|
6b7ebb71d1 | ||
|
8621dbf1e3 | ||
|
d210ec2193 | ||
|
c197d14f6f | ||
|
42a21f8b49 | ||
|
d474122539 | ||
|
168d6a09cf | ||
|
731c47903c | ||
|
fb37f3d831 | ||
|
9b21bc15ab | ||
|
9fa28bebd4 | ||
|
d494ffeeec | ||
|
27e7f75606 | ||
|
ed77ab5bc9 | ||
|
2e981adcaf | ||
|
4b7aadb056 | ||
|
5461d4030b | ||
|
4842a23663 | ||
|
162b4a0730 | ||
|
9afe9a289d | ||
|
142df82ff4 | ||
|
f9b90d9fdc | ||
|
483b7e201e | ||
|
38a50780e0 | ||
|
1af92433e1 | ||
|
1a60f42a77 | ||
|
e6acd909c4 | ||
|
af0bdfe8f9 | ||
|
6771499cad | ||
|
e5edade840 | ||
|
70dd8f311d | ||
|
4b366b67fe | ||
|
7f100a1bfd | ||
|
7883ac4c51 | ||
|
9605d7aecf | ||
|
6561a6d9f1 | ||
|
fb8dd9422a | ||
|
f3d3858d9d | ||
|
ef4cacca8c | ||
|
ddde4c5b4a | ||
|
6a5923d98b | ||
|
78e9c945d5 | ||
|
14f45babb8 | ||
|
473d8d1d58 | ||
|
5d4ad6acff | ||
|
bdda000998 | ||
|
5e52ee3b6b | ||
|
be2f8e6332 | ||
|
5179476882 | ||
|
546992a157 | ||
|
42dbfe4bbc | ||
|
17000fcf09 | ||
|
9b64282ff9 | ||
|
5853f94bac | ||
|
0bbb0004dc | ||
|
b09e64efd2 | ||
|
6c06483261 | ||
|
0a51869abc | ||
|
34608d0347 | ||
|
6cfba4725b | ||
|
1176ba4c4f | ||
|
49b6aac7f3 | ||
|
d21afce466 | ||
|
8aacff8862 | ||
|
7d810921c4 | ||
|
39571d9a8e | ||
|
0f38117095 | ||
|
b7d19e6e89 | ||
|
588fdc99c0 | ||
|
8ce6694883 | ||
|
062782eeb6 | ||
|
b2ab6cbe51 | ||
|
5268517b72 | ||
|
28e53a270e | ||
|
942d1a2c52 | ||
|
91a4e2ddee | ||
|
c76b5df13e | ||
|
dd777b6971 | ||
|
2164536c44 | ||
|
6ef20fc74b | ||
|
f9e69a68dd | ||
|
4619c8b95c | ||
|
32b4ab2141 | ||
|
24261e6bf1 | ||
|
98312780b1 | ||
|
1300521cc5 | ||
|
0e6267b5cb | ||
|
77e5298b0a | ||
|
aeb8ef9b36 | ||
|
2510f317c9 | ||
|
a9f754c30f | ||
|
5d5f0fba89 | ||
|
632d1d7e24 | ||
|
a3f5e0cf1c | ||
|
891a6d414b | ||
|
03594263e9 | ||
|
22b5bb9245 | ||
|
b404066054 | ||
|
d07c34ca22 | ||
|
184cd035e2 | ||
|
03c6dbf2ad | ||
|
87df6d1fda | ||
|
b729bb16e8 | ||
|
cfbae798a7 | ||
|
8f28eeb07b | ||
|
611fe64281 | ||
|
307b5beff5 | ||
|
54534a9566 | ||
|
17f229c030 | ||
|
a9391bb801 | ||
|
8153419845 | ||
|
4ac1f622d6 | ||
|
28b0ea54e4 | ||
|
c0f2f32697 | ||
|
cc40e31515 | ||
|
7b6de971ed | ||
|
52105677ae | ||
|
f3a24ee140 | ||
|
e162dbb592 | ||
|
22ecab12f4 | ||
|
706eb0f571 | ||
|
dc07b5d6c8 | ||
|
a9b14cdeb2 | ||
|
91c4cf1a1e | ||
|
785277c43e | ||
|
60b8d8ffd0 | ||
|
6381b90300 | ||
|
7ddbbe1f5d | ||
|
f02ce9fe09 | ||
|
6db7158900 | ||
|
1bcc8cb123 | ||
|
a174e956d0 | ||
|
a66733cbc8 | ||
|
5b82235037 | ||
|
496040af97 | ||
|
d68b53ff62 | ||
|
34317f0ea7 | ||
|
afaecba268 | ||
|
144a35a552 | ||
|
ca5db9ebd4 | ||
|
bdf1abdc33 | ||
|
85af7e8e0c | ||
|
64f383b4d2 | ||
|
4915f19f9f | ||
|
0155d16a04 | ||
|
3a446d9f25 | ||
|
2603e4dd7c | ||
|
69aa84bc6f | ||
|
130218c688 | ||
|
d528bd7ae5 | ||
|
3881e493b3 | ||
|
684bf27475 | ||
|
599c1f7350 | ||
|
3dbb9ce55a | ||
|
04678015fb | ||
|
10d67a882c | ||
|
3bd8d6489b | ||
|
da2739ad12 | ||
|
befa213795 | ||
|
ce7c8f5296 | ||
|
670f96bac8 | ||
|
4a6200e88a | ||
|
e76c592da9 | ||
|
d1fcce7b8e | ||
|
eb180a3de3 | ||
|
2c72303ba0 | ||
|
66141f4ed2 | ||
|
d2f5649cae | ||
|
84f34c0a49 | ||
|
28f470a3ce | ||
|
d0c04302fd | ||
|
42073e5d99 | ||
|
6670670f18 | ||
|
975416463e | ||
|
e7a6fd4a92 | ||
|
fca2cc7e92 | ||
|
e3fa216276 | ||
|
2c79bbfbb9 | ||
|
e4fc463359 | ||
|
386afb930a | ||
|
3853d901e6 | ||
|
2121fe087d | ||
|
ff670f4081 | ||
|
b50cd494d0 | ||
|
132d6ab934 | ||
|
acfac45485 | ||
|
95c2662086 | ||
|
81166070fa | ||
|
e01bf6c996 | ||
|
eaae071c8c | ||
|
1723fcfa1a | ||
|
882bebebd0 | ||
|
3f77956ff9 | ||
|
935a7ab768 | ||
|
59e64e77bf | ||
|
785ae90126 | ||
|
f6e880c2d5 | ||
|
b8361b1a77 | ||
|
1159dd00a9 | ||
|
a5d2d020f6 | ||
|
51b5d6ba02 | ||
|
59ac1d432f | ||
|
6e610c77d8 | ||
|
3eee2b4205 | ||
|
48d7835041 | ||
|
f41f6ae14c | ||
|
8636c05b29 | ||
|
e3f7b13876 | ||
|
6cc1b0ead1 | ||
|
070aa9a180 | ||
|
a56e1fc74f | ||
|
60e8d34f00 | ||
|
a38bfa323c | ||
|
a2e381b114 | ||
|
0aae1d0407 | ||
|
39776c533e | ||
|
119fb2a475 | ||
|
1e1d1badb9 | ||
|
41ce52a212 | ||
|
f2bd967a00 | ||
|
ccfb7ae737 | ||
|
6ced75e831 | ||
|
ad89e6b9ca | ||
|
3671c2d398 | ||
|
0f6a64525b | ||
|
47c1462ab0 | ||
|
ab59c8a8cc | ||
|
a7936d60b0 | ||
|
7d0e466ccf | ||
|
3582d51bc8 | ||
|
3c1e7d11f5 | ||
|
e20d163a15 | ||
|
285434c123 | ||
|
59766f9cc1 | ||
|
4f42200b7e | ||
|
6435a924c4 | ||
|
cd8c77d406 | ||
|
7aeb9419e7 | ||
|
8203bda340 | ||
|
3dc22c9925 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,7 +3,7 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
.flatpak-builder
|
.flatpak-builder
|
||||||
.catalogs
|
.catalogs
|
||||||
.lib
|
.local
|
||||||
.lc_messages
|
.lc_messages
|
||||||
.vscode
|
.vscode
|
||||||
.env.local
|
.env.local
|
||||||
@ -17,6 +17,7 @@ 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
|
||||||
|
323
CHANGELOG.md
Normal file
323
CHANGELOG.md
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
# 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
|
47
Dockerfile
47
Dockerfile
@ -1,23 +1,28 @@
|
|||||||
FROM debian:sid-slim
|
FROM debian:sid-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-gi \
|
desktop-file-utils \
|
||||||
|
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-4 \
|
gir1.2-gtksource-5 \
|
||||||
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 \
|
||||||
python3-lxml \
|
git \
|
||||||
meson \
|
libadwaita-1-dev \
|
||||||
ninja-build \
|
libgirepository-1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libgtk-4-dev \
|
libgtk-4-dev \
|
||||||
libhandy-1-dev \
|
libhandy-1-dev \
|
||||||
libadwaita-1-dev \
|
libwlroots-dev \
|
||||||
gettext \
|
meson \
|
||||||
desktop-file-utils
|
ninja-build \
|
||||||
|
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
|
||||||
@ -25,13 +30,31 @@ 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
|
RUN meson --prefix=/usr && ninja && ninja install
|
||||||
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"]
|
3
Makefile
3
Makefile
@ -1,4 +1,7 @@
|
|||||||
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
105
README.md
@ -1,12 +1,12 @@
|
|||||||

|

|
||||||
|
|
||||||
Cambalache is a new RAD tool for Gtk 4 and 3 with a clear MVC design and data model first philosophy.
|
Cambalache is a 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
the Gdk broadway backend.
|
a custom wayland compositor widget based on wlroots.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -28,26 +28,39 @@ 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 with broadway backend enabled
|
* [GTK](http://www.gtk.org) 3 and 4
|
||||||
* python-gi - Python GTK bindings
|
* python-gi - Python GTK bindings
|
||||||
* python3-lxml - Python libxml2 bindings
|
* python3-lxml - Python libxml2 bindings
|
||||||
* WebkitGTK - Webview for workspace
|
* [casilda](https://gitlab.gnome.org/jpu/casilda) - Workspace custom compositor
|
||||||
|
|
||||||
## Running from sources
|
## Flathub
|
||||||
|
|
||||||
To run it without installing use run-dev.py script, it will automatically compile
|
Flathub is the place to get and distribute apps for all of desktop Linux.
|
||||||
resources and create extra files needed to run.
|
It is powered by Flatpak, allowing Flathub apps to run on almost any Linux
|
||||||
|
distribution.
|
||||||
|
|
||||||
`./run-dev.py`
|
Instructions on how to install flatpak can be found [here](https://flatpak.org/setup/).
|
||||||
|
|
||||||
The minimum requirements are Gtk 3 and lxml, Gtk 4 is only needed to have a functional Gtk 4 workspace.
|
You can get the official build [here](https://flathub.org/apps/details/ar.xjuan.Cambalache)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
The preferred way to run Cambalache is using flatpak.
|
Use the following commands to install build dependencies:
|
||||||
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
|
||||||
@ -61,14 +74,9 @@ make install
|
|||||||
|
|
||||||
Will create the flatpak repository, then the bundle and install it
|
Will create the flatpak repository, then the bundle and install it
|
||||||
|
|
||||||
## Flathub
|
Run as:
|
||||||
|
|
||||||
You can get Cambalache prebuilt bundles [here](https://flathub.org/apps/details/ar.xjuan.Cambalache)
|
|
||||||
|
|
||||||
Use the following to install:
|
|
||||||
```
|
```
|
||||||
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
flatpak run --user ar.xjuan.Cambalache//master
|
||||||
flatpak install --user flathub ar.xjuan.Cambalache
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual installation
|
## Manual installation
|
||||||
@ -76,20 +84,20 @@ flatpak install --user flathub ar.xjuan.Cambalache
|
|||||||
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.
|
||||||
|
|
||||||
```
|
```
|
||||||
# Create build directory and configure project
|
# Configure project in _build directory
|
||||||
mkdir _build && cd _build
|
meson setup --wipe --prefix=~/.local _build .
|
||||||
meson --prefix=~/.local
|
|
||||||
|
|
||||||
# Build and install
|
# Build and install in ~/.local
|
||||||
ninja
|
ninja -C _build install
|
||||||
ninja install
|
|
||||||
```
|
```
|
||||||
To run it from .local/ you might need to setup PYTHONPATH and GI_TYPELIB_PATH env variable depending
|
|
||||||
on your distribution defaults
|
To run it from .local/ you might need to setup a few env variable depending on your distribution
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
@ -102,20 +110,52 @@ Build the image with:
|
|||||||
docker build -t cambalache .
|
docker build -t cambalache .
|
||||||
```
|
```
|
||||||
|
|
||||||
On linux, enable localhost connections to your X server and run with:
|
On linux you can run it on wayland 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)
|
||||||
@ -146,10 +186,11 @@ like all these [people](./SUPPORTERS.md) did.
|
|||||||
- ~8% commission fee
|
- ~8% commission fee
|
||||||
- ~8% payment processing fee
|
- ~8% payment processing fee
|
||||||
|
|
||||||
## Tools
|
## cmb-catalog-gen
|
||||||
|
|
||||||
- cambalache-db:
|
This tool is used to generate Cambalache catalogs from Gir files.
|
||||||
Generate Data Model from Gir files
|
|
||||||
|
|
||||||
- db-codegen:
|
A catalog is a XML file with all the necessary data for Cambalache to produce
|
||||||
Generate GObject classes from DB tables
|
UI files with widgets from a particular library, this includes the different
|
||||||
|
GTypes, with their properties, signals and everything else except
|
||||||
|
the actual object implementations.
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
# Cambalache supporters
|
# Cambalache supporters
|
||||||
|
|
||||||
Many thanks to all the people that suppport the project
|
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
|
||||||
- Patrick Griffis
|
- Patrick Griffis
|
||||||
- David
|
|
||||||
- Luis Barron
|
|
||||||
- Michel Fodje
|
|
||||||
- Platon workaccount
|
|
||||||
- Aemilia Scott
|
- Aemilia Scott
|
||||||
|
- Jonathan K.
|
||||||
|
- Luis Barron
|
||||||
|
- Mitch 4J
|
||||||
|
- JustRyan
|
||||||
|
- Platon workaccount
|
||||||
|
- ~1826340
|
||||||
|
- Mula Gabriel
|
||||||
- Felipe Borges
|
- Felipe Borges
|
||||||
- Johannes Deutsch
|
- Johannes Deutsch
|
||||||
- Jonathan K.
|
|
||||||
- Patrick
|
- Patrick
|
||||||
- 2 kojix
|
- 2 kojix
|
||||||
- Coleman
|
- Coleman
|
||||||
- Shogo Takata
|
|
||||||
- Muasim
|
- Muasim
|
||||||
|
- Shogo Takata
|
||||||
|
15
TODO.md
15
TODO.md
@ -1,15 +0,0 @@
|
|||||||
## Project:
|
|
||||||
|
|
||||||
- GResource
|
|
||||||
|
|
||||||
|
|
||||||
## GtkBuilder missing features:
|
|
||||||
|
|
||||||
- Internal children
|
|
||||||
- <child internal-child="name">
|
|
||||||
|
|
||||||
- GtkWidget
|
|
||||||
- <action-widgets>
|
|
||||||
<action-widget response="">value</action-widget>
|
|
||||||
</action-widgets>
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"app-id" : "ar.xjuan.Cambalache",
|
"app-id" : "ar.xjuan.Cambalache",
|
||||||
"runtime" : "org.gnome.Platform",
|
"runtime" : "org.gnome.Platform",
|
||||||
"runtime-version" : "46",
|
"runtime-version" : "48",
|
||||||
"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,16 +26,65 @@
|
|||||||
],
|
],
|
||||||
"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/30/39/7305428d1c4f28282a4f5bdbef24e0f905d351f34cf351ceb131f5cddf78/lxml-4.9.3.tar.gz",
|
"url" : "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz",
|
||||||
"sha256": "48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"
|
"sha256" : "773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -47,9 +96,15 @@
|
|||||||
{
|
{
|
||||||
"type" : "git",
|
"type" : "git",
|
||||||
"path" : ".",
|
"path" : ".",
|
||||||
"branch": "HEAD"
|
"branch" : "HEAD"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"config-opts" : [
|
||||||
|
"--libdir=lib"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"build-options" : {
|
||||||
|
"env" : { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
# 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
|
||||||
@ -28,10 +30,12 @@ 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__:
|
||||||
@ -46,8 +50,6 @@ if "N_" not in builtins.__dict__:
|
|||||||
# noqa: E402,E401
|
# noqa: E402,E401
|
||||||
from gi.repository import Gio, Gdk, Gtk
|
from gi.repository import Gio, Gdk, Gtk
|
||||||
|
|
||||||
# This will print an error and exit if there is no display available
|
|
||||||
Gtk.init()
|
|
||||||
|
|
||||||
resource = Gio.Resource.load(os.path.join(config.pkgdatadir, "cambalache.gresource"))
|
resource = Gio.Resource.load(os.path.join(config.pkgdatadir, "cambalache.gresource"))
|
||||||
resource._register()
|
resource._register()
|
||||||
@ -55,8 +57,10 @@ 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)
|
Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - 1)
|
||||||
Gtk.IconTheme.get_for_display(display).add_resource_path("/ar/xjuan/Cambalache/icons")
|
|
||||||
|
# FIXME: this is needed in flatpak for icons to work
|
||||||
|
Gtk.IconTheme.get_for_display(display).add_search_path("/app/share/icons")
|
||||||
|
|
||||||
|
|
||||||
def getLogger(name):
|
def getLogger(name):
|
||||||
@ -66,15 +70,17 @@ def getLogger(name):
|
|||||||
ch.setFormatter(formatter)
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
logger.setLevel(os.environ.get("MERENGUE_LOGLEVEL", "WARNING").upper())
|
logger.setLevel(os.environ.get("CAMBALACHE_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
|
||||||
@ -83,14 +89,19 @@ 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_tree_view import CmbTreeView
|
from .cmb_list_view import CmbListView
|
||||||
|
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
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?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>
|
||||||
|
@ -38,7 +38,25 @@ window.cmb-window label.message {
|
|||||||
background-color: rgba(0, 0, 0, .6);
|
background-color: rgba(0, 0, 0, .6);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.cmb-window.dark label.message {
|
window.cmb-window list.notifications {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@ -68,7 +86,9 @@ 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,
|
||||||
@ -87,11 +107,14 @@ 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;
|
||||||
}
|
}
|
||||||
@ -111,3 +134,7 @@ 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;
|
||||||
|
}
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
# 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
|
||||||
|
@ -20,91 +20,59 @@
|
|||||||
# 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
|
||||||
|
|
||||||
from gi.repository import GLib, Gdk, Gtk, Gio
|
gi.require_version('Adw', '1')
|
||||||
|
from gi.repository import GLib, Gdk, Gtk, Gio, Adw
|
||||||
|
|
||||||
from .cmb_window import CmbWindow
|
from .cmb_window import CmbWindow
|
||||||
from cambalache import CmbProject, config, _
|
from cambalache import CmbProject, utils, config, _
|
||||||
|
|
||||||
basedir = os.path.dirname(__file__) or "."
|
basedir = os.path.dirname(__file__) or "."
|
||||||
|
|
||||||
|
|
||||||
class CmbApplication(Gtk.Application):
|
class CmbApplication(Adw.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("export-all", b"E", GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME, _("Export project"), None)
|
self.add_main_option(
|
||||||
|
"export-all", b"E", GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME, _("Deprecated: Export project"), None
|
||||||
|
)
|
||||||
|
|
||||||
def __add_window(self):
|
def add_new_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, uiname=None):
|
def open_project(self, path, target_tk=None):
|
||||||
window = None
|
window = None
|
||||||
|
|
||||||
for win in self.get_windows():
|
for win in self.get_windows():
|
||||||
if win.project is not None and win.project.filename == path:
|
if win.project and win.project.filename == path:
|
||||||
window = win
|
window = win
|
||||||
|
|
||||||
if window is None:
|
if window is None:
|
||||||
window = self.__add_window()
|
window = self.add_new_window()
|
||||||
if path is not None:
|
if path is not None:
|
||||||
window.open_project(path, target_tk=target_tk, uiname=uiname)
|
window.open_project(path, target_tk=target_tk)
|
||||||
|
|
||||||
window.present()
|
window.present()
|
||||||
|
|
||||||
def import_file(self, path):
|
def import_file(self, path):
|
||||||
window = self.__add_window() if self.props.active_window is None else self.props.active_window
|
window = self.add_new_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 do_open(self, files, nfiles, hint):
|
def check_can_quit(self, window=None):
|
||||||
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 = []
|
||||||
@ -130,26 +98,8 @@ class CmbApplication(Gtk.Application):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Create Dialog
|
# Create Dialog
|
||||||
text = _("Save changes before closing?")
|
window = windows[0]
|
||||||
dialog = Gtk.MessageDialog(
|
dialog = window._close_project_dialog_new()
|
||||||
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
|
||||||
@ -214,28 +164,96 @@ class CmbApplication(Gtk.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"):
|
||||||
filename = options.lookup_value("export-all")
|
print("Export has been deprecated and does nothing. Every UI file is updated on project save.")
|
||||||
filename = "".join([chr(c) for c in filename.unpack()])
|
|
||||||
project = CmbProject(filename=filename)
|
|
||||||
project.export()
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return -1
|
return -1
|
||||||
|
@ -20,6 +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 Gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
@ -14,32 +15,56 @@
|
|||||||
<property name="view">shortcuts</property>
|
<property name="view">shortcuts</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkShortcutsShortcut">
|
<object class="GtkShortcutsShortcut">
|
||||||
<property name="accelerator"><Control>n</property>
|
<property name="accelerator"><primary>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"><Control>o</property>
|
<property name="accelerator"><primary>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"><Control>w</property>
|
<property name="accelerator"><primary>i</property>
|
||||||
|
<property name="title" translatable="yes">Import file</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkShortcutsShortcut">
|
||||||
|
<property name="accelerator"><primary>s</property>
|
||||||
|
<property name="title" translatable="yes">Save project</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkShortcutsShortcut">
|
||||||
|
<property name="accelerator"><primary><shift>s</property>
|
||||||
|
<property name="title" translatable="yes">Save project as</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkShortcutsShortcut">
|
||||||
|
<property name="accelerator"><primary>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"><Control>s</property>
|
<property name="accelerator"><primary>question</property>
|
||||||
<property name="title" translatable="yes">Save the project</property>
|
<property name="title" translatable="yes">Keyboard Shortcuts</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkShortcutsShortcut">
|
<object class="GtkShortcutsShortcut">
|
||||||
<property name="accelerator"><Control>e</property>
|
<property name="accelerator"><primary>q</property>
|
||||||
<property name="title" translatable="yes">Save and Export</property>
|
<property name="title" translatable="yes">Quit application</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@ -50,25 +75,31 @@
|
|||||||
<property name="view">shortcuts</property>
|
<property name="view">shortcuts</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkShortcutsShortcut">
|
<object class="GtkShortcutsShortcut">
|
||||||
<property name="accelerator"><Control>Insert</property>
|
<property name="accelerator">Delete</property>
|
||||||
|
<property name="title" translatable="yes">Delete object</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkShortcutsShortcut">
|
||||||
|
<property name="accelerator"><primary>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"><Control>Delete</property>
|
<property name="accelerator"><primary>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"><Control><shift>Insert</property>
|
<property name="accelerator"><primary><shift>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"><Control><shift>Delete</property>
|
<property name="accelerator"><primary><shift>Delete</property>
|
||||||
<property name="title" translatable="yes">Remove row</property>
|
<property name="title" translatable="yes">Remove row</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
# 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
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@ -20,25 +20,33 @@
|
|||||||
# 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"), "open_button", 3),
|
(_("You can open a project and find recently used"), "open_button", 5),
|
||||||
(
|
|
||||||
_("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),
|
||||||
(_("Add new UI file"), "add_button", 3),
|
(_("and Add new UI are directly accessible in the headerbar"), "add_button", 3),
|
||||||
(_("and Save are directly accessible in the headerbar"), "cmb_save_button", 6),
|
(_("together with the main menu"), "menu_button", 3),
|
||||||
(_("just like Save As"), "save_as_button", 2),
|
(
|
||||||
(_("and the main menu"), "menu_button", 3, "menu_button"),
|
_("Where you can create a new project"),
|
||||||
|
_("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),
|
||||||
(
|
(
|
||||||
@ -64,13 +72,6 @@ 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
@ -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.path())
|
conf.set('PYTHON', python_bin.full_path())
|
||||||
conf.set('localedir', localedir)
|
conf.set('localedir', localedir)
|
||||||
conf.set('pkgdatadir', pkgdatadir)
|
conf.set('pkgdatadir', pkgdatadir)
|
||||||
|
|
||||||
|
1
cambalache/app/metainfo.xml
Symbolic link
1
cambalache/app/metainfo.xml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../data/ar.xjuan.Cambalache.metainfo.xml.in
|
290
cambalache/cambalache.cmb
Normal file
290
cambalache/cambalache.cmb
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
<?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>
|
@ -40,6 +40,16 @@ 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;
|
||||||
@ -51,6 +61,22 @@ 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;
|
||||||
}
|
}
|
||||||
@ -58,3 +84,48 @@ 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;
|
||||||
|
}
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
<?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>control/cmb_translatable_widget.ui</file>
|
<file>cambalache.css</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_css_editor.ui</file>
|
<file>cmb_view.ui</file>
|
||||||
<file>cmb_fragment_editor.ui</file>
|
<file>control/cmb_file_button.ui</file>
|
||||||
<file>cmb_object_data_editor.ui</file>
|
<file>control/cmb_translatable_widget.ui</file>
|
||||||
<file>cmb_signal_editor.ui</file>
|
<file>db/cmb_base.sql</file>
|
||||||
<file>cambalache.css</file>
|
<file>db/cmb_history.sql</file>
|
||||||
<file>icons/scalable/actions/bind-symbolic.svg</file>
|
<file>db/cmb_project.sql</file>
|
||||||
<file>icons/scalable/actions/binded-symbolic.svg</file>
|
<file>icons/scalable/actions/binded-symbolic.svg</file>
|
||||||
|
<file>icons/scalable/actions/bind-symbolic.svg</file>
|
||||||
|
<file>cmb_notification_list_row.ui</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
236
cambalache/cmb_accessible_editor.py
Normal file
236
cambalache/cmb_accessible_editor.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
#
|
||||||
|
# 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")
|
@ -20,12 +20,15 @@
|
|||||||
# 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)
|
||||||
|
86
cambalache/cmb_blueprint.py
Normal file
86
cambalache/cmb_blueprint.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#
|
||||||
|
# Blueprint compiler integration functions
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025 Juan Pablo Ugarte
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation;
|
||||||
|
# version 2.1 of the License.
|
||||||
|
#
|
||||||
|
# library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
#
|
||||||
|
# Authors:
|
||||||
|
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-only
|
||||||
|
#
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
|
try:
|
||||||
|
import blueprintcompiler as bp
|
||||||
|
from blueprintcompiler import parser, tokenizer
|
||||||
|
from blueprintcompiler.decompiler import decompile_string
|
||||||
|
from blueprintcompiler.outputs import XmlOutput
|
||||||
|
except Exception:
|
||||||
|
bp = None
|
||||||
|
|
||||||
|
|
||||||
|
class CmbBlueprintError(Exception):
|
||||||
|
def __init__(self, message, errors=[]):
|
||||||
|
super().__init__(message)
|
||||||
|
self.errors = errors
|
||||||
|
|
||||||
|
|
||||||
|
class CmbBlueprintUnsupportedError(CmbBlueprintError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CmbBlueprintMissingError(CmbBlueprintError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("blueprintcompiler is not available")
|
||||||
|
|
||||||
|
|
||||||
|
def cmb_blueprint_decompile(data: str) -> str:
|
||||||
|
if bp is None:
|
||||||
|
raise CmbBlueprintMissingError()
|
||||||
|
|
||||||
|
try:
|
||||||
|
retval = decompile_string(data)
|
||||||
|
except bp.decompiler.UnsupportedError as e:
|
||||||
|
raise CmbBlueprintUnsupportedError(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise CmbBlueprintError(str(e))
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def cmb_blueprint_compile(data: str) -> str:
|
||||||
|
if bp is None:
|
||||||
|
raise CmbBlueprintMissingError()
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize(data)
|
||||||
|
ast, errors, warnings = parser.parse(tokens)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
f = io.StringIO("")
|
||||||
|
errors.pretty_print("temp", data, f)
|
||||||
|
f.seek(0)
|
||||||
|
raise CmbBlueprintError(f.read(), errors=errors)
|
||||||
|
|
||||||
|
if ast is None:
|
||||||
|
raise CmbBlueprintError("AST is None")
|
||||||
|
|
||||||
|
# Ignore warnings
|
||||||
|
|
||||||
|
retval = XmlOutput().emit(ast)
|
||||||
|
return retval.encode()
|
||||||
|
|
@ -20,6 +20,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@ -69,6 +71,10 @@ class CmbContextMenu(Gtk.PopoverMenu):
|
|||||||
"GtkAligment",
|
"GtkAligment",
|
||||||
"GtkEventBox"
|
"GtkEventBox"
|
||||||
]
|
]
|
||||||
|
else:
|
||||||
|
types += [
|
||||||
|
"GtkGraphicsOffload",
|
||||||
|
]
|
||||||
|
|
||||||
self.add_submenu.remove_all()
|
self.add_submenu.remove_all()
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<interface>
|
<interface>
|
||||||
<!-- interface-name cmb_context_menu.ui -->
|
<!-- interface-name cmb_context_menu.ui -->
|
||||||
<requires lib="gtk" version="4.0"/>
|
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||||
<requires lib="gio" version="2.0"/>
|
<requires lib="gio" version="2.0"/>
|
||||||
|
<requires lib="gtk" version="4.0"/>
|
||||||
<menu id="menu_model">
|
<menu id="menu_model">
|
||||||
<section>
|
<section>
|
||||||
<item>
|
<item>
|
||||||
|
@ -20,9 +20,14 @@
|
|||||||
# 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 _
|
||||||
|
|
||||||
@ -32,6 +37,7 @@ 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):
|
||||||
@ -51,8 +57,13 @@ class CmbCSS(CmbBaseCSS):
|
|||||||
if pspec.name == "filename":
|
if pspec.name == "filename":
|
||||||
self.load_css()
|
self.load_css()
|
||||||
|
|
||||||
def get_display_name(self):
|
@classmethod
|
||||||
return self.filename if self.filename else _("Unnamed CSS {css_id}").format(css_id=self.css_id)
|
def get_display_name(cls, css_id, filename):
|
||||||
|
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):
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
from cambalache import utils, _
|
from cambalache import utils, _
|
||||||
@ -79,6 +81,7 @@ 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:
|
||||||
@ -105,10 +108,6 @@ 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()
|
||||||
@ -136,7 +135,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.get_display_name(), active=ui.ui_id in provider_for, halign=Gtk.Align.START, visible=True
|
label=ui.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)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
@ -38,11 +39,8 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="CmbEntry" id="filename">
|
<object class="CmbFileButton" id="filename">
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="placeholder-text" translatable="yes"><file name relative to project></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>
|
||||||
@ -110,20 +108,6 @@
|
|||||||
<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>
|
||||||
@ -189,7 +173,6 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<!-- Custom object fragments -->
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
1579
cambalache/cmb_db.py
1579
cambalache/cmb_db.py
File diff suppressed because it is too large
Load Diff
294
cambalache/cmb_db_inspector.py
Normal file
294
cambalache/cmb_db_inspector.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
#
|
||||||
|
# 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()
|
21
cambalache/cmb_db_inspector.ui
Normal file
21
cambalache/cmb_db_inspector.ui
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?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>
|
@ -20,6 +20,8 @@
|
|||||||
# 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):
|
||||||
@ -37,24 +39,24 @@ def migrate_table_data_to_0_7_5(c, table, data):
|
|||||||
if table == "object":
|
if table == "object":
|
||||||
c.execute(
|
c.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE object SET position=new.position - 1
|
UPDATE temp.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 object
|
FROM temp.object
|
||||||
WHERE parent_id IS NOT NULL
|
WHERE parent_id IS NOT NULL
|
||||||
) AS new
|
) AS new
|
||||||
WHERE object.ui_id=new.ui_id AND object.object_id=new.object_id;
|
WHERE temp.object.ui_id=new.ui_id AND temp.object.object_id=new.object_id;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
c.execute(
|
c.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE object SET position=new.position - 1
|
UPDATE temp.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 object
|
FROM temp.object
|
||||||
WHERE parent_id IS NULL
|
WHERE parent_id IS NULL
|
||||||
) AS new
|
) AS new
|
||||||
WHERE object.ui_id=new.ui_id AND object.object_id=new.object_id;
|
WHERE temp.object.ui_id=new.ui_id AND temp.object.object_id=new.object_id;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -72,9 +74,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 object_property AS op
|
DELETE FROM temp.object_property AS op
|
||||||
WHERE value = 0 AND
|
WHERE value = 0 AND
|
||||||
(SELECT property_id FROM property WHERE owner_id=op.owner_id AND property_id=op.property_id AND is_object)
|
(SELECT property_id FROM temp.property WHERE owner_id=op.owner_id AND property_id=op.property_id AND is_object)
|
||||||
IS NOT NULL;
|
IS NOT NULL;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -116,9 +118,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 {table} SET translatable=1
|
UPDATE temp.{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 {table} SET translatable=NULL
|
UPDATE temp.{table} SET translatable=NULL
|
||||||
WHERE translatable IS NOT NULL AND translatable != 1;
|
WHERE translatable IS NOT NULL AND translatable != 1;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -127,8 +129,35 @@ 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 object_signal SET {prop}=1
|
UPDATE temp.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 object_signal SET {prop}=NULL WHERE {prop} IS NOT NULL AND after != 1;
|
UPDATE temp.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;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
112
cambalache/cmb_db_profile.py
Normal file
112
cambalache/cmb_db_profile.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
from .cmb_object import CmbObject
|
from .cmb_object import CmbObject
|
||||||
@ -68,7 +70,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 is not None:
|
if type(obj) is CmbObject and obj.parent:
|
||||||
binding = GObject.Object.bind_property(
|
binding = GObject.Object.bind_property(
|
||||||
obj,
|
obj,
|
||||||
"custom-child-fragment",
|
"custom-child-fragment",
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
|
151
cambalache/cmb_gresource.py
Normal file
151
cambalache/cmb_gresource.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
117
cambalache/cmb_gresource_editor.py
Normal file
117
cambalache/cmb_gresource_editor.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
#
|
||||||
|
# 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")
|
241
cambalache/cmb_gresource_editor.ui
Normal file
241
cambalache/cmb_gresource_editor.ui
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<?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>
|
@ -20,10 +20,13 @@
|
|||||||
# 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, CmbPropertyInfo
|
from .cmb_objects_base import CmbBaseLayoutProperty
|
||||||
|
from .cmb_property_info import CmbPropertyInfo
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
@ -32,9 +35,7 @@ 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)
|
||||||
@ -105,13 +106,6 @@ 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):
|
||||||
|
@ -20,11 +20,16 @@
|
|||||||
# 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)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# CmbListStore - Cambalache List Store
|
# Cambalache GListModel error item
|
||||||
#
|
#
|
||||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
# Copyright (C) 2024 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,27 +20,23 @@
|
|||||||
# 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
|
||||||
|
from .cmb_base import CmbBase
|
||||||
|
|
||||||
|
|
||||||
class CmbListStore(Gtk.ListStore):
|
# This class is used by GListModel implementations when they do not know which item to return
|
||||||
__gtype_name__ = "CmbListStore"
|
class CmbListError(CmbBase):
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
data = self.project._get_table_data(self.table)
|
@GObject.Property(type=str)
|
||||||
self.set_column_types(data["types"])
|
def display_name(self):
|
||||||
self.__populate()
|
return "list error"
|
||||||
|
|
||||||
def __populate(self):
|
@GObject.Property(type=int)
|
||||||
c = self.project.db.cursor()
|
def n_items(self):
|
||||||
for row in c.execute(self.query):
|
return 0
|
||||||
self.append(row)
|
|
||||||
|
|
||||||
c.close()
|
|
260
cambalache/cmb_list_view.py
Normal file
260
cambalache/cmb_list_view.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
#
|
||||||
|
# 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
|
50
cambalache/cmb_message_notification_view.py
Normal file
50
cambalache/cmb_message_notification_view.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#
|
||||||
|
# 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
|
22
cambalache/cmb_message_notification_view.ui
Normal file
22
cambalache/cmb_message_notification_view.ui
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?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>
|
378
cambalache/cmb_notification.py
Normal file
378
cambalache/cmb_notification.py
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
#
|
||||||
|
# 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()
|
60
cambalache/cmb_notification_list_row.py
Normal file
60
cambalache/cmb_notification_list_row.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
42
cambalache/cmb_notification_list_row.ui
Normal file
42
cambalache/cmb_notification_list_row.ui
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?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>
|
67
cambalache/cmb_notification_list_view.py
Normal file
67
cambalache/cmb_notification_list_view.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
26
cambalache/cmb_notification_list_view.ui
Normal file
26
cambalache/cmb_notification_list_view.ui
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?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>
|
@ -20,22 +20,26 @@
|
|||||||
# 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, Gio
|
||||||
|
|
||||||
|
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):
|
class CmbObject(CmbBaseObject, Gio.ListModel):
|
||||||
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__ = {
|
||||||
@ -46,20 +50,23 @@ class CmbObject(CmbBaseObject):
|
|||||||
"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 = []
|
self.__properties = None
|
||||||
self.properties_dict = {}
|
self.__properties_dict = None
|
||||||
self.layout = []
|
self.__layout = None
|
||||||
self.layout_dict = {}
|
self.__layout_dict = None
|
||||||
self.signals = []
|
self.__signals = None
|
||||||
self.signals_dict = {}
|
self.__signals_dict = None
|
||||||
self.data = []
|
self.__data = None
|
||||||
self.data_dict = {}
|
self.__data_dict = None
|
||||||
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)
|
||||||
|
|
||||||
@ -68,26 +75,79 @@ class CmbObject(CmbBaseObject):
|
|||||||
if self.project is None:
|
if self.project is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Append object to project automatically
|
self.__update_inline_property_id()
|
||||||
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.type_id}> {self.ui_id}:{self.object_id}"
|
return f"CmbObject<{self.display_name_type}> {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 in property_info:
|
for property_name, info in property_info.items():
|
||||||
info = property_info[property_name]
|
# Check if this property was already installed by a derived class
|
||||||
|
if property_name in self.__properties_dict:
|
||||||
|
continue
|
||||||
|
|
||||||
prop = CmbProperty(
|
prop = CmbProperty(
|
||||||
object=self,
|
object=self,
|
||||||
@ -100,16 +160,31 @@ class CmbObject(CmbBaseObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 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:
|
||||||
@ -131,14 +206,10 @@ class CmbObject(CmbBaseObject):
|
|||||||
info=info,
|
info=info,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keep a reference to the position layout property
|
self.__layout.append(prop)
|
||||||
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)
|
||||||
@ -150,8 +221,9 @@ class CmbObject(CmbBaseObject):
|
|||||||
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.signals.append(signal)
|
self.__populate_signals()
|
||||||
self.signals_dict[signal.signal_pk] = signal
|
self.__signals.append(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)
|
||||||
|
|
||||||
@ -162,11 +234,11 @@ class CmbObject(CmbBaseObject):
|
|||||||
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 in self.data:
|
if data.get_id_string() in self.data_dict:
|
||||||
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)
|
||||||
|
|
||||||
@ -177,6 +249,11 @@ class CmbObject(CmbBaseObject):
|
|||||||
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
|
||||||
@ -184,6 +261,11 @@ class CmbObject(CmbBaseObject):
|
|||||||
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
|
||||||
@ -197,13 +279,17 @@ class CmbObject(CmbBaseObject):
|
|||||||
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)
|
||||||
self.__populate_layout_properties_from_type(f"{parent.type_id}LayoutChild")
|
for owner_id in [parent.type_id] + parent.info.hierarchy:
|
||||||
|
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):
|
||||||
@ -218,15 +304,42 @@ class CmbObject(CmbBaseObject):
|
|||||||
|
|
||||||
@parent_id.setter
|
@parent_id.setter
|
||||||
def _set_parent_id(self, value):
|
def _set_parent_id(self, value):
|
||||||
self.db_set(
|
new_parent_id = value if value != 0 else None
|
||||||
"UPDATE object SET parent_id=? WHERE (ui_id, object_id) IS (?, ?);",
|
old_parent_id = self.parent_id if self.parent_id != 0 else None
|
||||||
(
|
|
||||||
self.ui_id,
|
if old_parent_id == new_parent_id:
|
||||||
self.object_id,
|
return
|
||||||
),
|
|
||||||
value if value != 0 else None,
|
# Save old parent and position
|
||||||
|
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)
|
||||||
@ -287,8 +400,8 @@ class CmbObject(CmbBaseObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
@ -333,23 +446,29 @@ class CmbObject(CmbBaseObject):
|
|||||||
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 not in self.data:
|
if data.get_id_string() not in self.data_dict:
|
||||||
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 in self.data
|
assert data.get_id_string() in self.data_dict
|
||||||
|
|
||||||
|
self.project.history_push(
|
||||||
|
_("Remove {key} from {name}").format(key=data.info.key, name=self.display_name_type)
|
||||||
|
)
|
||||||
|
|
||||||
self.project.db.execute(
|
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
|
||||||
@ -366,45 +485,76 @@ class CmbObject(CmbBaseObject):
|
|||||||
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=child.position, new=position)
|
_("Reorder object {name} from position {old} to {new}").format(name=name, old=old_position, new=position)
|
||||||
)
|
)
|
||||||
|
|
||||||
children = []
|
db = self.project.db
|
||||||
|
|
||||||
# Get children in order
|
# Consider this children
|
||||||
c = self.project.db.cursor()
|
#
|
||||||
for row in c.execute(
|
# label 0
|
||||||
"""
|
# button 1
|
||||||
SELECT object_id, position
|
# entry 2
|
||||||
FROM object
|
# switch 3
|
||||||
WHERE ui_id=? AND parent_id=? AND internal IS NULL AND object_id!=? AND object_id NOT IN
|
# toggle 4
|
||||||
(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
|
|
||||||
|
|
||||||
obj = self.project.get_object_by_id(self.ui_id, child_id)
|
# Disable check so we can set position temporally to -1
|
||||||
if obj:
|
db.ignore_check_constraints = True
|
||||||
children.append(obj)
|
db.execute("UPDATE object SET position=-1 WHERE ui_id=? AND object_id=?;", (self.ui_id, child.object_id))
|
||||||
|
|
||||||
# Insert child in new position
|
# Make room for new position
|
||||||
children.insert(position, child)
|
for select_stmt, update_stmt in [
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
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))
|
||||||
|
|
||||||
# Update all positions
|
# Set new position
|
||||||
for pos, obj in enumerate(children):
|
db.execute("UPDATE object SET position=? WHERE ui_id=? AND object_id=?;", (position, self.ui_id, child.object_id))
|
||||||
# Sync layout property
|
|
||||||
if obj.position_layout_property:
|
db.ignore_check_constraints = False
|
||||||
obj.position_layout_property.value = pos
|
|
||||||
else:
|
list_position = child.list_position
|
||||||
# Or object position
|
|
||||||
obj.position = pos
|
self.project._ignore_selection = True
|
||||||
|
# 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()
|
||||||
@ -426,22 +576,171 @@ class CmbObject(CmbBaseObject):
|
|||||||
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)
|
||||||
|
|
||||||
def get_display_name(self):
|
@GObject.Property(type=str)
|
||||||
return self.name if self.name is not None else self.type_id
|
def display_name_type(self):
|
||||||
|
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
|
||||||
|
@ -20,12 +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
|
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__)
|
||||||
|
|
||||||
@ -59,9 +61,6 @@ 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)
|
||||||
|
|
||||||
@ -176,6 +175,7 @@ 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,11 +194,17 @@ 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
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
@ -29,6 +31,12 @@ 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 you’re 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"
|
||||||
@ -67,7 +75,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)
|
||||||
else:
|
elif self.__data:
|
||||||
self.__data.parent.remove_data(self.__data)
|
self.__data.parent.remove_data(self.__data)
|
||||||
|
|
||||||
@GObject.Property(type=GObject.Object)
|
@GObject.Property(type=GObject.Object)
|
||||||
@ -115,7 +123,11 @@ class CmbObjectDataEditor(Gtk.Box):
|
|||||||
|
|
||||||
editor = self.__arg_editors.get(key, None)
|
editor = self.__arg_editors.get(key, None)
|
||||||
if editor:
|
if editor:
|
||||||
editor.cmb_value = self.data.get_arg(key)
|
val = 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)
|
||||||
@ -132,8 +144,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
|||||||
self.__update_view()
|
self.__update_view()
|
||||||
|
|
||||||
def __on_data_removed(self, obj, data):
|
def __on_data_removed(self, obj, data):
|
||||||
if self.object and self.info:
|
self.__remove_data_editor(data)
|
||||||
self.__remove_data_editor(data)
|
|
||||||
|
|
||||||
def __ensure_object_data(self, history_message):
|
def __ensure_object_data(self, history_message):
|
||||||
if self.data:
|
if self.data:
|
||||||
@ -239,7 +250,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)
|
editor = cmb_create_editor(project, info.type_id, data=self.data, parent=self.object)
|
||||||
self.__value_editor = editor
|
self.__value_editor = editor
|
||||||
|
|
||||||
if self.data:
|
if self.data:
|
||||||
@ -259,7 +270,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)
|
editor = cmb_create_editor(project, arg_info.type_id, parent=self.object)
|
||||||
self.__arg_editors[arg_info.key] = editor
|
self.__arg_editors[arg_info.key] = editor
|
||||||
|
|
||||||
# Initialize value
|
# Initialize value
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
@ -69,7 +70,6 @@
|
|||||||
<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>
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
@ -41,11 +43,17 @@ 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)
|
||||||
|
|
||||||
@ -54,7 +62,7 @@ class CmbObjectEditor(Gtk.Box):
|
|||||||
|
|
||||||
# Id/Class entry
|
# Id/Class entry
|
||||||
entry = CmbEntry()
|
entry = CmbEntry()
|
||||||
GObject.Object.bind_property(
|
self.bind_property(
|
||||||
self.__object,
|
self.__object,
|
||||||
"name",
|
"name",
|
||||||
entry,
|
entry,
|
||||||
@ -66,7 +74,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 not self.__object.parent_id:
|
if self.__object and self.__object.parent_id == 0:
|
||||||
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
|
||||||
@ -129,7 +137,7 @@ class CmbObjectEditor(Gtk.Box):
|
|||||||
|
|
||||||
combo = CmbChildTypeComboBox(object=self.__object)
|
combo = CmbChildTypeComboBox(object=self.__object)
|
||||||
|
|
||||||
GObject.Object.bind_property(
|
self.bind_property(
|
||||||
self.__object,
|
self.__object,
|
||||||
"type",
|
"type",
|
||||||
combo,
|
combo,
|
||||||
@ -207,12 +215,16 @@ 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
|
||||||
|
|
||||||
GObject.Object.bind_property(
|
self.bind_property(
|
||||||
prop,
|
prop,
|
||||||
"value",
|
"value",
|
||||||
editor,
|
editor,
|
||||||
@ -225,7 +237,10 @@ 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)
|
||||||
|
|
||||||
# Keep a dict of labels
|
if prop.info.disabled:
|
||||||
|
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)
|
||||||
@ -245,7 +260,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=None if data else info.data[data_key],
|
info=info.data[data_key],
|
||||||
)
|
)
|
||||||
|
|
||||||
grid.attach(editor, 0, i, 2, 1)
|
grid.attach(editor, 0, i, 2, 1)
|
||||||
@ -286,11 +301,16 @@ 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:
|
||||||
self.__object.connect("notify", self.__on_object_notify)
|
obj.connect("notify", self.__on_object_notify)
|
||||||
self.__object.ui.connect("notify", self.__on_object_ui_notify)
|
obj.ui.connect("notify", self.__on_object_ui_notify)
|
||||||
|
|
||||||
self.__update_view()
|
self.__update_view()
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Cambalache Base Object wrappers
|
# Cambalache Base Object wrappers
|
||||||
#
|
#
|
||||||
# Copyright (C) 2021-2022 Juan Pablo Ugarte
|
# Copyright (C) 2021-2024 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,6 +22,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 GObject
|
from gi.repository import GObject
|
||||||
from .cmb_base import CmbBase
|
from .cmb_base import CmbBase
|
||||||
@ -55,8 +57,8 @@ class CmbBaseLibraryInfo(CmbBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CmbPropertyInfo(CmbBase):
|
class CmbBasePropertyInfo(CmbBase):
|
||||||
__gtype_name__ = "CmbPropertyInfo"
|
__gtype_name__ = "CmbBasePropertyInfo"
|
||||||
|
|
||||||
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)
|
||||||
@ -81,13 +83,15 @@ class CmbPropertyInfo(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
|
||||||
)
|
)
|
||||||
is_position = GObject.Property(
|
deprecated = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
|
||||||
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)
|
||||||
@ -109,9 +113,11 @@ class CmbPropertyInfo(CmbBase):
|
|||||||
deprecated_version,
|
deprecated_version,
|
||||||
translatable,
|
translatable,
|
||||||
disable_inline_object,
|
disable_inline_object,
|
||||||
is_position,
|
deprecated,
|
||||||
required,
|
required,
|
||||||
workspace_default,
|
workspace_default,
|
||||||
|
original_owner_id,
|
||||||
|
disabled,
|
||||||
):
|
):
|
||||||
return cls(
|
return cls(
|
||||||
project=project,
|
project=project,
|
||||||
@ -128,9 +134,11 @@ class CmbPropertyInfo(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,
|
||||||
is_position=is_position,
|
deprecated=deprecated,
|
||||||
required=required,
|
required=required,
|
||||||
workspace_default=workspace_default,
|
workspace_default=workspace_default,
|
||||||
|
original_owner_id=original_owner_id,
|
||||||
|
disabled=disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -277,6 +285,30 @@ 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"
|
||||||
|
|
||||||
@ -421,6 +453,97 @@ 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"
|
||||||
|
|
||||||
|
118
cambalache/cmb_path.py
Normal file
118
cambalache/cmb_path.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
142
cambalache/cmb_poll_notification_view.py
Normal file
142
cambalache/cmb_poll_notification_view.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
||||||
|
|
64
cambalache/cmb_poll_notification_view.ui
Normal file
64
cambalache/cmb_poll_notification_view.ui
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?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"><small>Refresh</small></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>
|
76
cambalache/cmb_poll_option_check.py
Normal file
76
cambalache/cmb_poll_option_check.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#
|
||||||
|
# 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
|
23
cambalache/cmb_poll_option_check.ui
Normal file
23
cambalache/cmb_poll_option_check.ui
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?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
@ -20,11 +20,17 @@
|
|||||||
# 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, CmbPropertyInfo
|
from .cmb_objects_base import CmbBaseProperty
|
||||||
|
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):
|
||||||
@ -60,9 +66,96 @@ class CmbProperty(CmbBaseProperty):
|
|||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def _set_value(self, value):
|
def _set_value(self, value):
|
||||||
self.__update_values(value, self.bind_property)
|
self.__update_values(value, self.translatable, self.translation_context, self.translation_comments, self.bind_property)
|
||||||
|
|
||||||
def __update_values(self, value, bind_property):
|
@GObject.Property(type=bool, default=False)
|
||||||
|
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)
|
||||||
@ -72,18 +165,22 @@ 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)
|
(value is None or value == self.info.default_value or (self.info.is_object and value == 0)) and
|
||||||
) and bind_property is None:
|
bind_property is None and
|
||||||
c.execute(
|
not translatable and
|
||||||
"DELETE FROM object_property WHERE ui_id=? AND object_id=? AND owner_id=? AND property_id=?;",
|
translation_context is None and
|
||||||
(self.ui_id, self.object_id, self.owner_id, self.property_id),
|
translation_comments is None
|
||||||
)
|
):
|
||||||
|
self.reset()
|
||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
value is None
|
value == self.value and
|
||||||
and bind_source_id == self.bind_source_id
|
translatable == self.translatable and
|
||||||
and bind_owner_id == self.bind_owner_id
|
translation_context == self.translation_context and
|
||||||
and bind_property_id == self.bind_property_id
|
translation_comments == self.translation_comments and
|
||||||
|
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
|
||||||
|
|
||||||
@ -94,14 +191,23 @@ 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 value=?, bind_source_id=?, bind_owner_id=?, bind_property_id=?
|
SET
|
||||||
|
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,
|
||||||
@ -112,11 +218,19 @@ 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)
|
(
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
ui_id, object_id, owner_id, property_id,
|
||||||
|
value,
|
||||||
|
translatable, translation_context, translation_comments,
|
||||||
|
bind_source_id, bind_owner_id, bind_property_id
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
self.ui_id,
|
self.ui_id,
|
||||||
@ -124,12 +238,20 @@ 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)
|
||||||
|
|
||||||
@ -156,11 +278,22 @@ 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, bind_property)
|
self.__update_values(self.value, self.translatable, self.translation_context, self.translation_comments, 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)
|
||||||
self.version_warning = utils.get_version_warning(
|
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
|
||||||
|
66
cambalache/cmb_property_info.py
Normal file
66
cambalache/cmb_property_info.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
||||||
|
|
@ -20,14 +20,17 @@
|
|||||||
# 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_objects_base import CmbPropertyInfo
|
from .cmb_property_info import CmbPropertyInfo
|
||||||
from .control import CmbObjectChooser, CmbFlagsEntry
|
from .control import CmbObjectChooser, CmbFlagsEntry
|
||||||
|
from cambalache import _
|
||||||
|
|
||||||
|
|
||||||
class CmbPropertyLabel(Gtk.Button):
|
class CmbPropertyLabel(Gtk.Button):
|
||||||
@ -46,15 +49,17 @@ 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.label = Gtk.Label(xalign=0, visible=True)
|
self.props.focus_on_click = False
|
||||||
box = Gtk.Box(visible=True)
|
self.label = Gtk.Label(halign=Gtk.Align.START, valign=Gtk.Align.CENTER)
|
||||||
|
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, visible=True)
|
self.bind_icon = Gtk.Image(icon_size=Gtk.IconSize.NORMAL)
|
||||||
box.append(self.bind_icon)
|
box.append(self.bind_icon)
|
||||||
|
|
||||||
self.label.props.label = self.prop.property_id
|
# A11y properties are prefixed to avoid clashes, do not show prefix here
|
||||||
|
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())
|
||||||
@ -123,13 +128,23 @@ 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)
|
popover = Gtk.Popover(position=Gtk.PositionType.LEFT, css_classes=["cmb-binding-popover"])
|
||||||
popover.set_parent(self)
|
popover.set_parent(self)
|
||||||
|
|
||||||
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4, visible=True)
|
label = Gtk.Label(label="<b>Property Binding</b>", use_markup=True, halign=Gtk.Align.START, hexpand=True)
|
||||||
|
close = Gtk.Button(icon_name="window-close-symbolic", halign=Gtk.Align.END, css_classes=["close"])
|
||||||
|
close.connect("clicked", self.__on_close_clicked, popover)
|
||||||
|
|
||||||
grid.attach(Gtk.Label(label="<b>Property Binding</b>", use_markup=True, visible=True, xalign=0), 0, 0, 2, 1)
|
box = Gtk.Box(hexpand=True)
|
||||||
|
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)
|
||||||
@ -159,18 +174,16 @@ 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)]:
|
||||||
editor.props.visible = True
|
label = Gtk.Label(label=prop_label, xalign=0)
|
||||||
|
|
||||||
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", visible=True, halign=Gtk.Align.END)
|
clear = Gtk.Button(label=_("Clear"), 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)
|
||||||
@ -210,6 +223,9 @@ 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
|
||||||
|
@ -20,6 +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 GObject, Gtk, Pango
|
from gi.repository import GObject, Gtk, Pango
|
||||||
|
|
||||||
@ -76,7 +78,7 @@ class CmbSignalEditor(Gtk.Box):
|
|||||||
|
|
||||||
@object.setter
|
@object.setter
|
||||||
def _set_object(self, obj):
|
def _set_object(self, obj):
|
||||||
if self._object is not None:
|
if self._object:
|
||||||
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)
|
||||||
@ -84,7 +86,7 @@ class CmbSignalEditor(Gtk.Box):
|
|||||||
|
|
||||||
self._object = obj
|
self._object = obj
|
||||||
|
|
||||||
if obj is not None:
|
if obj:
|
||||||
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)
|
||||||
@ -136,13 +138,17 @@ 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")
|
||||||
@ -240,7 +246,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 is not None and info.detailed:
|
if info 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]
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
@ -36,9 +37,6 @@
|
|||||||
<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>
|
||||||
|
266
cambalache/cmb_tree_expander.py
Normal file
266
cambalache/cmb_tree_expander.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
@ -1,163 +0,0 @@
|
|||||||
#
|
|
||||||
# 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
|
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
|
@ -20,8 +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, Gtk
|
from gi.repository import GLib, GObject, Gio, Gtk
|
||||||
|
|
||||||
from .cmb_project import CmbProject
|
from .cmb_project import CmbProject
|
||||||
from .cmb_type_info import CmbTypeInfo
|
from .cmb_type_info import CmbTypeInfo
|
||||||
@ -46,13 +48,12 @@ 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()
|
||||||
treeview = Gtk.Template.Child()
|
listview = Gtk.Template.Child()
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.__project = None
|
self.__project = None
|
||||||
self.__model = None
|
|
||||||
self._search_text = ""
|
self._search_text = ""
|
||||||
self._filter = None
|
self.__model = None
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@ -92,10 +93,6 @@ 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
|
||||||
@ -111,7 +108,12 @@ 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 = Gtk.ListStore(str, str, CmbTypeInfo, bool)
|
store = Gio.ListStore()
|
||||||
|
|
||||||
|
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:
|
||||||
@ -133,15 +135,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([f"<i>▾ {category}</i>", "", None, False])
|
store.append(CmbTypeInfo(type_id=f"<i><b>▾ {category}</b></i>"))
|
||||||
|
|
||||||
self.__store_append_info(store, i)
|
store.append(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:
|
||||||
self.__store_append_info(store, project.type_info[constants.EXTERNAL_TYPE])
|
store.append(project.type_info[constants.EXTERNAL_TYPE])
|
||||||
|
|
||||||
return store
|
return filter_model
|
||||||
|
|
||||||
@GObject.Property(type=CmbProject)
|
@GObject.Property(type=CmbProject)
|
||||||
def project(self):
|
def project(self):
|
||||||
@ -149,24 +151,18 @@ class CmbTypeChooserWidget(Gtk.Box):
|
|||||||
|
|
||||||
@project.setter
|
@project.setter
|
||||||
def _set_project(self, project):
|
def _set_project(self, project):
|
||||||
if self.__project is not None:
|
if self.__project:
|
||||||
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._filter = Gtk.TreeModelFilter(child_model=self.__model) if self.__model else None
|
self.listview.set_model(Gtk.NoSelection(model=self.__model))
|
||||||
if self._filter:
|
|
||||||
self._filter.set_visible_func(self.__visible_func)
|
|
||||||
|
|
||||||
self.treeview.props.model = self._filter
|
if project:
|
||||||
|
|
||||||
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):
|
||||||
@ -179,61 +175,34 @@ 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._filter.refilter()
|
self.__model.props.filter.changed(Gtk.FilterChange.DIFFERENT)
|
||||||
|
|
||||||
@Gtk.Template.Callback("on_treeview_row_activated")
|
@Gtk.Template.Callback("on_listview_activate")
|
||||||
def __on_treeview_row_activated(self, treeview, path, column):
|
def __on_listview_activate(self, listview, position):
|
||||||
model = treeview.props.model
|
info = self.__model.get_item(position)
|
||||||
info = model[model.get_iter(path)][2]
|
|
||||||
|
|
||||||
if info is not None:
|
if info is not None and info.project:
|
||||||
self.emit("type-selected", info)
|
self.emit("type-selected", info)
|
||||||
|
|
||||||
def __visible_func(self, model, iter, data):
|
def __custom_filter_func(self, info, data):
|
||||||
type_id, type_id_lower, info, sensitive = model[iter]
|
return info.type_id.lower().find(self._search_text) >= 0
|
||||||
|
|
||||||
# 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.__store_append_info(self.__model, info)
|
self.__model.props.model.insert_sorted(info, lambda a, b, d: GLib.strcmp0(a.type_id, b.type_id), None)
|
||||||
|
|
||||||
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
|
||||||
for row in self.__model:
|
found, position = self.__model.props.model.find(info)
|
||||||
if info == row[2]:
|
if found:
|
||||||
self.__model.remove(row.iter)
|
self.__model.props.model.remove(position)
|
||||||
|
|
||||||
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")
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<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>
|
||||||
@ -14,34 +15,37 @@
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow">
|
<object class="GtkScrolledWindow" id="scrolledwindow">
|
||||||
<property name="child">
|
<property name="hexpand">True</property>
|
||||||
<object class="GtkTreeView" id="treeview">
|
<property name="propagate-natural-height">True</property>
|
||||||
<property name="activate-on-single-click">1</property>
|
<property name="propagate-natural-width">True</property>
|
||||||
<property name="enable-search">0</property>
|
<property name="vexpand">True</property>
|
||||||
<property name="headers-visible">0</property>
|
<child>
|
||||||
<signal name="row-activated" handler="on_treeview_row_activated"/>
|
<object class="GtkListView" id="listview">
|
||||||
<child internal-child="selection">
|
<property name="factory">
|
||||||
<object class="GtkTreeSelection" id="treeview-selection"/>
|
<object class="GtkBuilderListItemFactory">
|
||||||
</child>
|
<property name="bytes"><![CDATA[<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<child>
|
<interface>
|
||||||
<object class="GtkTreeViewColumn" id="column_adaptor">
|
<template class="GtkListItem" parent="GObject">
|
||||||
<child>
|
<property name="child">
|
||||||
<object class="GtkCellRendererText" id="adaptor_cell"/>
|
<object class="GtkInscription">
|
||||||
<!-- Custom child fragments -->
|
<binding name="markup">
|
||||||
<attributes>
|
<lookup name="type_id" type="CmbTypeInfo">
|
||||||
<attribute name="markup">0</attribute>
|
<lookup name="item">GtkListItem</lookup>
|
||||||
<attribute name="sensitive">3</attribute>
|
</lookup>
|
||||||
</attributes>
|
</binding>
|
||||||
</child>
|
</object>
|
||||||
|
</property>
|
||||||
|
</template>
|
||||||
|
</interface>]]></property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</property>
|
||||||
|
<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>
|
||||||
</property>
|
</child>
|
||||||
<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>
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
@ -27,13 +29,18 @@ 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):
|
||||||
@ -53,19 +60,33 @@ 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":
|
||||||
@ -134,7 +155,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, *row)
|
retval[row[1]] = Klass.from_row(self.project, *row)
|
||||||
|
|
||||||
c.close()
|
c.close()
|
||||||
return retval
|
return retval
|
||||||
@ -143,14 +164,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, owner_id, data_id, parent_id, key, type_id, translatable)
|
retval = CmbTypeDataInfo.from_row(self.project, 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, *row)
|
args[_key] = CmbTypeDataArgInfo.from_row(self.project, *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)):
|
||||||
@ -177,6 +198,52 @@ 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 = []
|
||||||
@ -214,7 +281,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=?", (self.type_id,)):
|
for row in c.execute(f"SELECT name, nick, value FROM type_{name} WHERE type_id=? ORDER BY nick;", (self.type_id,)):
|
||||||
retval.append(row)
|
retval.append(row)
|
||||||
|
|
||||||
c.close()
|
c.close()
|
||||||
@ -263,3 +330,48 @@ 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)
|
||||||
|
@ -20,25 +20,39 @@
|
|||||||
# 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
|
import os
|
||||||
|
from gi.repository import GObject, Gio
|
||||||
|
|
||||||
from .cmb_objects_base import CmbBaseUI
|
from .cmb_path import CmbPath
|
||||||
|
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):
|
class CmbUI(CmbBaseUI, Gio.ListModel):
|
||||||
__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,))
|
||||||
@ -51,6 +65,10 @@ class CmbUI(CmbBaseUI):
|
|||||||
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 = {}
|
||||||
|
|
||||||
@ -114,18 +132,21 @@ class CmbUI(CmbBaseUI):
|
|||||||
|
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
def get_display_name(self):
|
@classmethod
|
||||||
if self.filename:
|
def get_display_name(cls, ui_id, filename):
|
||||||
return self.filename
|
return os.path.basename(filename) if filename else _("Unnamed {ui_id}").format(ui_id=ui_id)
|
||||||
|
|
||||||
|
@GObject.Property(type=str)
|
||||||
|
def display_name(self):
|
||||||
|
filename = self.filename
|
||||||
template_id = self.template_id
|
template_id = self.template_id
|
||||||
|
|
||||||
if template_id:
|
if filename is None and 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 is not None:
|
if template:
|
||||||
return template.name
|
return template.name
|
||||||
|
|
||||||
return _("Unnamed {ui_id}").format(ui_id=self.ui_id)
|
return CmbUI.get_display_name(self.ui_id, filename)
|
||||||
|
|
||||||
def __get_infered_target(self, library_id):
|
def __get_infered_target(self, library_id):
|
||||||
ui_id = self.ui_id
|
ui_id = self.ui_id
|
||||||
@ -165,3 +186,37 @@ class CmbUI(CmbBaseUI):
|
|||||||
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
|
||||||
|
@ -20,9 +20,14 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +36,7 @@ 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()
|
||||||
@ -52,9 +58,6 @@ 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()
|
||||||
|
|
||||||
@ -75,10 +78,17 @@ 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 = GObject.Object.bind_property(
|
binding = obj.bind_property(
|
||||||
obj,
|
|
||||||
field,
|
field,
|
||||||
getattr(self, field),
|
getattr(self, field),
|
||||||
"cmb-value",
|
"cmb-value",
|
||||||
@ -86,37 +96,42 @@ class CmbUIEditor(Gtk.Grid):
|
|||||||
)
|
)
|
||||||
self._bindings.append(binding)
|
self._bindings.append(binding)
|
||||||
|
|
||||||
@Gtk.Template.Callback("on_remove_button_clicked")
|
if obj.project.target_tk == "gtk-4.0":
|
||||||
def __on_remove_button_clicked(self, button):
|
self.filename.mime_types = "application/x-gtk-builder;text/x-blueprint"
|
||||||
self.emit("remove-ui")
|
|
||||||
|
|
||||||
@Gtk.Template.Callback("on_export_button_clicked")
|
# filename -> format
|
||||||
def __on_export_button_clicked(self, button):
|
binding = obj.bind_property(
|
||||||
self.emit("export-ui")
|
"filename",
|
||||||
|
self.format,
|
||||||
|
"selected",
|
||||||
|
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||||
|
transform_to=self.__filename_to_format,
|
||||||
|
transform_from=self.__format_to_filename,
|
||||||
|
user_data=obj
|
||||||
|
)
|
||||||
|
self._bindings.append(binding)
|
||||||
|
|
||||||
@GObject.Signal(
|
self.format.show()
|
||||||
flags=GObject.SignalFlags.RUN_LAST,
|
self.format.set_sensitive(bool(obj.filename))
|
||||||
return_type=bool,
|
else:
|
||||||
arg_types=(),
|
self.filename.mime_types = "application/x-gtk-builder;application/x-glade"
|
||||||
accumulator=GObject.signal_accumulator_true_handled,
|
self.format.hide()
|
||||||
)
|
|
||||||
def export_ui(self):
|
|
||||||
if self.object:
|
|
||||||
self.object.project.export_ui(self.object)
|
|
||||||
|
|
||||||
return True
|
def __filename_to_format(self, binding, source_value, ui):
|
||||||
|
if not source_value:
|
||||||
|
self.format.props.sensitive = False
|
||||||
|
return 0
|
||||||
|
self.format.props.sensitive = True
|
||||||
|
|
||||||
@GObject.Signal(
|
return 1 if source_value.endswith(".blp") else 0
|
||||||
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)
|
|
||||||
|
|
||||||
return True
|
def __format_to_filename(self, binding, target_value, ui):
|
||||||
|
if not ui.filename:
|
||||||
|
self.format.props.sensitive = False
|
||||||
|
return None
|
||||||
|
self.format.props.sensitive = True
|
||||||
|
|
||||||
|
return os.path.splitext(ui.filename)[0] + (".blp" if target_value == 1 else ".ui")
|
||||||
|
|
||||||
|
|
||||||
Gtk.WidgetClass.set_css_name(CmbUIEditor, "CmbUIEditor")
|
Gtk.WidgetClass.set_css_name(CmbUIEditor, "CmbUIEditor")
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.97.1 -->
|
||||||
<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"/>
|
||||||
@ -26,7 +27,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">2</property>
|
<property name="row">3</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -36,7 +37,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">3</property>
|
<property name="row">4</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -46,7 +47,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">4</property>
|
<property name="row">5</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -56,16 +57,13 @@
|
|||||||
<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">5</property>
|
<property name="row">6</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="CmbEntry" id="filename">
|
<object class="CmbFileButton" id="filename">
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="placeholder-text" translatable="yes"><file name relative to project></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>
|
||||||
@ -80,7 +78,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">5</property>
|
<property name="row">6</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -97,7 +95,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">2</property>
|
<property name="row">3</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -114,7 +112,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">5</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -126,7 +124,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">1</property>
|
<property name="row">2</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -136,45 +134,7 @@
|
|||||||
<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">1</property>
|
<property name="row">2</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>
|
||||||
@ -191,7 +151,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">4</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -201,7 +161,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">6</property>
|
<property name="row">7</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@ -218,7 +178,34 @@
|
|||||||
<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">6</property>
|
<property name="row">7</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Format:</property>
|
||||||
|
<layout>
|
||||||
|
<property name="column">0</property>
|
||||||
|
<property name="row">1</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkDropDown" id="format">
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="model">
|
||||||
|
<object class="GtkStringList">
|
||||||
|
<property name="strings">Gtk Builder
|
||||||
|
Blueprint</property>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
<layout>
|
||||||
|
<property name="column">1</property>
|
||||||
|
<property name="column-span">1</property>
|
||||||
|
<property name="row">1</property>
|
||||||
|
<property name="row-span">1</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
|
57
cambalache/cmb_version_notification_view.py
Normal file
57
cambalache/cmb_version_notification_view.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#
|
||||||
|
# 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()
|
50
cambalache/cmb_version_notification_view.ui
Normal file
50
cambalache/cmb_version_notification_view.ui
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?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>
|
@ -20,66 +20,157 @@
|
|||||||
# 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
|
||||||
|
|
||||||
from gi.repository import GObject, GLib, Gtk, WebKit
|
gi.require_version('Casilda', '0.1')
|
||||||
|
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, _
|
from cambalache import getLogger, _, N_
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
basedir = os.path.dirname(__file__) or "."
|
basedir = os.path.dirname(__file__) or "."
|
||||||
|
|
||||||
GObject.type_ensure(WebKit.Settings.__gtype__)
|
GObject.type_ensure(Casilda.Compositor.__gtype__)
|
||||||
GObject.type_ensure(WebKit.WebView.__gtype__)
|
|
||||||
|
|
||||||
|
|
||||||
class CmbProcess(GObject.Object):
|
class CmbMerengueProcess(GObject.Object):
|
||||||
__gsignals__ = {
|
__gsignals__ = {
|
||||||
"stdout": (GObject.SignalFlags.RUN_LAST, bool, (GLib.IOCondition,)),
|
"handle-command": (GObject.SignalFlags.RUN_LAST, None, (str,)),
|
||||||
"exit": (GObject.SignalFlags.RUN_LAST, None, ()),
|
"exit": (GObject.SignalFlags.RUN_LAST, None, ()),
|
||||||
}
|
}
|
||||||
|
|
||||||
file = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
|
gtk_version = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
|
||||||
|
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)
|
||||||
|
|
||||||
self.pid = 0
|
@GObject.Property(type=str)
|
||||||
self.stdin = None
|
def wayland_display(self):
|
||||||
self.stdout = None
|
return self.__wayland_display
|
||||||
|
|
||||||
def stop(self):
|
@wayland_display.setter
|
||||||
if self.stdin:
|
def _set_wayland_display(self, wayland_display):
|
||||||
self.stdin.shutdown(False)
|
self.cleanup()
|
||||||
self.stdin = None
|
|
||||||
|
|
||||||
if self.stdout:
|
self.__wayland_display = wayland_display
|
||||||
self.stdout.shutdown(False)
|
|
||||||
self.stdout = None
|
|
||||||
|
|
||||||
if self.pid:
|
if wayland_display is None:
|
||||||
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
|
||||||
@ -87,27 +178,82 @@ class CmbProcess(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] + args,
|
[self.__file, self.gtk_version, self.__command_socket],
|
||||||
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 __on_exit(self, pid, status, data):
|
def __cleanup(self):
|
||||||
self.stop()
|
self.merengue_started = False
|
||||||
self.emit("exit")
|
|
||||||
|
|
||||||
def __on_stdout(self, channel, condition):
|
if self.__on_command_in_source:
|
||||||
return self.emit("stdout", condition)
|
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):
|
||||||
|
self.__cleanup()
|
||||||
|
self.__pid = 0
|
||||||
|
self.emit("exit")
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_view.ui")
|
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_view.ui")
|
||||||
@ -119,112 +265,106 @@ 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()
|
||||||
webview = Gtk.Template.Child()
|
compositor = 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.__restart_project = None
|
self.__ui = 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.__merengue_bin = os.path.join(config.merenguedir, "merengue", "merengue")
|
self.__click_gesture = Gtk.GestureClick(
|
||||||
self.__broadwayd_bin = GLib.find_program_in_path("broadwayd")
|
propagation_phase=Gtk.PropagationPhase.CAPTURE,
|
||||||
self.__gtk4_broadwayd_bin = GLib.find_program_in_path("gtk4-broadwayd")
|
button=3
|
||||||
|
)
|
||||||
|
self.__click_gesture.connect("pressed", self.__on_click_gesture_pressed)
|
||||||
|
self.compositor_box.add_controller(self.__click_gesture)
|
||||||
|
|
||||||
self.webview.connect("load-changed", self.__on_load_changed)
|
self.__merengue = CmbMerengueProcess(wayland_display=self.compositor.props.socket)
|
||||||
|
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)
|
||||||
|
|
||||||
def do_destroy(self):
|
# Ensure we delete all socket files when exiting
|
||||||
if self.__merengue:
|
atexit.register(self.__atexit)
|
||||||
self.__merengue_command("quit")
|
|
||||||
|
|
||||||
if self.__broadwayd:
|
@Gtk.Template.Callback("on_restart_button_clicked")
|
||||||
self.__broadwayd.stop()
|
def __on_restart_button_clicked(self, button):
|
||||||
|
self.restart_workspace()
|
||||||
|
|
||||||
def __evaluate_js(self, script):
|
def __atexit(self):
|
||||||
self.webview.evaluate_javascript(script, -1, None, None, None, None, None, None)
|
dirname = os.path.dirname(self.compositor.props.socket)
|
||||||
|
|
||||||
|
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):
|
||||||
self.__dark = dark
|
# This needs to be called in an idle because theme_bg_color has not changed at this point
|
||||||
self.__evaluate_js(f"document.body.style.background = '{'#222' if dark else 'inherit'}';")
|
GLib.idle_add(self.__set_dark_mode, dark)
|
||||||
|
|
||||||
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 is None or self.__merengue.stdin is None:
|
if self.__merengue.merengue_started:
|
||||||
return
|
self.__merengue.write_command(command, payload, args)
|
||||||
|
|
||||||
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 is not None and self.__ui_id > 0:
|
if self.__project and self.__ui:
|
||||||
if self.stack.props.visible_child_name == "ui_xml":
|
if self.stack.props.visible_child_name == "ui_xml":
|
||||||
ui = self.__get_ui_xml(self.__ui_id)
|
ui_source = self.__get_ui_xml(self.__ui.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_id = 0
|
self.__ui = None
|
||||||
|
|
||||||
def __get_ui_dirname(self, ui_id):
|
def __get_ui_dirname(self, ui_id):
|
||||||
dirname = GLib.get_home_dir()
|
dirname = GLib.get_home_dir()
|
||||||
@ -243,7 +383,7 @@ window.setupDocument = function (document) {
|
|||||||
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)
|
ui = self.__get_ui_xml(ui_id, merengue=True) if ui_id else None
|
||||||
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)
|
||||||
@ -277,7 +417,13 @@ window.setupDocument = function (document) {
|
|||||||
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):
|
||||||
if obj.info.workspace_type is None and prop.info.construct_only:
|
info = prop.info
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
@ -322,21 +468,27 @@ window.setupDocument = function (document) {
|
|||||||
if len(selection) > 0:
|
if len(selection) > 0:
|
||||||
obj = selection[0]
|
obj = selection[0]
|
||||||
|
|
||||||
if type(obj) not in [CmbUI, CmbObject]:
|
if isinstance(obj, CmbUI):
|
||||||
|
ui = obj
|
||||||
|
elif isinstance(obj, CmbObject):
|
||||||
|
ui = obj.ui
|
||||||
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
ui_id = obj.ui_id
|
ui_id = obj.ui_id
|
||||||
|
|
||||||
if self.__ui_id != ui_id:
|
if self.__ui != ui:
|
||||||
self.__ui_id = ui_id
|
self.__ui = ui
|
||||||
self.__merengue_update_ui(ui_id)
|
self.__merengue_update_ui(ui.ui_id)
|
||||||
|
|
||||||
objects = self.__get_selection_objects(selection, ui_id)
|
objects = self.__get_selection_objects(selection, ui.ui_id)
|
||||||
self.__merengue_command("selection_changed", args={"ui_id": ui_id, "selection": objects})
|
self.__merengue_command("selection_changed", args={"ui_id": ui_id, "selection": objects})
|
||||||
else:
|
else:
|
||||||
self.__ui_id = 0
|
self.__ui = None
|
||||||
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)
|
||||||
@ -379,13 +531,26 @@ window.setupDocument = function (document) {
|
|||||||
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 is not None:
|
if self.__project:
|
||||||
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)
|
||||||
@ -398,19 +563,20 @@ window.setupDocument = function (document) {
|
|||||||
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 is not None:
|
if project:
|
||||||
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)
|
||||||
@ -423,28 +589,25 @@ window.setupDocument = function (document) {
|
|||||||
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)
|
||||||
|
|
||||||
self.__merengue = CmbProcess(file=self.__merengue_bin)
|
# Run view process
|
||||||
self.__merengue.connect("stdout", self.__on_merengue_stdout)
|
if project.target_tk == "gtk+-3.0":
|
||||||
self.__merengue.connect("exit", self.__on_process_exit)
|
self.__merengue.gtk_version = "3.0"
|
||||||
|
elif project.target_tk == "gtk-4.0":
|
||||||
|
self.__merengue.gtk_version = "4.0"
|
||||||
|
|
||||||
self.__broadwayd_check(self.__project.target_tk)
|
# Clear any error
|
||||||
|
self.__set_error_message(None)
|
||||||
broadwayd = self.__gtk4_broadwayd_bin if self.__project.target_tk == "gtk-4.0" else self.__broadwayd_bin
|
self.__merengue.start()
|
||||||
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 = self.__project.target_tk
|
self.menu.target_tk = project.target_tk
|
||||||
|
|
||||||
@GObject.Property(type=str)
|
@GObject.Property(type=str)
|
||||||
def gtk_theme(self):
|
def gtk_theme(self):
|
||||||
@ -455,40 +618,24 @@ window.setupDocument = function (document) {
|
|||||||
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})
|
||||||
|
|
||||||
@Gtk.Template.Callback("on_context_menu")
|
def __on_click_gesture_pressed(self, gesture, n_press, x, y):
|
||||||
def __on_context_menu(self, webview, menu, hit_test_result):
|
if gesture.get_current_button() == 3:
|
||||||
self.menu.popup_at(*utils.get_pointer(self))
|
self.menu.popup_at(x, y)
|
||||||
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):
|
||||||
self.__restart_project = self.__project
|
# Clear last exit timestamp
|
||||||
self.project = None
|
self.__merengue_last_exit = 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)
|
||||||
@ -500,22 +647,17 @@ window.setupDocument = function (document) {
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
def __on_process_exit(self, process):
|
def __on_process_exit(self, process):
|
||||||
if process == self.__merengue:
|
if self.__merengue_last_exit is None:
|
||||||
if self.__merengue_last_exit is None:
|
self.__merengue_last_exit = time.monotonic()
|
||||||
self.__merengue_last_exit = time.monotonic()
|
|
||||||
else:
|
|
||||||
if (time.monotonic() - self.__merengue_last_exit) < 1:
|
|
||||||
self.__webview_set_msg(_("Workspace process error\nStopping auto restart"))
|
|
||||||
self.__merengue_last_exit = None
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.__broadwayd.pid == 0 and self.__merengue.pid == 0:
|
|
||||||
self.project = self.__restart_project
|
|
||||||
self.__restart_project = None
|
|
||||||
self.__ui_id = 0
|
|
||||||
else:
|
else:
|
||||||
self.__restart_project = self.__project
|
# Stop auto restart if Merengue exited less than 2 seconds ago
|
||||||
self.project = None
|
if (time.monotonic() - self.__merengue_last_exit) < 2:
|
||||||
|
self.__set_error_message(_("Workspace process error\nStopping auto restart"))
|
||||||
|
self.__merengue_last_exit = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__ui = None
|
||||||
|
self.__merengue.start()
|
||||||
|
|
||||||
def __command_selection_changed(self, selection):
|
def __command_selection_changed(self, selection):
|
||||||
objects = []
|
objects = []
|
||||||
@ -530,8 +672,11 @@ window.setupDocument = function (document) {
|
|||||||
if self.project is None:
|
if self.project is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for id in self.project.library_info:
|
for id, info in self.project.library_info.items():
|
||||||
info = self.project.library_info[id]
|
# Only load 3rd party libraries, Gtk ones are already loaded
|
||||||
|
if not info.third_party:
|
||||||
|
continue
|
||||||
|
|
||||||
self.__merengue_command(
|
self.__merengue_command(
|
||||||
"load_namespace",
|
"load_namespace",
|
||||||
args={
|
args={
|
||||||
@ -550,109 +695,50 @@ window.setupDocument = function (document) {
|
|||||||
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_stdout(self, process, condition):
|
def __on_merengue_handle_command(self, merengue, payload):
|
||||||
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(retval)
|
cmd = json.loads(payload)
|
||||||
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"Merenge output error: {e}")
|
logger.warning(f"Merengue command error: {e}")
|
||||||
self.__merengue.stop()
|
self.__merengue.stop()
|
||||||
return GLib.SOURCE_REMOVE
|
return
|
||||||
|
|
||||||
return GLib.SOURCE_CONTINUE
|
if command == "selection_changed":
|
||||||
|
self.__command_selection_changed(**args)
|
||||||
|
elif command == "started":
|
||||||
|
self.__merengue.merengue_started = True
|
||||||
|
self.__merengue_command("gtk_settings_get", args={"property": "gtk-theme-name"})
|
||||||
|
|
||||||
def __on_broadwayd_stdout(self, process, condition):
|
self.__load_namespaces()
|
||||||
if condition == GLib.IOCondition.HUP:
|
|
||||||
self.__broadwayd.stop()
|
|
||||||
return GLib.SOURCE_REMOVE
|
|
||||||
|
|
||||||
if self.__broadwayd.stdout is None:
|
self.__load_css_providers()
|
||||||
return GLib.SOURCE_REMOVE
|
|
||||||
|
|
||||||
status, retval, length, terminator = self.__broadwayd.stdout.read_line()
|
self.__ui = None
|
||||||
# path = retval.replace("Listening on ", "").strip()
|
self.__on_project_selection_changed(self.__project)
|
||||||
|
elif command == "placeholder_selected":
|
||||||
# Run view process
|
self.emit(
|
||||||
if self.__project.target_tk == "gtk+-3.0":
|
"placeholder-selected",
|
||||||
version = "3.0"
|
args["ui_id"],
|
||||||
elif self.__project.target_tk == "gtk-4.0":
|
args["object_id"],
|
||||||
version = "4.0"
|
args["layout"],
|
||||||
|
args["position"],
|
||||||
display = self.__port - 8080
|
args["child_type"],
|
||||||
|
)
|
||||||
env = json.loads(os.environ.get("MERENGUE_DEV_ENV", "{}"))
|
elif command == "placeholder_activated":
|
||||||
self.__merengue.run(
|
self.emit(
|
||||||
[version],
|
"placeholder-activated",
|
||||||
env
|
args["ui_id"],
|
||||||
| {
|
args["object_id"],
|
||||||
"GDK_BACKEND": "broadway",
|
args["layout"],
|
||||||
# 'GTK_DEBUG': 'interactive',
|
args["position"],
|
||||||
"BROADWAY_DISPLAY": f":{display}",
|
args["child_type"],
|
||||||
},
|
)
|
||||||
)
|
elif command == "gtk_settings_get":
|
||||||
|
if args["property"] == "gtk-theme-name":
|
||||||
# Load broadway desktop
|
self.__theme = args["value"]
|
||||||
self.webview.load_uri(f"http://127.0.0.1:{self.__port}")
|
self.notify("gtk_theme")
|
||||||
|
|
||||||
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:
|
||||||
@ -673,3 +759,4 @@ window.setupDocument = function (document) {
|
|||||||
|
|
||||||
|
|
||||||
Gtk.WidgetClass.set_css_name(CmbView, "CmbView")
|
Gtk.WidgetClass.set_css_name(CmbView, "CmbView")
|
||||||
|
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Created with Cambalache 0.17.3 -->
|
<!-- Created with Cambalache 0.95.0 -->
|
||||||
<interface>
|
<interface>
|
||||||
<!-- interface-name cmb_view.ui -->
|
<!-- interface-name cmb_view.ui -->
|
||||||
<requires lib="gtk" version="4.0"/>
|
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||||
<requires lib="webkitgtk" version="6.0"/>
|
<requires lib="gtk" version="4.14"/>
|
||||||
<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">
|
||||||
@ -22,11 +13,45 @@
|
|||||||
<child>
|
<child>
|
||||||
<object class="GtkStackPage">
|
<object class="GtkStackPage">
|
||||||
<property name="child">
|
<property name="child">
|
||||||
<object class="WebKitWebView" id="webview">
|
<object class="GtkBox" id="compositor_box">
|
||||||
<property name="can-focus">True</property>
|
<child>
|
||||||
<property name="settings">settings</property>
|
<object class="GtkGraphicsOffload" id="compositor_offload">
|
||||||
<property name="visible">True</property>
|
<property name="child">
|
||||||
<signal name="context-menu" handler="on_context_menu"/>
|
<object class="CasildaCompositor" id="compositor">
|
||||||
|
<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>
|
||||||
@ -48,7 +73,6 @@
|
|||||||
<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>
|
||||||
@ -67,6 +91,30 @@
|
|||||||
<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>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
VERSION = '@VERSION@'
|
VERSION = '@VERSION@'
|
||||||
FILE_FORMAT_VERSION = '@fileformatversion@'
|
FILE_FORMAT_VERSION = '@fileformatversion@'
|
||||||
pkgdatadir = '@pkgdatadir@'
|
pkgdatadir = '@pkgdatadir@'
|
||||||
catalogsdir = '@catalogsdir@'
|
|
||||||
merenguedir = '@merenguedir@'
|
merenguedir = '@merenguedir@'
|
||||||
|
catalogsdir = '@catalogsdir@'
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
# 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.
|
||||||
|
@ -19,9 +19,13 @@
|
|||||||
# 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
|
||||||
@ -32,6 +36,7 @@ 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
|
||||||
|
78
cambalache/control/cmb_boolean_undefined.py
Normal file
78
cambalache/control/cmb_boolean_undefined.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#
|
||||||
|
# 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
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
|
|
||||||
|
@ -20,6 +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 Gdk, GObject, Gtk, Pango
|
from gi.repository import Gdk, GObject, Gtk, Pango
|
||||||
|
|
||||||
|
@ -19,11 +19,14 @@
|
|||||||
# 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
|
||||||
@ -33,11 +36,12 @@ 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):
|
def cmb_create_editor(project, type_id, prop=None, data=None, parent=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)
|
||||||
@ -114,6 +118,8 @@ def cmb_create_editor(project, type_id, prop=None, data=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":
|
||||||
@ -128,11 +134,31 @@ def cmb_create_editor(project, type_id, prop=None, data=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":
|
||||||
# TODO: replace prop with project and is_inline
|
if prop is None:
|
||||||
editor = CmbObjectChooser(parent=prop.object, prop=prop)
|
editor = CmbObjectChooser(
|
||||||
if info.parent_id == "enum":
|
project=project,
|
||||||
|
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)
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
from .cmb_translatable_popover import CmbTranslatablePopover
|
from .cmb_translatable_popover import CmbTranslatablePopover
|
||||||
@ -52,4 +54,8 @@ 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 ""
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
from ..cmb_type_info import CmbTypeInfo
|
from ..cmb_type_info import CmbTypeInfo
|
||||||
@ -28,7 +30,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 | GObject.ParamFlags.CONSTRUCT_ONLY)
|
info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE)
|
||||||
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):
|
||||||
@ -40,7 +42,9 @@ 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")
|
||||||
@ -51,12 +55,13 @@ 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)
|
||||||
|
|
||||||
for row in self.info.enum:
|
if active_id == self.props.active_id:
|
||||||
enum_name = row[0]
|
return
|
||||||
enum_nick = row[1]
|
|
||||||
|
|
||||||
# Always use nick as value
|
self.props.active_id = active_id
|
||||||
if value == enum_name or value == enum_nick:
|
|
||||||
self.props.active_id = enum_nick
|
|
||||||
|
114
cambalache/control/cmb_file_button.py
Normal file
114
cambalache/control/cmb_file_button.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#
|
||||||
|
# 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
|
34
cambalache/control/cmb_file_button.ui
Normal file
34
cambalache/control/cmb_file_button.ui
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?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>
|
@ -20,11 +20,13 @@
|
|||||||
# 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, Gtk
|
from gi.repository import GObject, Gio, Gtk
|
||||||
|
|
||||||
|
|
||||||
class CmbFileEntry(Gtk.Entry):
|
class CmbFileEntry(Gtk.Entry):
|
||||||
@ -35,7 +37,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"
|
||||||
@ -43,19 +45,24 @@ 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, event):
|
def __on_icon_pressed(self, widget, icon_pos):
|
||||||
# Create Open Dialog
|
dialog = Gtk.FileDialog(
|
||||||
dialog = Gtk.FileChooserNative(
|
modal=True,
|
||||||
title=self.title, transient_for=self.get_toplevel(), action=Gtk.FileChooserAction.OPEN, filter=self.filter
|
title=self.title,
|
||||||
|
default_filter=self.filter,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.dirname is not None:
|
if self.dirname is not None:
|
||||||
dialog.set_current_folder(self.dirname)
|
dialog.set_initial_folder(Gio.File.new_for_path(self.dirname))
|
||||||
|
|
||||||
if dialog.run() == Gtk.ResponseType.ACCEPT:
|
def dialog_callback(dialog, res):
|
||||||
self.props.text = os.path.relpath(dialog.get_filename(), start=self.dirname)
|
try:
|
||||||
|
file = dialog.open_finish(res)
|
||||||
|
self.props.text = os.path.relpath(file.get_path(), start=self.dirname)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
dialog.destroy()
|
dialog.open(self.get_root(), None, dialog_callback)
|
||||||
|
|
||||||
def __on_text_notify(self, obj, pspec):
|
def __on_text_notify(self, obj, pspec):
|
||||||
self.notify("cmb-value")
|
self.notify("cmb-value")
|
||||||
@ -66,4 +73,6 @@ 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 ""
|
||||||
|
@ -20,6 +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 GObject, Gtk
|
from gi.repository import GObject, Gtk
|
||||||
from ..cmb_type_info import CmbTypeInfo
|
from ..cmb_type_info import CmbTypeInfo
|
||||||
@ -32,6 +34,7 @@ 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 = {}
|
||||||
@ -82,7 +85,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} | {flag_id}"
|
retval = flag_id if retval is None else f"{retval} {self.separator} {flag_id}"
|
||||||
|
|
||||||
return retval if retval is not None else ""
|
return retval if retval is not None else ""
|
||||||
|
|
||||||
@ -92,6 +95,9 @@ 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 = {}
|
||||||
@ -99,7 +105,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("|")]
|
tokens = [t.strip() for t in value.split(self.separator)]
|
||||||
|
|
||||||
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
Loading…
x
Reference in New Issue
Block a user